Compare commits
48 Commits
v26.3.0
...
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 |
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));
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
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",
|
||||
|
||||
43
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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": "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,21 +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": ["."] }]
|
||||
},
|
||||
"references": [{ "path": "../crdt" }, { "path": "../loot-core" }],
|
||||
"include": ["."],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
|
||||
"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,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 |
@@ -5,6 +5,14 @@
|
||||
"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 { loadPrefs } from '@desktop-client/prefs/prefsSlice';
|
||||
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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -9,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 { loadGlobalPrefs, loadPrefs } from '@desktop-client/prefs/prefsSlice';
|
||||
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';
|
||||
|
||||
|
||||
@@ -26,28 +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 { loadGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
|
||||
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');
|
||||
@@ -179,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()) {
|
||||
@@ -186,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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,17 +28,18 @@ import { FloatableSidebar } from './sidebar';
|
||||
import { ManageTagsPage } from './tags/ManageTagsPage';
|
||||
import { Titlebar } from './Titlebar';
|
||||
|
||||
import { accountQueries } from '@desktop-client/accounts';
|
||||
import { getLatestAppVersion, sync } from '@desktop-client/app/appSlice';
|
||||
import { ProtectedRoute } from '@desktop-client/auth/ProtectedRoute';
|
||||
import { Permissions } from '@desktop-client/auth/types';
|
||||
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 { useDispatch, useSelector } from '@desktop-client/redux';
|
||||
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',
|
||||
@@ -91,10 +92,7 @@ export function FinancesApp() {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// TODO: Replace with `useAccounts` hook once it's updated to return the useQuery results.
|
||||
const { data: accounts, isFetching: isAccountsFetching } = useQuery(
|
||||
accountQueries.list(),
|
||||
);
|
||||
const { data: accounts, isFetching: isAccountsFetching } = useAccounts();
|
||||
|
||||
const versionInfo = useSelector(state => state.app.versionInfo);
|
||||
const [notifyWhenUpdateIsAvailable] = useGlobalPref(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -18,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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -9,12 +9,12 @@ import { send } from 'loot-core/platform/client/connection';
|
||||
import { getUserAccessErrors } from 'loot-core/shared/errors';
|
||||
import type { UserAvailable } from 'loot-core/types/models';
|
||||
|
||||
import { Checkbox } from '@desktop-client/components/forms';
|
||||
import { Cell, Row } from '@desktop-client/components/table';
|
||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import { signOut } from '@desktop-client/users/usersSlice';
|
||||
import { Checkbox } from '#components/forms';
|
||||
import { Cell, Row } from '#components/table';
|
||||
import { useMetadataPref } from '#hooks/useMetadataPref';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { useDispatch } from '#redux';
|
||||
import { signOut } from '#users/usersSlice';
|
||||
|
||||
type UserAccessProps = {
|
||||
access: UserAvailable;
|
||||
|
||||
@@ -17,17 +17,14 @@ import type { NewUserEntity, UserEntity } from 'loot-core/types/models';
|
||||
import { UserDirectoryHeader } from './UserDirectoryHeader';
|
||||
import { UserDirectoryRow } from './UserDirectoryRow';
|
||||
|
||||
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 {
|
||||
SelectedProvider,
|
||||
useSelected,
|
||||
} from '@desktop-client/hooks/useSelected';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
import { signOut } from '@desktop-client/users/usersSlice';
|
||||
import { InfiniteScrollWrapper } from '#components/common/InfiniteScrollWrapper';
|
||||
import { Link } from '#components/common/Link';
|
||||
import { Search } from '#components/common/Search';
|
||||
import { SelectedProvider, useSelected } from '#hooks/useSelected';
|
||||
import { pushModal } from '#modals/modalsSlice';
|
||||
import { addNotification } from '#notifications/notificationsSlice';
|
||||
import { useDispatch } from '#redux';
|
||||
import { signOut } from '#users/usersSlice';
|
||||
|
||||
type ManageUserDirectoryContentProps = {
|
||||
isModal: boolean;
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Cell,
|
||||
SelectCell,
|
||||
TableHeader,
|
||||
} from '@desktop-client/components/table';
|
||||
import {
|
||||
useSelectedDispatch,
|
||||
useSelectedItems,
|
||||
} from '@desktop-client/hooks/useSelected';
|
||||
import { Cell, SelectCell, TableHeader } from '#components/table';
|
||||
import { useSelectedDispatch, useSelectedItems } from '#hooks/useSelected';
|
||||
|
||||
export function UserDirectoryHeader() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -7,8 +7,8 @@ import { View } from '@actual-app/components/view';
|
||||
|
||||
import { UserDirectory } from './UserDirectory';
|
||||
|
||||
import { Page } from '@desktop-client/components/Page';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { Page } from '#components/Page';
|
||||
import { useNavigate } from '#hooks/useNavigate';
|
||||
|
||||
export function UserDirectoryPage({
|
||||
bottomContent,
|
||||
|
||||
@@ -9,9 +9,9 @@ import { View } from '@actual-app/components/view';
|
||||
import { PossibleRoles } from 'loot-core/shared/user';
|
||||
import type { UserEntity } from 'loot-core/types/models';
|
||||
|
||||
import { Checkbox } from '@desktop-client/components/forms';
|
||||
import { Cell, Row, SelectCell } from '@desktop-client/components/table';
|
||||
import { useSelectedDispatch } from '@desktop-client/hooks/useSelected';
|
||||
import { Checkbox } from '#components/forms';
|
||||
import { Cell, Row, SelectCell } from '#components/table';
|
||||
import { useSelectedDispatch } from '#hooks/useSelected';
|
||||
|
||||
type UserDirectoryProps = {
|
||||
user: UserEntity;
|
||||
|
||||
@@ -15,6 +15,7 @@ type AlertProps = {
|
||||
color?: string;
|
||||
backgroundColor?: string;
|
||||
style?: CSSProperties;
|
||||
iconStyle?: CSSProperties;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
@@ -23,6 +24,7 @@ const Alert = ({
|
||||
color,
|
||||
backgroundColor,
|
||||
style,
|
||||
iconStyle,
|
||||
children,
|
||||
}: AlertProps) => {
|
||||
return (
|
||||
@@ -48,21 +50,29 @@ const Alert = ({
|
||||
alignSelf: 'stretch',
|
||||
flexShrink: 0,
|
||||
marginRight: 5,
|
||||
...iconStyle,
|
||||
}}
|
||||
>
|
||||
<Icon width={13} style={{ marginTop: 2 }} />
|
||||
</View>
|
||||
<Text style={{ zIndex: 1, lineHeight: 1.5 }}>{children}</Text>
|
||||
<Text style={{ width: '100%', zIndex: 1, lineHeight: 1.5 }}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type ScopedAlertProps = {
|
||||
style?: CSSProperties;
|
||||
iconStyle?: CSSProperties;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const Information = ({ style, children }: ScopedAlertProps) => {
|
||||
export const Information = ({
|
||||
style,
|
||||
iconStyle,
|
||||
children,
|
||||
}: ScopedAlertProps) => {
|
||||
return (
|
||||
<Alert
|
||||
icon={SvgInformationOutline}
|
||||
@@ -73,32 +83,35 @@ export const Information = ({ style, children }: ScopedAlertProps) => {
|
||||
padding: 5,
|
||||
...style,
|
||||
}}
|
||||
iconStyle={iconStyle}
|
||||
>
|
||||
{children}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export const Warning = ({ style, children }: ScopedAlertProps) => {
|
||||
export const Warning = ({ style, iconStyle, children }: ScopedAlertProps) => {
|
||||
return (
|
||||
<Alert
|
||||
icon={SvgExclamationOutline}
|
||||
color={theme.warningText}
|
||||
backgroundColor={theme.warningBackground}
|
||||
style={style}
|
||||
iconStyle={iconStyle}
|
||||
>
|
||||
{children}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export const Error = ({ style, children }: ScopedAlertProps) => {
|
||||
export const Error = ({ style, iconStyle, children }: ScopedAlertProps) => {
|
||||
return (
|
||||
<Alert
|
||||
icon={SvgExclamationOutline}
|
||||
color={theme.errorTextDarker}
|
||||
backgroundColor={theme.errorBackground}
|
||||
style={style}
|
||||
iconStyle={iconStyle}
|
||||
>
|
||||
{children}
|
||||
</Alert>
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { AccountEntity } from 'loot-core/types/models';
|
||||
import { Autocomplete } from './Autocomplete';
|
||||
import { ItemHeader } from './ItemHeader';
|
||||
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { useAccounts } from '#hooks/useAccounts';
|
||||
|
||||
type AccountAutocompleteItem = AccountEntity;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { StateChangeTypes } from 'downshift';
|
||||
|
||||
import { getNormalisedString } from 'loot-core/shared/normalisation';
|
||||
|
||||
import { useProperFocus } from '@desktop-client/hooks/useProperFocus';
|
||||
import { useProperFocus } from '#hooks/useProperFocus';
|
||||
|
||||
type CommonAutocompleteProps<T extends AutocompleteItem> = {
|
||||
focused?: boolean;
|
||||
@@ -462,184 +462,190 @@ function SingleAutocomplete<T extends AutocompleteItem>({
|
||||
isOpen,
|
||||
inputValue,
|
||||
highlightedIndex,
|
||||
}) => (
|
||||
// Super annoying but it works best to return a div so we
|
||||
// can't use a View here, but we can fake it be using the
|
||||
// className
|
||||
<div
|
||||
className={cx('view', css({ display: 'flex' }))}
|
||||
{...containerProps}
|
||||
>
|
||||
<View ref={triggerRef} style={{ flexShrink: 0 }}>
|
||||
{renderInput(
|
||||
(() => {
|
||||
const { className, style, ...restInputProps } =
|
||||
inputProps || {};
|
||||
const downshiftProps = getInputProps({
|
||||
ref: inputRef,
|
||||
...restInputProps,
|
||||
onFocus: e => {
|
||||
inputProps.onFocus?.(e);
|
||||
}) => {
|
||||
const wrappedGetItemProps = itemProps => getItemProps({ ...itemProps });
|
||||
return (
|
||||
// Super annoying but it works best to return a div so we
|
||||
// can't use a View here, but we can fake it be using the
|
||||
// className
|
||||
<div
|
||||
className={cx('view', css({ display: 'flex' }))}
|
||||
{...containerProps}
|
||||
>
|
||||
<View ref={triggerRef} style={{ flexShrink: 0 }}>
|
||||
{renderInput(
|
||||
(() => {
|
||||
const { className, style, ...restInputProps } =
|
||||
inputProps || {};
|
||||
const downshiftProps = getInputProps({
|
||||
ref: inputRef,
|
||||
...restInputProps,
|
||||
onFocus: e => {
|
||||
inputProps.onFocus?.(e);
|
||||
|
||||
if (openOnFocus) {
|
||||
open();
|
||||
}
|
||||
},
|
||||
onBlur: e => {
|
||||
// Should this be e.nativeEvent
|
||||
e['preventDownshiftDefault'] = true;
|
||||
inputProps.onBlur?.(e);
|
||||
if (openOnFocus) {
|
||||
open();
|
||||
}
|
||||
},
|
||||
onBlur: e => {
|
||||
// Should this be e.nativeEvent
|
||||
e['preventDownshiftDefault'] = true;
|
||||
inputProps.onBlur?.(e);
|
||||
|
||||
if (!closeOnBlur) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemsViewRef.current?.contains(e.relatedTarget)) {
|
||||
// Do not close when the user clicks on any of the items.
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (clearOnBlur) {
|
||||
if (e.target.value === '') {
|
||||
onSelect?.(null, e.target.value);
|
||||
setSelectedItem(null);
|
||||
close();
|
||||
if (!closeOnBlur) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If not using table behavior, reset the input on blur. Tables
|
||||
// handle saving the value on blur.
|
||||
const value = selectedItem
|
||||
? getItemId(selectedItem)
|
||||
: null;
|
||||
|
||||
resetState(value);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
const { onKeyDown } = inputProps || {};
|
||||
|
||||
// If the dropdown is open, an item is highlighted, and the user
|
||||
// pressed enter, always capture that and handle it ourselves
|
||||
if (isOpen) {
|
||||
if (e.key === 'Enter') {
|
||||
if (highlightedIndex != null) {
|
||||
if (
|
||||
inst.lastChangeType ===
|
||||
Downshift.stateChangeTypes.itemMouseEnter
|
||||
) {
|
||||
// If the last thing the user did was hover an item, intentionally
|
||||
// ignore the default behavior of selecting the item. It's too
|
||||
// common to accidentally hover an item and then save it
|
||||
e.preventDefault();
|
||||
} else {
|
||||
// Otherwise, stop propagation so that the table navigator
|
||||
// doesn't handle it
|
||||
e.stopPropagation();
|
||||
}
|
||||
} else if (!strict) {
|
||||
// Handle it ourselves
|
||||
e.stopPropagation();
|
||||
onSelect(value, (e.target as HTMLInputElement).value);
|
||||
return onSelectAfter();
|
||||
} else {
|
||||
// No highlighted item, still allow the table to save the item
|
||||
// as `null`, even though we're allowing the table to move
|
||||
e.preventDefault();
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
} else if (shouldSaveFromKey(e)) {
|
||||
e.preventDefault();
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle escape ourselves
|
||||
if (e.key === 'Escape') {
|
||||
e.nativeEvent['preventDownshiftDefault'] = true;
|
||||
|
||||
if (!embedded && isOpen) {
|
||||
if (itemsViewRef.current?.contains(e.relatedTarget)) {
|
||||
// Do not close when the user clicks on any of the items.
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
fireUpdate(
|
||||
onUpdate,
|
||||
strict,
|
||||
suggestions,
|
||||
null,
|
||||
getItemId(originalItem),
|
||||
);
|
||||
if (clearOnBlur) {
|
||||
if (e.target.value === '') {
|
||||
onSelect?.(null, e.target.value);
|
||||
setSelectedItem(null);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(getItemName(originalItem));
|
||||
setSelectedItem(
|
||||
findItem(strict, suggestions, originalItem),
|
||||
);
|
||||
setHighlightedIndex(null);
|
||||
if (embedded) {
|
||||
open();
|
||||
// If not using table behavior, reset the input on blur. Tables
|
||||
// handle saving the value on blur.
|
||||
const value = selectedItem
|
||||
? getItemId(selectedItem)
|
||||
: null;
|
||||
|
||||
resetState(value);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
const { onKeyDown } = inputProps || {};
|
||||
|
||||
return {
|
||||
...downshiftProps,
|
||||
...(className && { className }),
|
||||
...(style && { style }),
|
||||
};
|
||||
})(),
|
||||
)}
|
||||
</View>
|
||||
{isOpen &&
|
||||
filtered.length > 0 &&
|
||||
(embedded ? (
|
||||
<View
|
||||
ref={itemsViewRef}
|
||||
style={{ ...styles.darkScrollbar, marginTop: 5 }}
|
||||
data-testid="autocomplete"
|
||||
>
|
||||
{renderItems(
|
||||
filtered,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
placement="bottom start"
|
||||
offset={2}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={close}
|
||||
isNonModal
|
||||
style={{
|
||||
...styles.darkScrollbar,
|
||||
...styles.popover,
|
||||
backgroundColor: theme.menuAutoCompleteBackground,
|
||||
color: theme.menuAutoCompleteText,
|
||||
minWidth: 200,
|
||||
width: triggerRef.current?.clientWidth,
|
||||
}}
|
||||
data-testid="autocomplete"
|
||||
>
|
||||
<View ref={itemsViewRef}>
|
||||
// If the dropdown is open, an item is highlighted, and the user
|
||||
// pressed enter, always capture that and handle it ourselves
|
||||
if (isOpen) {
|
||||
if (e.key === 'Enter') {
|
||||
if (highlightedIndex != null) {
|
||||
if (
|
||||
inst.lastChangeType ===
|
||||
Downshift.stateChangeTypes.itemMouseEnter
|
||||
) {
|
||||
// If the last thing the user did was hover an item, intentionally
|
||||
// ignore the default behavior of selecting the item. It's too
|
||||
// common to accidentally hover an item and then save it
|
||||
e.preventDefault();
|
||||
} else {
|
||||
// Otherwise, stop propagation so that the table navigator
|
||||
// doesn't handle it
|
||||
e.stopPropagation();
|
||||
}
|
||||
} else if (!strict) {
|
||||
// Handle it ourselves
|
||||
e.stopPropagation();
|
||||
onSelect(
|
||||
value,
|
||||
(e.target as HTMLInputElement).value,
|
||||
);
|
||||
return onSelectAfter();
|
||||
} else {
|
||||
// No highlighted item, still allow the table to save the item
|
||||
// as `null`, even though we're allowing the table to move
|
||||
e.preventDefault();
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
} else if (shouldSaveFromKey(e)) {
|
||||
e.preventDefault();
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle escape ourselves
|
||||
if (e.key === 'Escape') {
|
||||
e.nativeEvent['preventDownshiftDefault'] = true;
|
||||
|
||||
if (!embedded && isOpen) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
fireUpdate(
|
||||
onUpdate,
|
||||
strict,
|
||||
suggestions,
|
||||
null,
|
||||
getItemId(originalItem),
|
||||
);
|
||||
|
||||
setValue(getItemName(originalItem));
|
||||
setSelectedItem(
|
||||
findItem(strict, suggestions, originalItem),
|
||||
);
|
||||
setHighlightedIndex(null);
|
||||
if (embedded) {
|
||||
open();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...downshiftProps,
|
||||
...(className && { className }),
|
||||
...(style && { style }),
|
||||
};
|
||||
})(),
|
||||
)}
|
||||
</View>
|
||||
{isOpen &&
|
||||
filtered.length > 0 &&
|
||||
(embedded ? (
|
||||
<View
|
||||
ref={itemsViewRef}
|
||||
style={{ ...styles.darkScrollbar, marginTop: 5 }}
|
||||
data-testid="autocomplete"
|
||||
>
|
||||
{renderItems(
|
||||
filtered,
|
||||
getItemProps,
|
||||
wrappedGetItemProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
)}
|
||||
</View>
|
||||
</Popover>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
placement="bottom start"
|
||||
offset={2}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={close}
|
||||
isNonModal
|
||||
style={{
|
||||
...styles.darkScrollbar,
|
||||
...styles.popover,
|
||||
backgroundColor: theme.menuAutoCompleteBackground,
|
||||
color: theme.menuAutoCompleteText,
|
||||
minWidth: 200,
|
||||
width: triggerRef.current?.clientWidth,
|
||||
}}
|
||||
data-testid="autocomplete"
|
||||
>
|
||||
<View ref={itemsViewRef}>
|
||||
{renderItems(
|
||||
filtered,
|
||||
wrappedGetItemProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
)}
|
||||
</View>
|
||||
</Popover>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Downshift>
|
||||
);
|
||||
}
|
||||
|
||||