Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fca85209f | ||
|
|
a9362cc6f9 | ||
|
|
40296dc876 | ||
|
|
ed1e0ceb30 | ||
|
|
55817b0e70 | ||
|
|
6d7d12138c | ||
|
|
52f1f79c01 | ||
|
|
991fc4f450 | ||
|
|
4a8c692d06 | ||
|
|
0609f47cc3 | ||
|
|
0d1e6f2ee7 | ||
|
|
8568aebdbb | ||
|
|
bfb7c1d213 | ||
|
|
e526555748 | ||
|
|
45c4b262a2 | ||
|
|
e1f805b9c9 | ||
|
|
d0c11cd3af | ||
|
|
0c5bce8baf | ||
|
|
e650e00cb8 | ||
|
|
422996f8a7 | ||
|
|
9cad57c607 | ||
|
|
2a0f8335ed | ||
|
|
08cbdab2a1 | ||
|
|
ec2de3b387 | ||
|
|
65372e86a5 | ||
|
|
53a61000a4 | ||
|
|
485902af6b | ||
|
|
5a67b7e822 | ||
|
|
b994a6a74a | ||
|
|
b1b266e83c | ||
|
|
58626c0026 | ||
|
|
45094daf2f | ||
|
|
2bb7b3c2ee | ||
|
|
d6f610a326 | ||
|
|
8a8113a648 | ||
|
|
029e2f09bf | ||
|
|
86007e392f | ||
|
|
4c4f2fd426 | ||
|
|
7a18827b1d | ||
|
|
77fd65b2e7 | ||
|
|
30f03e8079 | ||
|
|
6e16262b63 | ||
|
|
29a515f3fe | ||
|
|
a5ab1a8fae | ||
|
|
d37622162a | ||
|
|
e3a8366dd7 | ||
|
|
d5e49dde59 | ||
|
|
f5258e6ebe | ||
|
|
f4d80fad92 | ||
|
|
3324dd5fa0 | ||
|
|
14509d15df | ||
|
|
9b461c48c9 | ||
|
|
55f2d126b3 | ||
|
|
6ae2047ab8 | ||
|
|
9fdffcc8e9 | ||
|
|
3daff4381f | ||
|
|
5914469b11 | ||
|
|
39e7f2598b | ||
|
|
c8d326d24b | ||
|
|
d8639a2a71 | ||
|
|
734191424b | ||
|
|
5d4fcfde00 | ||
|
|
54d7e5460a | ||
|
|
43ebe9e0fd | ||
|
|
515bdf5a74 | ||
|
|
018714610a | ||
|
|
00ee165f8e | ||
|
|
68442ae9e6 |
75
.eslintrc.js
@@ -38,6 +38,7 @@ module.exports = {
|
||||
extends: [
|
||||
'react-app',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/typescript',
|
||||
@@ -57,7 +58,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
varsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^(_|React)',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
@@ -90,15 +91,7 @@ module.exports = {
|
||||
'react/prop-types': 'off',
|
||||
|
||||
// TODO: re-enable these rules
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
// 'react-hooks/exhaustive-deps': [
|
||||
// 'warn',
|
||||
// {
|
||||
// additionalHooks: 'useLiveQuery',
|
||||
// },
|
||||
// ],
|
||||
|
||||
'no-var': 'warn',
|
||||
'react/jsx-curly-brace-presence': 'warn',
|
||||
@@ -277,6 +270,70 @@ module.exports = {
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO: fix the issues in these files
|
||||
files: [
|
||||
'./packages/desktop-client/src/components/accounts/Account.jsx',
|
||||
'./packages/desktop-client/src/components/accounts/MobileAccount.jsx',
|
||||
'./packages/desktop-client/src/components/accounts/MobileAccounts.jsx',
|
||||
'./packages/desktop-client/src/components/App.tsx',
|
||||
'./packages/desktop-client/src/components/budget/BudgetCategories.jsx',
|
||||
'./packages/desktop-client/src/components/budget/BudgetSummaries.tsx',
|
||||
'./packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx',
|
||||
'./packages/desktop-client/src/components/budget/index.tsx',
|
||||
'./packages/desktop-client/src/components/budget/MobileBudget.tsx',
|
||||
'./packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx',
|
||||
'./packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx',
|
||||
'./packages/desktop-client/src/components/common/Menu.tsx',
|
||||
'./packages/desktop-client/src/components/FinancesApp.tsx',
|
||||
'./packages/desktop-client/src/components/GlobalKeys.ts',
|
||||
'./packages/desktop-client/src/components/KeyHandlers.tsx',
|
||||
'./packages/desktop-client/src/components/LoggedInUser.tsx',
|
||||
'./packages/desktop-client/src/components/manager/ManagementApp.jsx',
|
||||
'./packages/desktop-client/src/components/manager/subscribe/common.tsx',
|
||||
'./packages/desktop-client/src/components/ManageRules.tsx',
|
||||
'./packages/desktop-client/src/components/mobile/MobileAmountInput.jsx',
|
||||
'./packages/desktop-client/src/components/mobile/MobileNavTabs.tsx',
|
||||
'./packages/desktop-client/src/components/Modals.tsx',
|
||||
'./packages/desktop-client/src/components/modals/EditRule.jsx',
|
||||
'./packages/desktop-client/src/components/modals/ImportTransactions.jsx',
|
||||
'./packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx',
|
||||
'./packages/desktop-client/src/components/Notifications.tsx',
|
||||
'./packages/desktop-client/src/components/payees/ManagePayees.jsx',
|
||||
'./packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx',
|
||||
'./packages/desktop-client/src/components/payees/PayeeTable.tsx',
|
||||
'./packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx',
|
||||
'./packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx',
|
||||
'./packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx',
|
||||
'./packages/desktop-client/src/components/reports/reports/CustomReport.jsx',
|
||||
'./packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx',
|
||||
'./packages/desktop-client/src/components/reports/SaveReportName.tsx',
|
||||
'./packages/desktop-client/src/components/reports/useReport.ts',
|
||||
'./packages/desktop-client/src/components/schedules/ScheduleDetails.jsx',
|
||||
'./packages/desktop-client/src/components/schedules/SchedulesTable.tsx',
|
||||
'./packages/desktop-client/src/components/select/DateSelect.tsx',
|
||||
'./packages/desktop-client/src/components/sidebar/Tools.tsx',
|
||||
'./packages/desktop-client/src/components/sort.tsx',
|
||||
'./packages/desktop-client/src/components/spreadsheet/useSheetValue.ts',
|
||||
'./packages/desktop-client/src/components/table.tsx',
|
||||
'./packages/desktop-client/src/components/Titlebar.tsx',
|
||||
'./packages/desktop-client/src/components/transactions/MobileTransaction.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/SelectedTransactions.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/TransactionList.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/TransactionsTable.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx',
|
||||
'./packages/desktop-client/src/hooks/useAccounts.ts',
|
||||
'./packages/desktop-client/src/hooks/useCategories.ts',
|
||||
'./packages/desktop-client/src/hooks/usePayees.ts',
|
||||
'./packages/desktop-client/src/hooks/useProperFocus.tsx',
|
||||
'./packages/desktop-client/src/hooks/useSelected.tsx',
|
||||
'./packages/loot-core/src/client/query-hooks.tsx',
|
||||
],
|
||||
rules: {
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
|
||||
4
.github/actions/setup/action.yml
vendored
@@ -4,11 +4,11 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.16.0
|
||||
- name: Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
|
||||
14
.github/workflows/build.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
api:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build API
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Create package tgz
|
||||
run: cd packages/api && yarn pack && mv package.tgz actual-api.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-api
|
||||
path: packages/api/actual-api.tgz
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
crdt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build CRDT
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Create package tgz
|
||||
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-crdt
|
||||
path: packages/crdt/actual-crdt.tgz
|
||||
@@ -53,18 +53,18 @@ jobs:
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: ./bin/package-browser
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-web
|
||||
path: packages/desktop-client/build
|
||||
- name: Upload Build Stats
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-stats
|
||||
path: packages/desktop-client/build-stats
|
||||
|
||||
10
.github/workflows/check.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Lint
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Typecheck
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Test
|
||||
@@ -40,8 +40,8 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19'
|
||||
- name: Check migrations
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
@@ -22,14 +22,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:javascript'
|
||||
|
||||
12
.github/workflows/e2e-test.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
outputs:
|
||||
netlify_url: ${{ steps.netlify.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Wait for Netlify build to finish
|
||||
@@ -33,19 +33,20 @@ jobs:
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run E2E Tests on Netlify URL
|
||||
run: yarn e2e
|
||||
env:
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
vrt:
|
||||
name: Visual regression
|
||||
needs: netlify
|
||||
@@ -53,16 +54,17 @@ jobs:
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
run: yarn vrt
|
||||
env:
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
4
.github/workflows/electron-master.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
|
||||
4
.github/workflows/electron-pr.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Build Electron
|
||||
run: ./bin/package-electron
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
|
||||
@@ -24,8 +24,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# This is not a security concern because we have approved & merged the PR
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19'
|
||||
- name: Handle feature requests
|
||||
|
||||
2
.github/workflows/release-notes.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Check release notes
|
||||
if: startsWith(github.head_ref, 'release/') == false
|
||||
uses: actualbudget/actions/release-notes/check@main
|
||||
|
||||
4
.github/workflows/size-compare.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
echo "Build failed on PR branch or ${{github.base_ref}}"
|
||||
exit 1
|
||||
- name: Download build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
id: pr-build
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
path: base
|
||||
|
||||
- name: Download build artifact from PR
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
|
||||
@@ -4,4 +4,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.0.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
||||
|
||||
@@ -10,4 +10,4 @@ if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
|
||||
yarn
|
||||
fi
|
||||
|
||||
yarn start:browser
|
||||
BROWSER=0 yarn start:browser
|
||||
|
||||
@@ -8,6 +8,8 @@ services:
|
||||
actual-development:
|
||||
build: .
|
||||
image: actual-development
|
||||
environment:
|
||||
- HTTPS
|
||||
ports:
|
||||
- '3001:3001'
|
||||
volumes:
|
||||
|
||||
@@ -52,17 +52,19 @@
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"node-jq": "^4.0.1",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.2.4",
|
||||
"react-refresh": "^0.14.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript": "^5.0.2",
|
||||
"typescript-strict-plugin": "^2.2.2-beta.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"rollup": "4.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"packageManager": "yarn@4.0.1",
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"browserslist": [
|
||||
"electron 24.0",
|
||||
"defaults"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "6.4.0",
|
||||
"version": "6.6.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
@@ -21,17 +21,17 @@
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"better-sqlite3": "^9.3.0",
|
||||
"compare-versions": "^6.1.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.3.105",
|
||||
"@swc/jest": "^0.2.31",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.12.0-rc.1",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.3.105",
|
||||
"@swc/jest": "^0.2.31",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
|
||||
@@ -32,12 +32,19 @@ Prerequisites:
|
||||
|
||||
#### Running against the local server
|
||||
|
||||
First start the dev server:
|
||||
First start a dev instance:
|
||||
|
||||
```sh
|
||||
HTTPS=true yarn start
|
||||
```
|
||||
|
||||
or using the dev container:
|
||||
```
|
||||
HTTPS=true docker compose up --build
|
||||
```
|
||||
|
||||
Note the network IP address and port the dev instance is listening on.
|
||||
|
||||
Next, navigate to the root of your project folder, run the standartised docker container, and launch the visual regression tests from within it.
|
||||
|
||||
```sh
|
||||
@@ -47,11 +54,11 @@ docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/
|
||||
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
|
||||
|
||||
# Run the VRT tests: important - they MUST be ran against a HTTPS server
|
||||
E2E_START_URL=https://192.168.0.178:3001 yarn vrt
|
||||
# Run the VRT tests: important - they MUST be ran against a HTTPS server. Use the ip and port noted earlier
|
||||
E2E_START_URL=https://ip:port yarn vrt
|
||||
|
||||
# To update snapshots, use the following command:
|
||||
E2E_START_URL=https://192.168.0.178:3001 yarn vrt --update-snapshots
|
||||
E2E_START_URL=https://ip:port yarn vrt --update-snapshots
|
||||
```
|
||||
|
||||
#### Running against a remote server
|
||||
|
||||
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
@@ -49,7 +49,7 @@ test.describe('Mobile', () => {
|
||||
test('opens the accounts page and asserts on balances', async () => {
|
||||
const accountsPage = await navigation.goToAccountsPage();
|
||||
|
||||
const account = await accountsPage.getNthAccount(0);
|
||||
const account = await accountsPage.getNthAccount(1);
|
||||
|
||||
await expect(account.name).toHaveText('Ally Savings');
|
||||
await expect(account.balance).toHaveText('7,653.00');
|
||||
@@ -58,7 +58,7 @@ test.describe('Mobile', () => {
|
||||
|
||||
test('opens individual account page and checks that filtering is working', async () => {
|
||||
const accountsPage = await navigation.goToAccountsPage();
|
||||
const accountPage = await accountsPage.openNthAccount(1);
|
||||
const accountPage = await accountsPage.openNthAccount(0);
|
||||
|
||||
await expect(accountPage.heading).toHaveText('Bank of America');
|
||||
expect(await accountPage.getBalance()).toBeGreaterThan(0);
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
@@ -1,26 +1,26 @@
|
||||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "24.1.0",
|
||||
"version": "24.3.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@juggle/resize-observer": "^3.1.2",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@reach/listbox": "^0.18.0",
|
||||
"@react-aria/focus": "^3.14.0",
|
||||
"@react-aria/listbox": "^3.10.1",
|
||||
"@react-aria/utils": "^3.19.0",
|
||||
"@react-stately/collections": "^3.10.0",
|
||||
"@react-stately/list": "^3.9.1",
|
||||
"@react-aria/focus": "^3.16.0",
|
||||
"@react-aria/listbox": "^3.11.3",
|
||||
"@react-aria/utils": "^3.23.0",
|
||||
"@react-stately/collections": "^3.10.4",
|
||||
"@react-stately/list": "^3.10.2",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"@svgr/cli": "^8.0.1",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@swc/core": "^1.3.105",
|
||||
"@swc/helpers": "^0.5.3",
|
||||
"@swc/plugin-react-remove-properties": "^1.5.108",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@testing-library/react": "14.1.2",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
@@ -32,14 +32,14 @@
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^2.29.3",
|
||||
"debounce": "^1.2.0",
|
||||
"downshift": "7.6.0",
|
||||
"focus-visible": "^4.1.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"debounce": "^1.2.1",
|
||||
"downshift": "7.6.2",
|
||||
"focus-visible": "^4.1.5",
|
||||
"glamor": "^2.20.40",
|
||||
"hotkeys-js": "3.10.3",
|
||||
"hotkeys-js": "^3.13.5",
|
||||
"inter-ui": "^3.19.3",
|
||||
"jest": "^27.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-watch-typeahead": "^2.2.2",
|
||||
"mdast-util-newline-to-break": "^2.0.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
@@ -48,27 +48,26 @@
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-modal": "3.16.1",
|
||||
"react-redux": "7.2.1",
|
||||
"react-router-dom": "6.11.2",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router-dom": "6.21.3",
|
||||
"react-simple-pull-to-refresh": "^1.3.3",
|
||||
"react-spring": "^9.7.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"recharts": "^2.8.0",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"react-spring": "^9.7.3",
|
||||
"react-virtualized-auto-sizer": "^1.0.21",
|
||||
"recharts": "^2.10.4",
|
||||
"redux": "^4.2.1",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rollup-plugin-visualizer": "^5.11.0",
|
||||
"sass": "^1.63.6",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.70.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"typescript": "^5.0.2",
|
||||
"uuid": "^9.0.0",
|
||||
"victory": "^36.6.8",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.0.12",
|
||||
"vite-plugin-pwa": "^0.19.0",
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"vitest": "^1.2.1",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
|
||||
BIN
packages/desktop-client/public/maskable-192x192.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
packages/desktop-client/public/maskable-512x512.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
packages/desktop-client/public/screenshot_narrow.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
packages/desktop-client/public/screenshot_wide.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
@@ -1,20 +1,51 @@
|
||||
{
|
||||
"name": "Actual",
|
||||
"short_name": "Actual",
|
||||
"description": "A local-first personal finance tool",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/maskable-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/maskable-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/screenshot_wide.png",
|
||||
"form_factor": "wide",
|
||||
"label": "Actual Budget Homepage",
|
||||
"type": "image/png",
|
||||
"sizes": "1280x720"
|
||||
},
|
||||
{
|
||||
"src": "/screenshot_narrow.png",
|
||||
"form_factor": "narrow",
|
||||
"label": "Actual Budget Mobile Homepage",
|
||||
"type": "image/png",
|
||||
"sizes": "350x600"
|
||||
}
|
||||
],
|
||||
"theme_color": "#8812E1",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone",
|
||||
"start_url": "."
|
||||
"start_url": "./"
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import * as Platform from 'loot-core/src/client/platform';
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
import {
|
||||
init as initConnection,
|
||||
send,
|
||||
} from 'loot-core/src/platform/client/fetch';
|
||||
import { type GlobalPrefs } from 'loot-core/src/types/prefs';
|
||||
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { useLocalPref } from '../hooks/useLocalPref';
|
||||
import { installPolyfills } from '../polyfills';
|
||||
import { ResponsiveProvider } from '../ResponsiveProvider';
|
||||
import { styles, hasHiddenScrollbars, ThemeStyle } from '../style';
|
||||
@@ -31,26 +32,13 @@ import { UpdateNotification } from './UpdateNotification';
|
||||
type AppInnerProps = {
|
||||
budgetId: string;
|
||||
cloudFileId: string;
|
||||
loadingText: string;
|
||||
loadBudget: (
|
||||
id: string,
|
||||
loadingText?: string,
|
||||
options?: object,
|
||||
) => Promise<void>;
|
||||
closeBudget: () => Promise<void>;
|
||||
loadGlobalPrefs: () => Promise<GlobalPrefs>;
|
||||
};
|
||||
|
||||
function AppInner({
|
||||
budgetId,
|
||||
cloudFileId,
|
||||
loadingText,
|
||||
loadBudget,
|
||||
closeBudget,
|
||||
loadGlobalPrefs,
|
||||
}: AppInnerProps) {
|
||||
function AppInner({ budgetId, cloudFileId }: AppInnerProps) {
|
||||
const [initializing, setInitializing] = useState(true);
|
||||
const { showBoundary: showErrorBoundary } = useErrorBoundary();
|
||||
const loadingText = useSelector((state: State) => state.app.loadingText);
|
||||
const { loadBudget, closeBudget, loadGlobalPrefs } = useActions();
|
||||
|
||||
async function init() {
|
||||
const socketName = await global.Actual.getServerSocket();
|
||||
@@ -123,14 +111,9 @@ function ErrorFallback({ error }: FallbackProps) {
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const budgetId = useSelector(
|
||||
state => state.prefs.local && state.prefs.local.id,
|
||||
);
|
||||
const cloudFileId = useSelector(
|
||||
state => state.prefs.local && state.prefs.local.cloudFileId,
|
||||
);
|
||||
const loadingText = useSelector(state => state.app.loadingText);
|
||||
const { loadBudget, closeBudget, loadGlobalPrefs, sync } = useActions();
|
||||
const [budgetId] = useLocalPref('id');
|
||||
const [cloudFileId] = useLocalPref('cloudFileId');
|
||||
const { sync } = useActions();
|
||||
const [hiddenScrollbars, setHiddenScrollbars] = useState(
|
||||
hasHiddenScrollbars(),
|
||||
);
|
||||
@@ -179,14 +162,7 @@ export function App() {
|
||||
{process.env.REACT_APP_REVIEW_ID && !Platform.isPlaywright && (
|
||||
<DevelopmentTopBar />
|
||||
)}
|
||||
<AppInner
|
||||
budgetId={budgetId}
|
||||
cloudFileId={cloudFileId}
|
||||
loadingText={loadingText}
|
||||
loadBudget={loadBudget}
|
||||
closeBudget={closeBudget}
|
||||
loadGlobalPrefs={loadGlobalPrefs}
|
||||
/>
|
||||
<AppInner budgetId={budgetId} cloudFileId={cloudFileId} />
|
||||
</ErrorBoundary>
|
||||
<ThemeStyle />
|
||||
</View>
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTransition, animated } from 'react-spring';
|
||||
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
|
||||
import { theme, styles } from '../style';
|
||||
|
||||
import { AnimatedRefresh } from './AnimatedRefresh';
|
||||
@@ -9,7 +11,9 @@ import { Text } from './common/Text';
|
||||
import { View } from './common/View';
|
||||
|
||||
export function BankSyncStatus() {
|
||||
const accountsSyncing = useSelector(state => state.account.accountsSyncing);
|
||||
const accountsSyncing = useSelector(
|
||||
(state: State) => state.account.accountsSyncing,
|
||||
);
|
||||
|
||||
const name = accountsSyncing
|
||||
? accountsSyncing === '__all'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React, { type ReactElement, useEffect, useMemo } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend as Backend } from 'react-dnd-html5-backend';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Route,
|
||||
Routes,
|
||||
@@ -13,12 +14,12 @@ import {
|
||||
|
||||
import hotkeys from 'hotkeys-js';
|
||||
|
||||
import { AccountsProvider } from 'loot-core/src/client/data-hooks/accounts';
|
||||
import { PayeesProvider } from 'loot-core/src/client/data-hooks/payees';
|
||||
import { SpreadsheetProvider } from 'loot-core/src/client/SpreadsheetProvider';
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
import { checkForUpdateNotification } from 'loot-core/src/client/update-notification';
|
||||
import * as undo from 'loot-core/src/platform/client/undo';
|
||||
|
||||
import { useAccounts } from '../hooks/useAccounts';
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { useNavigate } from '../hooks/useNavigate';
|
||||
import { useResponsive } from '../ResponsiveProvider';
|
||||
@@ -39,7 +40,8 @@ import { Reports } from './reports';
|
||||
import { NarrowAlternate, WideComponent } from './responsive';
|
||||
import { ScrollProvider } from './ScrollProvider';
|
||||
import { Settings } from './settings';
|
||||
import { FloatableSidebar, SidebarProvider } from './sidebar';
|
||||
import { FloatableSidebar } from './sidebar';
|
||||
import { SidebarProvider } from './sidebar/SidebarProvider';
|
||||
import { Titlebar, TitlebarProvider } from './Titlebar';
|
||||
import { TransactionEdit } from './transactions/MobileTransaction';
|
||||
|
||||
@@ -71,18 +73,19 @@ function WideNotSupported({ children, redirectTo = '/budget' }) {
|
||||
return isNarrowWidth ? children : null;
|
||||
}
|
||||
|
||||
function RouterBehaviors({ getAccounts }) {
|
||||
function RouterBehaviors() {
|
||||
const navigate = useNavigate();
|
||||
const accounts = useAccounts();
|
||||
const accountsLoaded = useSelector(
|
||||
(state: State) => state.queries.accountsLoaded,
|
||||
);
|
||||
useEffect(() => {
|
||||
// Get the accounts and check if any exist. If there are no
|
||||
// accounts, we want to redirect the user to the All Accounts
|
||||
// screen which will prompt them to add an account
|
||||
getAccounts().then(accounts => {
|
||||
if (accounts.length === 0) {
|
||||
navigate('/accounts');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
// If there are no accounts, we want to redirect the user to
|
||||
// the All Accounts screen which will prompt them to add an account
|
||||
if (accountsLoaded && accounts.length === 0) {
|
||||
navigate('/accounts');
|
||||
}
|
||||
}, [accountsLoaded, accounts]);
|
||||
|
||||
const location = useLocation();
|
||||
const href = useHref(location);
|
||||
@@ -116,7 +119,7 @@ function FinancesAppWithoutContext() {
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<RouterBehaviors getAccounts={actions.getAccounts} />
|
||||
<RouterBehaviors />
|
||||
<ExposeNavigate />
|
||||
|
||||
<View style={{ height: '100%' }}>
|
||||
@@ -265,13 +268,9 @@ export function FinancesApp() {
|
||||
<TitlebarProvider>
|
||||
<SidebarProvider>
|
||||
<BudgetMonthCountProvider>
|
||||
<PayeesProvider>
|
||||
<AccountsProvider>
|
||||
<DndProvider backend={Backend}>
|
||||
<ScrollProvider>{app}</ScrollProvider>
|
||||
</DndProvider>
|
||||
</AccountsProvider>
|
||||
</PayeesProvider>
|
||||
<DndProvider backend={Backend}>
|
||||
<ScrollProvider>{app}</ScrollProvider>
|
||||
</DndProvider>
|
||||
</BudgetMonthCountProvider>
|
||||
</SidebarProvider>
|
||||
</TitlebarProvider>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { theme, styles, type CSSProperties } from '../style';
|
||||
|
||||
@@ -22,7 +24,7 @@ export function LoggedInUser({
|
||||
style,
|
||||
color,
|
||||
}: LoggedInUserProps) {
|
||||
const userData = useSelector(state => state.user.data);
|
||||
const userData = useSelector((state: State) => state.user.data);
|
||||
const { getUserData, signOut, closeBudget } = useActions();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, {
|
||||
type SetStateAction,
|
||||
type Dispatch,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { pushModal } from 'loot-core/src/client/actions/modals';
|
||||
import { initiallyLoadPayees } from 'loot-core/src/client/actions/queries';
|
||||
@@ -17,7 +17,9 @@ import { mapField, friendlyOp } from 'loot-core/src/shared/rules';
|
||||
import { describeSchedule } from 'loot-core/src/shared/schedules';
|
||||
import { type RuleEntity } from 'loot-core/src/types/models';
|
||||
|
||||
import { useAccounts } from '../hooks/useAccounts';
|
||||
import { useCategories } from '../hooks/useCategories';
|
||||
import { usePayees } from '../hooks/usePayees';
|
||||
import { useSelected, SelectedProvider } from '../hooks/useSelected';
|
||||
import { theme } from '../style';
|
||||
|
||||
@@ -103,11 +105,13 @@ function ManageRulesContent({
|
||||
|
||||
const { data: schedules } = SchedulesQuery.useQuery();
|
||||
const { list: categories } = useCategories();
|
||||
const state = useSelector(state => ({
|
||||
payees: state.queries.payees,
|
||||
accounts: state.queries.accounts,
|
||||
const payees = usePayees();
|
||||
const accounts = useAccounts();
|
||||
const state = {
|
||||
payees,
|
||||
accounts,
|
||||
schedules,
|
||||
}));
|
||||
};
|
||||
const filterData = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { savePrefs } from 'loot-core/src/client/actions';
|
||||
|
||||
import { useLocalPref } from '../hooks/useLocalPref';
|
||||
import { useResponsive } from '../ResponsiveProvider';
|
||||
import { theme, styles } from '../style';
|
||||
|
||||
@@ -14,27 +12,24 @@ import { Checkbox } from './forms';
|
||||
const buttonStyle = { border: 0, fontSize: 15, padding: '10px 13px' };
|
||||
|
||||
export function MobileWebMessage() {
|
||||
const hideMobileMessagePref = useSelector(state => {
|
||||
return (state.prefs.local && state.prefs.local.hideMobileMessage) || true;
|
||||
});
|
||||
const [hideMobileMessage = true, setHideMobileMessagePref] =
|
||||
useLocalPref('hideMobileMessage');
|
||||
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
|
||||
const [show, setShow] = useState(
|
||||
isNarrowWidth &&
|
||||
!hideMobileMessagePref &&
|
||||
!hideMobileMessage &&
|
||||
!document.cookie.match(/hideMobileMessage=true/),
|
||||
);
|
||||
const [requestDontRemindMe, setRequestDontRemindMe] = useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
function onTry() {
|
||||
setShow(false);
|
||||
|
||||
if (requestDontRemindMe) {
|
||||
// remember the pref indefinitely
|
||||
dispatch(savePrefs({ hideMobileMessage: true }));
|
||||
setHideMobileMessagePref(true);
|
||||
} else {
|
||||
// Set a cookie for 5 minutes
|
||||
const d = new Date();
|
||||
|
||||
@@ -3,12 +3,12 @@ import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
import { type PopModalAction } from 'loot-core/src/client/state-types/modals';
|
||||
import { send } from 'loot-core/src/platform/client/fetch';
|
||||
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { useCategories } from '../hooks/useCategories';
|
||||
import { useSyncServerStatus } from '../hooks/useSyncServerStatus';
|
||||
import { type CommonModalProps } from '../types/modals';
|
||||
|
||||
import { CategoryGroupMenu } from './modals/CategoryGroupMenu';
|
||||
import { CategoryMenu } from './modals/CategoryMenu';
|
||||
@@ -40,14 +40,18 @@ import { PostsOfflineNotification } from './schedules/PostsOfflineNotification';
|
||||
import { ScheduleDetails } from './schedules/ScheduleDetails';
|
||||
import { ScheduleLink } from './schedules/ScheduleLink';
|
||||
|
||||
export type CommonModalProps = {
|
||||
onClose: () => PopModalAction;
|
||||
onBack: () => PopModalAction;
|
||||
showBack: boolean;
|
||||
isCurrent: boolean;
|
||||
isHidden: boolean;
|
||||
stackIndex: number;
|
||||
};
|
||||
|
||||
export function Modals() {
|
||||
const modalStack = useSelector(state => state.modals.modalStack);
|
||||
const isHidden = useSelector(state => state.modals.isHidden);
|
||||
const accounts = useSelector(state => state.queries.accounts);
|
||||
const { grouped: categoryGroups, list: categories } = useCategories();
|
||||
const budgetId = useSelector(
|
||||
state => state.prefs.local && state.prefs.local.id,
|
||||
);
|
||||
const modalStack = useSelector((state: State) => state.modals.modalStack);
|
||||
const isHidden = useSelector((state: State) => state.modals.isHidden);
|
||||
const actions = useActions();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -97,8 +101,6 @@ export function Modals() {
|
||||
account={options.account}
|
||||
balance={options.balance}
|
||||
canDelete={options.canDelete}
|
||||
accounts={accounts.filter(acct => acct.closed === 0)}
|
||||
categoryGroups={categoryGroups}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
@@ -109,7 +111,6 @@ export function Modals() {
|
||||
modalProps={modalProps}
|
||||
externalAccounts={options.accounts}
|
||||
requisitionId={options.requisitionId}
|
||||
localAccounts={accounts.filter(acct => acct.closed === 0)}
|
||||
actions={actions}
|
||||
syncSource={options.syncSource}
|
||||
/>
|
||||
@@ -119,15 +120,8 @@ export function Modals() {
|
||||
return (
|
||||
<ConfirmCategoryDelete
|
||||
modalProps={modalProps}
|
||||
category={
|
||||
'category' in options &&
|
||||
categories.find(c => c.id === options.category)
|
||||
}
|
||||
group={
|
||||
'group' in options &&
|
||||
categoryGroups.find(g => g.id === options.group)
|
||||
}
|
||||
categoryGroups={categoryGroups}
|
||||
category={options.category}
|
||||
group={options.group}
|
||||
onDelete={options.onDelete}
|
||||
/>
|
||||
);
|
||||
@@ -145,7 +139,7 @@ export function Modals() {
|
||||
return (
|
||||
<LoadBackup
|
||||
watchUpdates
|
||||
budgetId={budgetId}
|
||||
budgetId={options.budgetId}
|
||||
modalProps={modalProps}
|
||||
actions={actions}
|
||||
backupDisabled={false}
|
||||
@@ -301,6 +295,7 @@ export function Modals() {
|
||||
modalProps={modalProps}
|
||||
id={options?.id || null}
|
||||
actions={actions}
|
||||
transaction={options?.transaction || null}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -311,6 +306,8 @@ export function Modals() {
|
||||
modalProps={modalProps}
|
||||
actions={actions}
|
||||
transactionIds={options?.transactionIds}
|
||||
getTransaction={options?.getTransaction}
|
||||
pushModal={options?.pushModal}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, {
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
import type { NotificationWithId } from 'loot-core/src/client/state-types/notifications';
|
||||
|
||||
import { useActions } from '../hooks/useActions';
|
||||
@@ -238,7 +239,9 @@ function Notification({
|
||||
|
||||
export function Notifications({ style }: { style?: CSSProperties }) {
|
||||
const { removeNotification } = useActions();
|
||||
const notifications = useSelector(state => state.notifications.notifications);
|
||||
const notifications = useSelector(
|
||||
(state: State) => state.notifications.notifications,
|
||||
);
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -7,8 +7,7 @@ import React, {
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { usePrivacyMode } from 'loot-core/src/client/privacy';
|
||||
|
||||
import { usePrivacyMode } from '../hooks/usePrivacyMode';
|
||||
import { useResponsive } from '../ResponsiveProvider';
|
||||
|
||||
import { View } from './common/View';
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
||||
|
||||
import type { Theme } from 'loot-core/src/types/prefs';
|
||||
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { SvgMoonStars, SvgSun, SvgSystem } from '../icons/v2';
|
||||
import { useResponsive } from '../ResponsiveProvider';
|
||||
import { type CSSProperties, themeOptions, useTheme } from '../style';
|
||||
@@ -16,8 +15,7 @@ type ThemeSelectorProps = {
|
||||
};
|
||||
|
||||
export function ThemeSelector({ style }: ThemeSelectorProps) {
|
||||
const theme = useTheme();
|
||||
const { saveGlobalPrefs } = useActions();
|
||||
const [theme, switchTheme] = useTheme();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
@@ -26,17 +24,15 @@ export function ThemeSelector({ style }: ThemeSelectorProps) {
|
||||
light: SvgSun,
|
||||
dark: SvgMoonStars,
|
||||
auto: SvgSystem,
|
||||
midnight: SvgMoonStars,
|
||||
} as const;
|
||||
|
||||
async function onMenuSelect(newTheme: string) {
|
||||
function onMenuSelect(newTheme: Theme) {
|
||||
setMenuOpen(false);
|
||||
|
||||
saveGlobalPrefs({
|
||||
theme: newTheme as Theme,
|
||||
});
|
||||
switchTheme(newTheme);
|
||||
}
|
||||
|
||||
const Icon = themeIcons?.[theme] || SvgSun;
|
||||
const Icon = themeIcons[theme] || SvgSun;
|
||||
|
||||
return isNarrowWidth ? null : (
|
||||
<Button
|
||||
|
||||
@@ -6,7 +6,6 @@ import React, {
|
||||
useContext,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
|
||||
import * as Platform from 'loot-core/src/client/platform';
|
||||
@@ -16,6 +15,8 @@ import { type LocalPrefs } from 'loot-core/src/types/prefs';
|
||||
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { useFeatureFlag } from '../hooks/useFeatureFlag';
|
||||
import { useGlobalPref } from '../hooks/useGlobalPref';
|
||||
import { useLocalPref } from '../hooks/useLocalPref';
|
||||
import { useNavigate } from '../hooks/useNavigate';
|
||||
import { SvgArrowLeft } from '../icons/v1';
|
||||
import {
|
||||
@@ -39,7 +40,7 @@ import { View } from './common/View';
|
||||
import { KeyHandlers } from './KeyHandlers';
|
||||
import { LoggedInUser } from './LoggedInUser';
|
||||
import { useServerURL } from './ServerContext';
|
||||
import { useSidebar } from './sidebar';
|
||||
import { useSidebar } from './sidebar/SidebarProvider';
|
||||
import { useSheetValue } from './spreadsheet/useSheetValue';
|
||||
import { ThemeSelector } from './ThemeSelector';
|
||||
import { Tooltip } from './tooltips';
|
||||
@@ -118,10 +119,8 @@ type PrivacyButtonProps = {
|
||||
};
|
||||
|
||||
function PrivacyButton({ style }: PrivacyButtonProps) {
|
||||
const isPrivacyEnabled = useSelector(
|
||||
state => state.prefs.local?.isPrivacyEnabled,
|
||||
);
|
||||
const { savePrefs } = useActions();
|
||||
const [isPrivacyEnabled, setPrivacyEnabledPref] =
|
||||
useLocalPref('isPrivacyEnabled');
|
||||
|
||||
const privacyIconStyle = { width: 15, height: 15 };
|
||||
|
||||
@@ -129,7 +128,7 @@ function PrivacyButton({ style }: PrivacyButtonProps) {
|
||||
<Button
|
||||
type="bare"
|
||||
aria-label={`${isPrivacyEnabled ? 'Disable' : 'Enable'} privacy mode`}
|
||||
onClick={() => savePrefs({ isPrivacyEnabled: !isPrivacyEnabled })}
|
||||
onClick={() => setPrivacyEnabledPref(!isPrivacyEnabled)}
|
||||
style={style}
|
||||
>
|
||||
{isPrivacyEnabled ? (
|
||||
@@ -146,7 +145,7 @@ type SyncButtonProps = {
|
||||
isMobile?: boolean;
|
||||
};
|
||||
function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
const cloudFileId = useSelector(state => state.prefs.local?.cloudFileId);
|
||||
const [cloudFileId] = useLocalPref('cloudFileId');
|
||||
const { sync } = useActions();
|
||||
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
@@ -286,9 +285,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
}
|
||||
|
||||
function BudgetTitlebar() {
|
||||
const maxMonths = useSelector(state => state.prefs.global?.maxMonths);
|
||||
const budgetType = useSelector(state => state.prefs.local?.budgetType);
|
||||
const { saveGlobalPrefs } = useActions();
|
||||
const [maxMonths, setMaxMonthsPref] = useGlobalPref('maxMonths');
|
||||
const [budgetType] = useLocalPref('budgetType');
|
||||
const { sendEvent } = useContext(TitlebarContext);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -317,7 +315,7 @@ function BudgetTitlebar() {
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<MonthCountSelector
|
||||
maxMonths={maxMonths || 1}
|
||||
onChange={value => saveGlobalPrefs({ maxMonths: value })}
|
||||
onChange={value => setMaxMonthsPref(value)}
|
||||
/>
|
||||
{reportBudgetEnabled && (
|
||||
<View style={{ marginLeft: -5 }}>
|
||||
@@ -390,9 +388,7 @@ export function Titlebar({ style }: TitlebarProps) {
|
||||
const sidebar = useSidebar();
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
const serverURL = useServerURL();
|
||||
const floatingSidebar = useSelector(
|
||||
state => state.prefs.global?.floatingSidebar,
|
||||
);
|
||||
const [floatingSidebar] = useGlobalPref('floatingSidebar');
|
||||
|
||||
return isNarrowWidth ? null : (
|
||||
<View
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { SvgClose } from '../icons/v1';
|
||||
import { theme } from '../style';
|
||||
@@ -11,9 +13,9 @@ import { Text } from './common/Text';
|
||||
import { View } from './common/View';
|
||||
|
||||
export function UpdateNotification() {
|
||||
const updateInfo = useSelector(state => state.app.updateInfo);
|
||||
const updateInfo = useSelector((state: State) => state.app.updateInfo);
|
||||
const showUpdateNotification = useSelector(
|
||||
state => state.app.showUpdateNotification,
|
||||
(state: State) => state.app.showUpdateNotification,
|
||||
);
|
||||
|
||||
const { updateApp, setAppState } = useActions();
|
||||
|
||||
@@ -26,7 +26,12 @@ import {
|
||||
} from 'loot-core/src/shared/transactions';
|
||||
import { applyChanges, groupById } from 'loot-core/src/shared/util';
|
||||
|
||||
import { useAccounts } from '../../hooks/useAccounts';
|
||||
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 { SelectedProviderWithItems } from '../../hooks/useSelected';
|
||||
import { styles, theme } from '../../style';
|
||||
import { Button } from '../common/Button';
|
||||
@@ -1199,6 +1204,7 @@ class AccountInternal extends PureComponent {
|
||||
|
||||
applySort = (field, ascDesc, prevField, prevAscDesc) => {
|
||||
const filters = this.state.filters;
|
||||
const isFiltered = filters.length > 0;
|
||||
const sortField = getField(!field ? this.state.sort.field : field);
|
||||
const sortAscDesc = !ascDesc ? this.state.sort.ascDesc : ascDesc;
|
||||
const sortPrevField = getField(
|
||||
@@ -1208,34 +1214,78 @@ class AccountInternal extends PureComponent {
|
||||
? this.state.sort.prevAscDesc
|
||||
: prevAscDesc;
|
||||
|
||||
if (!field) {
|
||||
//no sort was made (called by applyFilters)
|
||||
this.currentQuery = this.currentQuery.orderBy({
|
||||
const sortCurrentQuery = function (that, sortField, sortAscDesc) {
|
||||
if (sortField === 'cleared') {
|
||||
that.currentQuery = that.currentQuery.orderBy({
|
||||
reconciled: sortAscDesc,
|
||||
});
|
||||
}
|
||||
|
||||
that.currentQuery = that.currentQuery.orderBy({
|
||||
[sortField]: sortAscDesc,
|
||||
});
|
||||
} else {
|
||||
//sort called directly
|
||||
if (filters.length > 0) {
|
||||
//if filters already exist then apply them
|
||||
this.applyFilters([...filters]);
|
||||
this.currentQuery = this.currentQuery.orderBy({
|
||||
[sortField]: sortAscDesc,
|
||||
};
|
||||
|
||||
const sortRootQuery = function (that, sortField, sortAscDesc) {
|
||||
if (sortField === 'cleared') {
|
||||
that.currentQuery = that.rootQuery.orderBy({
|
||||
reconciled: sortAscDesc,
|
||||
});
|
||||
that.currentQuery = that.currentQuery.orderBy({
|
||||
cleared: sortAscDesc,
|
||||
});
|
||||
} else {
|
||||
//no filters exist make new rootquery
|
||||
this.currentQuery = this.rootQuery.orderBy({
|
||||
that.currentQuery = that.rootQuery.orderBy({
|
||||
[sortField]: sortAscDesc,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (sortPrevField) {
|
||||
//apply previos sort if it exists
|
||||
this.currentQuery = this.currentQuery.orderBy({
|
||||
};
|
||||
|
||||
// sort by previously used sort field, if any
|
||||
const maybeSortByPreviousField = function (
|
||||
that,
|
||||
sortPrevField,
|
||||
sortPrevAscDesc,
|
||||
) {
|
||||
if (!sortPrevField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sortPrevField === 'cleared') {
|
||||
that.currentQuery = that.currentQuery.orderBy({
|
||||
reconciled: sortPrevAscDesc,
|
||||
});
|
||||
}
|
||||
|
||||
that.currentQuery = that.currentQuery.orderBy({
|
||||
[sortPrevField]: sortPrevAscDesc,
|
||||
});
|
||||
};
|
||||
|
||||
switch (true) {
|
||||
// called by applyFilters to sort an already filtered result
|
||||
case !field:
|
||||
sortCurrentQuery(this, sortField, sortAscDesc);
|
||||
break;
|
||||
|
||||
// called directly from UI by sorting a column.
|
||||
// active filters need to be applied before sorting
|
||||
case isFiltered:
|
||||
this.applyFilters([...filters]);
|
||||
sortCurrentQuery(this, sortField, sortAscDesc);
|
||||
break;
|
||||
|
||||
// called directly from UI by sorting a column.
|
||||
// no active filters, start a new root query.
|
||||
case !isFiltered:
|
||||
sortRootQuery(this, sortField, sortAscDesc);
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
this.updateQuery(this.currentQuery, this.state.filters.length > 0);
|
||||
maybeSortByPreviousField(this, sortPrevField, sortPrevAscDesc);
|
||||
this.updateQuery(this.currentQuery, isFiltered);
|
||||
};
|
||||
|
||||
onSort = (headerClicked, ascDesc) => {
|
||||
@@ -1487,23 +1537,41 @@ export function Account() {
|
||||
const location = useLocation();
|
||||
|
||||
const { grouped: categoryGroups } = useCategories();
|
||||
const state = useSelector(state => ({
|
||||
newTransactions: state.queries.newTransactions,
|
||||
matchedTransactions: state.queries.matchedTransactions,
|
||||
accounts: state.queries.accounts,
|
||||
failedAccounts: state.account.failedAccounts,
|
||||
dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
|
||||
hideFraction: state.prefs.local.hideFraction || false,
|
||||
expandSplits: state.prefs.local['expand-splits'],
|
||||
showBalances: params.id && state.prefs.local['show-balances-' + params.id],
|
||||
showCleared: params.id && !state.prefs.local['hide-cleared-' + params.id],
|
||||
showExtraBalances:
|
||||
state.prefs.local['show-extra-balances-' + params.id || 'all-accounts'],
|
||||
payees: state.queries.payees,
|
||||
modalShowing: state.modals.modalStack.length > 0,
|
||||
accountsSyncing: state.account.accountsSyncing,
|
||||
lastUndoState: state.app.lastUndoState,
|
||||
}));
|
||||
const newTransactions = useSelector(state => state.queries.newTransactions);
|
||||
const matchedTransactions = useSelector(
|
||||
state => state.queries.matchedTransactions,
|
||||
);
|
||||
const accounts = useAccounts();
|
||||
const payees = usePayees();
|
||||
const failedAccounts = useFailedAccounts();
|
||||
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
||||
const [hideFraction = false] = useLocalPref('hideFraction');
|
||||
const [expandSplits] = useLocalPref('expand-splits');
|
||||
const [showBalances] = useLocalPref(`show-balances-${params.id}`);
|
||||
const [hideCleared] = useLocalPref(`hide-cleared-${params.id}`);
|
||||
const [showExtraBalances] = useLocalPref(
|
||||
`show-extra-balances-${params.id || 'all-accounts'}`,
|
||||
);
|
||||
const modalShowing = useSelector(state => state.modals.modalStack.length > 0);
|
||||
const accountsSyncing = useSelector(state => state.account.accountsSyncing);
|
||||
const lastUndoState = useSelector(state => state.app.lastUndoState);
|
||||
|
||||
const state = {
|
||||
newTransactions,
|
||||
matchedTransactions,
|
||||
accounts,
|
||||
failedAccounts,
|
||||
dateFormat,
|
||||
hideFraction,
|
||||
expandSplits,
|
||||
showBalances,
|
||||
showCleared: !hideCleared,
|
||||
showExtraBalances,
|
||||
payees,
|
||||
modalShowing,
|
||||
accountsSyncing,
|
||||
lastUndoState,
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const filtersList = useFilters();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { authorizeBank } from '../../gocardless';
|
||||
import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { useActions } from '../../hooks/useActions';
|
||||
import { SvgExclamationOutline } from '../../icons/v1';
|
||||
import { theme } from '../../style';
|
||||
@@ -49,7 +50,7 @@ function getErrorMessage(type, code) {
|
||||
}
|
||||
|
||||
export function AccountSyncCheck() {
|
||||
const accounts = useSelector(state => state.queries.accounts);
|
||||
const accounts = useAccounts();
|
||||
const failedAccounts = useSelector(state => state.account.failedAccounts);
|
||||
const { unlinkAccount, pushModal } = useActions();
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@ export function Balances({
|
||||
opacity: selectedItems.size > 0 || showExtraBalances ? 1 : 0,
|
||||
},
|
||||
'&:hover svg': { opacity: 1 },
|
||||
paddingTop: 1,
|
||||
paddingBottom: 1,
|
||||
}}
|
||||
>
|
||||
<CellValue
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import { useSyncServerStatus } from '../../hooks/useSyncServerStatus';
|
||||
import { AnimatedLoading } from '../../icons/AnimatedLoading';
|
||||
import { SvgAdd } from '../../icons/v1';
|
||||
@@ -21,7 +22,7 @@ import { Search } from '../common/Search';
|
||||
import { Stack } from '../common/Stack';
|
||||
import { View } from '../common/View';
|
||||
import { FilterButton } from '../filters/FiltersMenu';
|
||||
import { FiltersStack } from '../filters/SavedFilters';
|
||||
import { FiltersStack } from '../filters/FiltersStack';
|
||||
import { KeyHandlers } from '../KeyHandlers';
|
||||
import { NotesButton } from '../NotesButton';
|
||||
import { SelectedTransactionsButton } from '../transactions/SelectedTransactions';
|
||||
@@ -53,7 +54,6 @@ export function AccountHeader({
|
||||
search,
|
||||
filters,
|
||||
conditionsOp,
|
||||
savePrefs,
|
||||
pushModal,
|
||||
onSearch,
|
||||
onAddTransaction,
|
||||
@@ -83,11 +83,15 @@ export function AccountHeader({
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const searchInput = useRef(null);
|
||||
const splitsExpanded = useSplitsExpanded();
|
||||
const syncServerStatus = useSyncServerStatus();
|
||||
const isUsingServer = syncServerStatus !== 'no-server';
|
||||
const isServerOffline = syncServerStatus === 'offline';
|
||||
const [_, setExpandSplitsPref] = useLocalPref('expand-splits');
|
||||
|
||||
let canSync = account && account.account_id;
|
||||
let canSync = account && account.account_id && isUsingServer;
|
||||
if (!account) {
|
||||
// All accounts - check for any syncable account
|
||||
canSync = !!accounts.find(account => !!account.account_id);
|
||||
canSync = !!accounts.find(account => !!account.account_id) && isUsingServer;
|
||||
}
|
||||
|
||||
function onToggleSplits() {
|
||||
@@ -97,9 +101,7 @@ export function AccountHeader({
|
||||
id: tableRef.current.getScrolledItem(),
|
||||
});
|
||||
|
||||
savePrefs({
|
||||
'expand-splits': !(splitsExpanded.state.mode === 'expand'),
|
||||
});
|
||||
setExpandSplitsPref(!(splitsExpanded.state.mode === 'expand'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,20 +118,25 @@ export function AccountHeader({
|
||||
/>
|
||||
|
||||
<View style={{ ...styles.pageContent, paddingBottom: 10, flexShrink: 0 }}>
|
||||
<View style={{ marginTop: 2, alignItems: 'flex-start' }}>
|
||||
<View
|
||||
style={{ marginTop: 2, marginBottom: 10, alignItems: 'flex-start' }}
|
||||
>
|
||||
<View>
|
||||
{editingName ? (
|
||||
<InitialFocus>
|
||||
<Input
|
||||
defaultValue={accountName}
|
||||
onEnter={e => onSaveName(e.target.value)}
|
||||
onBlur={() => onExposeName(false)}
|
||||
onBlur={e => onSaveName(e.target.value)}
|
||||
onEscape={() => onExposeName(false)}
|
||||
style={{
|
||||
fontSize: 25,
|
||||
fontWeight: 500,
|
||||
marginTop: -5,
|
||||
marginBottom: -2,
|
||||
marginLeft: -5,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
}}
|
||||
/>
|
||||
</InitialFocus>
|
||||
@@ -153,7 +160,7 @@ export function AccountHeader({
|
||||
fontSize: 25,
|
||||
fontWeight: 500,
|
||||
marginRight: 5,
|
||||
marginBottom: 5,
|
||||
marginBottom: -1,
|
||||
}}
|
||||
data-testid="account-name"
|
||||
>
|
||||
@@ -185,7 +192,7 @@ export function AccountHeader({
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{ fontSize: 25, fontWeight: 500, marginBottom: 5 }}
|
||||
style={{ fontSize: 25, fontWeight: 500, marginBottom: -1 }}
|
||||
data-testid="account-name"
|
||||
>
|
||||
{account && account.closed
|
||||
@@ -210,7 +217,11 @@ export function AccountHeader({
|
||||
style={{ marginTop: 12 }}
|
||||
>
|
||||
{((account && !account.closed) || canSync) && (
|
||||
<Button type="bare" onClick={canSync ? onSync : onImport}>
|
||||
<Button
|
||||
type="bare"
|
||||
onClick={canSync ? onSync : onImport}
|
||||
disabled={canSync && isServerOffline}
|
||||
>
|
||||
{canSync ? (
|
||||
<>
|
||||
<AnimatedRefresh
|
||||
@@ -222,7 +233,7 @@ export function AccountHeader({
|
||||
}
|
||||
style={{ marginRight: 4 }}
|
||||
/>{' '}
|
||||
Sync
|
||||
{isServerOffline ? 'Sync offline' : 'Sync'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -19,8 +19,13 @@ import {
|
||||
ungroupTransactions,
|
||||
} from 'loot-core/src/shared/transactions';
|
||||
|
||||
import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useDateFormat } from '../../hooks/useDateFormat';
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import { useLocalPrefs } from '../../hooks/useLocalPrefs';
|
||||
import { useNavigate } from '../../hooks/useNavigate';
|
||||
import { usePayees } from '../../hooks/usePayees';
|
||||
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
|
||||
import { theme, styles } from '../../style';
|
||||
import { Button } from '../common/Button';
|
||||
@@ -72,19 +77,27 @@ function PreviewTransactions({ children }) {
|
||||
let paged;
|
||||
|
||||
export function Account(props) {
|
||||
const accounts = useSelector(state => state.queries.accounts);
|
||||
const accounts = useAccounts();
|
||||
const payees = usePayees();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [transactions, setTransactions] = useState([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [currentQuery, setCurrentQuery] = useState();
|
||||
|
||||
const state = useSelector(state => ({
|
||||
payees: state.queries.payees,
|
||||
newTransactions: state.queries.newTransactions,
|
||||
prefs: state.prefs.local,
|
||||
dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
|
||||
}));
|
||||
const newTransactions = useSelector(state => state.queries.newTransactions);
|
||||
const prefs = useLocalPrefs();
|
||||
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
||||
const [_numberFormat] = useLocalPref('numberFormat');
|
||||
const numberFormat = _numberFormat || 'comma-dot';
|
||||
const [hideFraction = false] = useLocalPref('hideFraction');
|
||||
|
||||
const state = {
|
||||
payees,
|
||||
newTransactions,
|
||||
prefs,
|
||||
dateFormat,
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const actionCreators = useMemo(
|
||||
@@ -134,11 +147,6 @@ export function Account(props) {
|
||||
}
|
||||
});
|
||||
|
||||
if (accounts.length === 0) {
|
||||
await actionCreators.getAccounts();
|
||||
}
|
||||
|
||||
await actionCreators.initiallyLoadPayees();
|
||||
await fetchTransactions();
|
||||
|
||||
actionCreators.markAccountRead(accountId);
|
||||
@@ -216,8 +224,6 @@ export function Account(props) {
|
||||
const balance = queries.accountBalance(account);
|
||||
const balanceCleared = queries.accountBalanceCleared(account);
|
||||
const balanceUncleared = queries.accountBalanceUncleared(account);
|
||||
const numberFormat = state.prefs.numberFormat || 'comma-dot';
|
||||
const hideFraction = state.prefs.hideFraction || false;
|
||||
|
||||
return (
|
||||
<SchedulesProvider
|
||||
|
||||
@@ -199,11 +199,11 @@ export function AccountDetails({
|
||||
</View>
|
||||
<PullToRefresh onRefresh={onRefresh}>
|
||||
<TransactionList
|
||||
account={account}
|
||||
transactions={allTransactions}
|
||||
categories={categories}
|
||||
accounts={accounts}
|
||||
payees={payees}
|
||||
showCategory={!account.offbudget}
|
||||
isNew={isNewTransaction}
|
||||
onLoadMore={onLoadMore}
|
||||
onSelect={onSelectTransaction}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { replaceModal, syncAndDownload } from 'loot-core/src/client/actions';
|
||||
import * as queries from 'loot-core/src/client/queries';
|
||||
|
||||
import { useActions } from '../../hooks/useActions';
|
||||
import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import { useNavigate } from '../../hooks/useNavigate';
|
||||
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
|
||||
import { SvgAdd } from '../../icons/v1';
|
||||
@@ -13,6 +15,7 @@ import { Button } from '../common/Button';
|
||||
import { Text } from '../common/Text';
|
||||
import { TextOneLine } from '../common/TextOneLine';
|
||||
import { View } from '../common/View';
|
||||
import { ROW_HEIGHT as MOBILE_NAV_HEIGHT } from '../mobile/MobileNavTabs';
|
||||
import { Page } from '../Page';
|
||||
import { PullToRefresh } from '../responsive/PullToRefresh';
|
||||
import { CellValue } from '../spreadsheet/CellValue';
|
||||
@@ -175,7 +178,11 @@ function AccountList({
|
||||
</Button>
|
||||
}
|
||||
padding={0}
|
||||
style={{ flex: 1, backgroundColor: theme.mobilePageBackground }}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.mobilePageBackground,
|
||||
paddingBottom: MOBILE_NAV_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{accounts.length === 0 && <EmptyMessage />}
|
||||
<PullToRefresh onRefresh={onSync}>
|
||||
@@ -216,26 +223,19 @@ function AccountList({
|
||||
}
|
||||
|
||||
export function Accounts() {
|
||||
const accounts = useSelector(state => state.queries.accounts);
|
||||
const dispatch = useDispatch();
|
||||
const accounts = useAccounts();
|
||||
const newTransactions = useSelector(state => state.queries.newTransactions);
|
||||
const updatedAccounts = useSelector(state => state.queries.updatedAccounts);
|
||||
const numberFormat = useSelector(
|
||||
state => state.prefs.local.numberFormat || 'comma-dot',
|
||||
);
|
||||
const hideFraction = useSelector(
|
||||
state => state.prefs.local.hideFraction || false,
|
||||
);
|
||||
const [_numberFormat] = useLocalPref('numberFormat');
|
||||
const numberFormat = _numberFormat || 'comma-dot';
|
||||
const [hideFraction = false] = useLocalPref('hideFraction');
|
||||
|
||||
const { list: categories } = useCategories();
|
||||
const { getAccounts, replaceModal, syncAndDownload } = useActions();
|
||||
|
||||
const transactions = useState({});
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => getAccounts())();
|
||||
}, []);
|
||||
|
||||
const onSelectAccount = id => {
|
||||
navigate(`/accounts/${id}`);
|
||||
};
|
||||
@@ -244,6 +244,14 @@ export function Accounts() {
|
||||
navigate(`/transaction/${transaction}`);
|
||||
};
|
||||
|
||||
const onAddAccount = () => {
|
||||
dispatch(replaceModal('add-account'));
|
||||
};
|
||||
|
||||
const onSync = () => {
|
||||
dispatch(syncAndDownload());
|
||||
};
|
||||
|
||||
useSetThemeColor(theme.mobileViewTheme);
|
||||
|
||||
return (
|
||||
@@ -260,10 +268,10 @@ export function Accounts() {
|
||||
getBalanceQuery={queries.accountBalance}
|
||||
getOnBudgetBalance={queries.budgetedAccountBalance}
|
||||
getOffBudgetBalance={queries.offbudgetAccountBalance}
|
||||
onAddAccount={() => replaceModal('add-account')}
|
||||
onAddAccount={onAddAccount}
|
||||
onSelectAccount={onSelectAccount}
|
||||
onSelectTransaction={onSelectTransaction}
|
||||
onSync={syncAndDownload}
|
||||
onSync={onSync}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,15 @@ import React, { Fragment, type ComponentProps, type ReactNode } from 'react';
|
||||
|
||||
import { css } from 'glamor';
|
||||
|
||||
import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
|
||||
import { type AccountEntity } from 'loot-core/src/types/models';
|
||||
|
||||
import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { useResponsive } from '../../ResponsiveProvider';
|
||||
import { type CSSProperties, theme } from '../../style';
|
||||
import { View } from '../common/View';
|
||||
|
||||
import { Autocomplete } from './Autocomplete';
|
||||
import { ItemHeader, type ItemHeaderProps } from './ItemHeader';
|
||||
|
||||
function AccountList({
|
||||
items,
|
||||
@@ -71,9 +72,7 @@ function AccountList({
|
||||
type AccountAutoCompleteProps = {
|
||||
embedded?: boolean;
|
||||
includeClosedAccounts: boolean;
|
||||
renderAccountItemGroupHeader?: (
|
||||
props: AccountItemGroupHeaderProps,
|
||||
) => ReactNode;
|
||||
renderAccountItemGroupHeader?: (props: ItemHeaderProps) => ReactNode;
|
||||
renderAccountItem?: (props: AccountItemProps) => ReactNode;
|
||||
closeOnBlur?: boolean;
|
||||
} & ComponentProps<typeof Autocomplete>;
|
||||
@@ -86,7 +85,7 @@ export function AccountAutocomplete({
|
||||
closeOnBlur,
|
||||
...props
|
||||
}: AccountAutoCompleteProps) {
|
||||
let accounts = useCachedAccounts() || [];
|
||||
let accounts = useAccounts() || [];
|
||||
|
||||
//remove closed accounts if needed
|
||||
//then sort by closed, then offbudget
|
||||
@@ -124,35 +123,10 @@ export function AccountAutocomplete({
|
||||
);
|
||||
}
|
||||
|
||||
type AccountItemGroupHeaderProps = {
|
||||
title: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function AccountItemGroupHeader({
|
||||
title,
|
||||
style,
|
||||
...props
|
||||
}: AccountItemGroupHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
color: theme.menuAutoCompleteTextHeader,
|
||||
padding: '4px 9px',
|
||||
...style,
|
||||
}}
|
||||
data-testid={`${title}-account-item-group`}
|
||||
{...props}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function defaultRenderAccountItemGroupHeader(
|
||||
props: AccountItemGroupHeaderProps,
|
||||
props: ItemHeaderProps,
|
||||
): ReactNode {
|
||||
return <AccountItemGroupHeader {...props} />;
|
||||
return <ItemHeader {...props} type="account" />;
|
||||
}
|
||||
|
||||
type AccountItemProps = {
|
||||
|
||||
@@ -359,6 +359,7 @@ function SingleAutocomplete<T extends Item>({
|
||||
|
||||
setValue(value);
|
||||
setIsChanged(true);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
onStateChange={changes => {
|
||||
if (
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Text } from '../common/Text';
|
||||
import { View } from '../common/View';
|
||||
|
||||
import { Autocomplete, defaultFilterSuggestion } from './Autocomplete';
|
||||
import { ItemHeader, type ItemHeaderProps } from './ItemHeader';
|
||||
|
||||
export type CategoryListProps = {
|
||||
items: Array<CategoryEntity & { group?: CategoryGroupEntity }>;
|
||||
@@ -33,9 +34,7 @@ export type CategoryListProps = {
|
||||
renderSplitTransactionButton?: (
|
||||
props: SplitTransactionButtonProps,
|
||||
) => ReactNode;
|
||||
renderCategoryItemGroupHeader?: (
|
||||
props: CategoryItemGroupHeaderProps,
|
||||
) => ReactNode;
|
||||
renderCategoryItemGroupHeader?: (props: ItemHeaderProps) => ReactNode;
|
||||
renderCategoryItem?: (props: CategoryItemProps) => ReactNode;
|
||||
};
|
||||
function CategoryList({
|
||||
@@ -103,9 +102,7 @@ type CategoryAutocompleteProps = ComponentProps<typeof Autocomplete> & {
|
||||
renderSplitTransactionButton?: (
|
||||
props: SplitTransactionButtonProps,
|
||||
) => ReactNode;
|
||||
renderCategoryItemGroupHeader?: (
|
||||
props: CategoryItemGroupHeaderProps,
|
||||
) => ReactNode;
|
||||
renderCategoryItemGroupHeader?: (props: ItemHeaderProps) => ReactNode;
|
||||
renderCategoryItem?: (props: CategoryItemProps) => ReactNode;
|
||||
};
|
||||
|
||||
@@ -177,35 +174,8 @@ export function CategoryAutocomplete({
|
||||
);
|
||||
}
|
||||
|
||||
type CategoryItemGroupHeaderProps = {
|
||||
title: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function CategoryItemGroupHeader({
|
||||
title,
|
||||
style,
|
||||
...props
|
||||
}: CategoryItemGroupHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
color: theme.menuAutoCompleteTextHeader,
|
||||
padding: '4px 9px',
|
||||
...style,
|
||||
}}
|
||||
data-testid={`${title}-category-item-group`}
|
||||
{...props}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function defaultRenderCategoryItemGroupHeader(
|
||||
props: CategoryItemGroupHeaderProps,
|
||||
) {
|
||||
return <CategoryItemGroupHeader {...props} />;
|
||||
function defaultRenderCategoryItemGroupHeader(props: ItemHeaderProps) {
|
||||
return <ItemHeader {...props} type="category" />;
|
||||
}
|
||||
|
||||
type SplitTransactionButtonProps = {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { type ComponentProps } from 'react';
|
||||
|
||||
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
|
||||
import { type TransactionFilterEntity } from 'loot-core/types/models/transaction-filter';
|
||||
|
||||
import { Autocomplete } from './Autocomplete';
|
||||
import { FilterList } from './FilterList';
|
||||
|
||||
export function FilterAutocomplete({
|
||||
embedded,
|
||||
...props
|
||||
}: {
|
||||
embedded?: boolean;
|
||||
} & ComponentProps<typeof Autocomplete<TransactionFilterEntity>>) {
|
||||
const filters = useFilters() || [];
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
strict={true}
|
||||
highlightFirst={true}
|
||||
embedded={embedded}
|
||||
suggestions={filters}
|
||||
renderItems={(items, getItemProps, highlightedIndex) => (
|
||||
<FilterList
|
||||
items={items}
|
||||
getItemProps={getItemProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
embedded={embedded}
|
||||
/>
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,21 @@
|
||||
import React, { type ComponentProps } from 'react';
|
||||
|
||||
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
|
||||
import { type TransactionFilterEntity } from 'loot-core/src/types/models';
|
||||
|
||||
import { theme } from '../../style';
|
||||
import { theme } from '../../style/theme';
|
||||
import { View } from '../common/View';
|
||||
|
||||
import { Autocomplete } from './Autocomplete';
|
||||
import { ItemHeader } from './ItemHeader';
|
||||
|
||||
type FilterListProps<T> = {
|
||||
items: T[];
|
||||
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>;
|
||||
highlightedIndex: number;
|
||||
embedded?: boolean;
|
||||
};
|
||||
|
||||
function FilterList<T extends { id: string; name: string }>({
|
||||
export function FilterList<T extends { id: string; name: string }>({
|
||||
items,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
embedded,
|
||||
}: FilterListProps<T>) {
|
||||
}: {
|
||||
items: T[];
|
||||
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>;
|
||||
highlightedIndex: number;
|
||||
embedded?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
@@ -30,6 +25,7 @@ function FilterList<T extends { id: string; name: string }>({
|
||||
...(!embedded && { maxHeight: 175 }),
|
||||
}}
|
||||
>
|
||||
<ItemHeader title="Saved Filters" type="filter" />
|
||||
{items.map((item, idx) => {
|
||||
return [
|
||||
<div
|
||||
@@ -55,32 +51,3 @@ function FilterList<T extends { id: string; name: string }>({
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type SavedFilterAutocompleteProps = {
|
||||
embedded?: boolean;
|
||||
} & ComponentProps<typeof Autocomplete<TransactionFilterEntity>>;
|
||||
|
||||
export function SavedFilterAutocomplete({
|
||||
embedded,
|
||||
...props
|
||||
}: SavedFilterAutocompleteProps) {
|
||||
const filters = useFilters() || [];
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
strict={true}
|
||||
highlightFirst={true}
|
||||
embedded={embedded}
|
||||
suggestions={filters}
|
||||
renderItems={(items, getItemProps, highlightedIndex) => (
|
||||
<FilterList
|
||||
items={items}
|
||||
getItemProps={getItemProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
embedded={embedded}
|
||||
/>
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import { theme } from '../../style/theme';
|
||||
import { type CSSProperties } from '../../style/types';
|
||||
|
||||
export type ItemHeaderProps = {
|
||||
title: string;
|
||||
style?: CSSProperties;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export function ItemHeader({ title, style, type, ...props }: ItemHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
color: theme.menuAutoCompleteTextHeader,
|
||||
padding: '4px 9px',
|
||||
...style,
|
||||
}}
|
||||
data-testid={`${title}-${type}-item-group`}
|
||||
{...props}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,14 +13,14 @@ import { useDispatch } from 'react-redux';
|
||||
import { css } from 'glamor';
|
||||
|
||||
import { createPayee } from 'loot-core/src/client/actions/queries';
|
||||
import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
|
||||
import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
|
||||
import { getActivePayees } from 'loot-core/src/client/reducers/queries';
|
||||
import {
|
||||
type AccountEntity,
|
||||
type PayeeEntity,
|
||||
} from 'loot-core/src/types/models';
|
||||
|
||||
import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { usePayees } from '../../hooks/usePayees';
|
||||
import { SvgAdd } from '../../icons/v1';
|
||||
import { useResponsive } from '../../ResponsiveProvider';
|
||||
import { type CSSProperties, theme } from '../../style';
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
defaultFilterSuggestion,
|
||||
AutocompleteFooter,
|
||||
} from './Autocomplete';
|
||||
import { ItemHeader, type ItemHeaderProps } from './ItemHeader';
|
||||
|
||||
function getPayeeSuggestions(payees, focusTransferPayees, accounts) {
|
||||
let activePayees = accounts ? getActivePayees(payees, accounts) : payees;
|
||||
@@ -163,7 +164,7 @@ type PayeeAutocompleteProps = {
|
||||
onSelect?: (value: string) => void;
|
||||
onManagePayees: () => void;
|
||||
renderCreatePayeeButton?: (props: CreatePayeeButtonProps) => ReactNode;
|
||||
renderPayeeItemGroupHeader?: (props: PayeeItemGroupHeaderProps) => ReactNode;
|
||||
renderPayeeItemGroupHeader?: (props: ItemHeaderProps) => ReactNode;
|
||||
renderPayeeItem?: (props: PayeeItemProps) => ReactNode;
|
||||
accounts?: AccountEntity[];
|
||||
payees?: PayeeEntity[];
|
||||
@@ -187,12 +188,12 @@ export function PayeeAutocomplete({
|
||||
payees,
|
||||
...props
|
||||
}: PayeeAutocompleteProps) {
|
||||
const cachedPayees = useCachedPayees();
|
||||
const retrievedPayees = usePayees();
|
||||
if (!payees) {
|
||||
payees = cachedPayees;
|
||||
payees = retrievedPayees;
|
||||
}
|
||||
|
||||
const cachedAccounts = useCachedAccounts();
|
||||
const cachedAccounts = useAccounts();
|
||||
if (!accounts) {
|
||||
accounts = cachedAccounts;
|
||||
}
|
||||
@@ -422,35 +423,8 @@ function defaultRenderCreatePayeeButton(
|
||||
return <CreatePayeeButton {...props} />;
|
||||
}
|
||||
|
||||
type PayeeItemGroupHeaderProps = {
|
||||
title: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function PayeeItemGroupHeader({
|
||||
title,
|
||||
style,
|
||||
...props
|
||||
}: PayeeItemGroupHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
color: theme.menuAutoCompleteTextHeader,
|
||||
padding: '4px 9px',
|
||||
...style,
|
||||
}}
|
||||
data-testid={`${title}-payee-item-group`}
|
||||
{...props}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function defaultRenderPayeeItemGroupHeader(
|
||||
props: PayeeItemGroupHeaderProps,
|
||||
): ReactNode {
|
||||
return <PayeeItemGroupHeader {...props} />;
|
||||
function defaultRenderPayeeItemGroupHeader(props: ItemHeaderProps): ReactNode {
|
||||
return <ItemHeader {...props} type="payee" />;
|
||||
}
|
||||
|
||||
type PayeeItemProps = {
|
||||
|
||||