Compare commits
189 Commits
scrollToLo
...
v24.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44375e72ad | ||
|
|
6454c10e63 | ||
|
|
2a9546ced1 | ||
|
|
8926ff69b1 | ||
|
|
340169bfb6 | ||
|
|
3a905d3f9a | ||
|
|
7738ea0c00 | ||
|
|
8e077e0282 | ||
|
|
ae608f0cb8 | ||
|
|
f1c0d0b8a6 | ||
|
|
d9adb750d4 | ||
|
|
1750cd9081 | ||
|
|
7769d0303e | ||
|
|
9108b63355 | ||
|
|
1b70e59bde | ||
|
|
b48d256ec4 | ||
|
|
9c0e6a307b | ||
|
|
3e5ce72e27 | ||
|
|
b347f03fbb | ||
|
|
f3660c166f | ||
|
|
aaf96bbc2c | ||
|
|
6d84b0e371 | ||
|
|
db4b504e53 | ||
|
|
d6afc85a8c | ||
|
|
ee21155d1a | ||
|
|
65a7c58441 | ||
|
|
51ec600de2 | ||
|
|
af5fd5b3ef | ||
|
|
eccdc52342 | ||
|
|
4c192d7e1e | ||
|
|
f715ceafc9 | ||
|
|
af73dcd722 | ||
|
|
5e3485a8e2 | ||
|
|
1458dbc307 | ||
|
|
9ac77af077 | ||
|
|
3e07d18acd | ||
|
|
fa6cc26416 | ||
|
|
a1ca871b24 | ||
|
|
d9066a49c4 | ||
|
|
63ad6dadf2 | ||
|
|
89b096aa65 | ||
|
|
ee0156d35d | ||
|
|
9c17d55e0d | ||
|
|
411a6791b2 | ||
|
|
6f3af7b609 | ||
|
|
43ff1c033e | ||
|
|
09c44d351d | ||
|
|
a22160579d | ||
|
|
81df2ce7fd | ||
|
|
119d0b339d | ||
|
|
d1362c3d74 | ||
|
|
8142dd1ec9 | ||
|
|
2afd6967b4 | ||
|
|
fe922ec22e | ||
|
|
30a70f5627 | ||
|
|
65c5f2c559 | ||
|
|
1abca7619d | ||
|
|
6a85f84565 | ||
|
|
65329398fd | ||
|
|
a2e434a1fb | ||
|
|
d2bbe6a98e | ||
|
|
2c1967d788 | ||
|
|
798aee78c3 | ||
|
|
2807c98c2c | ||
|
|
5e9b976676 | ||
|
|
44ce976ffa | ||
|
|
5ba80fcbdc | ||
|
|
7b77f60458 | ||
|
|
81f59ff776 | ||
|
|
63d9547e7c | ||
|
|
d18fd36ae1 | ||
|
|
2b1ba88983 | ||
|
|
8be867f884 | ||
|
|
cafe480ba4 | ||
|
|
6472c70960 | ||
|
|
56c5a533e7 | ||
|
|
7e3ff1ad03 | ||
|
|
e0d7233b40 | ||
|
|
1b4c4319e1 | ||
|
|
14f29941b0 | ||
|
|
4389329bfa | ||
|
|
3a38c32b4c | ||
|
|
c3c6acd37c | ||
|
|
8de0f6a72a | ||
|
|
2799dbee3e | ||
|
|
58eeee825e | ||
|
|
6653dca776 | ||
|
|
77ba15f54c | ||
|
|
653a0ab104 | ||
|
|
2c26fa51a3 | ||
|
|
dff9911a15 | ||
|
|
3d5818f017 | ||
|
|
efd294dcef | ||
|
|
0eb62a09bc | ||
|
|
73d52fa0d0 | ||
|
|
5b0cc63f73 | ||
|
|
26a591f07f | ||
|
|
fe8851c797 | ||
|
|
511f677ae4 | ||
|
|
1cef0d11ee | ||
|
|
536cabb75b | ||
|
|
cceda03905 | ||
|
|
982f555a21 | ||
|
|
fe70ecb635 | ||
|
|
5c0bee6031 | ||
|
|
4439bb6abe | ||
|
|
b432204b4b | ||
|
|
9a85a72089 | ||
|
|
a970a78932 | ||
|
|
ed65805d53 | ||
|
|
88ae7e9375 | ||
|
|
0135a4d1b9 | ||
|
|
4af2c4f214 | ||
|
|
89a8f102dc | ||
|
|
d032fce7ea | ||
|
|
2fdc7fef32 | ||
|
|
1e41d695c5 | ||
|
|
12f91f7d86 | ||
|
|
f75d0f8099 | ||
|
|
07bbe00059 | ||
|
|
be0d363576 | ||
|
|
c2e648c9d5 | ||
|
|
33049a77e7 | ||
|
|
89241623f3 | ||
|
|
8434e8f5ce | ||
|
|
9b99debacc | ||
|
|
a23ec33591 | ||
|
|
aaea04fc00 | ||
|
|
b4f0087eef | ||
|
|
f81c452ba5 | ||
|
|
7072674111 | ||
|
|
16e887c917 | ||
|
|
572033debe | ||
|
|
b85f9102ce | ||
|
|
942aebedd0 | ||
|
|
32d830440a | ||
|
|
4575616961 | ||
|
|
4a0e2ea306 | ||
|
|
14ec9a9089 | ||
|
|
e91b4070aa | ||
|
|
6dd34b0c63 | ||
|
|
ab4639f48f | ||
|
|
aa3cbd881b | ||
|
|
8681c9c3e6 | ||
|
|
9ec9aef632 | ||
|
|
3be7dd753d | ||
|
|
259e84cea5 | ||
|
|
f9014f0e19 | ||
|
|
e59f5c9af8 | ||
|
|
771c01c8b4 | ||
|
|
9f72b43826 | ||
|
|
ec3475d834 | ||
|
|
5ea9c587a8 | ||
|
|
1e38055376 | ||
|
|
0ee9126820 | ||
|
|
9e455e4c1e | ||
|
|
d77b54f27b | ||
|
|
ff36d1efbe | ||
|
|
cbbbaf65cf | ||
|
|
f129b07dc9 | ||
|
|
f1caf21deb | ||
|
|
a28ea6be8f | ||
|
|
f36c5e002b | ||
|
|
803289ee1f | ||
|
|
76cdad4fe6 | ||
|
|
d03b30bc00 | ||
|
|
710d9ab8ac | ||
|
|
d008944022 | ||
|
|
f18bce6094 | ||
|
|
31eb00a155 | ||
|
|
a67c969189 | ||
|
|
58e6c6f23a | ||
|
|
f046d75b75 | ||
|
|
30bcfedc86 | ||
|
|
866b4d6cd4 | ||
|
|
a42938fa64 | ||
|
|
e02b0f9bc7 | ||
|
|
049a41f366 | ||
|
|
7f30680fb3 | ||
|
|
2d4256b239 | ||
|
|
247e3e8d93 | ||
|
|
5951b92668 | ||
|
|
a9ee670eb4 | ||
|
|
3990aaf38f | ||
|
|
48f5880f1d | ||
|
|
3332f58376 | ||
|
|
46ea8fbf72 | ||
|
|
6a21f8e3de | ||
|
|
f02ca4e3d2 |
270
.eslintrc.js
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable rulesdir/typography */
|
||||
const path = require('path');
|
||||
|
||||
const rulesDirPlugin = require('eslint-plugin-rulesdir');
|
||||
@@ -34,9 +33,23 @@ const restrictedImportColors = [
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
plugins: ['prettier', 'import', 'rulesdir', '@typescript-eslint'],
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
jest: true,
|
||||
node: true,
|
||||
},
|
||||
plugins: [
|
||||
'prettier',
|
||||
'import',
|
||||
'rulesdir',
|
||||
'@typescript-eslint',
|
||||
'jsx-a11y',
|
||||
'react-hooks',
|
||||
],
|
||||
extends: [
|
||||
'react-app',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:prettier/recommended',
|
||||
@@ -51,6 +64,184 @@ module.exports = {
|
||||
vi: true,
|
||||
},
|
||||
rules: {
|
||||
// http://eslint.org/docs/rules/
|
||||
'array-callback-return': 'warn',
|
||||
'default-case': ['warn', { commentPattern: '^no default$' }],
|
||||
'dot-location': ['warn', 'property'],
|
||||
eqeqeq: ['warn', 'smart'],
|
||||
'new-parens': 'warn',
|
||||
'no-array-constructor': 'warn',
|
||||
'no-caller': 'warn',
|
||||
'no-cond-assign': ['warn', 'except-parens'],
|
||||
'no-const-assign': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'no-delete-var': 'warn',
|
||||
'no-dupe-args': 'warn',
|
||||
'no-dupe-class-members': 'warn',
|
||||
'no-dupe-keys': 'warn',
|
||||
'no-duplicate-case': 'warn',
|
||||
'no-empty-character-class': 'warn',
|
||||
'no-empty-pattern': 'warn',
|
||||
'no-eval': 'warn',
|
||||
'no-ex-assign': 'warn',
|
||||
'no-extend-native': 'warn',
|
||||
'no-extra-bind': 'warn',
|
||||
'no-extra-label': 'warn',
|
||||
'no-fallthrough': 'warn',
|
||||
'no-func-assign': 'warn',
|
||||
'no-implied-eval': 'warn',
|
||||
'no-invalid-regexp': 'warn',
|
||||
'no-iterator': 'warn',
|
||||
'no-label-var': 'warn',
|
||||
'no-labels': ['warn', { allowLoop: true, allowSwitch: false }],
|
||||
'no-lone-blocks': 'warn',
|
||||
'no-mixed-operators': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
['&', '|', '^', '~', '<<', '>>', '>>>'],
|
||||
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
|
||||
['&&', '||'],
|
||||
['in', 'instanceof'],
|
||||
],
|
||||
allowSamePrecedence: false,
|
||||
},
|
||||
],
|
||||
'no-multi-str': 'warn',
|
||||
'no-global-assign': 'warn',
|
||||
'no-unsafe-negation': 'warn',
|
||||
'no-new-func': 'warn',
|
||||
'no-new-object': 'warn',
|
||||
'no-new-symbol': 'warn',
|
||||
'no-new-wrappers': 'warn',
|
||||
'no-obj-calls': 'warn',
|
||||
'no-octal': 'warn',
|
||||
'no-octal-escape': 'warn',
|
||||
'no-redeclare': 'warn',
|
||||
'no-regex-spaces': 'warn',
|
||||
'no-script-url': 'warn',
|
||||
'no-self-assign': 'warn',
|
||||
'no-self-compare': 'warn',
|
||||
'no-sequences': 'warn',
|
||||
'no-shadow-restricted-names': 'warn',
|
||||
'no-sparse-arrays': 'warn',
|
||||
'no-template-curly-in-string': 'warn',
|
||||
'no-this-before-super': 'warn',
|
||||
'no-throw-literal': 'warn',
|
||||
'no-undef': 'error',
|
||||
'no-unreachable': 'warn',
|
||||
'no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
},
|
||||
],
|
||||
'no-unused-labels': 'warn',
|
||||
'no-use-before-define': [
|
||||
'warn',
|
||||
{
|
||||
functions: false,
|
||||
classes: false,
|
||||
variables: false,
|
||||
},
|
||||
],
|
||||
'no-useless-computed-key': 'warn',
|
||||
'no-useless-concat': 'warn',
|
||||
'no-useless-constructor': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'no-useless-rename': [
|
||||
'warn',
|
||||
{
|
||||
ignoreDestructuring: false,
|
||||
ignoreImport: false,
|
||||
ignoreExport: false,
|
||||
},
|
||||
],
|
||||
'no-with': 'warn',
|
||||
'no-whitespace-before-property': 'warn',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'require-yield': 'warn',
|
||||
'rest-spread-spacing': ['warn', 'never'],
|
||||
strict: ['warn', 'never'],
|
||||
'unicode-bom': ['warn', 'never'],
|
||||
'use-isnan': 'warn',
|
||||
'valid-typeof': 'warn',
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: 'require',
|
||||
property: 'ensure',
|
||||
message:
|
||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
||||
},
|
||||
{
|
||||
object: 'System',
|
||||
property: 'import',
|
||||
message:
|
||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
||||
},
|
||||
],
|
||||
'getter-return': 'warn',
|
||||
|
||||
// https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules
|
||||
'import/first': 'error',
|
||||
'import/no-amd': 'error',
|
||||
'import/no-anonymous-default-export': 'warn',
|
||||
'import/no-webpack-loader-syntax': 'error',
|
||||
|
||||
// https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
|
||||
'react/forbid-foreign-prop-types': ['warn', { allowInPropTypes: true }],
|
||||
'react/jsx-no-comment-textnodes': 'warn',
|
||||
'react/jsx-no-duplicate-props': 'warn',
|
||||
'react/jsx-no-target-blank': 'warn',
|
||||
'react/jsx-no-undef': 'error',
|
||||
'react/jsx-pascal-case': [
|
||||
'warn',
|
||||
{
|
||||
allowAllCaps: true,
|
||||
ignore: [],
|
||||
},
|
||||
],
|
||||
'react/no-danger-with-children': 'warn',
|
||||
// Disabled because of undesirable warnings
|
||||
// See https://github.com/facebook/create-react-app/issues/5204 for
|
||||
// blockers until its re-enabled
|
||||
// 'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
|
||||
// https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
|
||||
'jsx-a11y/alt-text': 'warn',
|
||||
'jsx-a11y/anchor-has-content': 'warn',
|
||||
'jsx-a11y/anchor-is-valid': [
|
||||
'warn',
|
||||
{
|
||||
aspects: ['noHref', 'invalidHref'],
|
||||
},
|
||||
],
|
||||
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
|
||||
'jsx-a11y/aria-props': 'warn',
|
||||
'jsx-a11y/aria-proptypes': 'warn',
|
||||
'jsx-a11y/aria-role': ['warn', { ignoreNonDOM: true }],
|
||||
'jsx-a11y/aria-unsupported-elements': 'warn',
|
||||
'jsx-a11y/heading-has-content': 'warn',
|
||||
'jsx-a11y/iframe-has-title': 'warn',
|
||||
'jsx-a11y/img-redundant-alt': 'warn',
|
||||
'jsx-a11y/no-access-key': 'warn',
|
||||
'jsx-a11y/no-distracting-elements': 'warn',
|
||||
'jsx-a11y/no-redundant-roles': 'warn',
|
||||
'jsx-a11y/role-has-required-aria-props': 'warn',
|
||||
'jsx-a11y/role-supports-aria-props': 'warn',
|
||||
'jsx-a11y/scope': 'warn',
|
||||
|
||||
// https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
|
||||
'prettier/prettier': 'warn',
|
||||
|
||||
// Note: base rule explicitly disabled in favor of the TS one
|
||||
@@ -60,6 +251,7 @@ module.exports = {
|
||||
{
|
||||
varsIgnorePattern: '^(_|React)',
|
||||
ignoreRestSiblings: true,
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -166,9 +358,63 @@ module.exports = {
|
||||
'prefer-const': 'warn',
|
||||
'prefer-spread': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'import/no-default-export': 'warn',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts?(x)'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
|
||||
// typescript-eslint specific options
|
||||
warnOnUnsupportedTypeScriptVersion: true,
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
// If adding a typescript-eslint version of an existing ESLint rule,
|
||||
// make sure to disable the ESLint rule here.
|
||||
rules: {
|
||||
// TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
|
||||
'default-case': 'off',
|
||||
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
|
||||
'no-dupe-class-members': 'off',
|
||||
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
|
||||
'no-undef': 'off',
|
||||
|
||||
// Add TypeScript specific rules (and turn off ESLint equivalents)
|
||||
'@typescript-eslint/consistent-type-assertions': 'warn',
|
||||
'no-array-constructor': 'off',
|
||||
'@typescript-eslint/no-array-constructor': 'warn',
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'warn',
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
'warn',
|
||||
{
|
||||
functions: false,
|
||||
classes: false,
|
||||
variables: false,
|
||||
typedefs: false,
|
||||
},
|
||||
],
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
},
|
||||
],
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['.eslintrc.js', './**/.eslintrc.js'],
|
||||
parserOptions: { project: null },
|
||||
@@ -189,7 +435,7 @@ module.exports = {
|
||||
'warn',
|
||||
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
||||
],
|
||||
'@typescript-eslint/ban-types': [
|
||||
'@typescript-eslint/no-restricted-types': [
|
||||
'warn',
|
||||
{
|
||||
types: {
|
||||
@@ -197,7 +443,6 @@ module.exports = {
|
||||
FunctionComponent: { message: ruleFCMsg },
|
||||
FC: { message: ruleFCMsg },
|
||||
},
|
||||
extendDefaults: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -332,8 +577,23 @@ module.exports = {
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'.eslintrc.js',
|
||||
'*.test.js',
|
||||
'*.test.ts',
|
||||
'*.test.jsx',
|
||||
'*.test.tsx',
|
||||
],
|
||||
rules: {
|
||||
'rulesdir/typography': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
|
||||
19
.github/workflows/electron-master.yml
vendored
@@ -48,13 +48,17 @@ jobs:
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
- name: Build Electron for Mac
|
||||
if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
env:
|
||||
# CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
# CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
- name: Build Electron
|
||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -62,13 +66,22 @@ jobs:
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
|
||||
8
.github/workflows/electron-pr.yml
vendored
@@ -52,5 +52,13 @@ jobs:
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
|
||||
12
.github/workflows/stale.yml
vendored
@@ -7,10 +7,20 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
days-before-issue-stale: -1
|
||||
stale-wip:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
|
||||
days-before-stale: 7
|
||||
any-of-labels: ':construction: WIP'
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: -1
|
||||
|
||||
39
.github/workflows/trafico.yml
vendored
@@ -1,39 +0,0 @@
|
||||
##########################################################################################
|
||||
# WARNING! This workflow uses the 'pull_request_target' event. That mans that it will #
|
||||
# always run in the context of the main actualbudget/actual repo, even if the PR is from #
|
||||
# a fork. This is necessary to get access to a GitHub token that can modify the PR. #
|
||||
# Be VERY CAREFUL about adding things to this workflow, since forks can inject #
|
||||
# arbitrary code into their branch, and can pollute the artifacts we download. Arbitrary #
|
||||
# code execution in this workflow could lead to a compromise of the main repo. #
|
||||
##########################################################################################
|
||||
# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests #
|
||||
##########################################################################################
|
||||
|
||||
name: Trafico Reviews
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- closed
|
||||
- reopened
|
||||
- synchronize
|
||||
- edited
|
||||
- review_requested
|
||||
- review_request_removed
|
||||
pull_request_review:
|
||||
types: [submitted, edited, dismissed]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
manage-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actualbudget/trafico@main
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
27
.github/workflows/wip.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Add WIP
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add_wip_prefix:
|
||||
if: |
|
||||
join(github.event.pull_request.requested_reviewers) == ''
|
||||
&& !contains(github.event.pull_request.title, 'WIP')
|
||||
&& !contains(github.event.pull_request.labels.*.name, 'WIP')
|
||||
&& github.event.pull_request.draft != true
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Add WIP
|
||||
env:
|
||||
TITLE: ${{ github.event.pull_request.title }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo ${{ secrets.GITHUB_TOKEN }} | gh auth login --with-token
|
||||
gh pr edit ${{ github.event.pull_request.number }} -t "[WIP] ${TITLE}"
|
||||
1
.gitignore
vendored
@@ -21,6 +21,7 @@ packages/api/dist
|
||||
packages/api/@types
|
||||
packages/crdt/dist
|
||||
packages/desktop-electron/client-build
|
||||
packages/desktop-electron/build
|
||||
packages/desktop-electron/.electron-symbols
|
||||
packages/desktop-electron/dist
|
||||
packages/desktop-electron/loot-core
|
||||
|
||||
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
yarn lint-staged
|
||||
893
.yarn/releases/yarn-4.0.2.cjs
vendored
894
.yarn/releases/yarn-4.3.1.cjs
vendored
Executable file
@@ -4,4 +4,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.3.1.cjs
|
||||
|
||||
38
README.md
@@ -14,22 +14,40 @@ Want to say thanks? Click the ⭐ at the top of the page.
|
||||
|
||||
- Actual [discord](https://discord.gg/pRYNYr4W5A) community.
|
||||
- Actual [Community Documentation](https://actualbudget.org/docs)
|
||||
- [Frequently asked questions](https://actualbudget.org/docs/faq)
|
||||
|
||||
## Installation
|
||||
|
||||
If you are only interested in running the latest version and not contributing to the source code, you don't need to clone this repo. You can get the latest version through npm.
|
||||
There are four ways to deploy Actual:
|
||||
|
||||
### The easy way: using a server (recommended)
|
||||
1. One-click deployment [via PikaPods](https://www.pikapods.com/pods?run=actual) (~1.40 $/month) - recommended for non-technical users
|
||||
1. Managed hosting [via Fly.io](https://actualbudget.org/docs/install/fly) (~1.50 $/month)
|
||||
1. Self-hosted by using [a Docker image](https://actualbudget.org/docs/install/docker)
|
||||
1. Local-only apps - [downloadable Windows, Mac and Linux apps](https://actualbudget.org/download/) you can run on your device
|
||||
|
||||
The easiest way to get Actual running is to use the [actual-server](https://github.com/actualbudget/actual-server) project. That is the server for syncing changes across devices, and it comes with the latest version of Actual. The server will provide both the web project and a server for syncing.
|
||||
Learn more in the [installation instructions docs](https://actualbudget.org/docs/install/).
|
||||
|
||||
You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.org/docs/install/local)
|
||||
## Ready to Start Budgeting?
|
||||
|
||||
Read about [Envelope budgeting](https://actualbudget.org/docs/getting-started/envelope-budgeting) to know more about the idea behind Actual Budget.
|
||||
|
||||
### Are you new to budgeting or want to start fresh?
|
||||
|
||||
Check out the community's [Starting Fresh](https://actualbudget.org/docs/getting-started/starting-fresh) guide so you can quickly get up and running!
|
||||
|
||||
### Are you migrating from other budgeting apps?
|
||||
|
||||
Check out the community's [Migration](https://actualbudget.org/docs/migration/) guide to start jumping on the Actual Budget train!
|
||||
|
||||
## Documentation
|
||||
|
||||
We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.org/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
|
||||
|
||||
## Code structure
|
||||
## Contributing
|
||||
|
||||
Actual is a community driven product. Learn more about [contributing to Actual](https://actualbudget.org/docs/contributing/).
|
||||
|
||||
### Code structure
|
||||
|
||||
The Actual app is split up into a few packages:
|
||||
|
||||
@@ -39,13 +57,21 @@ The Actual app is split up into a few packages:
|
||||
|
||||
More information on the project structure is available in our [community documentation](https://actualbudget.org/docs/contributing/project-details).
|
||||
|
||||
## Feature Requests
|
||||
### Feature Requests
|
||||
|
||||
Current feature requests can be seen [here](https://github.com/actualbudget/actual/issues?q=is%3Aissue+label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc).
|
||||
Vote for your favorite requests by reacting :+1: to the top comment of the request.
|
||||
|
||||
To add new feature requests, open a new Issue of the "Feature Request" type.
|
||||
|
||||
### Translation
|
||||
|
||||
Make Actual Budget accessible to more people by helping with the [Internationalization](https://actualbudget.org/docs/contributing/i18n/) of Actual. We are using a crowd sourcing tool to manage the translations, see our [Weblate Project](https://hosted.weblate.org/projects/actualbudget/). Weblate proudly supports open-source software projects through their [Libre plan](https://weblate.org/en/hosting/#libre).
|
||||
|
||||
## Repo Activity
|
||||
|
||||

|
||||
|
||||
## Sponsors
|
||||
|
||||
Thanks to our wonderful sponsors who make Actual budget possible!
|
||||
|
||||
@@ -34,8 +34,6 @@ if [ "$OSTYPE" == "msys" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
yarn rebuild-electron
|
||||
|
||||
yarn workspace loot-core build:node
|
||||
|
||||
yarn workspace @actual-app/web build --mode=desktop
|
||||
@@ -50,10 +48,10 @@ yarn workspace desktop-electron update-client
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build --publish never --arm64 --x64
|
||||
yarn build
|
||||
|
||||
echo "\nCreated release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build --publish never --x64
|
||||
SKIP_NOTARIZATION=true yarn build
|
||||
fi
|
||||
)
|
||||
|
||||
33
package.json
@@ -30,6 +30,7 @@
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"test": "yarn workspaces foreach --all --parallel --verbose run test",
|
||||
"test:debug": "yarn workspaces foreach --all --verbose run test",
|
||||
"e2e": "yarn workspaces foreach --all --parallel --verbose run e2e",
|
||||
@@ -40,24 +41,31 @@
|
||||
"lint": "eslint . --max-warnings 0 --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:verbose": "DEBUG=eslint:cli-engine eslint . --max-warnings 0",
|
||||
"typecheck": "yarn tsc && tsc-strict",
|
||||
"jq": "./node_modules/node-jq/bin/jq"
|
||||
"jq": "./node_modules/node-jq/bin/jq",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.1.0",
|
||||
"@typescript-eslint/parser": "^8.1.0",
|
||||
"confusing-browser-globals": "^1.0.11",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"eslint-import-resolver-typescript": "3.5.5",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.9",
|
||||
"node-jq": "^4.0.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.2.4",
|
||||
"prettier": "3.3.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript": "^5.0.2",
|
||||
"typescript-strict-plugin": "^2.2.2-beta.2"
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"rollup": "4.9.4"
|
||||
@@ -65,7 +73,10 @@
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,json}": "prettier --write"
|
||||
},
|
||||
"packageManager": "yarn@4.3.1",
|
||||
"browserslist": [
|
||||
"electron 24.0",
|
||||
"defaults"
|
||||
|
||||
@@ -58,6 +58,19 @@ describe('API CRUD operations', () => {
|
||||
await api.loadBudget(budgetName);
|
||||
});
|
||||
|
||||
// api: getBudgets
|
||||
test('getBudgets', async () => {
|
||||
const budgets = await api.getBudgets();
|
||||
expect(budgets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'test-budget',
|
||||
name: 'Default Test Db',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
// apis: getCategoryGroups, createCategoryGroup, updateCategoryGroup, deleteCategoryGroup
|
||||
test('CategoryGroups: successfully update category groups', async () => {
|
||||
const month = '2023-10';
|
||||
@@ -68,28 +81,22 @@ describe('API CRUD operations', () => {
|
||||
expect(groups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
hidden: 0,
|
||||
hidden: false,
|
||||
id: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
is_income: 0,
|
||||
is_income: false,
|
||||
name: 'Usual Expenses',
|
||||
sort_order: 16384,
|
||||
tombstone: 0,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
hidden: 0,
|
||||
hidden: false,
|
||||
id: 'a137772f-cf2f-4089-9432-822d2ddc1466',
|
||||
is_income: 0,
|
||||
is_income: false,
|
||||
name: 'Investments and Savings',
|
||||
sort_order: 32768,
|
||||
tombstone: 0,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
hidden: 0,
|
||||
hidden: false,
|
||||
id: '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00',
|
||||
is_income: 1,
|
||||
is_income: true,
|
||||
name: 'Income',
|
||||
sort_order: 32768,
|
||||
tombstone: 0,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
@@ -251,7 +258,7 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
});
|
||||
|
||||
//apis: createAccount, getAccounts, updateAccount, closeAccount, deleteAccount, reopenAccount
|
||||
//apis: createAccount, getAccounts, updateAccount, closeAccount, deleteAccount, reopenAccount, getAccountBalance
|
||||
test('Accounts: successfully complete account operators', async () => {
|
||||
const accountId1 = await api.createAccount(
|
||||
{ name: 'test-account1', offbudget: true },
|
||||
@@ -272,6 +279,9 @@ describe('API CRUD operations', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
expect(await api.getAccountBalance(accountId1)).toEqual(1000);
|
||||
expect(await api.getAccountBalance(accountId2)).toEqual(0);
|
||||
|
||||
await api.updateAccount(accountId1, { offbudget: false });
|
||||
await api.closeAccount(accountId1, accountId2, null);
|
||||
await api.deleteAccount(accountId2);
|
||||
@@ -547,10 +557,10 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
|
||||
// delete rules
|
||||
await api.deleteRule(rules[1]);
|
||||
await api.deleteRule(rules[1].id);
|
||||
expect(await api.getRules()).toHaveLength(1);
|
||||
|
||||
await api.deleteRule(rules[0]);
|
||||
await api.deleteRule(rules[0].id);
|
||||
expect(await api.getRules()).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -569,6 +579,11 @@ describe('API CRUD operations', () => {
|
||||
});
|
||||
expect(addResult).toBe('ok');
|
||||
|
||||
expect(await api.getAccountBalance(accountId)).toEqual(200);
|
||||
expect(
|
||||
await api.getAccountBalance(accountId, new Date(2023, 10, 2)),
|
||||
).toEqual(0);
|
||||
|
||||
// confirm added transactions exist
|
||||
let transactions = await api.getTransactions(
|
||||
accountId,
|
||||
|
||||
@@ -31,6 +31,10 @@ export async function downloadBudget(syncId, { password }: { password? } = {}) {
|
||||
return send('api/download-budget', { syncId, password });
|
||||
}
|
||||
|
||||
export async function getBudgets() {
|
||||
return send('api/get-budgets');
|
||||
}
|
||||
|
||||
export async function sync() {
|
||||
return send('api/sync');
|
||||
}
|
||||
@@ -125,6 +129,10 @@ export function deleteAccount(id) {
|
||||
return send('api/account-delete', { id });
|
||||
}
|
||||
|
||||
export function getAccountBalance(id, cutoff?) {
|
||||
return send('api/account-balance', { id, cutoff });
|
||||
}
|
||||
|
||||
export function getCategoryGroups() {
|
||||
return send('api/category-groups-get');
|
||||
}
|
||||
@@ -157,6 +165,10 @@ export function deleteCategory(id, transferCategoryId?) {
|
||||
return send('api/category-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getCommonPayees() {
|
||||
return send('api/common-payees-get');
|
||||
}
|
||||
|
||||
export function getPayees() {
|
||||
return send('api/payees-get');
|
||||
}
|
||||
@@ -173,6 +185,10 @@ export function deletePayee(id) {
|
||||
return send('api/payee-delete', { id });
|
||||
}
|
||||
|
||||
export function mergePayees(targetId, mergeIds) {
|
||||
return send('api/payees-merge', { targetId, mergeIds });
|
||||
}
|
||||
|
||||
export function getRules() {
|
||||
return send('api/rules-get');
|
||||
}
|
||||
@@ -189,6 +205,14 @@ export function updateRule(rule) {
|
||||
return send('api/rule-update', { rule });
|
||||
}
|
||||
|
||||
export function deleteRule(id) {
|
||||
return send('api/rule-delete', { id });
|
||||
export function deleteRule(id: string) {
|
||||
return send('api/rule-delete', id);
|
||||
}
|
||||
|
||||
export function holdBudgetForNextMonth(month, amount) {
|
||||
return send('api/budget-hold-for-next-month', { month, amount });
|
||||
}
|
||||
|
||||
export function resetBudgetHold(month) {
|
||||
return send('api/budget-reset-hold', { month });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "6.8.1",
|
||||
"version": "6.10.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
@@ -35,6 +35,6 @@
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.0.2"
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.0.2"
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as merkle from './merkle';
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
|
||||
node2 = node2[diffkey] || emptyTrie();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unreachable
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
describe('Timestamp', function () {
|
||||
|
||||
@@ -154,7 +154,7 @@ export class Timestamp {
|
||||
/**
|
||||
* maximum timestamp
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
static max = Timestamp.parse(
|
||||
'9999-12-31T23:59:59.999Z-FFFF-FFFFFFFFFFFFFFFF',
|
||||
)!;
|
||||
@@ -294,7 +294,7 @@ export class Timestamp {
|
||||
/**
|
||||
* zero/minimum timestamp
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
static zero = Timestamp.parse(
|
||||
'1970-01-01T00:00:00.000Z-0000-0000000000000000',
|
||||
)!;
|
||||
|
||||
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
@@ -16,8 +16,8 @@ export class AccountPage {
|
||||
this.cancelTransactionButton = this.page.getByRole('button', {
|
||||
name: 'Cancel',
|
||||
});
|
||||
this.menuButton = this.page.getByRole('button', {
|
||||
name: 'Menu',
|
||||
this.accountMenuButton = this.page.getByRole('button', {
|
||||
name: 'Account menu',
|
||||
});
|
||||
|
||||
this.transactionTable = this.page.getByTestId('transaction-table');
|
||||
@@ -30,16 +30,28 @@ export class AccountPage {
|
||||
this.selectTooltip = this.page.getByTestId('transactions-select-tooltip');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter details of a transaction
|
||||
*/
|
||||
async enterSingleTransaction(transaction) {
|
||||
await this.addNewTransactionButton.click();
|
||||
await this._fillTransactionFields(this.newTransactionRow, transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish adding a transaction
|
||||
*/
|
||||
async addEnteredTransaction() {
|
||||
await this.addTransactionButton.click();
|
||||
await this.cancelTransactionButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single transaction
|
||||
*/
|
||||
async createSingleTransaction(transaction) {
|
||||
await this.addNewTransactionButton.click();
|
||||
|
||||
await this._fillTransactionFields(this.newTransactionRow, transaction);
|
||||
|
||||
await this.addTransactionButton.click();
|
||||
await this.cancelTransactionButton.click();
|
||||
await this.enterSingleTransaction(transaction);
|
||||
await this.addEnteredTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,6 +94,15 @@ export class AccountPage {
|
||||
*/
|
||||
getNthTransaction(index) {
|
||||
const row = this.transactionTableRow.nth(index);
|
||||
|
||||
return this._getTransactionDetails(row);
|
||||
}
|
||||
|
||||
getEnteredTransaction() {
|
||||
return this._getTransactionDetails(this.newTransactionRow);
|
||||
}
|
||||
|
||||
_getTransactionDetails(row) {
|
||||
const account = row.getByTestId('account');
|
||||
|
||||
return {
|
||||
@@ -103,10 +124,10 @@ export class AccountPage {
|
||||
* Open the modal for closing the account.
|
||||
*/
|
||||
async clickCloseAccount() {
|
||||
await this.menuButton.click();
|
||||
await this.accountMenuButton.click();
|
||||
await this.page.getByRole('button', { name: 'Close Account' }).click();
|
||||
return new CloseAccountModal(
|
||||
this.page.locator('css=[aria-modal]'),
|
||||
this.page.getByTestId('close-account-modal'),
|
||||
this.page,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,22 +5,22 @@ export class ReportsPage {
|
||||
}
|
||||
|
||||
async waitToLoad() {
|
||||
return this.pageContent.getByRole('link', { name: /^Net/ }).waitFor();
|
||||
return this.pageContent.getByRole('button', { name: /^Net/ }).waitFor();
|
||||
}
|
||||
|
||||
async goToNetWorthPage() {
|
||||
await this.pageContent.getByRole('link', { name: /^Net/ }).click();
|
||||
await this.pageContent.getByRole('button', { name: /^Net/ }).click();
|
||||
return new ReportsPage(this.page);
|
||||
}
|
||||
|
||||
async goToCashFlowPage() {
|
||||
await this.pageContent.getByRole('link', { name: /^Cash/ }).click();
|
||||
await this.pageContent.getByRole('button', { name: /^Cash/ }).click();
|
||||
return new ReportsPage(this.page);
|
||||
}
|
||||
|
||||
async getAvailableReportList() {
|
||||
return this.pageContent
|
||||
.getByRole('link')
|
||||
.getByRole('button')
|
||||
.getByRole('heading')
|
||||
.allTextContents();
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class RulesPage {
|
||||
.first()
|
||||
.click();
|
||||
await this.page
|
||||
.getByRole('option', { exact: true, name: data.conditionsOp })
|
||||
.getByRole('button', { exact: true, name: data.conditionsOp })
|
||||
.click();
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export class RulesPage {
|
||||
await this._fillEditorFields(
|
||||
data.conditions,
|
||||
this.page.getByTestId('condition-list'),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,28 +64,19 @@ export class RulesPage {
|
||||
}
|
||||
|
||||
if (data.splits) {
|
||||
if (data.splits.beforeSplitActions) {
|
||||
let idx = data.actions?.length ?? 0;
|
||||
for (const splitActions of data.splits) {
|
||||
await this.page.getByTestId('add-split-transactions').click();
|
||||
await this._fillEditorFields(
|
||||
data.splits.beforeSplitActions,
|
||||
this.page.getByTestId('action-list'),
|
||||
splitActions,
|
||||
this.page.getByTestId('action-list').nth(idx),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.splits.splitActions) {
|
||||
let idx = data.splits?.beforeSplitActions.length ?? 0;
|
||||
for (const splitActions of data.splits.splitActions) {
|
||||
await this.page.getByTestId('add-split-transactions').click();
|
||||
await this._fillEditorFields(
|
||||
splitActions,
|
||||
this.page.getByTestId('action-list').nth(idx),
|
||||
);
|
||||
idx++;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _fillEditorFields(data, rootElement) {
|
||||
async _fillEditorFields(data, rootElement, fieldFirst = false) {
|
||||
for (const idx in data) {
|
||||
const { field, op, value } = data[idx];
|
||||
|
||||
@@ -94,16 +86,25 @@ export class RulesPage {
|
||||
await rootElement.getByRole('button', { name: 'Add entry' }).click();
|
||||
}
|
||||
|
||||
if (op && !fieldFirst) {
|
||||
await row.getByTestId('op-select').getByRole('button').first().click();
|
||||
await this.page.getByRole('button', { name: op, exact: true }).click();
|
||||
}
|
||||
|
||||
if (field) {
|
||||
await row.getByRole('button').first().click();
|
||||
await row
|
||||
.getByTestId('field-select')
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click();
|
||||
await this.page
|
||||
.getByRole('option', { exact: true, name: field })
|
||||
.getByRole('button', { name: field, exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
if (op) {
|
||||
await row.getByRole('button', { name: 'is' }).click();
|
||||
await this.page.getByRole('option', { name: op, exact: true }).click();
|
||||
if (op && fieldFirst) {
|
||||
await row.getByTestId('op-select').getByRole('button').first().click();
|
||||
await this.page.getByRole('button', { name: op, exact: true }).click();
|
||||
}
|
||||
|
||||
if (value) {
|
||||
|
||||
@@ -84,6 +84,10 @@ export class SchedulesPage {
|
||||
|
||||
if (data.amount) {
|
||||
await this.page.getByLabel('Amount').fill(String(data.amount));
|
||||
// For some readon, the input field does not trigger the change event on tests
|
||||
// but it works on the browser. We can revisit this once migration to
|
||||
// react aria components is complete.
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 76 KiB |
@@ -32,6 +32,7 @@ test.describe('Rules', () => {
|
||||
});
|
||||
|
||||
test('creates a rule and makes sure it is applied when creating a transaction', async () => {
|
||||
await rulesPage.searchFor('Fast Internet');
|
||||
await rulesPage.createRule({
|
||||
conditions: [
|
||||
{
|
||||
@@ -48,7 +49,6 @@ test.describe('Rules', () => {
|
||||
],
|
||||
});
|
||||
|
||||
await rulesPage.searchFor('Fast Internet');
|
||||
const rule = rulesPage.getNthRule(0);
|
||||
await expect(rule.conditions).toHaveText(['payee is Fast Internet']);
|
||||
await expect(rule.actions).toHaveText(['set category to General']);
|
||||
@@ -79,35 +79,34 @@ test.describe('Rules', () => {
|
||||
value: 'Ikea',
|
||||
},
|
||||
],
|
||||
splits: {
|
||||
beforeSplitActions: [
|
||||
actions: [
|
||||
{
|
||||
op: 'set',
|
||||
field: 'notes',
|
||||
value: 'food / entertainment',
|
||||
},
|
||||
],
|
||||
splits: [
|
||||
[
|
||||
{
|
||||
field: 'notes',
|
||||
value: 'food / entertainment',
|
||||
field: 'a fixed percent of the remainder',
|
||||
value: '90',
|
||||
},
|
||||
{
|
||||
field: 'category',
|
||||
value: 'Entertainment',
|
||||
},
|
||||
],
|
||||
splitActions: [
|
||||
[
|
||||
{
|
||||
field: 'a fixed percent',
|
||||
value: '90',
|
||||
},
|
||||
{
|
||||
field: 'category',
|
||||
value: 'Entertainment',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
field: 'an equal portion of the remainder',
|
||||
},
|
||||
{
|
||||
field: 'category',
|
||||
value: 'Food',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
field: 'an equal portion of the remainder',
|
||||
},
|
||||
{
|
||||
field: 'category',
|
||||
value: 'Food',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const accountPage = await navigation.goToAccountPage(
|
||||
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 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: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 82 KiB |
@@ -141,4 +141,26 @@ test.describe('Transactions', () => {
|
||||
await expect(thirdTransaction.credit).toHaveText('');
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('creates a transfer test transaction', async () => {
|
||||
await accountPage.enterSingleTransaction({
|
||||
payee: 'Bank of America',
|
||||
notes: 'Notes field',
|
||||
debit: '12.34',
|
||||
});
|
||||
|
||||
let transaction = accountPage.getEnteredTransaction();
|
||||
await expect(transaction.category.locator('input')).toHaveValue('Transfer');
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await accountPage.addEnteredTransaction();
|
||||
|
||||
transaction = accountPage.getNthTransaction(0);
|
||||
await expect(transaction.payee).toHaveText('Bank of America');
|
||||
await expect(transaction.notes).toHaveText('Notes field');
|
||||
await expect(transaction.category).toHaveText('Transfer');
|
||||
await expect(transaction.debit).toHaveText('12.34');
|
||||
await expect(transaction.credit).toHaveText('');
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
||||
|
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: 104 KiB After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
14
packages/desktop-client/i18next-parser.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
input: ['src/**/*.{js,jsx,ts,tsx}', '../loot-core/src/**/*.{js,jsx,ts,tsx}'],
|
||||
output: 'src/locale/$LOCALE.json',
|
||||
locales: ['en'],
|
||||
sort: true,
|
||||
keySeparator: false,
|
||||
namespaceSeparator: false,
|
||||
defaultValue: (locale, ns, key, value) => {
|
||||
if (locale === 'en') {
|
||||
return value || key;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "24.6.0",
|
||||
"version": "24.9.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
@@ -8,7 +8,6 @@
|
||||
"devDependencies": {
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@playwright/test": "1.41.1",
|
||||
"@reach/listbox": "^0.18.0",
|
||||
"@react-aria/focus": "^3.16.0",
|
||||
"@react-aria/listbox": "^3.11.3",
|
||||
"@react-aria/utils": "^3.23.0",
|
||||
@@ -25,6 +24,7 @@
|
||||
"@types/promise-retry": "^1.1.6",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/react-grid-layout": "^1",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/uuid": "^9.0.2",
|
||||
@@ -40,6 +40,9 @@
|
||||
"downshift": "7.6.2",
|
||||
"focus-visible": "^4.1.5",
|
||||
"glamor": "^2.20.40",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next-parser": "^9.0.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"inter-ui": "^3.19.3",
|
||||
"jest": "^27.5.1",
|
||||
"jest-watch-typeahead": "^2.2.2",
|
||||
@@ -48,13 +51,17 @@
|
||||
"memoize-one": "^6.0.0",
|
||||
"pikaday": "1.8.2",
|
||||
"promise-retry": "^2.0.1",
|
||||
"re-resizable": "^6.9.17",
|
||||
"react": "18.2.0",
|
||||
"react-aria-components": "^1.1.1",
|
||||
"react-aria": "^3.33.1",
|
||||
"react-aria-components": "^1.2.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-grid-layout": "^1.4.4",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-modal": "3.16.1",
|
||||
"react-redux": "7.2.9",
|
||||
@@ -86,6 +93,7 @@
|
||||
"build": "vite build",
|
||||
"build:browser": "cross-env ./bin/build-browser",
|
||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
||||
"generate:i18n": "i18next",
|
||||
"test": "vitest",
|
||||
"e2e": "npx playwright test --browser=chromium",
|
||||
"vrt": "cross-env VRT=true npx playwright test --browser=chromium"
|
||||
|
||||
@@ -57,6 +57,9 @@ export default defineConfig({
|
||||
timeout: 20000, // 20 seconds
|
||||
retries: 1,
|
||||
testDir: 'e2e/',
|
||||
reporter: !process.env.CI
|
||||
? [['html', { open: 'never', outputFolder: 'test-results/html' }]]
|
||||
: undefined,
|
||||
use: {
|
||||
userAgent: 'playwright',
|
||||
screenshot: 'on',
|
||||
|
||||
3
packages/desktop-client/public/shortcut-accounts.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M1.857 7.612H.98a.98.98 0 0 0-.98.98v2.224c0 .449.184.857.51 1.163.612.551 1.816 1.49 3.735 2.368a.571.571 0 0 1 .306.346l.939 3.286a.484.484 0 0 0 .469.347h1.47a.479.479 0 0 0 .469-.367l.47-1.817a.262.262 0 0 1 .326-.183c.571.122 1.183.163 1.795.163.613 0 1.225-.061 1.796-.163.143-.02.286.06.327.183l.47 1.817a.502.502 0 0 0 .468.367h1.47a.484.484 0 0 0 .47-.347l1.224-4.245a.487.487 0 0 1 .122-.224c.919-.98 1.53-2.163 1.735-3.47h.49c.53 0 .959-.448.938-.979-.02-.51-.47-.918-.98-.918h-.448C18.06 4.673 14.632 2 10.489 2c-1.775 0-3.428.49-4.755 1.326-.591-.408-1.469-.734-2.693-.632-.49.04-.715.612-.388.959.408.429.796 1 .877 1.735L1.857 7.612Zm3.122.98a.862.862 0 0 1-.857-.858c0-.469.388-.857.857-.857.47 0 .858.388.858.857 0 .47-.388.858-.858.858Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 838 B |
3
packages/desktop-client/public/shortcut-reports.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M19 18h-1v-8c0-.6-.4-1-1-1s-1 .4-1 1v8h-3V1c0-.6-.4-1-1-1s-1 .4-1 1v17H8V7c0-.6-.4-1-1-1s-1 .4-1 1v11H3V3c0-.6-.4-1-1-1s-1 .4-1 1v15c-.6 0-1 .4-1 1s.4 1 1 1h18c.6 0 1-.4 1-1s-.4-1-1-1z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 268 B |
4
packages/desktop-client/public/shortcut-transaction.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M23 11.5a1.5 1.5 0 0 1-1.5 1.5h-20a1.5 1.5 0 0 1 0-3h20a1.5 1.5 0 0 1 1.5 1.5Z" />
|
||||
<path d="M11.5 23a1.5 1.5 0 0 1-1.5-1.5v-20a1.5 1.5 0 0 1 3 0v20a1.5 1.5 0 0 1-1.5 1.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -28,6 +28,44 @@
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Add Transaction",
|
||||
"short_name": "Add Transaction",
|
||||
"description": "Add a new transaction",
|
||||
"url": "/transactions/new",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/shortcut-transaction.svg",
|
||||
"sizes": "150x150"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Accounts",
|
||||
"short_name": "Accounts",
|
||||
"description": "View all accounts",
|
||||
"url": "/accounts",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/shortcut-accounts.svg",
|
||||
"sizes": "150x150"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reports",
|
||||
"short_name": "Reports",
|
||||
"description": "View reports",
|
||||
"url": "/reports",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/shortcut-reports.svg",
|
||||
"sizes": "150x150"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/screenshot_wide.png",
|
||||
@@ -43,7 +81,7 @@
|
||||
"type": "image/png",
|
||||
"sizes": "350x600"
|
||||
}
|
||||
],
|
||||
],
|
||||
"theme_color": "#8812E1",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone",
|
||||
|
||||
@@ -135,6 +135,14 @@ global.Actual = {
|
||||
},
|
||||
};
|
||||
|
||||
function inputFocused(e) {
|
||||
return (
|
||||
e.target.tagName === 'INPUT' ||
|
||||
e.target.tagName === 'TEXTAREA' ||
|
||||
e.target.isContentEditable
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
// Cmd/Ctrl+o
|
||||
@@ -144,11 +152,7 @@ document.addEventListener('keydown', e => {
|
||||
}
|
||||
// Cmd/Ctrl+z
|
||||
else if (e.key.toLowerCase() === 'z') {
|
||||
if (
|
||||
e.target.tagName === 'INPUT' ||
|
||||
e.target.tagName === 'TEXTAREA' ||
|
||||
e.target.isContentEditable
|
||||
) {
|
||||
if (inputFocused(e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
@@ -160,5 +164,10 @@ document.addEventListener('keydown', e => {
|
||||
window.__actionsForMenu.undo();
|
||||
}
|
||||
}
|
||||
} else if (e.key === '?') {
|
||||
if (inputFocused(e)) {
|
||||
return;
|
||||
}
|
||||
window.__actionsForMenu.pushModal('keyboard-shortcuts');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type FallbackProps,
|
||||
} from 'react-error-boundary';
|
||||
import { HotkeysProvider } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
send,
|
||||
} from 'loot-core/src/platform/client/fetch';
|
||||
|
||||
import { useLocalPref } from '../hooks/useLocalPref';
|
||||
import { useMetadataPref } from '../hooks/useMetadataPref';
|
||||
import { installPolyfills } from '../polyfills';
|
||||
import { ResponsiveProvider } from '../ResponsiveProvider';
|
||||
import { styles, hasHiddenScrollbars, ThemeStyle } from '../style';
|
||||
@@ -33,7 +34,6 @@ import { DevelopmentTopBar } from './DevelopmentTopBar';
|
||||
import { FatalError } from './FatalError';
|
||||
import { FinancesApp } from './FinancesApp';
|
||||
import { ManagementApp } from './manager/ManagementApp';
|
||||
import { MobileWebMessage } from './mobile/MobileWebMessage';
|
||||
import { UpdateNotification } from './UpdateNotification';
|
||||
|
||||
type AppInnerProps = {
|
||||
@@ -42,6 +42,7 @@ type AppInnerProps = {
|
||||
};
|
||||
|
||||
function AppInner({ budgetId, cloudFileId }: AppInnerProps) {
|
||||
const { t } = useTranslation();
|
||||
const [initializing, setInitializing] = useState(true);
|
||||
const { showBoundary: showErrorBoundary } = useErrorBoundary();
|
||||
const loadingText = useSelector((state: State) => state.app.loadingText);
|
||||
@@ -52,7 +53,7 @@ function AppInner({ budgetId, cloudFileId }: AppInnerProps) {
|
||||
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: 'Initializing the connection to the local database...',
|
||||
loadingText: t('Initializing the connection to the local database...'),
|
||||
}),
|
||||
);
|
||||
await initConnection(socketName);
|
||||
@@ -60,7 +61,7 @@ function AppInner({ budgetId, cloudFileId }: AppInnerProps) {
|
||||
// Load any global prefs
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: 'Loading global preferences...',
|
||||
loadingText: t('Loading global preferences...'),
|
||||
}),
|
||||
);
|
||||
await dispatch(loadGlobalPrefs());
|
||||
@@ -68,18 +69,20 @@ function AppInner({ budgetId, cloudFileId }: AppInnerProps) {
|
||||
// Open the last opened budget, if any
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: 'Opening last budget...',
|
||||
loadingText: t('Opening last budget...'),
|
||||
}),
|
||||
);
|
||||
const budgetId = await send('get-last-opened-backup');
|
||||
if (budgetId) {
|
||||
await dispatch(loadBudget(budgetId, 'Loading the last budget file...'));
|
||||
await dispatch(
|
||||
loadBudget(budgetId, t('Loading the last budget file...')),
|
||||
);
|
||||
|
||||
// Check to see if this file has been remotely deleted (but
|
||||
// don't block on this in case they are offline or something)
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: 'Retrieving remote files...',
|
||||
loadingText: t('Retrieving remote files...'),
|
||||
}),
|
||||
);
|
||||
send('get-remote-files').then(files => {
|
||||
@@ -124,7 +127,6 @@ function AppInner({ budgetId, cloudFileId }: AppInnerProps) {
|
||||
))}
|
||||
|
||||
<UpdateNotification />
|
||||
<MobileWebMessage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -139,8 +141,8 @@ function ErrorFallback({ error }: FallbackProps) {
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [budgetId] = useLocalPref('id');
|
||||
const [cloudFileId] = useLocalPref('cloudFileId');
|
||||
const [budgetId] = useMetadataPref('id');
|
||||
const [cloudFileId] = useMetadataPref('cloudFileId');
|
||||
const [hiddenScrollbars, setHiddenScrollbars] = useState(
|
||||
hasHiddenScrollbars(),
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useState, type ReactNode } from 'react';
|
||||
import { LazyLoadFailedError } from 'loot-core/src/shared/errors';
|
||||
|
||||
import { Block } from './common/Block';
|
||||
import { Button } from './common/Button';
|
||||
import { Button } from './common/Button2';
|
||||
import { Link } from './common/Link';
|
||||
import { Modal } from './common/Modal';
|
||||
import { Paragraph } from './common/Paragraph';
|
||||
@@ -149,8 +149,8 @@ function SharedArrayBufferOverride() {
|
||||
I understand the risks, run Actual in the unsupported fallback mode
|
||||
</label>
|
||||
<Button
|
||||
disabled={!understand}
|
||||
onClick={() => {
|
||||
isDisabled={!understand}
|
||||
onPress={() => {
|
||||
window.localStorage.setItem('SharedArrayBufferOverride', 'true');
|
||||
window.location.reload();
|
||||
}}
|
||||
@@ -191,7 +191,7 @@ export function FatalError({ error }: FatalErrorProps) {
|
||||
)}
|
||||
|
||||
<Paragraph>
|
||||
<Button onClick={() => window.Actual?.relaunch()}>Restart app</Button>
|
||||
<Button onPress={() => window.Actual?.relaunch()}>Restart app</Button>
|
||||
</Paragraph>
|
||||
<Paragraph isLast={true} style={{ fontSize: 11 }}>
|
||||
<Link variant="text" onClick={() => setShowError(state => !state)}>
|
||||
|
||||
@@ -42,7 +42,7 @@ import { ScrollProvider } from './ScrollProvider';
|
||||
import { Settings } from './settings';
|
||||
import { FloatableSidebar } from './sidebar';
|
||||
import { SidebarProvider } from './sidebar/SidebarProvider';
|
||||
import { Titlebar, TitlebarProvider } from './Titlebar';
|
||||
import { Titlebar } from './Titlebar';
|
||||
|
||||
function NarrowNotSupported({
|
||||
redirectTo = '/budget',
|
||||
@@ -246,15 +246,13 @@ export function FinancesApp() {
|
||||
|
||||
return (
|
||||
<SpreadsheetProvider>
|
||||
<TitlebarProvider>
|
||||
<SidebarProvider>
|
||||
<BudgetMonthCountProvider>
|
||||
<DndProvider backend={Backend}>
|
||||
<ScrollProvider>{app}</ScrollProvider>
|
||||
</DndProvider>
|
||||
</BudgetMonthCountProvider>
|
||||
</SidebarProvider>
|
||||
</TitlebarProvider>
|
||||
<SidebarProvider>
|
||||
<BudgetMonthCountProvider>
|
||||
<DndProvider backend={Backend}>
|
||||
<ScrollProvider>{app}</ScrollProvider>
|
||||
</DndProvider>
|
||||
</BudgetMonthCountProvider>
|
||||
</SidebarProvider>
|
||||
</SpreadsheetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { type State } from 'loot-core/src/client/state-types';
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { theme, styles, type CSSProperties } from '../style';
|
||||
|
||||
import { Button } from './common/Button';
|
||||
import { Button } from './common/Button2';
|
||||
import { Menu } from './common/Menu';
|
||||
import { Popover } from './common/Popover';
|
||||
import { Text } from './common/Text';
|
||||
@@ -97,8 +97,8 @@ export function LoggedInUser({
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', ...style }}>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
onClick={() => setMenuOpen(true)}
|
||||
variant="bare"
|
||||
onPress={() => setMenuOpen(true)}
|
||||
style={color && { color }}
|
||||
>
|
||||
{serverMessage()}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { pushModal } from 'loot-core/src/client/actions/modals';
|
||||
import { initiallyLoadPayees } from 'loot-core/src/client/actions/queries';
|
||||
import { send } from 'loot-core/src/platform/client/fetch';
|
||||
import * as undo from 'loot-core/src/platform/client/undo';
|
||||
import { getNormalisedString } from 'loot-core/src/shared/normalisation';
|
||||
import { mapField, friendlyOp } from 'loot-core/src/shared/rules';
|
||||
import { describeSchedule } from 'loot-core/src/shared/schedules';
|
||||
import { type NewRuleEntity } from 'loot-core/src/types/models';
|
||||
@@ -23,7 +24,7 @@ import { usePayees } from '../hooks/usePayees';
|
||||
import { useSelected, SelectedProvider } from '../hooks/useSelected';
|
||||
import { theme } from '../style';
|
||||
|
||||
import { Button } from './common/Button';
|
||||
import { Button } from './common/Button2';
|
||||
import { Link } from './common/Link';
|
||||
import { Search } from './common/Search';
|
||||
import { Stack } from './common/Stack';
|
||||
@@ -78,6 +79,11 @@ function ruleToString(rule, data) {
|
||||
data.payees.find(p => p.id === schedule._payee),
|
||||
),
|
||||
];
|
||||
} else if (action.op === 'prepend-notes' || action.op === 'append-notes') {
|
||||
return [
|
||||
friendlyOp(action.op),
|
||||
'“' + mapValue(action.field, action.value, data) + '”',
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -125,9 +131,9 @@ function ManageRulesContent({
|
||||
(filter === ''
|
||||
? allRules
|
||||
: allRules.filter(rule =>
|
||||
ruleToString(rule, filterData)
|
||||
.toLowerCase()
|
||||
.includes(filter.toLowerCase()),
|
||||
getNormalisedString(ruleToString(rule, filterData)).includes(
|
||||
getNormalisedString(filter),
|
||||
),
|
||||
)
|
||||
).slice(0, 100 + page * 50),
|
||||
[allRules, filter, filterData, page],
|
||||
@@ -313,11 +319,11 @@ function ManageRulesContent({
|
||||
>
|
||||
<Stack direction="row" align="center" justify="flex-end" spacing={2}>
|
||||
{selectedInst.items.size > 0 && (
|
||||
<Button onClick={onDeleteSelected}>
|
||||
<Button onPress={onDeleteSelected}>
|
||||
Delete {selectedInst.items.size} rules
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" onClick={onCreateRule}>
|
||||
<Button variant="primary" onPress={onCreateRule}>
|
||||
Create new rule
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
import { closeModal } from 'loot-core/client/actions';
|
||||
import { type PopModalAction } from 'loot-core/src/client/state-types/modals';
|
||||
import { send } from 'loot-core/src/platform/client/fetch';
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
|
||||
import { useActions } from '../hooks/useActions';
|
||||
import { useModalState } from '../hooks/useModalState';
|
||||
import { useSyncServerStatus } from '../hooks/useSyncServerStatus';
|
||||
|
||||
import { ModalTitle } from './common/Modal';
|
||||
import { ModalTitle, ModalHeader } from './common/Modal2';
|
||||
import { AccountAutocompleteModal } from './modals/AccountAutocompleteModal';
|
||||
import { AccountMenuModal } from './modals/AccountMenuModal';
|
||||
import { BudgetListModal } from './modals/BudgetListModal';
|
||||
@@ -35,6 +35,7 @@ import { GoCardlessExternalMsg } from './modals/GoCardlessExternalMsg';
|
||||
import { GoCardlessInitialise } from './modals/GoCardlessInitialise';
|
||||
import { HoldBufferModal } from './modals/HoldBufferModal';
|
||||
import { ImportTransactions } from './modals/ImportTransactions';
|
||||
import { KeyboardShortcutModal } from './modals/KeyboardShortcutModal';
|
||||
import { LoadBackup } from './modals/LoadBackup';
|
||||
import { ManageRulesModal } from './modals/ManageRulesModal';
|
||||
import { MergeUnusedPayees } from './modals/MergeUnusedPayees';
|
||||
@@ -53,7 +54,6 @@ import { ScheduledTransactionMenuModal } from './modals/ScheduledTransactionMenu
|
||||
import { SelectLinkedAccounts } from './modals/SelectLinkedAccounts';
|
||||
import { SimpleFinInitialise } from './modals/SimpleFinInitialise';
|
||||
import { SingleInputModal } from './modals/SingleInputModal';
|
||||
import { SwitchBudgetTypeModal } from './modals/SwitchBudgetTypeModal';
|
||||
import { TransferModal } from './modals/TransferModal';
|
||||
import { DiscoverSchedules } from './schedules/DiscoverSchedules';
|
||||
import { PostsOfflineNotification } from './schedules/PostsOfflineNotification';
|
||||
@@ -71,64 +71,43 @@ export type CommonModalProps = {
|
||||
};
|
||||
|
||||
export function Modals() {
|
||||
const modalStack = useSelector((state: State) => state.modals.modalStack);
|
||||
const isHidden = useSelector((state: State) => state.modals.isHidden);
|
||||
const actions = useActions();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const { modalStack } = useModalState();
|
||||
|
||||
useEffect(() => {
|
||||
if (modalStack.length > 0) {
|
||||
actions.closeModal();
|
||||
dispatch(closeModal());
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
const syncServerStatus = useSyncServerStatus();
|
||||
|
||||
const modals = modalStack
|
||||
.map(({ name, options }, idx) => {
|
||||
const modalProps: CommonModalProps = {
|
||||
onClose: actions.popModal,
|
||||
onBack: actions.popModal,
|
||||
showBack: idx > 0,
|
||||
isCurrent: idx === modalStack.length - 1,
|
||||
isHidden,
|
||||
stackIndex: idx,
|
||||
};
|
||||
|
||||
.map(({ name, options }) => {
|
||||
switch (name) {
|
||||
case 'keyboard-shortcuts':
|
||||
return <KeyboardShortcutModal />;
|
||||
|
||||
case 'import-transactions':
|
||||
return (
|
||||
<ImportTransactions
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
return <ImportTransactions key={name} options={options} />;
|
||||
|
||||
case 'add-account':
|
||||
return (
|
||||
<CreateAccountModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
syncServerStatus={syncServerStatus}
|
||||
upgradingAccountId={options?.upgradingAccountId}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'add-local-account':
|
||||
return (
|
||||
<CreateLocalAccountModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
return <CreateLocalAccountModal key={name} />;
|
||||
|
||||
case 'close-account':
|
||||
return (
|
||||
<CloseAccountModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
account={options.account}
|
||||
balance={options.balance}
|
||||
canDelete={options.canDelete}
|
||||
@@ -139,10 +118,8 @@ export function Modals() {
|
||||
return (
|
||||
<SelectLinkedAccounts
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
externalAccounts={options.accounts}
|
||||
requisitionId={options.requisitionId}
|
||||
actions={actions}
|
||||
syncSource={options.syncSource}
|
||||
/>
|
||||
);
|
||||
@@ -151,7 +128,6 @@ export function Modals() {
|
||||
return (
|
||||
<ConfirmCategoryDelete
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
category={options.category}
|
||||
group={options.group}
|
||||
onDelete={options.onDelete}
|
||||
@@ -162,7 +138,6 @@ export function Modals() {
|
||||
return (
|
||||
<ConfirmUnlinkAccount
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
accountName={options.accountName}
|
||||
onUnlink={options.onUnlink}
|
||||
/>
|
||||
@@ -172,7 +147,7 @@ export function Modals() {
|
||||
return (
|
||||
<ConfirmTransactionEdit
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
onCancel={options.onCancel}
|
||||
onConfirm={options.onConfirm}
|
||||
confirmReason={options.confirmReason}
|
||||
/>
|
||||
@@ -182,7 +157,7 @@ export function Modals() {
|
||||
return (
|
||||
<ConfirmTransactionDelete
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
message={options.message}
|
||||
onConfirm={options.onConfirm}
|
||||
/>
|
||||
);
|
||||
@@ -193,26 +168,17 @@ export function Modals() {
|
||||
key={name}
|
||||
watchUpdates
|
||||
budgetId={options.budgetId}
|
||||
modalProps={modalProps}
|
||||
actions={actions}
|
||||
backupDisabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'manage-rules':
|
||||
return (
|
||||
<ManageRulesModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
payeeId={options?.payeeId}
|
||||
/>
|
||||
);
|
||||
return <ManageRulesModal key={name} payeeId={options?.payeeId} />;
|
||||
|
||||
case 'edit-rule':
|
||||
return (
|
||||
<EditRule
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
defaultRule={options.rule}
|
||||
onSave={options.onSave}
|
||||
/>
|
||||
@@ -222,7 +188,6 @@ export function Modals() {
|
||||
return (
|
||||
<MergeUnusedPayees
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
payeeIds={options.payeeIds}
|
||||
targetPayeeId={options.targetPayeeId}
|
||||
/>
|
||||
@@ -230,27 +195,18 @@ export function Modals() {
|
||||
|
||||
case 'gocardless-init':
|
||||
return (
|
||||
<GoCardlessInitialise
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
onSuccess={options.onSuccess}
|
||||
/>
|
||||
<GoCardlessInitialise key={name} onSuccess={options.onSuccess} />
|
||||
);
|
||||
|
||||
case 'simplefin-init':
|
||||
return (
|
||||
<SimpleFinInitialise
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
onSuccess={options.onSuccess}
|
||||
/>
|
||||
<SimpleFinInitialise key={name} onSuccess={options.onSuccess} />
|
||||
);
|
||||
|
||||
case 'gocardless-external-msg':
|
||||
return (
|
||||
<GoCardlessExternalMsg
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
onMoveExternal={options.onMoveExternal}
|
||||
onClose={() => {
|
||||
options.onClose?.();
|
||||
@@ -261,28 +217,15 @@ export function Modals() {
|
||||
);
|
||||
|
||||
case 'create-encryption-key':
|
||||
return (
|
||||
<CreateEncryptionKeyModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
return <CreateEncryptionKeyModal key={name} options={options} />;
|
||||
|
||||
case 'fix-encryption-key':
|
||||
return (
|
||||
<FixEncryptionKeyModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
return <FixEncryptionKeyModal key={name} options={options} />;
|
||||
|
||||
case 'edit-field':
|
||||
return (
|
||||
<EditField
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
name={options.name}
|
||||
onSubmit={options.onSubmit}
|
||||
onClose={options.onClose}
|
||||
@@ -293,7 +236,6 @@ export function Modals() {
|
||||
return (
|
||||
<CategoryAutocompleteModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
autocompleteProps={{
|
||||
value: null,
|
||||
onSelect: options.onSelect,
|
||||
@@ -309,7 +251,6 @@ export function Modals() {
|
||||
return (
|
||||
<AccountAutocompleteModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
autocompleteProps={{
|
||||
value: null,
|
||||
onSelect: options.onSelect,
|
||||
@@ -323,7 +264,6 @@ export function Modals() {
|
||||
return (
|
||||
<PayeeAutocompleteModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
autocompleteProps={{
|
||||
value: null,
|
||||
onSelect: options.onSelect,
|
||||
@@ -336,8 +276,13 @@ export function Modals() {
|
||||
return (
|
||||
<SingleInputModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
title={<ModalTitle title="New Category" shrinkOnOverflow />}
|
||||
name={name}
|
||||
Header={props => (
|
||||
<ModalHeader
|
||||
{...props}
|
||||
title={<ModalTitle title="New Category" shrinkOnOverflow />}
|
||||
/>
|
||||
)}
|
||||
inputPlaceholder="Category name"
|
||||
buttonText="Add"
|
||||
onValidate={options.onValidate}
|
||||
@@ -349,8 +294,15 @@ export function Modals() {
|
||||
return (
|
||||
<SingleInputModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
title={<ModalTitle title="New Category Group" shrinkOnOverflow />}
|
||||
name={name}
|
||||
Header={props => (
|
||||
<ModalHeader
|
||||
{...props}
|
||||
title={
|
||||
<ModalTitle title="New Category Group" shrinkOnOverflow />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
inputPlaceholder="Category group name"
|
||||
buttonText="Add"
|
||||
onValidate={options.onValidate}
|
||||
@@ -366,7 +318,6 @@ export function Modals() {
|
||||
>
|
||||
<RolloverBudgetSummaryModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
month={options.month}
|
||||
onBudgetAction={options.onBudgetAction}
|
||||
/>
|
||||
@@ -374,21 +325,13 @@ export function Modals() {
|
||||
);
|
||||
|
||||
case 'report-budget-summary':
|
||||
return (
|
||||
<ReportBudgetSummaryModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
month={options.month}
|
||||
/>
|
||||
);
|
||||
return <ReportBudgetSummaryModal key={name} month={options.month} />;
|
||||
|
||||
case 'schedule-edit':
|
||||
return (
|
||||
<ScheduleDetails
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
id={options?.id || null}
|
||||
actions={actions}
|
||||
transaction={options?.transaction || null}
|
||||
/>
|
||||
);
|
||||
@@ -397,46 +340,23 @@ export function Modals() {
|
||||
return (
|
||||
<ScheduleLink
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
actions={actions}
|
||||
transactionIds={options?.transactionIds}
|
||||
getTransaction={options?.getTransaction}
|
||||
pushModal={options?.pushModal}
|
||||
accountName={options?.accountName}
|
||||
onScheduleLinked={options?.onScheduleLinked}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'schedules-discover':
|
||||
return (
|
||||
<DiscoverSchedules
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
return <DiscoverSchedules key={name} />;
|
||||
|
||||
case 'schedule-posts-offline-notification':
|
||||
return (
|
||||
<PostsOfflineNotification
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'switch-budget-type':
|
||||
return (
|
||||
<SwitchBudgetTypeModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
onSwitch={options.onSwitch}
|
||||
/>
|
||||
);
|
||||
return <PostsOfflineNotification key={name} />;
|
||||
|
||||
case 'account-menu':
|
||||
return (
|
||||
<AccountMenuModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
accountId={options.accountId}
|
||||
onSave={options.onSave}
|
||||
onEditNotes={options.onEditNotes}
|
||||
@@ -450,11 +370,11 @@ export function Modals() {
|
||||
return (
|
||||
<CategoryMenuModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
categoryId={options.categoryId}
|
||||
onSave={options.onSave}
|
||||
onEditNotes={options.onEditNotes}
|
||||
onDelete={options.onDelete}
|
||||
onToggleVisibility={options.onToggleVisibility}
|
||||
onClose={options.onClose}
|
||||
/>
|
||||
);
|
||||
@@ -466,7 +386,6 @@ export function Modals() {
|
||||
value={monthUtils.sheetForMonth(options.month)}
|
||||
>
|
||||
<RolloverBudgetMenuModal
|
||||
modalProps={modalProps}
|
||||
categoryId={options.categoryId}
|
||||
onUpdateBudget={options.onUpdateBudget}
|
||||
onCopyLastMonthAverage={options.onCopyLastMonthAverage}
|
||||
@@ -483,7 +402,6 @@ export function Modals() {
|
||||
value={monthUtils.sheetForMonth(options.month)}
|
||||
>
|
||||
<ReportBudgetMenuModal
|
||||
modalProps={modalProps}
|
||||
categoryId={options.categoryId}
|
||||
onUpdateBudget={options.onUpdateBudget}
|
||||
onCopyLastMonthAverage={options.onCopyLastMonthAverage}
|
||||
@@ -497,13 +415,13 @@ export function Modals() {
|
||||
return (
|
||||
<CategoryGroupMenuModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
groupId={options.groupId}
|
||||
onSave={options.onSave}
|
||||
onAddCategory={options.onAddCategory}
|
||||
onEditNotes={options.onEditNotes}
|
||||
onSaveNotes={options.onSaveNotes}
|
||||
onDelete={options.onDelete}
|
||||
onToggleVisibility={options.onToggleVisibility}
|
||||
onClose={options.onClose}
|
||||
/>
|
||||
);
|
||||
@@ -512,7 +430,6 @@ export function Modals() {
|
||||
return (
|
||||
<NotesModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
id={options.id}
|
||||
name={options.name}
|
||||
onSave={options.onSave}
|
||||
@@ -526,7 +443,6 @@ export function Modals() {
|
||||
value={monthUtils.sheetForMonth(options.month)}
|
||||
>
|
||||
<RolloverBalanceMenuModal
|
||||
modalProps={modalProps}
|
||||
categoryId={options.categoryId}
|
||||
onCarryover={options.onCarryover}
|
||||
onTransfer={options.onTransfer}
|
||||
@@ -542,7 +458,6 @@ export function Modals() {
|
||||
value={monthUtils.sheetForMonth(options.month)}
|
||||
>
|
||||
<RolloverToBudgetMenuModal
|
||||
modalProps={modalProps}
|
||||
onTransfer={options.onTransfer}
|
||||
onCover={options.onCover}
|
||||
onHoldBuffer={options.onHoldBuffer}
|
||||
@@ -558,7 +473,6 @@ export function Modals() {
|
||||
value={monthUtils.sheetForMonth(options.month)}
|
||||
>
|
||||
<HoldBufferModal
|
||||
modalProps={modalProps}
|
||||
month={options.month}
|
||||
onSubmit={options.onSubmit}
|
||||
/>
|
||||
@@ -572,7 +486,6 @@ export function Modals() {
|
||||
value={monthUtils.sheetForMonth(options.month)}
|
||||
>
|
||||
<ReportBalanceMenuModal
|
||||
modalProps={modalProps}
|
||||
categoryId={options.categoryId}
|
||||
onCarryover={options.onCarryover}
|
||||
/>
|
||||
@@ -583,7 +496,6 @@ export function Modals() {
|
||||
return (
|
||||
<TransferModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
title={options.title}
|
||||
month={options.month}
|
||||
amount={options.amount}
|
||||
@@ -596,10 +508,10 @@ export function Modals() {
|
||||
return (
|
||||
<CoverModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
title={options.title}
|
||||
month={options.month}
|
||||
showToBeBudgeted={options.showToBeBudgeted}
|
||||
category={options.category}
|
||||
onSubmit={options.onSubmit}
|
||||
/>
|
||||
);
|
||||
@@ -608,7 +520,6 @@ export function Modals() {
|
||||
return (
|
||||
<ScheduledTransactionMenuModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
transactionId={options.transactionId}
|
||||
onPost={options.onPost}
|
||||
onSkip={options.onSkip}
|
||||
@@ -619,11 +530,9 @@ export function Modals() {
|
||||
return (
|
||||
<BudgetPageMenuModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
onAddCategoryGroup={options.onAddCategoryGroup}
|
||||
onToggleHiddenCategories={options.onToggleHiddenCategories}
|
||||
onSwitchBudgetFile={options.onSwitchBudgetFile}
|
||||
onSwitchBudgetType={options.onSwitchBudgetType}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -634,7 +543,6 @@ export function Modals() {
|
||||
value={monthUtils.sheetForMonth(options.month)}
|
||||
>
|
||||
<RolloverBudgetMonthMenuModal
|
||||
modalProps={modalProps}
|
||||
month={options.month}
|
||||
onBudgetAction={options.onBudgetAction}
|
||||
onEditNotes={options.onEditNotes}
|
||||
@@ -649,7 +557,6 @@ export function Modals() {
|
||||
value={monthUtils.sheetForMonth(options.month)}
|
||||
>
|
||||
<ReportBudgetMonthMenuModal
|
||||
modalProps={modalProps}
|
||||
month={options.month}
|
||||
onBudgetAction={options.onBudgetAction}
|
||||
onEditNotes={options.onEditNotes}
|
||||
@@ -658,7 +565,7 @@ export function Modals() {
|
||||
);
|
||||
|
||||
case 'budget-list':
|
||||
return <BudgetListModal key={name} modalProps={modalProps} />;
|
||||
return <BudgetListModal key={name} />;
|
||||
|
||||
default:
|
||||
console.error('Unknown modal:', name);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useNotes } from '../hooks/useNotes';
|
||||
import { SvgCustomNotesPaper } from '../icons/v2';
|
||||
import { type CSSProperties, theme } from '../style';
|
||||
|
||||
import { Button } from './common/Button';
|
||||
import { Button } from './common/Button2';
|
||||
import { Popover } from './common/Popover';
|
||||
import { Tooltip } from './common/Tooltip';
|
||||
import { View } from './common/View';
|
||||
@@ -52,7 +52,7 @@ export function NotesButton({
|
||||
<View style={{ flexShrink: 0 }}>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
variant="bare"
|
||||
aria-label="View notes"
|
||||
className={!hasNotes && !isOpen ? 'hover-visible' : ''}
|
||||
style={{
|
||||
@@ -61,8 +61,7 @@ export function NotesButton({
|
||||
...(hasNotes && { display: 'flex !important' }),
|
||||
...(isOpen && { color: theme.buttonNormalText }),
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
onPress={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,17 +5,18 @@ import React, {
|
||||
useMemo,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { removeNotification } from 'loot-core/client/actions';
|
||||
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';
|
||||
import { AnimatedLoading } from '../icons/AnimatedLoading';
|
||||
import { SvgDelete } from '../icons/v0';
|
||||
import { useResponsive } from '../ResponsiveProvider';
|
||||
import { styles, theme, type CSSProperties } from '../style';
|
||||
|
||||
import { Button, ButtonWithLoading } from './common/Button';
|
||||
import { Button, ButtonWithLoading } from './common/Button2';
|
||||
import { Link } from './common/Link';
|
||||
import { Stack } from './common/Stack';
|
||||
import { Text } from './common/Text';
|
||||
@@ -119,6 +120,11 @@ function Notification({
|
||||
[message, messageActions],
|
||||
);
|
||||
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
const narrowStyle: CSSProperties = isNarrowWidth
|
||||
? { minHeight: styles.mobileMinHeight }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -132,10 +138,11 @@ function Notification({
|
||||
>
|
||||
<Stack
|
||||
align="center"
|
||||
justify="space-between"
|
||||
direction="row"
|
||||
style={{
|
||||
padding: '14px 14px',
|
||||
fontSize: 14,
|
||||
...styles.mediumText,
|
||||
backgroundColor: positive
|
||||
? theme.noticeBackgroundLight
|
||||
: error
|
||||
@@ -155,7 +162,15 @@ function Notification({
|
||||
>
|
||||
<Stack align="flex-start">
|
||||
{title && (
|
||||
<View style={{ fontWeight: 700, marginBottom: 10 }}>{title}</View>
|
||||
<View
|
||||
style={{
|
||||
...styles.mediumText,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</View>
|
||||
)}
|
||||
<View>{processedMessage}</View>
|
||||
{pre
|
||||
@@ -177,15 +192,15 @@ function Notification({
|
||||
: null}
|
||||
{button && (
|
||||
<ButtonWithLoading
|
||||
type="bare"
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
variant="bare"
|
||||
isLoading={loading}
|
||||
onPress={async () => {
|
||||
setLoading(true);
|
||||
await button.action();
|
||||
onRemove();
|
||||
setLoading(false);
|
||||
}}
|
||||
style={{
|
||||
style={({ isHovered, isPressed }) => ({
|
||||
backgroundColor: 'transparent',
|
||||
border: `1px solid ${
|
||||
positive
|
||||
@@ -195,31 +210,32 @@ function Notification({
|
||||
: theme.warningBorder
|
||||
}`,
|
||||
color: 'currentColor',
|
||||
fontSize: 14,
|
||||
...styles.mediumText,
|
||||
flexShrink: 0,
|
||||
'&:hover, &:active': {
|
||||
backgroundColor: positive
|
||||
? theme.noticeBackground
|
||||
: error
|
||||
? theme.errorBackground
|
||||
: theme.warningBackground,
|
||||
},
|
||||
}}
|
||||
...(isHovered || isPressed
|
||||
? {
|
||||
backgroundColor: positive
|
||||
? theme.noticeBackground
|
||||
: error
|
||||
? theme.errorBackground
|
||||
: theme.warningBackground,
|
||||
}
|
||||
: {}),
|
||||
...narrowStyle,
|
||||
})}
|
||||
>
|
||||
{button.title}
|
||||
</ButtonWithLoading>
|
||||
)}
|
||||
</Stack>
|
||||
{sticky && (
|
||||
<Button
|
||||
type="bare"
|
||||
aria-label="Close"
|
||||
style={{ flexShrink: 0, color: 'currentColor' }}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<SvgDelete style={{ width: 9, height: 9, color: 'currentColor' }} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="bare"
|
||||
aria-label="Close"
|
||||
style={{ flexShrink: 0, color: 'currentColor' }}
|
||||
onPress={onRemove}
|
||||
>
|
||||
<SvgDelete style={{ width: 9, height: 9, color: 'currentColor' }} />
|
||||
</Button>
|
||||
</Stack>
|
||||
{overlayLoading && (
|
||||
<View
|
||||
@@ -244,16 +260,22 @@ function Notification({
|
||||
}
|
||||
|
||||
export function Notifications({ style }: { style?: CSSProperties }) {
|
||||
const { removeNotification } = useActions();
|
||||
const dispatch = useDispatch();
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
const notifications = useSelector(
|
||||
(state: State) => state.notifications.notifications,
|
||||
);
|
||||
const notificationInset = useSelector(
|
||||
(state: State) => state.notifications.inset,
|
||||
);
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: 20,
|
||||
right: 13,
|
||||
bottom: notificationInset?.bottom || 20,
|
||||
top: notificationInset?.top,
|
||||
right: notificationInset?.right || 13,
|
||||
left: notificationInset?.left || (isNarrowWidth ? 13 : undefined),
|
||||
zIndex: 10000,
|
||||
...style,
|
||||
}}
|
||||
@@ -266,7 +288,7 @@ export function Notifications({ style }: { style?: CSSProperties }) {
|
||||
if (note.onClose) {
|
||||
note.onClose();
|
||||
}
|
||||
removeNotification(note.id);
|
||||
dispatch(removeNotification(note.id));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SvgMoonStars, SvgSun, SvgSystem } from '../icons/v2';
|
||||
import { useResponsive } from '../ResponsiveProvider';
|
||||
import { type CSSProperties, themeOptions, useTheme } from '../style';
|
||||
|
||||
import { Button } from './common/Button';
|
||||
import { Button } from './common/Button2';
|
||||
import { Menu } from './common/Menu';
|
||||
import { Popover } from './common/Popover';
|
||||
|
||||
@@ -44,9 +44,9 @@ export function ThemeSelector({ style }: ThemeSelectorProps) {
|
||||
<>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
variant="bare"
|
||||
aria-label="Switch theme"
|
||||
onClick={() => setMenuOpen(true)}
|
||||
onPress={() => setMenuOpen(true)}
|
||||
style={style}
|
||||
>
|
||||
<Icon style={{ width: 13, height: 13, color: 'inherit' }} />
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useContext,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -13,13 +6,12 @@ import * as Platform from 'loot-core/src/client/platform';
|
||||
import * as queries from 'loot-core/src/client/queries';
|
||||
import { listen } from 'loot-core/src/platform/client/fetch';
|
||||
import { isDevelopmentEnvironment } from 'loot-core/src/shared/environment';
|
||||
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 { useMetadataPref } from '../hooks/useMetadataPref';
|
||||
import { useNavigate } from '../hooks/useNavigate';
|
||||
import { useSyncedPref } from '../hooks/useSyncedPref';
|
||||
import { SvgArrowLeft } from '../icons/v1';
|
||||
import {
|
||||
SvgAlertTriangle,
|
||||
@@ -33,10 +25,8 @@ import { theme, type CSSProperties, styles } from '../style';
|
||||
import { AccountSyncCheck } from './accounts/AccountSyncCheck';
|
||||
import { AnimatedRefresh } from './AnimatedRefresh';
|
||||
import { MonthCountSelector } from './budget/MonthCountSelector';
|
||||
import { Button, ButtonWithLoading } from './common/Button';
|
||||
import { Button } from './common/Button2';
|
||||
import { Link } from './common/Link';
|
||||
import { Paragraph } from './common/Paragraph';
|
||||
import { Popover } from './common/Popover';
|
||||
import { Text } from './common/Text';
|
||||
import { View } from './common/View';
|
||||
import { LoggedInUser } from './LoggedInUser';
|
||||
@@ -45,55 +35,6 @@ import { useSidebar } from './sidebar/SidebarProvider';
|
||||
import { useSheetValue } from './spreadsheet/useSheetValue';
|
||||
import { ThemeSelector } from './ThemeSelector';
|
||||
|
||||
export const SWITCH_BUDGET_MESSAGE_TYPE = 'budget/switch-type';
|
||||
|
||||
type SwitchBudgetTypeMessage = {
|
||||
type: typeof SWITCH_BUDGET_MESSAGE_TYPE;
|
||||
payload: {
|
||||
newBudgetType: LocalPrefs['budgetType'];
|
||||
};
|
||||
};
|
||||
export type TitlebarMessage = SwitchBudgetTypeMessage;
|
||||
|
||||
type Listener = (msg: TitlebarMessage) => void;
|
||||
export type TitlebarContextValue = {
|
||||
sendEvent: (msg: TitlebarMessage) => void;
|
||||
subscribe: (listener: Listener) => () => void;
|
||||
};
|
||||
|
||||
export const TitlebarContext = createContext<TitlebarContextValue>({
|
||||
sendEvent() {
|
||||
throw new Error('TitlebarContext not initialized');
|
||||
},
|
||||
subscribe() {
|
||||
throw new Error('TitlebarContext not initialized');
|
||||
},
|
||||
});
|
||||
|
||||
type TitlebarProviderProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function TitlebarProvider({ children }: TitlebarProviderProps) {
|
||||
const listeners = useRef<Listener[]>([]);
|
||||
|
||||
function sendEvent(msg: TitlebarMessage) {
|
||||
listeners.current.forEach(func => func(msg));
|
||||
}
|
||||
|
||||
function subscribe(listener: Listener) {
|
||||
listeners.current.push(listener);
|
||||
return () =>
|
||||
(listeners.current = listeners.current.filter(func => func !== listener));
|
||||
}
|
||||
|
||||
return (
|
||||
<TitlebarContext.Provider value={{ sendEvent, subscribe }}>
|
||||
{children}
|
||||
</TitlebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function UncategorizedButton() {
|
||||
const count: number | null = useSheetValue(queries.uncategorizedCount());
|
||||
if (count === null || count <= 0) {
|
||||
@@ -120,15 +61,27 @@ type PrivacyButtonProps = {
|
||||
|
||||
function PrivacyButton({ style }: PrivacyButtonProps) {
|
||||
const [isPrivacyEnabled, setPrivacyEnabledPref] =
|
||||
useLocalPref('isPrivacyEnabled');
|
||||
useSyncedPref('isPrivacyEnabled');
|
||||
|
||||
const privacyIconStyle = { width: 15, height: 15 };
|
||||
|
||||
useHotkeys(
|
||||
'shift+ctrl+p, shift+cmd+p, shift+meta+p',
|
||||
() => {
|
||||
setPrivacyEnabledPref(!isPrivacyEnabled);
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
scopes: ['app'],
|
||||
},
|
||||
[setPrivacyEnabledPref, isPrivacyEnabled],
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="bare"
|
||||
variant="bare"
|
||||
aria-label={`${isPrivacyEnabled ? 'Disable' : 'Enable'} privacy mode`}
|
||||
onClick={() => setPrivacyEnabledPref(!isPrivacyEnabled)}
|
||||
onPress={() => setPrivacyEnabledPref(!isPrivacyEnabled)}
|
||||
style={style}
|
||||
>
|
||||
{isPrivacyEnabled ? (
|
||||
@@ -145,7 +98,7 @@ type SyncButtonProps = {
|
||||
isMobile?: boolean;
|
||||
};
|
||||
function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
const [cloudFileId] = useLocalPref('cloudFileId');
|
||||
const [cloudFileId] = useMetadataPref('cloudFileId');
|
||||
const { sync } = useActions();
|
||||
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
@@ -244,10 +197,10 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="bare"
|
||||
variant="bare"
|
||||
aria-label="Sync"
|
||||
style={
|
||||
isMobile
|
||||
style={({ isHovered, isPressed }) => ({
|
||||
...(isMobile
|
||||
? {
|
||||
...style,
|
||||
WebkitAppRegion: 'none',
|
||||
@@ -257,11 +210,11 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
...style,
|
||||
WebkitAppRegion: 'none',
|
||||
color: desktopColor,
|
||||
}
|
||||
}
|
||||
hoveredStyle={hoveredStyle}
|
||||
activeStyle={activeStyle}
|
||||
onClick={sync}
|
||||
}),
|
||||
...(isHovered ? hoveredStyle : {}),
|
||||
...(isPressed ? activeStyle : {}),
|
||||
})}
|
||||
onPress={sync}
|
||||
>
|
||||
{isMobile ? (
|
||||
syncState === 'error' ? (
|
||||
@@ -287,31 +240,6 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
|
||||
function BudgetTitlebar() {
|
||||
const [maxMonths, setMaxMonthsPref] = useGlobalPref('maxMonths');
|
||||
const [budgetType] = useLocalPref('budgetType');
|
||||
const { sendEvent } = useContext(TitlebarContext);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
const reportBudgetEnabled = useFeatureFlag('reportBudget');
|
||||
|
||||
function onSwitchType() {
|
||||
setLoading(true);
|
||||
if (!loading) {
|
||||
const newBudgetType = budgetType === 'rollover' ? 'report' : 'rollover';
|
||||
sendEvent({
|
||||
type: SWITCH_BUDGET_MESSAGE_TYPE,
|
||||
payload: {
|
||||
newBudgetType,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [budgetType]);
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
@@ -319,61 +247,6 @@ function BudgetTitlebar() {
|
||||
maxMonths={maxMonths || 1}
|
||||
onChange={value => setMaxMonthsPref(value)}
|
||||
/>
|
||||
{reportBudgetEnabled && (
|
||||
<View style={{ marginLeft: -5 }}>
|
||||
<ButtonWithLoading
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
loading={loading}
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
padding: '4px 7px',
|
||||
}}
|
||||
title="Learn more about budgeting"
|
||||
onClick={() => setShowPopover(true)}
|
||||
>
|
||||
{budgetType === 'report' ? 'Report budget' : 'Rollover budget'}
|
||||
</ButtonWithLoading>
|
||||
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
placement="bottom start"
|
||||
isOpen={showPopover}
|
||||
onOpenChange={() => setShowPopover(false)}
|
||||
style={{
|
||||
padding: 10,
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
<Paragraph>
|
||||
You are currently using a{' '}
|
||||
<Text style={{ fontWeight: 600 }}>
|
||||
{budgetType === 'report' ? 'Report budget' : 'Rollover budget'}.
|
||||
</Text>{' '}
|
||||
Switching will not lose any data and you can always switch back.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<ButtonWithLoading
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={onSwitchType}
|
||||
>
|
||||
Switch to a{' '}
|
||||
{budgetType === 'report' ? 'Rollover budget' : 'Report budget'}
|
||||
</ButtonWithLoading>
|
||||
</Paragraph>
|
||||
<Paragraph isLast={true}>
|
||||
<Link
|
||||
variant="external"
|
||||
to="https://actualbudget.org/docs/experimental/report-budget"
|
||||
linkColor="muted"
|
||||
>
|
||||
How do these types of budgeting work?
|
||||
</Link>
|
||||
</Paragraph>
|
||||
</Popover>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -409,19 +282,15 @@ export function Titlebar({ style }: TitlebarProps) {
|
||||
>
|
||||
{(floatingSidebar || sidebar.alwaysFloats) && (
|
||||
<Button
|
||||
type="bare"
|
||||
aria-label="Sidebar menu"
|
||||
variant="bare"
|
||||
style={{ marginRight: 8 }}
|
||||
onPointerEnter={e => {
|
||||
onHoverStart={e => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
sidebar.setHidden(false);
|
||||
}
|
||||
}}
|
||||
onPointerLeave={e => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
sidebar.setHidden(true);
|
||||
}
|
||||
}}
|
||||
onPointerUp={e => {
|
||||
onPress={e => {
|
||||
if (e.pointerType !== 'mouse') {
|
||||
sidebar.setHidden(!sidebar.hidden);
|
||||
}
|
||||
@@ -439,7 +308,7 @@ export function Titlebar({ style }: TitlebarProps) {
|
||||
path="/accounts"
|
||||
element={
|
||||
location.state?.goBack ? (
|
||||
<Button type="bare" onClick={() => navigate(-1)}>
|
||||
<Button variant="bare" onPress={() => navigate(-1)}>
|
||||
<SvgArrowLeft
|
||||
width={10}
|
||||
height={10}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useActions } from '../hooks/useActions';
|
||||
import { SvgClose } from '../icons/v1';
|
||||
import { theme } from '../style';
|
||||
|
||||
import { Button } from './common/Button';
|
||||
import { Button } from './common/Button2';
|
||||
import { Link } from './common/Link';
|
||||
import { Text } from './common/Text';
|
||||
import { View } from './common/View';
|
||||
@@ -72,10 +72,10 @@ export function UpdateNotification() {
|
||||
</Link>
|
||||
)
|
||||
<Button
|
||||
type="bare"
|
||||
variant="bare"
|
||||
aria-label="Close"
|
||||
style={{ display: 'inline', padding: '1px 7px 2px 7px' }}
|
||||
onClick={() => {
|
||||
onPress={() => {
|
||||
// Set a flag to never show an update notification again for this session
|
||||
setAppState({
|
||||
updateInfo: null,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { useActions } from '../../hooks/useActions';
|
||||
import { SvgExclamationOutline } from '../../icons/v1';
|
||||
import { theme } from '../../style';
|
||||
import { Button } from '../common/Button';
|
||||
import { Button } from '../common/Button2';
|
||||
import { Link } from '../common/Link';
|
||||
import { Popover } from '../common/Popover';
|
||||
import { View } from '../common/View';
|
||||
@@ -39,7 +39,15 @@ function getErrorMessage(type, code) {
|
||||
return 'Your SimpleFIN Access Token is no longer valid. Please reset and generate a new token.';
|
||||
|
||||
case 'ACCOUNT_NEEDS_ATTENTION':
|
||||
return 'The account needs your attention at [SimpleFIN](https://beta-bridge.simplefin.org/auth/login).';
|
||||
return (
|
||||
<>
|
||||
The account needs your attention at{' '}
|
||||
<Link variant="external" to="https://bridge.simplefin.org/auth/login">
|
||||
SimpleFIN
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
}
|
||||
@@ -94,7 +102,7 @@ export function AccountSyncCheck() {
|
||||
<View>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
variant="bare"
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -103,7 +111,7 @@ export function AccountSyncCheck() {
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
onClick={() => setOpen(true)}
|
||||
onPress={() => setOpen(true)}
|
||||
>
|
||||
<SvgExclamationOutline
|
||||
style={{ width: 14, height: 14, marginRight: 5 }}
|
||||
@@ -129,13 +137,18 @@ export function AccountSyncCheck() {
|
||||
<View style={{ justifyContent: 'flex-end', flexDirection: 'row' }}>
|
||||
{showAuth ? (
|
||||
<>
|
||||
<Button onClick={unlink}>Unlink</Button>
|
||||
<Button type="primary" onClick={reauth} style={{ marginLeft: 5 }}>
|
||||
<Button onPress={unlink}>Unlink</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
autoFocus
|
||||
onPress={reauth}
|
||||
style={{ marginLeft: 5 }}
|
||||
>
|
||||
Reauthorize
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={unlink}>Unlink account</Button>
|
||||
<Button onPress={unlink}>Unlink account</Button>
|
||||
)}
|
||||
</View>
|
||||
</Popover>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { useHover } from 'usehooks-ts';
|
||||
|
||||
import { isPreviewId } from 'loot-core/shared/transactions';
|
||||
import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
||||
@@ -8,7 +10,7 @@ import { getScheduledAmount } from 'loot-core/src/shared/schedules';
|
||||
import { useSelectedItems } from '../../hooks/useSelected';
|
||||
import { SvgArrowButtonRight1 } from '../../icons/v2';
|
||||
import { theme } from '../../style';
|
||||
import { Button } from '../common/Button';
|
||||
import { Button } from '../common/Button2';
|
||||
import { Text } from '../common/Text';
|
||||
import { View } from '../common/View';
|
||||
import { PrivacyFilter } from '../PrivacyFilter';
|
||||
@@ -137,10 +139,12 @@ export function Balances({
|
||||
showExtraBalances,
|
||||
onToggleExtraBalances,
|
||||
account,
|
||||
filteredItems,
|
||||
isFiltered,
|
||||
filteredAmount,
|
||||
}) {
|
||||
const selectedItems = useSelectedItems();
|
||||
const buttonRef = useRef(null);
|
||||
const isButtonHovered = useHover(buttonRef);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -152,14 +156,11 @@ export function Balances({
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
data-testid="account-balance"
|
||||
type="bare"
|
||||
onClick={onToggleExtraBalances}
|
||||
variant="bare"
|
||||
onPress={onToggleExtraBalances}
|
||||
style={{
|
||||
'& svg': {
|
||||
opacity: selectedItems.size > 0 || showExtraBalances ? 1 : 0,
|
||||
},
|
||||
'&:hover svg': { opacity: 1 },
|
||||
paddingTop: 1,
|
||||
paddingBottom: 1,
|
||||
}}
|
||||
@@ -188,6 +189,10 @@ export function Balances({
|
||||
marginLeft: 10,
|
||||
color: theme.pillText,
|
||||
transform: showExtraBalances ? 'rotateZ(180deg)' : 'rotateZ(0)',
|
||||
opacity:
|
||||
isButtonHovered || selectedItems.size > 0 || showExtraBalances
|
||||
? 1
|
||||
: 0,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
@@ -196,9 +201,7 @@ export function Balances({
|
||||
{selectedItems.size > 0 && (
|
||||
<SelectedBalance selectedItems={selectedItems} account={account} />
|
||||
)}
|
||||
{filteredItems.length > 0 && (
|
||||
<FilteredBalance filteredAmount={filteredAmount} />
|
||||
)}
|
||||
{isFiltered && <FilteredBalance filteredAmount={filteredAmount} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from '../../icons/v2';
|
||||
import { theme, styles } from '../../style';
|
||||
import { AnimatedRefresh } from '../AnimatedRefresh';
|
||||
import { Button } from '../common/Button';
|
||||
import { Button } from '../common/Button2';
|
||||
import { InitialFocus } from '../common/InitialFocus';
|
||||
import { Input } from '../common/Input';
|
||||
import { Menu } from '../common/Menu';
|
||||
@@ -26,13 +26,12 @@ import { View } from '../common/View';
|
||||
import { FilterButton } from '../filters/FiltersMenu';
|
||||
import { FiltersStack } from '../filters/FiltersStack';
|
||||
import { NotesButton } from '../NotesButton';
|
||||
import { SelectedTransactionsButton } from '../transactions/SelectedTransactions';
|
||||
import { SelectedTransactionsButton } from '../transactions/SelectedTransactionsButton';
|
||||
|
||||
import { Balances } from './Balance';
|
||||
import { ReconcilingMessage, ReconcileMenu } from './Reconcile';
|
||||
|
||||
export function AccountHeader({
|
||||
filteredAmount,
|
||||
tableRef,
|
||||
editingName,
|
||||
isNameEditable,
|
||||
@@ -40,7 +39,7 @@ export function AccountHeader({
|
||||
accountName,
|
||||
account,
|
||||
filterId,
|
||||
filtersList,
|
||||
savedFilters,
|
||||
accountsSyncing,
|
||||
failedAccounts,
|
||||
accounts,
|
||||
@@ -53,10 +52,12 @@ export function AccountHeader({
|
||||
balanceQuery,
|
||||
reconcileAmount,
|
||||
canCalculateBalance,
|
||||
isFiltered,
|
||||
filteredAmount,
|
||||
isSorted,
|
||||
search,
|
||||
filters,
|
||||
conditionsOp,
|
||||
filterConditions,
|
||||
filterConditionsOp,
|
||||
pushModal,
|
||||
onSearch,
|
||||
onAddTransaction,
|
||||
@@ -73,16 +74,19 @@ export function AccountHeader({
|
||||
onBatchDelete,
|
||||
onBatchDuplicate,
|
||||
onBatchEdit,
|
||||
onBatchUnlink,
|
||||
onBatchLinkSchedule,
|
||||
onBatchUnlinkSchedule,
|
||||
onCreateRule,
|
||||
onApplyFilter,
|
||||
onUpdateFilter,
|
||||
onClearFilters,
|
||||
onReloadSavedFilter,
|
||||
onCondOpChange,
|
||||
onConditionsOpChange,
|
||||
onDeleteFilter,
|
||||
onScheduleAction,
|
||||
onSetTransfer,
|
||||
onMakeAsSplitTransaction,
|
||||
onMakeAsNonSplitTransactions,
|
||||
}) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const searchInput = useRef(null);
|
||||
@@ -127,6 +131,33 @@ export function AccountHeader({
|
||||
},
|
||||
[searchInput],
|
||||
);
|
||||
useHotkeys(
|
||||
't',
|
||||
() => onAddTransaction(),
|
||||
{
|
||||
preventDefault: true,
|
||||
scopes: ['app'],
|
||||
},
|
||||
[onAddTransaction],
|
||||
);
|
||||
useHotkeys(
|
||||
'ctrl+i, cmd+i, meta+i',
|
||||
() => onImport(),
|
||||
{
|
||||
scopes: ['app'],
|
||||
},
|
||||
[onImport],
|
||||
);
|
||||
useHotkeys(
|
||||
'ctrl+b, cmd+b, meta+b',
|
||||
() => onSync(),
|
||||
{
|
||||
enabled: canSync && !isServerOffline,
|
||||
preventDefault: true,
|
||||
scopes: ['app'],
|
||||
},
|
||||
[onSync],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -211,10 +242,10 @@ export function AccountHeader({
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="bare"
|
||||
variant="bare"
|
||||
aria-label="Edit account name"
|
||||
className="hover-visible"
|
||||
onClick={() => onExposeName(true)}
|
||||
onPress={() => onExposeName(true)}
|
||||
>
|
||||
<SvgPencil1
|
||||
style={{
|
||||
@@ -243,7 +274,7 @@ export function AccountHeader({
|
||||
showExtraBalances={showExtraBalances}
|
||||
onToggleExtraBalances={onToggleExtraBalances}
|
||||
account={account}
|
||||
filteredItems={filters}
|
||||
isFiltered={isFiltered}
|
||||
filteredAmount={filteredAmount}
|
||||
/>
|
||||
|
||||
@@ -255,9 +286,9 @@ export function AccountHeader({
|
||||
>
|
||||
{((account && !account.closed) || canSync) && (
|
||||
<Button
|
||||
type="bare"
|
||||
onClick={canSync ? onSync : onImport}
|
||||
disabled={canSync && isServerOffline}
|
||||
variant="bare"
|
||||
onPress={canSync ? onSync : onImport}
|
||||
isDisabled={canSync && isServerOffline}
|
||||
>
|
||||
{canSync ? (
|
||||
<>
|
||||
@@ -286,7 +317,7 @@ export function AccountHeader({
|
||||
</Button>
|
||||
)}
|
||||
{!showEmptyMessage && (
|
||||
<Button type="bare" onClick={onAddTransaction}>
|
||||
<Button variant="bare" onPress={onAddTransaction}>
|
||||
<SvgAdd width={10} height={10} style={{ marginRight: 3 }} /> Add
|
||||
New
|
||||
</Button>
|
||||
@@ -307,39 +338,55 @@ export function AccountHeader({
|
||||
</View>
|
||||
) : (
|
||||
<SelectedTransactionsButton
|
||||
account={account}
|
||||
getTransaction={id => transactions.find(t => t.id === id)}
|
||||
onShow={onShowTransactions}
|
||||
onDuplicate={onBatchDuplicate}
|
||||
onDelete={onBatchDelete}
|
||||
onEdit={onBatchEdit}
|
||||
onUnlink={onBatchUnlink}
|
||||
onLinkSchedule={onBatchLinkSchedule}
|
||||
onUnlinkSchedule={onBatchUnlinkSchedule}
|
||||
onCreateRule={onCreateRule}
|
||||
onSetTransfer={onSetTransfer}
|
||||
onScheduleAction={onScheduleAction}
|
||||
pushModal={pushModal}
|
||||
showMakeTransfer={showMakeTransfer}
|
||||
onMakeAsSplitTransaction={onMakeAsSplitTransaction}
|
||||
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="bare"
|
||||
disabled={search !== '' || filters.length > 0}
|
||||
style={{ padding: 6, marginLeft: 10 }}
|
||||
onClick={onToggleSplits}
|
||||
title={
|
||||
variant="bare"
|
||||
aria-label={
|
||||
splitsExpanded.state.mode === 'collapse'
|
||||
? 'Collapse split transactions'
|
||||
: 'Expand split transactions'
|
||||
}
|
||||
isDisabled={search !== '' || filterConditions.length > 0}
|
||||
style={{ padding: 6, marginLeft: 10 }}
|
||||
onPress={onToggleSplits}
|
||||
>
|
||||
{splitsExpanded.state.mode === 'collapse' ? (
|
||||
<SvgArrowsShrink3 style={{ width: 14, height: 14 }} />
|
||||
) : (
|
||||
<SvgArrowsExpand3 style={{ width: 14, height: 14 }} />
|
||||
)}
|
||||
<View
|
||||
title={
|
||||
splitsExpanded.state.mode === 'collapse'
|
||||
? 'Collapse split transactions'
|
||||
: 'Expand split transactions'
|
||||
}
|
||||
>
|
||||
{splitsExpanded.state.mode === 'collapse' ? (
|
||||
<SvgArrowsShrink3 style={{ width: 14, height: 14 }} />
|
||||
) : (
|
||||
<SvgArrowsExpand3 style={{ width: 14, height: 14 }} />
|
||||
)}
|
||||
</View>
|
||||
</Button>
|
||||
{account ? (
|
||||
<View>
|
||||
<MenuButton ref={triggerRef} onClick={() => setMenuOpen(true)} />
|
||||
<MenuButton
|
||||
aria-label="Account menu"
|
||||
ref={triggerRef}
|
||||
onPress={() => setMenuOpen(true)}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
@@ -366,7 +413,11 @@ export function AccountHeader({
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<MenuButton ref={triggerRef} onClick={() => setMenuOpen(true)} />
|
||||
<MenuButton
|
||||
aria-label="Account menu"
|
||||
ref={triggerRef}
|
||||
onPress={() => setMenuOpen(true)}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
@@ -391,17 +442,17 @@ export function AccountHeader({
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{filters && filters.length > 0 && (
|
||||
{filterConditions?.length > 0 && (
|
||||
<FiltersStack
|
||||
filters={filters}
|
||||
conditionsOp={conditionsOp}
|
||||
conditions={filterConditions}
|
||||
conditionsOp={filterConditionsOp}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onDeleteFilter={onDeleteFilter}
|
||||
onClearFilters={onClearFilters}
|
||||
onReloadSavedFilter={onReloadSavedFilter}
|
||||
filterId={filterId}
|
||||
filtersList={filtersList}
|
||||
onCondOpChange={onCondOpChange}
|
||||
savedFilters={savedFilters}
|
||||
onConditionsOpChange={onConditionsOpChange}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||