Compare commits
63 Commits
ts-LoadBac
...
v24.3.0
| 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 |
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"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ 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.
|
||||
|
||||
|
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: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 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: 82 KiB After Width: | Height: | Size: 82 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: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { type ComponentProps } from 'react';
|
||||
|
||||
import { useReports } from 'loot-core/client/data-hooks/reports';
|
||||
import { type CustomReportEntity } from 'loot-core/src/types/models/reports';
|
||||
|
||||
import { Autocomplete } from './Autocomplete';
|
||||
import { ReportList } from './ReportList';
|
||||
|
||||
export function ReportAutocomplete({
|
||||
embedded,
|
||||
...props
|
||||
}: {
|
||||
embedded?: boolean;
|
||||
} & ComponentProps<typeof Autocomplete<CustomReportEntity>>) {
|
||||
const reports = useReports() || [];
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
strict={true}
|
||||
highlightFirst={true}
|
||||
embedded={embedded}
|
||||
suggestions={reports}
|
||||
renderItems={(items, getItemProps, highlightedIndex) => (
|
||||
<ReportList
|
||||
items={items}
|
||||
getItemProps={getItemProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
embedded={embedded}
|
||||
/>
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { Fragment, type ComponentProps } from 'react';
|
||||
|
||||
import { theme } from '../../style/theme';
|
||||
import { View } from '../common/View';
|
||||
|
||||
import { ItemHeader } from './ItemHeader';
|
||||
|
||||
export function ReportList<T extends { id: string; name: string }>({
|
||||
items,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
embedded,
|
||||
}: {
|
||||
items: T[];
|
||||
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>;
|
||||
highlightedIndex: number;
|
||||
embedded?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
padding: '5px 0',
|
||||
...(!embedded && { maxHeight: 175 }),
|
||||
}}
|
||||
>
|
||||
<Fragment>{ItemHeader({ title: 'Saved Reports' })}</Fragment>
|
||||
{items.map((item, idx) => {
|
||||
return [
|
||||
<div
|
||||
{...(getItemProps ? getItemProps({ item }) : null)}
|
||||
key={item.id}
|
||||
style={{
|
||||
backgroundColor:
|
||||
highlightedIndex === idx
|
||||
? theme.menuAutoCompleteBackgroundHover
|
||||
: 'transparent',
|
||||
padding: 4,
|
||||
paddingLeft: 20,
|
||||
borderRadius: embedded ? 4 : 0,
|
||||
}}
|
||||
data-highlighted={highlightedIndex === idx || undefined}
|
||||
>
|
||||
{item.name}
|
||||
</div>,
|
||||
];
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { memo, useState, useMemo } from 'react';
|
||||
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import { theme, styles } from '../../style';
|
||||
import { View } from '../common/View';
|
||||
import { DropHighlightPosContext } from '../sort';
|
||||
@@ -17,12 +18,7 @@ import { separateGroups } from './util';
|
||||
export const BudgetCategories = memo(
|
||||
({
|
||||
categoryGroups,
|
||||
newCategoryForGroup,
|
||||
showHiddenCategories,
|
||||
isAddingGroup,
|
||||
editingCell,
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
dataComponents,
|
||||
onBudgetAction,
|
||||
onShowActivity,
|
||||
@@ -34,11 +30,16 @@ export const BudgetCategories = memo(
|
||||
onDeleteGroup,
|
||||
onReorderCategory,
|
||||
onReorderGroup,
|
||||
onShowNewCategory,
|
||||
onHideNewCategory,
|
||||
onShowNewGroup,
|
||||
onHideNewGroup,
|
||||
}) => {
|
||||
const [_collapsed, setCollapsedPref] = useLocalPref('budget.collapsed');
|
||||
const collapsed = _collapsed || [];
|
||||
const [showHiddenCategories] = useLocalPref('budget.showHiddenCategories');
|
||||
function onCollapse(value) {
|
||||
setCollapsedPref(value);
|
||||
}
|
||||
|
||||
const [isAddingGroup, setIsAddingGroup] = useState(false);
|
||||
const [newCategoryForGroup, setNewCategoryForGroup] = useState(null);
|
||||
const items = useMemo(() => {
|
||||
const [expenseGroups, incomeGroup] = separateGroups(categoryGroups);
|
||||
|
||||
@@ -133,15 +134,46 @@ export const BudgetCategories = memo(
|
||||
});
|
||||
} else if (state === 'end') {
|
||||
setDragState(null);
|
||||
setCollapsed(savedCollapsed || []);
|
||||
onCollapse(savedCollapsed || []);
|
||||
}
|
||||
}
|
||||
|
||||
function onToggleCollapse(id) {
|
||||
if (collapsed.includes(id)) {
|
||||
setCollapsed(collapsed.filter(id_ => id_ !== id));
|
||||
onCollapse(collapsed.filter(id_ => id_ !== id));
|
||||
} else {
|
||||
setCollapsed([...collapsed, id]);
|
||||
onCollapse([...collapsed, id]);
|
||||
}
|
||||
}
|
||||
|
||||
function onShowNewGroup() {
|
||||
setIsAddingGroup(true);
|
||||
}
|
||||
|
||||
function onHideNewGroup() {
|
||||
setIsAddingGroup(false);
|
||||
}
|
||||
|
||||
function _onSaveGroup(group) {
|
||||
onSaveGroup?.(group);
|
||||
if (group.id === 'new') {
|
||||
onHideNewGroup();
|
||||
}
|
||||
}
|
||||
|
||||
function onShowNewCategory(groupId) {
|
||||
onCollapse(collapsed.filter(c => c !== groupId));
|
||||
setNewCategoryForGroup(groupId);
|
||||
}
|
||||
|
||||
function onHideNewCategory() {
|
||||
setNewCategoryForGroup(null);
|
||||
}
|
||||
|
||||
function _onSaveCategory(category) {
|
||||
onSaveCategory?.(category);
|
||||
if (category.id === 'new') {
|
||||
onHideNewCategory();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +199,7 @@ export const BudgetCategories = memo(
|
||||
<SidebarGroup
|
||||
group={{ id: 'new', name: '' }}
|
||||
editing={true}
|
||||
onSave={onSaveGroup}
|
||||
onSave={_onSaveGroup}
|
||||
onHideNewGroup={onHideNewGroup}
|
||||
onEdit={onEditName}
|
||||
/>
|
||||
@@ -187,7 +219,7 @@ export const BudgetCategories = memo(
|
||||
id: 'new',
|
||||
}}
|
||||
editing={true}
|
||||
onSave={onSaveCategory}
|
||||
onSave={_onSaveCategory}
|
||||
onHideNewCategory={onHideNewCategory}
|
||||
onEditName={onEditName}
|
||||
/>
|
||||
@@ -204,7 +236,7 @@ export const BudgetCategories = memo(
|
||||
MonthComponent={dataComponents.ExpenseGroupComponent}
|
||||
dragState={dragState}
|
||||
onEditName={onEditName}
|
||||
onSave={onSaveGroup}
|
||||
onSave={_onSaveGroup}
|
||||
onDelete={onDeleteGroup}
|
||||
onDragChange={onDragChange}
|
||||
onReorderGroup={onReorderGroup}
|
||||
@@ -223,7 +255,7 @@ export const BudgetCategories = memo(
|
||||
dragState={dragState}
|
||||
onEditName={onEditName}
|
||||
onEditMonth={onEditMonth}
|
||||
onSave={onSaveCategory}
|
||||
onSave={_onSaveCategory}
|
||||
onDelete={onDeleteCategory}
|
||||
onDragChange={onDragChange}
|
||||
onReorder={onReorderCategory}
|
||||
@@ -255,7 +287,7 @@ export const BudgetCategories = memo(
|
||||
MonthComponent={dataComponents.IncomeGroupComponent}
|
||||
collapsed={collapsed.includes(item.value.id)}
|
||||
onEditName={onEditName}
|
||||
onSave={onSaveGroup}
|
||||
onSave={_onSaveGroup}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
/>
|
||||
@@ -270,7 +302,7 @@ export const BudgetCategories = memo(
|
||||
MonthComponent={dataComponents.IncomeCategoryComponent}
|
||||
onEditName={onEditName}
|
||||
onEditMonth={onEditMonth}
|
||||
onSave={onSaveCategory}
|
||||
onSave={_onSaveCategory}
|
||||
onDelete={onDeleteCategory}
|
||||
onDragChange={onDragChange}
|
||||
onReorder={onReorderCategory}
|
||||
@@ -313,3 +345,5 @@ export const BudgetCategories = memo(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BudgetCategories.displayName = 'BudgetCategories';
|
||||
|
||||
@@ -44,3 +44,5 @@ export const BudgetPageHeader = memo<BudgetPageHeaderProps>(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BudgetPageHeader.displayName = 'BudgetPageHeader';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { createRef, Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { savePrefs } from 'loot-core/src/client/actions';
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
|
||||
import { theme, styles } from '../../style';
|
||||
@@ -12,7 +14,7 @@ import { BudgetTotals } from './BudgetTotals';
|
||||
import { MonthsProvider } from './MonthsContext';
|
||||
import { findSortDown, findSortUp, getScrollbarWidth } from './util';
|
||||
|
||||
export class BudgetTable extends Component {
|
||||
class BudgetTableInner extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.budgetCategoriesRef = createRef();
|
||||
@@ -20,7 +22,6 @@ export class BudgetTable extends Component {
|
||||
this.state = {
|
||||
editing: null,
|
||||
draggingState: null,
|
||||
showHiddenCategories: props.prefs['budget.showHiddenCategories'] ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,26 +138,22 @@ export class BudgetTable extends Component {
|
||||
return monthUtils.addMonths(this.props.startMonth, monthIndex);
|
||||
};
|
||||
|
||||
// This is called via ref.
|
||||
clearEditing() {
|
||||
this.setState({ editing: null });
|
||||
}
|
||||
|
||||
toggleHiddenCategories = () => {
|
||||
this.setState(prevState => ({
|
||||
showHiddenCategories: !prevState.showHiddenCategories,
|
||||
}));
|
||||
this.props.savePrefs({
|
||||
'budget.showHiddenCategories': !this.state.showHiddenCategories,
|
||||
});
|
||||
this.props.onToggleHiddenCategories();
|
||||
};
|
||||
|
||||
expandAllCategories = () => {
|
||||
this.props.setCollapsed([]);
|
||||
this.props.onCollapse([]);
|
||||
};
|
||||
|
||||
collapseAllCategories = () => {
|
||||
const { setCollapsed, categoryGroups } = this.props;
|
||||
setCollapsed(categoryGroups.map(g => g.id));
|
||||
const { onCollapse, categoryGroups } = this.props;
|
||||
onCollapse(categoryGroups.map(g => g.id));
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -167,21 +164,13 @@ export class BudgetTable extends Component {
|
||||
startMonth,
|
||||
numMonths,
|
||||
monthBounds,
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
newCategoryForGroup,
|
||||
dataComponents,
|
||||
isAddingGroup,
|
||||
onSaveCategory,
|
||||
onSaveGroup,
|
||||
onDeleteCategory,
|
||||
onDeleteGroup,
|
||||
onShowNewCategory,
|
||||
onHideNewCategory,
|
||||
onShowNewGroup,
|
||||
onHideNewGroup,
|
||||
} = this.props;
|
||||
const { editing, draggingState, showHiddenCategories } = this.state;
|
||||
const { editing, draggingState } = this.state;
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -254,13 +243,8 @@ export class BudgetTable extends Component {
|
||||
innerRef={el => (this.budgetDataNode = el)}
|
||||
>
|
||||
<BudgetCategories
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
categoryGroups={categoryGroups}
|
||||
newCategoryForGroup={newCategoryForGroup}
|
||||
isAddingGroup={isAddingGroup}
|
||||
editingCell={editing}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
dataComponents={dataComponents}
|
||||
onEditMonth={this.onEditMonth}
|
||||
onEditName={this.onEditName}
|
||||
@@ -270,10 +254,6 @@ export class BudgetTable extends Component {
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onReorderCategory={this.onReorderCategory}
|
||||
onReorderGroup={this.onReorderGroup}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
onHideNewCategory={onHideNewCategory}
|
||||
onShowNewGroup={onShowNewGroup}
|
||||
onHideNewGroup={onHideNewGroup}
|
||||
onBudgetAction={this.onBudgetAction}
|
||||
onShowActivity={this.onShowActivity}
|
||||
/>
|
||||
@@ -285,3 +265,35 @@ export class BudgetTable extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { grouped: categoryGroups } = state.queries.categories;
|
||||
return {
|
||||
categoryGroups,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
const onCollapse = collapsedIds => {
|
||||
dispatch(savePrefs({ 'budget.collapsed': collapsedIds }));
|
||||
};
|
||||
|
||||
const onToggleHiddenCategories = () =>
|
||||
dispatch((innerDispatch, getState) => {
|
||||
const { prefs } = getState();
|
||||
const showHiddenCategories = prefs.local['budget.showHiddenCategories'];
|
||||
innerDispatch(
|
||||
savePrefs({
|
||||
'budget.showHiddenCategories': !showHiddenCategories,
|
||||
}),
|
||||
);
|
||||
});
|
||||
return {
|
||||
onCollapse,
|
||||
onToggleHiddenCategories,
|
||||
};
|
||||
};
|
||||
|
||||
export const BudgetTable = connect(mapStateToProps, mapDispatchToProps, null, {
|
||||
forwardRef: true,
|
||||
})(BudgetTableInner);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { forwardRef, useEffect, type ComponentProps } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { useActions } from '../../hooks/useActions';
|
||||
@@ -34,14 +33,13 @@ type DynamicBudgetTableInnerProps = {
|
||||
} & ComponentProps<typeof BudgetTable>;
|
||||
|
||||
const DynamicBudgetTableInner = forwardRef<
|
||||
BudgetTable,
|
||||
typeof BudgetTable,
|
||||
DynamicBudgetTableInnerProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
width,
|
||||
height,
|
||||
categoryGroups,
|
||||
prewarmStartMonth,
|
||||
startMonth,
|
||||
maxMonths = 3,
|
||||
@@ -52,7 +50,6 @@ const DynamicBudgetTableInner = forwardRef<
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const prefs = useSelector(state => state.prefs.local);
|
||||
const { setDisplayMax } = useBudgetMonthCount();
|
||||
const actions = useActions();
|
||||
|
||||
@@ -86,12 +83,10 @@ const DynamicBudgetTableInner = forwardRef<
|
||||
/>
|
||||
<BudgetTable
|
||||
ref={ref}
|
||||
categoryGroups={categoryGroups}
|
||||
prewarmStartMonth={prewarmStartMonth}
|
||||
startMonth={startMonth}
|
||||
numMonths={numMonths}
|
||||
monthBounds={monthBounds}
|
||||
prefs={prefs}
|
||||
{...actions}
|
||||
{...props}
|
||||
/>
|
||||
@@ -101,8 +96,10 @@ const DynamicBudgetTableInner = forwardRef<
|
||||
},
|
||||
);
|
||||
|
||||
DynamicBudgetTableInner.displayName = 'DynamicBudgetTableInner';
|
||||
|
||||
export const DynamicBudgetTable = forwardRef<
|
||||
BudgetTable,
|
||||
typeof BudgetTable,
|
||||
DynamicBudgetTableInnerProps
|
||||
>((props, ref) => {
|
||||
return (
|
||||
@@ -118,3 +115,5 @@ export const DynamicBudgetTable = forwardRef<
|
||||
</AutoSizer>
|
||||
);
|
||||
});
|
||||
|
||||
DynamicBudgetTable.displayName = 'DynamicBudgetTable';
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
|
||||
import { type PrefsState } from 'loot-core/src/client/state-types/prefs';
|
||||
import { send, listen } from 'loot-core/src/platform/client/fetch';
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
import {
|
||||
@@ -13,6 +11,7 @@ import {
|
||||
|
||||
import { type BoundActions, useActions } from '../../hooks/useActions';
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
|
||||
import { AnimatedLoading } from '../../icons/AnimatedLoading';
|
||||
import { theme } from '../../style';
|
||||
@@ -22,13 +21,9 @@ import { SyncRefresh } from '../SyncRefresh';
|
||||
import { BudgetTable } from './MobileBudgetTable';
|
||||
import { prewarmMonth, switchBudgetType } from './util';
|
||||
|
||||
const CATEGORY_BUDGET_EDIT_ACTION = 'category-budget';
|
||||
const BALANCE_MENU_OPEN_ACTION = 'balance-menu';
|
||||
|
||||
type BudgetInnerProps = {
|
||||
categories: CategoryEntity[];
|
||||
categoryGroups: CategoryGroupEntity[];
|
||||
prefs: PrefsState['local'];
|
||||
loadPrefs: BoundActions['loadPrefs'];
|
||||
savePrefs: BoundActions['savePrefs'];
|
||||
budgetType: 'rollover' | 'report';
|
||||
@@ -52,9 +47,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
const {
|
||||
categoryGroups,
|
||||
categories,
|
||||
prefs,
|
||||
loadPrefs,
|
||||
savePrefs,
|
||||
budgetType,
|
||||
spreadsheet,
|
||||
applyBudgetAction,
|
||||
@@ -76,8 +69,10 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
const [currentMonth, setCurrentMonth] = useState(currMonth);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editingBudgetCategoryId, setEditingBudgetCategoryId] = useState(null);
|
||||
const [openBalanceActionMenuId, setOpenBalanceActionMenuId] = useState(null);
|
||||
|
||||
const [_numberFormat] = useLocalPref('numberFormat');
|
||||
const numberFormat = _numberFormat || 'comma-dot';
|
||||
const [hideFraction = false] = useLocalPref('hideFraction');
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
@@ -360,32 +355,6 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const onEditCategoryBudget = id => {
|
||||
onEdit(CATEGORY_BUDGET_EDIT_ACTION, id);
|
||||
};
|
||||
|
||||
const onOpenBalanceActionMenu = id => {
|
||||
onEdit(BALANCE_MENU_OPEN_ACTION, id);
|
||||
};
|
||||
|
||||
const onEdit = (action, id) => {
|
||||
// Do not allow editing if another field is currently being edited.
|
||||
// Cancel the currently editing field in that case.
|
||||
const currentlyEditing = editingBudgetCategoryId || openBalanceActionMenuId;
|
||||
|
||||
setEditingBudgetCategoryId(
|
||||
action === CATEGORY_BUDGET_EDIT_ACTION && !currentlyEditing ? id : null,
|
||||
);
|
||||
setOpenBalanceActionMenuId(
|
||||
action === BALANCE_MENU_OPEN_ACTION && !currentlyEditing ? id : null,
|
||||
);
|
||||
|
||||
return { action, editingId: !currentlyEditing ? id : null };
|
||||
};
|
||||
|
||||
const numberFormat = prefs?.numberFormat || 'comma-dot';
|
||||
const hideFraction = prefs?.hideFraction || false;
|
||||
|
||||
if (!categoryGroups || !initialized) {
|
||||
return (
|
||||
<View
|
||||
@@ -412,7 +381,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
<BudgetTable
|
||||
// This key forces the whole table rerender when the number
|
||||
// format changes
|
||||
key={numberFormat + hideFraction}
|
||||
key={`${numberFormat}${hideFraction}`}
|
||||
categoryGroups={categoryGroups}
|
||||
type={budgetType}
|
||||
month={currentMonth}
|
||||
@@ -434,14 +403,9 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
onBudgetAction={applyBudgetAction}
|
||||
onRefresh={onRefresh}
|
||||
onSwitchBudgetType={onSwitchBudgetType}
|
||||
savePrefs={savePrefs}
|
||||
pushModal={pushModal}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
editingBudgetCategoryId={editingBudgetCategoryId}
|
||||
onEditCategoryBudget={onEditCategoryBudget}
|
||||
openBalanceActionMenuId={openBalanceActionMenuId}
|
||||
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
|
||||
/>
|
||||
)}
|
||||
</SyncRefresh>
|
||||
@@ -450,10 +414,8 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
|
||||
export function Budget() {
|
||||
const { list: categories, grouped: categoryGroups } = useCategories();
|
||||
const budgetType = useSelector(
|
||||
state => state.prefs.local?.budgetType || 'rollover',
|
||||
);
|
||||
const prefs = useSelector(state => state.prefs.local);
|
||||
const [_budgetType] = useLocalPref('budgetType');
|
||||
const budgetType = _budgetType || 'rollover';
|
||||
|
||||
const actions = useActions();
|
||||
const spreadsheet = useSpreadsheet();
|
||||
@@ -463,7 +425,6 @@ export function Budget() {
|
||||
categoryGroups={categoryGroups}
|
||||
categories={categories}
|
||||
budgetType={budgetType}
|
||||
prefs={prefs}
|
||||
{...actions}
|
||||
spreadsheet={spreadsheet}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { memo, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { memo, useRef, useState } from 'react';
|
||||
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
||||
@@ -7,6 +6,11 @@ import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries';
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
|
||||
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import {
|
||||
SingleActiveEditFormProvider,
|
||||
useSingleActiveEditForm,
|
||||
} from '../../hooks/useSingleActiveEditForm';
|
||||
import {
|
||||
SvgArrowThinLeft,
|
||||
SvgArrowThinRight,
|
||||
@@ -20,6 +24,7 @@ import { Label } from '../common/Label';
|
||||
import { Menu } from '../common/Menu';
|
||||
import { Text } from '../common/Text';
|
||||
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';
|
||||
@@ -134,6 +139,7 @@ function BudgetCell({
|
||||
month,
|
||||
onBudgetAction,
|
||||
onEdit,
|
||||
onBlur,
|
||||
isEditing,
|
||||
}) {
|
||||
const sheetValue = useSheetValue(binding);
|
||||
@@ -146,7 +152,7 @@ function BudgetCell({
|
||||
}
|
||||
|
||||
function onAmountClick() {
|
||||
onEdit?.(categoryId);
|
||||
onEdit?.();
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -162,7 +168,7 @@ function BudgetCell({
|
||||
focused={isEditing}
|
||||
textStyle={{ ...styles.smallText, ...textStyle }}
|
||||
onUpdate={updateBudgetAmount}
|
||||
onBlur={() => onEdit?.(null)}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<View
|
||||
role="button"
|
||||
@@ -244,10 +250,6 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
style,
|
||||
month,
|
||||
onEdit,
|
||||
isEditingBudget,
|
||||
onEditBudget,
|
||||
isBalanceActionMenuOpen,
|
||||
onOpenBalanceActionMenu,
|
||||
onBudgetAction,
|
||||
show3Cols,
|
||||
showBudgetedCol,
|
||||
@@ -255,11 +257,22 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
const opacity = blank ? 0 : 1;
|
||||
const balanceTooltip = useTooltip();
|
||||
|
||||
useEffect(() => {
|
||||
if (isBalanceActionMenuOpen) {
|
||||
const [isEditingBudget, setIsEditingBudget] = useState(false);
|
||||
const { onRequestActiveEdit, onClearActiveEdit } = useSingleActiveEditForm();
|
||||
|
||||
const onEditBudget = () => {
|
||||
onRequestActiveEdit(`${category.id}-budget`, () => {
|
||||
setIsEditingBudget(true);
|
||||
return () => setIsEditingBudget(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onOpenBalanceActionMenu = () => {
|
||||
onRequestActiveEdit(`${category.id}-balance`, () => {
|
||||
balanceTooltip.open();
|
||||
}
|
||||
}, [isBalanceActionMenuOpen, balanceTooltip]);
|
||||
return () => balanceTooltip.close();
|
||||
});
|
||||
};
|
||||
|
||||
const listItemRef = useRef();
|
||||
|
||||
@@ -317,6 +330,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
onBudgetAction={onBudgetAction}
|
||||
isEditing={isEditingBudget}
|
||||
onEdit={onEditBudget}
|
||||
onBlur={onClearActiveEdit}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
@@ -349,7 +363,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
>
|
||||
<span
|
||||
role="button"
|
||||
onPointerUp={() => onOpenBalanceActionMenu?.(category.id)}
|
||||
onPointerUp={() => onOpenBalanceActionMenu?.()}
|
||||
onPointerDown={e => e.preventDefault()}
|
||||
>
|
||||
<BalanceWithCarryover
|
||||
@@ -371,7 +385,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
monthIndex={monthUtils.getMonthIndex(month)}
|
||||
onBudgetAction={_onBudgetAction}
|
||||
onClose={() => {
|
||||
onOpenBalanceActionMenu?.(null);
|
||||
onClearActiveEdit();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -382,7 +396,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
monthIndex={monthUtils.getMonthIndex(month)}
|
||||
onBudgetAction={_onBudgetAction}
|
||||
onClose={() => {
|
||||
onOpenBalanceActionMenu?.(null);
|
||||
onClearActiveEdit();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -795,10 +809,6 @@ const ExpenseGroup = memo(function ExpenseGroup({
|
||||
editMode,
|
||||
onEditGroup,
|
||||
onEditCategory,
|
||||
editingBudgetCategoryId,
|
||||
onEditCategoryBudget,
|
||||
openBalanceActionMenuId,
|
||||
onOpenBalanceActionMenu,
|
||||
// gestures,
|
||||
month,
|
||||
// onReorderCategory,
|
||||
@@ -873,10 +883,6 @@ const ExpenseGroup = memo(function ExpenseGroup({
|
||||
{group.categories
|
||||
.filter(category => !category.hidden || showHiddenCategories)
|
||||
.map((category, index) => {
|
||||
const isEditingCategoryBudget =
|
||||
editingBudgetCategoryId === category.id;
|
||||
const isBalanceActionMenuOpen =
|
||||
openBalanceActionMenuId === category.id;
|
||||
return (
|
||||
<ExpenseCategory
|
||||
key={category.id}
|
||||
@@ -915,10 +921,6 @@ const ExpenseGroup = memo(function ExpenseGroup({
|
||||
showBudgetedCol={showBudgetedCol}
|
||||
editMode={editMode}
|
||||
onEdit={onEditCategory}
|
||||
isEditingBudget={isEditingCategoryBudget}
|
||||
onEditBudget={onEditCategoryBudget}
|
||||
isBalanceActionMenuOpen={isBalanceActionMenuOpen}
|
||||
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
|
||||
// gestures={gestures}
|
||||
month={month}
|
||||
// onReorder={onReorderCategory}
|
||||
@@ -1019,10 +1021,6 @@ function BudgetGroups({
|
||||
categoryGroups,
|
||||
onEditGroup,
|
||||
onEditCategory,
|
||||
editingBudgetCategoryId,
|
||||
onEditCategoryBudget,
|
||||
openBalanceActionMenuId,
|
||||
onOpenBalanceActionMenu,
|
||||
editMode,
|
||||
gestures,
|
||||
month,
|
||||
@@ -1048,71 +1046,67 @@ function BudgetGroups({
|
||||
const { incomeGroup, expenseGroups } = separateGroups(categoryGroups);
|
||||
|
||||
return (
|
||||
<View
|
||||
data-testid="budget-groups"
|
||||
style={{ flex: '1 0 auto', overflowY: 'auto', paddingBottom: 15 }}
|
||||
>
|
||||
{expenseGroups
|
||||
.filter(group => !group.hidden || showHiddenCategories)
|
||||
.map(group => {
|
||||
return (
|
||||
<ExpenseGroup
|
||||
key={group.id}
|
||||
type={type}
|
||||
group={group}
|
||||
showBudgetedCol={showBudgetedCol}
|
||||
gestures={gestures}
|
||||
month={month}
|
||||
editMode={editMode}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
editingBudgetCategoryId={editingBudgetCategoryId}
|
||||
onEditCategoryBudget={onEditCategoryBudget}
|
||||
openBalanceActionMenuId={openBalanceActionMenuId}
|
||||
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onAddCategory={onAddCategory}
|
||||
onReorderCategory={onReorderCategory}
|
||||
onReorderGroup={onReorderGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
show3Cols={show3Cols}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
pushModal={pushModal}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<SingleActiveEditFormProvider formName="mobile-budget-table">
|
||||
<View
|
||||
style={{
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
data-testid="budget-groups"
|
||||
style={{ flex: '1 0 auto', overflowY: 'auto', paddingBottom: 15 }}
|
||||
>
|
||||
<Button onClick={onAddGroup} style={{ fontSize: 12, margin: 10 }}>
|
||||
Add Group
|
||||
</Button>
|
||||
</View>
|
||||
{expenseGroups
|
||||
.filter(group => !group.hidden || showHiddenCategories)
|
||||
.map(group => {
|
||||
return (
|
||||
<ExpenseGroup
|
||||
key={group.id}
|
||||
type={type}
|
||||
group={group}
|
||||
showBudgetedCol={showBudgetedCol}
|
||||
gestures={gestures}
|
||||
month={month}
|
||||
editMode={editMode}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onAddCategory={onAddCategory}
|
||||
onReorderCategory={onReorderCategory}
|
||||
onReorderGroup={onReorderGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
show3Cols={show3Cols}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
pushModal={pushModal}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{incomeGroup && (
|
||||
<IncomeGroup
|
||||
type={type}
|
||||
group={incomeGroup}
|
||||
month={month}
|
||||
onAddCategory={onAddCategory}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
editMode={editMode}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
editingBudgetCategoryId={editingBudgetCategoryId}
|
||||
onEditCategoryBudget={onEditCategoryBudget}
|
||||
onBudgetAction={onBudgetAction}
|
||||
pushModal={pushModal}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Button onClick={onAddGroup} style={{ fontSize: 12, margin: 10 }}>
|
||||
Add Group
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{incomeGroup && (
|
||||
<IncomeGroup
|
||||
type={type}
|
||||
group={incomeGroup}
|
||||
month={month}
|
||||
onAddCategory={onAddCategory}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
editMode={editMode}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
pushModal={pushModal}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</SingleActiveEditFormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1139,14 +1133,9 @@ export function BudgetTable({
|
||||
onBudgetAction,
|
||||
onRefresh,
|
||||
onSwitchBudgetType,
|
||||
savePrefs,
|
||||
pushModal,
|
||||
onEditGroup,
|
||||
onEditCategory,
|
||||
editingBudgetCategoryId,
|
||||
onEditCategoryBudget,
|
||||
openBalanceActionMenuId,
|
||||
onOpenBalanceActionMenu,
|
||||
}) {
|
||||
const { width } = useResponsive();
|
||||
const show3Cols = width >= 360;
|
||||
@@ -1154,24 +1143,15 @@ export function BudgetTable({
|
||||
// let editMode = false; // neuter editMode -- sorry, not rewriting drag-n-drop right now
|
||||
const format = useFormat();
|
||||
|
||||
const mobileShowBudgetedColPref = useSelector(state => {
|
||||
return state.prefs?.local?.toggleMobileDisplayPref || true;
|
||||
});
|
||||
|
||||
const showHiddenCategories = useSelector(state => {
|
||||
return state.prefs?.local?.['budget.showHiddenCategories'] || false;
|
||||
});
|
||||
|
||||
const [showBudgetedCol, setShowBudgetedCol] = useState(
|
||||
!mobileShowBudgetedColPref &&
|
||||
!document.cookie.match(/mobileShowBudgetedColPref=true/),
|
||||
const [showSpentColumn = false, setShowSpentColumnPref] = useLocalPref(
|
||||
'mobile.showSpentColumn',
|
||||
);
|
||||
|
||||
const [showHiddenCategories = false, setShowHiddenCategoriesPref] =
|
||||
useLocalPref('budget.showHiddenCategories');
|
||||
|
||||
function toggleDisplay() {
|
||||
setShowBudgetedCol(!showBudgetedCol);
|
||||
if (!showBudgetedCol) {
|
||||
savePrefs({ mobileShowBudgetedColPref: true });
|
||||
}
|
||||
setShowSpentColumnPref(!showSpentColumn);
|
||||
}
|
||||
|
||||
const buttonStyle = {
|
||||
@@ -1187,9 +1167,7 @@ export function BudgetTable({
|
||||
};
|
||||
|
||||
const onToggleHiddenCategories = () => {
|
||||
savePrefs({
|
||||
'budget.showHiddenCategories': !showHiddenCategories,
|
||||
});
|
||||
setShowHiddenCategoriesPref(!showHiddenCategories);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1255,7 +1233,7 @@ export function BudgetTable({
|
||||
/>
|
||||
)}
|
||||
<View style={{ flex: 1 }} />
|
||||
{(show3Cols || showBudgetedCol) && (
|
||||
{(show3Cols || !showSpentColumn) && (
|
||||
<Button
|
||||
type="bare"
|
||||
disabled={show3Cols}
|
||||
@@ -1265,7 +1243,7 @@ export function BudgetTable({
|
||||
padding: '0 8px',
|
||||
margin: '0 -8px',
|
||||
background:
|
||||
showBudgetedCol && !show3Cols
|
||||
!showSpentColumn && !show3Cols
|
||||
? `linear-gradient(-45deg, ${theme.formInputBackgroundSelection} 8px, transparent 0)`
|
||||
: null,
|
||||
}}
|
||||
@@ -1302,7 +1280,7 @@ export function BudgetTable({
|
||||
</View>
|
||||
</Button>
|
||||
)}
|
||||
{(show3Cols || !showBudgetedCol) && (
|
||||
{(show3Cols || showSpentColumn) && (
|
||||
<Button
|
||||
type="bare"
|
||||
disabled={show3Cols}
|
||||
@@ -1310,7 +1288,7 @@ export function BudgetTable({
|
||||
style={{
|
||||
...buttonStyle,
|
||||
background:
|
||||
!showBudgetedCol && !show3Cols
|
||||
showSpentColumn && !show3Cols
|
||||
? `linear-gradient(45deg, ${theme.formInputBackgroundSelection} 8px, transparent 0)`
|
||||
: null,
|
||||
}}
|
||||
@@ -1373,11 +1351,16 @@ export function BudgetTable({
|
||||
// style={{ backgroundColor: colors.n10 }}
|
||||
// automaticallyAdjustContentInsets={false}
|
||||
// >
|
||||
<View data-testid="budget-table">
|
||||
<View
|
||||
data-testid="budget-table"
|
||||
style={{
|
||||
paddingBottom: MOBILE_NAV_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<BudgetGroups
|
||||
type={type}
|
||||
categoryGroups={categoryGroups}
|
||||
showBudgetedCol={showBudgetedCol}
|
||||
showBudgetedCol={!showSpentColumn}
|
||||
show3Cols={show3Cols}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
// gestures={gestures}
|
||||
@@ -1385,10 +1368,6 @@ export function BudgetTable({
|
||||
editMode={editMode}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
editingBudgetCategoryId={editingBudgetCategoryId}
|
||||
onEditCategoryBudget={onEditCategoryBudget}
|
||||
openBalanceActionMenuId={openBalanceActionMenuId}
|
||||
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onAddCategory={onAddCategory}
|
||||
@@ -1416,17 +1395,13 @@ export function BudgetTable({
|
||||
<BudgetGroups
|
||||
type={type}
|
||||
categoryGroups={categoryGroups}
|
||||
showBudgetedCol={showBudgetedCol}
|
||||
showBudgetedCol={!showSpentColumn}
|
||||
show3Cols={show3Cols}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
// gestures={gestures}
|
||||
editMode={editMode}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
editingBudgetCategoryId={editingBudgetCategoryId}
|
||||
onEditCategoryBudget={onEditCategoryBudget}
|
||||
openBalanceActionMenuId={openBalanceActionMenuId}
|
||||
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onAddCategory={onAddCategory}
|
||||
|
||||
@@ -22,7 +22,7 @@ function Calendar({ color, onClick }: CalendarProps) {
|
||||
|
||||
type MonthCountSelectorProps = {
|
||||
maxMonths: number;
|
||||
onChange: (value: number) => Promise<void>;
|
||||
onChange: (value: number) => void;
|
||||
};
|
||||
|
||||
export function MonthCountSelector({
|
||||
|
||||
@@ -126,7 +126,7 @@ export const MonthPicker = ({
|
||||
}),
|
||||
...(hovered &&
|
||||
!selected && {
|
||||
backgroundColor: 'rgba(100, 100, 100, .15)',
|
||||
backgroundColor: theme.buttonBareBackgroundHover,
|
||||
}),
|
||||
...(hovered &&
|
||||
selected && {
|
||||
|
||||
@@ -7,33 +7,31 @@ import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
type NavigateFunction,
|
||||
type PathMatch,
|
||||
useLocation,
|
||||
useMatch,
|
||||
} from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
|
||||
import { type QueriesState } from 'loot-core/src/client/state-types/queries';
|
||||
import { send, listen } from 'loot-core/src/platform/client/fetch';
|
||||
import {
|
||||
addCategory,
|
||||
addNotification,
|
||||
applyBudgetAction,
|
||||
createCategory,
|
||||
createGroup,
|
||||
deleteCategory,
|
||||
deleteGroup,
|
||||
getCategories,
|
||||
loadPrefs,
|
||||
moveCategory,
|
||||
moveCategoryGroup,
|
||||
pushModal,
|
||||
updateCategory,
|
||||
deleteCategory,
|
||||
addGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
} from 'loot-core/src/shared/categories';
|
||||
} from 'loot-core/src/client/actions';
|
||||
import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
|
||||
import { send, listen } from 'loot-core/src/platform/client/fetch';
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
import { type GlobalPrefs, type LocalPrefs } from 'loot-core/src/types/prefs';
|
||||
|
||||
import { type BoundActions, useActions } from '../../hooks/useActions';
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
|
||||
import { useGlobalPref } from '../../hooks/useGlobalPref';
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import { useNavigate } from '../../hooks/useNavigate';
|
||||
import { styles } from '../../style';
|
||||
import { View } from '../common/View';
|
||||
@@ -73,62 +71,41 @@ type RolloverComponents = {
|
||||
|
||||
type BudgetProps = {
|
||||
accountId?: string;
|
||||
startMonth: LocalPrefs['budget.startMonth'];
|
||||
collapsedPrefs: LocalPrefs['budget.collapsed'];
|
||||
summaryCollapsed: LocalPrefs['budget.summaryCollapsed'];
|
||||
budgetType: LocalPrefs['budgetType'];
|
||||
maxMonths: GlobalPrefs['maxMonths'];
|
||||
categoryGroups: QueriesState['categories']['grouped'];
|
||||
reportComponents: ReportComponents;
|
||||
rolloverComponents: RolloverComponents;
|
||||
titlebar: TitlebarContextValue;
|
||||
match: PathMatch<string>;
|
||||
spreadsheet: ReturnType<typeof useSpreadsheet>;
|
||||
navigate: NavigateFunction;
|
||||
getCategories: BoundActions['getCategories'];
|
||||
savePrefs: BoundActions['savePrefs'];
|
||||
createCategory: BoundActions['createCategory'];
|
||||
updateCategory: BoundActions['updateCategory'];
|
||||
pushModal: BoundActions['pushModal'];
|
||||
deleteCategory: BoundActions['deleteCategory'];
|
||||
createGroup: BoundActions['createGroup'];
|
||||
updateGroup: BoundActions['updateGroup'];
|
||||
deleteGroup: BoundActions['deleteGroup'];
|
||||
applyBudgetAction: BoundActions['applyBudgetAction'];
|
||||
moveCategory: BoundActions['moveCategory'];
|
||||
moveCategoryGroup: BoundActions['moveCategoryGroup'];
|
||||
loadPrefs: BoundActions['loadPrefs'];
|
||||
addNotification: BoundActions['addNotification'];
|
||||
};
|
||||
|
||||
function BudgetInner(props: BudgetProps) {
|
||||
const currentMonth = monthUtils.currentMonth();
|
||||
const tableRef = useRef(null);
|
||||
const spreadsheet = useSpreadsheet();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [_startMonth, setBudgetStartMonthPref] =
|
||||
useLocalPref('budget.startMonth');
|
||||
const startMonth = _startMonth || currentMonth;
|
||||
const [summaryCollapsed, setSummaryCollapsedPref] = useLocalPref(
|
||||
'budget.summaryCollapsed',
|
||||
);
|
||||
const [_budgetType] = useLocalPref('budgetType');
|
||||
const budgetType = _budgetType || 'rollover';
|
||||
const [_maxMonths] = useGlobalPref('maxMonths');
|
||||
const maxMonths = _maxMonths || 1;
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [prewarmStartMonth, setPrewarmStartMonth] = useState(
|
||||
props.startMonth || currentMonth,
|
||||
);
|
||||
|
||||
const [newCategoryForGroup, setNewCategoryForGroup] = useState(null);
|
||||
const [isAddingGroup, setIsAddingGroup] = useState(false);
|
||||
const [collapsed, setCollapsed] = useState(props.collapsedPrefs || []);
|
||||
const [bounds, setBounds] = useState({
|
||||
start: currentMonth,
|
||||
end: currentMonth,
|
||||
});
|
||||
const [categoryGroups, setCategoryGroups] = useState(null);
|
||||
const [summaryCollapsed, setSummaryCollapsed] = useState(
|
||||
props.summaryCollapsed,
|
||||
);
|
||||
const { grouped: categoryGroups } = useCategories();
|
||||
|
||||
async function loadCategories() {
|
||||
const result = await props.getCategories();
|
||||
setCategoryGroups(result.grouped);
|
||||
function loadCategories() {
|
||||
dispatch(getCategories());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { titlebar, budgetType } = props;
|
||||
const { titlebar } = props;
|
||||
|
||||
async function run() {
|
||||
loadCategories();
|
||||
@@ -138,9 +115,9 @@ function BudgetInner(props: BudgetProps) {
|
||||
|
||||
await prewarmAllMonths(
|
||||
budgetType,
|
||||
props.spreadsheet,
|
||||
spreadsheet,
|
||||
{ start, end },
|
||||
prewarmStartMonth,
|
||||
startMonth,
|
||||
);
|
||||
|
||||
setInitialized(true);
|
||||
@@ -185,10 +162,6 @@ function BudgetInner(props: BudgetProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
props.savePrefs({ 'budget.collapsed': collapsed });
|
||||
}, [collapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
send('get-budget-bounds').then(({ start, end }) => {
|
||||
if (bounds.start !== start || bounds.end !== end) {
|
||||
@@ -198,12 +171,10 @@ function BudgetInner(props: BudgetProps) {
|
||||
}, [props.accountId]);
|
||||
|
||||
const onMonthSelect = async (month, numDisplayed) => {
|
||||
setPrewarmStartMonth(month);
|
||||
setBudgetStartMonthPref(month);
|
||||
|
||||
const warmingMonth = month;
|
||||
|
||||
const startMonth = props.startMonth || currentMonth;
|
||||
|
||||
// We could be smarter about this, but this is a good start. We
|
||||
// optimize for the case where users press the left/right button
|
||||
// to move between months. This loads the month data all at once
|
||||
@@ -213,51 +184,37 @@ function BudgetInner(props: BudgetProps) {
|
||||
if (month < startMonth) {
|
||||
// pre-warm prev month
|
||||
await prewarmMonth(
|
||||
props.budgetType,
|
||||
props.spreadsheet,
|
||||
budgetType,
|
||||
spreadsheet,
|
||||
monthUtils.subMonths(month, 1),
|
||||
);
|
||||
} else if (month > startMonth) {
|
||||
// pre-warm next month
|
||||
await prewarmMonth(
|
||||
props.budgetType,
|
||||
props.spreadsheet,
|
||||
budgetType,
|
||||
spreadsheet,
|
||||
monthUtils.addMonths(month, numDisplayed),
|
||||
);
|
||||
}
|
||||
|
||||
if (warmingMonth === month) {
|
||||
props.savePrefs({ 'budget.startMonth': month });
|
||||
setBudgetStartMonthPref(month);
|
||||
}
|
||||
};
|
||||
|
||||
const onShowNewCategory = groupId => {
|
||||
setNewCategoryForGroup(groupId);
|
||||
setCollapsed(state => state.filter(c => c !== groupId));
|
||||
};
|
||||
|
||||
const onHideNewCategory = () => {
|
||||
setNewCategoryForGroup(null);
|
||||
};
|
||||
|
||||
const onShowNewGroup = () => {
|
||||
setIsAddingGroup(true);
|
||||
};
|
||||
|
||||
const onHideNewGroup = () => {
|
||||
setIsAddingGroup(false);
|
||||
};
|
||||
|
||||
const categoryNameAlreadyExistsNotification = name => {
|
||||
props.addNotification({
|
||||
type: 'error',
|
||||
message: `Category ‘${name}’ already exists in group (May be Hidden)`,
|
||||
});
|
||||
dispatch(
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: `Category ‘${name}’ already exists in group (May be Hidden)`,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveCategory = async category => {
|
||||
const cats = await send('get-categories');
|
||||
const exists =
|
||||
(await props.getCategories()).grouped
|
||||
cats.grouped
|
||||
.filter(g => g.id === category.cat_group)[0]
|
||||
.categories.filter(
|
||||
c => c.name.toUpperCase() === category.name.toUpperCase(),
|
||||
@@ -271,24 +228,16 @@ function BudgetInner(props: BudgetProps) {
|
||||
}
|
||||
|
||||
if (category.id === 'new') {
|
||||
const id = await props.createCategory(
|
||||
category.name,
|
||||
category.cat_group,
|
||||
category.is_income,
|
||||
category.hidden,
|
||||
);
|
||||
|
||||
setNewCategoryForGroup(null);
|
||||
setCategoryGroups(state =>
|
||||
addCategory(state, {
|
||||
...category,
|
||||
is_income: category.is_income ? 1 : 0,
|
||||
id,
|
||||
}),
|
||||
dispatch(
|
||||
createCategory(
|
||||
category.name,
|
||||
category.cat_group,
|
||||
category.is_income,
|
||||
category.hidden,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
props.updateCategory(category);
|
||||
setCategoryGroups(state => updateCategory(state, category));
|
||||
dispatch(updateCategory(category));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -296,38 +245,26 @@ function BudgetInner(props: BudgetProps) {
|
||||
const mustTransfer = await send('must-category-transfer', { id });
|
||||
|
||||
if (mustTransfer) {
|
||||
props.pushModal('confirm-category-delete', {
|
||||
category: id,
|
||||
onDelete: transferCategory => {
|
||||
if (id !== transferCategory) {
|
||||
props.deleteCategory(id, transferCategory);
|
||||
|
||||
setCategoryGroups(state => deleteCategory(state, id));
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
props.deleteCategory(id);
|
||||
|
||||
setCategoryGroups(state => deleteCategory(state, id));
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveGroup = async group => {
|
||||
if (group.id === 'new') {
|
||||
const id = await props.createGroup(group.name);
|
||||
setIsAddingGroup(false);
|
||||
setCategoryGroups(state =>
|
||||
addGroup(state, {
|
||||
...group,
|
||||
is_income: 0,
|
||||
categories: group.categories || [],
|
||||
id,
|
||||
dispatch(
|
||||
pushModal('confirm-category-delete', {
|
||||
category: id,
|
||||
onDelete: transferCategory => {
|
||||
if (id !== transferCategory) {
|
||||
dispatch(deleteCategory(id, transferCategory));
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
props.updateGroup(group);
|
||||
setCategoryGroups(state => updateGroup(state, group));
|
||||
dispatch(deleteCategory(id));
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveGroup = group => {
|
||||
if (group.id === 'new') {
|
||||
dispatch(createGroup(group.name));
|
||||
} else {
|
||||
dispatch(updateGroup(group));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -343,27 +280,25 @@ function BudgetInner(props: BudgetProps) {
|
||||
}
|
||||
|
||||
if (mustTransfer) {
|
||||
props.pushModal('confirm-category-delete', {
|
||||
group: id,
|
||||
onDelete: transferCategory => {
|
||||
props.deleteGroup(id, transferCategory);
|
||||
|
||||
setCategoryGroups(state => deleteGroup(state, id));
|
||||
},
|
||||
});
|
||||
dispatch(
|
||||
pushModal('confirm-category-delete', {
|
||||
group: id,
|
||||
onDelete: transferCategory => {
|
||||
dispatch(deleteGroup(id, transferCategory));
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
props.deleteGroup(id);
|
||||
|
||||
setCategoryGroups(state => deleteGroup(state, id));
|
||||
dispatch(deleteGroup(id));
|
||||
}
|
||||
};
|
||||
|
||||
const onBudgetAction = (month, type, args) => {
|
||||
props.applyBudgetAction(month, type, args);
|
||||
dispatch(applyBudgetAction(month, type, args));
|
||||
};
|
||||
|
||||
const onShowActivity = (categoryName, categoryId, month) => {
|
||||
props.navigate('/accounts', {
|
||||
navigate('/accounts', {
|
||||
state: {
|
||||
goBack: true,
|
||||
filterName: `${categoryName} (${monthUtils.format(
|
||||
@@ -379,7 +314,7 @@ function BudgetInner(props: BudgetProps) {
|
||||
};
|
||||
|
||||
const onReorderCategory = async sortInfo => {
|
||||
const cats = await props.getCategories();
|
||||
const cats = await send('get-categories');
|
||||
const moveCandidate = cats.list.filter(c => c.id === sortInfo.id)[0];
|
||||
const exists =
|
||||
cats.grouped
|
||||
@@ -394,23 +329,15 @@ function BudgetInner(props: BudgetProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId);
|
||||
setCategoryGroups(state =>
|
||||
moveCategory(state, sortInfo.id, sortInfo.groupId, sortInfo.targetId),
|
||||
);
|
||||
dispatch(moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId));
|
||||
};
|
||||
|
||||
const onReorderGroup = async sortInfo => {
|
||||
props.moveCategoryGroup(sortInfo.id, sortInfo.targetId);
|
||||
setCategoryGroups(state =>
|
||||
moveCategoryGroup(state, sortInfo.id, sortInfo.targetId),
|
||||
);
|
||||
dispatch(moveCategoryGroup(sortInfo.id, sortInfo.targetId));
|
||||
};
|
||||
|
||||
const onToggleCollapse = () => {
|
||||
const collapsed = !summaryCollapsed;
|
||||
setSummaryCollapsed(collapsed);
|
||||
props.savePrefs({ 'budget.summaryCollapsed': collapsed });
|
||||
setSummaryCollapsedPref(!summaryCollapsed);
|
||||
};
|
||||
|
||||
const onTitlebarEvent = async ({ type, payload }: TitlebarMessage) => {
|
||||
@@ -418,10 +345,12 @@ function BudgetInner(props: BudgetProps) {
|
||||
case SWITCH_BUDGET_MESSAGE_TYPE: {
|
||||
await switchBudgetType(
|
||||
payload.newBudgetType,
|
||||
props.spreadsheet,
|
||||
spreadsheet,
|
||||
bounds,
|
||||
prewarmStartMonth,
|
||||
() => props.loadPrefs(),
|
||||
startMonth,
|
||||
async () => {
|
||||
dispatch(loadPrefs());
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -429,23 +358,14 @@ function BudgetInner(props: BudgetProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
maxMonths: originalMaxMonths,
|
||||
budgetType: type,
|
||||
reportComponents,
|
||||
rolloverComponents,
|
||||
} = props;
|
||||
|
||||
const maxMonths = originalMaxMonths || 1;
|
||||
const { reportComponents, rolloverComponents } = props;
|
||||
|
||||
if (!initialized || !categoryGroups) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startMonth = props.startMonth || currentMonth;
|
||||
|
||||
let table;
|
||||
if (type === 'report') {
|
||||
if (budgetType === 'report') {
|
||||
table = (
|
||||
<ReportProvider
|
||||
summaryCollapsed={summaryCollapsed}
|
||||
@@ -454,22 +374,13 @@ function BudgetInner(props: BudgetProps) {
|
||||
>
|
||||
<DynamicBudgetTable
|
||||
ref={tableRef}
|
||||
type={type}
|
||||
categoryGroups={categoryGroups}
|
||||
prewarmStartMonth={prewarmStartMonth}
|
||||
type={budgetType}
|
||||
prewarmStartMonth={startMonth}
|
||||
startMonth={startMonth}
|
||||
monthBounds={bounds}
|
||||
maxMonths={maxMonths}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
newCategoryForGroup={newCategoryForGroup}
|
||||
isAddingGroup={isAddingGroup}
|
||||
dataComponents={reportComponents}
|
||||
onMonthSelect={onMonthSelect}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
onHideNewCategory={onHideNewCategory}
|
||||
onShowNewGroup={onShowNewGroup}
|
||||
onHideNewGroup={onHideNewGroup}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onSaveCategory={onSaveCategory}
|
||||
@@ -484,29 +395,19 @@ function BudgetInner(props: BudgetProps) {
|
||||
} else {
|
||||
table = (
|
||||
<RolloverContext
|
||||
categoryGroups={categoryGroups}
|
||||
summaryCollapsed={summaryCollapsed}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onToggleSummaryCollapse={onToggleCollapse}
|
||||
>
|
||||
<DynamicBudgetTable
|
||||
ref={tableRef}
|
||||
type={type}
|
||||
categoryGroups={categoryGroups}
|
||||
prewarmStartMonth={prewarmStartMonth}
|
||||
type={budgetType}
|
||||
prewarmStartMonth={startMonth}
|
||||
startMonth={startMonth}
|
||||
monthBounds={bounds}
|
||||
maxMonths={maxMonths}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
newCategoryForGroup={newCategoryForGroup}
|
||||
isAddingGroup={isAddingGroup}
|
||||
dataComponents={rolloverComponents}
|
||||
onMonthSelect={onMonthSelect}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
onHideNewCategory={onHideNewCategory}
|
||||
onShowNewGroup={onShowNewGroup}
|
||||
onHideNewGroup={onHideNewGroup}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onSaveCategory={onSaveCategory}
|
||||
@@ -533,28 +434,10 @@ const RolloverBudgetSummary = memo<{ month: string }>(props => {
|
||||
);
|
||||
});
|
||||
|
||||
export function Budget() {
|
||||
const startMonth = useSelector(
|
||||
state => state.prefs.local['budget.startMonth'],
|
||||
);
|
||||
const collapsedPrefs = useSelector(
|
||||
state => state.prefs.local['budget.collapsed'],
|
||||
);
|
||||
const summaryCollapsed = useSelector(
|
||||
state => state.prefs.local['budget.summaryCollapsed'],
|
||||
);
|
||||
const budgetType = useSelector(
|
||||
state => state.prefs.local.budgetType || 'rollover',
|
||||
);
|
||||
const maxMonths = useSelector(state => state.prefs.global.maxMonths);
|
||||
const { grouped: categoryGroups } = useCategories();
|
||||
RolloverBudgetSummary.displayName = 'RolloverBudgetSummary';
|
||||
|
||||
const actions = useActions();
|
||||
const spreadsheet = useSpreadsheet();
|
||||
export function Budget() {
|
||||
const titlebar = useContext(TitlebarContext);
|
||||
const location = useLocation();
|
||||
const match = useMatch(location.pathname);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const reportComponents = useMemo<ReportComponents>(
|
||||
() => ({
|
||||
@@ -596,19 +479,9 @@ export function Budget() {
|
||||
}}
|
||||
>
|
||||
<BudgetInner
|
||||
startMonth={startMonth}
|
||||
collapsedPrefs={collapsedPrefs}
|
||||
summaryCollapsed={summaryCollapsed}
|
||||
budgetType={budgetType}
|
||||
maxMonths={maxMonths}
|
||||
categoryGroups={categoryGroups}
|
||||
{...actions}
|
||||
reportComponents={reportComponents}
|
||||
rolloverComponents={rolloverComponents}
|
||||
spreadsheet={spreadsheet}
|
||||
titlebar={titlebar}
|
||||
navigate={navigate}
|
||||
match={match}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -6,14 +6,12 @@ import * as monthUtils from 'loot-core/src/shared/months';
|
||||
const Context = createContext(null);
|
||||
|
||||
type RolloverContextProps = {
|
||||
categoryGroups: unknown[];
|
||||
summaryCollapsed: boolean;
|
||||
onBudgetAction: (idx: number, action: string, arg?: unknown) => void;
|
||||
onToggleSummaryCollapse: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
export function RolloverContext({
|
||||
categoryGroups,
|
||||
summaryCollapsed,
|
||||
onBudgetAction,
|
||||
onToggleSummaryCollapse,
|
||||
@@ -25,7 +23,6 @@ export function RolloverContext({
|
||||
<Context.Provider
|
||||
value={{
|
||||
currentMonth,
|
||||
categoryGroups,
|
||||
summaryCollapsed,
|
||||
onBudgetAction,
|
||||
onToggleSummaryCollapse,
|
||||
|
||||
@@ -191,7 +191,7 @@ export function BudgetSummary({
|
||||
alignItems: 'center',
|
||||
padding: '10px 20px',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: theme.tableHeaderBackground,
|
||||
backgroundColor: theme.tableBackground,
|
||||
borderTop: '1px solid ' + theme.tableBorder,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -204,6 +204,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
type ButtonWithLoadingProps = ButtonProps & {
|
||||
loading?: boolean;
|
||||
};
|
||||
@@ -246,3 +248,5 @@ export const ButtonWithLoading = forwardRef<
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
ButtonWithLoading.displayName = 'ButtonWithLoading';
|
||||
|
||||
@@ -35,3 +35,5 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
@@ -28,3 +28,5 @@ export const ExternalLink = forwardRef<HTMLAnchorElement, ExternalLinkProps>(
|
||||
</a>
|
||||
),
|
||||
);
|
||||
|
||||
ExternalLink.displayName = 'ExternalLink';
|
||||
|
||||
@@ -26,6 +26,7 @@ export type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
style?: CSSProperties;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
onEnter?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEscape?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onUpdate?: (newValue: string) => void;
|
||||
focused?: boolean;
|
||||
};
|
||||
@@ -34,6 +35,7 @@ export function Input({
|
||||
style,
|
||||
inputRef,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onUpdate,
|
||||
focused,
|
||||
...nativeProps
|
||||
@@ -65,6 +67,10 @@ export function Input({
|
||||
onEnter(e);
|
||||
}
|
||||
|
||||
if (e.key === 'Escape' && onEscape) {
|
||||
onEscape(e);
|
||||
}
|
||||
|
||||
nativeProps.onKeyDown?.(e);
|
||||
}}
|
||||
onChange={e => {
|
||||
|
||||
@@ -38,11 +38,11 @@ type MenuItem = {
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
type MenuProps<T extends MenuItem = MenuItem> = {
|
||||
export type MenuProps<T extends MenuItem = MenuItem> = {
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
items: Array<T | typeof Menu.line>;
|
||||
onMenuSelect: (itemName: T['name']) => void;
|
||||
onMenuSelect?: (itemName: T['name']) => void;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
|
||||
@@ -95,3 +95,5 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Stack.displayName = 'Stack';
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
import { type RuleConditionEntity } from 'loot-core/src/types/models';
|
||||
|
||||
import { View } from '../common/View';
|
||||
|
||||
import { CondOpMenu } from './CondOpMenu';
|
||||
import { FilterExpression } from './FilterExpression';
|
||||
|
||||
type AppliedFiltersProps = {
|
||||
filters: RuleConditionEntity[];
|
||||
onUpdate: (
|
||||
filter: RuleConditionEntity,
|
||||
newFilter: RuleConditionEntity,
|
||||
) => RuleConditionEntity;
|
||||
onDelete: (filter: RuleConditionEntity) => void;
|
||||
conditionsOp: string;
|
||||
onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
|
||||
};
|
||||
|
||||
export function AppliedFilters({
|
||||
filters,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
conditionsOp,
|
||||
onCondOpChange,
|
||||
}: AppliedFiltersProps) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<CondOpMenu
|
||||
conditionsOp={conditionsOp}
|
||||
onCondOpChange={onCondOpChange}
|
||||
filters={filters}
|
||||
/>
|
||||
{filters.map((filter: RuleConditionEntity, i: number) => (
|
||||
<FilterExpression
|
||||
key={i}
|
||||
customName={filter.customName}
|
||||
field={filter.field}
|
||||
op={filter.op}
|
||||
value={filter.value}
|
||||
options={filter.options}
|
||||
onChange={newFilter => onUpdate(filter, newFilter)}
|
||||
onDelete={() => onDelete(filter)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
// @ts-strict-ignore
|
||||
import React from 'react';
|
||||
|
||||
import { SvgFilter } from '../../icons/v1';
|
||||
import { Button } from '../common/Button';
|
||||
|
||||
type CompactFiltersButtonProps = {
|
||||
onClick: (newValue) => void;
|
||||
};
|
||||
|
||||
export function CompactFiltersButton({ onClick }: CompactFiltersButtonProps) {
|
||||
export function CompactFiltersButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<Button type="bare" onClick={onClick}>
|
||||
<SvgFilter width={15} height={15} />
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models';
|
||||
|
||||
import { theme } from '../../style';
|
||||
import { Text } from '../common/Text';
|
||||
import { View } from '../common/View';
|
||||
import { FieldSelect } from '../modals/EditRule';
|
||||
|
||||
export function CondOpMenu({
|
||||
conditionsOp,
|
||||
onCondOpChange,
|
||||
filters,
|
||||
}: {
|
||||
conditionsOp: string;
|
||||
onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
|
||||
filters: RuleConditionEntity[];
|
||||
}) {
|
||||
return filters.length > 1 ? (
|
||||
<Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
|
||||
<FieldSelect
|
||||
style={{ display: 'inline-flex' }}
|
||||
fields={[
|
||||
['and', 'all'],
|
||||
['or', 'any'],
|
||||
]}
|
||||
value={conditionsOp}
|
||||
onChange={(name: string, value: string) =>
|
||||
onCondOpChange(value, filters)
|
||||
}
|
||||
/>
|
||||
of:
|
||||
</Text>
|
||||
) : (
|
||||
<View />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { mapField, friendlyOp } from 'loot-core/src/shared/rules';
|
||||
import { integerToCurrency } from 'loot-core/src/shared/util';
|
||||
import {
|
||||
type RuleConditionOp,
|
||||
type RuleConditionEntity,
|
||||
} from 'loot-core/src/types/models';
|
||||
|
||||
import { SvgDelete } from '../../icons/v0';
|
||||
import { type CSSProperties, theme } from '../../style';
|
||||
import { Button } from '../common/Button';
|
||||
import { Text } from '../common/Text';
|
||||
import { View } from '../common/View';
|
||||
import { Value } from '../rules/Value';
|
||||
|
||||
import { FilterEditor } from './FiltersMenu';
|
||||
import { subfieldFromFilter } from './subfieldFromFilter';
|
||||
|
||||
type FilterExpressionProps = {
|
||||
field: string | undefined;
|
||||
customName: string | undefined;
|
||||
op: RuleConditionOp | undefined;
|
||||
value: string | string[] | number | boolean | undefined;
|
||||
options: RuleConditionEntity['options'];
|
||||
style?: CSSProperties;
|
||||
onChange: (cond: RuleConditionEntity) => RuleConditionEntity;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export function FilterExpression({
|
||||
field: originalField,
|
||||
customName,
|
||||
op,
|
||||
value,
|
||||
options,
|
||||
style,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: FilterExpressionProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
const field = subfieldFromFilter({ field: originalField, value });
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.pillBackground,
|
||||
borderRadius: 4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginRight: 10,
|
||||
marginTop: 10,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="bare"
|
||||
disabled={customName != null}
|
||||
onClick={() => setEditing(true)}
|
||||
style={{ marginRight: -7 }}
|
||||
>
|
||||
<div style={{ paddingBlock: 1, paddingLeft: 5, paddingRight: 2 }}>
|
||||
{customName ? (
|
||||
<Text style={{ color: theme.pageTextPositive }}>{customName}</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={{ color: theme.pageTextPositive }}>
|
||||
{mapField(field, options)}
|
||||
</Text>{' '}
|
||||
<Text>{friendlyOp(op, null)}</Text>{' '}
|
||||
<Value
|
||||
value={value}
|
||||
field={field}
|
||||
inline={true}
|
||||
valueIsRaw={op === 'contains' || op === 'doesNotContain'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
<Button type="bare" onClick={onDelete} aria-label="Delete filter">
|
||||
<SvgDelete
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: 5,
|
||||
marginLeft: 3,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
{editing && (
|
||||
<FilterEditor
|
||||
field={originalField}
|
||||
op={op}
|
||||
value={
|
||||
field === 'amount' && typeof value === 'number'
|
||||
? integerToCurrency(value)
|
||||
: value
|
||||
}
|
||||
options={options}
|
||||
onSave={onChange}
|
||||
onClose={() => setEditing(false)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||