Merge branch 'master' into fix-issue-1253

This commit is contained in:
Matiss Janis Aboltins
2025-12-21 17:14:08 +00:00
committed by GitHub
1039 changed files with 10815 additions and 7582 deletions

5
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,5 @@
issue_enrichment:
auto_enrich:
enabled: false
reviews:
review_status: false

2
.gitattributes vendored
View File

@@ -12,6 +12,8 @@
*.sh text eol=lf
*.tsx text eol=lf
**/bin/* text eol=lf
yarn.lock text eol=lf
# Denote all files that are truly binary and should not be modified.

View File

@@ -26,7 +26,7 @@ body:
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen? If youre reporting an issue with imports, please attach a (redacted) version of the file youre having trouble importing. You may need to zip it before uploading.
description: Also tell us, what did you expect to happen? If you're reporting an issue with imports, please attach a (redacted) version of the file you're having trouble importing. You may need to zip it before uploading.
placeholder: Tell us what you see!
value: 'A bug happened!'
validations:

View File

@@ -7,19 +7,19 @@ body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! Please ensure you provide as much information as possible so we can better understand what youre proposing so we can come up with the best solution for everyone.
Thanks for taking the time to fill out this feature request! Please ensure you provide as much information as possible so we can better understand what you're proposing so we can come up with the best solution for everyone.
- type: checkboxes
id: existing-issue
attributes:
label: 'Verified feature request does not already exist?'
description: 'Please search to see if an issue or PR already exists for the feature youre requesting.'
description: "Please search to see if an issue or PR already exists for the feature you're requesting."
options:
- label: 'I have searched and found no existing issue'
required: true
- type: checkboxes
attributes:
label: '💻'
description: (Optional) Please check this box if youre willing to open a PR to implement this feature. Well help you get started and answer any questions you have along the way :)
description: (Optional) Please check this box if you're willing to open a PR to implement this feature. We'll help you get started and answer any questions you have along the way :)
options:
- label: Would you like to implement this feature?
- type: textarea
@@ -33,7 +33,7 @@ body:
id: solution
attributes:
label: Describe your ideal solution to this problem
description: Feel free to give multiple different ideas for how the problem could be solved — wed love to have a discussion to find the best way to solve your problem and related problems others may face! (Or leave this blank if you dont have a solution in mind yet.)
description: Feel free to give multiple different ideas for how the problem could be solved — we'd love to have a discussion to find the best way to solve your problem and related problems others may face! (Or leave this blank if you don't have a solution in mind yet.)
validations:
required: false
- type: textarea

View File

@@ -6,7 +6,7 @@ import fs from 'fs';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const commentId = process.env.GITHUB_EVENT_COMMENT_ID;
const commentId = String(process.env.GITHUB_EVENT_COMMENT_ID);
if (!token || !repo || !issueNumber || !commentId) {
console.log('Missing required environment variables');
@@ -51,7 +51,7 @@ async function checkFirstComment() {
const isFirstSummaryComment =
coderabbitSummaryComments.length === 1 &&
coderabbitSummaryComments[0].id == commentId;
String(coderabbitSummaryComments[0].id) === commentId;
console.log(
`CodeRabbit summary comments found: ${coderabbitSummaryComments.length}`,

View File

@@ -4,7 +4,6 @@
// 1. Identify the migrations in packages/loot-core/migrations/* on `master` and HEAD
// 2. Make sure that any new migrations on HEAD are dated after the latest migration on `master`.
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

View File

@@ -7,6 +7,7 @@ Andelskassen
AQL
Authelia
autocompletes
Belarusian
Blix
bnp
BSCHESMM
@@ -34,6 +35,7 @@ debian
dedupes
deleteaccount
DKB
DKK
dmg
easybank
Edenred
@@ -50,9 +52,11 @@ Fortuneo
gebabebb
GEBABEBB
Greenshot
GTQ
HSA
htpasswd
IBANs
IDR
iex
importtransactions
ING
@@ -82,6 +86,7 @@ minimalistic
monkeypatch
Monobank
Morrisons
MYR
NAIAGB
NDEADKKK
Netflix
@@ -96,6 +101,8 @@ offbudget
ofx
OFX
oneof
oxfmt
oxlint
payeerule
pikaday
pikapods
@@ -108,6 +115,7 @@ QFX
QIF
Quicken
returnsandreimbursements
responsitivity
Rezip
roadmap
RUpdate

View File

@@ -72,5 +72,6 @@ ignore$
(?:^|/)yarn\.lock$
(?:^|/)(?i)docusaurus.config.js
(?:^|/)(?i)README.md
(?:^|/)(?i).nojekyll
^\static/
\.tsx$

View File

@@ -44,6 +44,7 @@ crt
Danske
datadir
Depositos
deselection
DIREKT
Dockerfiles
Dominguez
@@ -113,6 +114,7 @@ Qatari
QNTOFRP
QONTO
Raiffeisen
REGEXREPLACE
revolut
RIED
RSchedule

View File

@@ -1,40 +0,0 @@
#!/bin/bash
current_commit=$(git rev-parse HEAD)
echo "Running on commit $COMMIT_SHA"
function get_status() {
echo "::group::API Response"
curl --header "Authorization: Bearer $GITHUB_TOKEN" "https://api.github.com/repos/actualbudget/actual/commits/$COMMIT_SHA/statuses" > /tmp/status.json
cat /tmp/status.json
echo "::endgroup::"
netlify=$(yarn jq '[.[] | select(.context == "netlify/actualbudget/deploy-preview")][0]' /tmp/status.json)
state=$(yarn jq -r '.state' <<< "$netlify")
echo "::group::Netlify Status"
echo "$netlify"
echo "::endgroup::"
}
get_status
while [ "$netlify" == "null" ]; do
echo "Waiting for Netlify to start building..."
sleep 10
get_status
done
while [ "$state" == "pending" ]; do
echo "Waiting for Netlify to finish building..."
sleep 10
get_status
done
if [ "$state" == "success" ]; then
echo -e "\033[0;32mNetlify build succeeded!\033[0m"
yarn jq -r '"url=" + .target_url' <<< "$netlify" > $GITHUB_OUTPUT
exit 0
else
echo -e "\033[0;31mNetlify build failed. Cancelling end-to-end tests.\033[0m"
exit 1
fi

View File

@@ -20,6 +20,8 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Check if this is CodeRabbit's first comment
id: check-first-comment
@@ -72,7 +74,7 @@ jobs:
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
run: node .github/actions/ai-generated-release-notes/create-release-notes-file.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}

View File

@@ -18,6 +18,8 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Format code
run: yarn lint:fix
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

View File

@@ -19,6 +19,8 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Count points
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

32
.github/workflows/docs-release.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Release Docs to Github Pages
# Release docs on every push to master
on:
push:
branches:
- master
paths:
- 'packages/docs/**'
- '.github/workflows/docs-spelling.yml'
- '.github/actions/docs-spelling/**'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy Docs
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Docusaurus Deploy
run: |
GIT_USER=MikesGlitch \
GIT_PASS=${{ secrets.DOCS_GITHUB_PAGES_DEPLOY }} \
GIT_USER_NAME=github-actions[bot] \
GIT_USER_EMAIL=github-actions[bot]@users.noreply.github.com \
yarn deploy:docs

View File

@@ -2,6 +2,16 @@ name: E2E Tests
on:
pull_request:
paths:
- 'packages/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/e2e-test.yml'
- '!packages/sync-server/**' # Sync server changes don't affect E2E tests
- '!packages/api/**' # API changes don't affect E2E tests
- '!packages/ci-actions/**' # CI actions changes don't affect E2E tests
- '!packages/docs/**' # Docs changes don't affect E2E tests
- '!packages/eslint-plugin-actual/**' # Eslint plugin changes don't affect E2E tests
env:
GITHUB_PR_NUMBER: ${{github.event.pull_request.number}}
@@ -11,42 +21,29 @@ concurrency:
cancel-in-progress: true
jobs:
netlify:
name: Wait for Netlify build to finish
runs-on: ubuntu-latest
outputs:
netlify_url: ${{ steps.netlify.outputs.url }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
- 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
functional:
name: Functional
needs: netlify
name: Functional (shard ${{ matrix.shard }}/5)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Trust the repository directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Run E2E Tests on Netlify URL
run: yarn e2e
env:
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
- name: Run E2E Tests
run: yarn e2e --shard=${{ matrix.shard }}/5
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: always()
with:
name: desktop-client-test-results
name: desktop-client-test-results-shard-${{ matrix.shard }}
path: packages/desktop-client/test-results/
retention-days: 30
overwrite: true
@@ -60,6 +57,8 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Trust the repository directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Run Desktop app E2E Tests
@@ -74,23 +73,68 @@ jobs:
overwrite: true
vrt:
name: Visual regression
needs: netlify
name: Visual regression (shard ${{ matrix.shard }}/5)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
- name: Run VRT Tests on Netlify URL
run: yarn vrt
env:
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
with:
download-translations: 'false'
- name: Run VRT Tests
run: yarn vrt --shard=${{ matrix.shard }}/5
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: always()
with:
name: desktop-client-test-results
path: packages/desktop-client/test-results/
name: vrt-blob-report-${{ matrix.shard }}
path: packages/desktop-client/blob-report/
retention-days: 1
overwrite: true
merge-vrt:
name: Merge VRT Reports
needs: vrt
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
container:
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
- name: Download all blob reports
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
path: packages/desktop-client/all-blob-reports
pattern: vrt-blob-report-*
merge-multiple: true
- name: Merge reports
id: merge-reports
run: yarn workspace @actual-app/web run playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
id: playwright-report-vrt
with:
name: html-report--attempt-${{ github.run_attempt }}
path: packages/desktop-client/playwright-report
retention-days: 30
overwrite: true
- name: Save VRT metadata for comment workflow
if: github.event_name == 'pull_request'
run: |
mkdir -p vrt-metadata
echo "${{ github.event.pull_request.number }}" > vrt-metadata/pr-number.txt
echo "${{ needs.vrt.result }}" > vrt-metadata/vrt-result.txt
echo "${{ steps.playwright-report-vrt.outputs.artifact-url }}" > vrt-metadata/artifact-url.txt
- name: Upload VRT metadata
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: vrt-comment-metadata
path: vrt-metadata/
retention-days: 1

66
.github/workflows/e2e-vrt-comment.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: VRT Comment
# This workflow posts VRT failure comments on PRs, including fork PRs.
# It runs with elevated permissions via workflow_run trigger.
on:
workflow_run:
workflows: ['E2E Tests']
types:
- completed
permissions:
actions: read
pull-requests: write
jobs:
comment:
name: Post VRT Comment
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request'
steps:
- name: Download VRT metadata
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: vrt-comment-metadata
path: /tmp/vrt-metadata
continue-on-error: true
- name: Extract metadata
id: metadata
run: |
if [ ! -f "/tmp/vrt-metadata/pr-number.txt" ]; then
echo "No metadata found, skipping..."
echo "should_comment=false" >> "$GITHUB_OUTPUT"
exit 0
fi
PR_NUMBER=$(cat "/tmp/vrt-metadata/pr-number.txt")
VRT_RESULT=$(cat "/tmp/vrt-metadata/vrt-result.txt")
ARTIFACT_URL=$(cat "/tmp/vrt-metadata/artifact-url.txt")
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "vrt_result=$VRT_RESULT" >> "$GITHUB_OUTPUT"
echo "artifact_url=$ARTIFACT_URL" >> "$GITHUB_OUTPUT"
if [ "$VRT_RESULT" = "failure" ]; then
echo "should_comment=true" >> "$GITHUB_OUTPUT"
echo "VRT tests failed for PR #$PR_NUMBER"
else
echo "should_comment=false" >> "$GITHUB_OUTPUT"
echo "VRT tests passed or skipped for PR #$PR_NUMBER"
fi
- name: Comment on PR with VRT report link
if: steps.metadata.outputs.should_comment == 'true'
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
with:
number: ${{ steps.metadata.outputs.pr_number }}
header: vrt-comment
hide_and_recreate: true
hide_classify: OUTDATED
message: |
<!-- vrt-comment -->
VRT tests ❌ failed. [View the test report](${{ steps.metadata.outputs.artifact_url }}).
To update the VRT screenshots, comment `/update-vrt` on this PR. The VRT update operation takes about 50 minutes.

View File

@@ -38,7 +38,12 @@ jobs:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install setuptools
- name: Process release version
id: process_version
run: |
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
name: Setup Flatpak dependencies
run: |
sudo apt-get update
sudo apt-get install flatpak -y
@@ -47,6 +52,14 @@ jobs:
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
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
TODAY=$(date +%Y-%m-%d)
VERSION=${{ steps.process_version.outputs.version }}
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
sudo apt-get install appstream
appstreamcli --version
appstreamcli validate "$METAINFO_FILE"
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Electron for Mac
@@ -78,10 +91,6 @@ jobs:
name: actual-electron-${{ matrix.os }}-appx
path: |
packages/desktop-electron/dist/*.appx
- name: Process release version
id: process_version
run: |
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Add to new release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:
@@ -92,15 +101,29 @@ jobs:
## Desktop releases
Please note: Microsoft store updates can sometimes lag behind the main release by a couple of days while they verify the new version.
<a href="https://apps.microsoft.com/detail/9p2hmlhsdbrm?cid=Github+Releases&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
<p>
<a href="https://apps.microsoft.com/detail/9p2hmlhsdbrm?cid=Github+Releases&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200" /></a>
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width="12" height="1" alt="" />
<a href="https://flathub.org/apps/com.actualbudget.actual"><img width="165" style="margin-left:12px;" alt="Get it on Flathub" src="https://flathub.org/api/badge?locale=en" /></a>
</p>
files: |
packages/desktop-electron/dist/*.dmg
packages/desktop-electron/dist/*.exe
!packages/desktop-electron/dist/Actual-windows.exe
packages/desktop-electron/dist/*.AppImage
packages/desktop-electron/dist/*.flatpak
- name: Retrieve AppImage SHA256 for Flathub
id: appimage_sha256
run: |
APPIMAGE_X64_SHA256=$(sha256sum packages/desktop-electron/dist/Actual-linux-x86_64.AppImage | awk '{ print $1 }')
APPIMAGE_ARM64_SHA256=$(sha256sum packages/desktop-electron/dist/Actual-linux-arm64.AppImage | awk '{ print $1 }')
echo "appimage_x64_sha256=$APPIMAGE_X64_SHA256" >> "$GITHUB_OUTPUT"
echo "appimage_arm64_sha256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_OUTPUT"
outputs:
version: ${{ steps.process_version.outputs.version }}
appimage_x64_sha256: ${{ steps.appimage_sha256.outputs.appimage_x64_sha256 }}
appimage_arm64_sha256: ${{ steps.appimage_sha256.outputs.appimage_arm64_sha256 }}
publish-microsoft-store:
needs: build
@@ -143,3 +166,39 @@ jobs:
-NoStatus `
-AutoCommit `
-Force
publish-flathub:
needs: build
runs-on: ubuntu-22.04
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
steps:
- name: Checkout Flathub repo
uses: actions/checkout@v6
with:
repository: flathub/com.actualbudget.actual
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
- name: Update manifest with new SHA256
run: |
# Replace x86_64 entry
sed -i "/x86_64.AppImage/{n;s|sha256:.*|sha256: ${{ needs.build.outputs.appimage_x64_sha256 }}|}" com.actualbudget.actual.yml
sed -i "/x86_64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${{ needs.build.outputs.version }}/Actual-linux-x86_64.AppImage|" com.actualbudget.actual.yml
# Replace arm64 entry
sed -i "/arm64.AppImage/{n;s|sha256:.*|sha256: ${{ needs.build.outputs.appimage_arm64_sha256 }}|}" com.actualbudget.actual.yml
sed -i "/arm64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${{ needs.build.outputs.version }}/Actual-linux-arm64.AppImage|" com.actualbudget.actual.yml
cat com.actualbudget.actual.yml
- name: Create PR in Flathub repo
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
branch: 'release/${{ needs.build.outputs.version }}'
title: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
body: |
This PR updates the Actual desktop flatpak to version ${{ needs.build.outputs.version }}.
:link: [View release notes](https://actualbudget.org/blog/release-${{ needs.build.outputs.version }})
reviewers: 'jfdoming,MatissJanis,youngcw' # The core team that have accepted the collaborator access to the Flathub repo

View File

@@ -9,6 +9,15 @@ env:
on:
pull_request:
paths:
- 'packages/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/electron-pr.yml'
- '!packages/api/**' # API changes don't affect Electron
- '!packages/ci-actions/**' # CI actions changes don't affect Electron
- '!packages/docs/**' # Docs changes don't affect Electron
- '!packages/eslint-plugin-actual/**' # Eslint plugin changes don't affect Electron
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -34,6 +43,7 @@ jobs:
source .venv/bin/activate
python3 -m pip install setuptools
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
name: Setup Flatpak dependencies
run: |
sudo apt-get update
sudo apt-get install flatpak -y
@@ -42,6 +52,14 @@ jobs:
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
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
TODAY=$(date +%Y-%m-%d)
VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
sudo apt-get install appstream
appstreamcli --version
appstreamcli validate "$METAINFO_FILE"
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Electron

View File

@@ -29,7 +29,7 @@ jobs:
The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+
Dont forget to upvote the top comment with 👍!
Don't forget to upvote the top comment with 👍!
<!-- feature-auto-close-comment -->
- name: Close Issue

View File

@@ -39,6 +39,7 @@ jobs:
python3 -m pip install setuptools
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
name: Setup Flatpak dependencies
run: |
sudo apt-get update
sudo apt-get install flatpak -y
@@ -48,6 +49,13 @@ jobs:
sudo flatpak install org.freedesktop.Platform//24.08 -y
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
TODAY=$(date +%Y-%m-%d)
VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
sudo apt-get install appstream
appstreamcli --version
appstreamcli validate "$METAINFO_FILE"
- name: Set up environment
uses: ./.github/actions/setup

View File

@@ -15,7 +15,13 @@ on:
pull_request_target:
paths:
- 'packages/**'
- '!packages/sync-server/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/size-compare.yml'
- '!packages/sync-server/**' # Sync server changes don't affect the size of the web/api
- '!packages/ci-actions/**' # CI actions changes don't affect the size of the web/api
- '!packages/docs/**' # Docs changes don't affect the size of the web/api
- '!packages/eslint-plugin-actual/**' # Eslint plugin changes don't affect the size of the web/api
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}

View File

@@ -58,7 +58,8 @@ jobs:
with:
repository: ${{ steps.metadata.outputs.head_repo }}
ref: ${{ steps.metadata.outputs.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
persist-credentials: false
fetch-depth: 0
- name: Validate and apply patch
@@ -121,22 +122,15 @@ jobs:
env:
HEAD_REF: ${{ steps.metadata.outputs.head_ref }}
HEAD_REPO: ${{ steps.metadata.outputs.head_repo }}
GITHUB_TOKEN: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
run: |
# Use PAT in URL to ensure push triggers CI workflows
# Note: GitHub Actions automatically masks secrets in logs
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${HEAD_REPO}.git"
git push origin "HEAD:refs/heads/$HEAD_REF"
echo "Successfully pushed VRT updates to $HEAD_REPO@$HEAD_REF"
- name: Comment on PR - Success
if: steps.apply.outputs.applied == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
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.'
});
- name: Comment on PR - Failure
if: failure() && steps.metadata.outputs.pr_number != ''
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0

View File

@@ -1,51 +1,82 @@
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
# Triggered by commenting "/update-vrt" on a pull request.
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'packages/**'
- '.github/workflows/vrt-update-generate.yml'
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: false
jobs:
add-reaction:
name: Add 👀 Reaction
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')
permissions:
pull-requests: write
steps:
- name: Add 👀 reaction to comment
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes'
});
generate-vrt-updates:
name: Generate VRT Updates
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.56.0-jammy
steps:
- name: Get PR details
id: pr
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
core.setOutput('head_sha', pr.head.sha);
core.setOutput('head_ref', pr.head.ref);
core.setOutput('head_repo', pr.head.repo.full_name);
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ steps.pr.outputs.head_sha }}
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- 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
- name: Run VRT Tests
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
@@ -84,7 +115,7 @@ jobs:
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: vrt-patch-${{ github.event.pull_request.number }}
name: vrt-patch-${{ github.event.issue.number }}
path: vrt-update.patch
retention-days: 5
@@ -92,14 +123,14 @@ jobs:
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
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
- name: Upload PR metadata
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: vrt-metadata-${{ github.event.pull_request.number }}
name: vrt-metadata-${{ github.event.issue.number }}
path: pr-metadata/
retention-days: 5

10
.gitignore vendored
View File

@@ -15,9 +15,17 @@ export-2020-01-10.csv
# JavaScript
node_modules
packages/api/app/bundle.api.js
packages/api/app/stats.json
packages/api/dist
packages/api/@types
packages/crdt/dist
packages/desktop-client/build-stats
packages/desktop-client/dev-dist
packages/desktop-client/public/kcab
packages/desktop-client/locale
packages/desktop-client/playwright-report
packages/desktop-client/test-results
packages/desktop-electron/client-build
packages/desktop-electron/build
packages/desktop-electron/.electron-symbols
@@ -25,6 +33,8 @@ packages/desktop-electron/dist
packages/desktop-electron/loot-core
packages/desktop-client/service-worker
packages/plugins-service/dist
packages/loot-core/lib-dist
packages/sync-server/coverage
bundle.desktop.js
bundle.desktop.js.map
bundle.mobile.js

10
.oxfmtrc.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "avoid",
"printWidth": 80,
"ignorePatterns": [
"packages/docs/*" // TOOD: fixme; temporary
]
}

433
.oxlintrc.json Normal file
View File

@@ -0,0 +1,433 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "import", "jsx-a11y"],
"jsPlugins": ["./packages/eslint-plugin-actual/lib/index.js"],
"env": {
"browser": true,
"jest": true,
"node": true
},
"globals": {
"vi": "readonly",
"backend": "readonly",
"importScripts": "readonly",
"FS": "readonly" // TODO: remove this
},
"rules": {
// TODO fix all these and re-enable
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/tabindex-no-positive": "off",
// Actual rules
"actual/typography": "warn",
"actual/no-untranslated-strings": "error",
"actual/prefer-trans-over-t": "error",
"actual/prefer-if-statement": "warn",
"actual/prefer-logger-over-console": "error",
// JSX A11y rules
"jsx-a11y/no-autofocus": [
"warn",
{
"ignoreNonDOM": true
}
],
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-has-content": "warn",
"jsx-a11y/anchor-is-valid": [
"warn",
{
"aspects": ["noHref", "invalidHref"]
}
],
"jsx-a11y/aria-activedescendant-has-tabindex": "warn",
"jsx-a11y/aria-props": "warn",
"jsx-a11y/aria-proptypes": "warn",
"jsx-a11y/aria-role": [
"warn",
{
"ignoreNonDOM": true
}
],
"jsx-a11y/aria-unsupported-elements": "warn",
"jsx-a11y/heading-has-content": "warn",
"jsx-a11y/iframe-has-title": "warn",
"jsx-a11y/img-redundant-alt": "warn",
"jsx-a11y/no-access-key": "warn",
"jsx-a11y/no-distracting-elements": "warn",
"jsx-a11y/no-redundant-roles": "warn",
"jsx-a11y/role-has-required-aria-props": "warn",
"jsx-a11y/role-supports-aria-props": "warn",
"jsx-a11y/scope": "warn",
// Typescript rules
"typescript/ban-ts-comment": [
"warn",
{
// TODO: remove this
"ts-ignore": "allow-with-description"
}
],
"typescript/consistent-type-definitions": ["warn", "type"],
"typescript/consistent-type-imports": [
"warn",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/no-implied-eval": "warn",
"typescript/no-explicit-any": "warn",
"typescript/no-restricted-types": [
"warn",
{
"types": {
// forbid FC as superfluous
"FunctionComponent": {
"message": "Type the props argument and let TS infer or use ComponentType for a component prop"
},
"FC": {
"message": "Type the props argument and let TS infer or use ComponentType for a component prop"
}
}
}
],
"typescript/no-var-requires": "warn",
// Import rules
"import/first": "error",
"import/no-amd": "error",
"import/no-default-export": "warn",
"import/no-webpack-loader-syntax": "error",
"import/no-useless-path-segments": "warn",
"import/no-unresolved": "warn",
"import/no-unused-modules": "warn",
"import/no-duplicates": [
"warn",
{
"prefer-inline": true
}
],
// React rules
"react/exhaustive-deps": [
"warn",
{
"additionalHooks": "(useQuery|useEffectAfterMount)"
}
],
"react/jsx-curly-brace-presence": "warn",
"react/jsx-filename-extension": [
"warn",
{
"extensions": [".jsx", ".tsx"],
"allow": "as-needed"
}
],
"react/jsx-no-comment-textnodes": "warn",
"react/jsx-no-duplicate-props": "warn",
"react/jsx-no-target-blank": "warn",
"react/jsx-no-undef": "error",
"react/jsx-no-useless-fragment": "warn",
"react/jsx-pascal-case": [
"warn",
{
"allowAllCaps": true,
"ignore": []
}
],
"react/no-danger-with-children": "warn",
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-unstable-nested-components": "warn",
"react/require-render-return": "error",
"react/rules-of-hooks": "error",
"react/self-closing-comp": "warn",
"react/style-prop-object": "warn",
"react/jsx-boolean-value": "warn",
// ESLint rules
"eslint/array-callback-return": "warn",
// "eslint/curly": ["warn", "multi-line", "consistent"], // TODO: re-enable? this rule is really slow
"eslint/default-case": [
"warn",
{
"commentPattern": "^no default$"
}
],
"eslint/eqeqeq": ["warn", "smart"],
"eslint/no-array-constructor": "warn",
"eslint/no-caller": "warn",
"eslint/no-cond-assign": ["warn", "except-parens"],
"eslint/no-const-assign": "warn",
"eslint/no-control-regex": "warn",
"eslint/no-delete-var": "warn",
"eslint/no-dupe-class-members": "warn",
"eslint/no-dupe-keys": "warn",
"eslint/no-duplicate-case": "warn",
"eslint/no-empty-character-class": "warn",
"eslint/no-empty-function": "warn",
"eslint/no-empty-pattern": "warn",
"eslint/no-eval": "warn",
"eslint/no-ex-assign": "warn",
"eslint/no-extend-native": "warn",
"eslint/no-extra-bind": "warn",
"eslint/no-extra-label": "warn",
"eslint/no-fallthrough": "warn",
"eslint/no-func-assign": "warn",
"eslint/no-invalid-regexp": "warn",
"eslint/no-iterator": "warn",
"eslint/no-label-var": "warn",
"eslint/no-var": "warn",
"eslint/no-labels": [
"warn",
{
"allowLoop": true,
"allowSwitch": false
}
],
"eslint/no-new-func": "warn",
"eslint/no-script-url": "warn",
"eslint/no-self-assign": "warn",
"eslint/no-self-compare": "warn",
"eslint/no-sequences": "warn",
"eslint/no-shadow-restricted-names": "warn",
"eslint/no-sparse-arrays": "warn",
"eslint/no-template-curly-in-string": "warn",
"eslint/no-this-before-super": "warn",
"eslint/no-throw-literal": "warn",
"eslint/no-unreachable": "warn",
"eslint/no-obj-calls": "warn",
"eslint/no-new-wrappers": "warn",
"eslint/no-unsafe-negation": "warn",
"eslint/no-multi-str": "warn",
"eslint/no-global-assign": "warn",
"eslint/no-lone-blocks": "warn",
"eslint/no-unused-labels": "warn",
"eslint/no-object-constructor": "warn",
"eslint/no-new-native-nonconstructor": "warn",
"eslint/no-redeclare": "warn",
"eslint/no-useless-computed-key": "warn",
"eslint/no-useless-concat": "warn",
"eslint/no-useless-escape": "warn",
"eslint/require-yield": "warn",
"eslint/getter-return": "warn",
"eslint/unicode-bom": ["warn", "never"],
"eslint/no-use-isnan": "warn",
"eslint/valid-typeof": "warn",
"eslint/no-useless-rename": [
"warn",
{
"ignoreDestructuring": false,
"ignoreImport": false,
"ignoreExport": false
}
],
"eslint/no-with": "warn",
"eslint/no-regex-spaces": "warn",
"eslint/no-restricted-globals": [
"warn",
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
"addEventListener",
"blur",
"close",
"closed",
"confirm",
"defaultStatus",
"defaultstatus",
"event",
"external",
"find",
"focus",
"frameElement",
"frames",
"history",
"innerHeight",
"innerWidth",
"length",
"location",
"locationbar",
"menubar",
"moveBy",
"moveTo",
"name",
"onblur",
"onerror",
"onfocus",
"onload",
"onresize",
"onunload",
"open",
"opener",
"opera",
"outerHeight",
"outerWidth",
"pageXOffset",
"pageYOffset",
"parent",
"print",
"removeEventListener",
"resizeBy",
"resizeTo",
"screen",
"screenLeft",
"screenTop",
"screenX",
"screenY",
"scroll",
"scrollbars",
"scrollBy",
"scrollTo",
"scrollX",
"scrollY",
"status",
"statusbar",
"stop",
"toolbar",
"top"
],
"eslint/no-restricted-imports": [
"warn",
{
"paths": [
{
"name": "react-router",
"importNames": ["useNavigate"],
"message": "Please import Actual's useNavigate() hook from `src/hooks` instead."
},
{
"name": "react-redux",
"importNames": ["useDispatch"],
"message": "Please import Actual's useDispatch() hook from `src/redux` instead."
},
{
"name": "react-redux",
"importNames": ["useSelector"],
"message": "Please import Actual's useSelector() hook from `src/redux` instead."
},
{
"name": "react-redux",
"importNames": ["useStore"],
"message": "Please import Actual's useStore() hook from `src/redux` instead."
}
],
"patterns": [
{
"group": ["**/*.api", "**/*.web", "**/*.electron"],
"message": "Don't directly reference imports from other platforms"
},
{
"group": ["uuid"],
"importNames": ["*"],
"message": "Use `import { v4 as uuidv4 } from 'uuid'` instead"
},
{
"group": ["**/style", "**/colors"],
"importNames": ["colors"],
"message": "Please use themes instead of colors"
},
{
"group": ["@actual-app/web/**/*"],
"message": "Please do not import `@actual-app/web` in `loot-core`"
}
]
}
],
"eslint/no-useless-constructor": "warn",
"eslint/no-undef": "warn",
"eslint/no-unused-expressions": "warn"
},
"overrides": [
{
// TODO: fix the issues in these files
"files": [
"packages/component-library/src/Menu.tsx",
"packages/desktop-client/src/components/accounts/Account.jsx",
"packages/desktop-client/src/components/accounts/MobileAccount.jsx",
"packages/desktop-client/src/components/accounts/MobileAccounts.jsx",
"packages/desktop-client/src/components/budget/BudgetCategories.jsx",
"packages/desktop-client/src/components/budget/BudgetSummaries.tsx",
"packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx",
"packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx",
"packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx",
"packages/desktop-client/src/components/budget/index.tsx",
"packages/desktop-client/src/components/budget/MobileBudget.tsx",
"packages/desktop-client/src/components/FinancesApp.tsx",
"packages/desktop-client/src/components/GlobalKeys.ts",
"packages/desktop-client/src/components/LoggedInUser.tsx",
"packages/desktop-client/src/components/manager/ManagementApp.jsx",
"packages/desktop-client/src/components/manager/subscribe/common.tsx",
"packages/desktop-client/src/components/ManageRules.tsx",
"packages/desktop-client/src/components/mobile/MobileAmountInput.jsx",
"packages/desktop-client/src/components/mobile/MobileNavTabs.tsx",
"packages/desktop-client/src/components/Modals.tsx",
"packages/desktop-client/src/components/modals/EditRule.jsx",
"packages/desktop-client/src/components/modals/ImportTransactions.jsx",
"packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx",
"packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx",
"packages/desktop-client/src/components/Notifications.tsx",
"packages/desktop-client/src/components/payees/ManagePayees.jsx",
"packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx",
"packages/desktop-client/src/components/payees/PayeeTable.tsx",
"packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx",
"packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx",
"packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx",
"packages/desktop-client/src/components/reports/reports/CustomReport.jsx",
"packages/desktop-client/src/components/reports/reports/CustomReport.tsx",
"packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx",
"packages/desktop-client/src/components/reports/SaveReportName.tsx",
"packages/desktop-client/src/components/reports/useReport.ts",
"packages/desktop-client/src/components/schedules/ScheduleDetails.jsx",
"packages/desktop-client/src/components/schedules/ScheduleEditModal.tsx",
"packages/desktop-client/src/components/schedules/SchedulesTable.tsx",
"packages/desktop-client/src/components/select/DateSelect.tsx",
"packages/desktop-client/src/components/sidebar/Tools.tsx",
"packages/desktop-client/src/components/sort.tsx",
"packages/desktop-client/src/hooks/useEffectAfterMount.ts",
"packages/desktop-client/src/hooks/useQuery.ts"
],
"rules": {
"react/exhaustive-deps": "off"
}
},
{
"files": ["**/*.test.{js,ts,jsx,tsx}", "packages/docs/**/*"],
"rules": {
"actual/no-untranslated-strings": "off",
"actual/prefer-logger-over-console": "off"
}
},
{
"files": [
"packages/api/migrations/*",
"packages/loot-core/migrations/*",
"packages/sync-server/src/app-gocardless/banks/*.js",
"*.config.{ts,mts,mjs}"
],
"rules": {
"import/no-default-export": "off"
}
},
// TODO: enable these
{
"files": [
"packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx",
"packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx",
"packages/desktop-client/src/components/budget/BudgetCategories.tsx",
"packages/desktop-client/src/components/budget/envelope/BalanceMovementMenu.tsx",
"packages/desktop-client/src/components/ManageRules.tsx",
"packages/desktop-client/src/components/mobile/budget/ExpenseGroupList.tsx",
"packages/desktop-client/src/components/modals/EditFieldModal.tsx",
"packages/desktop-client/src/components/reports/reports/Calendar.tsx",
"packages/desktop-client/src/components/schedules/ScheduleLink.tsx",
"packages/desktop-client/src/components/ServerContext.tsx",
"packages/desktop-client/src/components/table.tsx"
],
"rules": {
"eslint/no-empty-function": "off"
}
}
]
}

View File

@@ -1,36 +0,0 @@
sync_pb.*
packages/api/app/bundle.api.js
packages/api/app/stats.json
packages/api/dist
packages/api/@types
packages/api/migrations
packages/crdt/dist
packages/component-library/src/icons/**/*
packages/desktop-client/bundle.browser.js
packages/desktop-client/stats.json
packages/desktop-client/.swc/
packages/desktop-client/build/
packages/desktop-client/dev-dist/
packages/desktop-client/locale/
packages/desktop-client/build-electron/
packages/desktop-client/build-stats/
packages/desktop-client/public/kcab/
packages/desktop-client/public/data/
packages/desktop-client/**/node_modules/*
packages/desktop-client/node_modules/
packages/desktop-client/test-results/
packages/desktop-client/playwright-report/
packages/desktop-electron/client-build/
packages/desktop-electron/build/
packages/desktop-electron/dist/
packages/loot-core/**/node_modules/*
packages/loot-core/**/lib-dist/*
packages/loot-core/**/proto/*
packages/sync-server/coverage/
packages/sync-server/user-files/
packages/sync-server/server-files/
.yarn/*
upcoming-release-notes/*
# temporary
packages/docs/*

View File

@@ -1,5 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "avoid"
}

View File

@@ -7,7 +7,7 @@ This guide provides comprehensive information for AI agents (like Cursor) workin
**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/actual/tree/master/packages/docs or https://actualbudget.org/docs
- **Community Docs**: Documentation is part of the monorepo at `packages/docs/`. Published at https://actualbudget.org/docs
- **License**: MIT
- **Primary Language**: TypeScript (with React)
- **Build System**: Yarn 4 workspaces (monorepo)
@@ -173,6 +173,19 @@ Custom ESLint rules specific to Actual.
- `typography`: Typography rules
- `prefer-if-statement`: Prefers explicit if statements
#### 10. **docs** (`packages/docs/`)
Documentation website built with Docusaurus.
- Documentation is part of the monorepo
- Built with Docusaurus 3
- Commands:
```bash
yarn workspace docs start
yarn workspace docs build
yarn start:docs # From root
```
## Development Workflow
### 1. Making Changes
@@ -201,9 +214,6 @@ yarn test:debug
# Run tests for a specific package
yarn workspace loot-core run test
# Run a specific test file (watch mode)
yarn workspace loot-core run test path/to/test.test.ts
```
**E2E Tests (Playwright)**
@@ -382,6 +392,7 @@ describe('ComponentName', () => {
- `/CONTRIBUTING.md` - Points to community docs
- `/upcoming-release-notes/` - Release notes for next version
- `/CODEOWNERS` - Code ownership definitions
- `/packages/docs/` - Documentation website (Docusaurus)
### Build Artifacts (Don't Edit)
@@ -403,6 +414,8 @@ describe('ComponentName', () => {
- `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)
- `packages/docs/docs/` - Documentation source files (Markdown)
- `packages/docs/docs/contributing/` - Developer documentation
## Common Development Tasks
@@ -412,9 +425,6 @@ describe('ComponentName', () => {
# Run all tests across all packages (recommended)
yarn test
# Unit test for a specific file in loot-core (watch mode)
yarn workspace loot-core run test src/path/to/file.test.ts
# E2E test for a specific file
yarn workspace @actual-app/web run playwright test accounts.test.ts --browser=chromium
```

View File

@@ -8,6 +8,7 @@ CI=${CI:-false}
cd "$ROOT/.."
POSITIONAL=()
SKIP_EXE_BUILD=false
SKIP_TRANSLATIONS=false
while [[ $# -gt 0 ]]; do
key="$1"
@@ -20,6 +21,10 @@ while [[ $# -gt 0 ]]; do
SKIP_EXE_BUILD=true
shift
;;
--skip-translations)
SKIP_TRANSLATIONS=true
shift
;;
*)
POSITIONAL+=("$1")
shift
@@ -29,15 +34,19 @@ done
set -- "${POSITIONAL[@]}"
# Get translations
echo "Updating translations..."
if ! [ -d packages/desktop-client/locale ]; then
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
if [ $SKIP_TRANSLATIONS == false ]; then
# Get translations
echo "Updating translations..."
if ! [ -d packages/desktop-client/locale ]; then
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
fi
pushd packages/desktop-client/locale > /dev/null
git pull
popd > /dev/null
packages/desktop-client/bin/remove-untranslated-languages
fi
pushd packages/desktop-client/locale > /dev/null
git pull
popd > /dev/null
packages/desktop-client/bin/remove-untranslated-languages
export NODE_OPTIONS="--max-old-space-size=4096"

View File

@@ -6,7 +6,6 @@ import prompts from 'prompts';
async function run() {
const username = await execAsync(
// eslint-disable-next-line actual/typography
"gh api user --jq '.login'",
'To avoid having to enter your username, consider installing the official GitHub CLI (https://github.com/cli/cli) and logging in with `gh auth login`.',
);
@@ -161,8 +160,7 @@ category: ${type}
authors: [${username}]
---
${summary}
`;
${summary}`;
}
// simple exec that fails silently and returns an empty string on failure

View File

@@ -1,87 +1,17 @@
import tsParser from '@typescript-eslint/parser';
import { defineConfig } from 'eslint/config';
import pluginImport from 'eslint-plugin-import';
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
import pluginReact from 'eslint-plugin-react';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import pluginPerfectionist from 'eslint-plugin-perfectionist';
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import pluginTypescript from 'typescript-eslint';
// eslint-disable-next-line import/extensions
import pluginActual from './packages/eslint-plugin-actual/lib/index.js';
const confusingBrowserGlobals = [
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
'addEventListener',
'blur',
'close',
'closed',
'confirm',
'defaultStatus',
'defaultstatus',
'event',
'external',
'find',
'focus',
'frameElement',
'frames',
'history',
'innerHeight',
'innerWidth',
'length',
'location',
'locationbar',
'menubar',
'moveBy',
'moveTo',
'name',
'onblur',
'onerror',
'onfocus',
'onload',
'onresize',
'onunload',
'open',
'opener',
'opera',
'outerHeight',
'outerWidth',
'pageXOffset',
'pageYOffset',
'parent',
'print',
'removeEventListener',
'resizeBy',
'resizeTo',
'screen',
'screenLeft',
'screenTop',
'screenX',
'screenY',
'scroll',
'scrollbars',
'scrollBy',
'scrollTo',
'scrollX',
'scrollY',
'status',
'statusbar',
'stop',
'toolbar',
'top',
];
export default defineConfig(
{
ignores: [
//temporary
'packages/docs',
'packages/api/app/bundle.api.js',
'packages/api/app/stats.json',
'packages/api/@types',
'packages/api/migrations',
'packages/crdt/src/proto/sync_pb.js',
'packages/component-library/src/icons/**/*',
'packages/desktop-client/bundle.browser.js',
'packages/desktop-client/dev-dist/',
@@ -95,6 +25,7 @@ export default defineConfig(
'packages/desktop-electron/client-build/',
'packages/loot-core/**/lib-dist/*',
'packages/loot-core/**/proto/*',
'packages/sync-server/coverage/',
'packages/sync-server/user-files/',
'packages/sync-server/server-files/',
'.yarn/*',
@@ -104,26 +35,6 @@ export default defineConfig(
'**/node_modules/',
],
},
{
// Temporary until the sync-server is migrated to TypeScript
files: [
'packages/sync-server/**/*.spec.{js,jsx}',
'packages/sync-server/**/*.test.{js,jsx}',
],
languageOptions: {
globals: {
vi: true,
describe: true,
expect: true,
it: true,
beforeAll: true,
beforeEach: true,
afterAll: true,
afterEach: true,
test: true,
},
},
},
{
linterOptions: {
reportUnusedDisableDirectives: true,
@@ -133,172 +44,37 @@ export default defineConfig(
...globals.browser,
...globals.commonjs,
...globals.node,
...globals.jest,
globalThis: false,
vi: true,
RequestInfo: true,
RequestInit: true,
ParentNode: true,
FS: true,
IDBValidKey: true,
NodeJS: true,
Electron: true,
// Worker globals
FetchEvent: true,
ExtendableEvent: true,
ExtendableMessageEvent: true,
ServiceWorkerGlobalScope: true,
},
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
},
},
},
pluginReact.configs.flat.recommended,
pluginReact.configs.flat['jsx-runtime'],
pluginTypescript.configs.recommended,
pluginImport.flatConfigs.recommended,
pluginTypescript.configs.base,
{
plugins: {
actual: pluginActual,
perfectionist: pluginPerfectionist,
},
rules: {
'actual/no-untranslated-strings': 'error',
'actual/prefer-trans-over-t': 'error',
},
},
{
files: ['**/*.{js,ts,jsx,tsx,mjs,mts}'],
plugins: {
'jsx-a11y': pluginJSXA11y,
'react-hooks': pluginReactHooks,
},
rules: {
// http://eslint.org/docs/rules/
'array-callback-return': 'warn',
'default-case': [
'warn',
{
commentPattern: '^no default$',
},
],
curly: ['warn', 'multi-line', 'consistent'],
'dot-location': ['warn', 'property'],
eqeqeq: ['warn', 'smart'],
'new-parens': 'warn',
'no-array-constructor': 'warn',
'no-caller': 'warn',
'no-cond-assign': ['warn', 'except-parens'],
'no-const-assign': 'warn',
'no-control-regex': 'warn',
'no-delete-var': 'warn',
'no-dupe-args': 'warn',
'no-dupe-class-members': 'warn',
'no-dupe-keys': 'warn',
'no-duplicate-case': 'warn',
'no-empty-character-class': 'warn',
'no-empty-pattern': 'warn',
'no-eval': 'warn',
'no-ex-assign': 'warn',
'no-extend-native': 'warn',
'no-extra-bind': 'warn',
'no-extra-label': 'warn',
'no-fallthrough': 'warn',
'no-func-assign': 'warn',
'no-implied-eval': 'warn',
'no-invalid-regexp': 'warn',
'no-iterator': 'warn',
'no-label-var': 'warn',
'no-labels': [
'warn',
{
allowLoop: true,
allowSwitch: false,
},
],
'no-lone-blocks': 'warn',
'no-mixed-operators': [
'warn',
{
groups: [
['&', '|', '^', '~', '<<', '>>', '>>>'],
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof'],
],
allowSamePrecedence: false,
},
],
'no-multi-str': 'warn',
'no-global-assign': 'warn',
'no-unsafe-negation': 'warn',
'no-new-func': 'warn',
'no-new-object': 'warn',
'no-new-symbol': 'warn',
'no-new-wrappers': 'warn',
'no-obj-calls': 'warn',
'no-octal': 'warn',
'no-octal-escape': 'warn',
'no-redeclare': 'warn',
'no-regex-spaces': 'warn',
'no-script-url': 'warn',
'no-self-assign': 'warn',
'no-self-compare': 'warn',
'no-sequences': 'warn',
'no-shadow-restricted-names': 'warn',
'no-sparse-arrays': 'warn',
'no-template-curly-in-string': 'warn',
'no-this-before-super': 'warn',
'no-throw-literal': 'warn',
'no-undef': 'error',
'no-unreachable': 'warn',
'no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'no-unused-labels': 'warn',
'no-use-before-define': [
'warn',
{
functions: false,
classes: false,
variables: false,
},
],
'no-useless-computed-key': 'warn',
'no-useless-concat': 'warn',
'no-useless-constructor': 'warn',
'no-useless-escape': 'warn',
'no-useless-rename': [
'warn',
{
ignoreDestructuring: false,
ignoreImport: false,
ignoreExport: false,
},
],
'no-with': 'warn',
'no-whitespace-before-property': 'warn',
'require-yield': 'warn',
'rest-spread-spacing': ['warn', 'never'],
strict: ['warn', 'never'],
'unicode-bom': ['warn', 'never'],
'use-isnan': 'warn',
'valid-typeof': 'warn',
'no-restricted-properties': [
'error',
{
@@ -315,168 +91,37 @@ export default defineConfig(
},
],
'getter-return': 'warn',
// https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules
'import/first': 'error',
'import/no-amd': 'error',
'import/no-anonymous-default-export': 'warn',
'import/no-webpack-loader-syntax': 'error',
'import/extensions': [
'warn',
'never',
{
json: 'always',
},
],
'import/no-useless-path-segments': 'warn',
'import/no-duplicates': [
'perfectionist/sort-imports': [
'warn',
{
'prefer-inline': true,
},
],
'import/order': [
'warn',
{
alphabetize: {
caseInsensitive: true,
order: 'asc',
},
groups: ['builtin', 'external', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
pathGroups: [
groups: [
'react',
'builtin',
'external',
'loot-core',
'parent',
'sibling',
'index',
'desktop-client',
],
customGroups: [
{
// Enforce that React (and react-related packages) is the first import
group: 'builtin',
pattern: 'react?(-*)',
position: 'before',
groupName: 'react',
elementNamePattern: '^react(-.*)?$',
},
{
// Separate imports from Actual from "real" external imports
group: 'external',
pattern: 'loot-{core,design}/**/*',
position: 'after',
groupName: 'loot-core',
elementNamePattern: '^loot-core',
},
{
groupName: 'desktop-client',
elementNamePattern: '^@desktop-client',
},
],
pathGroupsExcludedImportTypes: ['react'],
newlinesBetween: 'always',
},
],
// https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
'react/forbid-foreign-prop-types': [
'warn',
{
allowInPropTypes: true,
},
],
'react/jsx-no-comment-textnodes': 'warn',
'react/jsx-no-duplicate-props': 'warn',
'react/jsx-no-target-blank': 'warn',
'react/jsx-no-undef': 'error',
'react/jsx-pascal-case': [
'warn',
{
allowAllCaps: true,
ignore: [],
},
],
'react/no-danger-with-children': 'warn',
// Disabled because of undesirable warnings
// See https://github.com/facebook/create-react-app/issues/5204 for
// blockers until its re-enabled
// 'react/no-deprecated': 'warn',
'react/no-direct-mutation-state': 'warn',
'react/no-is-mounted': 'warn',
'react/no-typos': 'error',
'react/require-render-return': 'error',
'react/style-prop-object': 'warn',
'react/jsx-no-useless-fragment': 'warn',
'react/self-closing-comp': 'warn',
'react/jsx-filename-extension': [
'warn',
{
extensions: ['.jsx', '.tsx'],
allow: 'as-needed',
},
],
'react/no-unstable-nested-components': [
'warn',
{
allowAsProps: true,
customValidators: ['formatter'],
},
],
// Don't need this as we're using TypeScript
'react/prop-types': 'off',
// https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
'jsx-a11y/alt-text': 'warn',
'jsx-a11y/anchor-has-content': 'warn',
'jsx-a11y/anchor-is-valid': [
'warn',
{
aspects: ['noHref', 'invalidHref'],
},
],
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
'jsx-a11y/aria-props': 'warn',
'jsx-a11y/aria-proptypes': 'warn',
'jsx-a11y/aria-role': [
'warn',
{
ignoreNonDOM: true,
},
],
'jsx-a11y/aria-unsupported-elements': 'warn',
'jsx-a11y/heading-has-content': 'warn',
'jsx-a11y/iframe-has-title': 'warn',
'jsx-a11y/img-redundant-alt': 'warn',
'jsx-a11y/no-access-key': 'warn',
'jsx-a11y/no-distracting-elements': 'warn',
'jsx-a11y/no-redundant-roles': 'warn',
'jsx-a11y/role-has-required-aria-props': 'warn',
'jsx-a11y/role-supports-aria-props': 'warn',
'jsx-a11y/scope': 'warn',
// https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': [
'warn',
{
additionalHooks: '(useQuery|useEffectAfterMount)',
},
],
'actual/typography': 'warn',
'actual/prefer-if-statement': 'warn',
'actual/prefer-logger-over-console': 'error',
// Note: base rule explicitly disabled in favor of the TS one
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
varsIgnorePattern: '^(_|React)',
argsIgnorePattern: '^(_|React)',
ignoreRestSiblings: true,
caughtErrors: 'none',
},
],
'no-restricted-globals': ['warn', ...confusingBrowserGlobals],
// https://github.com/eslint/eslint/issues/16954
// https://github.com/eslint/eslint/issues/16953
'no-loop-func': 'off',
// TODO: re-enable these rules
'react/react-in-jsx-scope': 'off',
'no-var': 'warn',
'react/jsx-curly-brace-presence': 'warn',
'object-shorthand': ['warn', 'properties'],
'no-restricted-syntax': [
@@ -495,141 +140,7 @@ export default defineConfig(
},
],
'no-restricted-imports': [
'warn',
{
paths: [
{
name: 'react-router',
importNames: ['useNavigate'],
message:
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
},
{
name: 'react-redux',
importNames: ['useDispatch'],
message:
"Please import Actual's useDispatch() hook from `src/redux` instead.",
},
{
name: 'react-redux',
importNames: ['useSelector'],
message:
"Please import Actual's useSelector() hook from `src/redux` instead.",
},
{
name: 'react-redux',
importNames: ['useStore'],
message:
"Please import Actual's useStore() hook from `src/redux` instead.",
},
],
patterns: [
{
group: ['*.api', '*.web', '*.electron'],
message: "Don't directly reference imports from other platforms",
},
{
group: ['uuid'],
importNames: ['*'],
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
},
{
group: ['**/style', '**/colors'],
importNames: ['colors'],
message: 'Please use themes instead of colors',
},
{
group: ['@actual-app/web/*'],
message: 'Please do not import `@actual-app/web` in `loot-core`',
},
],
},
],
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description',
},
],
// Rules disabled during TS migration
'@typescript-eslint/no-var-requires': 'off',
'prefer-const': 'warn',
'prefer-spread': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-require-imports': 'off',
'import/no-default-export': 'warn',
},
},
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
ecmaVersion: 2018,
sourceType: 'module',
parserOptions: {
projectService: true,
ecmaFeatures: {
jsx: true,
},
// typescript-eslint specific options
warnOnUnsupportedTypeScriptVersion: true,
},
},
// If adding a typescript-eslint version of an existing ESLint rule,
// make sure to disable the ESLint rule here.
rules: {
// TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
'default-case': 'off',
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
'no-dupe-class-members': 'off',
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
'no-undef': 'off',
// TypeScript already handles these (https://typescript-eslint.io/troubleshooting/typed-linting/performance/#eslint-plugin-import)
'import/named': 'off',
'import/namespace': 'off',
'import/default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-unresolved': 'off',
// Add TypeScript specific rules (and turn off ESLint equivalents)
'@typescript-eslint/consistent-type-assertions': 'warn',
'no-array-constructor': 'off',
'@typescript-eslint/no-array-constructor': 'warn',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'warn',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': [
'warn',
{
functions: false,
classes: false,
variables: false,
typedefs: false,
},
],
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'warn',
},
},
{
@@ -646,169 +157,9 @@ export default defineConfig(
},
},
{
files: [
'packages/desktop-client/**/*.{ts,tsx}',
'packages/loot-core/src/client/**/*.{ts,tsx}',
],
files: ['packages/docs/**/*'],
rules: {
// enforce import type
'@typescript-eslint/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/no-restricted-types': [
'warn',
{
types: {
// forbid FC as superfluous
FunctionComponent: {
message:
'Type the props argument and let TS infer or use ComponentType for a component prop',
},
FC: {
message:
'Type the props argument and let TS infer or use ComponentType for a component prop',
},
},
},
],
},
},
{
files: [
'packages/loot-core/src/types/**/*',
'packages/loot-core/src/client/state-types/**/*',
'**/icons/**/*',
'**/{mocks,__mocks__}/**/*',
// can't correctly resolve usages
'**/*.{testing,electron,browser,web,api}.ts',
],
rules: {
'import/no-unused-modules': 'off',
},
},
{
files: ['packages/api/migrations/*', 'packages/loot-core/migrations/*'],
rules: {
'import/no-default-export': 'off',
},
},
{
files: ['packages/api/index.ts'],
rules: {
'import/no-unresolved': 'off',
},
},
// Allow configuring vitest with default exports (recommended as per vitest docs)
{
files: [
'**/vitest.config.{ts,mts}',
'**/vitest.web.config.ts',
'**/vite.config.{ts,mts}',
'eslint.config.mjs',
],
rules: {
'import/no-anonymous-default-export': 'off',
'import/no-default-export': 'off',
},
},
{
// TODO: fix the issues in these files
files: [
'packages/desktop-client/src/components/accounts/Account.jsx',
'packages/desktop-client/src/components/accounts/MobileAccount.jsx',
'packages/desktop-client/src/components/accounts/MobileAccounts.jsx',
'packages/desktop-client/src/components/budget/BudgetCategories.jsx',
'packages/desktop-client/src/components/budget/BudgetSummaries.tsx',
'packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx',
'packages/desktop-client/src/components/budget/index.tsx',
'packages/desktop-client/src/components/budget/MobileBudget.tsx',
'packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx',
'packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx',
'packages/component-library/src/Menu.tsx',
'packages/desktop-client/src/components/FinancesApp.tsx',
'packages/desktop-client/src/components/GlobalKeys.ts',
'packages/desktop-client/src/components/LoggedInUser.tsx',
'packages/desktop-client/src/components/manager/ManagementApp.jsx',
'packages/desktop-client/src/components/manager/subscribe/common.tsx',
'packages/desktop-client/src/components/ManageRules.tsx',
'packages/desktop-client/src/components/mobile/MobileAmountInput.jsx',
'packages/desktop-client/src/components/mobile/MobileNavTabs.tsx',
'packages/desktop-client/src/components/Modals.tsx',
'packages/desktop-client/src/components/modals/EditRule.jsx',
'packages/desktop-client/src/components/modals/ImportTransactions.jsx',
'packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx',
'packages/desktop-client/src/components/Notifications.tsx',
'packages/desktop-client/src/components/payees/ManagePayees.jsx',
'packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx',
'packages/desktop-client/src/components/payees/PayeeTable.tsx',
'packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx',
'packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx',
'packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx',
'packages/desktop-client/src/components/reports/reports/CustomReport.jsx',
'packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx',
'packages/desktop-client/src/components/reports/SaveReportName.tsx',
'packages/desktop-client/src/components/reports/useReport.ts',
'packages/desktop-client/src/components/schedules/ScheduleDetails.jsx',
'packages/desktop-client/src/components/schedules/SchedulesTable.tsx',
'packages/desktop-client/src/components/select/DateSelect.tsx',
'packages/desktop-client/src/components/sidebar/Tools.tsx',
'packages/desktop-client/src/components/sort.tsx',
],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
{
files: [
'eslint.config.mjs',
'**/*.test.js',
'**/*.test.ts',
'**/*.test.jsx',
'**/*.test.tsx',
'**/*.spec.js',
],
rules: {
'actual/typography': 'off',
'actual/no-untranslated-strings': 'off',
'actual/prefer-logger-over-console': 'off',
},
},
{
files: [
'packages/desktop-client/**/*.{ts,tsx}',
'packages/loot-core/src/client/**/*.{ts,tsx}',
],
ignores: ['**/**/globals.d.ts'],
rules: {
// enforce type over interface
'@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
},
},
{
files: ['packages/sync-server/**/*'],
// TODO: fix the issues in these files
rules: {
'import/extensions': 'off',
'actual/typography': 'off',
},
},
{
files: ['packages/sync-server/src/app-gocardless/banks/*.js'],
rules: {
'import/no-anonymous-default-export': 'off',
'import/no-default-export': 'off',
'no-restricted-syntax': 'off',
},
},
);

