Merge branch 'master' into fix-issue-1253
5
.coderabbit.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
issue_enrichment:
|
||||
auto_enrich:
|
||||
enabled: false
|
||||
reviews:
|
||||
review_status: false
|
||||
2
.gitattributes
vendored
@@ -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.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -26,7 +26,7 @@ body:
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
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.
|
||||
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:
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -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 you’re 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 you’re 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 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 :)
|
||||
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 — 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.)
|
||||
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
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
1
.github/actions/check-migrations.js
vendored
@@ -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');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
1
.github/actions/docs-spelling/excludes.txt
vendored
@@ -72,5 +72,6 @@ ignore$
|
||||
(?:^|/)yarn\.lock$
|
||||
(?:^|/)(?i)docusaurus.config.js
|
||||
(?:^|/)(?i)README.md
|
||||
(?:^|/)(?i).nojekyll
|
||||
^\static/
|
||||
\.tsx$
|
||||
|
||||
2
.github/actions/docs-spelling/expect.txt
vendored
@@ -44,6 +44,7 @@ crt
|
||||
Danske
|
||||
datadir
|
||||
Depositos
|
||||
deselection
|
||||
DIREKT
|
||||
Dockerfiles
|
||||
Dominguez
|
||||
@@ -113,6 +114,7 @@ Qatari
|
||||
QNTOFRP
|
||||
QONTO
|
||||
Raiffeisen
|
||||
REGEXREPLACE
|
||||
revolut
|
||||
RIED
|
||||
RSchedule
|
||||
|
||||
40
.github/actions/netlify-wait-for-build
vendored
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
2
.github/workflows/autofix.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/count-points.yml
vendored
@@ -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
@@ -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
|
||||
106
.github/workflows/e2e-test.yml
vendored
@@ -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
@@ -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.
|
||||
73
.github/workflows/electron-master.yml
vendored
@@ -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
|
||||
|
||||
18
.github/workflows/electron-pr.yml
vendored
@@ -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
|
||||
|
||||
@@ -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+
|
||||
|
||||
Don’t forget to upvote the top comment with 👍!
|
||||
Don't forget to upvote the top comment with 👍!
|
||||
|
||||
<!-- feature-auto-close-comment -->
|
||||
- name: Close Issue
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
8
.github/workflows/size-compare.yml
vendored
@@ -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 }}
|
||||
|
||||
20
.github/workflows/vrt-update-apply.yml
vendored
@@ -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
|
||||
|
||||
77
.github/workflows/vrt-update-generate.yml
vendored
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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/*
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
24
AGENTS.md
@@ -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
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
30
package.json
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -125,10 +125,10 @@ export function addTransactions(
|
||||
});
|
||||
}
|
||||
|
||||
export interface ImportTransactionsOpts {
|
||||
export type ImportTransactionsOpts = {
|
||||
defaultCleared?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export function importTransactions(
|
||||
accountId: APIAccountEntity['id'],
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// Using ES2021 because that’s 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"],
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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".',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
Ref,
|
||||
RefObject,
|
||||
type Ref,
|
||||
type RefObject,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
ComponentPropsWithRef,
|
||||
type ChangeEvent,
|
||||
type ComponentPropsWithRef,
|
||||
type KeyboardEvent,
|
||||
type FocusEvent,
|
||||
} from 'react';
|
||||
|
||||
@@ -34,7 +34,7 @@ export const Popover = ({
|
||||
|
||||
return (
|
||||
<ReactAriaPopover
|
||||
data-popover={true}
|
||||
data-popover
|
||||
ref={ref}
|
||||
placement="bottom end"
|
||||
offset={1}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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: '' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `@actual-app/crdt`
|
||||
|
||||
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.
|
||||
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). 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`.
|
||||
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`.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
/* oxlint-disable typescript/no-namespace */
|
||||
// package:
|
||||
// file: sync.proto
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// Using ES2021 because that’s 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"],
|
||||
}
|
||||
|
||||
1
packages/desktop-client/.gitignore
vendored
@@ -7,6 +7,7 @@ node_modules
|
||||
coverage
|
||||
test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
|
||||
# production
|
||||
build
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -28,7 +28,7 @@ test.describe('Mobile Bank Sync', () => {
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await page.close();
|
||||
await page?.close();
|
||||
});
|
||||
|
||||
test('checks the page visuals', async () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
@@ -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 () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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('Where’s 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('Where’s the server?');
|
||||
await expect(configurationPage.heading).toHaveText("Where's the server?");
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 444 KiB After Width: | Height: | Size: 467 KiB |
|
Before Width: | Height: | Size: 514 KiB After Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 507 KiB After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 511 KiB After Width: | Height: | Size: 530 KiB |
|
Before Width: | Height: | Size: 581 KiB After Width: | Height: | Size: 591 KiB |
|
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 580 KiB |
@@ -19,7 +19,7 @@ export class ConfigurationPage {
|
||||
}
|
||||
|
||||
async clickOnNoServer() {
|
||||
await this.page.getByRole('button', { name: 'Don’t use a server' }).click();
|
||||
await this.page.getByRole('button', { name: "Don't use a server" }).click();
|
||||
}
|
||||
|
||||
async startFresh() {
|
||||
|
||||
@@ -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 month’s 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',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ test.describe('Mobile Payees', () => {
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await page.close();
|
||||
await page?.close();
|
||||
});
|
||||
|
||||
test('checks the page visuals', async () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |