Compare commits
630 Commits
v23.10.0
...
react-aria
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58b57aefe1 | ||
|
|
e25683f130 | ||
|
|
496c76c7f9 | ||
|
|
b7d4964539 | ||
|
|
7479df359a | ||
|
|
b1b14d0813 | ||
|
|
b710b9675e | ||
|
|
f8fb4a9ba7 | ||
|
|
9f738956d7 | ||
|
|
dc9ce974a5 | ||
|
|
27974c63fd | ||
|
|
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 | ||
|
|
7f658691bb | ||
|
|
5b1a730f11 | ||
|
|
0c14eb17c4 | ||
|
|
7bb0425c81 | ||
|
|
8832c2b234 | ||
|
|
437e202d27 | ||
|
|
d34f5eccb6 | ||
|
|
f1d3902e3e | ||
|
|
8b6ef7b325 | ||
|
|
6ad0b47c7c | ||
|
|
96964224f4 | ||
|
|
0ed5e3ebe6 | ||
|
|
64cd6ee3c9 | ||
|
|
abc4636662 | ||
|
|
ade25b3304 | ||
|
|
b192ad955e | ||
|
|
e9da476b51 | ||
|
|
12719e3049 | ||
|
|
e178d9914d | ||
|
|
6fd728aa2d | ||
|
|
bf26ca4eb9 | ||
|
|
f606d92c5c | ||
|
|
8b850f1410 | ||
|
|
c992e340ca | ||
|
|
06f9db06b0 | ||
|
|
2b96bb3d52 | ||
|
|
ebb9452b8f | ||
|
|
196f03b84e | ||
|
|
93e784a0fe | ||
|
|
026194e5e2 | ||
|
|
98341b440a | ||
|
|
417f5805a8 | ||
|
|
094f0b8a91 | ||
|
|
b89a32025a | ||
|
|
d62919a357 | ||
|
|
1c7d9bf141 | ||
|
|
6d117f44de | ||
|
|
64821e6a64 | ||
|
|
e7c6611c88 | ||
|
|
6220aadb2d | ||
|
|
2959054d0c | ||
|
|
7d960579f9 | ||
|
|
5fd1d05670 | ||
|
|
0e86dea544 | ||
|
|
c92266fd7f | ||
|
|
08aff05a06 | ||
|
|
1195af76a6 | ||
|
|
e4bc0caa51 | ||
|
|
c3783b6498 | ||
|
|
55ef74eabc | ||
|
|
9667e1c269 | ||
|
|
b803731bc5 | ||
|
|
8415e7cddc | ||
|
|
5f16349a19 | ||
|
|
aff5bba4ec | ||
|
|
d79b8c6cb2 | ||
|
|
bbc123c3b8 | ||
|
|
04273d8064 | ||
|
|
3f1fd55a7b | ||
|
|
bfda42a2be | ||
|
|
201f0fd336 | ||
|
|
99ab8db99f | ||
|
|
f604fdaf36 | ||
|
|
c311d4a8df | ||
|
|
1fc7c9974b | ||
|
|
e63fb46c8f | ||
|
|
2901b5e5d0 | ||
|
|
df7ad22377 | ||
|
|
b11a3dc267 | ||
|
|
5a77051a72 | ||
|
|
f8352808c4 | ||
|
|
e5933ad8cd | ||
|
|
e3b4c3f559 | ||
|
|
2d1abf477c | ||
|
|
392f8085c1 | ||
|
|
6021d4ab0b | ||
|
|
f1f1ba87e2 | ||
|
|
7cee3b86ce | ||
|
|
9acea32f3a | ||
|
|
44770a36c7 | ||
|
|
3e03718f13 | ||
|
|
9c3075f60f | ||
|
|
7e04226b34 | ||
|
|
b0f55fae38 | ||
|
|
7c658da15d | ||
|
|
ddddec029f | ||
|
|
98d4cf450e | ||
|
|
243703b2f7 | ||
|
|
e917c96407 | ||
|
|
8a70d2464d | ||
|
|
dcb25bb5e6 | ||
|
|
f20e9c6bdb | ||
|
|
f73f6c7f0d | ||
|
|
37457cd6cd | ||
|
|
c16f4c71c5 | ||
|
|
452ca82287 | ||
|
|
ffddd9e8a5 | ||
|
|
2178da0414 | ||
|
|
f8809df59c | ||
|
|
04bc0c3c64 | ||
|
|
0aa6a057b1 | ||
|
|
b34aaab5f5 | ||
|
|
688bfe59e8 | ||
|
|
8f543a29e0 | ||
|
|
ad73a404c4 | ||
|
|
4ed9f4fff4 | ||
|
|
176c8466a4 | ||
|
|
703c319f7f | ||
|
|
ca8a8174c4 | ||
|
|
f8af8f95ac | ||
|
|
98a7aac736 | ||
|
|
d49f907f53 | ||
|
|
164dd399b0 | ||
|
|
f41d3f22c7 | ||
|
|
469c789c14 | ||
|
|
6ec3728280 | ||
|
|
c8cc479358 | ||
|
|
51ad488ce2 | ||
|
|
985411d48f | ||
|
|
53b5f3a0fc | ||
|
|
3ec4ef71c6 | ||
|
|
5df02c19dc | ||
|
|
464c9de6fb | ||
|
|
aad609f427 | ||
|
|
7313cf722d | ||
|
|
978a658c1e | ||
|
|
cdad43ff32 | ||
|
|
4c29afd424 | ||
|
|
c98a21b030 | ||
|
|
f5fde34952 | ||
|
|
59f854c075 | ||
|
|
85affae70f | ||
|
|
57d4cc57cd | ||
|
|
7d892d8164 | ||
|
|
84fbc7e464 | ||
|
|
5f77b87b07 | ||
|
|
97ec8f8d3a | ||
|
|
407ad4deee | ||
|
|
1c04aeae39 | ||
|
|
02e7d036d5 | ||
|
|
16ef674910 | ||
|
|
19bcfbe6e9 | ||
|
|
a2c1c2dea6 | ||
|
|
e9b5bfcc53 | ||
|
|
672dd5ea4b | ||
|
|
b101f9989b | ||
|
|
cf9b8c357e | ||
|
|
c71e1d2e8a | ||
|
|
1e462714e4 | ||
|
|
d9f55460dd | ||
|
|
77a670bbc3 | ||
|
|
1a1fe9ac9d | ||
|
|
16df116d1d | ||
|
|
d7075ae551 | ||
|
|
e500cba7e9 | ||
|
|
2d188b3941 | ||
|
|
e88ea69801 | ||
|
|
9aeab0ff5b | ||
|
|
36c700d92d | ||
|
|
eed6105222 | ||
|
|
291e3a8d14 | ||
|
|
edd34b7903 | ||
|
|
345ea71eed | ||
|
|
d89a016ab1 | ||
|
|
b87951855b | ||
|
|
70e37c0119 | ||
|
|
f55bd860ba | ||
|
|
0f960df8cf | ||
|
|
915c562545 | ||
|
|
770d86258f | ||
|
|
a955fe2474 | ||
|
|
4e9130ac29 | ||
|
|
e94a5505d8 | ||
|
|
1ee2cbec1c | ||
|
|
da6b039f10 | ||
|
|
310cc04a2b | ||
|
|
6fce10aea0 | ||
|
|
97a664159b | ||
|
|
c2291d4b5d | ||
|
|
d6de90a296 | ||
|
|
90305965ba | ||
|
|
b655009477 | ||
|
|
2e600e52c7 | ||
|
|
b5f617dbe5 | ||
|
|
b3fc23201e | ||
|
|
6f251e6024 | ||
|
|
b356004f68 | ||
|
|
66dc593fa5 | ||
|
|
8b34ba3958 | ||
|
|
2df774d9f5 | ||
|
|
c96c957a34 | ||
|
|
f3d7641ba9 | ||
|
|
83fd85f059 | ||
|
|
308f8339ae | ||
|
|
9697795279 | ||
|
|
9b40610f26 | ||
|
|
2a1c452aac | ||
|
|
1dd1c188d6 | ||
|
|
8f634099e2 | ||
|
|
7ee48e4c1d | ||
|
|
270705b3cd | ||
|
|
9588a76109 | ||
|
|
ada9e7da31 | ||
|
|
38ffccb903 | ||
|
|
f5023a7c07 | ||
|
|
bc6a0f8e60 | ||
|
|
5ee7d336ef | ||
|
|
24e42daa51 | ||
|
|
4bafd13c55 | ||
|
|
afaee6bc16 | ||
|
|
e44fbb3847 | ||
|
|
ff70c654a2 | ||
|
|
586a26968c | ||
|
|
107acdb36b | ||
|
|
5343030800 | ||
|
|
d7635755f2 | ||
|
|
501c6a02cc | ||
|
|
6281cc751e | ||
|
|
dc7cab501e | ||
|
|
f51376a4a5 | ||
|
|
6f600a4fee | ||
|
|
c82a6dc5ef | ||
|
|
ab97a3fbcd | ||
|
|
8b15c8cc17 | ||
|
|
02ec279a64 | ||
|
|
ab8c3c018a | ||
|
|
8356640e45 | ||
|
|
1767a32b3d | ||
|
|
5bcfc71be6 | ||
|
|
998efb9447 | ||
|
|
dc159d71a2 | ||
|
|
7db7b5c400 | ||
|
|
823b426952 | ||
|
|
8827169bfa | ||
|
|
90eaf2ba17 | ||
|
|
a5fa0f3bb6 | ||
|
|
4570459d85 | ||
|
|
76cbd44c75 | ||
|
|
0e0d960cd4 | ||
|
|
98c17bd5e0 | ||
|
|
601c9aa7df | ||
|
|
3b77609159 | ||
|
|
66261641a0 | ||
|
|
4b034468e3 | ||
|
|
b700aee87d | ||
|
|
9fca85209f | ||
|
|
a9362cc6f9 | ||
|
|
40296dc876 | ||
|
|
ed1e0ceb30 | ||
|
|
55817b0e70 | ||
|
|
6d7d12138c | ||
|
|
52f1f79c01 | ||
|
|
991fc4f450 | ||
|
|
4a8c692d06 | ||
|
|
0609f47cc3 | ||
|
|
0d1e6f2ee7 | ||
|
|
8568aebdbb | ||
|
|
bfb7c1d213 | ||
|
|
e526555748 | ||
|
|
45c4b262a2 | ||
|
|
e1f805b9c9 | ||
|
|
d0c11cd3af | ||
|
|
0c5bce8baf | ||
|
|
e650e00cb8 | ||
|
|
422996f8a7 | ||
|
|
9cad57c607 | ||
|
|
2a0f8335ed | ||
|
|
08cbdab2a1 | ||
|
|
ec2de3b387 | ||
|
|
65372e86a5 | ||
|
|
53a61000a4 | ||
|
|
485902af6b | ||
|
|
5a67b7e822 | ||
|
|
b994a6a74a | ||
|
|
b1b266e83c | ||
|
|
58626c0026 | ||
|
|
45094daf2f | ||
|
|
2bb7b3c2ee | ||
|
|
d6f610a326 | ||
|
|
8a8113a648 | ||
|
|
029e2f09bf | ||
|
|
86007e392f | ||
|
|
4c4f2fd426 | ||
|
|
7a18827b1d | ||
|
|
77fd65b2e7 | ||
|
|
30f03e8079 | ||
|
|
6e16262b63 | ||
|
|
29a515f3fe | ||
|
|
a5ab1a8fae | ||
|
|
d37622162a | ||
|
|
e3a8366dd7 | ||
|
|
d5e49dde59 | ||
|
|
f5258e6ebe | ||
|
|
f4d80fad92 | ||
|
|
3324dd5fa0 | ||
|
|
14509d15df | ||
|
|
9b461c48c9 | ||
|
|
55f2d126b3 | ||
|
|
6ae2047ab8 | ||
|
|
9fdffcc8e9 | ||
|
|
3daff4381f | ||
|
|
5914469b11 | ||
|
|
39e7f2598b | ||
|
|
c8d326d24b | ||
|
|
d8639a2a71 | ||
|
|
734191424b | ||
|
|
5d4fcfde00 | ||
|
|
54d7e5460a | ||
|
|
43ebe9e0fd | ||
|
|
515bdf5a74 | ||
|
|
018714610a | ||
|
|
00ee165f8e | ||
|
|
68442ae9e6 | ||
|
|
b937bfae04 | ||
|
|
317e7f135e | ||
|
|
5adb083575 | ||
|
|
524bd4e9eb | ||
|
|
9dfd6ce34c | ||
|
|
5d28bc0e3b | ||
|
|
a4e97e0070 | ||
|
|
a6e38ad2ae | ||
|
|
bb0ae4ebc3 | ||
|
|
dd29e02c5c | ||
|
|
75186183eb | ||
|
|
83f13cbdc8 | ||
|
|
0045d9212e | ||
|
|
dd254c6c23 | ||
|
|
c66d6e00f5 | ||
|
|
ae3be13aa8 | ||
|
|
cd9d1fb674 | ||
|
|
edba670d3a | ||
|
|
fbb1a9647d | ||
|
|
06cf65497f | ||
|
|
62a0a0fedc | ||
|
|
106dce25dd | ||
|
|
ff11399180 | ||
|
|
363c9e4afb | ||
|
|
e745a4073b | ||
|
|
7d1a895b48 | ||
|
|
a8b42bcd50 | ||
|
|
f33bce41ea | ||
|
|
cdefe6133f | ||
|
|
44a5199a31 | ||
|
|
dccad902d6 | ||
|
|
b477f7c2f1 | ||
|
|
540c410957 | ||
|
|
e205344867 | ||
|
|
8b968579b1 | ||
|
|
6ce794ffcc | ||
|
|
d5359a96ca | ||
|
|
3eee0b11d2 | ||
|
|
e792afb1fd | ||
|
|
4fb55d0d70 | ||
|
|
b330991855 | ||
|
|
165ad45822 | ||
|
|
295917036b | ||
|
|
ef9a7cfe85 | ||
|
|
4ece4a7ff6 | ||
|
|
761b3c6a16 | ||
|
|
7ace8c52dd | ||
|
|
f6dd0ecdb9 | ||
|
|
97a4296d7c | ||
|
|
89698480a5 | ||
|
|
1a6db82cfb | ||
|
|
6ce502ea24 | ||
|
|
fd962a97b0 | ||
|
|
300ed824f0 | ||
|
|
caca2497ea | ||
|
|
33e778fe9f | ||
|
|
8c43c78fc7 | ||
|
|
e0d82fd4f9 | ||
|
|
b6462347a9 | ||
|
|
8bf0f8e5bf | ||
|
|
bc07235017 | ||
|
|
794476ac51 | ||
|
|
30bc216142 | ||
|
|
6f8d2574ab | ||
|
|
9262b46428 | ||
|
|
8e1c11e18e | ||
|
|
6b5ee3f774 | ||
|
|
02d3f96c20 | ||
|
|
829c83afb2 | ||
|
|
5d29585fb7 | ||
|
|
882fd9f5cd | ||
|
|
d203def230 | ||
|
|
4d7cfab8bc | ||
|
|
84a9269ae4 | ||
|
|
83d2472a55 | ||
|
|
458d556e51 | ||
|
|
d9aeb8db1e | ||
|
|
85c0352abb | ||
|
|
7a40a645e6 | ||
|
|
7e9921e9e5 | ||
|
|
02aff1acbe | ||
|
|
e0d66d3083 | ||
|
|
d999e466b7 | ||
|
|
e589163bb7 | ||
|
|
d2b86d100c | ||
|
|
d8996405c4 | ||
|
|
e548125907 | ||
|
|
edbcaba89b | ||
|
|
b39d106c04 | ||
|
|
4894118809 | ||
|
|
c6723da780 | ||
|
|
7a8c320560 | ||
|
|
6fbf0fdc10 | ||
|
|
efaefcfaa9 | ||
|
|
73d281b6ee | ||
|
|
03e943f383 | ||
|
|
5854dffd50 | ||
|
|
c727e3e980 | ||
|
|
b385c715ef | ||
|
|
77e550f028 | ||
|
|
9ea8e86bb3 | ||
|
|
273fa8cd59 | ||
|
|
d4c8b5fa16 | ||
|
|
e62e8ca24e | ||
|
|
4497fbcb10 | ||
|
|
8a15caf42d | ||
|
|
af9efa09f0 | ||
|
|
a5557ca032 | ||
|
|
0bf587bcf5 | ||
|
|
e11b65719e | ||
|
|
c09a85f340 | ||
|
|
f90fe04b2b | ||
|
|
ca55d9c85e | ||
|
|
4ada92071e | ||
|
|
e848946898 | ||
|
|
9da05543c3 | ||
|
|
8a721bf2e0 | ||
|
|
36914abcd4 | ||
|
|
cd2d186599 | ||
|
|
b3bcff094d | ||
|
|
7955fe7aed | ||
|
|
2741422c0a | ||
|
|
cde4551eb7 | ||
|
|
7174fccfe4 | ||
|
|
0303292a28 | ||
|
|
f95df06800 | ||
|
|
3680d45814 | ||
|
|
dc872647a9 | ||
|
|
7a45d467b7 | ||
|
|
79aa07ff1a | ||
|
|
c4ff099f26 | ||
|
|
ad2b577515 | ||
|
|
5f52801869 | ||
|
|
6bfd9586e0 | ||
|
|
f36713e0be | ||
|
|
8b20169b59 | ||
|
|
da03acc394 | ||
|
|
5d1f2d48ae | ||
|
|
c96782d7c1 | ||
|
|
5d61dfce68 | ||
|
|
5a81a25b7e | ||
|
|
29f323e721 | ||
|
|
c462604833 | ||
|
|
e48f36cd20 | ||
|
|
1ed84059d7 | ||
|
|
5d8b96a749 | ||
|
|
0920bdc3fa | ||
|
|
c3a9b4dda3 | ||
|
|
eab0068428 | ||
|
|
0d1b962b2f | ||
|
|
67b9143dfc | ||
|
|
e7f298e32a | ||
|
|
516f0c259f | ||
|
|
2c87c60328 | ||
|
|
b4fd6e86ed | ||
|
|
a3e71a8b49 | ||
|
|
aa9c992f2e | ||
|
|
ca498b19cc | ||
|
|
8be2389fd9 | ||
|
|
46988800ef | ||
|
|
881999bcfe | ||
|
|
9c124e8e44 | ||
|
|
3306963c54 | ||
|
|
baa474843a | ||
|
|
6ed1b58321 | ||
|
|
28266ed9a2 | ||
|
|
cd6bd9ece8 | ||
|
|
fb2f712c16 | ||
|
|
f58fae8d16 | ||
|
|
a10b10a87b | ||
|
|
47007232b8 | ||
|
|
4761e9ce2f | ||
|
|
849262c95e | ||
|
|
e6e184c412 | ||
|
|
9c6d9ecf0a | ||
|
|
65273127f5 | ||
|
|
b5530085bb | ||
|
|
9ec82d52c9 | ||
|
|
93922567fc | ||
|
|
7c0b7a2f17 | ||
|
|
e8055bbc35 | ||
|
|
7fdd9767ba | ||
|
|
1f105999af | ||
|
|
f970263c72 | ||
|
|
fecc9178b3 | ||
|
|
08c80b6f58 | ||
|
|
a3995582c4 | ||
|
|
5008eb022f | ||
|
|
353e7be31f | ||
|
|
30c7024663 | ||
|
|
22efe74ec8 | ||
|
|
5792b15cb5 | ||
|
|
168b79a178 | ||
|
|
7062f2a7d9 | ||
|
|
af666458d3 | ||
|
|
184b302273 | ||
|
|
3004f336da | ||
|
|
45bb7696c3 | ||
|
|
199a9f838d | ||
|
|
df5aa3186c | ||
|
|
1c68c3f964 | ||
|
|
a15f80e771 | ||
|
|
be18891957 | ||
|
|
a6dae398da | ||
|
|
d8c885bf4e | ||
|
|
f94d8aff9f | ||
|
|
a549cdf0e7 | ||
|
|
b7a2f16246 | ||
|
|
0ec88ecc24 | ||
|
|
39fa9f2097 | ||
|
|
9040cab600 | ||
|
|
e56cb4bc85 | ||
|
|
fc08baf7ae | ||
|
|
7325153da1 | ||
|
|
e12793e7eb | ||
|
|
5fe146ee0a | ||
|
|
0af2987cef | ||
|
|
e266093a4a | ||
|
|
19f0efb654 | ||
|
|
e0b2eab475 | ||
|
|
25f4e2a8b5 | ||
|
|
d338a73794 | ||
|
|
75f2bf8b1b | ||
|
|
eb54487d8e | ||
|
|
5cc6bad34b | ||
|
|
acf4456077 | ||
|
|
b45615e6fc | ||
|
|
38609ee25a | ||
|
|
cae2b9cd5a | ||
|
|
4cacc845c0 | ||
|
|
3dfbd23f96 | ||
|
|
21effa654d | ||
|
|
c33dc6fbad | ||
|
|
057caf127a | ||
|
|
767bc8ecb6 | ||
|
|
bdf5c45cda | ||
|
|
3dfe633428 | ||
|
|
efba86a72d | ||
|
|
316da144e1 | ||
|
|
d3ab8f9812 | ||
|
|
0ea7f1852b | ||
|
|
3c341fc583 | ||
|
|
de90504a6d | ||
|
|
f6e2d3b1f3 | ||
|
|
f1973d55c0 | ||
|
|
476070b0a7 |
@@ -1,5 +1,6 @@
|
||||
packages/api/app/bundle.api.js
|
||||
packages/api/dist
|
||||
packages/api/@types
|
||||
packages/api/migrations
|
||||
|
||||
packages/crdt/dist
|
||||
@@ -24,7 +25,3 @@ packages/import-ynab5/**/node_modules/*
|
||||
packages/loot-core/**/node_modules/*
|
||||
packages/loot-core/**/lib-dist/*
|
||||
packages/loot-core/**/proto/*
|
||||
|
||||
packages/node-libofx/libofx.*.js
|
||||
packages/node-libofx/libofx/
|
||||
packages/node-libofx/OpenSP-*/
|
||||
|
||||
141
.eslintrc.js
@@ -38,19 +38,27 @@ module.exports = {
|
||||
extends: [
|
||||
'react-app',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/typescript',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: { project: [path.join(__dirname, './tsconfig.json')] },
|
||||
reportUnusedDisableDirectives: true,
|
||||
globals: {
|
||||
globalThis: false,
|
||||
vi: true,
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
|
||||
// Note: base rule explicitly disabled in favor of the TS one
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
args: 'none',
|
||||
varsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^(_|React)',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
@@ -61,8 +69,16 @@ module.exports = {
|
||||
require('confusing-browser-globals').filter(g => g !== 'self'),
|
||||
),
|
||||
|
||||
'react/jsx-filename-extension': [
|
||||
'warn',
|
||||
{ extensions: ['.jsx', '.tsx'], allow: 'as-needed' },
|
||||
],
|
||||
'react/jsx-no-useless-fragment': 'warn',
|
||||
'react/self-closing-comp': 'warn',
|
||||
'react/no-unstable-nested-components': [
|
||||
'warn',
|
||||
{ allowAsProps: true, customValidators: ['formatter'] },
|
||||
],
|
||||
|
||||
'rulesdir/typography': 'warn',
|
||||
'rulesdir/prefer-if-statement': 'warn',
|
||||
@@ -75,16 +91,11 @@ module.exports = {
|
||||
'react/prop-types': 'off',
|
||||
|
||||
// TODO: re-enable these rules
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/no-children-prop': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
// 'react-hooks/exhaustive-deps': [
|
||||
// 'warn',
|
||||
// {
|
||||
// additionalHooks: 'useLiveQuery',
|
||||
// },
|
||||
// ],
|
||||
|
||||
'no-var': 'warn',
|
||||
'react/jsx-curly-brace-presence': 'warn',
|
||||
'object-shorthand': ['warn', 'properties'],
|
||||
|
||||
'import/extensions': [
|
||||
'warn',
|
||||
@@ -135,10 +146,9 @@ module.exports = {
|
||||
'Using default React import is discouraged, please use named exports directly instead.',
|
||||
},
|
||||
{
|
||||
// forbid <a> in favor of <LinkButton> or <ExternalLink>
|
||||
// forbid <a> in favor of <Link>
|
||||
selector: 'JSXOpeningElement[name.name="a"]',
|
||||
message:
|
||||
'Using <a> is discouraged, please use <LinkButton> or <ExternalLink> instead.',
|
||||
message: 'Using <a> is discouraged, please use <Link> instead.',
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': [
|
||||
@@ -146,11 +156,17 @@ module.exports = {
|
||||
{ patterns: [...restrictedImportPatterns, ...restrictedImportColors] },
|
||||
],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{ 'ts-ignore': 'allow-with-description' },
|
||||
],
|
||||
|
||||
// Rules disable during TS migration
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'prefer-const': 'off',
|
||||
'prefer-const': 'warn',
|
||||
'prefer-spread': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'import/no-default-export': 'warn',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -186,6 +202,26 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./packages/desktop-client/**/*'],
|
||||
excludedFiles: [
|
||||
'./packages/desktop-client/src/hooks/useNavigate.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['react-router-dom'],
|
||||
importNames: ['useNavigate'],
|
||||
message: 'Please use Actual’s useNavigate() hook instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./packages/loot-core/src/**/*'],
|
||||
rules: {
|
||||
@@ -224,11 +260,80 @@ module.exports = {
|
||||
'no-restricted-imports': ['off', { patterns: restrictedImportColors }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'./packages/api/migrations/*',
|
||||
'./packages/loot-core/migrations/*',
|
||||
],
|
||||
rules: {
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO: fix the issues in these files
|
||||
files: [
|
||||
'./packages/desktop-client/src/components/accounts/Account.jsx',
|
||||
'./packages/desktop-client/src/components/accounts/MobileAccount.jsx',
|
||||
'./packages/desktop-client/src/components/accounts/MobileAccounts.jsx',
|
||||
'./packages/desktop-client/src/components/App.tsx',
|
||||
'./packages/desktop-client/src/components/budget/BudgetCategories.jsx',
|
||||
'./packages/desktop-client/src/components/budget/BudgetSummaries.tsx',
|
||||
'./packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx',
|
||||
'./packages/desktop-client/src/components/budget/index.tsx',
|
||||
'./packages/desktop-client/src/components/budget/MobileBudget.tsx',
|
||||
'./packages/desktop-client/src/components/budget/rollover/HoldMenu.tsx',
|
||||
'./packages/desktop-client/src/components/budget/rollover/TransferMenu.tsx',
|
||||
'./packages/desktop-client/src/components/common/Menu.tsx',
|
||||
'./packages/desktop-client/src/components/FinancesApp.tsx',
|
||||
'./packages/desktop-client/src/components/GlobalKeys.ts',
|
||||
'./packages/desktop-client/src/components/LoggedInUser.tsx',
|
||||
'./packages/desktop-client/src/components/manager/ManagementApp.jsx',
|
||||
'./packages/desktop-client/src/components/manager/subscribe/common.tsx',
|
||||
'./packages/desktop-client/src/components/ManageRules.tsx',
|
||||
'./packages/desktop-client/src/components/mobile/MobileAmountInput.jsx',
|
||||
'./packages/desktop-client/src/components/mobile/MobileNavTabs.tsx',
|
||||
'./packages/desktop-client/src/components/Modals.tsx',
|
||||
'./packages/desktop-client/src/components/modals/EditRule.jsx',
|
||||
'./packages/desktop-client/src/components/modals/ImportTransactions.jsx',
|
||||
'./packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx',
|
||||
'./packages/desktop-client/src/components/Notifications.tsx',
|
||||
'./packages/desktop-client/src/components/payees/ManagePayees.jsx',
|
||||
'./packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx',
|
||||
'./packages/desktop-client/src/components/payees/PayeeTable.tsx',
|
||||
'./packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx',
|
||||
'./packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx',
|
||||
'./packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx',
|
||||
'./packages/desktop-client/src/components/reports/reports/CustomReport.jsx',
|
||||
'./packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx',
|
||||
'./packages/desktop-client/src/components/reports/SaveReportName.tsx',
|
||||
'./packages/desktop-client/src/components/reports/useReport.ts',
|
||||
'./packages/desktop-client/src/components/schedules/ScheduleDetails.jsx',
|
||||
'./packages/desktop-client/src/components/schedules/SchedulesTable.tsx',
|
||||
'./packages/desktop-client/src/components/select/DateSelect.tsx',
|
||||
'./packages/desktop-client/src/components/sidebar/Tools.tsx',
|
||||
'./packages/desktop-client/src/components/sort.tsx',
|
||||
'./packages/desktop-client/src/components/spreadsheet/useSheetValue.ts',
|
||||
'./packages/desktop-client/src/components/table.tsx',
|
||||
'./packages/desktop-client/src/components/Titlebar.tsx',
|
||||
'./packages/desktop-client/src/components/transactions/MobileTransaction.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/SelectedTransactions.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/TransactionList.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/TransactionsTable.jsx',
|
||||
'./packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx',
|
||||
'./packages/desktop-client/src/hooks/useAccounts.ts',
|
||||
'./packages/desktop-client/src/hooks/useCategories.ts',
|
||||
'./packages/desktop-client/src/hooks/usePayees.ts',
|
||||
'./packages/desktop-client/src/hooks/useProperFocus.tsx',
|
||||
'./packages/desktop-client/src/hooks/useSelected.tsx',
|
||||
'./packages/loot-core/src/client/query-hooks.tsx',
|
||||
],
|
||||
rules: {
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
|
||||
2
.gitattributes
vendored
@@ -16,4 +16,4 @@ yarn.lock text eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpg binary
|
||||
|
||||
1
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
||||
# Funding policies: https://actualbudget.org/docs/contributing/leadership/funding
|
||||
open_collective: actual
|
||||
github: actualbudget
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -8,6 +8,13 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please ensure you provide as much information as possible to better assist in confirming and identifying a fix for the bug.
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
attributes:
|
||||
value: |
|
||||
**IMPORTANT:** we use Github Issues only for BUG REPORTS and FEATURE REQUESTS. If you are looking for help/support - please reach out to the [community on Discord](https://discord.gg/pRYNYr4W5A). All non-bug and non-feature-request issues will be closed.
|
||||
|
||||
**Bank-sync problems (SimpleFin / GoCardless)?** Reach out via the [community Discord](https://discord.gg/pRYNYr4W5A) first and open an issue only if the community deems the issue to be a legitimate bug in Actual.
|
||||
- type: checkboxes
|
||||
id: existing-issue
|
||||
attributes:
|
||||
@@ -16,6 +23,8 @@ body:
|
||||
options:
|
||||
- label: 'I have searched and found no existing issue'
|
||||
required: true
|
||||
- label: 'I will be providing steps how to reproduce the bug (in most cases this will also mean uploading a demo budget file)'
|
||||
required: true
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -27,13 +36,6 @@ body:
|
||||
value: 'A bug happened!'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: errors-received
|
||||
attributes:
|
||||
label: 'What error did you receive?'
|
||||
description: 'If you received an error or a message on the screen, please provide that here.'
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
id: env-info
|
||||
attributes:
|
||||
@@ -47,6 +49,7 @@ body:
|
||||
- Locally via Yarn
|
||||
- Docker
|
||||
- Fly.io
|
||||
- Pikapods
|
||||
- NAS
|
||||
- Desktop App (Electron)
|
||||
- Other
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Bank-sync issues
|
||||
url: https://discord.gg/pRYNYr4W5A
|
||||
about: Is bank-sync not working? Returning too much or too few information? Reach out to the community on Discord.
|
||||
- name: Support
|
||||
url: https://discord.gg/pRYNYr4W5A
|
||||
about: Need help with something? Having troubles setting up? Or perhaps issues using the API? Reach out to the community on Discord.
|
||||
|
||||
4
.github/actions/setup/action.yml
vendored
@@ -4,11 +4,11 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.16.0
|
||||
- name: Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
|
||||
14
.github/workflows/build.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
api:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build API
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Create package tgz
|
||||
run: cd packages/api && yarn pack && mv package.tgz actual-api.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-api
|
||||
path: packages/api/actual-api.tgz
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
crdt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build CRDT
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Create package tgz
|
||||
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-crdt
|
||||
path: packages/crdt/actual-crdt.tgz
|
||||
@@ -53,18 +53,18 @@ jobs:
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: ./bin/package-browser
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-web
|
||||
path: packages/desktop-client/build
|
||||
- name: Upload Build Stats
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-stats
|
||||
path: packages/desktop-client/build-stats
|
||||
|
||||
10
.github/workflows/check.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Lint
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Typecheck
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Test
|
||||
@@ -40,8 +40,8 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19'
|
||||
- name: Check migrations
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
@@ -22,14 +22,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:javascript'
|
||||
|
||||
16
.github/workflows/e2e-test.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
outputs:
|
||||
netlify_url: ${{ steps.netlify.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Wait for Netlify build to finish
|
||||
@@ -31,38 +31,40 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.37.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run E2E Tests on Netlify URL
|
||||
run: yarn e2e
|
||||
env:
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
vrt:
|
||||
name: Visual regression
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.37.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
run: yarn vrt
|
||||
env:
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
74
.github/workflows/electron-master.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Electron Master
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# this is so the assets can be added to the release
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
run: |
|
||||
mkdir .venv
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python3 -m pip install setuptools
|
||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
run: |
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
run: ./bin/package-electron
|
||||
env:
|
||||
# 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: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
- 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/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
56
.github/workflows/electron-pr.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Electron
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
run: |
|
||||
mkdir .venv
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python3 -m pip install setuptools
|
||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
run: |
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
run: ./bin/package-electron
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
42
.github/workflows/electron.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Electron
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
run: ./bin/package-electron
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
@@ -24,8 +24,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# This is not a security concern because we have approved & merged the PR
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19'
|
||||
- name: Handle feature requests
|
||||
|
||||
43
.github/workflows/netlify-release.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Deploy Netlify Release
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Install Netlify
|
||||
run: npm install netlify-cli@17.10.1 -g
|
||||
|
||||
- name: Build Actual
|
||||
run: ./bin/package-browser
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: netlify_deploy
|
||||
run: |
|
||||
netlify deploy \
|
||||
--dir packages/desktop-client/build \
|
||||
--site ${{ secrets.NETLIFY_SITE_ID }} \
|
||||
--auth ${{ secrets.NETLIFY_API_TOKEN }} \
|
||||
--filter @actual-app/web \
|
||||
--prod
|
||||
2
.github/workflows/release-notes.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Check release notes
|
||||
if: startsWith(github.head_ref, 'release/') == false
|
||||
uses: actualbudget/actions/release-notes/check@main
|
||||
|
||||
14
.github/workflows/size-compare.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
echo "Build failed on PR branch or ${{github.base_ref}}"
|
||||
exit 1
|
||||
- name: Download build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
id: pr-build
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
path: base
|
||||
|
||||
- name: Download build artifact from PR
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
@@ -64,13 +64,17 @@ jobs:
|
||||
|
||||
- name: Strip content hashes from stats files
|
||||
run: |
|
||||
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./head/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./head/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./head/*.json
|
||||
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./base/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./base/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./base/*.json
|
||||
- uses: github/webpack-bundlesize-compare-action@v1.8.2
|
||||
- uses: twk3/rollup-size-compare-action@v1.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
current-stats-json-path: ./head/desktop-client-stats.json
|
||||
base-stats-json-path: ./base/desktop-client-stats.json
|
||||
current-stats-json-path: ./head/web-stats.json
|
||||
base-stats-json-path: ./base/web-stats.json
|
||||
title: desktop-client
|
||||
|
||||
- uses: github/webpack-bundlesize-compare-action@v1.8.2
|
||||
|
||||
39
.gitignore
vendored
@@ -1,28 +1,34 @@
|
||||
# Sample Data
|
||||
/data/*
|
||||
!data/.gitkeep
|
||||
/data2
|
||||
Actual-*
|
||||
**/xcuserdata/*
|
||||
export-2020-01-10.csv
|
||||
|
||||
# Secrets
|
||||
.secret-tokens
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
**/*.log
|
||||
|
||||
# JavaScript
|
||||
node_modules
|
||||
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
|
||||
node_modules
|
||||
.DS_Store
|
||||
lerna-debug.log
|
||||
Actual-*
|
||||
.#*
|
||||
**/xcuserdata/*
|
||||
.secret-tokens
|
||||
bundle.desktop.js
|
||||
bundle.desktop.js.map
|
||||
bundle.mobile.js
|
||||
bundle.mobile.js.map
|
||||
export-2020-01-10.csv
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
**/*.log
|
||||
|
||||
# Yarn
|
||||
.pnp.*
|
||||
@@ -35,3 +41,12 @@ export-2020-01-10.csv
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
|
||||
# Misc
|
||||
.#*
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
2
.secret-tokens.example
Normal file
@@ -0,0 +1,2 @@
|
||||
export APPLE_ID=example@email.com
|
||||
export APPLE_APP_SPECIFIC_PASSWORD=password
|
||||
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
873
.yarn/releases/yarn-3.5.1.cjs
vendored
893
.yarn/releases/yarn-4.0.2.cjs
vendored
Executable file
12
.yarnrc.yml
@@ -1,9 +1,7 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.5.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
||||
|
||||
@@ -10,4 +10,4 @@ if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
|
||||
yarn
|
||||
fi
|
||||
|
||||
yarn start:browser
|
||||
BROWSER=0 yarn start:browser
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
ROOT=`dirname $0`
|
||||
RELEASE=""
|
||||
RELEASE_NOTES="" # TODO: figure out automation for release notes when we start publishing electron versions
|
||||
CI=${CI:-false}
|
||||
|
||||
cd "$ROOT/.."
|
||||
@@ -39,7 +38,7 @@ yarn rebuild-electron
|
||||
|
||||
yarn workspace loot-core build:node
|
||||
|
||||
yarn workspace @actual-app/web build
|
||||
yarn workspace @actual-app/web build --mode=desktop
|
||||
|
||||
yarn workspace desktop-electron update-client
|
||||
|
||||
@@ -47,15 +46,13 @@ yarn workspace desktop-electron update-client
|
||||
cd packages/desktop-electron;
|
||||
yarn clean;
|
||||
|
||||
export npm_config_better_sqlite3_binary_host="https://static.actualbudget.com/prebuild/better-sqlite3"
|
||||
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build --publish always -c.releaseInfo.releaseNotes="$RELEASE_NOTES" --arm64 --x64
|
||||
yarn build --publish never --arm64 --x64
|
||||
|
||||
echo "\nCreated release with release notes \"$RELEASE_NOTES\""
|
||||
echo "\nCreated release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build --publish never --x64
|
||||
fi
|
||||
|
||||
32
bin/run-vrt
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
|
||||
# See here for more information: https://github.com/actualbudget/actual/tree/master/packages/desktop-client#visual-regression
|
||||
|
||||
if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
|
||||
yarn
|
||||
fi
|
||||
|
||||
E2E_START_URL="${E2E_START_URL:-https://localhost:3001}"
|
||||
VRT_ARGS=""
|
||||
|
||||
# Loop through all arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
--e2e-start-url)
|
||||
E2E_START_URL="$2"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
VRT_ARGS="$VRT_ARGS $1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
echo "Running VRT tests with the following parameters:"
|
||||
echo "E2E_START_URL: $E2E_START_URL"
|
||||
echo "VRT_ARGS: $VRT_ARGS"
|
||||
|
||||
docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash \
|
||||
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"
|
||||
@@ -8,6 +8,8 @@ services:
|
||||
actual-development:
|
||||
build: .
|
||||
image: actual-development
|
||||
environment:
|
||||
- HTTPS
|
||||
ports:
|
||||
- '3001:3001'
|
||||
volumes:
|
||||
|
||||
29
package.json
@@ -30,37 +30,42 @@
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"test": "yarn workspaces foreach --parallel --verbose run test",
|
||||
"test:debug": "yarn workspaces foreach --verbose run test",
|
||||
"e2e": "yarn workspaces foreach --parallel --verbose run e2e",
|
||||
"vrt": "yarn workspaces foreach --parallel --verbose run vrt",
|
||||
"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",
|
||||
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"lint": "eslint . --max-warnings 0 --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:verbose": "DEBUG=eslint:cli-engine eslint . --max-warnings 0",
|
||||
"typecheck": "yarn tsc",
|
||||
"typecheck": "yarn tsc && tsc-strict",
|
||||
"jq": "./node_modules/node-jq/bin/jq"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.37.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": "4.2.1",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"node-jq": "^4.0.1",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"prettier": "2.8.2",
|
||||
"react-refresh": "^0.14.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.2.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript": "^5.0.2"
|
||||
"typescript": "^5.0.2",
|
||||
"typescript-strict-plugin": "^2.2.2-beta.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"rollup": "4.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"packageManager": "yarn@3.5.1",
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"browserslist": [
|
||||
"electron 24.0",
|
||||
"defaults"
|
||||
|
||||
1
packages/api/.gitignore
vendored
@@ -2,3 +2,4 @@ app/bundle.api.js*
|
||||
app/stats.json
|
||||
migrations
|
||||
default-db.sqlite
|
||||
mocks/budgets/**/*
|
||||
|
||||
21
packages/api/__snapshots__/methods.test.ts.snap
Normal file
@@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`API setup and teardown successfully loads budget 1`] = `
|
||||
Array [
|
||||
"2016-10",
|
||||
"2016-11",
|
||||
"2016-12",
|
||||
"2017-01",
|
||||
"2017-02",
|
||||
"2017-03",
|
||||
"2017-04",
|
||||
"2017-05",
|
||||
"2017-06",
|
||||
"2017-07",
|
||||
"2017-08",
|
||||
"2017-09",
|
||||
"2017-10",
|
||||
"2017-11",
|
||||
"2017-12",
|
||||
]
|
||||
`;
|
||||
@@ -23,7 +23,7 @@ class Query {
|
||||
}
|
||||
|
||||
unfilter(exprs) {
|
||||
let exprSet = new Set(exprs);
|
||||
const exprSet = new Set(exprs);
|
||||
return new Query({
|
||||
...this.state,
|
||||
filterExpressions: this.state.filterExpressions.filter(
|
||||
@@ -37,13 +37,13 @@ class Query {
|
||||
exprs = [exprs];
|
||||
}
|
||||
|
||||
let query = new Query({ ...this.state, selectExpressions: exprs });
|
||||
const query = new Query({ ...this.state, selectExpressions: exprs });
|
||||
query.state.calculation = false;
|
||||
return query;
|
||||
}
|
||||
|
||||
calculate(expr) {
|
||||
let query = this.select({ result: expr });
|
||||
const query = this.select({ result: expr });
|
||||
query.state.calculation = true;
|
||||
return query;
|
||||
}
|
||||
@@ -99,6 +99,6 @@ class Query {
|
||||
}
|
||||
}
|
||||
|
||||
export default function q(table) {
|
||||
export function q(table) {
|
||||
return new Query({ table });
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
|
||||
// eslint-disable-next-line import/extensions
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
import * as injected from './injected';
|
||||
|
||||
let actualApp;
|
||||
export const internal = bundle.lib;
|
||||
|
||||
// DEPRECATED: remove the next line in @actual-app/api v7
|
||||
export * as methods from './methods';
|
||||
|
||||
export * from './methods';
|
||||
export * as utils from './utils';
|
||||
|
||||
export async function init(config = {}) {
|
||||
if (actualApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
global.fetch = (...args) =>
|
||||
import('node-fetch').then(({ default: fetch }) => fetch(...args));
|
||||
|
||||
await bundle.init(config);
|
||||
actualApp = bundle.lib;
|
||||
|
||||
injected.override(bundle.lib.send);
|
||||
return bundle.lib;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (actualApp) {
|
||||
await actualApp.send('sync');
|
||||
await actualApp.send('close-budget');
|
||||
actualApp = null;
|
||||
}
|
||||
}
|
||||
53
packages/api/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type {
|
||||
RequestInfo as FetchInfo,
|
||||
RequestInit as FetchInit,
|
||||
// @ts-ignore: false-positive commonjs module error on build until typescript 5.3
|
||||
} from 'node-fetch'; // with { 'resolution-mode': 'import' };
|
||||
|
||||
// loot-core types
|
||||
import type { InitConfig } from 'loot-core/server/main';
|
||||
|
||||
// @ts-ignore: bundle not available until we build it
|
||||
// eslint-disable-next-line import/extensions
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
import * as injected from './injected';
|
||||
import { validateNodeVersion } from './validateNodeVersion';
|
||||
|
||||
let actualApp: null | typeof bundle.lib;
|
||||
export const internal = bundle.lib;
|
||||
|
||||
// DEPRECATED: remove the next line in @actual-app/api v7
|
||||
export * as methods from './methods';
|
||||
|
||||
export * from './methods';
|
||||
export * as utils from './utils';
|
||||
|
||||
export async function init(config: InitConfig = {}) {
|
||||
if (actualApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateNodeVersion();
|
||||
|
||||
if (!globalThis.fetch) {
|
||||
globalThis.fetch = (url: URL | RequestInfo, init?: RequestInit) => {
|
||||
return import('node-fetch').then(({ default: fetch }) =>
|
||||
fetch(url as unknown as FetchInfo, init as unknown as FetchInit),
|
||||
) as unknown as Promise<Response>;
|
||||
};
|
||||
}
|
||||
|
||||
await bundle.init(config);
|
||||
actualApp = bundle.lib;
|
||||
|
||||
injected.override(bundle.lib.send);
|
||||
return bundle.lib;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (actualApp) {
|
||||
await actualApp.send('sync');
|
||||
await actualApp.send('close-budget');
|
||||
actualApp = null;
|
||||
}
|
||||
}
|
||||
24
packages/api/jest.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
moduleFileExtensions: [
|
||||
'testing.js',
|
||||
'testing.ts',
|
||||
'api.js',
|
||||
'api.ts',
|
||||
'api.tsx',
|
||||
'electron.js',
|
||||
'electron.ts',
|
||||
'mjs',
|
||||
'js',
|
||||
'ts',
|
||||
'tsx',
|
||||
'json',
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
watchPathIgnorePatterns: ['<rootDir>/mocks/budgets/'],
|
||||
setupFilesAfterEnv: ['<rootDir>/../loot-core/src/mocks/setup.ts'],
|
||||
transformIgnorePatterns: ['/node_modules/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
};
|
||||
@@ -1,155 +0,0 @@
|
||||
import * as injected from './injected';
|
||||
|
||||
export { default as q } from './app/query';
|
||||
|
||||
function send(name, args) {
|
||||
return injected.send(name, args);
|
||||
}
|
||||
|
||||
export async function runImport(name, func) {
|
||||
await send('api/start-import', { budgetName: name });
|
||||
try {
|
||||
await func();
|
||||
} catch (e) {
|
||||
await send('api/abort-import');
|
||||
throw e;
|
||||
}
|
||||
await send('api/finish-import');
|
||||
}
|
||||
|
||||
export async function loadBudget(budgetId) {
|
||||
return send('api/load-budget', { id: budgetId });
|
||||
}
|
||||
|
||||
export async function downloadBudget(syncId, { password } = {}) {
|
||||
return send('api/download-budget', { syncId, password });
|
||||
}
|
||||
|
||||
export async function sync() {
|
||||
return send('api/sync');
|
||||
}
|
||||
|
||||
export async function batchBudgetUpdates(func) {
|
||||
await send('api/batch-budget-start');
|
||||
try {
|
||||
await func();
|
||||
} finally {
|
||||
await send('api/batch-budget-end');
|
||||
}
|
||||
}
|
||||
|
||||
export function runQuery(query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function getBudgetMonths() {
|
||||
return send('api/budget-months');
|
||||
}
|
||||
|
||||
export function getBudgetMonth(month) {
|
||||
return send('api/budget-month', { month });
|
||||
}
|
||||
|
||||
export function setBudgetAmount(month, categoryId, value) {
|
||||
return send('api/budget-set-amount', { month, categoryId, amount: value });
|
||||
}
|
||||
|
||||
export function setBudgetCarryover(month, categoryId, flag) {
|
||||
return send('api/budget-set-carryover', { month, categoryId, flag });
|
||||
}
|
||||
|
||||
export function addTransactions(accountId, transactions) {
|
||||
return send('api/transactions-add', { accountId, transactions });
|
||||
}
|
||||
|
||||
export function importTransactions(accountId, transactions) {
|
||||
return send('api/transactions-import', { accountId, transactions });
|
||||
}
|
||||
|
||||
export function getTransactions(accountId, startDate, endDate) {
|
||||
return send('api/transactions-get', { accountId, startDate, endDate });
|
||||
}
|
||||
|
||||
export function filterTransactions(accountId, text) {
|
||||
return send('api/transactions-filter', { accountId, text });
|
||||
}
|
||||
|
||||
export function updateTransaction(id, fields) {
|
||||
return send('api/transaction-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteTransaction(id) {
|
||||
return send('api/transaction-delete', { id });
|
||||
}
|
||||
|
||||
export function getAccounts() {
|
||||
return send('api/accounts-get');
|
||||
}
|
||||
|
||||
export function createAccount(account, initialBalance) {
|
||||
return send('api/account-create', { account, initialBalance });
|
||||
}
|
||||
|
||||
export function updateAccount(id, fields) {
|
||||
return send('api/account-update', { id, fields });
|
||||
}
|
||||
|
||||
export function closeAccount(id, transferAccountId, transferCategoryId) {
|
||||
return send('api/account-close', {
|
||||
id,
|
||||
transferAccountId,
|
||||
transferCategoryId,
|
||||
});
|
||||
}
|
||||
|
||||
export function reopenAccount(id) {
|
||||
return send('api/account-reopen', { id });
|
||||
}
|
||||
|
||||
export function deleteAccount(id) {
|
||||
return send('api/account-delete', { id });
|
||||
}
|
||||
|
||||
export function createCategoryGroup(group) {
|
||||
return send('api/category-group-create', { group });
|
||||
}
|
||||
|
||||
export function updateCategoryGroup(id, fields) {
|
||||
return send('api/category-group-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteCategoryGroup(id, transferCategoryId) {
|
||||
return send('api/category-group-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getCategories() {
|
||||
return send('api/categories-get', { grouped: false });
|
||||
}
|
||||
|
||||
export function createCategory(category) {
|
||||
return send('api/category-create', { category });
|
||||
}
|
||||
|
||||
export function updateCategory(id, fields) {
|
||||
return send('api/category-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteCategory(id, transferCategoryId) {
|
||||
return send('api/category-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getPayees() {
|
||||
return send('api/payees-get');
|
||||
}
|
||||
|
||||
export function createPayee(payee) {
|
||||
return send('api/payee-create', { payee });
|
||||
}
|
||||
|
||||
export function updatePayee(id, fields) {
|
||||
return send('api/payee-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deletePayee(id) {
|
||||
return send('api/payee-delete', { id });
|
||||
}
|
||||
663
packages/api/methods.test.ts
Normal file
@@ -0,0 +1,663 @@
|
||||
// @ts-strict-ignore
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as api from './index';
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
|
||||
beforeEach(async () => {
|
||||
// we need real datetime if we are going to mix new timestamps with our mock data
|
||||
global.restoreDateNow();
|
||||
|
||||
const budgetPath = path.join(__dirname, '/mocks/budgets/', budgetName);
|
||||
await fs.rm(budgetPath, { force: true, recursive: true });
|
||||
|
||||
await createTestBudget('default-budget-template', budgetName);
|
||||
await api.init({
|
||||
dataDir: path.join(__dirname, '/mocks/budgets/'),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
global.currentMonth = null;
|
||||
await api.shutdown();
|
||||
});
|
||||
|
||||
async function createTestBudget(templateName: string, name: string) {
|
||||
const templatePath = path.join(
|
||||
__dirname,
|
||||
'/../loot-core/src/mocks/files',
|
||||
templateName,
|
||||
);
|
||||
const budgetPath = path.join(__dirname, '/mocks/budgets/', name);
|
||||
|
||||
await fs.mkdir(budgetPath);
|
||||
await fs.copyFile(
|
||||
path.join(templatePath, 'metadata.json'),
|
||||
path.join(budgetPath, 'metadata.json'),
|
||||
);
|
||||
await fs.copyFile(
|
||||
path.join(templatePath, 'db.sqlite'),
|
||||
path.join(budgetPath, 'db.sqlite'),
|
||||
);
|
||||
}
|
||||
|
||||
describe('API setup and teardown', () => {
|
||||
// apis: loadBudget, getBudgetMonths
|
||||
test('successfully loads budget', async () => {
|
||||
await expect(api.loadBudget(budgetName)).resolves.toBeUndefined();
|
||||
|
||||
await expect(api.getBudgetMonths()).resolves.toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API CRUD operations', () => {
|
||||
beforeEach(async () => {
|
||||
// load test budget
|
||||
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';
|
||||
global.currentMonth = month;
|
||||
|
||||
// get existing category groups
|
||||
const groups = await api.getCategoryGroups();
|
||||
expect(groups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
hidden: 0,
|
||||
id: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
is_income: 0,
|
||||
name: 'Usual Expenses',
|
||||
sort_order: 16384,
|
||||
tombstone: 0,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
hidden: 0,
|
||||
id: 'a137772f-cf2f-4089-9432-822d2ddc1466',
|
||||
is_income: 0,
|
||||
name: 'Investments and Savings',
|
||||
sort_order: 32768,
|
||||
tombstone: 0,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
hidden: 0,
|
||||
id: '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00',
|
||||
is_income: 1,
|
||||
name: 'Income',
|
||||
sort_order: 32768,
|
||||
tombstone: 0,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// create our test category group
|
||||
const mainGroupId = await api.createCategoryGroup({
|
||||
name: 'test-group',
|
||||
});
|
||||
|
||||
let budgetMonth = await api.getBudgetMonth(month);
|
||||
expect(budgetMonth.categoryGroups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: mainGroupId,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// update group
|
||||
await api.updateCategoryGroup(mainGroupId, {
|
||||
name: 'update-tests',
|
||||
});
|
||||
|
||||
budgetMonth = await api.getBudgetMonth(month);
|
||||
expect(budgetMonth.categoryGroups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: mainGroupId,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// delete group
|
||||
await api.deleteCategoryGroup(mainGroupId);
|
||||
|
||||
budgetMonth = await api.getBudgetMonth(month);
|
||||
expect(budgetMonth.categoryGroups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
id: mainGroupId,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
// apis: createCategory, getCategories, updateCategory, deleteCategory
|
||||
test('Categories: successfully update categories', async () => {
|
||||
const month = '2023-10';
|
||||
global.currentMonth = month;
|
||||
|
||||
// create our test category group
|
||||
const mainGroupId = await api.createCategoryGroup({
|
||||
name: 'test-group',
|
||||
});
|
||||
const secondaryGroupId = await api.createCategoryGroup({
|
||||
name: 'test-secondary-group',
|
||||
});
|
||||
const categoryId = await api.createCategory({
|
||||
name: 'test-budget',
|
||||
group_id: mainGroupId,
|
||||
});
|
||||
const categoryIdHidden = await api.createCategory({
|
||||
name: 'test-budget-hidden',
|
||||
group_id: mainGroupId,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
let categories = await api.getCategories();
|
||||
expect(categories).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: categoryId,
|
||||
name: 'test-budget',
|
||||
hidden: false,
|
||||
group_id: mainGroupId,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: categoryIdHidden,
|
||||
name: 'test-budget-hidden',
|
||||
hidden: true,
|
||||
group_id: mainGroupId,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// update/move category
|
||||
await api.updateCategory(categoryId, {
|
||||
name: 'updated-budget',
|
||||
group_id: secondaryGroupId,
|
||||
});
|
||||
|
||||
await api.updateCategory(categoryIdHidden, {
|
||||
name: 'updated-budget-hidden',
|
||||
group_id: secondaryGroupId,
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
categories = await api.getCategories();
|
||||
expect(categories).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: categoryId,
|
||||
name: 'updated-budget',
|
||||
hidden: false,
|
||||
group_id: secondaryGroupId,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: categoryIdHidden,
|
||||
name: 'updated-budget-hidden',
|
||||
hidden: false,
|
||||
group_id: secondaryGroupId,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// delete categories
|
||||
await api.deleteCategory(categoryId);
|
||||
|
||||
expect(categories).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
id: categoryId,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
// apis: setBudgetAmount, setBudgetCarryover, getBudgetMonth
|
||||
test('Budgets: successfully update budgets', async () => {
|
||||
const month = '2023-10';
|
||||
global.currentMonth = month;
|
||||
|
||||
// create some new categories to test with
|
||||
const groupId = await api.createCategoryGroup({
|
||||
name: 'tests',
|
||||
});
|
||||
const categoryId = await api.createCategory({
|
||||
name: 'test-budget',
|
||||
group_id: groupId,
|
||||
});
|
||||
|
||||
await api.setBudgetAmount(month, categoryId, 100);
|
||||
await api.setBudgetCarryover(month, categoryId, true);
|
||||
|
||||
const budgetMonth = await api.getBudgetMonth(month);
|
||||
expect(budgetMonth.categoryGroups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: groupId,
|
||||
categories: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: categoryId,
|
||||
budgeted: 100,
|
||||
carryover: true,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
//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 },
|
||||
1000,
|
||||
);
|
||||
const accountId2 = await api.createAccount({ name: 'test-account2' }, 0);
|
||||
let accounts = await api.getAccounts();
|
||||
|
||||
// accounts successfully created
|
||||
expect(accounts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: accountId1,
|
||||
name: 'test-account1',
|
||||
offbudget: true,
|
||||
}),
|
||||
expect.objectContaining({ id: accountId2, name: 'test-account2' }),
|
||||
]),
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
// accounts successfully updated, and one of them deleted
|
||||
accounts = await api.getAccounts();
|
||||
expect(accounts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: accountId1,
|
||||
name: 'test-account1',
|
||||
closed: true,
|
||||
offbudget: false,
|
||||
}),
|
||||
expect.not.objectContaining({ id: accountId2 }),
|
||||
]),
|
||||
);
|
||||
|
||||
await api.reopenAccount(accountId1);
|
||||
|
||||
// the non-deleted account is reopened
|
||||
accounts = await api.getAccounts();
|
||||
expect(accounts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: accountId1,
|
||||
name: 'test-account1',
|
||||
closed: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
// apis: createPayee, getPayees, updatePayee, deletePayee
|
||||
test('Payees: successfully update payees', async () => {
|
||||
const payeeId1 = await api.createPayee({ name: 'test-payee1' });
|
||||
const payeeId2 = await api.createPayee({ name: 'test-payee2' });
|
||||
let payees = await api.getPayees();
|
||||
|
||||
// payees successfully created
|
||||
expect(payees).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: payeeId1,
|
||||
name: 'test-payee1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: payeeId2,
|
||||
name: 'test-payee2',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
await api.updatePayee(payeeId1, { name: 'test-updated-payee' });
|
||||
await api.deletePayee(payeeId2);
|
||||
|
||||
// confirm update and delete were successful
|
||||
payees = await api.getPayees();
|
||||
expect(payees).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: payeeId1,
|
||||
name: 'test-updated-payee',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
name: 'test-payee1',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
id: payeeId2,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
|
||||
test('Rules: successfully update rules', async () => {
|
||||
await api.createPayee({ name: 'test-payee' });
|
||||
await api.createPayee({ name: 'test-payee2' });
|
||||
|
||||
// create our test rules
|
||||
const rule = await api.createRule({
|
||||
stage: 'pre',
|
||||
conditionsOp: 'and',
|
||||
conditions: [
|
||||
{
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
value: 'test-payee',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
op: 'set',
|
||||
field: 'category',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
},
|
||||
],
|
||||
});
|
||||
const rule2 = await api.createRule({
|
||||
stage: 'pre',
|
||||
conditionsOp: 'and',
|
||||
conditions: [
|
||||
{
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
value: 'test-payee2',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
op: 'set',
|
||||
field: 'category',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// get existing rules
|
||||
const rules = await api.getRules();
|
||||
expect(rules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee2',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule2.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// get by payee
|
||||
expect(await api.getPayeeRules('test-payee')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(await api.getPayeeRules('test-payee2')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee2',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule2.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// update one rule
|
||||
const updatedRule = {
|
||||
...rule,
|
||||
stage: 'post',
|
||||
conditionsOp: 'or',
|
||||
};
|
||||
expect(await api.updateRule(updatedRule)).toEqual(updatedRule);
|
||||
|
||||
expect(await api.getRules()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'or',
|
||||
id: rule.id,
|
||||
stage: 'post',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee2',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule2.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// delete rules
|
||||
await api.deleteRule(rules[1]);
|
||||
expect(await api.getRules()).toHaveLength(1);
|
||||
|
||||
await api.deleteRule(rules[0]);
|
||||
expect(await api.getRules()).toHaveLength(0);
|
||||
});
|
||||
|
||||
// apis: addTransactions, getTransactions, importTransactions, updateTransaction, deleteTransaction
|
||||
test('Transactions: successfully update transactions', async () => {
|
||||
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
||||
|
||||
let newTransaction = [
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-11-03', imported_id: '12', amount: 100, notes: '' },
|
||||
];
|
||||
|
||||
const addResult = await api.addTransactions(accountId, newTransaction, {
|
||||
learnCategories: true,
|
||||
runTransfers: true,
|
||||
});
|
||||
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,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions).toEqual(
|
||||
expect.arrayContaining(
|
||||
newTransaction.map(trans => expect.objectContaining(trans)),
|
||||
),
|
||||
);
|
||||
expect(transactions).toHaveLength(2);
|
||||
|
||||
newTransaction = [
|
||||
{ date: '2023-12-03', imported_id: '11', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-12-03', imported_id: '12', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-12-03', imported_id: '22', amount: 200, notes: '' },
|
||||
];
|
||||
|
||||
const reconciled = await api.importTransactions(accountId, newTransaction);
|
||||
|
||||
// Expect it to reconcile and to have updated one of the previous transactions
|
||||
expect(reconciled.added).toHaveLength(1);
|
||||
expect(reconciled.updated).toHaveLength(1);
|
||||
|
||||
// confirm imported transactions exist
|
||||
transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-12-01',
|
||||
'2023-12-31',
|
||||
);
|
||||
expect(transactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ imported_id: '22', amount: 200 }),
|
||||
]),
|
||||
);
|
||||
expect(transactions).toHaveLength(1);
|
||||
|
||||
// confirm imported transactions update perfomed
|
||||
transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ notes: 'notes', amount: 100 }),
|
||||
]),
|
||||
);
|
||||
expect(transactions).toHaveLength(2);
|
||||
|
||||
const idToUpdate = reconciled.added[0];
|
||||
const idToDelete = reconciled.updated[0];
|
||||
await api.updateTransaction(idToUpdate, { amount: 500 });
|
||||
await api.deleteTransaction(idToDelete);
|
||||
|
||||
// confirm updates and deletions work
|
||||
transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-12-01',
|
||||
'2023-12-31',
|
||||
);
|
||||
expect(transactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: idToUpdate, amount: 500 }),
|
||||
expect.not.objectContaining({ id: idToDelete }),
|
||||
]),
|
||||
);
|
||||
expect(transactions).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
206
packages/api/methods.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// @ts-strict-ignore
|
||||
import type { Handlers } from 'loot-core/src/types/handlers';
|
||||
|
||||
import * as injected from './injected';
|
||||
|
||||
export { q } from './app/query';
|
||||
|
||||
function send<K extends keyof Handlers, T extends Handlers[K]>(
|
||||
name: K,
|
||||
args?: Parameters<T>[0],
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
return injected.send(name, args);
|
||||
}
|
||||
|
||||
export async function runImport(name, func) {
|
||||
await send('api/start-import', { budgetName: name });
|
||||
try {
|
||||
await func();
|
||||
} catch (e) {
|
||||
await send('api/abort-import');
|
||||
throw e;
|
||||
}
|
||||
await send('api/finish-import');
|
||||
}
|
||||
|
||||
export async function loadBudget(budgetId) {
|
||||
return send('api/load-budget', { id: budgetId });
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
export async function runBankSync(args?: { accountId: string }) {
|
||||
return send('api/bank-sync', args);
|
||||
}
|
||||
|
||||
export async function batchBudgetUpdates(func) {
|
||||
await send('api/batch-budget-start');
|
||||
try {
|
||||
await func();
|
||||
} finally {
|
||||
await send('api/batch-budget-end');
|
||||
}
|
||||
}
|
||||
|
||||
export function runQuery(query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function getBudgetMonths() {
|
||||
return send('api/budget-months');
|
||||
}
|
||||
|
||||
export function getBudgetMonth(month) {
|
||||
return send('api/budget-month', { month });
|
||||
}
|
||||
|
||||
export function setBudgetAmount(month, categoryId, value) {
|
||||
return send('api/budget-set-amount', { month, categoryId, amount: value });
|
||||
}
|
||||
|
||||
export function setBudgetCarryover(month, categoryId, flag) {
|
||||
return send('api/budget-set-carryover', { month, categoryId, flag });
|
||||
}
|
||||
|
||||
export function addTransactions(
|
||||
accountId,
|
||||
transactions,
|
||||
{ learnCategories = false, runTransfers = false } = {},
|
||||
) {
|
||||
return send('api/transactions-add', {
|
||||
accountId,
|
||||
transactions,
|
||||
learnCategories,
|
||||
runTransfers,
|
||||
});
|
||||
}
|
||||
|
||||
export function importTransactions(accountId, transactions) {
|
||||
return send('api/transactions-import', { accountId, transactions });
|
||||
}
|
||||
|
||||
export function getTransactions(accountId, startDate, endDate) {
|
||||
return send('api/transactions-get', { accountId, startDate, endDate });
|
||||
}
|
||||
|
||||
export function updateTransaction(id, fields) {
|
||||
return send('api/transaction-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteTransaction(id) {
|
||||
return send('api/transaction-delete', { id });
|
||||
}
|
||||
|
||||
export function getAccounts() {
|
||||
return send('api/accounts-get');
|
||||
}
|
||||
|
||||
export function createAccount(account, initialBalance?) {
|
||||
return send('api/account-create', { account, initialBalance });
|
||||
}
|
||||
|
||||
export function updateAccount(id, fields) {
|
||||
return send('api/account-update', { id, fields });
|
||||
}
|
||||
|
||||
export function closeAccount(id, transferAccountId?, transferCategoryId?) {
|
||||
return send('api/account-close', {
|
||||
id,
|
||||
transferAccountId,
|
||||
transferCategoryId,
|
||||
});
|
||||
}
|
||||
|
||||
export function reopenAccount(id) {
|
||||
return send('api/account-reopen', { id });
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
export function createCategoryGroup(group) {
|
||||
return send('api/category-group-create', { group });
|
||||
}
|
||||
|
||||
export function updateCategoryGroup(id, fields) {
|
||||
return send('api/category-group-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteCategoryGroup(id, transferCategoryId?) {
|
||||
return send('api/category-group-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getCategories() {
|
||||
return send('api/categories-get', { grouped: false });
|
||||
}
|
||||
|
||||
export function createCategory(category) {
|
||||
return send('api/category-create', { category });
|
||||
}
|
||||
|
||||
export function updateCategory(id, fields) {
|
||||
return send('api/category-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteCategory(id, transferCategoryId?) {
|
||||
return send('api/category-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getPayees() {
|
||||
return send('api/payees-get');
|
||||
}
|
||||
|
||||
export function createPayee(payee) {
|
||||
return send('api/payee-create', { payee });
|
||||
}
|
||||
|
||||
export function updatePayee(id, fields) {
|
||||
return send('api/payee-update', { id, fields });
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
export function getPayeeRules(id) {
|
||||
return send('api/payee-rules-get', { id });
|
||||
}
|
||||
|
||||
export function createRule(rule) {
|
||||
return send('api/rule-create', { rule });
|
||||
}
|
||||
|
||||
export function updateRule(rule) {
|
||||
return send('api/rule-update', { rule });
|
||||
}
|
||||
|
||||
export function deleteRule(id) {
|
||||
return send('api/rule-delete', { id });
|
||||
}
|
||||
0
packages/api/mocks/budgets/.gitkeep
Normal file
@@ -1,27 +1,40 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "6.2.1",
|
||||
"version": "6.8.2",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "@types/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"@types"
|
||||
],
|
||||
"scripts": {
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:node": "tsc --p tsconfig.dist.json",
|
||||
"build:node": "tsc --p tsconfig.dist.json && tsc-alias -p tsconfig.dist.json",
|
||||
"build:migrations": "cp migrations/*.sql dist/migrations",
|
||||
"build:default-db": "cp default-db.sqlite dist/",
|
||||
"build": "rm -rf dist && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db"
|
||||
"build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
|
||||
"test": "yarn run build:app && jest -c jest.config.js",
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.6.0",
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^9.6.0",
|
||||
"compare-versions": "^6.1.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.5.3",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
"compilerOptions": {
|
||||
// Using ES2021 because that’s the newest version where
|
||||
// the latest Node 16.x release supports all of the features
|
||||
"target": "es2021",
|
||||
"target": "ES2021",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node16",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist"
|
||||
"outDir": "dist",
|
||||
"declarationDir": "@types",
|
||||
"paths": {
|
||||
"loot-core/src/*": ["./loot-core/*"],
|
||||
"loot-core/*": ["./@types/loot-core/*"],
|
||||
}
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["dist"]
|
||||
"exclude": ["**/node_modules/*", "dist", "@types"]
|
||||
}
|
||||
|
||||
16
packages/api/validateNodeVersion.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { satisfies } from 'compare-versions';
|
||||
|
||||
import * as packageJson from './package.json';
|
||||
|
||||
export function validateNodeVersion() {
|
||||
if (process?.versions?.node) {
|
||||
const nodeVersion = process?.versions?.node;
|
||||
const minimumNodeVersion = packageJson.engines.node;
|
||||
|
||||
if (!satisfies(nodeVersion, minimumNodeVersion)) {
|
||||
throw new Error(
|
||||
`@actual-app/api requires a node version ${minimumNodeVersion}. Found that you are using: ${nodeVersion}. Please upgrade to a higher version`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,14 @@
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.12.0-rc.1",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.3.82",
|
||||
"@swc/jest": "^0.2.29",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@swc/core": "^1.5.3",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ describe('merkle trie', () => {
|
||||
});
|
||||
|
||||
test('diffing works with empty tries', () => {
|
||||
let trie1 = merkle.emptyTrie();
|
||||
let trie2 = merkle.insert(
|
||||
const trie1 = merkle.emptyTrie();
|
||||
const trie2 = merkle.insert(
|
||||
merkle.emptyTrie(),
|
||||
Timestamp.parse('2009-01-02T10:17:37.789Z-0000-0000testinguuid1')!,
|
||||
);
|
||||
@@ -79,7 +79,7 @@ describe('merkle trie', () => {
|
||||
});
|
||||
|
||||
test('pruning works and keeps correct hashes', () => {
|
||||
let messages = [
|
||||
const messages = [
|
||||
message('2018-11-01T01:00:00.000Z-0000-0123456789ABCDEF', 1000),
|
||||
message('2018-11-01T01:09:00.000Z-0000-0123456789ABCDEF', 1100),
|
||||
message('2018-11-01T01:18:00.000Z-0000-0123456789ABCDEF', 1200),
|
||||
@@ -101,13 +101,13 @@ describe('merkle trie', () => {
|
||||
expect(trie.hash).toBe(2496);
|
||||
expect(trie).toMatchSnapshot();
|
||||
|
||||
let pruned = merkle.prune(trie);
|
||||
const pruned = merkle.prune(trie);
|
||||
expect(pruned.hash).toBe(2496);
|
||||
expect(pruned).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('diffing differently shaped tries returns correct time', () => {
|
||||
let messages = [
|
||||
const messages = [
|
||||
message('2018-11-01T01:00:00.000Z-0000-0123456789ABCDEF', 1000),
|
||||
message('2018-11-01T01:09:00.000Z-0000-0123456789ABCDEF', 1100),
|
||||
message('2018-11-01T01:18:00.000Z-0000-0123456789ABCDEF', 1200),
|
||||
@@ -122,7 +122,7 @@ describe('merkle trie', () => {
|
||||
message('2018-11-01T02:37:00.000Z-0000-0123456789ABCDEF', 2100),
|
||||
];
|
||||
|
||||
let trie = insertMessages({}, messages);
|
||||
const trie = insertMessages({}, messages);
|
||||
|
||||
// Case 0: It always returns a base time when comparing with an
|
||||
// empty trie
|
||||
@@ -136,7 +136,7 @@ describe('merkle trie', () => {
|
||||
// Case 1: Add an older message that modifies the trie in such a
|
||||
// way that it modifies the 1st out of 3 branches (so it will be
|
||||
// pruned away)
|
||||
let trie1 = insertMessages(trie, [
|
||||
const trie1 = insertMessages(trie, [
|
||||
message('2018-11-01T00:59:00.000Z-0000-0123456789ABCDEF', 900),
|
||||
]);
|
||||
|
||||
@@ -167,7 +167,7 @@ describe('merkle trie', () => {
|
||||
// Case 2: Add two messages similar to the above case, but the
|
||||
// second message modifies the 2nd key at the same level as the
|
||||
// first message modifying the 1st key
|
||||
let trie2 = insertMessages(trie, [
|
||||
const trie2 = insertMessages(trie, [
|
||||
message('2018-11-01T00:59:00.000Z-0000-0123456789ABCDEF', 900),
|
||||
message('2018-11-01T01:15:00.000Z-0000-0123456789ABCDEF', 1422),
|
||||
]);
|
||||
|
||||
@@ -36,7 +36,7 @@ export function getKeys(trie: TrieNode): NumberTrieNodeKey[] {
|
||||
export function keyToTimestamp(key: string): number {
|
||||
// 16 is the length of the base 3 value of the current time in
|
||||
// minutes. Ensure it's padded to create the full value
|
||||
let fullkey = key + '0'.repeat(16 - key.length);
|
||||
const fullkey = key + '0'.repeat(16 - key.length);
|
||||
|
||||
// Parse the base 3 representation
|
||||
return parseInt(fullkey, 3) * 1000 * 60;
|
||||
@@ -46,8 +46,8 @@ export function keyToTimestamp(key: string): number {
|
||||
* Mutates `trie` to insert a node at `timestamp`
|
||||
*/
|
||||
export function insert(trie: TrieNode, timestamp: Timestamp) {
|
||||
let hash = timestamp.hash();
|
||||
let key = Number(Math.floor(timestamp.millis() / 1000 / 60)).toString(3);
|
||||
const hash = timestamp.hash();
|
||||
const key = Number(Math.floor(timestamp.millis() / 1000 / 60)).toString(3);
|
||||
|
||||
trie = Object.assign({}, trie, { hash: (trie.hash || 0) ^ hash });
|
||||
return insertKey(trie, key, hash);
|
||||
@@ -68,8 +68,8 @@ function insertKey(trie: TrieNode, key: string, hash: number): TrieNode {
|
||||
}
|
||||
|
||||
export function build(timestamps: Timestamp[]) {
|
||||
let trie = emptyTrie();
|
||||
for (let timestamp of timestamps) {
|
||||
const trie = emptyTrie();
|
||||
for (const timestamp of timestamps) {
|
||||
insert(trie, timestamp);
|
||||
}
|
||||
return trie;
|
||||
@@ -89,11 +89,11 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
|
||||
// left (this shouldn't happen, if that's the case the hash check at
|
||||
// the top of this function should pass)
|
||||
while (1) {
|
||||
let keyset = new Set([...getKeys(node1), ...getKeys(node2)]);
|
||||
let keys = [...keyset.values()];
|
||||
const keyset = new Set([...getKeys(node1), ...getKeys(node2)]);
|
||||
const keys = [...keyset.values()];
|
||||
keys.sort();
|
||||
|
||||
let diffkey = null;
|
||||
let diffkey: null | '0' | '1' | '2' = null;
|
||||
|
||||
// Traverse down the trie through keys that aren't the same. We
|
||||
// traverse down the keys in order. Stop in two cases: either one
|
||||
@@ -110,10 +110,10 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
|
||||
// changed time that we know of, because of pruning it might take
|
||||
// multiple passes to sync up a trie.
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let key = keys[i];
|
||||
const key = keys[i];
|
||||
|
||||
let next1 = node1[key];
|
||||
let next2 = node2[key];
|
||||
const next1 = node1[key];
|
||||
const next2 = node2[key];
|
||||
|
||||
if (!next1 || !next2) {
|
||||
break;
|
||||
@@ -143,13 +143,13 @@ export function prune(trie: TrieNode, n = 2): TrieNode {
|
||||
return trie;
|
||||
}
|
||||
|
||||
let keys = getKeys(trie);
|
||||
const keys = getKeys(trie);
|
||||
keys.sort();
|
||||
|
||||
let next: TrieNode = { hash: trie.hash };
|
||||
const next: TrieNode = { hash: trie.hash };
|
||||
|
||||
// Prune child nodes.
|
||||
for (let k of keys.slice(-n)) {
|
||||
for (const k of keys.slice(-n)) {
|
||||
const node = trie[k];
|
||||
|
||||
if (!node) {
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('Timestamp', function () {
|
||||
|
||||
describe('parsing', function () {
|
||||
it('should not parse', function () {
|
||||
let invalidInputs = [
|
||||
const invalidInputs = [
|
||||
null,
|
||||
undefined,
|
||||
{},
|
||||
@@ -44,19 +44,19 @@ describe('Timestamp', function () {
|
||||
'9999-12-31T23:59:59.999Z-10000-FFFFFFFFFFFFFFFF',
|
||||
'9999-12-31T23:59:59.999Z-FFFF-10000000000000000',
|
||||
];
|
||||
for (let invalidInput of invalidInputs) {
|
||||
for (const invalidInput of invalidInputs) {
|
||||
expect(Timestamp.parse(invalidInput as string)).toBe(null);
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse', function () {
|
||||
let validInputs = [
|
||||
const validInputs = [
|
||||
'1970-01-01T00:00:00.000Z-0000-0000000000000000',
|
||||
'2015-04-24T22:23:42.123Z-1000-0123456789ABCDEF',
|
||||
'9999-12-31T23:59:59.999Z-FFFF-FFFFFFFFFFFFFFFF',
|
||||
];
|
||||
for (let validInput of validInputs) {
|
||||
let parsed = Timestamp.parse(validInput)!;
|
||||
for (const validInput of validInputs) {
|
||||
const parsed = Timestamp.parse(validInput)!;
|
||||
expect(typeof parsed).toBe('object');
|
||||
expect(parsed.millis() >= 0).toBeTruthy();
|
||||
expect(parsed.millis() < 253402300800000).toBeTruthy();
|
||||
|
||||
@@ -80,7 +80,7 @@ export function makeClientId() {
|
||||
return uuidv4().replace(/-/g, '').slice(-16);
|
||||
}
|
||||
|
||||
let config = {
|
||||
const config = {
|
||||
// Allow 5 minutes of clock drift
|
||||
maxDrift: 5 * 60 * 1000,
|
||||
};
|
||||
@@ -96,9 +96,9 @@ export class Timestamp {
|
||||
|
||||
constructor(millis: number, counter: number, node: string) {
|
||||
this._state = {
|
||||
millis: millis,
|
||||
counter: counter,
|
||||
node: node,
|
||||
millis,
|
||||
counter,
|
||||
node,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,11 +168,11 @@ export class Timestamp {
|
||||
return timestamp;
|
||||
}
|
||||
if (typeof timestamp === 'string') {
|
||||
let parts = timestamp.split('-');
|
||||
const parts = timestamp.split('-');
|
||||
if (parts && parts.length === 5) {
|
||||
let millis = Date.parse(parts.slice(0, 3).join('-')).valueOf();
|
||||
let counter = parseInt(parts[3], 16);
|
||||
let node = parts[4];
|
||||
const millis = Date.parse(parts.slice(0, 3).join('-')).valueOf();
|
||||
const counter = parseInt(parts[3], 16);
|
||||
const node = parts[4];
|
||||
if (
|
||||
!isNaN(millis) &&
|
||||
millis >= 0 &&
|
||||
@@ -198,17 +198,17 @@ export class Timestamp {
|
||||
}
|
||||
|
||||
// retrieve the local wall time
|
||||
let phys = Date.now();
|
||||
const phys = Date.now();
|
||||
|
||||
// unpack the clock.timestamp logical time and counter
|
||||
let lOld = clock.timestamp.millis();
|
||||
let cOld = clock.timestamp.counter();
|
||||
const lOld = clock.timestamp.millis();
|
||||
const cOld = clock.timestamp.counter();
|
||||
|
||||
// calculate the next logical time and counter
|
||||
// * ensure that the logical time never goes backward
|
||||
// * increment the counter if phys time does not advance
|
||||
let lNew = Math.max(lOld, phys);
|
||||
let cNew = lOld === lNew ? cOld + 1 : 0;
|
||||
const lNew = Math.max(lOld, phys);
|
||||
const cNew = lOld === lNew ? cOld + 1 : 0;
|
||||
|
||||
// check the result for drift and counter overflow
|
||||
if (lNew - phys > config.maxDrift) {
|
||||
@@ -238,11 +238,11 @@ export class Timestamp {
|
||||
}
|
||||
|
||||
// retrieve the local wall time
|
||||
let phys = Date.now();
|
||||
const phys = Date.now();
|
||||
|
||||
// unpack the message wall time/counter
|
||||
let lMsg = msg.millis();
|
||||
let cMsg = msg.counter();
|
||||
const lMsg = msg.millis();
|
||||
const cMsg = msg.counter();
|
||||
|
||||
// assert the node id and remote clock drift
|
||||
// if (msg.node() === clock.timestamp.node()) {
|
||||
@@ -253,8 +253,8 @@ export class Timestamp {
|
||||
}
|
||||
|
||||
// unpack the clock.timestamp logical time and counter
|
||||
let lOld = clock.timestamp.millis();
|
||||
let cOld = clock.timestamp.counter();
|
||||
const lOld = clock.timestamp.millis();
|
||||
const cOld = clock.timestamp.counter();
|
||||
|
||||
// calculate the next logical time and counter
|
||||
// . ensure that the logical time never goes backward
|
||||
@@ -262,15 +262,15 @@ export class Timestamp {
|
||||
// . if max = old > message, increment local counter
|
||||
// . if max = messsage > old, increment message counter
|
||||
// . otherwise, clocks are monotonic, reset counter
|
||||
let lNew = Math.max(Math.max(lOld, phys), lMsg);
|
||||
let cNew =
|
||||
const lNew = Math.max(Math.max(lOld, phys), lMsg);
|
||||
const cNew =
|
||||
lNew === lOld && lNew === lMsg
|
||||
? Math.max(cOld, cMsg) + 1
|
||||
: lNew === lOld
|
||||
? cOld + 1
|
||||
: lNew === lMsg
|
||||
? cMsg + 1
|
||||
: 0;
|
||||
? cOld + 1
|
||||
: lNew === lMsg
|
||||
? cMsg + 1
|
||||
: 0;
|
||||
|
||||
// check the result for drift and counter overflow
|
||||
if (lNew - phys > config.maxDrift) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
// Using ES2021 because that’s the newest version where
|
||||
// the latest Node 16.x release supports all of the features
|
||||
"target": "es2021",
|
||||
"target": "ES2021",
|
||||
"module": "CommonJS",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
|
||||
2
packages/desktop-client/.gitignore
vendored
@@ -10,11 +10,13 @@ test-results
|
||||
# production
|
||||
build
|
||||
build-stats
|
||||
stats.json
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
npm-debug.log
|
||||
.swc
|
||||
|
||||
*kcab.*
|
||||
public/kcab
|
||||
|
||||
@@ -32,31 +32,64 @@ Prerequisites:
|
||||
|
||||
#### Running against the local server
|
||||
|
||||
First start the dev server:
|
||||
First start a dev instance:
|
||||
|
||||
```sh
|
||||
HTTPS=true yarn start
|
||||
```
|
||||
Next, navigate to the root of your project folder, run the standartised docker container, and launch the visual regression tests from within it.
|
||||
|
||||
or using the dev container:
|
||||
```
|
||||
HTTPS=true docker compose up --build
|
||||
```
|
||||
|
||||
Note the network IP address and port the dev instance is listening on.
|
||||
|
||||
Next, navigate to the root of your project folder, run the standardized docker container, and launch the visual regression tests from within it.
|
||||
|
||||
Run via yarn:
|
||||
|
||||
```sh
|
||||
# By default, this connects to https://localhost:3001
|
||||
yarn vrt:docker
|
||||
|
||||
# To use a different ip and port:
|
||||
yarn vrt:docker --e2e-start-url https://ip:port
|
||||
|
||||
# To update snapshots, use the following command:
|
||||
yarn vrt:docker --e2e-start-url https://ip:port --update-snapshots
|
||||
```
|
||||
|
||||
Run manually:
|
||||
|
||||
```sh
|
||||
# Run docker container
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.37.0-jammy /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
|
||||
|
||||
# If you recieve an error such as "docker: invalid reference format", please instead use the following command:
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.37.0-jammy /bin/bash
|
||||
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
|
||||
|
||||
# Run the VRT tests: important - they MUST be ran against a HTTPS server
|
||||
E2E_START_URL=https://192.168.0.178:3001 yarn vrt
|
||||
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
|
||||
# Use the ip and port noted earlier
|
||||
E2E_START_URL=https://ip:port yarn vrt
|
||||
|
||||
# To update snapshots, use the following command:
|
||||
E2E_START_URL=https://192.168.0.178:3001 yarn vrt --update-snapshots
|
||||
E2E_START_URL=https://ip:port yarn vrt --update-snapshots
|
||||
```
|
||||
|
||||
#### Running against a remote server
|
||||
|
||||
You can also run the tests against a remote server by passing the URL:
|
||||
|
||||
Run in standardized docker container:
|
||||
```sh
|
||||
E2E_START_URL=https://my-remote-server.com yarn vrt:docker
|
||||
|
||||
# Or pass in server URL as argument
|
||||
yarn vrt:docker --e2e-start-url https://my-remote-server.com
|
||||
```
|
||||
|
||||
Run locally:
|
||||
```sh
|
||||
E2E_START_URL=https://my-remote-server.com yarn vrt
|
||||
```
|
||||
|
||||
@@ -8,7 +8,6 @@ echo "Building the browser..."
|
||||
rm -fr build
|
||||
|
||||
export IS_GENERIC_BROWSER=1
|
||||
export INLINE_RUNTIME_CHUNK=false
|
||||
export REACT_APP_BACKEND_WORKER_HASH=`ls "$ROOT"/../public/kcab/kcab.worker.*.js | sed 's/.*kcab\.worker\.\(.*\)\.js/\1/'`
|
||||
|
||||
yarn build
|
||||
@@ -16,4 +15,4 @@ yarn build
|
||||
rm -fr build-stats
|
||||
mkdir build-stats
|
||||
mv build/kcab/stats.json build-stats/loot-core-stats.json
|
||||
mv build/stats.json build-stats/desktop-client-stats.json
|
||||
mv ./stats.json build-stats/web-stats.json
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
loaderByName,
|
||||
removeLoaders,
|
||||
addAfterLoader,
|
||||
addPlugins,
|
||||
} = require('@craco/craco');
|
||||
const chokidar = require('chokidar');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const { IgnorePlugin } = require('webpack');
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||
|
||||
if (process.env.CI) {
|
||||
process.env.DISABLE_ESLINT_PLUGIN = 'true';
|
||||
}
|
||||
|
||||
// Forward Netlify env variables
|
||||
if (process.env.REVIEW_ID) {
|
||||
process.env.REACT_APP_REVIEW_ID = process.env.REVIEW_ID;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
configure: (webpackConfig, { env, paths }) => {
|
||||
webpackConfig.mode =
|
||||
process.env.NODE_ENV === 'development' ? 'development' : 'production';
|
||||
|
||||
// swc-loader
|
||||
addAfterLoader(webpackConfig, loaderByName('babel-loader'), {
|
||||
test: /\.m?[tj]sx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('swc-loader'),
|
||||
});
|
||||
|
||||
// remove the babel loaders
|
||||
removeLoaders(webpackConfig, loaderByName('babel-loader'));
|
||||
|
||||
addPlugins(webpackConfig, [
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'disabled',
|
||||
generateStatsFile: true,
|
||||
}),
|
||||
// Pikaday throws a warning if Moment.js is not installed however it doesn't
|
||||
// actually require it to be installed. As we don't use Moment.js ourselves
|
||||
// then we can just silence this warning.
|
||||
new IgnorePlugin({
|
||||
contextRegExp: /pikaday$/,
|
||||
resourceRegExp: /moment$/,
|
||||
}),
|
||||
]);
|
||||
|
||||
webpackConfig.resolve.extensions = [
|
||||
'.web.js',
|
||||
'.web.jsx',
|
||||
'.web.ts',
|
||||
'.web.tsx',
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
...webpackConfig.resolve.extensions,
|
||||
];
|
||||
|
||||
if (process.env.IS_GENERIC_BROWSER) {
|
||||
webpackConfig.resolve.extensions = [
|
||||
'.browser.js',
|
||||
'.browser.jsx',
|
||||
'.browser.ts',
|
||||
'.browser.tsx',
|
||||
...webpackConfig.resolve.extensions,
|
||||
];
|
||||
}
|
||||
|
||||
webpackConfig.optimization = {
|
||||
...webpackConfig.optimization,
|
||||
minimize:
|
||||
process.env.CI === 'true' || process.env.NODE_ENV !== 'development',
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
// `terserOptions` options will be passed to `swc` (`@swc/core`)
|
||||
// Link to options - https://swc.rs/docs/config-js-minify
|
||||
terserOptions: {
|
||||
compress: false,
|
||||
mangle: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return webpackConfig;
|
||||
},
|
||||
},
|
||||
devServer: (devServerConfig, { env, paths, proxy, allowedHost }) => {
|
||||
devServerConfig.onBeforeSetupMiddleware = server => {
|
||||
chokidar
|
||||
.watch([
|
||||
path.resolve('../loot-core/lib-dist/*.js'),
|
||||
path.resolve('../loot-core/lib-dist/browser/*.js'),
|
||||
])
|
||||
.on('all', function () {
|
||||
for (const ws of server.webSocketServer.clients) {
|
||||
ws.send(JSON.stringify({ type: 'static-changed' }));
|
||||
}
|
||||
});
|
||||
};
|
||||
devServerConfig.headers = {
|
||||
...devServerConfig.headers,
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
};
|
||||
|
||||
return devServerConfig;
|
||||
},
|
||||
};
|
||||
@@ -2,12 +2,12 @@ import { test, expect } from '@playwright/test';
|
||||
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
import screenshotConfig from './screenshot.config';
|
||||
|
||||
test.describe('Accounts', () => {
|
||||
let page;
|
||||
let navigation;
|
||||
let configurationPage;
|
||||
let accountPage;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
@@ -23,7 +23,7 @@ test.describe('Accounts', () => {
|
||||
});
|
||||
|
||||
test('creates a new account and views the initial balance transaction', async () => {
|
||||
const accountPage = await navigation.createAccount({
|
||||
accountPage = await navigation.createAccount({
|
||||
name: 'New Account',
|
||||
offBudget: false,
|
||||
balance: 100,
|
||||
@@ -35,20 +35,68 @@ test.describe('Accounts', () => {
|
||||
await expect(transaction.category).toHaveText('Starting Balances');
|
||||
await expect(transaction.debit).toHaveText('');
|
||||
await expect(transaction.credit).toHaveText('100.00');
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('closes an account', async () => {
|
||||
const accountPage = await navigation.goToAccountPage('Roth IRA');
|
||||
accountPage = await navigation.goToAccountPage('Roth IRA');
|
||||
|
||||
await expect(accountPage.accountName).toHaveText('Roth IRA');
|
||||
|
||||
const modal = await accountPage.clickCloseAccount();
|
||||
await modal.selectTransferAccount('Vanguard 401k');
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
await modal.closeAccount();
|
||||
|
||||
await expect(accountPage.accountName).toHaveText('Closed: Roth IRA');
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test.describe('Budgeted Accounts', () => {
|
||||
// Reset filters
|
||||
test.afterEach(async () => {
|
||||
await accountPage.removeFilter(0);
|
||||
});
|
||||
|
||||
test('creates a transfer from two existing transactions', async () => {
|
||||
accountPage = await navigation.goToAccountPage('For budget');
|
||||
await expect(accountPage.accountName).toHaveText('Budgeted Accounts');
|
||||
|
||||
await accountPage.filterByNote('Test Acc Transfer');
|
||||
|
||||
await accountPage.createSingleTransaction({
|
||||
account: 'Ally Savings',
|
||||
payee: '',
|
||||
notes: 'Test Acc Transfer',
|
||||
category: 'Food',
|
||||
debit: '34.56',
|
||||
});
|
||||
|
||||
await accountPage.createSingleTransaction({
|
||||
account: 'HSBC',
|
||||
payee: '',
|
||||
notes: 'Test Acc Transfer',
|
||||
category: 'Food',
|
||||
credit: '34.56',
|
||||
});
|
||||
|
||||
await page.waitForTimeout(100); // Give time for the previous transaction to be rendered
|
||||
|
||||
await accountPage.selectNthTransaction(0);
|
||||
await accountPage.selectNthTransaction(1);
|
||||
await accountPage.clickSelectAction('Make transfer');
|
||||
|
||||
let transaction = accountPage.getNthTransaction(0);
|
||||
await expect(transaction.payee).toHaveText('Ally Savings');
|
||||
await expect(transaction.category).toHaveText('Transfer');
|
||||
await expect(transaction.credit).toHaveText('34.56');
|
||||
await expect(transaction.account).toHaveText('HSBC');
|
||||
|
||||
transaction = accountPage.getNthTransaction(1);
|
||||
await expect(transaction.payee).toHaveText('HSBC');
|
||||
await expect(transaction.category).toHaveText('Transfer');
|
||||
await expect(transaction.debit).toHaveText('34.56');
|
||||
await expect(transaction.account).toHaveText('Ally Savings');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -1,7 +1,6 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import screenshotConfig from './screenshot.config';
|
||||
|
||||
test.describe('Budget', () => {
|
||||
let page;
|
||||
@@ -34,7 +33,7 @@ test.describe('Budget', () => {
|
||||
await expect(summary.getByText(/^Overspent in /)).toBeVisible();
|
||||
await expect(summary.getByText('Budgeted')).toBeVisible();
|
||||
await expect(summary.getByText('For Next Month')).toBeVisible();
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('transfer funds to another category', async () => {
|
||||
@@ -47,7 +46,7 @@ test.describe('Budget', () => {
|
||||
expect(await budgetPage.getBalanceForRow(2)).toEqual(
|
||||
currentFundsA + currentFundsB,
|
||||
);
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('budget table is rendered', async () => {
|
||||
@@ -60,12 +59,9 @@ test.describe('Budget', () => {
|
||||
});
|
||||
|
||||
test('clicking on spent amounts opens a transaction page', async () => {
|
||||
let categoryName = await budgetPage.getCategoryNameForRow(1);
|
||||
let accountPage = await budgetPage.clickOnSpentAmountForRow(1);
|
||||
const accountPage = await budgetPage.clickOnSpentAmountForRow(1);
|
||||
expect(page.url()).toContain('/accounts');
|
||||
expect(await accountPage.accountName.textContent()).toMatch(
|
||||
new RegExp(String.raw`${categoryName} \(\w+ \d+\)`),
|
||||
);
|
||||
expect(await accountPage.accountName.textContent()).toMatch('All Accounts');
|
||||
await page.getByRole('button', { name: 'Back' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -102,10 +102,22 @@
|
||||
"name": "Store",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "620e85b1-2ae7-45b1-bb3e-b875ea5c553a",
|
||||
"name": "Work",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"payee_locations": [],
|
||||
"category_groups": [
|
||||
{
|
||||
"id": "a5c355c2-3b77-4a7f-b8b3-c832b10cfec8",
|
||||
"name": "Income",
|
||||
"hidden": false,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "d5c355c2-3b77-4a7f-b8b3-c832b10cfec9",
|
||||
"name": "Internal Master Category",
|
||||
@@ -611,6 +623,30 @@
|
||||
"goal_overall_funded": null,
|
||||
"goal_overall_left": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "1429f287-50aa-49d8-a89c-752cbd167d6c",
|
||||
"category_group_id": "a5c355c2-3b77-4a7f-b8b3-c832b10cfec8",
|
||||
"name": "Income",
|
||||
"hidden": false,
|
||||
"original_category_group_id": null,
|
||||
"note": null,
|
||||
"budgeted": 0,
|
||||
"activity": 0,
|
||||
"balance": 0,
|
||||
"goal_type": "NEED",
|
||||
"goal_day": null,
|
||||
"goal_cadence": 1,
|
||||
"goal_cadence_frequency": 1,
|
||||
"goal_creation_month": null,
|
||||
"goal_target": 0,
|
||||
"goal_target_month": null,
|
||||
"goal_percentage_complete": null,
|
||||
"goal_months_to_budget": null,
|
||||
"goal_under_funded": null,
|
||||
"goal_overall_funded": null,
|
||||
"goal_overall_left": null,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"months": [
|
||||
@@ -1597,7 +1633,7 @@
|
||||
"date": "2023-08-04",
|
||||
"amount": 0,
|
||||
"memo": "getting paid",
|
||||
"cleared": "cleared",
|
||||
"cleared": "reconciled",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
@@ -1657,7 +1693,7 @@
|
||||
"date": "2023-08-04",
|
||||
"amount": 1000000,
|
||||
"memo": "",
|
||||
"cleared": "cleared",
|
||||
"cleared": "reconciled",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
@@ -1671,9 +1707,90 @@
|
||||
"import_payee_name_original": null,
|
||||
"debt_transaction_type": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "213526fc-ba49-4790-8a96-cc2a50182728",
|
||||
"date": "2023-09-04",
|
||||
"amount": -100000,
|
||||
"memo": "Test transaction",
|
||||
"cleared": "cleared",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "2a20470a-634f-4efa-a7f6-f1c0b0bdda41",
|
||||
"category_id": "36120d44-6c61-4402-985a-891a8d267858",
|
||||
"transfer_account_id": null,
|
||||
"transfer_transaction_id": null,
|
||||
"matched_transaction_id": null,
|
||||
"import_id": null,
|
||||
"import_payee_name": null,
|
||||
"import_payee_name_original": null,
|
||||
"debt_transaction_type": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "024494a1-f1e0-4667-9fc0-91e4a4262193",
|
||||
"date": "2023-09-04",
|
||||
"amount": 50000,
|
||||
"memo": "split part b",
|
||||
"cleared": "cleared",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"account_id": "125f339b-2a63-481e-84c0-f04d898905d2",
|
||||
"payee_id": "",
|
||||
"category_id": null,
|
||||
"transfer_account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"transfer_transaction_id": null,
|
||||
"matched_transaction_id": null,
|
||||
"import_id": null,
|
||||
"import_payee_name": null,
|
||||
"import_payee_name_original": null,
|
||||
"debt_transaction_type": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "9a22f287-f1e0-4667-9fc0-91e4a4262193",
|
||||
"date": "2024-02-02",
|
||||
"amount": 2000000,
|
||||
"memo": "Paycheck",
|
||||
"cleared": "cleared",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "620e85b1-2ae7-45b1-bb3e-b875ea5c553a",
|
||||
"category_id": "1429f287-50aa-49d8-a89c-752cbd167d6c",
|
||||
"transfer_account_id": null,
|
||||
"transfer_transaction_id": null,
|
||||
"matched_transaction_id": null,
|
||||
"import_id": null,
|
||||
"import_payee_name": null,
|
||||
"import_payee_name_original": null,
|
||||
"debt_transaction_type": null,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"subtransactions": [
|
||||
{
|
||||
"id": "d8ec8c84-5033-4f7e-8485-66bfe19a70d6",
|
||||
"transaction_id": "213526fc-ba49-4790-8a96-cc2a50182728",
|
||||
"amount": -50000,
|
||||
"memo": "split part a",
|
||||
"payee_id": "2a20470a-634f-4efa-a7f6-f1c0b0bdda41",
|
||||
"category_id": "36120d44-6c61-4402-985a-891a8d267858",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "870d8780-79cf-4197-a341-47d24b2b5a59",
|
||||
"transaction_id": "213526fc-ba49-4790-8a96-cc2a50182728",
|
||||
"amount": -50000,
|
||||
"memo": "split part b",
|
||||
"payee_id": "8d3017e0-2aa6-4fe2-b011-c53c9f147eb6",
|
||||
"category_id": null,
|
||||
"transfer_account_id": "125f339b-2a63-481e-84c0-f04d898905d2",
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"subtransactions": [],
|
||||
"scheduled_transactions": [],
|
||||
"scheduled_subtransactions": []
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { test, expect } from '@playwright/test';
|
||||
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
import screenshotConfig from './screenshot.config';
|
||||
|
||||
test.describe('Mobile', () => {
|
||||
let page;
|
||||
@@ -43,47 +42,52 @@ test.describe('Mobile', () => {
|
||||
'Mortgage',
|
||||
'Water',
|
||||
'Power',
|
||||
'Starting Balances',
|
||||
'Misc',
|
||||
'Income',
|
||||
]);
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('opens the accounts page and asserts on balances', async () => {
|
||||
const accountsPage = await navigation.goToAccountsPage();
|
||||
|
||||
const account = await accountsPage.getNthAccount(0);
|
||||
const account = await accountsPage.getNthAccount(1);
|
||||
|
||||
await expect(account.name).toHaveText('Ally Savings');
|
||||
await expect(account.balance).toHaveText('7,653.00');
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('opens individual account page and checks that filtering is working', async () => {
|
||||
const accountsPage = await navigation.goToAccountsPage();
|
||||
const accountPage = await accountsPage.openNthAccount(1);
|
||||
const accountPage = await accountsPage.openNthAccount(0);
|
||||
|
||||
await expect(accountPage.heading).toHaveText('Bank of America');
|
||||
expect(await accountPage.getBalance()).toBeGreaterThan(0);
|
||||
|
||||
await expect(accountPage.noTransactionsFoundError).not.toBeVisible();
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await accountPage.searchByText('nothing should be found');
|
||||
await expect(accountPage.noTransactionsFoundError).toBeVisible();
|
||||
await expect(accountPage.transactions).toHaveCount(0);
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await accountPage.searchByText('Kroger');
|
||||
await expect(accountPage.transactions).not.toHaveCount(0);
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('creates a transaction via footer button', async () => {
|
||||
const transactionEntryPage = await navigation.goToTransactionEntryPage();
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await expect(transactionEntryPage.header).toHaveText('New Transaction');
|
||||
|
||||
await transactionEntryPage.amountField.fill('12.34');
|
||||
// Click anywhere to cancel active edit.
|
||||
await transactionEntryPage.header.click();
|
||||
await transactionEntryPage.fillField(
|
||||
page.getByTestId('payee-field'),
|
||||
'Kroger',
|
||||
@@ -96,14 +100,14 @@ test.describe('Mobile', () => {
|
||||
page.getByTestId('account-field'),
|
||||
'Ally Savings',
|
||||
);
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
const accountPage = await transactionEntryPage.createTransaction();
|
||||
|
||||
await expect(accountPage.transactions.nth(0)).toHaveText(
|
||||
'KrogerClothing-12.34',
|
||||
);
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('creates a transaction from `/accounts/:id` page', async () => {
|
||||
@@ -112,9 +116,11 @@ test.describe('Mobile', () => {
|
||||
const transactionEntryPage = await accountPage.clickCreateTransaction();
|
||||
|
||||
await expect(transactionEntryPage.header).toHaveText('New Transaction');
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await transactionEntryPage.amountField.fill('12.34');
|
||||
// Click anywhere to cancel active edit.
|
||||
await transactionEntryPage.header.click();
|
||||
await transactionEntryPage.fillField(
|
||||
page.getByTestId('payee-field'),
|
||||
'Kroger',
|
||||
@@ -133,7 +139,7 @@ test.describe('Mobile', () => {
|
||||
|
||||
test('checks that settings page can be opened', async () => {
|
||||
const settingsPage = await navigation.goToSettingsPage();
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
@@ -144,6 +150,6 @@ test.describe('Mobile', () => {
|
||||
expect(await download.suggestedFilename()).toMatch(
|
||||
/^\d{4}-\d{2}-\d{2}-.*.zip$/,
|
||||
);
|
||||
await expect(page).toHaveScreenshot(screenshotConfig(page));
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 27 KiB |