Compare commits
61 Commits
react-quer
...
matiss/des
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb618beadf | ||
|
|
b4c81974ab | ||
|
|
6b996c11d8 | ||
|
|
282a99db2f | ||
|
|
3a22f1a153 | ||
|
|
d30162672c | ||
|
|
db03d77e81 | ||
|
|
8a8fb2da51 | ||
|
|
a65ab2b4ce | ||
|
|
f29c031735 | ||
|
|
c06f96f015 | ||
|
|
0b21b572fe | ||
|
|
bf1d220ced | ||
|
|
94dd8f73c0 | ||
|
|
85e08b2e9e | ||
|
|
60e2665fcc | ||
|
|
102be1c54d | ||
|
|
448da13cf5 | ||
|
|
41679235be | ||
|
|
73fa068fe9 | ||
|
|
1fe588c143 | ||
|
|
edce092ae8 | ||
|
|
77411394f6 | ||
|
|
235d94478f | ||
|
|
7e0edd43ec | ||
|
|
fdf5c8d0a9 | ||
|
|
a8ec84ceac | ||
|
|
b727124603 | ||
|
|
8bb7f207f2 | ||
|
|
6e0c15eb12 | ||
|
|
4e2cec2c7a | ||
|
|
078603cadf | ||
|
|
b3a86b5392 | ||
|
|
295a565e55 | ||
|
|
387c8fce16 | ||
|
|
c7ebfd8ad4 | ||
|
|
e1f834371b | ||
|
|
4caee99955 | ||
|
|
286d05d187 | ||
|
|
cf05a7ea01 | ||
|
|
b373b612a4 | ||
|
|
3797cff716 | ||
|
|
9e2793d413 | ||
|
|
3201819df9 | ||
|
|
eca50f28b0 | ||
|
|
c82ee91b12 | ||
|
|
cb8ff337dc | ||
|
|
c37a5a02aa | ||
|
|
f9e09ca59b | ||
|
|
8081b8829e | ||
|
|
f2f79d378c | ||
|
|
c5cca67399 | ||
|
|
eabf09587f | ||
|
|
6022929551 | ||
|
|
e65429497d | ||
|
|
3758d72b65 | ||
|
|
032d10ac42 | ||
|
|
f97a89dc28 | ||
|
|
a4bd301ec6 | ||
|
|
18072e1d8b | ||
|
|
a1e0b3f45d |
@@ -13,8 +13,6 @@ reviews:
|
||||
mode: off
|
||||
enabled: false
|
||||
labeling_instructions:
|
||||
- label: 'suspect ai generated'
|
||||
instructions: 'This issue or PR is suspected to be generated by AI. Add this only if "AI generated" label is not present. Add it always if the commit or PR title is prefixed with "[AI]".'
|
||||
- label: 'API'
|
||||
instructions: 'This issue or PR updates the API in `packages/api`.'
|
||||
- label: 'documentation'
|
||||
|
||||
74
.cursor/rules/pr-and-commit.mdc
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
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,12 +37,14 @@ 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));
|
||||
|
||||
2
.github/actions/docs-spelling/expect.txt
vendored
@@ -31,6 +31,7 @@ CAGLPTPL
|
||||
Caixa
|
||||
CAMT
|
||||
cashflow
|
||||
Catppuccin
|
||||
Cetelem
|
||||
cimode
|
||||
Citi
|
||||
@@ -109,6 +110,7 @@ KBCBE
|
||||
Keycloak
|
||||
Khurozov
|
||||
KORT
|
||||
KRW
|
||||
Kreditbank
|
||||
lage
|
||||
LHV
|
||||
|
||||
3
.github/actions/docs-spelling/patterns.txt
vendored
@@ -79,3 +79,6 @@
|
||||
|
||||
# allowlist specific non-English words with non-ASCII characters
|
||||
\b(Länsförsäkringar|München|Złoty)\b
|
||||
|
||||
# allowlist specific proper nouns
|
||||
\b(CodeRabbit)\b
|
||||
|
||||
70
.github/agents/pr-and-commit-rules.md
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
# 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)
|
||||
26
.github/scripts/count-points.mjs
vendored
@@ -8,13 +8,13 @@ const CONFIG = {
|
||||
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 4, // Awarded to whoever merges the release PR
|
||||
PR_CONTRIBUTION_POINTS: {
|
||||
Features: 2,
|
||||
Enhancements: 2,
|
||||
Bugfix: 3,
|
||||
Maintenance: 2,
|
||||
Unknown: 2,
|
||||
},
|
||||
PR_CONTRIBUTION_POINTS: [
|
||||
{ categories: ['Features'], points: 2 },
|
||||
{ categories: ['Enhancements'], points: 2 },
|
||||
{ categories: ['Bugfixes', 'Bugfix'], points: 3 },
|
||||
{ categories: ['Maintenance'], points: 2 },
|
||||
{ categories: ['Unknown'], points: 2 },
|
||||
],
|
||||
// Point tiers for code changes (non-docs)
|
||||
CODE_PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 500, points: 8 },
|
||||
@@ -130,11 +130,14 @@ async function getPRCategoryAndPoints(
|
||||
'utf-8',
|
||||
);
|
||||
const category = parseReleaseNotesCategory(content);
|
||||
const tier = CONFIG.PR_CONTRIBUTION_POINTS.find(e =>
|
||||
e.categories.includes(category),
|
||||
);
|
||||
|
||||
if (category && CONFIG.PR_CONTRIBUTION_POINTS[category]) {
|
||||
if (tier) {
|
||||
return {
|
||||
category,
|
||||
points: CONFIG.PR_CONTRIBUTION_POINTS[category],
|
||||
points: tier.points,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -142,9 +145,12 @@ async function getPRCategoryAndPoints(
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
const unknownTier = CONFIG.PR_CONTRIBUTION_POINTS.find(e =>
|
||||
e.categories.includes('Unknown'),
|
||||
);
|
||||
return {
|
||||
category: 'Unknown',
|
||||
points: CONFIG.PR_CONTRIBUTION_POINTS.Unknown,
|
||||
points: unknownTier.points,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
19
.github/workflows/ai-generated-release-notes.yml
vendored
@@ -41,21 +41,12 @@ 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' && steps.check-base-branch.outputs.targets_master == 'true'
|
||||
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/')
|
||||
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,8 +60,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 22
|
||||
download-translations: 'false'
|
||||
- name: Check migrations
|
||||
run: node ./.github/actions/check-migrations.js
|
||||
run: yarn workspace @actual-app/ci-actions tsx bin/check-migrations.ts
|
||||
|
||||
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 5
|
||||
curl --fail -sS -LI -w '%{http_code}\n' --retry 10 --retry-delay 1 --retry-connrefused localhost:5006
|
||||
sleep 10
|
||||
curl --fail -sS -LI -w '%{http_code}\n' --retry 20 --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,53 +156,3 @@ 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
|
||||
|
||||
37
.github/workflows/merge-freeze-unfreeze.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# When the "unfreeze" label is added to a PR, add that PR to Merge Freeze's unblocked list
|
||||
# so it can be merged during a freeze. Uses pull_request_target so the workflow runs in
|
||||
# the base repo and has access to MERGEFREEZE_ACCESS_TOKEN for fork PRs; it does not
|
||||
# checkout or run any PR code. Requires MERGEFREEZE_ACCESS_TOKEN repo secret
|
||||
# (project-specific token from Merge Freeze Web API panel for actualbudget/actual / master).
|
||||
# See: https://docs.mergefreeze.com/web-api#post-freeze-status
|
||||
|
||||
name: Merge Freeze – add PR to unblocked list
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
unfreeze:
|
||||
if: ${{ github.event.label.name == 'unfreeze' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
concurrency:
|
||||
group: merge-freeze-unfreeze-${{ github.ref }}-labels
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: POST to Merge Freeze – add PR to unblocked list
|
||||
env:
|
||||
MERGEFREEZE_ACCESS_TOKEN: ${{ secrets.MERGEFREEZE_ACCESS_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
USER_NAME: ${{ github.actor }}
|
||||
run: |
|
||||
set -e
|
||||
if [ -z "$MERGEFREEZE_ACCESS_TOKEN" ]; then
|
||||
echo "::error::MERGEFREEZE_ACCESS_TOKEN secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
url="https://www.mergefreeze.com/api/branches/actualbudget/actual/master/?access_token=${MERGEFREEZE_ACCESS_TOKEN}"
|
||||
payload=$(jq -n --arg user_name "$USER_NAME" --argjson pr "$PR_NUMBER" '{frozen: true, user_name: $user_name, unblocked_prs: [$pr]}')
|
||||
curl -sf -X POST "$url" -H "Content-Type: application/json" -d "$payload"
|
||||
echo "Merge Freeze updated: PR #$PR_NUMBER added to unblocked list."
|
||||
25
.github/workflows/pr-ai-label-cleanup.yml
vendored
@@ -1,25 +0,0 @@
|
||||
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'
|
||||
});
|
||||
126
.github/workflows/publish-flathub.yml
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
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'
|
||||
2
.gitignore
vendored
@@ -33,7 +33,9 @@ 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
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
{
|
||||
"groupName": "desktop-client",
|
||||
"elementNamePattern": ["@desktop-client/**"]
|
||||
"elementNamePattern": ["@desktop-client/**", "#**", "#**/**"]
|
||||
}
|
||||
],
|
||||
"newlinesBetween": true
|
||||
|
||||
@@ -102,7 +102,11 @@
|
||||
// we want to allow unions such as "{ name: DbAccount['name'] | DbPayee['name'] }"
|
||||
"typescript/no-duplicate-type-constituents": "off",
|
||||
"typescript/await-thenable": "error",
|
||||
"typescript/no-floating-promises": "warn", // TODO: covert to error
|
||||
"typescript/no-floating-promises": "error",
|
||||
"typescript/require-array-sort-compare": "error",
|
||||
"typescript/unbound-method": "error",
|
||||
"typescript/no-for-in-array": "warn", // TODO: covert to error
|
||||
"typescript/restrict-template-expressions": "warn", // TODO: covert to error
|
||||
|
||||
// Import rules
|
||||
"import/consistent-type-specifier-style": "error",
|
||||
|
||||
84
AGENTS.md
@@ -44,25 +44,9 @@ yarn start:desktop
|
||||
|
||||
### ⚠️ CRITICAL REQUIREMENT: AI-Generated Commit Messages and PR Titles
|
||||
|
||||
**THIS IS A MANDATORY REQUIREMENT THAT MUST BE FOLLOWED WITHOUT EXCEPTION:**
|
||||
**ALL commit messages and PR titles MUST be prefixed with `[AI]`.** No exceptions.
|
||||
|
||||
- **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.**
|
||||
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.
|
||||
|
||||
### Task Orchestration with Lage
|
||||
|
||||
@@ -314,6 +298,7 @@ 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
|
||||
@@ -360,13 +345,7 @@ Always maintain newlines between import groups.
|
||||
|
||||
**Git Commands:**
|
||||
|
||||
- **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
|
||||
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete git safety rules, commit message requirements, and PR workflow.
|
||||
|
||||
## File Structure Patterns
|
||||
|
||||
@@ -529,7 +508,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
||||
|
||||
1. Clean build artifacts: `rm -rf packages/*/dist packages/*/lib-dist packages/*/build`
|
||||
2. Reinstall dependencies: `yarn install`
|
||||
3. Check Node.js version (requires >=20)
|
||||
3. Check Node.js version (requires >=22)
|
||||
4. Check Yarn version (requires ^4.9.1)
|
||||
|
||||
## Testing Patterns
|
||||
@@ -565,7 +544,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
||||
|
||||
Before committing changes, ensure:
|
||||
|
||||
- [ ] **MANDATORY: Commit message is prefixed with `[AI]`** - This is a hard requirement with no exceptions
|
||||
- [ ] Commit and PR rules followed (see [PR and Commit Rules](.github/agents/pr-and-commit-rules.md))
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
@@ -578,17 +557,7 @@ Before committing changes, ensure:
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Code Review Guidelines
|
||||
|
||||
@@ -619,7 +588,7 @@ yarn install:server
|
||||
|
||||
## Environment Requirements
|
||||
|
||||
- **Node.js**: >=20
|
||||
- **Node.js**: >=22
|
||||
- **Yarn**: ^4.9.1 (managed by packageManager field)
|
||||
- **Browser Targets**: Electron >= 35.0, modern browsers (see browserslist)
|
||||
|
||||
@@ -632,3 +601,40 @@ The codebase is actively being migrated:
|
||||
- **React.\* → Named Imports**: Legacy React.\* patterns being removed
|
||||
|
||||
When working with older code, follow the newer patterns described in this guide.
|
||||
|
||||
## Cursor Cloud specific instructions
|
||||
|
||||
### Services overview
|
||||
|
||||
| Service | Command | Port | Required |
|
||||
| ------------------- | ----------------------- | ---- | ----------------------------- |
|
||||
| Web Frontend (Vite) | `yarn start` | 3001 | Yes |
|
||||
| Sync Server | `yarn start:server-dev` | 5006 | Optional (sync features only) |
|
||||
|
||||
All storage is **SQLite** (file-based via `better-sqlite3`). No external databases or services are needed.
|
||||
|
||||
### Running the app
|
||||
|
||||
- `yarn start` builds the plugins-service worker, loot-core browser backend, and starts the Vite dev server on port **3001**.
|
||||
- `yarn start:server-dev` starts both the sync server (port 5006) and the web frontend together.
|
||||
- The Vite HMR dev server serves many unbundled modules. In constrained environments, the browser may hit `ERR_INSUFFICIENT_RESOURCES`. If that happens, use `yarn build:browser` followed by serving the built output from `packages/desktop-client/build/` with proper COOP/COEP headers (`Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Embedder-Policy: require-corp`).
|
||||
|
||||
### Lint, test, typecheck
|
||||
|
||||
Standard commands documented in `package.json` scripts and the Quick Start section above:
|
||||
|
||||
- `yarn lint` / `yarn lint:fix` (uses oxlint + oxfmt)
|
||||
- `yarn test` (lage across all workspaces)
|
||||
- `yarn typecheck` (tsc + lage typecheck)
|
||||
|
||||
### Testing and previewing the app
|
||||
|
||||
When running the app for manual testing or demos, use **"View demo"** on the initial setup screen (after selecting "Don't use a server"). This creates a test budget pre-populated with realistic sample data (accounts, transactions, categories, and budgeted amounts), which is far more useful than starting with an empty budget.
|
||||
|
||||
### Gotchas
|
||||
|
||||
- The `engines` field requires **Node.js >=22** and **Yarn ^4.9.1**. The `.nvmrc` specifies `v22/*`.
|
||||
- Pre-commit hook runs `lint-staged` (oxfmt + oxlint) via Husky. Run `yarn prepare` once after install to set up hooks.
|
||||
- Lage caches test results in `.lage/`. If tests behave unexpectedly, clear with `rm -rf .lage`.
|
||||
- Native modules (`better-sqlite3`, `bcrypt`) require build tools (`gcc`, `make`, `python3`). These are pre-installed in the Cloud VM.
|
||||
- All yarn commands must be run from the repository root, never from child workspaces.
|
||||
|
||||
@@ -59,6 +59,9 @@ yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
yarn workspace @actual-app/sync-server build
|
||||
|
||||
# Emit loot-core declarations so desktop-electron (which includes typings/window.ts) can build
|
||||
yarn workspace loot-core exec tsc -p tsconfig.json
|
||||
|
||||
yarn workspace desktop-electron update-client
|
||||
|
||||
(
|
||||
|
||||
@@ -54,10 +54,10 @@
|
||||
"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 --type-aware",
|
||||
"lint:fix": "oxfmt . && oxlint --fix --type-aware",
|
||||
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
|
||||
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
|
||||
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
||||
"typecheck": "tsc -p tsconfig.root.json --noEmit && lage typecheck",
|
||||
"typecheck": "tsc -b && tsc -p tsconfig.root.json --noEmit && lage typecheck",
|
||||
"jq": "./node_modules/node-jq/bin/jq",
|
||||
"prepare": "husky"
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
"oxfmt --no-error-on-unmatched-pattern"
|
||||
],
|
||||
"*.{js,mjs,jsx,ts,tsx}": [
|
||||
"oxlint --fix --type-aware"
|
||||
"oxlint --fix --type-aware --quiet"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -3,26 +3,18 @@ import type {
|
||||
RequestInit as FetchInit,
|
||||
} from 'node-fetch';
|
||||
|
||||
// loot-core types
|
||||
import type { InitConfig } from 'loot-core/server/main';
|
||||
import { init as initLootCore } from 'loot-core/server/main';
|
||||
import type { InitConfig, lib } 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';
|
||||
|
||||
export async function init(config: InitConfig = {}) {
|
||||
if (actualApp) {
|
||||
return;
|
||||
}
|
||||
/** @deprecated Please use return value of `init` instead */
|
||||
export let internal: typeof lib | null = null;
|
||||
|
||||
export async function init(config: InitConfig = {}) {
|
||||
validateNodeVersion();
|
||||
|
||||
if (!globalThis.fetch) {
|
||||
@@ -33,21 +25,19 @@ export async function init(config: InitConfig = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
await bundle.init(config);
|
||||
actualApp = bundle.lib;
|
||||
|
||||
injected.override(bundle.lib.send);
|
||||
return bundle.lib;
|
||||
internal = await initLootCore(config);
|
||||
return internal;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (actualApp) {
|
||||
if (internal) {
|
||||
try {
|
||||
await actualApp.send('sync');
|
||||
await internal.send('sync');
|
||||
} catch {
|
||||
// most likely that no budget is loaded, so the sync failed
|
||||
}
|
||||
await actualApp.send('close-budget');
|
||||
actualApp = null;
|
||||
|
||||
await internal.send('close-budget');
|
||||
internal = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// TODO: comment on why it works this way
|
||||
|
||||
export let send;
|
||||
|
||||
export function override(sendImplementation) {
|
||||
send = sendImplementation;
|
||||
}
|
||||
@@ -1,10 +1,29 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
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;
|
||||
|
||||
@@ -7,7 +7,9 @@ import type {
|
||||
APIScheduleEntity,
|
||||
APITagEntity,
|
||||
} from 'loot-core/server/api-models';
|
||||
import { lib } from 'loot-core/server/main';
|
||||
import type { Query } from 'loot-core/shared/query';
|
||||
import type { ImportTransactionsOpts } from 'loot-core/types/api-handlers';
|
||||
import type { Handlers } from 'loot-core/types/handlers';
|
||||
import type {
|
||||
ImportTransactionEntity,
|
||||
@@ -15,15 +17,13 @@ import type {
|
||||
TransactionEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import * as injected from './injected';
|
||||
|
||||
export { q } from './app/query';
|
||||
|
||||
function send<K extends keyof Handlers, T extends Handlers[K]>(
|
||||
name: K,
|
||||
args?: Parameters<T>[0],
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
return injected.send(name, args);
|
||||
return lib.send(name, args);
|
||||
}
|
||||
|
||||
export async function runImport(
|
||||
@@ -126,11 +126,6 @@ export function addTransactions(
|
||||
});
|
||||
}
|
||||
|
||||
export type ImportTransactionsOpts = {
|
||||
defaultCleared?: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
export function importTransactions(
|
||||
accountId: APIAccountEntity['id'],
|
||||
transactions: ImportTransactionEntity[],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "26.2.1",
|
||||
"version": "26.3.0",
|
||||
"description": "An API for Actual",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
@@ -10,27 +10,25 @@
|
||||
"main": "dist/index.js",
|
||||
"types": "@types/index.d.ts",
|
||||
"scripts": {
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:crdt": "yarn workspace @actual-app/crdt build",
|
||||
"build:node": "tsc && tsc-alias",
|
||||
"build:migrations": "mkdir dist/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"
|
||||
"build": "yarn workspace loot-core exec tsc && vite build && node scripts/inline-loot-core-types.mjs",
|
||||
"test": "vitest --run",
|
||||
"typecheck": "tsc --noEmit && tsc-strict"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"compare-versions": "^6.1.1",
|
||||
"loot-core": "workspace:^",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsc-alias": "^1.8.16",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-strict-plugin": "^2.4.4",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-peggy-loader": "^2.0.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
60
packages/api/scripts/inline-loot-core-types.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Post-build script: copies loot-core declaration tree into @types/loot-core
|
||||
* and rewrites index.d.ts to reference it so the published package is self-contained.
|
||||
* Run after vite build; requires loot-core declarations (yarn workspace loot-core exec tsc).
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const apiRoot = path.resolve(__dirname, '..');
|
||||
const typesDir = path.join(apiRoot, '@types');
|
||||
const indexDts = path.join(typesDir, 'index.d.ts');
|
||||
const lootCoreDeclRoot = path.resolve(apiRoot, '../loot-core/lib-dist/decl');
|
||||
const lootCoreDeclSrc = path.join(lootCoreDeclRoot, 'src');
|
||||
const lootCoreDeclTypings = path.join(lootCoreDeclRoot, 'typings');
|
||||
const lootCoreTypesDir = path.join(typesDir, 'loot-core');
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(indexDts)) {
|
||||
console.error('Missing @types/index.d.ts; run vite build first.');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!fs.existsSync(lootCoreDeclSrc)) {
|
||||
console.error(
|
||||
'Missing loot-core declarations; run: yarn workspace loot-core exec tsc',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Remove existing loot-core output (dir or legacy single file)
|
||||
if (fs.existsSync(lootCoreTypesDir)) {
|
||||
fs.rmSync(lootCoreTypesDir, { recursive: true });
|
||||
}
|
||||
const legacyDts = path.join(typesDir, 'loot-core.d.ts');
|
||||
if (fs.existsSync(legacyDts)) {
|
||||
fs.rmSync(legacyDts);
|
||||
}
|
||||
|
||||
// Copy declaration tree: src (main exports) plus emitted typings so no declarations are dropped
|
||||
fs.cpSync(lootCoreDeclSrc, lootCoreTypesDir, { recursive: true });
|
||||
if (fs.existsSync(lootCoreDeclTypings)) {
|
||||
fs.cpSync(lootCoreDeclTypings, path.join(lootCoreTypesDir, 'typings'), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Rewrite index.d.ts: remove reference, point imports at local ./loot-core/
|
||||
let indexContent = fs.readFileSync(indexDts, 'utf8');
|
||||
indexContent = indexContent.replace(
|
||||
/\/\/\/ <reference path="\.\/loot-core\.d\.ts" \/>\n?/,
|
||||
'',
|
||||
);
|
||||
indexContent = indexContent
|
||||
.replace(/'loot-core\//g, "'./loot-core/")
|
||||
.replace(/"loot-core\//g, '"./loot-core/');
|
||||
fs.writeFileSync(indexDts, indexContent, 'utf8');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,20 +1,22 @@
|
||||
{
|
||||
"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": "CommonJS",
|
||||
"moduleResolution": "node10",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"declarationDir": "@types",
|
||||
"paths": {
|
||||
"loot-core/*": ["./@types/loot-core/src/*"]
|
||||
},
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo",
|
||||
"plugins": [{ "name": "typescript-strict-plugin", "paths": ["."] }]
|
||||
},
|
||||
"include": [".", "../../packages/loot-core/typings/pegjs.ts"],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
|
||||
"references": [{ "path": "../crdt" }, { "path": "../loot-core" }],
|
||||
"include": ["."],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts", "*.config.ts"]
|
||||
}
|
||||
|
||||
2
packages/api/typings.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
declare module 'hyperformula/i18n/languages/enUS';
|
||||
declare module '*.pegjs';
|
||||
1
packages/api/typings/pegjs.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module '*.pegjs';
|
||||
@@ -1,6 +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';
|
||||
import { lib } from 'loot-core/server/main';
|
||||
|
||||
export const amountToInteger = bundle.lib.amountToInteger;
|
||||
export const integerToAmount = bundle.lib.integerToAmount;
|
||||
export const amountToInteger = lib.amountToInteger;
|
||||
export const integerToAmount = lib.integerToAmount;
|
||||
|
||||
99
packages/api/vite.config.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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'],
|
||||
alias: [
|
||||
{
|
||||
find: /^@actual-app\/crdt(\/.*)?$/,
|
||||
replacement: path.resolve(__dirname, '../crdt/src') + '$1',
|
||||
},
|
||||
],
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
maxWorkers: 2,
|
||||
},
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
export default {
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
maxWorkers: 2,
|
||||
},
|
||||
};
|
||||
@@ -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`.
|
||||
|
||||
const { spawnSync } = require('child_process');
|
||||
const path = require('path');
|
||||
import { spawnSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const migrationsDir = path.join(
|
||||
__dirname,
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'packages',
|
||||
@@ -16,7 +16,7 @@ const migrationsDir = path.join(
|
||||
'migrations',
|
||||
);
|
||||
|
||||
function readMigrations(ref) {
|
||||
function readMigrations(ref: string) {
|
||||
const { stdout } = spawnSync('git', [
|
||||
'ls-tree',
|
||||
'--name-only',
|
||||
@@ -3,9 +3,18 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest --run"
|
||||
"tsx": "node --import=extensionless/register --experimental-strip-types",
|
||||
"test": "vitest --run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"extensionless": "^2.0.6",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"extensionless": {
|
||||
"lookFor": [
|
||||
"ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
15
packages/ci-actions/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"types": ["node"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src/**/*", "bin/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"storybook": "^10.2.7",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -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();
|
||||
keys.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
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();
|
||||
keys.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
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,6 +1,7 @@
|
||||
{
|
||||
"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",
|
||||
@@ -8,8 +9,10 @@
|
||||
"moduleResolution": "node10",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strict": true,
|
||||
"outDir": "dist"
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
},
|
||||
"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: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,10 +1,18 @@
|
||||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "26.2.1",
|
||||
"version": "26.3.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"imports": {
|
||||
"#*": [
|
||||
"./src/*.ts",
|
||||
"./src/*.tsx",
|
||||
"./src/*/index.ts",
|
||||
"./src/*/index.tsx"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PORT=3001 vite",
|
||||
"start:browser": "cross-env ./bin/watch-browser",
|
||||
@@ -97,7 +105,6 @@
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.18",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import memoizeOne from 'memoize-one';
|
||||
import { groupById } from 'loot-core/shared/util';
|
||||
import type { AccountEntity } from 'loot-core/types/models';
|
||||
|
||||
import { resetApp } from '@desktop-client/app/appSlice';
|
||||
import { resetApp } from '#app/appSlice';
|
||||
|
||||
const sliceName = 'account';
|
||||
|
||||
|
||||
@@ -23,13 +23,13 @@ import {
|
||||
} from './accountsSlice';
|
||||
import { accountQueries } from './queries';
|
||||
|
||||
import { sync } from '@desktop-client/app/appSlice';
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { payeeQueries } from '@desktop-client/payees';
|
||||
import { useDispatch, useStore } from '@desktop-client/redux';
|
||||
import type { AppDispatch } from '@desktop-client/redux/store';
|
||||
import { setNewTransactions } from '@desktop-client/transactions/transactionsSlice';
|
||||
import { sync } from '#app/appSlice';
|
||||
import { useAccounts } from '#hooks/useAccounts';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { payeeQueries } from '#payees';
|
||||
import { useDispatch, useStore } from '#redux';
|
||||
import type { AppDispatch } from '#redux/store';
|
||||
import { setNewTransactions } from '#transactions/transactionsSlice';
|
||||
|
||||
const invalidateQueries = (queryClient: QueryClient, queryKey?: QueryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
|
||||
@@ -5,10 +5,10 @@ import { send } from 'loot-core/platform/client/connection';
|
||||
import { getUploadError } from 'loot-core/shared/errors';
|
||||
import type { AtLeastOne } from 'loot-core/types/util';
|
||||
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { prefQueries } from '@desktop-client/prefs';
|
||||
import { createAppAsyncThunk } from '@desktop-client/redux';
|
||||
import { getIsOutdated, getLatestVersion } from '@desktop-client/util/versions';
|
||||
import { pushModal } from '#modals/modalsSlice';
|
||||
import { loadPrefs } from '#prefs/prefsSlice';
|
||||
import { createAppAsyncThunk } from '#redux';
|
||||
import { getIsOutdated, getLatestVersion } from '#util/versions';
|
||||
|
||||
const sliceName = 'app';
|
||||
|
||||
@@ -89,8 +89,8 @@ export const resetSync = createAppAsyncThunk(
|
||||
|
||||
export const sync = createAppAsyncThunk(
|
||||
`${sliceName}/sync`,
|
||||
async (_, { extra: { queryClient } }) => {
|
||||
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||
async (_, { dispatch, getState }) => {
|
||||
const prefs = getState().prefs.local;
|
||||
if (prefs && prefs.id) {
|
||||
const result = await send('sync');
|
||||
if (result && 'error' in result) {
|
||||
@@ -98,9 +98,7 @@ export const sync = createAppAsyncThunk(
|
||||
}
|
||||
|
||||
// Update the prefs
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: prefQueries.lists(),
|
||||
});
|
||||
await dispatch(loadPrefs());
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -109,10 +107,8 @@ export const sync = createAppAsyncThunk(
|
||||
|
||||
export const getLatestAppVersion = createAppAsyncThunk(
|
||||
`${sliceName}/getLatestAppVersion`,
|
||||
async (_, { dispatch, extra: { queryClient } }) => {
|
||||
const globalPrefs = await queryClient.ensureQueryData(
|
||||
prefQueries.listGlobal(),
|
||||
);
|
||||
async (_, { dispatch, getState }) => {
|
||||
const globalPrefs = getState().prefs.global;
|
||||
if (globalPrefs && globalPrefs.notifyWhenUpdateIsAvailable) {
|
||||
const theLatestVersion = await getLatestVersion();
|
||||
dispatch(
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { ReactNode } from 'react';
|
||||
|
||||
import type { Permissions } from './types';
|
||||
|
||||
import { useServerURL } from '@desktop-client/components/ServerContext';
|
||||
import { useSelector } from '@desktop-client/redux';
|
||||
import { useServerURL } from '#components/ServerContext';
|
||||
import { useSelector } from '#redux';
|
||||
|
||||
type AuthContextType = {
|
||||
hasPermission: (permission?: Permissions) => boolean;
|
||||
|
||||
@@ -9,8 +9,8 @@ import type { RemoteFile, SyncedLocalFile } from 'loot-core/types/file';
|
||||
import { useAuth } from './AuthProvider';
|
||||
import type { Permissions } from './types';
|
||||
|
||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||
import { useSelector } from '@desktop-client/redux';
|
||||
import { useMetadataPref } from '#hooks/useMetadataPref';
|
||||
import { useSelector } from '#redux';
|
||||
|
||||
type ProtectedRouteProps = {
|
||||
permission: Permissions;
|
||||
|
||||
@@ -219,8 +219,8 @@ global.Actual = {
|
||||
return worker;
|
||||
},
|
||||
|
||||
setTheme: async theme => {
|
||||
await window.__actionsForMenu.setTheme(theme);
|
||||
setTheme: theme => {
|
||||
window.__actionsForMenu.saveGlobalPrefs({ prefs: { theme } });
|
||||
},
|
||||
|
||||
moveBudgetDirectory: () => {
|
||||
|
||||
@@ -14,10 +14,10 @@ import type {
|
||||
|
||||
import { categoryQueries } from './queries';
|
||||
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import type { AppDispatch } from '@desktop-client/redux/store';
|
||||
import { pushModal } from '#modals/modalsSlice';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { useDispatch } from '#redux';
|
||||
import type { AppDispatch } from '#redux/store';
|
||||
|
||||
function invalidateQueries(queryClient: QueryClient, queryKey?: QueryKey) {
|
||||
void queryClient.invalidateQueries({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { type QueryClient } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
@@ -10,11 +9,11 @@ import type { Budget } from 'loot-core/types/budget';
|
||||
import type { File } from 'loot-core/types/file';
|
||||
import type { Handlers } from 'loot-core/types/handlers';
|
||||
|
||||
import { resetApp, setAppState } from '@desktop-client/app/appSlice';
|
||||
import { closeModal, pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { prefQueries } from '@desktop-client/prefs';
|
||||
import { createAppAsyncThunk } from '@desktop-client/redux';
|
||||
import { signOut } from '@desktop-client/users/usersSlice';
|
||||
import { resetApp, setAppState } from '#app/appSlice';
|
||||
import { closeModal, pushModal } from '#modals/modalsSlice';
|
||||
import { loadGlobalPrefs, loadPrefs } from '#prefs/prefsSlice';
|
||||
import { createAppAsyncThunk } from '#redux';
|
||||
import { signOut } from '#users/usersSlice';
|
||||
|
||||
const sliceName = 'budgetfiles';
|
||||
|
||||
@@ -56,7 +55,7 @@ type LoadBudgetPayload = {
|
||||
|
||||
export const loadBudget = createAppAsyncThunk(
|
||||
`${sliceName}/loadBudget`,
|
||||
async ({ id, options = {} }: LoadBudgetPayload, { dispatch, extra }) => {
|
||||
async ({ id, options = {} }: LoadBudgetPayload, { dispatch }) => {
|
||||
dispatch(setAppState({ loadingText: t('Loading...') }));
|
||||
|
||||
// Loading a budget may fail
|
||||
@@ -90,43 +89,23 @@ export const loadBudget = createAppAsyncThunk(
|
||||
}
|
||||
} else {
|
||||
dispatch(closeModal());
|
||||
extra.queryClient.invalidateQueries({
|
||||
queryKey: prefQueries.lists(),
|
||||
});
|
||||
await dispatch(loadPrefs());
|
||||
}
|
||||
|
||||
dispatch(setAppState({ loadingText: null }));
|
||||
},
|
||||
);
|
||||
|
||||
function invalidateClosedBudgetQueries(queryClient: QueryClient) {
|
||||
// Invalidate all queries but do not cause a refetch.
|
||||
// This is because we want to clear out all the budget data from the queries,
|
||||
// but we don't want to trigger a bunch of error states from the queries trying
|
||||
// to fetch data for a budget that is now closed. The next time a budget is loaded,
|
||||
// the queries will refetch with the correct budget id.
|
||||
queryClient.invalidateQueries({
|
||||
refetchType: 'none',
|
||||
});
|
||||
|
||||
// Invalidate the metadata query since the budget is now closed.
|
||||
// We want to cause a refetch so that the app can update to show the correct state
|
||||
// (e.g. show the manager page if no budget is open).
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: prefQueries.listMetadata().queryKey,
|
||||
});
|
||||
}
|
||||
|
||||
export const closeBudget = createAppAsyncThunk(
|
||||
`${sliceName}/closeBudget`,
|
||||
async (_, { dispatch, extra: { queryClient } }) => {
|
||||
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||
async (_, { dispatch, getState, extra: { queryClient } }) => {
|
||||
const prefs = getState().prefs.local;
|
||||
if (prefs && prefs.id) {
|
||||
dispatch(resetApp());
|
||||
queryClient.clear();
|
||||
dispatch(setAppState({ loadingText: t('Closing...') }));
|
||||
await send('close-budget');
|
||||
dispatch(setAppState({ loadingText: null }));
|
||||
invalidateClosedBudgetQueries(queryClient);
|
||||
if (localStorage.getItem('SharedArrayBufferOverride')) {
|
||||
window.location.reload();
|
||||
}
|
||||
@@ -136,11 +115,11 @@ export const closeBudget = createAppAsyncThunk(
|
||||
|
||||
export const closeBudgetUI = createAppAsyncThunk(
|
||||
`${sliceName}/closeBudgetUI`,
|
||||
async (_, { dispatch, extra: { queryClient } }) => {
|
||||
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||
async (_, { dispatch, getState, extra: { queryClient } }) => {
|
||||
const prefs = getState().prefs.local;
|
||||
if (prefs && prefs.id) {
|
||||
dispatch(resetApp());
|
||||
invalidateClosedBudgetQueries(queryClient);
|
||||
queryClient.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -167,7 +146,7 @@ export const createBudget = createAppAsyncThunk(
|
||||
`${sliceName}/createBudget`,
|
||||
async (
|
||||
{ testMode = false, demoMode = false }: CreateBudgetPayload,
|
||||
{ dispatch, extra: { queryClient } },
|
||||
{ dispatch },
|
||||
) => {
|
||||
dispatch(
|
||||
setAppState({
|
||||
@@ -185,9 +164,7 @@ export const createBudget = createAppAsyncThunk(
|
||||
dispatch(closeModal());
|
||||
|
||||
await dispatch(loadAllFiles());
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: prefQueries.lists(),
|
||||
});
|
||||
await dispatch(loadPrefs());
|
||||
|
||||
// Set the loadingText to null after we've loaded the budget prefs
|
||||
// so that the existing manager page doesn't flash
|
||||
@@ -266,19 +243,14 @@ type ImportBudgetPayload = {
|
||||
|
||||
export const importBudget = createAppAsyncThunk(
|
||||
`${sliceName}/importBudget`,
|
||||
async (
|
||||
{ filepath, type }: ImportBudgetPayload,
|
||||
{ dispatch, extra: { queryClient } },
|
||||
) => {
|
||||
async ({ filepath, type }: ImportBudgetPayload, { dispatch }) => {
|
||||
const { error } = await send('import-budget', { filepath, type });
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
dispatch(closeModal());
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: prefQueries.lists(),
|
||||
});
|
||||
await dispatch(loadPrefs());
|
||||
},
|
||||
);
|
||||
|
||||
@@ -332,7 +304,7 @@ export const downloadBudget = createAppAsyncThunk(
|
||||
`${sliceName}/downloadBudget`,
|
||||
async (
|
||||
{ cloudFileId, replace = false }: DownloadBudgetPayload,
|
||||
{ dispatch, extra: { queryClient } },
|
||||
{ dispatch },
|
||||
): Promise<string | null> => {
|
||||
dispatch(
|
||||
setAppState({
|
||||
@@ -396,10 +368,8 @@ export const downloadBudget = createAppAsyncThunk(
|
||||
if (!id) {
|
||||
throw new Error('No id returned from download.');
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: prefQueries.listGlobal().queryKey,
|
||||
});
|
||||
await Promise.all([
|
||||
dispatch(loadGlobalPrefs()),
|
||||
dispatch(loadAllFiles()),
|
||||
dispatch(loadBudget({ id })),
|
||||
]);
|
||||
@@ -417,11 +387,8 @@ type LoadBackupPayload = {
|
||||
// Take in the budget id so that backups can be loaded when a budget isn't opened
|
||||
export const loadBackup = createAppAsyncThunk(
|
||||
`${sliceName}/loadBackup`,
|
||||
async (
|
||||
{ budgetId, backupId }: LoadBackupPayload,
|
||||
{ dispatch, extra: { queryClient } },
|
||||
) => {
|
||||
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||
async ({ budgetId, backupId }: LoadBackupPayload, { dispatch, getState }) => {
|
||||
const prefs = getState().prefs.local;
|
||||
if (prefs && prefs.id) {
|
||||
await dispatch(closeBudget());
|
||||
}
|
||||
@@ -433,8 +400,8 @@ export const loadBackup = createAppAsyncThunk(
|
||||
|
||||
export const makeBackup = createAppAsyncThunk(
|
||||
`${sliceName}/makeBackup`,
|
||||
async (_, { extra: { queryClient } }) => {
|
||||
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||
async (_, { getState }) => {
|
||||
const prefs = getState().prefs.local;
|
||||
if (prefs && prefs.id) {
|
||||
await send('backup-make', { id: prefs.id });
|
||||
}
|
||||
|
||||
@@ -26,27 +26,26 @@ import { Modals } from './Modals';
|
||||
import { SidebarProvider } from './sidebar/SidebarProvider';
|
||||
import { UpdateNotification } from './UpdateNotification';
|
||||
|
||||
import { setAppState, sync } from '@desktop-client/app/appSlice';
|
||||
import {
|
||||
closeBudget,
|
||||
loadBudget,
|
||||
} from '@desktop-client/budgetfiles/budgetfilesSlice';
|
||||
import { handleGlobalEvents } from '@desktop-client/global-events';
|
||||
import { useIsTestEnv } from '@desktop-client/hooks/useIsTestEnv';
|
||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||
import { SpreadsheetProvider } from '@desktop-client/hooks/useSpreadsheet';
|
||||
import { setI18NextLanguage } from '@desktop-client/i18n';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { installPolyfills } from '@desktop-client/polyfills';
|
||||
import { prefQueries } from '@desktop-client/prefs';
|
||||
import { useDispatch, useSelector, useStore } from '@desktop-client/redux';
|
||||
import { setAppState, sync } from '#app/appSlice';
|
||||
import { closeBudget, loadBudget } from '#budgetfiles/budgetfilesSlice';
|
||||
import { handleGlobalEvents } from '#global-events';
|
||||
import { useIsTestEnv } from '#hooks/useIsTestEnv';
|
||||
import { useMetadataPref } from '#hooks/useMetadataPref';
|
||||
import { useOnVisible } from '#hooks/useOnVisible';
|
||||
import { SpreadsheetProvider } from '#hooks/useSpreadsheet';
|
||||
import { setI18NextLanguage } from '#i18n';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { installPolyfills } from '#polyfills';
|
||||
import { loadGlobalPrefs } from '#prefs/prefsSlice';
|
||||
import { useDispatch, useSelector, useStore } from '#redux';
|
||||
import {
|
||||
CustomThemeStyle,
|
||||
hasHiddenScrollbars,
|
||||
ThemeStyle,
|
||||
useTheme,
|
||||
} from '@desktop-client/style';
|
||||
import { signOut } from '@desktop-client/users/usersSlice';
|
||||
import { ExposeNavigate } from '@desktop-client/util/router-tools';
|
||||
} from '#style';
|
||||
import { signOut } from '#users/usersSlice';
|
||||
import { ExposeNavigate } from '#util/router-tools';
|
||||
|
||||
function AppInner() {
|
||||
const [budgetId] = useMetadataPref('id');
|
||||
@@ -60,8 +59,6 @@ function AppInner() {
|
||||
setI18NextLanguage(null);
|
||||
}, []);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
const maybeUpdate = async <T,>(cb?: () => T): Promise<T | void> => {
|
||||
if (global.Actual.isUpdateReadyForDownload()) {
|
||||
@@ -92,7 +89,7 @@ function AppInner() {
|
||||
loadingText: t('Loading global preferences...'),
|
||||
}),
|
||||
);
|
||||
void queryClient.prefetchQuery(prefQueries.listGlobal());
|
||||
await dispatch(loadGlobalPrefs());
|
||||
|
||||
// Open the last opened budget, if any
|
||||
dispatch(
|
||||
@@ -180,6 +177,11 @@ export function App() {
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useOnVisible(async () => {
|
||||
console.debug('triggering sync because of visibility change');
|
||||
await dispatch(sync());
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function checkScrollbars() {
|
||||
if (hiddenScrollbars !== hasHiddenScrollbars()) {
|
||||
@@ -187,25 +189,9 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
let isSyncing = false;
|
||||
|
||||
async function onVisibilityChange() {
|
||||
if (!isSyncing) {
|
||||
console.debug('triggering sync because of visibility change');
|
||||
isSyncing = true;
|
||||
await dispatch(sync());
|
||||
isSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('focus', checkScrollbars);
|
||||
window.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', checkScrollbars);
|
||||
window.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
};
|
||||
}, [dispatch, hiddenScrollbars]);
|
||||
return () => window.removeEventListener('focus', checkScrollbars);
|
||||
}, [hiddenScrollbars]);
|
||||
|
||||
const [theme] = useTheme();
|
||||
|
||||
@@ -240,6 +226,7 @@ export function App() {
|
||||
<AppInner />
|
||||
</ErrorBoundary>
|
||||
<ThemeStyle />
|
||||
<CustomThemeStyle />
|
||||
<ErrorBoundary FallbackComponent={FatalError}>
|
||||
<Modals />
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { Background } from './Background';
|
||||
|
||||
import { useSelector } from '@desktop-client/redux';
|
||||
import { useSelector } from '#redux';
|
||||
|
||||
type AppBackgroundProps = {
|
||||
isLoading?: boolean;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { View } from '@actual-app/components/view';
|
||||
|
||||
import { AnimatedRefresh } from './AnimatedRefresh';
|
||||
|
||||
import { useSelector } from '@desktop-client/redux';
|
||||
import { useSelector } from '#redux';
|
||||
|
||||
export function BankSyncStatus() {
|
||||
const accountsSyncing = useSelector(state => state.account.accountsSyncing);
|
||||
|
||||
@@ -24,23 +24,19 @@ import { Command } from 'cmdk';
|
||||
|
||||
import { CellValue, CellValueText } from './spreadsheet/CellValue';
|
||||
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { useDashboardPages } from '@desktop-client/hooks/useDashboardPages';
|
||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||
import { useModalState } from '@desktop-client/hooks/useModalState';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { useReports } from '@desktop-client/hooks/useReports';
|
||||
import type {
|
||||
Binding,
|
||||
SheetFields,
|
||||
SheetNames,
|
||||
} from '@desktop-client/spreadsheet';
|
||||
import { useAccounts } from '#hooks/useAccounts';
|
||||
import { useDashboardPages } from '#hooks/useDashboardPages';
|
||||
import { useMetadataPref } from '#hooks/useMetadataPref';
|
||||
import { useModalState } from '#hooks/useModalState';
|
||||
import { useNavigate } from '#hooks/useNavigate';
|
||||
import { useReports } from '#hooks/useReports';
|
||||
import type { Binding, SheetFields, SheetNames } from '#spreadsheet';
|
||||
import {
|
||||
accountBalance,
|
||||
allAccountBalance,
|
||||
offBudgetAccountBalance,
|
||||
onBudgetAccountBalance,
|
||||
} from '@desktop-client/spreadsheet/bindings';
|
||||
} from '#spreadsheet/bindings';
|
||||
|
||||
type SearchableItem = {
|
||||
id: string;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Link } from './common/Link';
|
||||
import { Modal, ModalHeader } from './common/Modal';
|
||||
import { Checkbox } from './forms';
|
||||
|
||||
import { useModalState } from '@desktop-client/hooks/useModalState';
|
||||
import { useModalState } from '#hooks/useModalState';
|
||||
|
||||
type AppError = Error & {
|
||||
type?: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Navigate, Route, Routes, useHref, useLocation } from 'react-router';
|
||||
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { usePrefetchQuery } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import * as undo from 'loot-core/platform/client/undo';
|
||||
|
||||
@@ -28,19 +28,18 @@ import { FloatableSidebar } from './sidebar';
|
||||
import { ManageTagsPage } from './tags/ManageTagsPage';
|
||||
import { Titlebar } from './Titlebar';
|
||||
|
||||
import { getLatestAppVersion, sync } from '@desktop-client/app/appSlice';
|
||||
import { ProtectedRoute } from '@desktop-client/auth/ProtectedRoute';
|
||||
import { Permissions } from '@desktop-client/auth/types';
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||
import { useMetaThemeColor } from '@desktop-client/hooks/useMetaThemeColor';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { ScrollProvider } from '@desktop-client/hooks/useScrollListener';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { prefQueries } from '@desktop-client/prefs';
|
||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
||||
import { CustomThemeStyle } from '@desktop-client/style';
|
||||
import { accountQueries } from '#accounts';
|
||||
import { getLatestAppVersion, sync } from '#app/appSlice';
|
||||
import { ProtectedRoute } from '#auth/ProtectedRoute';
|
||||
import { Permissions } from '#auth/types';
|
||||
import { useAccounts } from '#hooks/useAccounts';
|
||||
import { useGlobalPref } from '#hooks/useGlobalPref';
|
||||
import { useLocalPref } from '#hooks/useLocalPref';
|
||||
import { useMetaThemeColor } from '#hooks/useMetaThemeColor';
|
||||
import { useNavigate } from '#hooks/useNavigate';
|
||||
import { ScrollProvider } from '#hooks/useScrollListener';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { useDispatch, useSelector } from '#redux';
|
||||
|
||||
function NarrowNotSupported({
|
||||
redirectTo = '/budget',
|
||||
@@ -197,7 +196,6 @@ export function FinancesApp() {
|
||||
<RouterBehaviors />
|
||||
<GlobalKeys />
|
||||
<CommandBar />
|
||||
<CustomThemeStyle />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
||||
|
||||
import * as Platform from 'loot-core/shared/platform';
|
||||
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { useNavigate } from '#hooks/useNavigate';
|
||||
|
||||
export function GlobalKeys() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -10,9 +10,9 @@ import { Popover } from '@actual-app/components/popover';
|
||||
import { SpaceBetween } from '@actual-app/components/space-between';
|
||||
import { useToggle } from 'usehooks-ts';
|
||||
|
||||
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import { useFeatureFlag } from '#hooks/useFeatureFlag';
|
||||
import { pushModal } from '#modals/modalsSlice';
|
||||
import { useDispatch } from '#redux';
|
||||
|
||||
const getPageDocs = (page: string) => {
|
||||
switch (page) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { styles } from '@actual-app/components/styles';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { listen } from 'loot-core/platform/client/connection';
|
||||
import type { RemoteFile, SyncedLocalFile } from 'loot-core/types/file';
|
||||
@@ -19,13 +18,13 @@ import type { TransObjectLiteral } from 'loot-core/types/util';
|
||||
import { PrivacyFilter } from './PrivacyFilter';
|
||||
import { useMultiuserEnabled, useServerURL } from './ServerContext';
|
||||
|
||||
import { useAuth } from '@desktop-client/auth/AuthProvider';
|
||||
import { Permissions } from '@desktop-client/auth/types';
|
||||
import { closeBudget } from '@desktop-client/budgetfiles/budgetfilesSlice';
|
||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
||||
import { getUserData, signOut } from '@desktop-client/users/usersSlice';
|
||||
import { useAuth } from '#auth/AuthProvider';
|
||||
import { Permissions } from '#auth/types';
|
||||
import { closeBudget } from '#budgetfiles/budgetfilesSlice';
|
||||
import { useMetadataPref } from '#hooks/useMetadataPref';
|
||||
import { useNavigate } from '#hooks/useNavigate';
|
||||
import { useDispatch, useSelector } from '#redux';
|
||||
import { getUserData, signOut } from '#users/usersSlice';
|
||||
|
||||
type LoggedInUserProps = {
|
||||
hideIfNoServer?: boolean;
|
||||
@@ -56,6 +55,7 @@ export function LoggedInUser({
|
||||
f => f.state === 'remote' || f.state === 'synced' || f.state === 'detached',
|
||||
) as (SyncedLocalFile | RemoteFile)[];
|
||||
const currentFile = remoteFiles.find(f => f.cloudFileId === cloudFileId);
|
||||
const hasSyncedPrefs = useSelector(state => state.prefs.synced);
|
||||
|
||||
const initializeUserData = useCallback(async () => {
|
||||
try {
|
||||
@@ -235,7 +235,7 @@ export function LoggedInUser({
|
||||
multiuserEnabled &&
|
||||
userData &&
|
||||
userData?.displayName &&
|
||||
!budgetId && (
|
||||
!hasSyncedPrefs && (
|
||||
<small>
|
||||
(
|
||||
<Trans>
|
||||
@@ -251,7 +251,7 @@ export function LoggedInUser({
|
||||
multiuserEnabled &&
|
||||
userData &&
|
||||
userData?.displayName &&
|
||||
budgetId && (
|
||||
hasSyncedPrefs && (
|
||||
<small>
|
||||
(
|
||||
<Trans>
|
||||
|
||||
@@ -28,16 +28,13 @@ import { Search } from './common/Search';
|
||||
import { RulesHeader } from './rules/RulesHeader';
|
||||
import { RulesList } from './rules/RulesList';
|
||||
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
import { usePayees } from '@desktop-client/hooks/usePayees';
|
||||
import { useSchedules } from '@desktop-client/hooks/useSchedules';
|
||||
import {
|
||||
SelectedProvider,
|
||||
useSelected,
|
||||
} from '@desktop-client/hooks/useSelected';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import { useAccounts } from '#hooks/useAccounts';
|
||||
import { useCategories } from '#hooks/useCategories';
|
||||
import { usePayees } from '#hooks/usePayees';
|
||||
import { useSchedules } from '#hooks/useSchedules';
|
||||
import { SelectedProvider, useSelected } from '#hooks/useSelected';
|
||||
import { pushModal } from '#modals/modalsSlice';
|
||||
import { useDispatch } from '#redux';
|
||||
|
||||
export type FilterData = {
|
||||
payees?: Array<{ id: string; name: string }>;
|
||||
|
||||
@@ -78,11 +78,11 @@ import { ScheduleEditModal } from './schedules/ScheduleEditModal';
|
||||
import { ScheduleLink } from './schedules/ScheduleLink';
|
||||
import { UpcomingLength } from './schedules/UpcomingLength';
|
||||
|
||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||
import { useModalState } from '@desktop-client/hooks/useModalState';
|
||||
import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
|
||||
import { closeModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import { useMetadataPref } from '#hooks/useMetadataPref';
|
||||
import { useModalState } from '#hooks/useModalState';
|
||||
import { SheetNameProvider } from '#hooks/useSheetName';
|
||||
import { closeModal } from '#modals/modalsSlice';
|
||||
import { useDispatch } from '#redux';
|
||||
|
||||
export function Modals() {
|
||||
const location = useLocation();
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
markdownBaseStyles,
|
||||
remarkBreaks,
|
||||
sequentialNewlinesPlugin,
|
||||
} from '@desktop-client/util/markdown';
|
||||
} from '#util/markdown';
|
||||
|
||||
const remarkPlugins = [sequentialNewlinesPlugin, remarkGfm, remarkBreaks];
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { send } from 'loot-core/platform/client/connection';
|
||||
|
||||
import { Notes } from './Notes';
|
||||
|
||||
import { useNotes } from '@desktop-client/hooks/useNotes';
|
||||
import { useNotes } from '#hooks/useNotes';
|
||||
|
||||
type NotesButtonProps = {
|
||||
id: string;
|
||||
|
||||
@@ -18,9 +18,9 @@ import { css } from '@emotion/css';
|
||||
import { Link } from './common/Link';
|
||||
import { MODAL_Z_INDEX } from './common/Modal';
|
||||
|
||||
import { removeNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import type { NotificationWithId } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
||||
import { removeNotification } from '#notifications/notificationsSlice';
|
||||
import type { NotificationWithId } from '#notifications/notificationsSlice';
|
||||
import { useDispatch, useSelector } from '#redux';
|
||||
|
||||
// Notification stacking configuration
|
||||
const MAX_VISIBLE_NOTIFICATIONS = 3; // Maximum number of notifications visible in the stack
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { usePrivacyMode } from '@desktop-client/hooks/usePrivacyMode';
|
||||
import { usePrivacyMode } from '#hooks/usePrivacyMode';
|
||||
|
||||
type ConditionalPrivacyFilterProps = {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -12,8 +12,9 @@ import { t } from 'i18next';
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
import type { Handlers } from 'loot-core/types/handlers';
|
||||
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import { useOnVisible } from '#hooks/useOnVisible';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { useDispatch } from '#redux';
|
||||
|
||||
type LoginMethod = {
|
||||
method: string;
|
||||
@@ -110,6 +111,16 @@ export function ServerProvider({ children }: { children: ReactNode }) {
|
||||
void run();
|
||||
}, []);
|
||||
|
||||
useOnVisible(
|
||||
async () => {
|
||||
const version = await getServerVersion();
|
||||
setVersion(version);
|
||||
},
|
||||
{
|
||||
isEnabled: !!serverURL,
|
||||
},
|
||||
);
|
||||
|
||||
const refreshLoginMethods = useCallback(async () => {
|
||||
if (serverURL) {
|
||||
const data: Awaited<ReturnType<Handlers['subscribe-get-login-methods']>> =
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Popover } from '@actual-app/components/popover';
|
||||
|
||||
import type { Theme } from 'loot-core/types/prefs';
|
||||
|
||||
import { themeOptions, useTheme } from '@desktop-client/style';
|
||||
import { themeOptions, useTheme } from '#style';
|
||||
|
||||
type ThemeSelectorProps = {
|
||||
style?: CSSProperties;
|
||||
|
||||
@@ -34,15 +34,15 @@ import { useServerURL } from './ServerContext';
|
||||
import { useSidebar } from './sidebar/SidebarProvider';
|
||||
import { ThemeSelector } from './ThemeSelector';
|
||||
|
||||
import { sync } from '@desktop-client/app/appSlice';
|
||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||
import { useIsTestEnv } from '@desktop-client/hooks/useIsTestEnv';
|
||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
|
||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import * as bindings from '@desktop-client/spreadsheet/bindings';
|
||||
import { sync } from '#app/appSlice';
|
||||
import { useGlobalPref } from '#hooks/useGlobalPref';
|
||||
import { useIsTestEnv } from '#hooks/useIsTestEnv';
|
||||
import { useMetadataPref } from '#hooks/useMetadataPref';
|
||||
import { useNavigate } from '#hooks/useNavigate';
|
||||
import { useSheetValue } from '#hooks/useSheetValue';
|
||||
import { useSyncedPref } from '#hooks/useSyncedPref';
|
||||
import { useDispatch } from '#redux';
|
||||
import * as bindings from '#spreadsheet/bindings';
|
||||
|
||||
function UncategorizedButton() {
|
||||
const count: number | null = useSheetValue(bindings.uncategorizedCount());
|
||||
@@ -106,11 +106,11 @@ function PrivacyButton({ style }: PrivacyButtonProps) {
|
||||
);
|
||||
}
|
||||
|
||||
type SyncButtonProps = {
|
||||
type ServerSyncButtonProps = {
|
||||
style?: CSSProperties;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
function ServerSyncButton({ style, isMobile = false }: ServerSyncButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const [cloudFileId] = useMetadataPref('cloudFileId');
|
||||
const dispatch = useDispatch();
|
||||
@@ -166,7 +166,7 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
: syncState === 'disabled' ||
|
||||
syncState === 'offline' ||
|
||||
syncState === 'local'
|
||||
? theme.tableTextLight
|
||||
? theme.buttonBareDisabledText
|
||||
: 'inherit';
|
||||
|
||||
const activeStyle = isMobile
|
||||
@@ -213,7 +213,7 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
variant="bare"
|
||||
aria-label={t('Sync')}
|
||||
aria-label={t('Server Sync')}
|
||||
className={css({
|
||||
...(isMobile
|
||||
? {
|
||||
@@ -230,6 +230,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
'&[data-pressed]': activeStyle,
|
||||
})}
|
||||
onPress={onSync}
|
||||
isDisabled={syncState === 'offline'}
|
||||
aria-disabled={syncState === 'offline'}
|
||||
>
|
||||
{isMobile ? (
|
||||
syncState === 'error' ? (
|
||||
@@ -243,11 +245,7 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
<AnimatedRefresh animating={syncing} />
|
||||
)}
|
||||
<Text style={isMobile ? { ...mobileTextStyle } : { marginLeft: 3 }}>
|
||||
{syncState === 'disabled'
|
||||
? t('Disabled')
|
||||
: syncState === 'offline'
|
||||
? t('Offline')
|
||||
: t('Sync')}
|
||||
{syncState === 'disabled' ? t('Disabled') : null}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
@@ -346,7 +344,7 @@ export function Titlebar({ style }: TitlebarProps) {
|
||||
<UncategorizedButton />
|
||||
{isDevelopmentEnvironment() && !isTestEnv && <ThemeSelector />}
|
||||
<PrivacyButton />
|
||||
{serverURL ? <SyncButton /> : null}
|
||||
{serverURL ? <ServerSyncButton /> : null}
|
||||
<LoggedInUser />
|
||||
<HelpMenu />
|
||||
</SpaceBetween>
|
||||
|
||||
@@ -9,8 +9,8 @@ import { View } from '@actual-app/components/view';
|
||||
|
||||
import { Link } from './common/Link';
|
||||
|
||||
import { setAppState, updateApp } from '@desktop-client/app/appSlice';
|
||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
||||
import { setAppState, updateApp } from '#app/appSlice';
|
||||
import { useDispatch, useSelector } from '#redux';
|
||||
|
||||
export function UpdateNotification() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -46,44 +46,44 @@ import {
|
||||
useSyncAndDownloadMutation,
|
||||
useUnlinkAccountMutation,
|
||||
useUpdateAccountMutation,
|
||||
} from '@desktop-client/accounts';
|
||||
import { markAccountRead } from '@desktop-client/accounts/accountsSlice';
|
||||
import type { SavedFilter } from '@desktop-client/components/filters/SavedFilterMenuButton';
|
||||
import { TransactionList } from '@desktop-client/components/transactions/TransactionList';
|
||||
import { validateAccountName } from '@desktop-client/components/util/accountValidation';
|
||||
import { useAccountPreviewTransactions } from '@desktop-client/hooks/useAccountPreviewTransactions';
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
||||
import { useFailedAccounts } from '@desktop-client/hooks/useFailedAccounts';
|
||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||
import { usePayees } from '@desktop-client/hooks/usePayees';
|
||||
import { getSchedulesQuery } from '@desktop-client/hooks/useSchedules';
|
||||
import { SelectedProviderWithItems } from '@desktop-client/hooks/useSelected';
|
||||
import type { Actions } from '@desktop-client/hooks/useSelected';
|
||||
} from '#accounts';
|
||||
import { markAccountRead } from '#accounts/accountsSlice';
|
||||
import type { SavedFilter } from '#components/filters/SavedFilterMenuButton';
|
||||
import { TransactionList } from '#components/transactions/TransactionList';
|
||||
import { validateAccountName } from '#components/util/accountValidation';
|
||||
import { useAccountPreviewTransactions } from '#hooks/useAccountPreviewTransactions';
|
||||
import { useAccounts } from '#hooks/useAccounts';
|
||||
import { SchedulesProvider } from '#hooks/useCachedSchedules';
|
||||
import { useCategories } from '#hooks/useCategories';
|
||||
import { useDateFormat } from '#hooks/useDateFormat';
|
||||
import { useFailedAccounts } from '#hooks/useFailedAccounts';
|
||||
import { useLocalPref } from '#hooks/useLocalPref';
|
||||
import { usePayees } from '#hooks/usePayees';
|
||||
import { getSchedulesQuery } from '#hooks/useSchedules';
|
||||
import { SelectedProviderWithItems } from '#hooks/useSelected';
|
||||
import type { Actions } from '#hooks/useSelected';
|
||||
import {
|
||||
SplitsExpandedProvider,
|
||||
useSplitsExpanded,
|
||||
} from '@desktop-client/hooks/useSplitsExpanded';
|
||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||
import { useTransactionBatchActions } from '@desktop-client/hooks/useTransactionBatchActions';
|
||||
import { useTransactionFilters } from '@desktop-client/hooks/useTransactionFilters';
|
||||
import { calculateRunningBalancesBottomUp } from '@desktop-client/hooks/useTransactions';
|
||||
} from '#hooks/useSplitsExpanded';
|
||||
import { useSyncedPref } from '#hooks/useSyncedPref';
|
||||
import { useTransactionBatchActions } from '#hooks/useTransactionBatchActions';
|
||||
import { useTransactionFilters } from '#hooks/useTransactionFilters';
|
||||
import { calculateRunningBalancesBottomUp } from '#hooks/useTransactions';
|
||||
import {
|
||||
openAccountCloseModal,
|
||||
pushModal,
|
||||
replaceModal,
|
||||
} from '@desktop-client/modals/modalsSlice';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useCreatePayeeMutation } from '@desktop-client/payees';
|
||||
import * as queries from '@desktop-client/queries';
|
||||
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
|
||||
import { pagedQuery } from '@desktop-client/queries/pagedQuery';
|
||||
import type { PagedQuery } from '@desktop-client/queries/pagedQuery';
|
||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
||||
import type { AppDispatch } from '@desktop-client/redux/store';
|
||||
import { updateNewTransactions } from '@desktop-client/transactions/transactionsSlice';
|
||||
} from '#modals/modalsSlice';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { useCreatePayeeMutation } from '#payees';
|
||||
import * as queries from '#queries';
|
||||
import { aqlQuery } from '#queries/aqlQuery';
|
||||
import { pagedQuery } from '#queries/pagedQuery';
|
||||
import type { PagedQuery } from '#queries/pagedQuery';
|
||||
import { useDispatch, useSelector } from '#redux';
|
||||
import type { AppDispatch } from '#redux/store';
|
||||
import { updateNewTransactions } from '#transactions/transactionsSlice';
|
||||
|
||||
type ConditionEntity = Partial<RuleConditionEntity> | TransactionFilterEntity;
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import { View } from '@actual-app/components/view';
|
||||
|
||||
import type { AccountEntity } from 'loot-core/types/models';
|
||||
|
||||
import { useUnlinkAccountMutation } from '@desktop-client/accounts';
|
||||
import { Link } from '@desktop-client/components/common/Link';
|
||||
import { authorizeBank } from '@desktop-client/gocardless';
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { useFailedAccounts } from '@desktop-client/hooks/useFailedAccounts';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import { useUnlinkAccountMutation } from '#accounts';
|
||||
import { Link } from '#components/common/Link';
|
||||
import { authorizeBank } from '#gocardless';
|
||||
import { useAccounts } from '#hooks/useAccounts';
|
||||
import { useFailedAccounts } from '#hooks/useFailedAccounts';
|
||||
import { useDispatch } from '#redux';
|
||||
|
||||
function useErrorMessage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -68,6 +68,11 @@ function useErrorMessage() {
|
||||
</Trans>
|
||||
);
|
||||
|
||||
case 'ACCOUNT_MISSING':
|
||||
return t(
|
||||
'This account was not found in SimpleFIN. Try unlinking and relinking the account.',
|
||||
);
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
|
||||
@@ -15,17 +15,14 @@ import { getScheduledAmount } from 'loot-core/shared/schedules';
|
||||
import { isPreviewId } from 'loot-core/shared/transactions';
|
||||
import type { AccountEntity } from 'loot-core/types/models';
|
||||
|
||||
import { FinancialText } from '@desktop-client/components/FinancialText';
|
||||
import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
|
||||
import {
|
||||
CellValue,
|
||||
CellValueText,
|
||||
} from '@desktop-client/components/spreadsheet/CellValue';
|
||||
import { useCachedSchedules } from '@desktop-client/hooks/useCachedSchedules';
|
||||
import { useFormat } from '@desktop-client/hooks/useFormat';
|
||||
import { useSelectedItems } from '@desktop-client/hooks/useSelected';
|
||||
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
|
||||
import type { Binding } from '@desktop-client/spreadsheet';
|
||||
import { FinancialText } from '#components/FinancialText';
|
||||
import { PrivacyFilter } from '#components/PrivacyFilter';
|
||||
import { CellValue, CellValueText } from '#components/spreadsheet/CellValue';
|
||||
import { useCachedSchedules } from '#hooks/useCachedSchedules';
|
||||
import { useFormat } from '#hooks/useFormat';
|
||||
import { useSelectedItems } from '#hooks/useSelected';
|
||||
import { useSheetValue } from '#hooks/useSheetValue';
|
||||
import type { Binding } from '#spreadsheet';
|
||||
|
||||
type DetailedBalanceProps = {
|
||||
name: string;
|
||||
|
||||
@@ -13,12 +13,12 @@ import { Area, AreaChart, Tooltip as RechartsTooltip, YAxis } from 'recharts';
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import { integerToCurrency } from 'loot-core/shared/util';
|
||||
|
||||
import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
|
||||
import { useRechartsAnimation } from '@desktop-client/components/reports/chart-theme';
|
||||
import { LoadingIndicator } from '@desktop-client/components/reports/LoadingIndicator';
|
||||
import { useLocale } from '@desktop-client/hooks/useLocale';
|
||||
import * as query from '@desktop-client/queries';
|
||||
import { liveQuery } from '@desktop-client/queries/liveQuery';
|
||||
import { PrivacyFilter } from '#components/PrivacyFilter';
|
||||
import { useRechartsAnimation } from '#components/reports/chart-theme';
|
||||
import { LoadingIndicator } from '#components/reports/LoadingIndicator';
|
||||
import { useLocale } from '#hooks/useLocale';
|
||||
import * as query from '#queries';
|
||||
import { liveQuery } from '#queries/liveQuery';
|
||||
|
||||
const LABEL_WIDTH = 70;
|
||||
|
||||
|
||||
@@ -41,19 +41,19 @@ import { Balances } from './Balance';
|
||||
import { BalanceHistoryGraph } from './BalanceHistoryGraph';
|
||||
import { ReconcileMenu, ReconcilingMessage } from './Reconcile';
|
||||
|
||||
import { AnimatedRefresh } from '@desktop-client/components/AnimatedRefresh';
|
||||
import { Search } from '@desktop-client/components/common/Search';
|
||||
import { FilterButton } from '@desktop-client/components/filters/FiltersMenu';
|
||||
import { FiltersStack } from '@desktop-client/components/filters/FiltersStack';
|
||||
import type { SavedFilter } from '@desktop-client/components/filters/SavedFilterMenuButton';
|
||||
import { NotesButton } from '@desktop-client/components/NotesButton';
|
||||
import { SelectedTransactionsButton } from '@desktop-client/components/transactions/SelectedTransactionsButton';
|
||||
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
||||
import { useLocale } from '@desktop-client/hooks/useLocale';
|
||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||
import { useSplitsExpanded } from '@desktop-client/hooks/useSplitsExpanded';
|
||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||
import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus';
|
||||
import { AnimatedRefresh } from '#components/AnimatedRefresh';
|
||||
import { Search } from '#components/common/Search';
|
||||
import { FilterButton } from '#components/filters/FiltersMenu';
|
||||
import { FiltersStack } from '#components/filters/FiltersStack';
|
||||
import type { SavedFilter } from '#components/filters/SavedFilterMenuButton';
|
||||
import { NotesButton } from '#components/NotesButton';
|
||||
import { SelectedTransactionsButton } from '#components/transactions/SelectedTransactionsButton';
|
||||
import { useDateFormat } from '#hooks/useDateFormat';
|
||||
import { useLocale } from '#hooks/useLocale';
|
||||
import { useLocalPref } from '#hooks/useLocalPref';
|
||||
import { useSplitsExpanded } from '#hooks/useSplitsExpanded';
|
||||
import { useSyncedPref } from '#hooks/useSyncedPref';
|
||||
import { useSyncServerStatus } from '#hooks/useSyncServerStatus';
|
||||
|
||||
type AccountHeaderProps = {
|
||||
tableRef: TableRef;
|
||||
|
||||
@@ -9,10 +9,10 @@ import type { AccountEntity } from 'loot-core/types/models';
|
||||
|
||||
import { ReconcileMenu, ReconcilingMessage } from './Reconcile';
|
||||
|
||||
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
|
||||
import { TestProviders } from '@desktop-client/mocks';
|
||||
import { useSheetValue } from '#hooks/useSheetValue';
|
||||
import { TestProviders } from '#mocks';
|
||||
|
||||
vi.mock('@desktop-client/hooks/useSheetValue', () => ({
|
||||
vi.mock('#hooks/useSheetValue', () => ({
|
||||
useSheetValue: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ import { tsToRelativeTime } from 'loot-core/shared/util';
|
||||
import type { AccountEntity } from 'loot-core/types/models';
|
||||
import type { TransObjectLiteral } from 'loot-core/types/util';
|
||||
|
||||
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
||||
import { useFormat } from '@desktop-client/hooks/useFormat';
|
||||
import { useLocale } from '@desktop-client/hooks/useLocale';
|
||||
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
|
||||
import * as bindings from '@desktop-client/spreadsheet/bindings';
|
||||
import { useDateFormat } from '#hooks/useDateFormat';
|
||||
import { useFormat } from '#hooks/useFormat';
|
||||
import { useLocale } from '#hooks/useLocale';
|
||||
import { useSheetValue } from '#hooks/useSheetValue';
|
||||
import * as bindings from '#spreadsheet/bindings';
|
||||
|
||||
type ReconcilingMessageProps = {
|
||||
balanceQuery: { name: `balance-query-${string}`; query: Query };
|
||||
|
||||
@@ -19,13 +19,13 @@ import type { UserAccessEntity, UserAvailable } from 'loot-core/types/models';
|
||||
import { UserAccessHeader } from './UserAccessHeader';
|
||||
import { UserAccessRow } from './UserAccessRow';
|
||||
|
||||
import { InfiniteScrollWrapper } from '@desktop-client/components/common/InfiniteScrollWrapper';
|
||||
import { Link } from '@desktop-client/components/common/Link';
|
||||
import { Search } from '@desktop-client/components/common/Search';
|
||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import { InfiniteScrollWrapper } from '#components/common/InfiniteScrollWrapper';
|
||||
import { Link } from '#components/common/Link';
|
||||
import { Search } from '#components/common/Search';
|
||||
import { useMetadataPref } from '#hooks/useMetadataPref';
|
||||
import { pushModal } from '#modals/modalsSlice';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { useDispatch } from '#redux';
|
||||
|
||||
type ManageUserAccessContentProps = {
|
||||
isModal: boolean;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Cell, TableHeader } from '@desktop-client/components/table';
|
||||
import { Cell, TableHeader } from '#components/table';
|
||||
|
||||
export function UserAccessHeader() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { UserAccess } from './UserAccess';
|
||||
|
||||
import { Page } from '@desktop-client/components/Page';
|
||||
import { Page } from '#components/Page';
|
||||
|
||||
export function UserAccessPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||