Compare commits
65 Commits
v25.10.0
...
cursor/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9af9dc84e9 | ||
|
|
57710006e0 | ||
|
|
94332016e8 | ||
|
|
0af2c6c2fb | ||
|
|
97482a082d | ||
|
|
31a9ba629b | ||
|
|
7c19a6333c | ||
|
|
2bce9d707c | ||
|
|
4e42cda29e | ||
|
|
0ce07d7692 | ||
|
|
0e3415c145 | ||
|
|
19675a7de6 | ||
|
|
5a1ceed7d9 | ||
|
|
86c1c30c97 | ||
|
|
96ac1292f9 | ||
|
|
328dfae8bf | ||
|
|
37247395e2 | ||
|
|
02b0e24d6e | ||
|
|
dfaa75f1cf | ||
|
|
adae3e4352 | ||
|
|
7a3794295f | ||
|
|
c1d97fcc75 | ||
|
|
3715f16888 | ||
|
|
edad7ce0e3 | ||
|
|
737341ffb6 | ||
|
|
6147495003 | ||
|
|
614bedcfbf | ||
|
|
248b1034d7 | ||
|
|
4f88afa266 | ||
|
|
5c23aad3c2 | ||
|
|
5b9bcc94f6 | ||
|
|
87d54251cd | ||
|
|
2e439aacba | ||
|
|
244140314c | ||
|
|
f7b40fca64 | ||
|
|
dc811552be | ||
|
|
295839ebbb | ||
|
|
99ca34458e | ||
|
|
90ac8d8520 | ||
|
|
52aeec2d59 | ||
|
|
0c280d60f6 | ||
|
|
148ca92584 | ||
|
|
90e848ebe8 | ||
|
|
b034d5039f | ||
|
|
5ac29473f2 | ||
|
|
3b0db2bed7 | ||
|
|
7a886810bc | ||
|
|
8bf0997275 | ||
|
|
2f965266ab | ||
|
|
499f24f7fd | ||
|
|
4c5be62f56 | ||
|
|
1446c7d93f | ||
|
|
ad9980307e | ||
|
|
d4ad31fb0c | ||
|
|
05355788e4 | ||
|
|
805e2b1807 | ||
|
|
e54dc0c1ca | ||
|
|
e1c2f0a181 | ||
|
|
cc2e329e8e | ||
|
|
71f849d1e1 | ||
|
|
0ea8bc1fb4 | ||
|
|
f0c7953c0b | ||
|
|
4cf5f9b183 | ||
|
|
80fd997540 | ||
|
|
da93ddf63b |
@@ -1,12 +0,0 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
When running yarn commands - always run them in the root directory. Do not run them in child workspaces.
|
||||
|
||||
The following commands can be useful:
|
||||
|
||||
- `yarn typecheck` to run typechecker
|
||||
- `yarn lint` to run the code linter and formatter
|
||||
- `yarn lint:fix` to fix some of the code lint issues (running this is preferred over `yarn lint`)
|
||||
- `yarn test` to run all the tests
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs: *.ts,*.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
You are an expert in TypeScript and React.
|
||||
|
||||
Code Style and Structure
|
||||
|
||||
- Write concise, technical TypeScript code.
|
||||
- Use functional and declarative programming patterns; avoid classes.
|
||||
- Prefer iteration and modularization over code duplication.
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., isLoaded, hasError).
|
||||
- Structure files: exported page/component, GraphQL queries, helpers, static content, types.
|
||||
- When creating a new component, place it in its own file rather than grouping multiple components in a single file.
|
||||
|
||||
Naming Conventions
|
||||
|
||||
- Favor named exports for components and utilities.
|
||||
|
||||
TypeScript Usage
|
||||
|
||||
- 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`.
|
||||
|
||||
Syntax and Formatting
|
||||
|
||||
- Use the "function" keyword for pure functions.
|
||||
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
|
||||
- Use declarative JSX, keeping JSX minimal and readable.
|
||||
|
||||
Change validation
|
||||
|
||||
- Run `yarn typecheck` in the root directory to validate that the generated TypeScript code is correct
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
Vitest test runner is used for unit tests.
|
||||
|
||||
When running unit tests, always include the flag `--watch=false` to prevent watch mode.
|
||||
|
||||
To run unit tests for a specific package in the monorepo, use the following command:
|
||||
|
||||
`yarn workspace <workspaceNameFromPackageJson> run test <pathToTest>`
|
||||
|
||||
Recommendation: Minimize the number of dependencies you mock. The fewer dependencies you mock, the better.
|
||||
18
.github/workflows/build.yml
vendored
@@ -12,6 +12,8 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
repository_dispatch:
|
||||
types: [vrt-update-applied]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -22,6 +24,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build API
|
||||
@@ -38,6 +44,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build CRDT
|
||||
@@ -54,6 +64,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
@@ -73,6 +87,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Server
|
||||
|
||||
24
.github/workflows/check.yml
vendored
@@ -5,6 +5,8 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
repository_dispatch:
|
||||
types: [vrt-update-applied]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -15,6 +17,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Lint
|
||||
@@ -23,6 +29,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Typecheck
|
||||
@@ -31,6 +41,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
@@ -41,16 +55,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
migrations:
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'repository_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
15
.github/workflows/docker-edge.yml
vendored
@@ -1,21 +1,16 @@
|
||||
name: Build Edge Docker Image
|
||||
|
||||
# Edge Docker images are built for every commit, and daily
|
||||
# Edge Docker images are built for every push to master
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/sync-server/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/sync-server/**'
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: docker-edge-build
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
6
.github/workflows/e2e-test.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
name: Functional Desktop App
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
|
||||
7
.github/workflows/electron-master.yml
vendored
@@ -44,9 +44,9 @@ jobs:
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Sdk//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform//24.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron for Mac
|
||||
@@ -57,6 +57,7 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
- name: Build Electron
|
||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
|
||||
6
.github/workflows/electron-pr.yml
vendored
@@ -39,9 +39,9 @@ jobs:
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Sdk//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform//24.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
|
||||
2
.github/workflows/size-compare.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: base
|
||||
|
||||
@@ -62,6 +63,7 @@ jobs:
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
|
||||
119
.github/workflows/update-vrt.yml
vendored
@@ -1,119 +0,0 @@
|
||||
name: /update-vrt
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ contains(github.event.comment.body, '/update-vrt') }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-vrt:
|
||||
name: Update VRT
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run VRT Tests on Desktop app
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./.github/actions/netlify-wait-for-build
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
run: yarn vrt --update-snapshots
|
||||
env:
|
||||
E2E_START_URL: ${{ steps.netlify.outputs.url }}
|
||||
- name: Create patch
|
||||
run: |
|
||||
git config --system --add safe.directory "*"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git reset
|
||||
git add "**/*.png"
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update VRT"
|
||||
git format-patch -1 HEAD --stdout > Update-VRT.patch
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: patch
|
||||
path: Update-VRT.patch
|
||||
|
||||
push-patch:
|
||||
runs-on: ubuntu-latest
|
||||
needs: update-vrt
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: patch
|
||||
- name: Apply patch and push
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git apply Update-VRT.patch
|
||||
git add "**/*.png"
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update VRT"
|
||||
git push origin HEAD:${BRANCH_NAME}
|
||||
- name: Add finished reaction
|
||||
uses: dkershner6/reaction-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: 'rocket'
|
||||
|
||||
add-starting-reaction:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: React to comment
|
||||
uses: dkershner6/reaction-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: '+1'
|
||||
180
.github/workflows/vrt-update-apply.yml
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
name: VRT Update - Apply
|
||||
# SECURITY: This workflow runs in trusted base repo context.
|
||||
# It treats the patch artifact as untrusted data, validates it contains only PNGs,
|
||||
# and safely applies it to the contributor's fork branch.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['VRT Update - Generate']
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
apply-vrt-updates:
|
||||
name: Apply VRT Updates
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download patch artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: vrt-patch-*
|
||||
path: /tmp/artifacts
|
||||
|
||||
- name: Download metadata artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: vrt-metadata-*
|
||||
path: /tmp/metadata
|
||||
|
||||
- name: Extract metadata
|
||||
id: metadata
|
||||
run: |
|
||||
# Find the metadata directory (will be vrt-metadata-{PR_NUMBER})
|
||||
METADATA_DIR=$(find /tmp/metadata -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
|
||||
if [ -z "$METADATA_DIR" ]; then
|
||||
echo "No metadata found, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PR_NUMBER=$(cat "$METADATA_DIR/pr-number.txt")
|
||||
HEAD_REF=$(cat "$METADATA_DIR/head-ref.txt")
|
||||
HEAD_REPO=$(cat "$METADATA_DIR/head-repo.txt")
|
||||
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT"
|
||||
echo "head_repo=$HEAD_REPO" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Found PR #$PR_NUMBER: $HEAD_REPO @ $HEAD_REF"
|
||||
|
||||
- name: Checkout fork branch
|
||||
if: steps.metadata.outputs.pr_number != ''
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.metadata.outputs.head_repo }}
|
||||
ref: ${{ steps.metadata.outputs.head_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate and apply patch
|
||||
if: steps.metadata.outputs.pr_number != ''
|
||||
id: apply
|
||||
run: |
|
||||
# Find the patch file
|
||||
PATCH_DIR=$(find /tmp/artifacts -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
PATCH_FILE="$PATCH_DIR/vrt-update.patch"
|
||||
|
||||
if [ ! -f "$PATCH_FILE" ]; then
|
||||
echo "No patch file found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found patch file: $PATCH_FILE"
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
echo "Validating patch contains only PNG files..."
|
||||
if grep -E '^(\+\+\+|---) [ab]/' "$PATCH_FILE" | grep -v '\.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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract file list for verification
|
||||
FILES_CHANGED=$(grep -E '^\+\+\+ b/' "$PATCH_FILE" | sed 's/^+++ b\///' | wc -l)
|
||||
echo "Patch modifies $FILES_CHANGED PNG file(s)"
|
||||
|
||||
# Configure git
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Apply patch
|
||||
echo "Applying patch..."
|
||||
if git apply --check "$PATCH_FILE" 2>&1; then
|
||||
git apply "$PATCH_FILE"
|
||||
|
||||
# Stage only PNG files (extra safety)
|
||||
git add "**/*.png"
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes after applying patch"
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Commit
|
||||
git commit -m "Update VRT screenshots" -m "Auto-generated by VRT workflow" -m "PR: #${{ steps.metadata.outputs.pr_number }}"
|
||||
|
||||
echo "applied=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Patch could not be applied cleanly"
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
echo "error=Patch conflicts with current branch state" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
env:
|
||||
HEAD_REF: ${{ steps.metadata.outputs.head_ref }}
|
||||
HEAD_REPO: ${{ steps.metadata.outputs.head_repo }}
|
||||
run: |
|
||||
git push origin "HEAD:refs/heads/$HEAD_REF"
|
||||
echo "Successfully pushed VRT updates to $HEAD_REPO@$HEAD_REF"
|
||||
|
||||
- name: Trigger CI workflows
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// Dispatch a custom event to trigger CI workflows
|
||||
// This will cause the CI workflows to run in the PR context
|
||||
try {
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
event_type: 'vrt-update-applied',
|
||||
client_payload: {
|
||||
pr_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
head_ref: '${{ steps.metadata.outputs.head_ref }}',
|
||||
head_repo: '${{ steps.metadata.outputs.head_repo }}'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Successfully triggered CI workflows via repository_dispatch');
|
||||
} catch (error) {
|
||||
console.log(`Failed to trigger CI workflows: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Comment on PR - Success
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '✅ VRT screenshots have been automatically updated and CI workflows have been triggered.'
|
||||
});
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
if: failure() && steps.metadata.outputs.pr_number != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const error = `${{ steps.apply.outputs.error }}` || 'Unknown error occurred';
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `❌ Failed to apply VRT updates: ${error}\n\nPlease check the workflow logs for details.`
|
||||
});
|
||||
105
.github/workflows/vrt-update-generate.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
name: VRT Update - Generate
|
||||
# SECURITY: This workflow runs in untrusted fork context with no write permissions.
|
||||
# It only generates VRT patch artifacts that are later applied by vrt-update-apply.yml
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- '.github/workflows/vrt-update-generate.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
generate-vrt-updates:
|
||||
name: Generate VRT Updates
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Run VRT Tests on Desktop app
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
|
||||
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./.github/actions/netlify-wait-for-build
|
||||
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
continue-on-error: true
|
||||
run: yarn vrt --update-snapshots
|
||||
env:
|
||||
E2E_START_URL: ${{ steps.netlify.outputs.url }}
|
||||
|
||||
- name: Create patch with PNG changes only
|
||||
id: create-patch
|
||||
run: |
|
||||
# Trust the repository directory (required for container environments)
|
||||
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"
|
||||
|
||||
# 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"
|
||||
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
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
if grep -E '^(\+\+\+|---) [ab]/' vrt-update.patch | grep -v '\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Patch created successfully with PNG changes only"
|
||||
|
||||
- name: Upload patch artifact
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vrt-patch-${{ github.event.pull_request.number }}
|
||||
path: vrt-update.patch
|
||||
retention-days: 5
|
||||
|
||||
- name: Save PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
run: |
|
||||
mkdir -p pr-metadata
|
||||
echo "${{ github.event.pull_request.number }}" > pr-metadata/pr-number.txt
|
||||
echo "${{ github.event.pull_request.head.ref }}" > pr-metadata/head-ref.txt
|
||||
echo "${{ github.event.pull_request.head.repo.full_name }}" > pr-metadata/head-repo.txt
|
||||
|
||||
- name: Upload PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vrt-metadata-${{ github.event.pull_request.number }}
|
||||
path: pr-metadata/
|
||||
retention-days: 5
|
||||
5
.gitignore
vendored
@@ -7,9 +7,6 @@ Actual-*
|
||||
**/xcuserdata/*
|
||||
export-2020-01-10.csv
|
||||
|
||||
# Secrets
|
||||
.secret-tokens
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
@@ -26,6 +23,8 @@ packages/desktop-electron/build
|
||||
packages/desktop-electron/.electron-symbols
|
||||
packages/desktop-electron/dist
|
||||
packages/desktop-electron/loot-core
|
||||
packages/desktop-client/service-worker
|
||||
packages/plugins-service/dist
|
||||
bundle.desktop.js
|
||||
bundle.desktop.js.map
|
||||
bundle.mobile.js
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export APPLE_ID=example@email.com
|
||||
export APPLE_APP_SPECIFIC_PASSWORD=password
|
||||
942
.yarn/releases/yarn-4.10.3.cjs
vendored
Executable file
948
.yarn/releases/yarn-4.9.1.cjs
vendored
@@ -6,4 +6,4 @@ enableTransparentWorkspaces: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||
|
||||
538
AGENTS.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# AGENTS.md - Guide for AI Agents Working with Actual Budget
|
||||
|
||||
This guide provides comprehensive information for AI agents (like Cursor) working with the Actual Budget codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Actual Budget** is a local-first personal finance tool written in TypeScript/JavaScript. It's 100% free and open-source with synchronization capabilities across devices.
|
||||
|
||||
- **Repository**: https://github.com/actualbudget/actual
|
||||
- **Community Docs**: https://github.com/actualbudget/docs or https://actualbudget.org/docs
|
||||
- **License**: MIT
|
||||
- **Primary Language**: TypeScript (with React)
|
||||
- **Build System**: Yarn 4 workspaces (monorepo)
|
||||
|
||||
## Quick Start Commands
|
||||
|
||||
### Essential Commands (Run from Root)
|
||||
|
||||
```bash
|
||||
# Type checking (ALWAYS run before committing)
|
||||
yarn typecheck
|
||||
|
||||
# Linting and formatting (with auto-fix)
|
||||
yarn lint:fix
|
||||
|
||||
# Run all tests
|
||||
yarn test
|
||||
|
||||
# Start development server (browser)
|
||||
yarn start
|
||||
|
||||
# Start with sync server
|
||||
yarn start:server-dev
|
||||
|
||||
# Start desktop app development
|
||||
yarn start:desktop
|
||||
```
|
||||
|
||||
### Important Rules
|
||||
|
||||
- **ALWAYS run yarn commands from the root directory** - never run them in child workspaces
|
||||
- Use `yarn workspace <workspace-name> run <command>` for workspace-specific tasks
|
||||
- Include `--watch=false` flag when running unit tests to prevent watch mode
|
||||
|
||||
## Architecture & Package Structure
|
||||
|
||||
### Core Packages
|
||||
|
||||
#### 1. **loot-core** (`packages/loot-core/`)
|
||||
|
||||
The core application logic that runs on any platform.
|
||||
|
||||
- Business logic, database operations, and calculations
|
||||
- Platform-agnostic code
|
||||
- Exports for both browser and node environments
|
||||
- Test commands:
|
||||
```bash
|
||||
yarn workspace loot-core run test --watch=false
|
||||
```
|
||||
|
||||
#### 2. **desktop-client** (`packages/desktop-client/` - aliased as `@actual-app/web`)
|
||||
|
||||
The React-based UI for web and desktop.
|
||||
|
||||
- React components using functional programming patterns
|
||||
- E2E tests using Playwright
|
||||
- Vite for bundling
|
||||
- Commands:
|
||||
|
||||
```bash
|
||||
# Development
|
||||
yarn workspace @actual-app/web start:browser
|
||||
|
||||
# Build
|
||||
yarn workspace @actual-app/web build
|
||||
|
||||
# E2E tests
|
||||
yarn workspace @actual-app/web e2e
|
||||
|
||||
# Visual regression tests
|
||||
yarn workspace @actual-app/web vrt
|
||||
```
|
||||
|
||||
#### 3. **desktop-electron** (`packages/desktop-electron/`)
|
||||
|
||||
Electron wrapper for the desktop application.
|
||||
|
||||
- Window management and native OS integration
|
||||
- E2E tests for Electron-specific features
|
||||
|
||||
#### 4. **api** (`packages/api/` - aliased as `@actual-app/api`)
|
||||
|
||||
Public API for programmatic access to Actual.
|
||||
|
||||
- Node.js API
|
||||
- Designed for integrations and automation
|
||||
- Commands:
|
||||
```bash
|
||||
yarn workspace @actual-app/api build
|
||||
yarn workspace @actual-app/api test --watch=false
|
||||
```
|
||||
|
||||
#### 5. **sync-server** (`packages/sync-server/` - aliased as `@actual-app/sync-server`)
|
||||
|
||||
Synchronization server for multi-device support.
|
||||
|
||||
- Express-based server
|
||||
- Currently transitioning to TypeScript (mostly JavaScript)
|
||||
- Commands:
|
||||
```bash
|
||||
yarn workspace @actual-app/sync-server start
|
||||
```
|
||||
|
||||
#### 6. **component-library** (`packages/component-library/` - aliased as `@actual-app/components`)
|
||||
|
||||
Reusable React UI components.
|
||||
|
||||
- Shared components like Button, Input, Menu, etc.
|
||||
- Theme system and design tokens
|
||||
- Icons (375+ icons in SVG/TSX format)
|
||||
|
||||
#### 7. **crdt** (`packages/crdt/` - aliased as `@actual-app/crdt`)
|
||||
|
||||
CRDT (Conflict-free Replicated Data Type) implementation for data synchronization.
|
||||
|
||||
- Protocol buffers for serialization
|
||||
- Core sync logic
|
||||
|
||||
#### 8. **plugins-service** (`packages/plugins-service/`)
|
||||
|
||||
Service for handling plugins/extensions.
|
||||
|
||||
#### 9. **eslint-plugin-actual** (`packages/eslint-plugin-actual/`)
|
||||
|
||||
Custom ESLint rules specific to Actual.
|
||||
|
||||
- `no-untranslated-strings`: Enforces i18n usage
|
||||
- `prefer-trans-over-t`: Prefers Trans component over t() function
|
||||
- `prefer-logger-over-console`: Enforces using logger instead of console
|
||||
- `typography`: Typography rules
|
||||
- `prefer-if-statement`: Prefers explicit if statements
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Making Changes
|
||||
|
||||
When implementing changes:
|
||||
|
||||
1. Read relevant files to understand current implementation
|
||||
2. Make focused, incremental changes
|
||||
3. Run type checking: `yarn typecheck`
|
||||
4. Run linting: `yarn lint:fix`
|
||||
5. Run relevant tests
|
||||
6. Fix any linter errors that are introduced
|
||||
|
||||
### 2. Testing Strategy
|
||||
|
||||
**Unit Tests (Vitest)**
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
yarn test
|
||||
|
||||
# Specific package
|
||||
yarn workspace loot-core run test --watch=false
|
||||
|
||||
# Specific test file
|
||||
yarn workspace loot-core run test path/to/test.test.ts --watch=false
|
||||
```
|
||||
|
||||
**E2E Tests (Playwright)**
|
||||
|
||||
```bash
|
||||
# Desktop client E2E
|
||||
yarn workspace @actual-app/web e2e
|
||||
|
||||
# Desktop Electron E2E
|
||||
yarn e2e:desktop
|
||||
|
||||
# Visual regression tests
|
||||
yarn vrt
|
||||
|
||||
# Visual regression in Docker (consistent environment)
|
||||
yarn vrt:docker
|
||||
```
|
||||
|
||||
**Testing Best Practices:**
|
||||
|
||||
- Minimize mocked dependencies - prefer real implementations
|
||||
- Use descriptive test names
|
||||
- Vitest globals are available: `describe`, `it`, `expect`, `beforeEach`, etc.
|
||||
- For sync-server tests, globals are explicitly defined in config
|
||||
|
||||
### 3. Type Checking
|
||||
|
||||
TypeScript configuration uses:
|
||||
|
||||
- Incremental compilation
|
||||
- Strict type checking with `typescript-strict-plugin`
|
||||
- Platform-specific exports in `loot-core` (node vs browser)
|
||||
|
||||
Always run `yarn typecheck` before committing.
|
||||
|
||||
### 4. Internationalization (i18n)
|
||||
|
||||
- Use `Trans` component instead of `t()` function when possible
|
||||
- All user-facing strings must be translated
|
||||
- Generate i18n files: `yarn generate:i18n`
|
||||
- Custom ESLint rules enforce translation usage
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
### TypeScript Guidelines
|
||||
|
||||
**Type Usage:**
|
||||
|
||||
- Use TypeScript for all code
|
||||
- Prefer `type` over `interface`
|
||||
- Avoid `enum` - use objects or maps
|
||||
- Avoid `any` or `unknown` unless absolutely necessary
|
||||
- Look for existing type definitions in the codebase
|
||||
- Avoid type assertions (`as`, `!`) - prefer `satisfies`
|
||||
- Use inline type imports: `import { type MyType } from '...'`
|
||||
|
||||
**Naming:**
|
||||
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., `isLoaded`, `hasError`)
|
||||
- Named exports for components and utilities (avoid default exports except in specific cases)
|
||||
|
||||
**Code Structure:**
|
||||
|
||||
- Functional and declarative programming patterns - avoid classes
|
||||
- Use the `function` keyword for pure functions
|
||||
- Prefer iteration and modularization over code duplication
|
||||
- Structure files: exported component/page, helpers, static content, types
|
||||
- Create new components in their own files
|
||||
|
||||
**React Patterns:**
|
||||
|
||||
- Don't use `React.FunctionComponent` or `React.FC` - type props directly
|
||||
- Don't use `React.*` patterns - use named imports instead
|
||||
- Use `<Link>` instead of `<a>` tags
|
||||
- Use custom hooks from `src/hooks` (not react-router directly):
|
||||
- `useNavigate()` from `src/hooks` (not react-router)
|
||||
- `useDispatch()`, `useSelector()`, `useStore()` from `src/redux` (not react-redux)
|
||||
- Avoid unstable nested components
|
||||
- Use `satisfies` for type narrowing
|
||||
|
||||
**JSX Style:**
|
||||
|
||||
- Declarative JSX, minimal and readable
|
||||
- Avoid unnecessary curly braces in conditionals
|
||||
- Use concise syntax for simple statements
|
||||
- Prefer explicit expressions (`condition && <Component />`)
|
||||
|
||||
### Import Organization
|
||||
|
||||
Imports are automatically organized by ESLint with the following order:
|
||||
|
||||
1. React imports (first)
|
||||
2. Built-in Node.js modules
|
||||
3. External packages
|
||||
4. Actual packages (`loot-core`, `@actual-app/components` - legacy pattern `loot-design` may appear in old code)
|
||||
5. Parent imports
|
||||
6. Sibling imports
|
||||
7. Index imports
|
||||
|
||||
Always maintain newlines between import groups.
|
||||
|
||||
### Platform-Specific Code
|
||||
|
||||
- Don't directly reference platform-specific imports (`.api`, `.web`, `.electron`)
|
||||
- Use conditional exports in `loot-core` for platform-specific code
|
||||
- Platform resolution happens at build time via package.json exports
|
||||
|
||||
### Restricted Patterns
|
||||
|
||||
**Never:**
|
||||
|
||||
- Use `console.*` (use logger instead - enforced by ESLint)
|
||||
- Import from `uuid` without destructuring: use `import { v4 as uuidv4 } from 'uuid'`
|
||||
- Import colors directly - use theme instead
|
||||
- Import `@actual-app/web/*` in `loot-core`
|
||||
|
||||
**Git Commands:**
|
||||
|
||||
- Never update git config
|
||||
- Never run destructive git operations (force push, hard reset) unless explicitly requested
|
||||
- Never skip hooks (--no-verify, --no-gpg-sign)
|
||||
- Never force push to main/master
|
||||
- Never commit unless explicitly asked
|
||||
|
||||
## File Structure Patterns
|
||||
|
||||
### Typical Component File
|
||||
|
||||
```typescript
|
||||
import { type ComponentType } from 'react';
|
||||
// ... other imports
|
||||
|
||||
type MyComponentProps = {
|
||||
// Props definition
|
||||
};
|
||||
|
||||
export function MyComponent({ prop1, prop2 }: MyComponentProps) {
|
||||
// Component logic
|
||||
return (
|
||||
// JSX
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Test File
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
// ... imports
|
||||
|
||||
describe('ComponentName', () => {
|
||||
it('should behave as expected', () => {
|
||||
// Test logic
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Important Directories & Files
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- `/package.json` - Root workspace configuration, scripts
|
||||
- `/eslint.config.mjs` - ESLint configuration (flat config format)
|
||||
- `/tsconfig.json` - Root TypeScript configuration
|
||||
- `/.cursorignore`, `/.gitignore` - Ignored files
|
||||
- `/yarn.lock` - Dependency lockfile (Yarn 4)
|
||||
|
||||
### Documentation
|
||||
|
||||
- `/README.md` - Project overview
|
||||
- `/CONTRIBUTING.md` - Points to community docs
|
||||
- `/upcoming-release-notes/` - Release notes for next version
|
||||
- `/CODEOWNERS` - Code ownership definitions
|
||||
|
||||
### Build Artifacts (Don't Edit)
|
||||
|
||||
- `packages/*/lib-dist/` - Built output
|
||||
- `packages/*/dist/` - Built output
|
||||
- `packages/*/build/` - Built output
|
||||
- `packages/desktop-client/playwright-report/` - Test reports
|
||||
- `packages/desktop-client/test-results/` - Test results
|
||||
|
||||
### Key Source Directories
|
||||
|
||||
- `packages/loot-core/src/client/` - Client-side core logic
|
||||
- `packages/loot-core/src/server/` - Server-side core logic
|
||||
- `packages/loot-core/src/shared/` - Shared utilities
|
||||
- `packages/loot-core/src/types/` - Type definitions
|
||||
- `packages/desktop-client/src/components/` - React components
|
||||
- `packages/desktop-client/src/hooks/` - Custom React hooks
|
||||
- `packages/desktop-client/e2e/` - End-to-end tests
|
||||
- `packages/component-library/src/` - Reusable components
|
||||
- `packages/component-library/src/icons/` - Icon components (auto-generated, don't edit)
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Running Specific Tests
|
||||
|
||||
```bash
|
||||
# Unit test for a specific file in loot-core
|
||||
yarn workspace loot-core run test src/path/to/file.test.ts --watch=false
|
||||
|
||||
# E2E test for a specific file
|
||||
yarn workspace @actual-app/web run playwright test accounts.test.ts --browser=chromium
|
||||
```
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
# Browser build
|
||||
yarn build:browser
|
||||
|
||||
# Desktop build
|
||||
yarn build:desktop
|
||||
|
||||
# API build
|
||||
yarn build:api
|
||||
|
||||
# Sync server build
|
||||
yarn build:server
|
||||
```
|
||||
|
||||
### Type Checking Specific Packages
|
||||
|
||||
TypeScript uses project references. Run `yarn typecheck` from root to check all packages.
|
||||
|
||||
### Debugging Tests
|
||||
|
||||
```bash
|
||||
# Run tests in debug mode (without parallelization)
|
||||
yarn test:debug
|
||||
|
||||
# Run specific E2E test with headed browser
|
||||
yarn workspace @actual-app/web run playwright test --headed --debug accounts.test.ts
|
||||
```
|
||||
|
||||
### Working with Icons
|
||||
|
||||
Icons in `packages/component-library/src/icons/` are auto-generated. Don't manually edit them.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Type Errors
|
||||
|
||||
1. Run `yarn typecheck` to see all type errors
|
||||
2. Check if types are imported correctly
|
||||
3. Look for existing type definitions in `packages/loot-core/src/types/`
|
||||
4. Use `satisfies` instead of `as` for type narrowing
|
||||
|
||||
### Linter Errors
|
||||
|
||||
1. Run `yarn lint:fix` to auto-fix many issues
|
||||
2. Check ESLint output for specific rule violations
|
||||
3. Custom rules:
|
||||
- `actual/no-untranslated-strings` - Add i18n
|
||||
- `actual/prefer-trans-over-t` - Use Trans component
|
||||
- `actual/prefer-logger-over-console` - Use logger
|
||||
- Check `eslint.config.mjs` for complete rules
|
||||
|
||||
### Test Failures
|
||||
|
||||
1. Check if test is running in correct environment (node vs web)
|
||||
2. For Vitest: check `vitest.config.ts` or `vitest.web.config.ts`
|
||||
3. For Playwright: check `playwright.config.ts`
|
||||
4. Ensure mock minimization - prefer real implementations
|
||||
|
||||
### Import Resolution Issues
|
||||
|
||||
1. Check `tsconfig.json` for path mappings
|
||||
2. Check package.json `exports` field (especially for loot-core)
|
||||
3. Verify platform-specific imports (`.web`, `.electron`, `.api`)
|
||||
4. Use absolute imports in `desktop-client` (enforced by ESLint)
|
||||
|
||||
### Build Failures
|
||||
|
||||
1. Clean build artifacts: `rm -rf packages/*/dist packages/*/lib-dist packages/*/build`
|
||||
2. Reinstall dependencies: `yarn install`
|
||||
3. Check Node.js version (requires >=20)
|
||||
4. Check Yarn version (requires ^4.9.1)
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Located alongside source files or in `__tests__` directories
|
||||
- Use `.test.ts`, `.test.tsx`, `.spec.js` extensions
|
||||
- Vitest is the test runner
|
||||
- Minimize mocking - prefer real implementations
|
||||
|
||||
### E2E Tests
|
||||
|
||||
- Located in `packages/desktop-client/e2e/`
|
||||
- Use Playwright test runner
|
||||
- Visual regression snapshots in `*-snapshots/` directories
|
||||
- Page models in `e2e/page-models/` for reusable page interactions
|
||||
- Mobile tests have `.mobile.test.ts` suffix
|
||||
|
||||
### Visual Regression Tests (VRT)
|
||||
|
||||
- Run with `VRT=true` environment variable
|
||||
- Snapshots stored per test file
|
||||
- Use Docker for consistent environment: `yarn vrt:docker`
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Community Documentation**: https://actualbudget.org/docs/contributing/
|
||||
- **Discord Community**: https://discord.gg/pRYNYr4W5A
|
||||
- **GitHub Issues**: https://github.com/actualbudget/actual/issues
|
||||
- **Feature Requests**: Label "needs votes" sorted by reactions
|
||||
|
||||
## Code Quality Checklist
|
||||
|
||||
Before committing changes, ensure:
|
||||
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] No new console.\* usage (use logger)
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Prefer `type` over `interface`
|
||||
- [ ] Named exports used (not default exports)
|
||||
- [ ] Imports are properly ordered
|
||||
- [ ] Platform-specific code uses proper exports
|
||||
- [ ] No unnecessary type assertions
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
When creating pull requests:
|
||||
|
||||
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Bundle Size**: Check with rollup-plugin-visualizer
|
||||
- **Type Checking**: Uses incremental compilation
|
||||
- **Testing**: Tests run in parallel by default
|
||||
- **Linting**: ESLint caches results for faster subsequent runs
|
||||
|
||||
## Workspace Commands Reference
|
||||
|
||||
```bash
|
||||
# List all workspaces
|
||||
yarn workspaces list
|
||||
|
||||
# Run command in specific workspace
|
||||
yarn workspace <workspace-name> run <command>
|
||||
|
||||
# Run command in all workspaces
|
||||
yarn workspaces foreach --all run <command>
|
||||
|
||||
# Install production dependencies only (for server deployment)
|
||||
yarn install:server
|
||||
```
|
||||
|
||||
## Environment Requirements
|
||||
|
||||
- **Node.js**: >=20
|
||||
- **Yarn**: ^4.9.1 (managed by packageManager field)
|
||||
- **Browser Targets**: Electron >= 35.0, modern browsers (see browserslist)
|
||||
|
||||
## Migration Notes
|
||||
|
||||
The codebase is actively being migrated:
|
||||
|
||||
- **JavaScript → TypeScript**: sync-server is in progress
|
||||
- **Classes → Functions**: Prefer functional patterns
|
||||
- **React.\* → Named Imports**: Legacy React.\* patterns being removed
|
||||
|
||||
When working with older code, follow the newer patterns described in this guide.
|
||||
@@ -16,6 +16,7 @@ packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace loot-core build:node
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
@@ -61,14 +62,11 @@ yarn workspace desktop-electron update-client
|
||||
echo "Skipping exe build"
|
||||
else
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build
|
||||
|
||||
echo "Created release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build
|
||||
yarn build
|
||||
fi
|
||||
fi
|
||||
)
|
||||
|
||||
@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
|
||||
echo "E2E_START_URL: $E2E_START_URL"
|
||||
echo "VRT_ARGS: $VRT_ARGS"
|
||||
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.52.0-jammy /bin/bash \
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash \
|
||||
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"
|
||||
|
||||
@@ -83,6 +83,7 @@ export default pluginTypescript.config(
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'packages/desktop-client/bundle.browser.js',
|
||||
'packages/desktop-client/build/',
|
||||
'packages/desktop-client/service-worker/*',
|
||||
'packages/desktop-client/build-electron/',
|
||||
'packages/desktop-client/build-stats/',
|
||||
'packages/desktop-client/public/kcab/',
|
||||
@@ -98,6 +99,7 @@ export default pluginTypescript.config(
|
||||
'packages/loot-core/**/lib-dist/*',
|
||||
'packages/loot-core/**/proto/*',
|
||||
'packages/sync-server/build/',
|
||||
'packages/plugins-service/dist/',
|
||||
'.yarn/*',
|
||||
'.github/*',
|
||||
],
|
||||
|
||||
41
package.json
@@ -23,17 +23,20 @@
|
||||
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
|
||||
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
||||
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
||||
"desktop-dependencies": "yarn rebuild-electron && yarn workspace loot-core build:browser",
|
||||
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:browser-backend build:plugins-service",
|
||||
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
|
||||
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
||||
"start:browser": "npm-run-all --parallel 'start:browser-*'",
|
||||
"start:browser": "yarn workspace plugins-service build-dev && npm-run-all --parallel 'start:browser-*'",
|
||||
"start:service-plugins": "yarn workspace plugins-service watch",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"build:browser-backend": "yarn workspace loot-core build:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:plugins-service": "yarn workspace plugins-service build",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
||||
@@ -44,7 +47,7 @@
|
||||
"playwright": "yarn workspace @actual-app/web run playwright",
|
||||
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
"lint": "prettier --check . && eslint . --max-warnings 0",
|
||||
"lint:fix": "prettier --check --write . && eslint . --max-warnings 0 --fix",
|
||||
@@ -55,32 +58,32 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/node": "^22.17.0",
|
||||
"@types/node": "^22.18.8",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.3.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^6.0.0-rc.2",
|
||||
"eslint-plugin-react-hooks": "^6.1.1",
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"globals": "^15.15.0",
|
||||
"globals": "^16.4.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.2",
|
||||
"lint-staged": "^16.2.3",
|
||||
"minimatch": "^10.0.3",
|
||||
"node-jq": "^6.0.1",
|
||||
"node-jq": "^6.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"p-limit": "^6.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"p-limit": "^7.1.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prompts": "^2.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
@@ -97,7 +100,7 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"browserslist": [
|
||||
"electron >= 35.0",
|
||||
"defaults"
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export function amountToInteger(n) {
|
||||
return Math.round(n * 100);
|
||||
}
|
||||
|
||||
export function integerToAmount(n) {
|
||||
return parseFloat((n / 100).toFixed(2));
|
||||
}
|
||||
6
packages/api/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-ignore: bundle not available until we build it
|
||||
// eslint-disable-next-line import/extensions
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
|
||||
export const amountToInteger = bundle.lib.amountToInteger;
|
||||
export const integerToAmount = bundle.lib.integerToAmount;
|
||||
@@ -8,14 +8,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
"react-aria-components": "^1.8.0",
|
||||
"react-aria-components": "^1.13.0",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.1.12",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.21.4",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"protoc-gen-js": "^3.21.4-4",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
3
packages/desktop-client/.gitignore
vendored
@@ -14,6 +14,9 @@ build-electron
|
||||
build-stats
|
||||
stats.json
|
||||
|
||||
# generated service worker
|
||||
service-worker/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
@@ -65,10 +65,10 @@ Run manually:
|
||||
|
||||
```sh
|
||||
# Run docker container
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.52.0-jammy /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
|
||||
|
||||
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.52.0-jammy /bin/bash
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
|
||||
|
||||
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
|
||||
# Use the ip and port noted earlier
|
||||
|
||||
@@ -9,6 +9,7 @@ rm -fr build
|
||||
|
||||
export IS_GENERIC_BROWSER=1
|
||||
export REACT_APP_BACKEND_WORKER_HASH=`ls "$ROOT"/../public/kcab/kcab.worker.*.js | sed 's/.*kcab\.worker\.\(.*\)\.js/\1/'`
|
||||
export REACT_APP_PLUGIN_SERVICE_WORKER_HASH=`ls "$ROOT"/../service-worker/plugin-sw.*.js | sed 's/.*plugin-sw\.\(.*\)\.js/\1/'`
|
||||
|
||||
yarn build
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ cd "$ROOT/.."
|
||||
export IS_GENERIC_BROWSER=1
|
||||
export PORT=3001
|
||||
export REACT_APP_BACKEND_WORKER_HASH="dev"
|
||||
export REACT_APP_PLUGIN_SERVICE_WORKER_HASH="dev"
|
||||
|
||||
yarn start
|
||||
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 78 KiB |
@@ -3,6 +3,7 @@ import { type Locator, type Page } from '@playwright/test';
|
||||
import { MobileAccountPage } from './mobile-account-page';
|
||||
import { MobileAccountsPage } from './mobile-accounts-page';
|
||||
import { MobileBudgetPage } from './mobile-budget-page';
|
||||
import { MobilePayeesPage } from './mobile-payees-page';
|
||||
import { MobileReportsPage } from './mobile-reports-page';
|
||||
import { MobileRulesPage } from './mobile-rules-page';
|
||||
import { MobileTransactionEntryPage } from './mobile-transaction-entry-page';
|
||||
@@ -21,6 +22,7 @@ const ROUTES_BY_PAGE = {
|
||||
Accounts: '/accounts',
|
||||
Transaction: '/transactions/new',
|
||||
Reports: '/reports',
|
||||
Payees: '/payees',
|
||||
Rules: '/rules',
|
||||
Settings: '/settings',
|
||||
};
|
||||
@@ -166,6 +168,13 @@ export class MobileNavigation {
|
||||
);
|
||||
}
|
||||
|
||||
async goToPayeesPage() {
|
||||
return await this.navigateToPage(
|
||||
'Payees',
|
||||
() => new MobilePayeesPage(this.page),
|
||||
);
|
||||
}
|
||||
|
||||
async goToRulesPage() {
|
||||
return await this.navigateToPage(
|
||||
'Rules',
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
|
||||
export class MobilePayeesPage {
|
||||
readonly page: Page;
|
||||
readonly searchBox: Locator;
|
||||
readonly payeesList: Locator;
|
||||
readonly emptyMessage: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.searchBox = page.getByPlaceholder('Filter payees…');
|
||||
this.payeesList = page.getByRole('grid', { name: 'Payees' });
|
||||
this.emptyMessage = page.getByText('No payees found.');
|
||||
this.loadingIndicator = page.getByTestId('animated-loading');
|
||||
}
|
||||
|
||||
async waitFor(options?: {
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||
timeout?: number;
|
||||
}) {
|
||||
await this.payeesList.waitFor(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for payees using the search box
|
||||
*/
|
||||
async searchFor(text: string) {
|
||||
await this.searchBox.fill(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search box
|
||||
*/
|
||||
async clearSearch() {
|
||||
await this.searchBox.fill('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nth payee item (0-based index)
|
||||
*/
|
||||
getNthPayee(index: number) {
|
||||
return this.getAllPayees().nth(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible payee items
|
||||
*/
|
||||
getAllPayees() {
|
||||
return this.payeesList.getByRole('gridcell');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a payee to view/edit rules
|
||||
*/
|
||||
async clickPayee(index: number) {
|
||||
const payee = this.getNthPayee(index);
|
||||
await payee.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of visible payees
|
||||
*/
|
||||
async getPayeeCount() {
|
||||
const payees = this.getAllPayees();
|
||||
return await payees.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for loading to complete
|
||||
*/
|
||||
async waitForLoadingToComplete(timeout: number = 10000) {
|
||||
await this.loadingIndicator.waitFor({ state: 'hidden', timeout });
|
||||
}
|
||||
}
|
||||
@@ -40,17 +40,14 @@ export class MobileRulesPage {
|
||||
* Get the nth rule item (0-based index)
|
||||
*/
|
||||
getNthRule(index: number) {
|
||||
return this.page
|
||||
.getByRole('button')
|
||||
.filter({ hasText: /IF|THEN/ })
|
||||
.nth(index);
|
||||
return this.getAllRules().nth(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible rule items
|
||||
*/
|
||||
getAllRules() {
|
||||
return this.page.getByRole('button').filter({ hasText: /IF|THEN/ });
|
||||
return this.page.getByRole('grid', { name: 'Rules' }).getByRole('row');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,28 +73,6 @@ export class MobileRulesPage {
|
||||
return await rules.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the search bar has a border
|
||||
*/
|
||||
async hasSearchBarBorder() {
|
||||
const searchContainer = this.searchBox.locator('..');
|
||||
const borderStyle = await searchContainer.evaluate(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.borderBottomWidth;
|
||||
});
|
||||
return borderStyle === '2px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the background color of the search box
|
||||
*/
|
||||
async getSearchBackgroundColor() {
|
||||
return await this.searchBox.evaluate(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.backgroundColor;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a rule contains specific text
|
||||
*/
|
||||
@@ -112,7 +87,7 @@ export class MobileRulesPage {
|
||||
*/
|
||||
async getRuleStage(index: number) {
|
||||
const rule = this.getNthRule(index);
|
||||
const stageBadge = rule.locator('span').first();
|
||||
const stageBadge = rule.getByTestId('rule-stage-badge');
|
||||
return await stageBadge.textContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,15 @@ export class RulesPage {
|
||||
|
||||
if (op && !fieldFirst) {
|
||||
await row.getByTestId('op-select').getByRole('button').first().click();
|
||||
await this.page.getByRole('button', { name: op, exact: true }).click();
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (field) {
|
||||
@@ -133,12 +141,26 @@ export class RulesPage {
|
||||
.click();
|
||||
await this.page
|
||||
.getByRole('button', { name: field, exact: true })
|
||||
.click();
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: field, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (op && fieldFirst) {
|
||||
await row.getByTestId('op-select').getByRole('button').first().click();
|
||||
await this.page.getByRole('button', { name: op, exact: true }).click();
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (value) {
|
||||
|
||||
@@ -28,7 +28,10 @@ export class SchedulesPage {
|
||||
|
||||
await this._fillScheduleFields(data);
|
||||
|
||||
await this.page.getByRole('button', { name: 'Add' }).click();
|
||||
await this.page
|
||||
.getByTestId('schedule-edit-modal')
|
||||
.getByRole('button', { name: 'Add' })
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
112
packages/desktop-client/e2e/payees.mobile.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
import { type MobilePayeesPage } from './page-models/mobile-payees-page';
|
||||
|
||||
test.describe('Mobile Payees', () => {
|
||||
let page: Page;
|
||||
let navigation: MobileNavigation;
|
||||
let payeesPage: MobilePayeesPage;
|
||||
let configurationPage: ConfigurationPage;
|
||||
|
||||
test.beforeEach(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
navigation = new MobileNavigation(page);
|
||||
configurationPage = new ConfigurationPage(page);
|
||||
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({
|
||||
width: 350,
|
||||
height: 600,
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await configurationPage.createTestFile();
|
||||
|
||||
// Navigate to payees page and wait for it to load
|
||||
payeesPage = await navigation.goToPayeesPage();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('checks the page visuals', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
// Check that the header is present
|
||||
await expect(page.getByRole('heading', { name: 'Payees' })).toBeVisible();
|
||||
|
||||
// Check that the search box is present with proper placeholder
|
||||
await expect(payeesPage.searchBox).toBeVisible();
|
||||
await expect(payeesPage.searchBox).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Filter payees…',
|
||||
);
|
||||
|
||||
const payeeCount = await payeesPage.getPayeeCount();
|
||||
expect(payeeCount).toBeGreaterThan(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('filters out unrelated payees', async () => {
|
||||
await payeesPage.searchFor('asdfasdf-nonsense');
|
||||
|
||||
// Get the text 'No payees found.' from the page
|
||||
const noPayeesMessage = page.getByText('No payees found.');
|
||||
|
||||
// Assert it is visible
|
||||
await expect(noPayeesMessage).toBeVisible();
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('clicking on a payee opens rule creation form', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
const payeeCount = await payeesPage.getPayeeCount();
|
||||
expect(payeeCount).toBeGreaterThan(0);
|
||||
|
||||
await payeesPage.clickPayee(0);
|
||||
|
||||
// Should navigate to rules page for creating a new rule
|
||||
await expect(page).toHaveURL(/\/rules/);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('page handles empty state gracefully', async () => {
|
||||
// Search for something that won't match to get empty state
|
||||
await payeesPage.searchFor('NonExistentPayee123456789');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check that empty message is shown
|
||||
const emptyMessage = page.getByText('No payees found.');
|
||||
await expect(emptyMessage).toBeVisible();
|
||||
|
||||
// Check that no payee items are visible
|
||||
const payees = payeesPage.getAllPayees();
|
||||
await expect(payees).toHaveCount(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('search functionality works correctly', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
// Test searching for a specific payee
|
||||
await payeesPage.searchFor('Fast Internet');
|
||||
|
||||
// Should show at least one result
|
||||
const payeeCount = await payeesPage.getPayeeCount();
|
||||
expect(payeeCount).toBeGreaterThan(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
// Clear search
|
||||
await payeesPage.clearSearch();
|
||||
|
||||
// Should show all payees again
|
||||
const allPayeeCount = await payeesPage.getPayeeCount();
|
||||
expect(allPayeeCount).toBeGreaterThan(payeeCount);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -33,7 +33,17 @@ test.describe.parallel('Reports', () => {
|
||||
test('loads net worth and cash flow reports', async () => {
|
||||
const reports = await reportsPage.getAvailableReportList();
|
||||
|
||||
expect(reports).toEqual(['Net Worth', 'Cash Flow', 'Monthly Spending']);
|
||||
expect(reports).toEqual([
|
||||
'Total Income (YTD)',
|
||||
'Total Expenses (YTD)',
|
||||
'Avg Per Month',
|
||||
'Avg Per Transaction',
|
||||
'Net Worth',
|
||||
'Cash Flow',
|
||||
'This Month',
|
||||
'Budget Overview',
|
||||
'3-Month Average',
|
||||
]);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 32 KiB |
@@ -155,6 +155,8 @@ test.describe('Transactions', () => {
|
||||
await expect(transaction.category.locator('input')).toHaveValue('Transfer');
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
const balanceBeforeTransaction =
|
||||
await accountPage.accountBalance.textContent();
|
||||
await accountPage.addEnteredTransaction();
|
||||
|
||||
transaction = accountPage.getNthTransaction(0);
|
||||
@@ -163,6 +165,14 @@ test.describe('Transactions', () => {
|
||||
await expect(transaction.category).toHaveText('Transfer');
|
||||
await expect(transaction.debit).toHaveText('12.34');
|
||||
await expect(transaction.credit).toHaveText('');
|
||||
|
||||
// Wait for balance to update after adding transaction
|
||||
await expect(async () => {
|
||||
const balanceAfterTransaction =
|
||||
await accountPage.accountBalance.textContent();
|
||||
expect(balanceAfterTransaction).not.toBe(balanceBeforeTransaction);
|
||||
}).toPass();
|
||||
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 93 KiB |
@@ -7,7 +7,6 @@
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Actual</title>
|
||||
<link rel="canonical" href="/" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
@@ -108,10 +107,6 @@
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.js-focus-visible :focus:not(.focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -8,38 +8,36 @@
|
||||
"devDependencies": {
|
||||
"@actual-app/components": "workspace:*",
|
||||
"@emotion/css": "^11.13.5",
|
||||
"@fontsource/redacted-script": "^5.2.5",
|
||||
"@fontsource/redacted-script": "^5.2.8",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@playwright/test": "1.56.0",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"@swc/core": "^1.11.24",
|
||||
"@swc/core": "^1.13.5",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/lodash": "^4",
|
||||
"@types/promise-retry": "^1.1.6",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@types/react-grid-layout": "^1",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"auto-text-size": "^0.2.3",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"chokidar": "^3.6.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"downshift": "7.6.2",
|
||||
"focus-visible": "^4.1.5",
|
||||
"i18next": "^25.2.1",
|
||||
"downshift": "9.0.10",
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-parser": "^9.3.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"inter-ui": "^3.19.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loot-core": "workspace:*",
|
||||
"mdast-util-newline-to-break": "^2.0.0",
|
||||
@@ -48,35 +46,34 @@
|
||||
"promise-retry": "^2.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "19.1.1",
|
||||
"react-aria": "^3.39.0",
|
||||
"react-aria-components": "^1.8.0",
|
||||
"react": "19.2.0",
|
||||
"react-aria": "^3.44.0",
|
||||
"react-aria-components": "^1.13.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-grid-layout": "^1.5.1",
|
||||
"react-hotkeys-hook": "^4.6.2",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-dom": "19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-grid-layout": "^1.5.2",
|
||||
"react-hotkeys-hook": "^5.1.0",
|
||||
"react-i18next": "^16.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-modal": "3.16.3",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "7.6.2",
|
||||
"react-router": "7.9.4",
|
||||
"react-simple-pull-to-refresh": "^1.3.3",
|
||||
"react-spring": "^10.0.0",
|
||||
"react-stately": "^3.37.0",
|
||||
"react-spring": "10.0.0",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"recharts": "^2.15.3",
|
||||
"recharts": "^3.2.1",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.89.0",
|
||||
"rollup-plugin-visualizer": "^6.0.4",
|
||||
"sass": "^1.93.2",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^7.1.9",
|
||||
"vite-plugin-pwa": "^1.0.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
|
||||
@@ -178,7 +178,6 @@ global.Actual = {
|
||||
// Wait for the app to reload
|
||||
await new Promise(() => {});
|
||||
},
|
||||
updateAppMenu: () => {},
|
||||
|
||||
ipcConnect: () => {},
|
||||
getServerSocket: async () => {
|
||||
@@ -191,35 +190,3 @@ global.Actual = {
|
||||
|
||||
moveBudgetDirectory: () => {},
|
||||
};
|
||||
|
||||
function inputFocused(e) {
|
||||
return (
|
||||
e.target.tagName === 'INPUT' ||
|
||||
e.target.tagName === 'TEXTAREA' ||
|
||||
e.target.isContentEditable
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
// Cmd/Ctrl+o
|
||||
if (e.key === 'o') {
|
||||
e.preventDefault();
|
||||
window.__actionsForMenu.closeBudget();
|
||||
}
|
||||
// Cmd/Ctrl+z
|
||||
else if (e.key.toLowerCase() === 'z') {
|
||||
if (inputFocused(e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
// Redo
|
||||
window.__actionsForMenu.redo();
|
||||
} else {
|
||||
// Undo
|
||||
window.__actionsForMenu.undo();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -212,10 +212,28 @@ export const moveCategoryGroup = createAppAsyncThunk(
|
||||
},
|
||||
);
|
||||
|
||||
function translateCategories(
|
||||
categories: CategoryEntity[] | undefined,
|
||||
): CategoryEntity[] | undefined {
|
||||
return categories?.map(cat => ({
|
||||
...cat,
|
||||
name:
|
||||
cat.name?.toLowerCase() === 'starting balances'
|
||||
? t('Starting Balances')
|
||||
: cat.name,
|
||||
}));
|
||||
}
|
||||
|
||||
export const getCategories = createAppAsyncThunk(
|
||||
`${sliceName}/getCategories`,
|
||||
async () => {
|
||||
const categories: CategoryViews = await send('get-categories');
|
||||
categories.list = translateCategories(categories.list) as CategoryEntity[];
|
||||
categories.grouped.forEach(group => {
|
||||
group.categories = translateCategories(
|
||||
group.categories,
|
||||
) as CategoryEntity[];
|
||||
});
|
||||
return categories;
|
||||
},
|
||||
{
|
||||
@@ -233,6 +251,12 @@ export const reloadCategories = createAppAsyncThunk(
|
||||
`${sliceName}/reloadCategories`,
|
||||
async () => {
|
||||
const categories: CategoryViews = await send('get-categories');
|
||||
categories.list = translateCategories(categories.list) as CategoryEntity[];
|
||||
categories.grouped.forEach(group => {
|
||||
group.categories = translateCategories(
|
||||
group.categories,
|
||||
) as CategoryEntity[];
|
||||
});
|
||||
return categories;
|
||||
},
|
||||
);
|
||||
@@ -556,6 +580,7 @@ export const getCategoriesById = memoizeOne(
|
||||
res[cat.id] = cat;
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
);
|
||||
@@ -584,6 +609,12 @@ function _loadCategories(
|
||||
categories: BudgetState['categories'],
|
||||
) {
|
||||
state.categories = categories;
|
||||
categories.list = translateCategories(categories.list) as CategoryEntity[];
|
||||
categories.grouped.forEach(group => {
|
||||
group.categories = translateCategories(
|
||||
group.categories,
|
||||
) as CategoryEntity[];
|
||||
});
|
||||
state.isCategoriesLoading = false;
|
||||
state.isCategoriesLoaded = true;
|
||||
state.isCategoriesDirty = false;
|
||||
|
||||
@@ -498,7 +498,7 @@ function sortFiles(arr: File[]) {
|
||||
let i = name1 < name2 ? -1 : name1 > name2 ? 1 : 0;
|
||||
if (i === 0) {
|
||||
const xId = x.state === 'remote' ? x.cloudFileId : x.id;
|
||||
const yId = x.state === 'remote' ? x.cloudFileId : x.id;
|
||||
const yId = y.state === 'remote' ? y.cloudFileId : y.id;
|
||||
i = xId < yId ? -1 : xId > yId ? 1 : 0;
|
||||
}
|
||||
return i;
|
||||
|
||||
@@ -135,10 +135,6 @@ function AppInner() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, showErrorBoundary]);
|
||||
|
||||
useEffect(() => {
|
||||
global.Actual.updateAppMenu(budgetId);
|
||||
}, [budgetId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userData?.tokenExpired) {
|
||||
dispatch(
|
||||
@@ -215,7 +211,7 @@ export function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ExposeNavigate />
|
||||
<HotkeysProvider initiallyActiveScopes={['*']}>
|
||||
<HotkeysProvider initiallyActiveScopes={['app']}>
|
||||
<SpreadsheetProvider>
|
||||
<SidebarProvider>
|
||||
<BudgetMonthCountProvider>
|
||||
|
||||
@@ -22,7 +22,6 @@ export function AppBackground({ isLoading }: AppBackgroundProps) {
|
||||
from: { opacity: 0, transform: 'translateY(-100px)' },
|
||||
enter: { opacity: 1, transform: 'translateY(0)' },
|
||||
leave: { opacity: 0, transform: 'translateY(100px)' },
|
||||
unique: true,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,6 @@ export function BankSyncStatus() {
|
||||
from: { opacity: 0, transform: 'translateY(-100px)' },
|
||||
enter: { opacity: 1, transform: 'translateY(0)' },
|
||||
leave: { opacity: 0, transform: 'translateY(-100px)' },
|
||||
unique: true,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export const HelpMenu = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useHotkeys('shift+?', () => setMenuOpen(true));
|
||||
useHotkeys('?', () => setMenuOpen(true), { useKey: true });
|
||||
|
||||
return (
|
||||
<SpaceBetween>
|
||||
|
||||
@@ -107,6 +107,8 @@ export function ruleToString(rule: RuleEntity, data: FilterData) {
|
||||
} else if (action.op === 'prepend-notes' || action.op === 'append-notes') {
|
||||
const noteValue = String(action.value || '');
|
||||
return [friendlyOp(action.op), '\u201c' + noteValue + '\u201d'];
|
||||
} else if (action.op === 'delete-transaction') {
|
||||
return [friendlyOp(action.op), '(delete)'];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -14,75 +14,15 @@ import remarkGfm from 'remark-gfm';
|
||||
import {
|
||||
remarkBreaks,
|
||||
sequentialNewlinesPlugin,
|
||||
markdownBaseStyles,
|
||||
} from '@desktop-client/util/markdown';
|
||||
|
||||
const remarkPlugins = [sequentialNewlinesPlugin, remarkGfm, remarkBreaks];
|
||||
|
||||
const markdownStyles = css({
|
||||
const markdownStyles = css(markdownBaseStyles, {
|
||||
display: 'block',
|
||||
maxWidth: 350,
|
||||
padding: 8,
|
||||
overflowWrap: 'break-word',
|
||||
'& p': {
|
||||
margin: 0,
|
||||
':not(:first-child)': {
|
||||
marginTop: '0.25rem',
|
||||
},
|
||||
},
|
||||
'& ul, & ol': {
|
||||
listStylePosition: 'inside',
|
||||
margin: 0,
|
||||
paddingLeft: 0,
|
||||
},
|
||||
'&>* ul, &>* ol': {
|
||||
marginLeft: '1.5rem',
|
||||
},
|
||||
'& li>p': {
|
||||
display: 'contents',
|
||||
},
|
||||
'& blockquote': {
|
||||
paddingLeft: '0.75rem',
|
||||
borderLeft: '3px solid ' + theme.markdownDark,
|
||||
margin: 0,
|
||||
},
|
||||
'& hr': {
|
||||
borderTop: 'none',
|
||||
borderLeft: 'none',
|
||||
borderRight: 'none',
|
||||
borderBottom: '1px solid ' + theme.markdownNormal,
|
||||
},
|
||||
'& code': {
|
||||
backgroundColor: theme.markdownLight,
|
||||
padding: '0.1rem 0.5rem',
|
||||
borderRadius: '0.25rem',
|
||||
},
|
||||
'& pre': {
|
||||
padding: '0.5rem',
|
||||
backgroundColor: theme.markdownLight,
|
||||
borderRadius: '0.5rem',
|
||||
margin: 0,
|
||||
':not(:first-child)': {
|
||||
marginTop: '0.25rem',
|
||||
},
|
||||
'& code': {
|
||||
background: 'inherit',
|
||||
padding: 0,
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
'& table, & th, & td': {
|
||||
border: '1px solid ' + theme.markdownNormal,
|
||||
},
|
||||
'& table': {
|
||||
borderCollapse: 'collapse',
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
'& td': {
|
||||
padding: '0.25rem 0.75rem',
|
||||
},
|
||||
'& h3': {
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
|
||||
type NotesProps = {
|
||||
|
||||
@@ -35,12 +35,14 @@ export function ThemeSelector({ style }: ThemeSelectorProps) {
|
||||
development: SvgMoonStars,
|
||||
} as const;
|
||||
|
||||
type ThemeIconKey = keyof typeof themeIcons;
|
||||
|
||||
function onMenuSelect(newTheme: Theme) {
|
||||
setMenuOpen(false);
|
||||
switchTheme(newTheme);
|
||||
}
|
||||
|
||||
const Icon = themeIcons[theme] || SvgSun;
|
||||
const Icon = themeIcons[theme as ThemeIconKey] || SvgSun;
|
||||
|
||||
if (isNarrowWidth) {
|
||||
return null;
|
||||
|
||||
@@ -20,10 +20,7 @@ import { View } from '@actual-app/components/view';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { listen } from 'loot-core/platform/client/fetch';
|
||||
import {
|
||||
isDevelopmentEnvironment,
|
||||
isElectron,
|
||||
} from 'loot-core/shared/environment';
|
||||
import { isDevelopmentEnvironment } from 'loot-core/shared/environment';
|
||||
import * as Platform from 'loot-core/shared/platform';
|
||||
|
||||
import { AccountSyncCheck } from './accounts/AccountSyncCheck';
|
||||
@@ -352,7 +349,7 @@ export function Titlebar({ style }: TitlebarProps) {
|
||||
<PrivacyButton />
|
||||
{serverURL ? <SyncButton /> : null}
|
||||
<LoggedInUser />
|
||||
{!isElectron() && <HelpMenu />}
|
||||
<HelpMenu />
|
||||
</SpaceBetween>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1048,7 +1048,8 @@ class AccountInternal extends PureComponent<
|
||||
|
||||
// sync the reconciliation transaction
|
||||
await send('transactions-batch-update', {
|
||||
added: ruledTransactions,
|
||||
added: ruledTransactions.filter(trans => !trans.tombstone),
|
||||
deleted: ruledTransactions.filter(trans => trans.tombstone),
|
||||
});
|
||||
await this.refetchTransactions();
|
||||
};
|
||||
|
||||