Compare commits
42 Commits
claude/bro
...
react-quer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59501edb56 | ||
|
|
d4d4cde3c7 | ||
|
|
acd1309752 | ||
|
|
95fd47d255 | ||
|
|
24bd6dff45 | ||
|
|
3f1ba9ce88 | ||
|
|
7ebf0c3693 | ||
|
|
18f7eee81a | ||
|
|
e980c0cef7 | ||
|
|
b891f203d3 | ||
|
|
a950ad9086 | ||
|
|
ac668664b4 | ||
|
|
038b46df9a | ||
|
|
dba2a2a1a8 | ||
|
|
ebd68a38ef | ||
|
|
b126693968 | ||
|
|
9c4df27a8a | ||
|
|
2aff508368 | ||
|
|
5b63efaa10 | ||
|
|
673b5d241f | ||
|
|
2223aa1c96 | ||
|
|
ecc95084f5 | ||
|
|
089ad26419 | ||
|
|
0cd55d5f36 | ||
|
|
dcd0486f9a | ||
|
|
ca7ea8a896 | ||
|
|
a8f2cd6e36 | ||
|
|
8b2e114e99 | ||
|
|
27030adf40 | ||
|
|
98a758c1b4 | ||
|
|
d9a18d03ee | ||
|
|
705b0f5d28 | ||
|
|
cea282f3bc | ||
|
|
c00db8ed95 | ||
|
|
b02386d654 | ||
|
|
27a43d6560 | ||
|
|
9e317b0a71 | ||
|
|
11a4eb65a0 | ||
|
|
e3c178b89a | ||
|
|
478aac731e | ||
|
|
953c9fcf09 | ||
|
|
fc1811c0db |
@@ -1,74 +0,0 @@
|
||||
---
|
||||
description: Rules for AI-generated commits and pull requests
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# PR and Commit Rules for AI Agents
|
||||
|
||||
Canonical source: `.github/agents/pr-and-commit-rules.md`
|
||||
|
||||
## Commit Rules
|
||||
|
||||
### [AI] Prefix Requirement
|
||||
|
||||
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `[AI] Fix type error in account validation`
|
||||
- `[AI] Add support for new transaction categories`
|
||||
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
|
||||
|
||||
### Git Safety Rules
|
||||
|
||||
- **Never** update git config
|
||||
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
|
||||
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
|
||||
- **Never** force push to `main`/`master`
|
||||
- **Never** commit unless explicitly asked by the user
|
||||
|
||||
## Pre-Commit Quality Checklist
|
||||
|
||||
Before committing, ensure all of the following:
|
||||
|
||||
- [ ] Commit message is prefixed with `[AI]`
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
|
||||
|
||||
## Pull Request Rules
|
||||
|
||||
### [AI] Prefix Requirement
|
||||
|
||||
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `[AI] Fix type error in account validation`
|
||||
- `[AI] Add support for new transaction categories`
|
||||
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
|
||||
### Labels
|
||||
|
||||
Add the **"AI generated"** label to all AI-created pull requests.
|
||||
|
||||
### PR Template: Do Not Fill In
|
||||
|
||||
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is.
|
||||
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese** (简体中文).
|
||||
|
||||
## Quick-Reference Workflow
|
||||
|
||||
1. Make your changes
|
||||
2. Run `yarn typecheck` — fix any errors
|
||||
3. Run `yarn lint:fix` — fix any remaining lint errors
|
||||
4. Run relevant tests (`yarn test` for all, or workspace-specific)
|
||||
5. Stage files and commit with `[AI]` prefix — do not skip hooks
|
||||
6. When creating a PR:
|
||||
- Use `[AI]` prefix in the title
|
||||
- Add the `"AI generated"` label
|
||||
- Leave the PR template blank (do not fill it in)
|
||||
@@ -37,14 +37,12 @@ async function getPRDetails() {
|
||||
console.log('- PR Author:', pr.user.login);
|
||||
console.log('- PR Title:', pr.title);
|
||||
console.log('- Base Branch:', pr.base.ref);
|
||||
console.log('- Head Branch:', pr.head.ref);
|
||||
|
||||
const result = {
|
||||
number: pr.number,
|
||||
author: pr.user.login,
|
||||
title: pr.title,
|
||||
baseBranch: pr.base.ref,
|
||||
headBranch: pr.head.ref,
|
||||
};
|
||||
|
||||
setOutput('result', JSON.stringify(result));
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// overview:
|
||||
// 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`.
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const { spawnSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const migrationsDir = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'packages',
|
||||
@@ -16,7 +16,7 @@ const migrationsDir = path.join(
|
||||
'migrations',
|
||||
);
|
||||
|
||||
function readMigrations(ref: string) {
|
||||
function readMigrations(ref) {
|
||||
const { stdout } = spawnSync('git', [
|
||||
'ls-tree',
|
||||
'--name-only',
|
||||
70
.github/agents/pr-and-commit-rules.md
vendored
@@ -1,70 +0,0 @@
|
||||
# PR and Commit Rules for AI Agents
|
||||
|
||||
This is the single source of truth for all commit and pull request rules that AI agents must follow when working with Actual Budget.
|
||||
|
||||
## Commit Rules
|
||||
|
||||
### [AI] Prefix Requirement
|
||||
|
||||
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `[AI] Fix type error in account validation`
|
||||
- `[AI] Add support for new transaction categories`
|
||||
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
|
||||
|
||||
### Git Safety Rules
|
||||
|
||||
- **Never** update git config
|
||||
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
|
||||
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
|
||||
- **Never** force push to `main`/`master`
|
||||
- **Never** commit unless explicitly asked by the user
|
||||
|
||||
## Pre-Commit Quality Checklist
|
||||
|
||||
Before committing, ensure all of the following:
|
||||
|
||||
- [ ] Commit message is prefixed with `[AI]`
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
|
||||
|
||||
## Pull Request Rules
|
||||
|
||||
### [AI] Prefix Requirement
|
||||
|
||||
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `[AI] Fix type error in account validation`
|
||||
- `[AI] Add support for new transaction categories`
|
||||
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
|
||||
### Labels
|
||||
|
||||
Add the **"AI generated"** label to all AI-created pull requests. This helps maintainers understand the nature of the contribution.
|
||||
|
||||
### PR Template: Do Not Fill In
|
||||
|
||||
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is. Humans are expected to fill in the Description, Related issue(s), Testing, and Checklist sections.
|
||||
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese**, using Chinese characters (简体中文) for all content you add.
|
||||
|
||||
## Quick-Reference Workflow
|
||||
|
||||
Follow these steps when committing and creating PRs:
|
||||
|
||||
1. Make your changes
|
||||
2. Run `yarn typecheck` — fix any errors
|
||||
3. Run `yarn lint:fix` — fix any remaining lint errors
|
||||
4. Run relevant tests (`yarn test` for all, or workspace-specific)
|
||||
5. Stage files and commit with `[AI]` prefix — do not skip hooks
|
||||
6. When creating a PR:
|
||||
- Use `[AI]` prefix in the title
|
||||
- Add the `"AI generated"` label
|
||||
- Leave the PR template blank (do not fill it in)
|
||||
19
.github/workflows/ai-generated-release-notes.yml
vendored
@@ -41,12 +41,21 @@ jobs:
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
||||
- 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' &&
|
||||
fromJSON(steps.pr-details.outputs.result).baseBranch == 'master' &&
|
||||
!startsWith(fromJSON(steps.pr-details.outputs.result).headBranch, 'release/')
|
||||
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:
|
||||
|
||||
7
.github/workflows/check.yml
vendored
@@ -60,9 +60,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
download-translations: 'false'
|
||||
node-version: 22
|
||||
- name: Check migrations
|
||||
run: yarn workspace @actual-app/ci-actions tsx bin/check-migrations.ts
|
||||
run: node ./.github/actions/check-migrations.js
|
||||
|
||||
4
.github/workflows/docker-edge.yml
vendored
@@ -87,8 +87,8 @@ jobs:
|
||||
- name: Test that the docker image boots
|
||||
run: |
|
||||
docker run --detach --network=host actualbudget/actual-server-testing
|
||||
sleep 10
|
||||
curl --fail -sS -LI -w '%{http_code}\n' --retry 20 --retry-delay 1 --retry-connrefused localhost:5006
|
||||
sleep 5
|
||||
curl --fail -sS -LI -w '%{http_code}\n' --retry 10 --retry-delay 1 --retry-connrefused localhost:5006
|
||||
|
||||
# This will use the cache from the earlier build step and not rebuild the image
|
||||
# https://docs.docker.com/build/ci/github-actions/test-before-push/
|
||||
|
||||
50
.github/workflows/electron-master.yml
vendored
@@ -156,3 +156,53 @@ 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: Download Linux artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: actual-electron-ubuntu-22.04
|
||||
|
||||
- name: Calculate AppImage SHA256
|
||||
id: appimage_sha256
|
||||
run: |
|
||||
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"
|
||||
|
||||
- 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: ${{ env.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: ${{ env.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 }}'
|
||||
draft: true
|
||||
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
|
||||
|
||||
126
.github/workflows/publish-flathub.yml
vendored
@@ -1,126 +0,0 @@
|
||||
name: Publish Flathub
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag (e.g. v25.3.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: publish-flathub
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish-flathub:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Resolve version
|
||||
id: resolve_version
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
if [[ "$EVENT_NAME" == "release" ]]; then
|
||||
TAG="$RELEASE_TAG"
|
||||
else
|
||||
TAG="$INPUT_TAG"
|
||||
fi
|
||||
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "::error::No tag provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate tag format (v-prefixed semver, e.g. v25.3.0 or v1.2.3-beta.1)
|
||||
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
|
||||
echo "::error::Invalid tag format: $TAG (expected v-prefixed semver, e.g. v25.3.0)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="${TAG#v}"
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved tag=$TAG version=$VERSION"
|
||||
|
||||
- name: Verify release assets exist
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
TAG="${{ steps.resolve_version.outputs.tag }}"
|
||||
|
||||
echo "Checking release assets for tag $TAG..."
|
||||
ASSETS=$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq '.assets[].name')
|
||||
|
||||
echo "Found assets:"
|
||||
echo "$ASSETS"
|
||||
|
||||
if ! echo "$ASSETS" | grep -qx "Actual-linux-x86_64.AppImage"; then
|
||||
echo "::error::Missing asset: Actual-linux-x86_64.AppImage"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$ASSETS" | grep -qx "Actual-linux-arm64.AppImage"; then
|
||||
echo "::error::Missing asset: Actual-linux-arm64.AppImage"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All required AppImage assets found."
|
||||
|
||||
- name: Calculate AppImage SHA256 (streamed)
|
||||
run: |
|
||||
VERSION="${{ steps.resolve_version.outputs.version }}"
|
||||
BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
|
||||
|
||||
echo "Streaming x86_64 AppImage to compute SHA256..."
|
||||
APPIMAGE_X64_SHA256=$(curl -fSL "${BASE_URL}/Actual-linux-x86_64.AppImage" | sha256sum | awk '{ print $1 }')
|
||||
echo "x86_64 SHA256: $APPIMAGE_X64_SHA256"
|
||||
|
||||
echo "Streaming arm64 AppImage to compute SHA256..."
|
||||
APPIMAGE_ARM64_SHA256=$(curl -fSL "${BASE_URL}/Actual-linux-arm64.AppImage" | sha256sum | awk '{ print $1 }')
|
||||
echo "arm64 SHA256: $APPIMAGE_ARM64_SHA256"
|
||||
|
||||
echo "APPIMAGE_X64_SHA256=$APPIMAGE_X64_SHA256" >> "$GITHUB_ENV"
|
||||
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Checkout Flathub repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: flathub/com.actualbudget.actual
|
||||
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
|
||||
|
||||
- name: Update manifest with new version
|
||||
run: |
|
||||
VERSION="${{ steps.resolve_version.outputs.version }}"
|
||||
|
||||
# Replace x86_64 entry
|
||||
sed -i "/x86_64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_X64_SHA256 }}|}" com.actualbudget.actual.yml
|
||||
sed -i "/x86_64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${VERSION}/Actual-linux-x86_64.AppImage|" com.actualbudget.actual.yml
|
||||
|
||||
# Replace arm64 entry
|
||||
sed -i "/arm64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_ARM64_SHA256 }}|}" com.actualbudget.actual.yml
|
||||
sed -i "/arm64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${VERSION}/Actual-linux-arm64.AppImage|" com.actualbudget.actual.yml
|
||||
|
||||
echo "Updated manifest:"
|
||||
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 ${{ steps.resolve_version.outputs.version }}'
|
||||
branch: 'release/${{ steps.resolve_version.outputs.version }}'
|
||||
title: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'
|
||||
body: |
|
||||
This PR updates the Actual desktop flatpak to version ${{ steps.resolve_version.outputs.version }}.
|
||||
|
||||
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.resolve_version.outputs.version }})
|
||||
reviewers: 'jfdoming,MatissJanis,youngcw'
|
||||
@@ -20,13 +20,11 @@ jobs:
|
||||
- name: Update package versions
|
||||
run: |
|
||||
# Get new nightly versions
|
||||
NEW_CORE_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/loot-core/package.json --type nightly)
|
||||
NEW_WEB_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
|
||||
NEW_SYNC_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
|
||||
NEW_API_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
|
||||
|
||||
# Set package versions
|
||||
npm version $NEW_CORE_VERSION --no-git-tag-version --workspace=@actual-app/core --no-workspaces-update
|
||||
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
|
||||
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
|
||||
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
|
||||
@@ -35,10 +33,6 @@ jobs:
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
- name: Pack the core package
|
||||
run: |
|
||||
yarn workspace @actual-app/core pack --filename @actual-app/core.tgz
|
||||
|
||||
- name: Build Server & Web
|
||||
run: yarn build:server
|
||||
|
||||
@@ -59,7 +53,6 @@ jobs:
|
||||
with:
|
||||
name: npm-packages
|
||||
path: |
|
||||
packages/loot-core/@actual-app/core.tgz
|
||||
packages/desktop-client/@actual-app/web.tgz
|
||||
packages/sync-server/@actual-app/sync-server.tgz
|
||||
packages/api/@actual-app/api.tgz
|
||||
@@ -83,12 +76,6 @@ jobs:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Core
|
||||
run: |
|
||||
npm publish loot-core/@actual-app/core.tgz --access public --tag nightly
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Web
|
||||
run: |
|
||||
npm publish desktop-client/@actual-app/web.tgz --access public --tag nightly
|
||||
|
||||
11
.github/workflows/publish-npm-packages.yml
vendored
@@ -16,10 +16,6 @@ jobs:
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Pack the core package
|
||||
run: |
|
||||
yarn workspace @actual-app/core pack --filename @actual-app/core.tgz
|
||||
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
@@ -40,7 +36,6 @@ jobs:
|
||||
with:
|
||||
name: npm-packages
|
||||
path: |
|
||||
packages/loot-core/@actual-app/core.tgz
|
||||
packages/desktop-client/@actual-app/web.tgz
|
||||
packages/sync-server/@actual-app/sync-server.tgz
|
||||
packages/api/@actual-app/api.tgz
|
||||
@@ -64,12 +59,6 @@ jobs:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Core
|
||||
run: |
|
||||
npm publish loot-core/@actual-app/core.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Web
|
||||
run: |
|
||||
npm publish desktop-client/@actual-app/web.tgz --access public
|
||||
|
||||
2
.gitignore
vendored
@@ -33,9 +33,7 @@ packages/desktop-electron/dist
|
||||
packages/desktop-electron/loot-core
|
||||
packages/desktop-client/service-worker
|
||||
packages/plugins-service/dist
|
||||
packages/component-library/dist
|
||||
packages/loot-core/lib-dist
|
||||
**/.tsbuildinfo
|
||||
packages/sync-server/coverage
|
||||
bundle.desktop.js
|
||||
bundle.desktop.js.map
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"builtin",
|
||||
"external",
|
||||
"loot-core",
|
||||
["parent", "subpath"],
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
"desktop-client"
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
{
|
||||
"groupName": "loot-core",
|
||||
"elementNamePattern": ["loot-core/**", "@actual-app/core/**"]
|
||||
"elementNamePattern": ["loot-core/**"]
|
||||
},
|
||||
{
|
||||
"groupName": "desktop-client",
|
||||
|
||||
@@ -101,18 +101,8 @@
|
||||
"typescript/no-var-requires": "error",
|
||||
// we want to allow unions such as "{ name: DbAccount['name'] | DbPayee['name'] }"
|
||||
"typescript/no-duplicate-type-constituents": "off",
|
||||
// we want to allow unions such as "string | 'network' | 'file-key-mismatch'"
|
||||
"typescript/no-redundant-type-constituents": "off",
|
||||
"typescript/await-thenable": "error",
|
||||
"typescript/no-floating-promises": "error",
|
||||
"typescript/require-array-sort-compare": "error",
|
||||
"typescript/unbound-method": "error",
|
||||
"typescript/no-for-in-array": "error",
|
||||
"typescript/restrict-template-expressions": "error",
|
||||
"typescript/no-misused-spread": "warn", // TODO: enable this
|
||||
"typescript/no-base-to-string": "warn", // TODO: enable this
|
||||
"typescript/no-unsafe-unary-minus": "warn", // TODO: enable this
|
||||
"typescript/no-unsafe-type-assertion": "warn", // TODO: enable this
|
||||
"typescript/no-floating-promises": "warn", // TODO: covert to error
|
||||
|
||||
// Import rules
|
||||
"import/consistent-type-specifier-style": "error",
|
||||
|
||||
49
AGENTS.md
@@ -44,9 +44,25 @@ yarn start:desktop
|
||||
|
||||
### ⚠️ CRITICAL REQUIREMENT: AI-Generated Commit Messages and PR Titles
|
||||
|
||||
**ALL commit messages and PR titles MUST be prefixed with `[AI]`.** No exceptions.
|
||||
**THIS IS A MANDATORY REQUIREMENT THAT MUST BE FOLLOWED WITHOUT EXCEPTION:**
|
||||
|
||||
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for the full specification, including git safety rules, pre-commit checklist, and PR workflow.
|
||||
- **ALL commit messages MUST be prefixed with `[AI]`**
|
||||
- **ALL pull request titles MUST be prefixed with `[AI]`**
|
||||
|
||||
**Examples:**
|
||||
|
||||
- ✅ `[AI] Fix type error in account validation`
|
||||
- ✅ `[AI] Add support for new transaction categories`
|
||||
- ❌ `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
- ❌ `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
|
||||
|
||||
**This requirement applies to:**
|
||||
|
||||
- Every single commit message created by AI agents
|
||||
- Every single pull request title created by AI agents
|
||||
- No exceptions are permitted
|
||||
|
||||
**This is a hard requirement that agents MUST follow. Failure to include the `[AI]` prefix is a violation of these instructions.**
|
||||
|
||||
### Task Orchestration with Lage
|
||||
|
||||
@@ -84,7 +100,7 @@ The core application logic that runs on any platform.
|
||||
|
||||
```bash
|
||||
# Run all loot-core tests
|
||||
yarn workspace @actual-app/core run test
|
||||
yarn workspace loot-core run test
|
||||
|
||||
# Or run tests across all packages using lage
|
||||
yarn test
|
||||
@@ -219,7 +235,7 @@ yarn test
|
||||
yarn test:debug
|
||||
|
||||
# Run tests for a specific package
|
||||
yarn workspace @actual-app/core run test
|
||||
yarn workspace loot-core run test
|
||||
```
|
||||
|
||||
**E2E Tests (Playwright)**
|
||||
@@ -298,7 +314,6 @@ Always run `yarn typecheck` before committing.
|
||||
|
||||
**React Patterns:**
|
||||
|
||||
- The project uses **React Compiler** (`babel-plugin-react-compiler`) in the desktop-client. The compiler auto-memoizes component bodies, so you can omit manual `useCallback`, `useMemo`, and `React.memo` when adding or refactoring code; prefer inline callbacks and values unless a stable identity is required by a non-compiled dependency.
|
||||
- Don't use `React.FunctionComponent` or `React.FC` - type props directly
|
||||
- Don't use `React.*` patterns - use named imports instead
|
||||
- Use `<Link>` instead of `<a>` tags
|
||||
@@ -345,7 +360,13 @@ Always maintain newlines between import groups.
|
||||
|
||||
**Git Commands:**
|
||||
|
||||
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete git safety rules, commit message requirements, and PR workflow.
|
||||
- **MANDATORY: ALL commit messages MUST be prefixed with `[AI]`** - This is a hard requirement with no exceptions
|
||||
- **MANDATORY: ALL pull request titles MUST be prefixed with `[AI]`** - This is a hard requirement with no exceptions
|
||||
- Never update git config
|
||||
- Never run destructive git operations (force push, hard reset) unless explicitly requested
|
||||
- Never skip hooks (--no-verify, --no-gpg-sign)
|
||||
- Never force push to main/master
|
||||
- Never commit unless explicitly asked
|
||||
|
||||
## File Structure Patterns
|
||||
|
||||
@@ -544,7 +565,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
||||
|
||||
Before committing changes, ensure:
|
||||
|
||||
- [ ] Commit and PR rules followed (see [PR and Commit Rules](.github/agents/pr-and-commit-rules.md))
|
||||
- [ ] **MANDATORY: Commit message is prefixed with `[AI]`** - This is a hard requirement with no exceptions
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
@@ -557,7 +578,17 @@ Before committing changes, ensure:
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete PR creation rules, including title prefix requirements, labeling, and PR template handling.
|
||||
When creating pull requests:
|
||||
|
||||
- **MANDATORY PREFIX REQUIREMENT**: **ALL pull request titles MUST be prefixed with `[AI]`** - This is a hard requirement that MUST be followed without exception
|
||||
- ✅ Correct: `[AI] Fix type error in account validation`
|
||||
- ❌ Incorrect: `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
- **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.
|
||||
|
||||
### PR Template: Do Not Fill In
|
||||
|
||||
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is. We expect **humans** to fill in the Description, Related issue(s), Testing, and Checklist sections.
|
||||
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese**, using Chinese characters (简体中文) for all content you add.
|
||||
|
||||
## Code Review Guidelines
|
||||
|
||||
@@ -625,7 +656,7 @@ Standard commands documented in `package.json` scripts and the Quick Start secti
|
||||
|
||||
- `yarn lint` / `yarn lint:fix` (uses oxlint + oxfmt)
|
||||
- `yarn test` (lage across all workspaces)
|
||||
- `yarn typecheck` (tsgo + lage typecheck)
|
||||
- `yarn typecheck` (tsc + lage typecheck)
|
||||
|
||||
### Testing and previewing the app
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ packages/desktop-client/bin/remove-untranslated-languages
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace @actual-app/core build:browser
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
|
||||
echo "packages/desktop-client/build"
|
||||
|
||||
@@ -51,17 +51,14 @@ fi
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace @actual-app/core build:node
|
||||
yarn workspace loot-core build:node
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
# required for running the sync-server server
|
||||
yarn workspace @actual-app/core build:browser
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
yarn workspace @actual-app/sync-server build
|
||||
|
||||
# Emit @actual-app/core declarations so desktop-electron (which includes typings/window.ts) can build
|
||||
yarn workspace @actual-app/core exec tsgo -p tsconfig.json
|
||||
|
||||
yarn workspace desktop-electron update-client
|
||||
|
||||
(
|
||||
|
||||
@@ -3,7 +3,6 @@ module.exports = {
|
||||
pipeline: {
|
||||
typecheck: {
|
||||
type: 'npmScript',
|
||||
dependsOn: ['^typecheck'],
|
||||
},
|
||||
test: {
|
||||
type: 'npmScript',
|
||||
|
||||
18
package.json
@@ -25,16 +25,16 @@
|
||||
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
||||
"start:docs": "yarn workspace docs start",
|
||||
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:browser-backend build:plugins-service",
|
||||
"start:desktop-node": "yarn workspace @actual-app/core watch:node",
|
||||
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
|
||||
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
||||
"start:browser": "yarn workspace plugins-service build-dev && npm-run-all --parallel 'start:browser-*'",
|
||||
"start:service-plugins": "yarn workspace plugins-service watch",
|
||||
"start:browser-backend": "yarn workspace @actual-app/core watch:browser",
|
||||
"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 @actual-app/core build:browser",
|
||||
"build:browser-backend": "yarn workspace loot-core build:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
@@ -53,11 +53,11 @@
|
||||
"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 @actual-app/core rebuild",
|
||||
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
|
||||
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
"lint": "oxfmt --check . && oxlint --type-aware",
|
||||
"lint:fix": "oxfmt . && oxlint --fix --type-aware",
|
||||
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
||||
"typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck",
|
||||
"typecheck": "tsc -p tsconfig.root.json --noEmit && lage typecheck",
|
||||
"jq": "./node_modules/node-jq/bin/jq",
|
||||
"prepare": "husky"
|
||||
},
|
||||
@@ -65,7 +65,6 @@
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@types/node": "^22.19.10",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
@@ -88,7 +87,6 @@
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"adm-zip": "patch:adm-zip@npm%3A0.5.16#~/.yarn/patches/adm-zip-npm-0.5.16-4556fea098.patch",
|
||||
"rollup": "4.40.1",
|
||||
"socks": ">=2.8.3"
|
||||
},
|
||||
@@ -97,7 +95,7 @@
|
||||
"oxfmt --no-error-on-unmatched-pattern"
|
||||
],
|
||||
"*.{js,mjs,jsx,ts,tsx}": [
|
||||
"oxlint --fix --type-aware --quiet"
|
||||
"oxlint --fix --type-aware"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -3,18 +3,26 @@ import type {
|
||||
RequestInit as FetchInit,
|
||||
} from 'node-fetch';
|
||||
|
||||
import { init as initLootCore } from '@actual-app/core/server/main';
|
||||
import type { InitConfig, lib } from '@actual-app/core/server/main';
|
||||
// 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';
|
||||
import { validateNodeVersion } from './validateNodeVersion';
|
||||
|
||||
let actualApp: null | typeof bundle.lib;
|
||||
export const internal = bundle.lib;
|
||||
|
||||
export * from './methods';
|
||||
export * as utils from './utils';
|
||||
|
||||
/** @deprecated Please use return value of `init` instead */
|
||||
export let internal: typeof lib | null = null;
|
||||
|
||||
export async function init(config: InitConfig = {}) {
|
||||
if (actualApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateNodeVersion();
|
||||
|
||||
if (!globalThis.fetch) {
|
||||
@@ -25,19 +33,21 @@ export async function init(config: InitConfig = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
internal = await initLootCore(config);
|
||||
return internal;
|
||||
await bundle.init(config);
|
||||
actualApp = bundle.lib;
|
||||
|
||||
injected.override(bundle.lib.send);
|
||||
return bundle.lib;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (internal) {
|
||||
if (actualApp) {
|
||||
try {
|
||||
await internal.send('sync');
|
||||
await actualApp.send('sync');
|
||||
} catch {
|
||||
// most likely that no budget is loaded, so the sync failed
|
||||
}
|
||||
|
||||
await internal.send('close-budget');
|
||||
internal = null;
|
||||
await actualApp.send('close-budget');
|
||||
actualApp = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { init as initLootCore } from '@actual-app/core/server/main';
|
||||
import type { InitConfig, lib } from '@actual-app/core/server/main';
|
||||
|
||||
export * from './methods';
|
||||
export * as utils from './utils';
|
||||
|
||||
let internal: typeof lib | null = null;
|
||||
|
||||
export async function init(config: InitConfig = {}) {
|
||||
internal = await initLootCore(config);
|
||||
return internal;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (internal) {
|
||||
try {
|
||||
await internal.send('sync');
|
||||
} catch {
|
||||
// most likely that no budget is loaded, so the sync failed
|
||||
}
|
||||
|
||||
await internal.send('close-budget');
|
||||
internal = null;
|
||||
}
|
||||
}
|
||||
7
packages/api/injected.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// TODO: comment on why it works this way
|
||||
|
||||
export let send;
|
||||
|
||||
export function override(sendImplementation) {
|
||||
send = sendImplementation;
|
||||
}
|
||||
@@ -1,29 +1,10 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import type { RuleEntity } from '@actual-app/core/types/models';
|
||||
import type { RuleEntity } from 'loot-core/types/models';
|
||||
|
||||
import * as api from './index';
|
||||
|
||||
// In tests we run from source; loot-core's API fs uses __dirname (for the built dist/).
|
||||
// Mock the fs so path constants point at loot-core package root where migrations live.
|
||||
vi.mock(
|
||||
'../loot-core/src/platform/server/fs/index.api',
|
||||
async importOriginal => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
const pathMod = await import('path');
|
||||
const lootCoreRoot = pathMod.join(__dirname, '..', 'loot-core');
|
||||
return {
|
||||
...actual,
|
||||
migrationsPath: pathMod.join(lootCoreRoot, 'migrations'),
|
||||
bundledDatabasePath: pathMod.join(lootCoreRoot, 'default-db.sqlite'),
|
||||
demoBudgetPath: pathMod.join(lootCoreRoot, 'demo-budget'),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
|
||||
global.IS_TESTING = true;
|
||||
|
||||
@@ -6,16 +6,16 @@ import type {
|
||||
APIPayeeEntity,
|
||||
APIScheduleEntity,
|
||||
APITagEntity,
|
||||
} from '@actual-app/core/server/api-models';
|
||||
import { lib } from '@actual-app/core/server/main';
|
||||
import type { Query } from '@actual-app/core/shared/query';
|
||||
import type { ImportTransactionsOpts } from '@actual-app/core/types/api-handlers';
|
||||
import type { Handlers } from '@actual-app/core/types/handlers';
|
||||
} from 'loot-core/server/api-models';
|
||||
import type { Query } from 'loot-core/shared/query';
|
||||
import type { Handlers } from 'loot-core/types/handlers';
|
||||
import type {
|
||||
ImportTransactionEntity,
|
||||
RuleEntity,
|
||||
TransactionEntity,
|
||||
} from '@actual-app/core/types/models';
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import * as injected from './injected';
|
||||
|
||||
export { q } from './app/query';
|
||||
|
||||
@@ -23,7 +23,7 @@ function send<K extends keyof Handlers, T extends Handlers[K]>(
|
||||
name: K,
|
||||
args?: Parameters<T>[0],
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
return lib.send(name, args);
|
||||
return injected.send(name, args);
|
||||
}
|
||||
|
||||
export async function runImport(
|
||||
@@ -126,6 +126,11 @@ export function addTransactions(
|
||||
});
|
||||
}
|
||||
|
||||
export type ImportTransactionsOpts = {
|
||||
defaultCleared?: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
export function importTransactions(
|
||||
accountId: APIAccountEntity['id'],
|
||||
transactions: ImportTransactionEntity[],
|
||||
|
||||
@@ -8,43 +8,30 @@
|
||||
"dist"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/browser.js",
|
||||
"types": "@types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/browser.js"
|
||||
},
|
||||
"default": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn build:node && yarn build:browser",
|
||||
"build:node": "vite build",
|
||||
"build:browser": "vite build --config vite.browser.config.ts",
|
||||
"test": "vitest --run",
|
||||
"typecheck": "tsgo -b && tsc-strict"
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:crdt": "yarn workspace @actual-app/crdt build",
|
||||
"build:node": "tsc && tsc-alias",
|
||||
"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 clean && yarn run build:app && yarn run build:crdt && vitest --run",
|
||||
"clean": "rm -rf dist @types",
|
||||
"typecheck": "yarn build && tsc --noEmit && tsc-strict"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/core": "workspace:*",
|
||||
"@actual-app/crdt": "workspace:*",
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"compare-versions": "^6.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-strict-plugin": "^2.4.4",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-peggy-loader": "^2.0.1",
|
||||
"vitest": "^4.1.0"
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
// Using ES2021 because that's the newest version where
|
||||
// the latest Node 16.x release supports all of the features
|
||||
"target": "ES2021",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node10",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"declarationDir": "@types",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo",
|
||||
"paths": {
|
||||
"loot-core/*": ["./@types/loot-core/src/*"]
|
||||
},
|
||||
"plugins": [{ "name": "typescript-strict-plugin", "paths": ["."] }]
|
||||
},
|
||||
"references": [{ "path": "../crdt" }, { "path": "../loot-core" }],
|
||||
"include": ["."],
|
||||
"exclude": [
|
||||
"**/node_modules/*",
|
||||
"dist",
|
||||
"@types",
|
||||
"*.test.ts",
|
||||
"*.config.ts",
|
||||
"*.browser.config.ts"
|
||||
]
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
declare module 'hyperformula/i18n/languages/enUS';
|
||||
declare module '*.pegjs';
|
||||
@@ -1,4 +1,6 @@
|
||||
import { lib } from '@actual-app/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';
|
||||
|
||||
export const amountToInteger = lib.amountToInteger;
|
||||
export const integerToAmount = lib.integerToAmount;
|
||||
export const amountToInteger = bundle.lib.amountToInteger;
|
||||
export const integerToAmount = bundle.lib.integerToAmount;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
|
||||
const distDir = path.resolve(__dirname, 'dist');
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: 'esnext',
|
||||
outDir: distDir,
|
||||
emptyOutDir: false,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'index.web.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'browser.js',
|
||||
},
|
||||
},
|
||||
plugins: [peggyLoader()],
|
||||
resolve: {
|
||||
// Default extensions — picks up browser implementations (index.ts)
|
||||
// instead of .api.ts (which resolves to Node.js/Electron code)
|
||||
extensions: ['.js', '.ts', '.tsx', '.json'],
|
||||
},
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
|
||||
const lootCoreRoot = path.resolve(__dirname, '../loot-core');
|
||||
const distDir = path.resolve(__dirname, 'dist');
|
||||
const typesDir = path.resolve(__dirname, '@types');
|
||||
|
||||
function cleanOutputDirs() {
|
||||
return {
|
||||
name: 'clean-output-dirs',
|
||||
buildStart() {
|
||||
if (fs.existsSync(distDir)) fs.rmSync(distDir, { recursive: true });
|
||||
if (fs.existsSync(typesDir)) fs.rmSync(typesDir, { recursive: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function copyMigrationsAndDefaultDb() {
|
||||
return {
|
||||
name: 'copy-migrations-and-default-db',
|
||||
closeBundle() {
|
||||
const migrationsSrc = path.join(lootCoreRoot, 'migrations');
|
||||
const defaultDbPath = path.join(lootCoreRoot, 'default-db.sqlite');
|
||||
|
||||
if (!fs.existsSync(migrationsSrc)) {
|
||||
throw new Error(`migrations directory not found at ${migrationsSrc}`);
|
||||
}
|
||||
const migrationsStat = fs.statSync(migrationsSrc);
|
||||
if (!migrationsStat.isDirectory()) {
|
||||
throw new Error(`migrations path is not a directory: ${migrationsSrc}`);
|
||||
}
|
||||
|
||||
const migrationsDest = path.join(distDir, 'migrations');
|
||||
fs.mkdirSync(migrationsDest, { recursive: true });
|
||||
for (const name of fs.readdirSync(migrationsSrc)) {
|
||||
if (name.endsWith('.sql') || name.endsWith('.js')) {
|
||||
fs.copyFileSync(
|
||||
path.join(migrationsSrc, name),
|
||||
path.join(migrationsDest, name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(defaultDbPath)) {
|
||||
throw new Error(`default-db.sqlite not found at ${defaultDbPath}`);
|
||||
}
|
||||
fs.copyFileSync(defaultDbPath, path.join(distDir, 'default-db.sqlite'));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
ssr: { noExternal: true, external: ['better-sqlite3'] },
|
||||
build: {
|
||||
ssr: true,
|
||||
target: 'node20',
|
||||
outDir: distDir,
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'index.ts'),
|
||||
formats: ['cjs'],
|
||||
fileName: () => 'index.js',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
cleanOutputDirs(),
|
||||
peggyLoader(),
|
||||
dts({
|
||||
tsconfigPath: path.resolve(__dirname, 'tsconfig.json'),
|
||||
outDir: path.resolve(__dirname, '@types'),
|
||||
rollupTypes: true,
|
||||
}),
|
||||
copyMigrationsAndDefaultDb(),
|
||||
visualizer({ template: 'raw-data', filename: 'app/stats.json' }),
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.api.ts', '.js', '.ts', '.tsx', '.json'],
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
maxWorkers: 2,
|
||||
},
|
||||
});
|
||||
10
packages/api/vitest.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
maxWorkers: 2,
|
||||
},
|
||||
};
|
||||
1
packages/ci-actions/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
dist/*
|
||||
@@ -3,18 +3,9 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"tsx": "node --import=extensionless/register --experimental-strip-types",
|
||||
"test": "vitest --run",
|
||||
"typecheck": "tsgo -b"
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||
"extensionless": "^2.0.6",
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"extensionless": {
|
||||
"lookFor": [
|
||||
"ts"
|
||||
]
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"types": ["node"],
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*", "bin/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
@@ -32,9 +32,11 @@ const config: StorybookConfig = {
|
||||
const { mergeConfig } = await import('vite');
|
||||
|
||||
return mergeConfig(config, {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
// Telling Vite how to resolve path aliases
|
||||
plugins: [viteTsconfigPaths({ root: '../..' })],
|
||||
esbuild: {
|
||||
// Needed to handle JSX in .ts/.tsx files
|
||||
jsx: 'automatic',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@ 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
|
||||
// TODO: this needs refactoring
|
||||
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';
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"test:web": "ENV=web vitest --run -c vitest.web.config.ts",
|
||||
"start:storybook": "storybook dev -p 6006",
|
||||
"build:storybook": "storybook build",
|
||||
"typecheck": "tsgo -b"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
@@ -54,14 +54,11 @@
|
||||
"@storybook/react-vite": "^10.2.7",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.2.5",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"eslint-plugin-storybook": "^10.2.7",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"storybook": "^10.2.7",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.1.0"
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19.2",
|
||||
|
||||
@@ -16,8 +16,8 @@ import { View } from './View';
|
||||
|
||||
const MenuLine: unique symbol = Symbol('menu-line');
|
||||
const MenuLabel: unique symbol = Symbol('menu-label');
|
||||
Menu.line = MenuLine as typeof MenuLine;
|
||||
Menu.label = MenuLabel as typeof MenuLabel;
|
||||
Menu.line = MenuLine;
|
||||
Menu.label = MenuLabel;
|
||||
|
||||
type KeybindingProps = {
|
||||
keyName: ReactNode;
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from 'path';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
@@ -24,7 +23,13 @@ export default defineConfig({
|
||||
maxWorkers: 2,
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@actual-app\/crdt(\/.*)?$/,
|
||||
replacement: path.resolve('../../../crdt/src$1'),
|
||||
},
|
||||
],
|
||||
extensions: resolveExtensions,
|
||||
},
|
||||
plugins: [react(), peggyLoader()],
|
||||
plugins: [peggyLoader()],
|
||||
});
|
||||
|
||||
@@ -8,23 +8,12 @@
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build:node": "tsgo",
|
||||
"build:node": "tsc",
|
||||
"proto:generate": "./bin/generate-proto",
|
||||
"build": "rm -rf dist && yarn run build:node",
|
||||
"test": "vitest --run",
|
||||
"typecheck": "tsgo -b"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.21.4",
|
||||
@@ -33,9 +22,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-protobuf": "3.15.12",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||
"protoc-gen-js": "3.21.4-4",
|
||||
"ts-protoc-gen": "0.15.0",
|
||||
"vitest": "^4.1.0"
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
|
||||
while (true) {
|
||||
const keyset = new Set([...getKeys(node1), ...getKeys(node2)]);
|
||||
const keys = [...keyset.values()];
|
||||
keys.sort((a, b) => a.localeCompare(b));
|
||||
keys.sort();
|
||||
|
||||
let diffkey: null | '0' | '1' | '2' = null;
|
||||
|
||||
@@ -145,7 +145,7 @@ export function prune(trie: TrieNode, n = 2): TrieNode {
|
||||
}
|
||||
|
||||
const keys = getKeys(trie);
|
||||
keys.sort((a, b) => a.localeCompare(b));
|
||||
keys.sort();
|
||||
|
||||
const next: TrieNode = { hash: trie.hash };
|
||||
|
||||
|
||||
@@ -121,12 +121,12 @@ describe('Timestamp', function () {
|
||||
it('should fail with counter overflow', function () {
|
||||
now = 40;
|
||||
for (let i = 0; i < 65536; i++) Timestamp.send();
|
||||
expect(() => Timestamp.send()).toThrow(Timestamp.OverflowError);
|
||||
expect(Timestamp.send).toThrow(Timestamp.OverflowError);
|
||||
});
|
||||
|
||||
it('should fail with clock drift', function () {
|
||||
now = -(5 * 60 * 1000 + 1);
|
||||
expect(() => Timestamp.send()).toThrow(Timestamp.ClockDriftError);
|
||||
expect(Timestamp.send).toThrow(Timestamp.ClockDriftError);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
// Using ES2021 because that's the newest version where
|
||||
// the latest Node 16.x release supports all of the features
|
||||
"target": "ES2021",
|
||||
@@ -9,10 +8,8 @@
|
||||
"moduleResolution": "node10",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
|
||||
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -61,7 +61,7 @@ export class ConfigurationPage {
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unrecognized import type: ${String(type)}`);
|
||||
throw new Error(`Unrecognized import type: ${type}`);
|
||||
}
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
|
||||
@@ -39,7 +39,7 @@ export class CustomReportPage {
|
||||
.click();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognized mode: ${String(mode)}`);
|
||||
throw new Error(`Unrecognized mode: ${mode}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
const NO_PAYEES_FOUND_TEXT = 'No payees found.';
|
||||
|
||||
export class MobilePayeesPage {
|
||||
readonly page: Page;
|
||||
readonly searchBox: Locator;
|
||||
readonly payeesList: Locator;
|
||||
readonly emptyMessage: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
readonly noPayeesFoundText: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.searchBox = page.getByPlaceholder('Filter payees…');
|
||||
this.payeesList = page.getByRole('grid', { name: 'Payees' });
|
||||
this.emptyMessage = page.getByText('No payees found.');
|
||||
this.loadingIndicator = page.getByTestId('animated-loading');
|
||||
this.noPayeesFoundText = this.payeesList.getByText(NO_PAYEES_FOUND_TEXT);
|
||||
}
|
||||
|
||||
async waitFor(options?: {
|
||||
@@ -47,7 +47,11 @@ export class MobilePayeesPage {
|
||||
* Get all visible payee items
|
||||
*/
|
||||
getAllPayees() {
|
||||
return this.payeesList.getByRole('gridcell');
|
||||
// `GridList.renderEmptyState` still renders a row with "No payees found" text
|
||||
// when no payees are present, so we need to filter that out to get the actual payee items.
|
||||
return this.payeesList
|
||||
.getByRole('row')
|
||||
.filter({ hasNotText: NO_PAYEES_FOUND_TEXT });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,11 +69,4 @@ export class MobilePayeesPage {
|
||||
const payees = this.getAllPayees();
|
||||
return await payees.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for loading to complete
|
||||
*/
|
||||
async waitForLoadingToComplete(timeout: number = 10000) {
|
||||
await this.loadingIndicator.waitFor({ state: 'hidden', timeout });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
const NO_RULES_FOUND_TEXT =
|
||||
'No rules found. Create your first rule to get started!';
|
||||
|
||||
export class MobileRulesPage {
|
||||
readonly page: Page;
|
||||
readonly searchBox: Locator;
|
||||
readonly addButton: Locator;
|
||||
readonly rulesList: Locator;
|
||||
readonly emptyMessage: Locator;
|
||||
readonly noRulesFoundText: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.searchBox = page.getByPlaceholder('Filter rules…');
|
||||
this.addButton = page.getByRole('button', { name: 'Add new rule' });
|
||||
this.rulesList = page.getByRole('main');
|
||||
this.emptyMessage = page.getByText('No rules found');
|
||||
this.rulesList = page.getByRole('grid', { name: 'Rules' });
|
||||
this.noRulesFoundText = this.rulesList.getByText(NO_RULES_FOUND_TEXT);
|
||||
}
|
||||
|
||||
async waitFor(options?: {
|
||||
@@ -47,7 +50,11 @@ export class MobileRulesPage {
|
||||
* Get all visible rule items
|
||||
*/
|
||||
getAllRules() {
|
||||
return this.page.getByRole('grid', { name: 'Rules' }).getByRole('row');
|
||||
// `GridList.renderEmptyState` still renders a row with "No rules found" text
|
||||
// when no rules are present, so we need to filter that out to get the actual rule items.
|
||||
return this.rulesList
|
||||
.getByRole('row')
|
||||
.filter({ hasNotText: NO_RULES_FOUND_TEXT });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
const NO_SCHEDULES_FOUND_TEXT =
|
||||
'No schedules found. Create your first schedule to get started!';
|
||||
|
||||
export class MobileSchedulesPage {
|
||||
readonly page: Page;
|
||||
readonly searchBox: Locator;
|
||||
readonly addButton: Locator;
|
||||
readonly schedulesList: Locator;
|
||||
readonly emptyMessage: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
readonly noSchedulesFoundText: 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.noSchedulesFoundText = this.schedulesList.getByText(
|
||||
NO_SCHEDULES_FOUND_TEXT,
|
||||
);
|
||||
this.loadingIndicator = page.getByTestId('animated-loading');
|
||||
}
|
||||
|
||||
async waitFor(options?: {
|
||||
@@ -51,7 +52,11 @@ export class MobileSchedulesPage {
|
||||
* Get all visible schedule items
|
||||
*/
|
||||
getAllSchedules() {
|
||||
return this.schedulesList.getByRole('gridcell');
|
||||
// `GridList.renderEmptyState` still renders a row with "No schedules found" text
|
||||
// when no schedules are present, so we need to filter that out to get the actual schedule items.
|
||||
return this.schedulesList
|
||||
.getByRole('row')
|
||||
.filter({ hasNotText: NO_SCHEDULES_FOUND_TEXT });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,11 +81,4 @@ export class MobileSchedulesPage {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ test.describe('Mobile Payees', () => {
|
||||
});
|
||||
|
||||
test('checks the page visuals', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
// Check that the header is present
|
||||
await expect(page.getByRole('heading', { name: 'Payees' })).toBeVisible();
|
||||
|
||||
@@ -63,8 +61,6 @@ test.describe('Mobile Payees', () => {
|
||||
});
|
||||
|
||||
test('clicking on a payee opens payee edit page', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
const payeeCount = await payeesPage.getPayeeCount();
|
||||
expect(payeeCount).toBeGreaterThan(0);
|
||||
|
||||
@@ -89,8 +85,7 @@ test.describe('Mobile Payees', () => {
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check that empty message is shown
|
||||
const emptyMessage = page.getByText('No payees found.');
|
||||
await expect(emptyMessage).toBeVisible();
|
||||
await expect(payeesPage.noPayeesFoundText).toBeVisible();
|
||||
|
||||
// Check that no payee items are visible
|
||||
const payees = payeesPage.getAllPayees();
|
||||
@@ -99,8 +94,6 @@ test.describe('Mobile Payees', () => {
|
||||
});
|
||||
|
||||
test('search functionality works correctly', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
// Test searching for a specific payee
|
||||
await payeesPage.searchFor('Fast Internet');
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 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: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -79,8 +79,7 @@ test.describe('Mobile Rules', () => {
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check that empty message is shown
|
||||
const emptyMessage = page.getByText(/No rules found/);
|
||||
await expect(emptyMessage).toBeVisible();
|
||||
await expect(rulesPage.noRulesFoundText).toBeVisible();
|
||||
|
||||
// Check that no rule items are visible
|
||||
const rules = rulesPage.getAllRules();
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
@@ -27,6 +27,7 @@ test.describe('Mobile Schedules', () => {
|
||||
|
||||
// Navigate to schedules page and wait for it to load
|
||||
schedulesPage = await navigation.goToSchedulesPage();
|
||||
await schedulesPage.waitFor();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
@@ -34,8 +35,6 @@ test.describe('Mobile Schedules', () => {
|
||||
});
|
||||
|
||||
test('checks the page visuals', async () => {
|
||||
await schedulesPage.waitForLoadingToComplete();
|
||||
|
||||
// Check that the header is present
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Schedules' }),
|
||||
@@ -60,14 +59,15 @@ test.describe('Mobile Schedules', () => {
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check that empty message is shown
|
||||
await expect(schedulesPage.emptyMessage).toBeVisible();
|
||||
await expect(schedulesPage.noSchedulesFoundText).toBeVisible();
|
||||
|
||||
// Check that no schedule items are visible
|
||||
const schedules = schedulesPage.getAllSchedules();
|
||||
await expect(schedules).toHaveCount(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('clicking on a schedule opens edit form', async () => {
|
||||
await schedulesPage.waitForLoadingToComplete();
|
||||
|
||||
// Wait for at least one schedule to be present
|
||||
await expect(async () => {
|
||||
const scheduleCount = await schedulesPage.getScheduleCount();
|
||||
@@ -89,8 +89,6 @@ test.describe('Mobile Schedules', () => {
|
||||
});
|
||||
|
||||
test('searches and filters schedules', async () => {
|
||||
await schedulesPage.waitForLoadingToComplete();
|
||||
|
||||
// Wait for schedules to load
|
||||
await expect(async () => {
|
||||
const scheduleCount = await schedulesPage.getScheduleCount();
|
||||
@@ -118,8 +116,6 @@ test.describe('Mobile Schedules', () => {
|
||||
});
|
||||
|
||||
test('displays schedule details correctly in list', async () => {
|
||||
await schedulesPage.waitForLoadingToComplete();
|
||||
|
||||
// Wait for schedules to load
|
||||
await expect(async () => {
|
||||
const scheduleCount = await schedulesPage.getScheduleCount();
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |