Compare commits
139 Commits
v26.1.0
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32498b0026 | ||
|
|
f5896fe770 | ||
|
|
dfb6612333 | ||
|
|
fcf42bef63 | ||
|
|
ee2ed81c0a | ||
|
|
0d6742664b | ||
|
|
37e807f161 | ||
|
|
c9abc62b3a | ||
|
|
234f257260 | ||
|
|
49d583e4ad | ||
|
|
460cb7b6cd | ||
|
|
1eb68c8e19 | ||
|
|
2ec592c2d6 | ||
|
|
19edbeb5c2 | ||
|
|
d8b7e45aaa | ||
|
|
f71249f510 | ||
|
|
ebf8e985ad | ||
|
|
690e2d0871 | ||
|
|
91faa2f7f1 | ||
|
|
4888d3faed | ||
|
|
3091320719 | ||
|
|
72304c6182 | ||
|
|
e4903ca6e3 | ||
|
|
d768cfa508 | ||
|
|
e0ed53c4af | ||
|
|
c3e3a258e0 | ||
|
|
f55a42d817 | ||
|
|
331aafda30 | ||
|
|
be95b9a3d5 | ||
|
|
8eadd09bfc | ||
|
|
414aa95fd8 | ||
|
|
19ed2423d4 | ||
|
|
4986e433a5 | ||
|
|
9a3415adab | ||
|
|
bcfefde4ad | ||
|
|
c4514b1fe6 | ||
|
|
7a5bfe7c20 | ||
|
|
323403b5f7 | ||
|
|
a35fdf4d18 | ||
|
|
d0a72f10b6 | ||
|
|
b651238ad2 | ||
|
|
8d1f0cf1de | ||
|
|
b50c45c9c9 | ||
|
|
8f2369d5b8 | ||
|
|
713ac88fee | ||
|
|
bcbcd6ad9f | ||
|
|
c9a0ffa91c | ||
|
|
6a9df6562c | ||
|
|
d4144f4b9c | ||
|
|
176336e7f3 | ||
|
|
de0f4e9440 | ||
|
|
65dee4c627 | ||
|
|
9376217c5e | ||
|
|
e6e108ffbd | ||
|
|
517b1b4a81 | ||
|
|
cdae09e554 | ||
|
|
59233c4786 | ||
|
|
502babd310 | ||
|
|
3c302b3af9 | ||
|
|
4c2d3e6998 | ||
|
|
e79d91b000 | ||
|
|
2457a0b454 | ||
|
|
84a1d12dae | ||
|
|
b50d48a31a | ||
|
|
9289932af9 | ||
|
|
fe624f0158 | ||
|
|
7443886856 | ||
|
|
58b1420c60 | ||
|
|
7035b32f26 | ||
|
|
ee0e7ed3e0 | ||
|
|
b6452f930b | ||
|
|
bf814a6873 | ||
|
|
81c5dd347f | ||
|
|
cd87d5a899 | ||
|
|
f5e1d5eab4 | ||
|
|
6bf119786c | ||
|
|
14d4c7748d | ||
|
|
edf61a477a | ||
|
|
0724f7eaef | ||
|
|
d01d0eacb8 | ||
|
|
bb70074f35 | ||
|
|
5860b95c9c | ||
|
|
a58bac6de6 | ||
|
|
d01f38d480 | ||
|
|
4b1f127910 | ||
|
|
f5377ac7b7 | ||
|
|
826ad86ada | ||
|
|
c1720f35fd | ||
|
|
93cce07542 | ||
|
|
b88feb9336 | ||
|
|
0c3a515e29 | ||
|
|
5d82435700 | ||
|
|
bcf53007ca | ||
|
|
64aa88aa22 | ||
|
|
6d8cba1564 | ||
|
|
914074d4b6 | ||
|
|
2b8a15f51c | ||
|
|
0467b13848 | ||
|
|
843e957757 | ||
|
|
073725e270 | ||
|
|
0e20e17fa4 | ||
|
|
f1fd99eeac | ||
|
|
cf3a42792f | ||
|
|
d41af58daf | ||
|
|
25ee19c1e1 | ||
|
|
fed1cd7d30 | ||
|
|
da1a2457ba | ||
|
|
5673ca5049 | ||
|
|
4b8feb1dfc | ||
|
|
771e5072cb | ||
|
|
fd42705c75 | ||
|
|
8a1e1923dd | ||
|
|
d30f3aa36d | ||
|
|
36a3586648 | ||
|
|
ea551608df | ||
|
|
30953d3d9f | ||
|
|
9e7a4cde36 | ||
|
|
3966778de3 | ||
|
|
a9509f477f | ||
|
|
a2a82e90d6 | ||
|
|
acc21f1762 | ||
|
|
4055725106 | ||
|
|
31349b7bc8 | ||
|
|
5f1c13e25c | ||
|
|
fa45342d8d | ||
|
|
d52f920e98 | ||
|
|
97c2854825 | ||
|
|
6a9e173a1a | ||
|
|
bf505c2bd5 | ||
|
|
64db9a59f4 | ||
|
|
7f3aa3a2b5 | ||
|
|
929131b776 | ||
|
|
d9c759ff1b | ||
|
|
52559eb221 | ||
|
|
3e565fa513 | ||
|
|
f1faf45659 | ||
|
|
88fbfe7078 | ||
|
|
235d771d58 | ||
|
|
384128ec50 |
@@ -2,7 +2,25 @@ issue_enrichment:
|
||||
auto_enrich:
|
||||
enabled: false
|
||||
reviews:
|
||||
request_changes_workflow: true
|
||||
review_status: false
|
||||
high_level_summary: false
|
||||
finishing_touches:
|
||||
docstrings:
|
||||
enabled: false
|
||||
pre_merge_checks:
|
||||
docstrings:
|
||||
mode: off
|
||||
enabled: false
|
||||
labeling_instructions:
|
||||
- label: 'suspect ai generated'
|
||||
instructions: 'This issue or PR is suspected to be generated by AI.'
|
||||
- label: 'API'
|
||||
instructions: 'This issue or PR updates the API in `packages/api`.'
|
||||
- label: 'documentation'
|
||||
instructions: 'This issue updates the documentation in `packages/docs`.'
|
||||
- label: 'contains DB migrations'
|
||||
instructions: 'This issue or PR contains DB migrations in `packages/loot-core/migrations`.'
|
||||
- label: 'size small'
|
||||
instructions: 'This issue or PR is a small change (less than 50 lines of code) that is expected to have a small impact on the codebase.'
|
||||
auto_apply_labels: true
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import fs from 'fs';
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import fs from 'fs';
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
|
||||
const commentBody = process.env.GITHUB_EVENT_COMMENT_BODY;
|
||||
const prDetailsJson = process.env.PR_DETAILS;
|
||||
@@ -33,11 +33,11 @@ try {
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfix", or "Maintenance". No other text or explanation.',
|
||||
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfixes", or "Maintenance". No other text or explanation.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfix: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
|
||||
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfixes: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
|
||||
},
|
||||
],
|
||||
max_tokens: 10,
|
||||
@@ -86,7 +86,7 @@ try {
|
||||
// Validate the category response
|
||||
const validCategories = [
|
||||
'Features',
|
||||
'Bugfix',
|
||||
'Bugfixes',
|
||||
'Enhancements',
|
||||
'Maintenance',
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
|
||||
const commentBody = process.env.GITHUB_EVENT_COMMENT_BODY;
|
||||
const prDetailsJson = process.env.PR_DETAILS;
|
||||
@@ -71,7 +71,7 @@ try {
|
||||
console.log('Generated summary:', summary);
|
||||
|
||||
const result = {
|
||||
summary: summary,
|
||||
summary,
|
||||
prNumber: prDetails.number,
|
||||
author: prDetails.author,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import fs from 'fs';
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
|
||||
@@ -35,11 +36,13 @@ async function getPRDetails() {
|
||||
console.log('- PR Number:', pr.number);
|
||||
console.log('- PR Author:', pr.user.login);
|
||||
console.log('- PR Title:', pr.title);
|
||||
console.log('- Base Branch:', pr.base.ref);
|
||||
|
||||
const result = {
|
||||
number: pr.number,
|
||||
author: pr.user.login,
|
||||
title: pr.title,
|
||||
baseBranch: pr.base.ref,
|
||||
};
|
||||
|
||||
setOutput('result', JSON.stringify(result));
|
||||
|
||||
13
.github/actions/check-migrations.js
vendored
@@ -4,8 +4,8 @@
|
||||
// 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 path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const migrationsDir = path.join(
|
||||
__dirname,
|
||||
@@ -35,14 +35,15 @@ function readMigrations(ref) {
|
||||
}
|
||||
|
||||
spawnSync('git', ['fetch', 'origin', 'master']);
|
||||
let masterMigrations = readMigrations('origin/master');
|
||||
let headMigrations = readMigrations('HEAD');
|
||||
const masterMigrations = readMigrations('origin/master');
|
||||
const headMigrations = readMigrations('HEAD');
|
||||
|
||||
let latestMasterMigration = masterMigrations[masterMigrations.length - 1].date;
|
||||
let newMigrations = headMigrations.filter(
|
||||
const latestMasterMigration =
|
||||
masterMigrations[masterMigrations.length - 1].date;
|
||||
const newMigrations = headMigrations.filter(
|
||||
migration => !masterMigrations.find(m => m.name === migration.name),
|
||||
);
|
||||
let badMigrations = newMigrations.filter(
|
||||
const badMigrations = newMigrations.filter(
|
||||
migration => migration.date <= latestMasterMigration,
|
||||
);
|
||||
|
||||
|
||||
10
.github/actions/docs-spelling/expect.txt
vendored
@@ -5,6 +5,7 @@ Activo
|
||||
AESUDEF
|
||||
ALZEY
|
||||
Anglais
|
||||
ANZ
|
||||
aql
|
||||
AUR
|
||||
Authentik
|
||||
@@ -38,6 +39,7 @@ Cloudflare
|
||||
CMCIFRPAXXX
|
||||
COBADEFF
|
||||
CODEOWNERS
|
||||
COEP
|
||||
commerzbank
|
||||
Copiar
|
||||
COUNTA
|
||||
@@ -45,6 +47,7 @@ COUNTBLANK
|
||||
countif
|
||||
CREGBEBB
|
||||
crt
|
||||
CZK
|
||||
Danske
|
||||
datadir
|
||||
DATEDIF
|
||||
@@ -66,7 +69,6 @@ Fineco
|
||||
Finicity
|
||||
Fintro
|
||||
Finverse
|
||||
flathub
|
||||
Flathub
|
||||
FORTUNEO
|
||||
FTNOFRP
|
||||
@@ -81,6 +83,7 @@ HABAL
|
||||
Hampel
|
||||
HELADEF
|
||||
HLOOKUP
|
||||
HUF
|
||||
IFERROR
|
||||
IFNA
|
||||
INDUSTRIEL
|
||||
@@ -114,12 +117,14 @@ LKR
|
||||
MAXA
|
||||
mbank
|
||||
mdc
|
||||
metainfo
|
||||
modals
|
||||
Moldovan
|
||||
murmurhash
|
||||
NETWORKDAYS
|
||||
nginx
|
||||
OIDC
|
||||
Okabe
|
||||
overbudgeted
|
||||
overbudgeting
|
||||
oxc
|
||||
@@ -132,7 +137,6 @@ prefs
|
||||
Primoco
|
||||
Priotecs
|
||||
proactively
|
||||
pwa
|
||||
Qatari
|
||||
QNTOFRP
|
||||
QONTO
|
||||
@@ -170,12 +174,14 @@ touchscreen
|
||||
triaging
|
||||
UAH
|
||||
ubuntu
|
||||
undici
|
||||
userinfo
|
||||
Userscripts
|
||||
UZS
|
||||
VLOOKUP
|
||||
vrt
|
||||
VUB
|
||||
Wallos
|
||||
websecure
|
||||
WEEKNUM
|
||||
Widiba
|
||||
|
||||
2
.github/scripts/count-points.mjs
vendored
@@ -278,7 +278,7 @@ async function countContributorPoints() {
|
||||
|
||||
if (
|
||||
event.event === 'closed' &&
|
||||
event.state_reason === 'not_planned'
|
||||
['not_planned', 'duplicate'].includes(event.state_reason)
|
||||
) {
|
||||
const closer = event.actor.login;
|
||||
const userStats = stats.get(closer);
|
||||
|
||||
15
.github/workflows/ai-generated-release-notes.yml
vendored
@@ -41,8 +41,21 @@ jobs:
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
||||
- name: Check if release notes file already exists
|
||||
- name: Check if PR targets master branch
|
||||
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null'
|
||||
id: check-base-branch
|
||||
run: |
|
||||
BASE_BRANCH=$(echo '${{ steps.pr-details.outputs.result }}' | jq -r '.baseBranch')
|
||||
echo "Base branch: $BASE_BRANCH"
|
||||
if [ "$BASE_BRANCH" = "master" ]; then
|
||||
echo "targets_master=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "targets_master=false" >> $GITHUB_OUTPUT
|
||||
echo "PR does not target master branch, skipping release notes generation"
|
||||
fi
|
||||
|
||||
- name: Check if release notes file already exists
|
||||
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null' && steps.check-base-branch.outputs.targets_master == 'true'
|
||||
id: check-release-notes-exists
|
||||
run: node .github/actions/ai-generated-release-notes/check-release-notes-exists.js
|
||||
env:
|
||||
|
||||
32
.github/workflows/docs-release.yml
vendored
@@ -1,32 +0,0 @@
|
||||
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
|
||||
9
.github/workflows/e2e-test.yml
vendored
@@ -12,6 +12,7 @@ on:
|
||||
- '!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
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
GITHUB_PR_NUMBER: ${{github.event.pull_request.number}}
|
||||
@@ -29,7 +30,7 @@ jobs:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
@@ -52,7 +53,7 @@ jobs:
|
||||
name: Functional Desktop App
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
@@ -80,7 +81,7 @@ jobs:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
@@ -103,7 +104,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !cancelled() }}
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
|
||||
9
.github/workflows/electron-master.yml
vendored
@@ -52,14 +52,13 @@ 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
|
||||
sudo flatpak install org.flatpak.Builder -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"
|
||||
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron for Mac
|
||||
@@ -171,8 +170,8 @@ jobs:
|
||||
- name: Calculate AppImage SHA256
|
||||
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 }')
|
||||
APPIMAGE_X64_SHA256=$(sha256sum Actual-linux-x86_64.AppImage | awk '{ print $1 }')
|
||||
APPIMAGE_ARM64_SHA256=$(sha256sum Actual-linux-arm64.AppImage | awk '{ print $1 }')
|
||||
echo "APPIMAGE_X64_SHA256=$APPIMAGE_X64_SHA256" >> "$GITHUB_ENV"
|
||||
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
|
||||
|
||||
|
||||
5
.github/workflows/electron-pr.yml
vendored
@@ -52,14 +52,13 @@ 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
|
||||
sudo flatpak install org.flatpak.Builder -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"
|
||||
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
|
||||
48
.github/workflows/fork-pr-welcome.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Fork PR Welcome
|
||||
|
||||
##########################################################################################
|
||||
# WARNING! This workflow uses the 'pull_request_target' event. That means that it will #
|
||||
# always run in the context of the main actualbudget/actual repo, even if the PR is from #
|
||||
# a fork. This is necessary to get access to a GitHub token that can post a comment on #
|
||||
# the PR. Be VERY CAREFUL about adding things to this workflow, since forks can inject #
|
||||
# arbitrary code into their branch, and can pollute the artifacts we download. Arbitrary #
|
||||
# code execution in this workflow could lead to a compromise of the main repo. #
|
||||
##########################################################################################
|
||||
# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests #
|
||||
##########################################################################################
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
name: Post Welcome Message
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Post welcome comment
|
||||
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ github.event.pull_request.number }}
|
||||
header: fork-pr-welcome
|
||||
hide_and_recreate: true
|
||||
hide_classify: OUTDATED
|
||||
message: |
|
||||
<!-- fork-pr-welcome -->
|
||||
👋 Hello contributor!
|
||||
|
||||
We would love to review your PR! Before we can do that, please make sure:
|
||||
|
||||
- ✅ All CI checks pass
|
||||
- ✅ The PR is moved from draft to open (if applicable)
|
||||
- ✅ The "[WIP]" prefix is removed from the PR title
|
||||
- ✅ All CodeRabbit code review comments are resolved (if you disagree with anything - reply to the bot with your reasoning so we can read through it). The bot will eventually approve the PR.
|
||||
|
||||
We do this to reduce the TOIL the core contributor team has to go through for each PR and to allow for speedy reviews and merges.
|
||||
|
||||
For more information, please see our [Contributing Guide](https://actualbudget.org/docs/contributing/).
|
||||
6
.github/workflows/generate-release-pr.yml
vendored
@@ -35,7 +35,10 @@ jobs:
|
||||
pkg="${packages[$key]}"
|
||||
|
||||
if [[ -n "${{ github.event.inputs.version }}" ]]; then
|
||||
version="${{ github.event.inputs.version }}"
|
||||
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
|
||||
--package-json "./packages/$pkg/package.json" \
|
||||
--version "${{ github.event.inputs.version }}" \
|
||||
--update)
|
||||
else
|
||||
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
|
||||
--package-json "./packages/$pkg/package.json" \
|
||||
@@ -50,6 +53,7 @@ jobs:
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
|
||||
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
title: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
body: 'Generated by [generate-release-pr.yml](../tree/master/.github/workflows/generate-release-pr.yml)'
|
||||
|
||||
25
.github/workflows/pr-ai-label-cleanup.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Remove 'suspect ai generated' label when 'AI generated' is present
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
remove-suspect-label:
|
||||
if: >-
|
||||
${{ contains(github.event.pull_request.labels.*.name, 'AI generated') &&
|
||||
contains(github.event.pull_request.labels.*.name, 'suspect ai generated') }}
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'suspect ai generated'
|
||||
});
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.event.repository.fork == false
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
@@ -48,14 +49,13 @@ 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
|
||||
sudo flatpak install org.flatpak.Builder -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"
|
||||
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Publish nightly npm packages
|
||||
|
||||
# Nightly npm packages are built daily
|
||||
# Nightly npm packages are built daily at midnight UTC
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
6
.github/workflows/size-compare.yml
vendored
@@ -139,7 +139,8 @@ jobs:
|
||||
--head desktop-client=./head/web-stats.json \
|
||||
--head loot-core=./head/loot-core-stats.json \
|
||||
--head api=./head/api-stats.json \
|
||||
--identifier combined > bundle-stats-comment.md
|
||||
--identifier combined \
|
||||
--format pr-body > bundle-stats-comment.md
|
||||
- name: Post combined bundle stats comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -148,4 +149,5 @@ jobs:
|
||||
run: |
|
||||
node packages/ci-actions/bin/update-bundle-stats-comment.mjs \
|
||||
--comment-file bundle-stats-comment.md \
|
||||
--identifier '<!--- bundlestats-action-comment key:combined --->'
|
||||
--identifier combined \
|
||||
--target pr-body
|
||||
|
||||
2
.github/workflows/vrt-update-generate.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/update-vrt')
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
steps:
|
||||
- name: Get PR details
|
||||
id: pr
|
||||
|
||||
3
.gitignore
vendored
@@ -76,3 +76,6 @@ build/
|
||||
|
||||
# Lage cache
|
||||
.lage/
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
@@ -4,7 +4,31 @@
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 80,
|
||||
"ignorePatterns": [
|
||||
"packages/docs/*" // TOOD: fixme; temporary
|
||||
]
|
||||
"experimentalSortImports": {
|
||||
"groups": [
|
||||
"react",
|
||||
"builtin",
|
||||
"external",
|
||||
"loot-core",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
"desktop-client"
|
||||
],
|
||||
"customGroups": [
|
||||
{
|
||||
"groupName": "react",
|
||||
"elementNamePattern": ["react"]
|
||||
},
|
||||
{
|
||||
"groupName": "loot-core",
|
||||
"elementNamePattern": ["loot-core"]
|
||||
},
|
||||
{
|
||||
"groupName": "desktop-client",
|
||||
"elementNamePattern": ["@desktop-client"]
|
||||
}
|
||||
],
|
||||
"newlinesBetween": true
|
||||
}
|
||||
}
|
||||
|
||||
106
.oxlintrc.json
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "import", "jsx-a11y"],
|
||||
"jsPlugins": ["./packages/eslint-plugin-actual/lib/index.js"],
|
||||
"jsPlugins": [
|
||||
"./packages/eslint-plugin-actual/lib/index.js",
|
||||
"eslint-plugin-typescript-paths",
|
||||
"eslint-plugin-perfectionist"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
@@ -11,13 +15,16 @@
|
||||
"vi": "readonly",
|
||||
"backend": "readonly",
|
||||
"importScripts": "readonly",
|
||||
"FS": "readonly" // TODO: remove this
|
||||
"FS": "readonly"
|
||||
},
|
||||
"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",
|
||||
// Import sorting
|
||||
"perfectionist/sort-named-imports": [
|
||||
"warn",
|
||||
{
|
||||
"groups": ["value-import", "type-import"]
|
||||
}
|
||||
],
|
||||
|
||||
// Actual rules
|
||||
"actual/typography": "warn",
|
||||
@@ -25,6 +32,10 @@
|
||||
"actual/prefer-trans-over-t": "error",
|
||||
"actual/prefer-if-statement": "warn",
|
||||
"actual/prefer-logger-over-console": "error",
|
||||
"actual/object-shorthand-properties": "warn",
|
||||
"actual/prefer-const": "warn",
|
||||
"actual/no-anchor-tag": "warn",
|
||||
"actual/no-react-default-import": "warn",
|
||||
|
||||
// JSX A11y rules
|
||||
"jsx-a11y/no-autofocus": [
|
||||
@@ -62,13 +73,7 @@
|
||||
"jsx-a11y/scope": "warn",
|
||||
|
||||
// Typescript rules
|
||||
"typescript/ban-ts-comment": [
|
||||
"warn",
|
||||
{
|
||||
// TODO: remove this
|
||||
"ts-ignore": "allow-with-description"
|
||||
}
|
||||
],
|
||||
"typescript/ban-ts-comment": ["warn"],
|
||||
"typescript/consistent-type-definitions": ["warn", "type"],
|
||||
"typescript/consistent-type-imports": [
|
||||
"warn",
|
||||
@@ -149,7 +154,7 @@
|
||||
|
||||
// ESLint rules
|
||||
"eslint/array-callback-return": "warn",
|
||||
// "eslint/curly": ["warn", "multi-line", "consistent"], // TODO: re-enable? this rule is really slow
|
||||
"eslint/curly": ["warn", "multi-line", "consistent"],
|
||||
"eslint/default-case": [
|
||||
"warn",
|
||||
{
|
||||
@@ -341,55 +346,9 @@
|
||||
},
|
||||
"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"
|
||||
],
|
||||
"files": ["packages/desktop-electron/**/*"],
|
||||
"rules": {
|
||||
"react/exhaustive-deps": "off"
|
||||
"react/rules-of-hooks": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -410,19 +369,28 @@
|
||||
"import/no-default-export": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/docs/**/*"],
|
||||
"rules": {
|
||||
"actual/no-anchor-tag": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/desktop-client/**/*.{js,ts,jsx,tsx}"],
|
||||
"rules": {
|
||||
"typescript-paths/absolute-parent-import": [
|
||||
"error",
|
||||
{ "preferPathOverBaseUrl": true }
|
||||
],
|
||||
"typescript-paths/absolute-import": ["error", { "enableAlias": false }]
|
||||
}
|
||||
},
|
||||
// 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": {
|
||||
|
||||
12
AGENTS.md
@@ -169,7 +169,7 @@ Custom ESLint rules specific to Actual.
|
||||
|
||||
- `no-untranslated-strings`: Enforces i18n usage
|
||||
- `prefer-trans-over-t`: Prefers Trans component over t() function
|
||||
- `prefer-logger-over-console`: Enforces using logger instead of console
|
||||
- `prefer-logger-over-console`: Enforces using logger instead of console in `packages/loot-core/`
|
||||
- `typography`: Typography rules
|
||||
- `prefer-if-statement`: Prefers explicit if statements
|
||||
|
||||
@@ -259,6 +259,10 @@ Always run `yarn typecheck` before committing.
|
||||
- Generate i18n files: `yarn generate:i18n`
|
||||
- Custom ESLint rules enforce translation usage
|
||||
|
||||
### 5. Financial Number Typography
|
||||
|
||||
- Wrap standalone financial numbers with `FinancialText` or apply `styles.tnum` directly if wrapping is not possible
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
### TypeScript Guidelines
|
||||
@@ -328,7 +332,6 @@ Always maintain newlines between import groups.
|
||||
|
||||
**Never:**
|
||||
|
||||
- Use `console.*` (use logger instead - enforced by ESLint)
|
||||
- Import from `uuid` without destructuring: use `import { v4 as uuidv4 } from 'uuid'`
|
||||
- Import colors directly - use theme instead
|
||||
- Import `@actual-app/web/*` in `loot-core`
|
||||
@@ -541,7 +544,6 @@ Before committing changes, ensure:
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] No new console.\* usage (use logger)
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Prefer `type` over `interface`
|
||||
- [ ] Named exports used (not default exports)
|
||||
@@ -555,6 +557,10 @@ When creating pull requests:
|
||||
|
||||
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
|
||||
|
||||
## Code Review Guidelines
|
||||
|
||||
When performing code reviews (especially for LLM agents): **see [CODE_REVIEW_GUIDELINES.md](./CODE_REVIEW_GUIDELINES.md)** for specific guidelines.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Bundle Size**: Check with rollup-plugin-visualizer
|
||||
|
||||
94
CODE_REVIEW_GUIDELINES.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# CODE_REVIEW_GUIDELINES.md - Guidelines for LLM Agents Performing Code Reviews
|
||||
|
||||
This document provides specific guidelines for LLM agents performing code reviews on the Actual Budget codebase. These guidelines help maintain code quality, consistency, and follow the project's design principles.
|
||||
|
||||
## Settings Proliferation
|
||||
|
||||
**Do NOT add new settings for every little UI tweak.**
|
||||
|
||||
Actual Budget follows a design philosophy that prioritizes simplicity and avoids settings bloat. Before introducing code that adds new settings:
|
||||
|
||||
- Consider if the UI tweak can be achieved through existing theme/design tokens
|
||||
- Evaluate whether the setting provides meaningful value to users
|
||||
- Check if the change aligns with Actual's design guidelines
|
||||
- Prefer hardcoded values or theme-based solutions over adding user-facing settings
|
||||
|
||||
## TypeScript Strict Mode Suppressions
|
||||
|
||||
**Do NOT approve code that adds new `@ts-strict-ignore` comments.**
|
||||
|
||||
The project uses strict TypeScript checking via `typescript-strict-plugin`. Adding `@ts-strict-ignore` comments undermines type safety. Instead, review should encourage:
|
||||
|
||||
- Fixing the underlying type issue
|
||||
- Using proper type definitions
|
||||
- Refactoring code to satisfy strict type checking
|
||||
- Only in exceptional cases, document why strict checking cannot be applied and seek alternative solutions
|
||||
|
||||
## Linter Suppressions
|
||||
|
||||
**Do NOT approve code that adds new `eslint-disable` or `oxlint-disable` comments.**
|
||||
|
||||
Linter rules are in place for good reasons. Instead of suppressing them:
|
||||
|
||||
- Fix the underlying issue
|
||||
- If the rule is incorrectly flagging valid code, consider if the code can be refactored
|
||||
- Only approve suppressions if there's a documented, exceptional reason
|
||||
|
||||
## Type Assertions
|
||||
|
||||
**Prefer `x satisfies SomeType` over `x as SomeType` for type coercions.**
|
||||
|
||||
The `satisfies` operator provides better type safety by:
|
||||
|
||||
- Ensuring the value actually satisfies the type (narrowing)
|
||||
- Preserving the actual type information for better inference
|
||||
- Catching type mismatches at compile time
|
||||
|
||||
**Exception:** If you truly need to assert a type that TypeScript cannot verify (e.g., runtime type guards), use `as` but require a comment explaining why it's safe.
|
||||
|
||||
## Avoiding `any` and `unknown`
|
||||
|
||||
**Flag code that uses `any` or `unknown` unless absolutely necessary.**
|
||||
|
||||
The use of `any` or `unknown` should be rare and well-justified. Before approving:
|
||||
|
||||
- Require explicit justification for why the type cannot be determined
|
||||
- Suggest using proper type definitions or generics
|
||||
- Consider if the type can be narrowed or properly inferred
|
||||
- Look for existing type definitions in `packages/loot-core/src/types/`
|
||||
|
||||
Only approve `any` or `unknown` if there's a documented, exceptional reason (e.g., interop with untyped external libraries, gradual migration).
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
**All user-facing strings must be translated.**
|
||||
|
||||
The project has custom ESLint rules (`actual/no-untranslated-strings`) that enforce i18n usage, but reviewers should actively flag untranslated strings:
|
||||
|
||||
- Use `Trans` component instead of `t()` function when possible
|
||||
- All text visible to users must use i18n functions
|
||||
- Flag hardcoded strings that should be translated
|
||||
|
||||
## Test Mocking
|
||||
|
||||
**Minimize mocked dependencies; prefer real implementations.**
|
||||
|
||||
When reviewing tests, encourage the use of real implementations over mocks:
|
||||
|
||||
- Prefer real dependencies, utilities, and data structures
|
||||
- Only mock when the real implementation is impractical (e.g., external APIs, file system in unit tests)
|
||||
- Ensure mocks accurately represent real behavior
|
||||
|
||||
Over-mocking makes tests brittle and less reliable. Real implementations provide better confidence that code works correctly.
|
||||
|
||||
## Financial Number Typography
|
||||
|
||||
Standalone financial numbers should have tabular number styles applied.
|
||||
|
||||
- Standalone financial numbers should be wrapped with `FinancialText` or `styles.tnum` should be applied directly if wrapping is not possible
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- See [AGENTS.md](./AGENTS.md) for general development guidelines
|
||||
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines
|
||||
- Community documentation: [https://actualbudget.org/docs/contributing/](https://actualbudget.org/docs/contributing/)
|
||||
@@ -37,7 +37,7 @@ async function run() {
|
||||
choices: [
|
||||
{ title: '✨ Features', value: 'Features' },
|
||||
{ title: '👍 Enhancements', value: 'Enhancements' },
|
||||
{ title: '🐛 Bugfix', value: 'Bugfix' },
|
||||
{ title: '🐛 Bugfixes', value: 'Bugfixes' },
|
||||
{ title: '⚙️ Maintenance', value: 'Maintenance' },
|
||||
],
|
||||
},
|
||||
@@ -160,7 +160,8 @@ category: ${type}
|
||||
authors: [${username}]
|
||||
---
|
||||
|
||||
${summary}`;
|
||||
${summary}
|
||||
`;
|
||||
}
|
||||
|
||||
// simple exec that fails silently and returns an empty string on failure
|
||||
|
||||
@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
|
||||
echo "E2E_START_URL: $E2E_START_URL"
|
||||
echo "VRT_ARGS: $VRT_ARGS"
|
||||
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash \
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash \
|
||||
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import tsparser from '@typescript-eslint/parser';
|
||||
import pluginPerfectionist from 'eslint-plugin-perfectionist';
|
||||
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export default defineConfig(
|
||||
{
|
||||
ignores: [
|
||||
'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/',
|
||||
'packages/desktop-client/service-worker/*',
|
||||
'packages/desktop-client/build-electron/',
|
||||
'packages/desktop-client/build-stats/',
|
||||
'packages/desktop-client/public/kcab/',
|
||||
'packages/desktop-client/public/data/',
|
||||
'packages/desktop-client/test-results/',
|
||||
'packages/desktop-client/playwright-report/',
|
||||
'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/*',
|
||||
'.github/*',
|
||||
'**/build/',
|
||||
'**/dist/',
|
||||
'**/node_modules/',
|
||||
],
|
||||
},
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
perfectionist: pluginPerfectionist,
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: 'require',
|
||||
property: 'ensure',
|
||||
message:
|
||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
||||
},
|
||||
{
|
||||
object: 'System',
|
||||
property: 'import',
|
||||
message:
|
||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
||||
},
|
||||
],
|
||||
|
||||
'perfectionist/sort-imports': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
'react',
|
||||
'builtin',
|
||||
'external',
|
||||
'loot-core',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
'desktop-client',
|
||||
],
|
||||
customGroups: [
|
||||
{
|
||||
groupName: 'react',
|
||||
elementNamePattern: '^react(-.*)?$',
|
||||
},
|
||||
{
|
||||
groupName: 'loot-core',
|
||||
elementNamePattern: '^loot-core',
|
||||
},
|
||||
{
|
||||
groupName: 'desktop-client',
|
||||
elementNamePattern: '^@desktop-client',
|
||||
},
|
||||
],
|
||||
newlinesBetween: 'always',
|
||||
},
|
||||
],
|
||||
|
||||
'object-shorthand': ['warn', 'properties'],
|
||||
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
{
|
||||
// forbid React.* as they are legacy https://twitter.com/dan_abramov/status/1308739731551858689
|
||||
selector:
|
||||
":matches(MemberExpression[object.name='React'], TSQualifiedName[left.name='React'])",
|
||||
message:
|
||||
'Using default React import is discouraged, please use named exports directly instead.',
|
||||
},
|
||||
{
|
||||
// forbid <a> in favor of <Link>
|
||||
selector: 'JSXOpeningElement[name.name="a"]',
|
||||
message: 'Using <a> is discouraged, please use <Link> instead.',
|
||||
},
|
||||
],
|
||||
|
||||
'prefer-const': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*.{js,ts,jsx,tsx}'],
|
||||
plugins: {
|
||||
'typescript-paths': pluginTypescriptPaths,
|
||||
},
|
||||
rules: {
|
||||
'typescript-paths/absolute-parent-import': [
|
||||
'error',
|
||||
{ preferPathOverBaseUrl: true },
|
||||
],
|
||||
'typescript-paths/absolute-import': ['error', { enableAlias: false }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/docs/**/*'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
31
package.json
@@ -7,11 +7,11 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/actualbudget/actual/issues/"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:actualbudget/actual.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
@@ -33,6 +33,7 @@
|
||||
"start:service-plugins": "yarn workspace plugins-service watch",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"start:storybook": "yarn workspace @actual-app/components start:storybook",
|
||||
"build:browser-backend": "yarn workspace loot-core build:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
@@ -40,6 +41,7 @@
|
||||
"build:plugins-service": "yarn workspace plugins-service build",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"build:docs": "yarn workspace docs build",
|
||||
"build:storybook": "yarn workspace @actual-app/components build:storybook",
|
||||
"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",
|
||||
@@ -52,8 +54,8 @@
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
"lint": "oxfmt --check . && oxlint --deny-warnings && eslint . --max-warnings 0",
|
||||
"lint:fix": "oxfmt . && oxlint --deny-warnings --fix && eslint . --max-warnings 0 --fix",
|
||||
"lint": "oxfmt --check . && oxlint --deny-warnings",
|
||||
"lint:fix": "oxfmt . && oxlint --deny-warnings --fix",
|
||||
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
||||
"typecheck": "yarn tsc --incremental && tsc-strict",
|
||||
"jq": "./node_modules/node-jq/bin/jq",
|
||||
@@ -61,9 +63,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@types/node": "^22.19.1",
|
||||
"@types/node": "^22.19.3",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.50.0",
|
||||
"baseline-browser-mapping": "^2.9.14",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-perfectionist": "^4.15.1",
|
||||
@@ -75,8 +77,8 @@
|
||||
"minimatch": "^10.1.1",
|
||||
"node-jq": "^6.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"oxfmt": "^0.17.0",
|
||||
"oxlint": "^1.32.0",
|
||||
"oxfmt": "^0.26.0",
|
||||
"oxlint": "^1.41.0",
|
||||
"p-limit": "^7.2.0",
|
||||
"prompts": "^2.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
@@ -88,22 +90,21 @@
|
||||
"rollup": "4.40.1",
|
||||
"socks": ">=2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"yarn": "^4.9.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{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"
|
||||
"oxlint --deny-warnings --fix"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"browserslist": [
|
||||
"electron >= 35.0",
|
||||
"defaults"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"yarn": "^4.9.1"
|
||||
},
|
||||
"packageManager": "yarn@4.10.3"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
// loot-core types
|
||||
import type { InitConfig } from 'loot-core/server/main';
|
||||
|
||||
// oxlint-disable-next-line typescript/ban-ts-comment
|
||||
// @ts-ignore: bundle not available until we build it
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
import * as injected from './injected';
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "26.1.0",
|
||||
"license": "MIT",
|
||||
"version": "26.2.0",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"@types",
|
||||
"dist"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"types": "@types/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"@types"
|
||||
],
|
||||
"scripts": {
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:crdt": "yarn workspace @actual-app/crdt build",
|
||||
@@ -24,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^13.0.0"
|
||||
@@ -32,6 +29,9 @@
|
||||
"devDependencies": {
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.9"
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
// oxlint-disable-next-line typescript/ban-ts-comment
|
||||
// @ts-ignore: bundle not available until we build it
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ function parseArgs(argv) {
|
||||
return {
|
||||
sections,
|
||||
identifier: getSingleValue(args, 'identifier') ?? 'bundle-stats',
|
||||
format: getSingleValue(args, 'format') ?? 'pr-body',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -463,6 +464,12 @@ const TOTAL_HEADERS = makeHeader([
|
||||
'Total bundle size',
|
||||
'% Changed',
|
||||
]);
|
||||
const SUMMARY_HEADERS = makeHeader([
|
||||
'Bundle',
|
||||
'Files count',
|
||||
'Total bundle size',
|
||||
'% Changed',
|
||||
]);
|
||||
const TABLE_HEADERS = makeHeader(['Asset', 'File Size', '% Changed']);
|
||||
const CHUNK_TABLE_HEADERS = makeHeader(['File', 'Δ', 'Size']);
|
||||
|
||||
@@ -596,6 +603,24 @@ function printTotalAssetTable(statsDiff) {
|
||||
return `**Total**\n${TOTAL_HEADERS}\n${printAssetTableRow(statsDiff.total)}`;
|
||||
}
|
||||
|
||||
function printSummaryTable(sections) {
|
||||
if (sections.length === 0) {
|
||||
return `${SUMMARY_HEADERS}\nNo bundle stats were generated.`;
|
||||
}
|
||||
|
||||
const rows = sections.map(section => {
|
||||
const total = section.statsDiff.total;
|
||||
return [
|
||||
section.name,
|
||||
total.name,
|
||||
toFileSizeDiffCell(total),
|
||||
conditionalPercentage(total.diffPercentage),
|
||||
].join(' | ');
|
||||
});
|
||||
|
||||
return `${SUMMARY_HEADERS}\n${rows.join('\n')}`;
|
||||
}
|
||||
|
||||
function renderSection(title, statsDiff, chunkModuleDiff) {
|
||||
const { total, ...groups } = statsDiff;
|
||||
const parts = [`#### ${title}`, '', printTotalAssetTable({ total })];
|
||||
@@ -615,8 +640,30 @@ function renderSection(title, statsDiff, chunkModuleDiff) {
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
function renderSections(sections) {
|
||||
return sections
|
||||
.map(section =>
|
||||
renderSection(section.name, section.statsDiff, section.chunkDiff),
|
||||
)
|
||||
.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
function getIdentifierMarkers(key) {
|
||||
const label = 'bundlestats-action-comment';
|
||||
return {
|
||||
start: `<!--- ${label} key:${key} start --->`,
|
||||
end: `<!--- ${label} key:${key} end --->`,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const allowedFormats = new Set(['comment', 'pr-body']);
|
||||
if (!allowedFormats.has(args.format)) {
|
||||
throw new Error(
|
||||
`Invalid format "${args.format}". Use "comment" or "pr-body".`,
|
||||
);
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[bundle-stats] Found ${args.sections.length} sections to process`,
|
||||
@@ -654,22 +701,29 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
const identifier = `<!--- bundlestats-action-comment key:${args.identifier} --->`;
|
||||
const markers = getIdentifierMarkers(args.identifier);
|
||||
const sectionsContent = renderSections(sections);
|
||||
const summaryTable = printSummaryTable(sections);
|
||||
|
||||
const comment = [
|
||||
const detailedBody = ['### Bundle Stats', '', sectionsContent].join('\n');
|
||||
|
||||
const commentBody = [markers.start, detailedBody, '', markers.end, ''].join(
|
||||
'\n',
|
||||
);
|
||||
|
||||
const prBody = [
|
||||
markers.start,
|
||||
'### Bundle Stats',
|
||||
'',
|
||||
sections
|
||||
.map(section =>
|
||||
renderSection(section.name, section.statsDiff, section.chunkDiff),
|
||||
)
|
||||
.join('\n\n---\n\n'),
|
||||
summaryTable,
|
||||
'',
|
||||
identifier,
|
||||
`<details>\n<summary>View detailed bundle stats</summary>\n\n${sectionsContent}\n</details>`,
|
||||
'',
|
||||
markers.end,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
process.stdout.write(comment);
|
||||
process.stdout.write(args.format === 'comment' ? commentBody : prBody);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
|
||||
@@ -19,6 +19,10 @@ const options = {
|
||||
type: 'string', // nightly, hotfix, monthly, auto
|
||||
short: 't',
|
||||
},
|
||||
version: {
|
||||
type: 'string',
|
||||
short: 'v',
|
||||
},
|
||||
update: {
|
||||
type: 'boolean',
|
||||
short: 'u',
|
||||
@@ -44,16 +48,21 @@ try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const currentVersion = packageJson.version;
|
||||
|
||||
const explicitVersion = values.version;
|
||||
let newVersion;
|
||||
try {
|
||||
newVersion = getNextVersion({
|
||||
currentVersion,
|
||||
type: values.type,
|
||||
currentDate: new Date(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
if (explicitVersion) {
|
||||
newVersion = explicitVersion;
|
||||
} else {
|
||||
try {
|
||||
newVersion = getNextVersion({
|
||||
currentVersion,
|
||||
type: values.type,
|
||||
currentDate: new Date(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(newVersion);
|
||||
|
||||
@@ -14,10 +14,14 @@ import process from 'node:process';
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
const BOT_BOUNDARY_MARKER = '<!--- actual-bot-sections --->';
|
||||
const BOT_BOUNDARY_TEXT = `${BOT_BOUNDARY_MARKER}\n<hr />`;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
commentFile: null,
|
||||
identifier: null,
|
||||
target: 'comment',
|
||||
};
|
||||
|
||||
for (let i = 2; i < argv.length; i += 2) {
|
||||
@@ -41,6 +45,9 @@ function parseArgs(argv) {
|
||||
case '--identifier':
|
||||
args.identifier = value;
|
||||
break;
|
||||
case '--target':
|
||||
args.target = value;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument "${key}".`);
|
||||
}
|
||||
@@ -54,6 +61,12 @@ function parseArgs(argv) {
|
||||
throw new Error('Missing required argument "--identifier".');
|
||||
}
|
||||
|
||||
if (!['comment', 'pr-body'].includes(args.target)) {
|
||||
throw new Error(
|
||||
`Invalid value "${args.target}" for "--target". Use "comment" or "pr-body".`,
|
||||
);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -110,20 +123,123 @@ function isGitHubActionsBot(comment) {
|
||||
return comment.user?.login === 'github-actions[bot]';
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function getIdentifierMarkers(identifier) {
|
||||
if (identifier.includes('<!---')) {
|
||||
return {
|
||||
start: identifier,
|
||||
end: null,
|
||||
};
|
||||
}
|
||||
|
||||
const label = 'bundlestats-action-comment';
|
||||
return {
|
||||
start: `<!--- ${label} key:${identifier} start --->`,
|
||||
end: `<!--- ${label} key:${identifier} end --->`,
|
||||
};
|
||||
}
|
||||
|
||||
function upsertBlock(existingBody, block, markers) {
|
||||
const body = existingBody ?? '';
|
||||
|
||||
if (markers.end) {
|
||||
const pattern = new RegExp(
|
||||
`${escapeRegExp(markers.start)}[\\s\\S]*?${escapeRegExp(markers.end)}`,
|
||||
'm',
|
||||
);
|
||||
|
||||
if (pattern.test(body)) {
|
||||
return body.replace(pattern, block.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (body.trim().length === 0) {
|
||||
return block.trim();
|
||||
}
|
||||
|
||||
const separator = body.endsWith('\n') ? '\n' : '\n\n';
|
||||
const boundary = body.includes(BOT_BOUNDARY_MARKER)
|
||||
? ''
|
||||
: `${BOT_BOUNDARY_TEXT}\n\n`;
|
||||
return `${body}${separator}${boundary}${block.trim()}`;
|
||||
}
|
||||
|
||||
async function updatePullRequestBody(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
pullNumber,
|
||||
block,
|
||||
markers,
|
||||
) {
|
||||
const { data } = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
const nextBody = upsertBlock(data.body ?? '', block, markers);
|
||||
|
||||
await octokit.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
body: nextBody,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteExistingComment(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
issueNumber,
|
||||
markers,
|
||||
) {
|
||||
const comments = await listComments(octokit, owner, repo, issueNumber);
|
||||
const existingComment = comments.find(
|
||||
comment =>
|
||||
isGitHubActionsBot(comment) && comment.body?.includes(markers.start),
|
||||
);
|
||||
|
||||
if (existingComment) {
|
||||
await octokit.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existingComment.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { commentFile, identifier } = parseArgs(process.argv);
|
||||
const { commentFile, identifier, target } = parseArgs(process.argv);
|
||||
const commentBody = await loadCommentBody(commentFile);
|
||||
const token = assertGitHubToken();
|
||||
const { owner, repo } = getRepoInfo();
|
||||
const issueNumber = getPullRequestNumber();
|
||||
const markers = getIdentifierMarkers(identifier);
|
||||
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
const comments = await listComments(octokit, owner, repo, issueNumber);
|
||||
if (target === 'pr-body') {
|
||||
await updatePullRequestBody(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
issueNumber,
|
||||
commentBody,
|
||||
markers,
|
||||
);
|
||||
await deleteExistingComment(octokit, owner, repo, issueNumber, markers);
|
||||
console.log('Updated pull request body with bundle stats.');
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = await listComments(octokit, owner, repo, issueNumber);
|
||||
const existingComment = comments.find(
|
||||
comment =>
|
||||
isGitHubActionsBot(comment) && comment.body?.includes(identifier),
|
||||
isGitHubActionsBot(comment) && comment.body?.includes(markers.start),
|
||||
);
|
||||
|
||||
if (existingComment) {
|
||||
@@ -134,15 +250,16 @@ async function main() {
|
||||
body: commentBody,
|
||||
});
|
||||
console.log('Updated existing bundle stats comment.');
|
||||
} else {
|
||||
await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body: commentBody,
|
||||
});
|
||||
console.log('Created new bundle stats comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body: commentBody,
|
||||
});
|
||||
console.log('Created new bundle stats comment.');
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"name": "@actual-app/ci-actions",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"vitest": "^4.0.9"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^4.0.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getNextVersion } from './get-next-package-version';
|
||||
|
||||
|
||||
3
packages/component-library/.oxlintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"jsPlugins": ["eslint-plugin-storybook"]
|
||||
}
|
||||
43
packages/component-library/.storybook/main.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string) {
|
||||
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
'../src/Introduction.mdx',
|
||||
'../src/**/*.mdx',
|
||||
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
getAbsolutePath('@chromatic-com/storybook'),
|
||||
getAbsolutePath('@storybook/addon-a11y'),
|
||||
getAbsolutePath('@storybook/addon-docs'),
|
||||
],
|
||||
framework: getAbsolutePath('@storybook/react-vite'),
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
async viteFinal(config) {
|
||||
const { mergeConfig } = await import('vite');
|
||||
|
||||
return mergeConfig(config, {
|
||||
// Telling Vite how to resolve path aliases
|
||||
plugins: [viteTsconfigPaths({ root: '../..' })],
|
||||
esbuild: {
|
||||
// Needed to handle JSX in .ts/.tsx files
|
||||
jsx: 'automatic',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
74
packages/component-library/.storybook/manager.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { addons } from 'storybook/manager-api';
|
||||
import { create } from 'storybook/theming/create';
|
||||
|
||||
// Colors from the Actual Budget light theme palette
|
||||
const purple500 = '#8719e0';
|
||||
const purple400 = '#9a3de8';
|
||||
const navy900 = '#102a43';
|
||||
const navy700 = '#334e68';
|
||||
const navy600 = '#486581';
|
||||
const navy150 = '#d9e2ec';
|
||||
const navy100 = '#e8ecf0';
|
||||
const white = '#ffffff';
|
||||
|
||||
// Create a custom Storybook theme matching Actual Budget's light theme
|
||||
const theme = create({
|
||||
base: 'light',
|
||||
brandTitle: 'Actual Budget',
|
||||
brandUrl: 'https://actualbudget.org',
|
||||
brandImage: 'https://actualbudget.org/img/actual.webp',
|
||||
brandTarget: '_blank',
|
||||
|
||||
// UI colors
|
||||
colorPrimary: purple500,
|
||||
colorSecondary: purple400,
|
||||
|
||||
// App chrome
|
||||
appBg: navy100,
|
||||
appContentBg: white,
|
||||
appPreviewBg: white,
|
||||
appBorderColor: navy150,
|
||||
appBorderRadius: 4,
|
||||
|
||||
// Fonts
|
||||
fontBase:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
fontCode: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
|
||||
|
||||
// Text colors
|
||||
textColor: navy900,
|
||||
textInverseColor: white,
|
||||
textMutedColor: navy600,
|
||||
|
||||
// Toolbar
|
||||
barTextColor: navy700,
|
||||
barHoverColor: purple500,
|
||||
barSelectedColor: purple500,
|
||||
barBg: white,
|
||||
|
||||
// Form colors
|
||||
buttonBg: white,
|
||||
buttonBorder: navy900,
|
||||
booleanBg: navy150,
|
||||
booleanSelectedBg: purple500,
|
||||
inputBg: white,
|
||||
inputBorder: navy900,
|
||||
inputTextColor: navy900,
|
||||
inputBorderRadius: 4,
|
||||
});
|
||||
|
||||
addons.setConfig({
|
||||
theme,
|
||||
enableShortcuts: true,
|
||||
isFullscreen: false,
|
||||
isToolshown: true,
|
||||
sidebar: {
|
||||
collapsedRoots: [],
|
||||
filters: {
|
||||
patterns: item => {
|
||||
// Hide stories that are marked as internal
|
||||
return !item.tags?.includes('internal');
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
88
packages/component-library/.storybook/preview.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import type { Preview } from '@storybook/react-vite';
|
||||
|
||||
// Not ideal to import from desktop-client, but we need a source of truth for theme variables
|
||||
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
|
||||
import * as developmentTheme from '../../desktop-client/src/style/themes/development';
|
||||
import * as lightTheme from '../../desktop-client/src/style/themes/light';
|
||||
import * as midnightTheme from '../../desktop-client/src/style/themes/midnight';
|
||||
|
||||
const THEMES = {
|
||||
light: lightTheme,
|
||||
dark: darkTheme,
|
||||
midnight: midnightTheme,
|
||||
development: developmentTheme,
|
||||
} as const;
|
||||
|
||||
type ThemeName = keyof typeof THEMES;
|
||||
|
||||
const ThemedStory = ({
|
||||
themeName,
|
||||
children,
|
||||
}: {
|
||||
themeName?: ThemeName;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
if (!themeName || !THEMES[themeName]) {
|
||||
throw new Error(`No theme specified`);
|
||||
}
|
||||
|
||||
const css = Object.entries(THEMES[themeName])
|
||||
.map(([key, value]) => `--color-${key}: ${value};`)
|
||||
.join('\n');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<style>{`:root {\n${css}}`}</style>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
(Story, { globals }) => {
|
||||
const themeName = globals.theme;
|
||||
|
||||
return (
|
||||
<ThemedStory themeName={themeName}>
|
||||
<Story />
|
||||
</ThemedStory>
|
||||
);
|
||||
},
|
||||
],
|
||||
globalTypes: {
|
||||
theme: {
|
||||
name: 'Theme',
|
||||
description: 'Global theme for components',
|
||||
defaultValue: 'light',
|
||||
toolbar: {
|
||||
icon: 'circlehollow',
|
||||
items: [
|
||||
{ value: 'light', title: 'Light' },
|
||||
{ value: 'dark', title: 'Dark' },
|
||||
{ value: 'midnight', title: 'Midnight' },
|
||||
{ value: 'development', title: 'Development' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -2,22 +2,6 @@
|
||||
"name": "@actual-app/components",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2",
|
||||
"react-dom": ">=18.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
"react-aria-components": "^1.13.0",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.2.5",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"vitest": "^4.0.9"
|
||||
},
|
||||
"exports": {
|
||||
"./hooks/*": "./src/hooks/*.ts",
|
||||
"./icons/logo": "./src/icons/logo/index.ts",
|
||||
@@ -53,6 +37,30 @@
|
||||
"scripts": {
|
||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
||||
"test": "npm-run-all -cp 'test:*'",
|
||||
"test:web": "ENV=web vitest --run -c vitest.web.config.ts"
|
||||
"test:web": "ENV=web vitest --run -c vitest.web.config.ts",
|
||||
"start:storybook": "storybook dev -p 6006",
|
||||
"build:storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
"react-aria-components": "^1.14.0",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^5.0.0",
|
||||
"@storybook/addon-a11y": "^10.2.0",
|
||||
"@storybook/addon-docs": "^10.2.0",
|
||||
"@storybook/react-vite": "^10.2.0",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.2.5",
|
||||
"eslint-plugin-storybook": "^10.2.0",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"storybook": "^10.2.0",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2",
|
||||
"react-dom": ">=18.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ComponentProps, type ReactNode, type CSSProperties } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, type ReactNode } from 'react';
|
||||
|
||||
import { Block } from './Block';
|
||||
import { View } from './View';
|
||||
|
||||
100
packages/component-library/src/Button.stories.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
import { Button } from './Button';
|
||||
|
||||
const meta = {
|
||||
title: 'Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
onClick: { action: 'clicked' },
|
||||
},
|
||||
args: { onClick: fn() },
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
bounce: false,
|
||||
children: 'Button Text',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Primary button variant uses the following theme CSS variables:
|
||||
- \`--color-buttonPrimaryText\`
|
||||
- \`--color-buttonPrimaryTextHover\`
|
||||
- \`--color-buttonPrimaryBackground\`
|
||||
- \`--color-buttonPrimaryBackgroundHover\`
|
||||
- \`--color-buttonPrimaryBorder\`
|
||||
- \`--color-buttonPrimaryShadow\`
|
||||
- \`--color-buttonPrimaryDisabledText\`
|
||||
- \`--color-buttonPrimaryDisabledBackground\`
|
||||
- \`--color-buttonPrimaryDisabledBorder\`
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Normal: Story = {
|
||||
args: {
|
||||
variant: 'normal',
|
||||
bounce: false,
|
||||
children: 'Button Text',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Normal button variant uses the following theme CSS variables:
|
||||
- \`--color-buttonNormalText\`
|
||||
- \`--color-buttonNormalTextHover\`
|
||||
- \`--color-buttonNormalBackground\`
|
||||
- \`--color-buttonNormalBackgroundHover\`
|
||||
- \`--color-buttonNormalBorder\`
|
||||
- \`--color-buttonNormalShadow\`
|
||||
- \`--color-buttonNormalSelectedText\`
|
||||
- \`--color-buttonNormalSelectedBackground\`
|
||||
- \`--color-buttonNormalDisabledText\`
|
||||
- \`--color-buttonNormalDisabledBackground\`
|
||||
- \`--color-buttonNormalDisabledBorder\`
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Bare: Story = {
|
||||
args: {
|
||||
variant: 'bare',
|
||||
bounce: false,
|
||||
children: 'Button Text',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Bare button variant uses the following theme CSS variables:
|
||||
- \`--color-buttonBareText\`
|
||||
- \`--color-buttonBareTextHover\`
|
||||
- \`--color-buttonBareBackground\`
|
||||
- \`--color-buttonBareBackgroundHover\`
|
||||
- \`--color-buttonBareBackgroundActive\`
|
||||
- \`--color-buttonBareDisabledText\`
|
||||
- \`--color-buttonBareDisabledBackground\`
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import React, {
|
||||
forwardRef,
|
||||
useMemo,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ReactNode,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { Button as ReactAriaButton } from 'react-aria-components';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ComponentProps, forwardRef } from 'react';
|
||||
import { forwardRef, type ComponentProps } from 'react';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { View } from './View';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { type ChangeEvent, type ReactNode } from 'react';
|
||||
import {
|
||||
ColorPicker as AriaColorPicker,
|
||||
type ColorPickerProps as AriaColorPickerProps,
|
||||
ColorSwatch as AriaColorSwatch,
|
||||
ColorSwatchPicker as AriaColorSwatchPicker,
|
||||
ColorField,
|
||||
ColorSwatchPickerItem,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
ColorSwatch as AriaColorSwatch,
|
||||
type ColorSwatchProps,
|
||||
ColorSwatchPicker as AriaColorSwatchPicker,
|
||||
ColorSwatchPickerItem,
|
||||
ColorField,
|
||||
parseColor,
|
||||
type ColorPickerProps as AriaColorPickerProps,
|
||||
type ColorSwatchProps,
|
||||
} from 'react-aria-components';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
@@ -32,7 +32,6 @@ function ColorSwatch(props: ColorSwatchProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// colors from https://materialui.co/colors
|
||||
const DEFAULT_COLOR_SET = [
|
||||
'#690CB0',
|
||||
'#D32F2F',
|
||||
@@ -54,6 +53,26 @@ const DEFAULT_COLOR_SET = [
|
||||
'#5D4037',
|
||||
'#616161',
|
||||
'#455A64',
|
||||
'#FF6666', //red
|
||||
'#FF99FF', //magenta
|
||||
'#C39DDF', //purple
|
||||
'#6666FF', //blue
|
||||
'#B2FFFF', //cyan
|
||||
'#99cb99', //green
|
||||
'#FFFF7F', //yellow
|
||||
'#FFAB66', //orange
|
||||
'#D4B89C', //brown
|
||||
'#BFBFBF', //gray
|
||||
'#FFAEAE', //colors repeat from above with a lighter tint
|
||||
'#FFCCFF',
|
||||
'#E4D4FF', //this is now purple125
|
||||
'#B0B0FF',
|
||||
'#D8FFFF',
|
||||
'#CFE5CF',
|
||||
'#FFFFB2',
|
||||
'#FFD5B3',
|
||||
'#E4D3C3',
|
||||
'#DADADA',
|
||||
];
|
||||
|
||||
type ColorSwatchPickerProps = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ReactNode, type CSSProperties } from 'react';
|
||||
import { type CSSProperties, type ReactNode } from 'react';
|
||||
|
||||
import { View } from './View';
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useEffect,
|
||||
useRef,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
type RefObject,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
type InitialFocusProps<T extends HTMLElement> = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {
|
||||
type ChangeEvent,
|
||||
type ComponentPropsWithRef,
|
||||
type KeyboardEvent,
|
||||
type FocusEvent,
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
import { Input as ReactAriaInput } from 'react-aria-components';
|
||||
|
||||
|
||||
27
packages/component-library/src/Introduction.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
<Meta title="Introduction" />
|
||||
|
||||
# Actual Budget Component Library
|
||||
|
||||
Welcome to the **Actual Budget Component Library**. Explore our UI components, see how they look across different themes, and learn how to use them in your code.
|
||||
|
||||
### What you can do here
|
||||
|
||||
- ✨ **Browse components** in the sidebar
|
||||
- 🎨 **Switch themes** using the toolbar above
|
||||
- 📚 **Read documentation** and see code examples
|
||||
- 🔍 **Test variations** and component states
|
||||
- ♿ **Check accessibility** compliance
|
||||
|
||||
### Getting Started
|
||||
|
||||
Select a component from the sidebar to explore its documentation, variants, and interactive controls.
|
||||
|
||||
---
|
||||
|
||||
### Useful Links
|
||||
|
||||
- [Actual Budget Website](https://actualbudget.org)
|
||||
- [Documentation](https://actualbudget.org/docs)
|
||||
- [GitHub Repository](https://github.com/actualbudget/actual)
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, type ReactNode, type CSSProperties } from 'react';
|
||||
import { forwardRef, type CSSProperties, type ReactNode } from 'react';
|
||||
|
||||
import { styles } from './styles';
|
||||
import { Text } from './Text';
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ComponentType,
|
||||
type SVGProps,
|
||||
type CSSProperties,
|
||||
type KeyboardEvent,
|
||||
type ReactNode,
|
||||
type SVGProps,
|
||||
} from 'react';
|
||||
|
||||
import { Button } from './Button';
|
||||
@@ -78,62 +79,81 @@ export function Menu<const NameType = string>({
|
||||
}: MenuProps<NameType>) {
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const items = allItems.filter(x => x);
|
||||
const filteredItems = items.filter(
|
||||
item => item && item !== Menu.line && item.type !== Menu.label,
|
||||
);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const currentIndex = filteredItems.indexOf(items[hoveredIndex || 0]);
|
||||
const transformIndex = (idx: number) => items.indexOf(filteredItems[idx]);
|
||||
|
||||
function hoverPrevious() {
|
||||
setHoveredIndex(
|
||||
hoveredIndex === null ? 0 : transformIndex(Math.max(currentIndex - 1, 0)),
|
||||
);
|
||||
}
|
||||
|
||||
function hoverNext() {
|
||||
setHoveredIndex(
|
||||
hoveredIndex === null
|
||||
? 0
|
||||
: transformIndex(Math.min(currentIndex + 1, filteredItems.length - 1)),
|
||||
);
|
||||
}
|
||||
|
||||
function selectItem() {
|
||||
const item = items[hoveredIndex || 0];
|
||||
if (
|
||||
hoveredIndex !== null &&
|
||||
item !== Menu.line &&
|
||||
!isLabel(item) &&
|
||||
!item.disabled
|
||||
) {
|
||||
onMenuSelect?.(item.name);
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
hoverPrevious();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
hoverNext();
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
selectItem();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (
|
||||
activeElement &&
|
||||
(['input', 'select', 'textarea'].includes(
|
||||
activeElement.tagName.toLowerCase(),
|
||||
) ||
|
||||
activeElement.hasAttribute('contenteditable') ||
|
||||
activeElement.getAttribute('role') === 'textbox')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = elRef.current;
|
||||
el?.focus();
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const filteredItems = items.filter(
|
||||
item => item && item !== Menu.line && item.type !== Menu.label,
|
||||
);
|
||||
const currentIndex = filteredItems.indexOf(items[hoveredIndex || 0]);
|
||||
|
||||
const transformIndex = (idx: number) => items.indexOf(filteredItems[idx]);
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setHoveredIndex(
|
||||
hoveredIndex === null
|
||||
? 0
|
||||
: transformIndex(Math.max(currentIndex - 1, 0)),
|
||||
);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setHoveredIndex(
|
||||
hoveredIndex === null
|
||||
? 0
|
||||
: transformIndex(
|
||||
Math.min(currentIndex + 1, filteredItems.length - 1),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
const item = items[hoveredIndex || 0];
|
||||
if (hoveredIndex !== null && item !== Menu.line && !isLabel(item)) {
|
||||
onMenuSelect?.(item.name);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
el?.addEventListener('keydown', onKeyDown);
|
||||
|
||||
return () => {
|
||||
el?.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [hoveredIndex]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
className={className}
|
||||
style={{ outline: 'none', borderRadius: 4, overflow: 'hidden', ...style }}
|
||||
tabIndex={1}
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDown}
|
||||
innerRef={elRef}
|
||||
>
|
||||
{header}
|
||||
@@ -166,6 +186,7 @@ export function Menu<const NameType = string>({
|
||||
|
||||
return (
|
||||
<Button
|
||||
excludeFromTabOrder
|
||||
key={String(item.name)}
|
||||
variant="bare"
|
||||
slot={slot}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ComponentProps, useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, type ComponentProps } from 'react';
|
||||
import { Popover as ReactAriaPopover } from 'react-aria-components';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {
|
||||
type HTMLProps,
|
||||
type Ref,
|
||||
type ReactNode,
|
||||
forwardRef,
|
||||
type HTMLProps,
|
||||
type ReactNode,
|
||||
type Ref,
|
||||
} from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { type SVGProps, useState } from 'react';
|
||||
import React, { useState, type SVGProps } from 'react';
|
||||
|
||||
export const SvgLoading = (props: SVGProps<SVGSVGElement>) => {
|
||||
const { color = 'currentColor' } = props;
|
||||
|
||||
@@ -91,7 +91,10 @@ export const styles: Record<string, any> = {
|
||||
},
|
||||
shadowLarge,
|
||||
tnum: {
|
||||
fontFeatureSettings: '"tnum"',
|
||||
// tnum: Tabular numbers
|
||||
// ss01: Open digits
|
||||
// ss04: Disambiguation w/o zero
|
||||
fontFeatureSettings: '"tnum", "ss01", "ss04"',
|
||||
},
|
||||
notFixed: { fontFeatureSettings: '' },
|
||||
text: {
|
||||
@@ -159,4 +162,11 @@ export const styles: Record<string, any> = {
|
||||
padding: 16,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
tableContainer: {
|
||||
flex: 1,
|
||||
border: '1px solid ' + theme.tableBorder,
|
||||
borderTopLeftRadius: 6,
|
||||
borderTopRightRadius: 6,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,6 +13,9 @@ export const theme = {
|
||||
pageTextPositive: 'var(--color-pageTextPositive)',
|
||||
pageTextLink: 'var(--color-pageTextLink)',
|
||||
pageTextLinkLight: 'var(--color-pageTextLinkLight)',
|
||||
numberPositive: 'var(--color-numberPositive)',
|
||||
numberNegative: 'var(--color-numberNegative)',
|
||||
numberNeutral: 'var(--color-numberNeutral)',
|
||||
cardBackground: 'var(--color-cardBackground)',
|
||||
cardBorder: 'var(--color-cardBorder)',
|
||||
cardShadow: 'var(--color-cardShadow)',
|
||||
@@ -43,6 +46,7 @@ export const theme = {
|
||||
sidebarItemBackgroundHover: 'var(--color-sidebarItemBackgroundHover)',
|
||||
sidebarItemText: 'var(--color-sidebarItemText)',
|
||||
sidebarItemTextSelected: 'var(--color-sidebarItemTextSelected)',
|
||||
sidebarBudgetName: 'var(--color-sidebarBudgetName)',
|
||||
menuBackground: 'var(--color-menuBackground)',
|
||||
menuItemBackground: 'var(--color-menuItemBackground)',
|
||||
menuItemBackgroundHover: 'var(--color-menuItemBackgroundHover)',
|
||||
@@ -186,6 +190,19 @@ export const theme = {
|
||||
reportsGray: 'var(--color-reportsGray)',
|
||||
reportsLabel: 'var(--color-reportsLabel)',
|
||||
reportsInnerLabel: 'var(--color-reportsInnerLabel)',
|
||||
reportsChartFill: 'var(--color-reportsChartFill)',
|
||||
reportsNumberPositive: 'var(--color-reportsNumberPositive)',
|
||||
reportsNumberNegative: 'var(--color-reportsNumberNegative)',
|
||||
reportsNumberNeutral: 'var(--color-reportsNumberNeutral)',
|
||||
budgetNumberPositive: 'var(--color-budgetNumberPositive)',
|
||||
budgetNumberNegative: 'var(--color-budgetNumberNegative)',
|
||||
budgetNumberNeutral: 'var(--color-budgetNumberNeutral)',
|
||||
budgetNumberZero: 'var(--color-budgetNumberZero)',
|
||||
toBudgetPositive: 'var(--color-toBudgetPositive)',
|
||||
toBudgetZero: 'var(--color-toBudgetZero)',
|
||||
toBudgetNegative: 'var(--color-toBudgetNegative)',
|
||||
templateNumberFunded: 'var(--color-templateNumberFunded)',
|
||||
templateNumberUnderFunded: 'var(--color-templateNumberUnderFunded)',
|
||||
noteTagBackground: 'var(--color-noteTagBackground)',
|
||||
noteTagBackgroundHover: 'var(--color-noteTagBackgroundHover)',
|
||||
noteTagDefault: 'var(--color-noteTagDefault)',
|
||||
@@ -201,4 +218,5 @@ export const theme = {
|
||||
tooltipBackground: 'var(--color-tooltipBackground)',
|
||||
tooltipBorder: 'var(--color-tooltipBorder)',
|
||||
calendarCellBackground: 'var(--color-calendarCellBackground)',
|
||||
overlayBackground: 'var(--color-overlayBackground)',
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@actual-app/crdt",
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
"description": "CRDT layer of Actual",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build:node": "tsc --p tsconfig.dist.json",
|
||||
"proto:generate": "./bin/generate-proto",
|
||||
@@ -24,6 +24,6 @@
|
||||
"protoc-gen-js": "3.21.4-4",
|
||||
"ts-protoc-gen": "0.15.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.9"
|
||||
"vitest": "^4.0.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -65,10 +65,10 @@ Run manually:
|
||||
|
||||
```sh
|
||||
# Run docker container
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash
|
||||
|
||||
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash
|
||||
|
||||
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
|
||||
# Use the ip and port noted earlier
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 69 KiB |