View File

@@ -40,19 +40,20 @@
"build:plugins-service": "yarn workspace plugins-service build",
"build:api": "yarn workspace @actual-app/api build",
"build:docs": "yarn workspace docs build",
"deploy:docs": "yarn workspace docs deploy",
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
"test": "lage test --continue",
"test:debug": "lage test --no-cache --continue",
"e2e": "yarn workspace @actual-app/web run e2e",
"e2e:desktop": "yarn build:desktop --skip-exe-build && yarn workspace desktop-electron e2e",
"e2e:desktop": "yarn build:desktop --skip-exe-build --skip-translations && yarn workspace desktop-electron e2e",
"playwright": "yarn workspace @actual-app/web run playwright",
"vrt": "yarn workspace @actual-app/web run vrt",
"vrt:docker": "./bin/run-vrt",
"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",
"lint": "oxfmt --check . && oxlint --deny-warnings && eslint . --max-warnings 0",
"lint:fix": "oxfmt . && oxlint --deny-warnings --fix && eslint . --max-warnings 0 --fix",
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
"typecheck": "yarn tsc --incremental && tsc-strict",
"jq": "./node_modules/node-jq/bin/jq",
@@ -62,26 +63,22 @@
"@octokit/rest": "^22.0.1",
"@types/node": "^22.19.1",
"@types/prompts": "^2.4.9",
"@typescript-eslint/parser": "^8.46.4",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint": "^9.39.2",
"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": "^7.0.1",
"eslint-plugin-perfectionist": "^4.15.1",
"eslint-plugin-typescript-paths": "^0.0.33",
"globals": "^16.5.0",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"lage": "^2.14.15",
"lint-staged": "^16.2.6",
"lint-staged": "^16.2.7",
"minimatch": "^10.1.1",
"node-jq": "^6.3.1",
"npm-run-all": "^4.1.5",
"oxfmt": "^0.17.0",
"oxlint": "^1.32.0",
"p-limit": "^7.2.0",
"prettier": "^3.6.2",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
@@ -98,9 +95,12 @@
"yarn": "^4.9.1"
},
"lint-staged": {
"*.{js,mjs,jsx,ts,tsx,md,json,yml}": [
"eslint --fix",
"prettier --write"
"*.{js,mjs,jsx,ts,tsx,md,json,yml,yaml}": [
"oxfmt --no-error-on-unmatched-pattern"
],
"*.{js,mjs,jsx,ts,tsx}": [
"oxlint --deny-warnings --fix",
"eslint --max-warnings 0 --fix --no-warn-ignored"
]
},
"packageManager": "yarn@4.10.3",

View File

@@ -97,6 +97,14 @@ class Query {
serialize() {
return this.state;
}
reset() {
return q(this.state.table);
}
serializeAsString() {
return JSON.stringify(this.serialize());
}
}
export function q(table) {

View File

@@ -7,7 +7,6 @@ import type {
import type { InitConfig } from 'loot-core/server/main';
// @ts-ignore: bundle not available until we build it
// eslint-disable-next-line import/extensions
import * as bundle from './app/bundle.api.js';
import * as injected from './injected';
import { validateNodeVersion } from './validateNodeVersion';
@@ -44,7 +43,7 @@ export async function shutdown() {
if (actualApp) {
try {
await actualApp.send('sync');
} catch (e) {
} catch {
// most likely that no budget is loaded, so the sync failed
}
await actualApp.send('close-budget');

View File

@@ -125,10 +125,10 @@ export function addTransactions(
});
}
export interface ImportTransactionsOpts {
export type ImportTransactionsOpts = {
defaultCleared?: boolean;
dryRun?: boolean;
}
};
export function importTransactions(
accountId: APIAccountEntity['id'],

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "25.11.0",
"version": "25.12.0",
"license": "MIT",
"description": "An API for Actual",
"engines": {
@@ -19,7 +19,7 @@
"build:migrations": "cp migrations/*.sql dist/migrations",
"build:default-db": "cp default-db.sqlite dist/",
"build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
"test": "yarn run build:app && yarn run build:crdt && vitest --run",
"test": "yarn run clean && yarn run build:app && yarn run build:crdt && vitest --run",
"clean": "rm -rf dist @types"
},
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
// Using ES2021 because thats the newest version where
// Using ES2021 because that's the newest version where
// the latest Node 16.x release supports all of the features
"target": "ES2021",
"module": "CommonJS",
@@ -11,9 +11,9 @@
"outDir": "dist",
"declarationDir": "@types",
"paths": {
"loot-core/*": ["./@types/loot-core/src/*"]
}
"loot-core/*": ["./@types/loot-core/src/*"],
},
},
"include": ["."],
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"],
}

View File

@@ -1,5 +1,4 @@
// @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;

View File

@@ -22,7 +22,7 @@ function parseRawArgs(argv) {
if (!key?.startsWith('--')) {
throw new Error(
`Unexpected argument ${key ?? ''}. Use --key value pairs.`,
`Unexpected argument "${key ?? ''}". Use --key value pairs.`,
);
}
@@ -34,7 +34,7 @@ function parseRawArgs(argv) {
}
if (values.length === 0) {
throw new Error(`Missing value for argument ${key}.`);
throw new Error(`Missing value for argument "${key}".`);
}
const keyName = key.slice(2);
@@ -55,14 +55,14 @@ function getSingleValue(args, key) {
return undefined;
}
if (values.length !== 1) {
throw new Error(`Argument --${key} must have exactly one value.`);
throw new Error(`Argument "--${key}" must have exactly one value.`);
}
return values[0];
}
function parseMapping(values, key, description) {
if (!values || values.length === 0) {
throw new Error(`Missing required argument --${key} (${description}).`);
throw new Error(`Missing required argument "--${key}" (${description}).`);
}
if (values.length === 1) {
@@ -81,7 +81,7 @@ function parseMapping(values, key, description) {
Object.entries(parsed).map(([name, pathValue]) => {
if (typeof pathValue !== 'string') {
throw new Error(
`Value for ${name} in --${key} must be a string path.`,
`Value for "${name}" in "--${key}" must be a string path.`,
);
}
return [name, pathValue];
@@ -91,7 +91,7 @@ function parseMapping(values, key, description) {
const message =
error instanceof Error ? error.message : 'Unknown parsing error';
throw new Error(
`Failed to parse --${key} value as JSON object: ${message}`,
`Failed to parse "--${key}" value as JSON object: ${message}`,
);
}
}
@@ -104,7 +104,7 @@ function parseMapping(values, key, description) {
if (!rawName || rawPathParts.length === 0) {
throw new Error(
`Argument --${key} must be provided as name=path pairs or a JSON object.`,
`Argument "--${key}" must be provided as name=path pairs or a JSON object.`,
);
}
@@ -112,12 +112,12 @@ function parseMapping(values, key, description) {
const pathValue = rawPathParts.join('=').trim();
if (!name) {
throw new Error(`Argument --${key} contains an empty bundle name.`);
throw new Error(`Argument "--${key}" contains an empty bundle name.`);
}
if (!pathValue) {
throw new Error(
`Argument --${key} for bundle ${name} must include a non-empty path.`,
`Argument "--${key}" for bundle "${name}" must include a non-empty path.`,
);
}
@@ -125,7 +125,7 @@ function parseMapping(values, key, description) {
}
if (entries.size === 0) {
throw new Error(`Argument --${key} must define at least one bundle.`);
throw new Error(`Argument "--${key}" must define at least one bundle.`);
}
return entries;
@@ -152,7 +152,7 @@ function parseArgs(argv) {
if (!headPath) {
throw new Error(
`Bundle ${name} is missing a corresponding --head entry.`,
`Bundle "${name}" is missing a corresponding "--head" entry.`,
);
}
@@ -166,7 +166,7 @@ function parseArgs(argv) {
for (const name of headMap.keys()) {
if (!baseMap.has(name)) {
throw new Error(
`Bundle ${name} is missing a corresponding --base entry.`,
`Bundle "${name}" is missing a corresponding "--base" entry.`,
);
}
}
@@ -194,8 +194,8 @@ async function loadStats(filePath) {
error instanceof Error
? error.message
: 'Unknown error while parsing stats file';
console.error(`[bundle-stats] Failed to parse ${filePath}: ${message}`);
throw new Error(`Failed to load stats file ${filePath}: ${message}`);
console.error(`[bundle-stats] Failed to parse "${filePath}": ${message}`);
throw new Error(`Failed to load stats file "${filePath}": ${message}`);
}
}

View File

@@ -6,7 +6,6 @@
import fs from 'node:fs';
import { parseArgs } from 'node:util';
// eslint-disable-next-line import/extensions
import { getNextVersion } from '../src/versions/get-next-package-version.js';
const args = process.argv;

View File

@@ -26,12 +26,12 @@ function parseArgs(argv) {
if (!key?.startsWith('--')) {
throw new Error(
`Unexpected argument ${key ?? ''}. Use --key value pairs.`,
`Unexpected argument "${key ?? ''}". Use --key value pairs.`,
);
}
if (typeof value === 'undefined') {
throw new Error(`Missing value for argument ${key}.`);
throw new Error(`Missing value for argument "${key}".`);
}
switch (key) {
@@ -42,16 +42,16 @@ function parseArgs(argv) {
args.identifier = value;
break;
default:
throw new Error(`Unknown argument ${key}.`);
throw new Error(`Unknown argument "${key}".`);
}
}
if (!args.commentFile) {
throw new Error('Missing required argument --comment-file.');
throw new Error('Missing required argument "--comment-file".');
}
if (!args.identifier) {
throw new Error('Missing required argument --identifier.');
throw new Error('Missing required argument "--identifier".');
}
return args;
@@ -70,7 +70,7 @@ function getRepoInfo() {
const [owner, repo] = repository.split('/');
if (!owner || !repo) {
throw new Error(`Invalid GITHUB_REPOSITORY value ${repository}.`);
throw new Error(`Invalid GITHUB_REPOSITORY value "${repository}".`);
}
return { owner, repo };

View File

@@ -66,7 +66,7 @@ export function getNextVersion({
return `${nextVersionYear}.${nextVersionMonth}.0`;
default:
throw new Error(
'Invalid type specified. Use auto, nightly, hotfix, or monthly.',
'Invalid type specified. Use "auto", "nightly", "hotfix", or "monthly".',
);
}
}

View File

@@ -1,11 +1,11 @@
import { ChangeEvent, ReactNode } from 'react';
import { type ChangeEvent, type ReactNode } from 'react';
import {
ColorPicker as AriaColorPicker,
ColorPickerProps as AriaColorPickerProps,
type ColorPickerProps as AriaColorPickerProps,
Dialog,
DialogTrigger,
ColorSwatch as AriaColorSwatch,
ColorSwatchProps,
type ColorSwatchProps,
ColorSwatchPicker as AriaColorSwatchPicker,
ColorSwatchPickerItem,
ColorField,
@@ -56,10 +56,10 @@ const DEFAULT_COLOR_SET = [
'#455A64',
];
interface ColorSwatchPickerProps {
type ColorSwatchPickerProps = {
columns?: number;
colorset?: string[];
}
};
function ColorSwatchPicker({
columns = 5,
@@ -89,7 +89,6 @@ function ColorSwatchPicker({
cursor: 'pointer',
'&[data-selected]::after': {
// eslint-disable-next-line actual/typography
content: '""',
position: 'absolute',
inset: 0,
@@ -123,11 +122,11 @@ function ColorSwatchPicker({
}
const isColor = (value: string) => /^#[0-9a-fA-F]{6}$/.test(value);
interface ColorPickerProps extends AriaColorPickerProps {
type ColorPickerProps = {
children?: ReactNode;
columns?: number;
colorset?: string[];
}
} & AriaColorPickerProps;
export function ColorPicker({
children,

View File

@@ -3,8 +3,8 @@ import {
cloneElement,
isValidElement,
type ReactElement,
Ref,
RefObject,
type Ref,
type RefObject,
useEffect,
useRef,
} from 'react';

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { forwardRef, Ref } from 'react';
import { forwardRef, type Ref } from 'react';
import { render } from '@testing-library/react';

View File

@@ -1,6 +1,6 @@
import React, {
ChangeEvent,
ComponentPropsWithRef,
type ChangeEvent,
type ComponentPropsWithRef,
type KeyboardEvent,
type FocusEvent,
} from 'react';

View File

@@ -34,7 +34,7 @@ export const Popover = ({
return (
<ReactAriaPopover
data-popover={true}
data-popover
ref={ref}
placement="bottom end"
offset={1}

View File

@@ -36,6 +36,7 @@ export const Toggle = ({
})}
type="checkbox"
/>
{/* oxlint-disable-next-line eslint-plugin-jsx-a11y(label-has-associated-control) */}
<label
data-toggle-container
data-on={isOn}
@@ -62,7 +63,6 @@ export const Toggle = ({
data-on={isOn}
className={css(
{
// eslint-disable-next-line actual/typography
content: '" "',
position: 'absolute',
top: '2px',

View File

@@ -1,6 +1,9 @@
import { type Config } from '@svgr/core';
const tmpl: Config['template'] = ({ imports, interfaces, componentName, props, jsx }, { tpl }) => {
const tmpl: Config['template'] = (
{ imports, interfaces, componentName, props, jsx },
{ tpl },
) => {
return tpl`
${imports};

View File

@@ -3,7 +3,7 @@ import { keyframes } from '@emotion/css';
import { theme } from './theme';
import { tokens } from './tokens';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
export type CSSProperties = Record<string, any>;
const MOBILE_MIN_HEIGHT = 40;
@@ -12,7 +12,7 @@ const shadowLarge = {
boxShadow: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
export const styles: Record<string, any> = {
incomeHeaderHeight: 70,
cardShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
@@ -91,7 +91,6 @@ export const styles: Record<string, any> = {
},
shadowLarge,
tnum: {
// eslint-disable-next-line actual/typography
fontFeatureSettings: '"tnum"',
},
notFixed: { fontFeatureSettings: '' },

View File

@@ -1,6 +1,6 @@
# `@actual-app/crdt`
This package contains the core CRDT logic that enables Actuals syncing. It is shared between the client and server. We may or may not follow semver when updating this package; any usage of it outside Actual is undocumented and at your own risk.
This package contains the core CRDT logic that enables Actual's syncing. It is shared between the client and server. We may or may not follow semver when updating this package; any usage of it outside Actual is undocumented and at your own risk.
## protobuf
@@ -10,7 +10,7 @@ We use [protobuf](https://developers.google.com/protocol-buffers/) to encode mes
The protobuf is generated by using the [protoc](https://github.com/protocolbuffers/protobuf) compiler.
This can be installed by downloading one of the [pre-built binaries](https://github.com/protocolbuffers/protobuf/releases/) and placing it in your `$PATH`. The version used to build the current protobuf is [v3.20.1](https://github.com/protocolbuffers/protobuf/releases/tag/v3.20.1). Youll also need to [download the latest version of `protoc-gen-js`](https://github.com/protocolbuffers/protobuf-javascript/releases/latest). For convenience, you can put both of these binaries in `./bin`.
This can be installed by downloading one of the [pre-built binaries](https://github.com/protocolbuffers/protobuf/releases/) and placing it in your `$PATH`. The version used to build the current protobuf is [v3.20.1](https://github.com/protocolbuffers/protobuf/releases/tag/v3.20.1). You'll also need to [download the latest version of `protoc-gen-js`](https://github.com/protocolbuffers/protobuf-javascript/releases/latest). For convenience, you can put both of these binaries in `./bin`.
Once installed, the protobuf can be generated by running `./bin/generate-proto`.

View File

@@ -16,7 +16,7 @@ protoc --plugin="protoc-gen-ts=../../node_modules/.bin/protoc-gen-ts" \
--proto_path=src/proto \
sync.proto
../../node_modules/.bin/prettier --write src/proto/*.d.ts
../../node_modules/.bin/oxfmt src/proto/*.d.ts
for file in src/proto/*.d.ts; do
{ echo "/* eslint-disable @typescript-eslint/no-namespace */"; sed 's/export class/export declare class/g' "$file"; } > "${file%.d.ts}.ts"

View File

@@ -7,7 +7,7 @@
// * Need to check to make sure if account exists when handling
// * transaction changes in syncing
import { Timestamp } from './timestamp';
import { type Timestamp } from './timestamp';
/**
* Represents a node within a trinary radix trie.
@@ -88,7 +88,7 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
// where the hashes differ, or otherwise when there are no leaves
// left (this shouldn't happen, if that's the case the hash check at
// the top of this function should pass)
while (1) {
while (true) {
const keyset = new Set([...getKeys(node1), ...getKeys(node2)]);
const keys = [...keyset.values()];
keys.sort();
@@ -134,7 +134,7 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
node2 = node2[diffkey] || emptyTrie();
}
// eslint-disable-next-line no-unreachable
// oxlint-disable-next-line no-unreachable
return null;
}

View File

@@ -1,7 +1,7 @@
import murmurhash from 'murmurhash';
import { v4 as uuidv4 } from 'uuid';
import { TrieNode } from './merkle';
import { type TrieNode } from './merkle';
/**
* Hybrid Unique Logical Clock (HULC) timestamp generator
@@ -57,7 +57,7 @@ export function deserializeClock(clock: string): Clock {
let data;
try {
data = JSON.parse(clock);
} catch (e) {
} catch {
data = {
timestamp: '1970-01-01T00:00:00.000Z-0000-' + makeClientId(),
merkle: {},

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* oxlint-disable typescript/no-explicit-any */
import './proto/sync_pb.js'; // Import for side effects
export {

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-namespace */
/* oxlint-disable typescript/no-namespace */
// package:
// file: sync.proto

View File

@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
// Using ES2021 because thats the newest version where
// Using ES2021 because that's the newest version where
// the latest Node 16.x release supports all of the features
"target": "ES2021",
"module": "CommonJS",
@@ -9,8 +9,8 @@
"noEmit": false,
"declaration": true,
"strict": true,
"outDir": "dist"
"outDir": "dist",
},
"include": ["."],
"exclude": ["dist", "**/*.test.ts", "**/*.spec.ts"]
"exclude": ["dist", "**/*.test.ts", "**/*.spec.ts"],
}

View File

@@ -7,6 +7,7 @@ node_modules
coverage
test-results
playwright-report
blob-report
# production
build

View File

@@ -23,7 +23,7 @@ test.describe('Mobile Accounts', () => {
});
test.afterEach(async () => {
await page.close();
await page?.close();
});
test('opens the accounts page and asserts on balances', async () => {

View File

@@ -23,7 +23,7 @@ test.describe('Accounts', () => {
});
test.afterEach(async () => {
await page.close();
await page?.close();
});
test('creates a new account and views the initial balance transaction', async () => {

View File

@@ -28,7 +28,7 @@ test.describe('Mobile Bank Sync', () => {
});
test.afterEach(async () => {
await page.close();
await page?.close();
});
test('checks the page visuals', async () => {

View File

@@ -11,23 +11,21 @@ test.describe('Bank Sync', () => {
let bankSyncPage: BankSyncPage;
let configurationPage: ConfigurationPage;
test.beforeAll(async ({ browser }) => {
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
navigation = new Navigation(page);
configurationPage = new ConfigurationPage(page);
await page.goto('/');
await configurationPage.createTestFile();
});
test.afterAll(async () => {
await page.close();
});
test.beforeEach(async () => {
bankSyncPage = await navigation.goToBankSyncPage();
});
test.afterEach(async () => {
await page?.close();
});
test('checks the page visuals', async () => {
await bankSyncPage.waitToLoad();
await expect(page).toMatchThemeScreenshots();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -9,7 +9,7 @@ test.describe('Budget', () => {
let configurationPage: ConfigurationPage;
let budgetPage: BudgetPage;
test.beforeAll(async ({ browser }) => {
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
configurationPage = new ConfigurationPage(page);
@@ -22,8 +22,8 @@ test.describe('Budget', () => {
await page.mouse.move(0, 0);
});
test.afterAll(async () => {
await page.close();
test.afterEach(async () => {
await page?.close();
});
test('renders the summary information: available funds, overspent, budgeted and for next month', async () => {

View File

@@ -7,7 +7,7 @@ test.describe('Command bar', () => {
let page: Page;
let configurationPage: ConfigurationPage;
test.beforeAll(async ({ browser }) => {
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
configurationPage = new ConfigurationPage(page);
@@ -26,8 +26,8 @@ test.describe('Command bar', () => {
});
});
test.afterAll(async () => {
await page.close();
test.afterEach(async () => {
await page?.close();
});
test('Check the command bar visuals', async () => {
@@ -59,7 +59,9 @@ test.describe('Command bar', () => {
await commandBar.fill('reports');
await page.keyboard.press('Enter');
await expect(page.getByTestId('reports-page')).toBeVisible();
await expect(page.getByText('Loading reports...')).not.toBeVisible(); // wait for screen to load
await expect(page.getByText('Loading reports...')).not.toBeVisible({
timeout: 10000, // Wait for 10 seconds max for reports to load
}); // wait for screen to load
// Navigate to schedule page
await page.keyboard.press('ControlOrMeta+k');

View File

@@ -14,7 +14,6 @@ export const expect = baseExpect.extend({
}
const config = {
// eslint-disable-next-line actual/typography
mask: [locator.locator('[data-vrt-mask="true"]')],
maxDiffPixels: 5,
};

View File

@@ -7,7 +7,7 @@ test.describe('Help menu', () => {
let page: Page;
let configurationPage: ConfigurationPage;
test.beforeAll(async ({ browser }) => {
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
configurationPage = new ConfigurationPage(page);
@@ -20,8 +20,8 @@ test.describe('Help menu', () => {
await page.mouse.move(0, 0);
});
test.afterAll(async () => {
await page.close();
test.afterEach(async () => {
await page?.close();
});
test('Check the help menu visuals', async () => {

View File

@@ -21,11 +21,11 @@ test.describe('Onboarding', () => {
});
test.afterEach(async () => {
await page.close();
await page?.close();
});
test('checks the page visuals', async () => {
await expect(configurationPage.heading).toHaveText('Wheres the server?');
await expect(configurationPage.heading).toHaveText("Where's the server?");
await expect(page).toMatchThemeScreenshots();
await configurationPage.clickOnNoServer();
@@ -92,7 +92,7 @@ test.describe('Onboarding', () => {
await expect(accountPage.accountBalance).toHaveText('0.00');
});
test('navigates back to start page by clicking on no server in an empty budget file', async () => {
test('navigates back to start page by clicking on "no server" in an empty budget file', async () => {
await configurationPage.clickOnNoServer();
const accountPage = await configurationPage.startFresh();
@@ -101,6 +101,6 @@ test.describe('Onboarding', () => {
await navigation.clickOnNoServer();
await page.getByRole('button', { name: 'Start using a server' }).click();
await expect(configurationPage.heading).toHaveText('Wheres the server?');
await expect(configurationPage.heading).toHaveText("Where's the server?");
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 KiB

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 KiB

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 581 KiB

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 580 KiB

View File

@@ -19,7 +19,7 @@ export class ConfigurationPage {
}
async clickOnNoServer() {
await this.page.getByRole('button', { name: 'Dont use a server' }).click();
await this.page.getByRole('button', { name: "Don't use a server" }).click();
}
async startFresh() {

View File

@@ -18,7 +18,7 @@ export class BudgetMenuModal {
this.heading = locator.getByRole('heading');
this.budgetAmountInput = locator.getByTestId('amount-input');
this.copyLastMonthBudgetButton = locator.getByRole('button', {
name: 'Copy last months budget',
name: "Copy last month's budget",
});
this.setTo3MonthAverageButton = locator.getByRole('button', {
name: 'Set to 3 month average',
@@ -30,7 +30,7 @@ export class BudgetMenuModal {
name: 'Set to yearly average',
});
this.applyBudgetTemplateButton = locator.getByRole('button', {
name: 'Apply budget template',
name: 'Overwrite with template',
});
}

View File

@@ -8,7 +8,7 @@ import { EnvelopeBudgetSummaryModal } from './mobile-envelope-budget-summary-mod
import { TrackingBudgetSummaryModal } from './mobile-tracking-budget-summary-modal';
export class MobileBudgetPage {
readonly MONTH_HEADER_DATE_FORMAT = 'MMMM yy';
readonly MONTH_HEADER_DATE_FORMAT = "MMMM ''yy";
readonly page: Page;
readonly heading: Locator;
@@ -325,7 +325,7 @@ export class MobileBudgetPage {
}
throw new Error(
'Neither To Budget nor Overbudgeted button could be located on the page.',
'Neither "To Budget" nor "Overbudgeted" button could be located on the page.',
);
}
@@ -363,7 +363,7 @@ export class MobileBudgetPage {
}
throw new Error(
'None of Saved, Projected savings, or Overspent buttons could be located on the page.',
'None of "Saved", "Projected savings", or "Overspent" buttons could be located on the page.',
);
}

View File

@@ -7,6 +7,7 @@ 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 { MobileSchedulesPage } from './mobile-schedules-page';
import { MobileTransactionEntryPage } from './mobile-transaction-entry-page';
import { SettingsPage } from './settings-page';
@@ -24,6 +25,7 @@ const ROUTES_BY_PAGE = {
Accounts: '/accounts',
Transaction: '/transactions/new',
Reports: '/reports',
Schedules: '/schedules',
Payees: '/payees',
Rules: '/rules',
'Bank Sync': '/bank-sync',
@@ -178,6 +180,13 @@ export class MobileNavigation {
);
}
async goToSchedulesPage() {
return this.navigateToPage(
'Schedules',
() => new MobileSchedulesPage(this.page),
);
}
async goToRulesPage() {
return await this.navigateToPage(
'Rules',

View File

@@ -0,0 +1,86 @@
import { type Locator, type Page } from '@playwright/test';
export class MobileSchedulesPage {
readonly page: Page;
readonly searchBox: Locator;
readonly addButton: Locator;
readonly schedulesList: Locator;
readonly emptyMessage: Locator;
readonly loadingIndicator: Locator;
constructor(page: Page) {
this.page = page;
this.searchBox = page.getByPlaceholder('Filter schedules…');
this.addButton = page.getByRole('button', { name: 'Add new schedule' });
this.schedulesList = page.getByRole('grid', { name: 'Schedules' });
this.emptyMessage = page.getByText(
'No schedules found. Create your first schedule to get started!',
);
this.loadingIndicator = page.getByTestId('animated-loading');
}
async waitFor(options?: {
state?: 'attached' | 'detached' | 'visible' | 'hidden';
timeout?: number;
}) {
await this.schedulesList.waitFor(options);
}
/**
* Search for schedules 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 schedule item (0-based index)
*/
getNthSchedule(index: number) {
return this.getAllSchedules().nth(index);
}
/**
* Get all visible schedule items
*/
getAllSchedules() {
return this.schedulesList.getByRole('gridcell');
}
/**
* Click on a schedule to open the edit page
*/
async clickSchedule(index: number) {
const schedule = this.getNthSchedule(index);
await schedule.click();
}
/**
* Click the add button to create a new schedule
*/
async clickAddSchedule() {
await this.addButton.click();
}
/**
* Get the number of visible schedules
*/
async getScheduleCount() {
const schedules = this.getAllSchedules();
return await schedules.count();
}
/**
* Wait for loading to complete
*/
async waitForLoadingToComplete(timeout: number = 10000) {
await this.loadingIndicator.waitFor({ state: 'hidden', timeout });
}
}

View File

@@ -30,7 +30,7 @@ test.describe('Mobile Payees', () => {
});
test.afterEach(async () => {
await page.close();
await page?.close();
});
test('checks the page visuals', async () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Some files were not shown because too many files have changed in this diff Show More