mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 03:32:54 -05:00
Compare commits
2054 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65c0a5a316 | ||
|
|
b0deedb411 | ||
|
|
103d95bbc8 | ||
|
|
d11fc59ec9 | ||
|
|
1c931cf01c | ||
|
|
7f50c73866 | ||
|
|
a7cde1f90d | ||
|
|
398ada0afd | ||
|
|
8b928b3b21 | ||
|
|
3897a5a51c | ||
|
|
94f94497af | ||
|
|
a72fd74c5e | ||
|
|
5214549ed3 | ||
|
|
3c8212e130 | ||
|
|
eface19216 | ||
|
|
f413fa03ae | ||
|
|
81b30d74e4 | ||
|
|
0fddcac76d | ||
|
|
1511587e88 | ||
|
|
ff7c358b83 | ||
|
|
f88b00ae77 | ||
|
|
f5a1293e3c | ||
|
|
5a79beadfa | ||
|
|
6e934e46a8 | ||
|
|
18423a3167 | ||
|
|
c1a70377b9 | ||
|
|
dd4b891fd9 | ||
|
|
89a92ecbbe | ||
|
|
bde99b75ae | ||
|
|
8d3b046bef | ||
|
|
f7f58847dc | ||
|
|
23752f1da9 | ||
|
|
1cc2da1de3 | ||
|
|
04b5bdc62f | ||
|
|
fc78d5b546 | ||
|
|
f5586501bf | ||
|
|
d902c38253 | ||
|
|
a085945898 | ||
|
|
def06693ab | ||
|
|
bf72a26e5c | ||
|
|
10c35e18d3 | ||
|
|
aea6e79402 | ||
|
|
81db7399da | ||
|
|
7c9a499f91 | ||
|
|
df9e6ec66d | ||
|
|
a4d3f277ea | ||
|
|
cab24df3ce | ||
|
|
c92e445416 | ||
|
|
e86cbcdbb3 | ||
|
|
3a14d46a05 | ||
|
|
14c1d9f03b | ||
|
|
1884a188b7 | ||
|
|
c471c8908b | ||
|
|
7a252d0694 | ||
|
|
3b6c97faab | ||
|
|
bc09c0412c | ||
|
|
87779d3199 | ||
|
|
e5eb8646c2 | ||
|
|
0504becaf5 | ||
|
|
04f8140d26 | ||
|
|
9fd004205c | ||
|
|
f90fc69341 | ||
|
|
7e1c0a8682 | ||
|
|
454f019f7a | ||
|
|
f1a4c888b2 | ||
|
|
524707cecc | ||
|
|
8351edc8bb | ||
|
|
4957cd652b | ||
|
|
ad58561f4e | ||
|
|
dbdc5af2b9 | ||
|
|
679b94a626 | ||
|
|
5e217ec9a2 | ||
|
|
52ce2a179e | ||
|
|
be1c194eb4 | ||
|
|
7d9190ea9c | ||
|
|
c883e71e49 | ||
|
|
6710456c05 | ||
|
|
be2126e136 | ||
|
|
ac08b87273 | ||
|
|
7ddd79c61e | ||
|
|
4ee70e7f1f | ||
|
|
7faecf4273 | ||
|
|
ce99c49ddb | ||
|
|
c947d964fa | ||
|
|
46aa79ab7c | ||
|
|
af3598ec76 | ||
|
|
83ba751410 | ||
|
|
722e30e385 | ||
|
|
e88bd783f2 | ||
|
|
8c0ca48781 | ||
|
|
5f1fadb7cc | ||
|
|
cac4761982 | ||
|
|
e1a694a554 | ||
|
|
c4ad24edde | ||
|
|
f5b9a5b23d | ||
|
|
8b49a25ab4 | ||
|
|
15298703ac | ||
|
|
7096d00fc6 | ||
|
|
e8b4a750ed | ||
|
|
4a7b0e7365 | ||
|
|
f1da358186 | ||
|
|
a23a28522f | ||
|
|
6a5de96033 | ||
|
|
abeeb05091 | ||
|
|
bd063423e5 | ||
|
|
ca480a8269 | ||
|
|
bdf4dda3a8 | ||
|
|
0c1c8e6adb | ||
|
|
ed91fb1ef4 | ||
|
|
b14c77aed7 | ||
|
|
64df0e107c | ||
|
|
9b59333563 | ||
|
|
b09d800e40 | ||
|
|
d1c3b9bab1 | ||
|
|
663f830cc9 | ||
|
|
0312f516ad | ||
|
|
9dcd22962c | ||
|
|
f09f4af667 | ||
|
|
1f5e5d41a4 | ||
|
|
46f04f5d4c | ||
|
|
caaa801d24 | ||
|
|
5448a5c264 | ||
|
|
977657a0be | ||
|
|
2f8b839036 | ||
|
|
1cf64f87ab | ||
|
|
012cfd09ea | ||
|
|
39361e5b62 | ||
|
|
5ada00cf97 | ||
|
|
0f2226e993 | ||
|
|
f0c81eebbf | ||
|
|
a84af23e7e | ||
|
|
1442662eb7 | ||
|
|
4850034e6f | ||
|
|
90dc050102 | ||
|
|
7791b7401e | ||
|
|
a97471557b | ||
|
|
5f823156b7 | ||
|
|
dd2b0a8bd5 | ||
|
|
c53e9e9e41 | ||
|
|
6cbf3e33e6 | ||
|
|
cdbf3e06c1 | ||
|
|
1f2c6541b8 | ||
|
|
ecb4d7153c | ||
|
|
e70dc4efb0 | ||
|
|
bbebf71378 | ||
|
|
f35c5a0ed9 | ||
|
|
9d63b23463 | ||
|
|
705985a8df | ||
|
|
eb31071043 | ||
|
|
91c4e3e067 | ||
|
|
7cb53502b8 | ||
|
|
87c26042b9 | ||
|
|
6070166f4e | ||
|
|
66619fa20d | ||
|
|
5e8a24f283 | ||
|
|
278e4ad74f | ||
|
|
c347653566 | ||
|
|
c4593f3be9 | ||
|
|
99724f611c | ||
|
|
f07ad1f8c6 | ||
|
|
4bfb64cdfc | ||
|
|
626e7973ac | ||
|
|
7ae6442296 | ||
|
|
774402503f | ||
|
|
cf360ad398 | ||
|
|
2a275b3821 | ||
|
|
87428a7b65 | ||
|
|
6655f51ccc | ||
|
|
ceeef91a45 | ||
|
|
b831d15eab | ||
|
|
26907d3b12 | ||
|
|
b9eaeafc1c | ||
|
|
45d53ff771 | ||
|
|
b1627d7073 | ||
|
|
5fc3e2ea47 | ||
|
|
97b28ca375 | ||
|
|
aa529a2cf1 | ||
|
|
205ccfe3d6 | ||
|
|
b92fa709eb | ||
|
|
5d91d29d77 | ||
|
|
61d41cc28a | ||
|
|
5921a35340 | ||
|
|
6573a52411 | ||
|
|
bec841932d | ||
|
|
629b001c01 | ||
|
|
4bb59fd7f6 | ||
|
|
a1be1d43f6 | ||
|
|
1c6697a7ee | ||
|
|
da13dfa570 | ||
|
|
6bcccaa943 | ||
|
|
58f87dc80f | ||
|
|
5a34c06859 | ||
|
|
92c93b3f6e | ||
|
|
c017b8a228 | ||
|
|
34ffc5c4b2 | ||
|
|
14b0cd7b1d | ||
|
|
daca767808 | ||
|
|
6111f94b51 | ||
|
|
ce0ca60bcf | ||
|
|
3fbe6d05c8 | ||
|
|
cc1c11aac9 | ||
|
|
7dad36528c | ||
|
|
c956f8003b | ||
|
|
0637b1d5f8 | ||
|
|
e6ed4505b3 | ||
|
|
a5d591fed7 | ||
|
|
215e00ac14 | ||
|
|
1f44903e4b | ||
|
|
1808f51e85 | ||
|
|
0f1c231d37 | ||
|
|
bd77dfd111 | ||
|
|
39cfa11b25 | ||
|
|
af0a14ce3d | ||
|
|
1f2155053f | ||
|
|
d5ebcced38 | ||
|
|
7c2408daa6 | ||
|
|
95180cc780 | ||
|
|
82e1922bee | ||
|
|
8f66605994 | ||
|
|
eadd11b7f0 | ||
|
|
832fd1e5d8 | ||
|
|
928260ca3a | ||
|
|
be5bfa275e | ||
|
|
1e65939147 | ||
|
|
7060e4b657 | ||
|
|
2005c1b0ac | ||
|
|
da613ab673 | ||
|
|
d894281465 | ||
|
|
5c577aa069 | ||
|
|
e6aeea668b | ||
|
|
ded2f39e13 | ||
|
|
3f6068fe88 | ||
|
|
9213ed75b5 | ||
|
|
93262e7fb4 | ||
|
|
cd8bb8e139 | ||
|
|
bd126b499b | ||
|
|
8976ffc256 | ||
|
|
0b2c8ccd88 | ||
|
|
cde81da72c | ||
|
|
2ef397112c | ||
|
|
6c57b4ec9e | ||
|
|
6cfb9d2a7a | ||
|
|
4ce5e2fd07 | ||
|
|
11bde73fa5 | ||
|
|
efb50edf9f | ||
|
|
96c37350d5 | ||
|
|
f80eb888a0 | ||
|
|
94666a2ac1 | ||
|
|
b6fbcef6f0 | ||
|
|
1165c4c008 | ||
|
|
70f6afbda6 | ||
|
|
0d06bc1f7e | ||
|
|
6281d54a38 | ||
|
|
d637a69ee4 | ||
|
|
8446356cc6 | ||
|
|
ec977ee51a | ||
|
|
ef95850e93 | ||
|
|
81fc029a03 | ||
|
|
9e6a486c90 | ||
|
|
9af3539b91 | ||
|
|
62d8358f90 | ||
|
|
219e139d55 | ||
|
|
298b734539 | ||
|
|
e96b986ad0 | ||
|
|
5104a1a563 | ||
|
|
6ea77324ef | ||
|
|
2b908e9263 | ||
|
|
a2892270d2 | ||
|
|
d649eec4db | ||
|
|
5717d90544 | ||
|
|
a35af73023 | ||
|
|
e4b40fb831 | ||
|
|
fa8ff79208 | ||
|
|
3ce7ae91d9 | ||
|
|
645958bbeb | ||
|
|
1b25235cc7 | ||
|
|
f207803f7a | ||
|
|
df958eb35c | ||
|
|
39dbdc0418 | ||
|
|
df7bc5d2f0 | ||
|
|
5e7538fde3 | ||
|
|
2c0bd6bafd | ||
|
|
d3a7b6228a | ||
|
|
501c8653ef | ||
|
|
484211185b | ||
|
|
22623ce65e | ||
|
|
8506b87f2c | ||
|
|
c25e3d4163 | ||
|
|
69a04a5c21 | ||
|
|
826511779e | ||
|
|
339fac2806 | ||
|
|
2ebaa527be | ||
|
|
c5411518c4 | ||
|
|
36839ff153 | ||
|
|
9d6db12921 | ||
|
|
590ac1f95e | ||
|
|
8e76a65e0c | ||
|
|
c3eda4247e | ||
|
|
022b9b76b1 | ||
|
|
19f0037256 | ||
|
|
c626fc2f17 | ||
|
|
30f21497a6 | ||
|
|
f523d25052 | ||
|
|
278ac0c730 | ||
|
|
0696c8113d | ||
|
|
688de5f604 | ||
|
|
881410bc74 | ||
|
|
b4d2d6a884 | ||
|
|
5cf439883e | ||
|
|
23bb89b96e | ||
|
|
7010ab1eb6 | ||
|
|
18f538c54b | ||
|
|
e170c0d274 | ||
|
|
dad702e5c2 | ||
|
|
224d445840 | ||
|
|
670419b087 | ||
|
|
58baf74992 | ||
|
|
d08be58f95 | ||
|
|
db68170cce | ||
|
|
1e1092e472 | ||
|
|
d1324408f4 | ||
|
|
9e478014c5 | ||
|
|
dd69e539d3 | ||
|
|
2cb668a40c | ||
|
|
3cefd98ce9 | ||
|
|
fa2830a1fd | ||
|
|
57ac062edc | ||
|
|
0c94214a8f | ||
|
|
b1bf7ee7cd | ||
|
|
266de169db | ||
|
|
d412590b33 | ||
|
|
635ef27696 | ||
|
|
2b72b2f2f2 | ||
|
|
985b653a87 | ||
|
|
f14b160e5c | ||
|
|
8eafa1e741 | ||
|
|
aefd9504bf | ||
|
|
1f6977da81 | ||
|
|
290402ee6a | ||
|
|
c3b95886db | ||
|
|
e53d444c32 | ||
|
|
ed098c4a69 | ||
|
|
c0f9073f35 | ||
|
|
19c6f85f5e | ||
|
|
d4f1f703ea | ||
|
|
914f59197f | ||
|
|
7c24c269e2 | ||
|
|
c52e5c856d | ||
|
|
b08756cc39 | ||
|
|
29fc22a171 | ||
|
|
815f69a051 | ||
|
|
83ceea4250 | ||
|
|
59d685fab6 | ||
|
|
a267e3abb5 | ||
|
|
e078ed21ba | ||
|
|
b98ff3f50d | ||
|
|
879869c85a | ||
|
|
41d5922635 | ||
|
|
6f07894be7 | ||
|
|
871de93f2d | ||
|
|
15b2ef1591 | ||
|
|
2cd3c9f8a9 | ||
|
|
81afedb610 | ||
|
|
1c9b43671e | ||
|
|
1c05d7e5fe | ||
|
|
6666014fe5 | ||
|
|
dc425042ec | ||
|
|
b5f8aa4d05 | ||
|
|
e659ccf3f4 | ||
|
|
603f970f12 | ||
|
|
59835a3ac1 | ||
|
|
b349edd9e0 | ||
|
|
f265dd9df0 | ||
|
|
a6da06a8ef | ||
|
|
f25dc1f261 | ||
|
|
5751d5d107 | ||
|
|
4b063450a4 | ||
|
|
fbb0f9bd75 | ||
|
|
6af0dbab56 | ||
|
|
5c94e3878e | ||
|
|
10ca29e1e9 | ||
|
|
4d89a9b86a | ||
|
|
34f3ccacf6 | ||
|
|
1b883aa0ab | ||
|
|
c9e6d7897b | ||
|
|
cc347aef08 | ||
|
|
49c5adc9cf | ||
|
|
b8c92b98b8 | ||
|
|
f6f49b1fe7 | ||
|
|
df42cccce7 | ||
|
|
54054736e9 | ||
|
|
5cf170a442 | ||
|
|
f9eb017a54 | ||
|
|
15351e034e | ||
|
|
1895bc80c2 | ||
|
|
a91a8859ab | ||
|
|
a3256f5686 | ||
|
|
715bc00e3b | ||
|
|
4e07357221 | ||
|
|
03f2cabc18 | ||
|
|
259beb7665 | ||
|
|
0f3efde855 | ||
|
|
9aac44c58f | ||
|
|
0d9528e22c | ||
|
|
3f31d19d8a | ||
|
|
225c93914c | ||
|
|
c25e97b0f6 | ||
|
|
e775306f81 | ||
|
|
02824ad240 | ||
|
|
1a13e98f49 | ||
|
|
3d9e90f797 | ||
|
|
c30981638e | ||
|
|
b253246fe2 | ||
|
|
778fc713f3 | ||
|
|
e0f0d8e241 | ||
|
|
310d299ebd | ||
|
|
130f357bab | ||
|
|
f89817170a | ||
|
|
ec37b39e34 | ||
|
|
23f75a6b6a | ||
|
|
f206ba2f0f | ||
|
|
bd5c0cb981 | ||
|
|
3635c8c88a | ||
|
|
5cb97d6f2f | ||
|
|
e8af5b9014 | ||
|
|
328196c485 | ||
|
|
644fe8bdc6 | ||
|
|
15b1b73379 | ||
|
|
8c7e93616f | ||
|
|
a56d6f9e05 | ||
|
|
75acfc79e1 | ||
|
|
300ddc6311 | ||
|
|
a8c4c5fa23 | ||
|
|
05dda5f9d7 | ||
|
|
37ad584826 | ||
|
|
f9c08a995d | ||
|
|
e37a42faf9 | ||
|
|
9f279486ce | ||
|
|
0b3155608c | ||
|
|
3301cfa2fd | ||
|
|
23de23bd4e | ||
|
|
79f640cbc0 | ||
|
|
f786bdcec3 | ||
|
|
f3ae31055e | ||
|
|
21cb684b26 | ||
|
|
e455369443 | ||
|
|
6d122c898d | ||
|
|
e6024f7a8b | ||
|
|
1485d9c871 | ||
|
|
290c6f646f | ||
|
|
85b3c5714e | ||
|
|
ce4b80f499 | ||
|
|
464d9878c6 | ||
|
|
71c208e444 | ||
|
|
1dce3183e5 | ||
|
|
051c8a6ed0 | ||
|
|
bdeb19424b | ||
|
|
5369494925 | ||
|
|
e653ad33a6 | ||
|
|
a7b8d1251c | ||
|
|
d5e0b7da5d | ||
|
|
279d545a28 | ||
|
|
0b6ea52d9b | ||
|
|
38c5f89c41 | ||
|
|
b774a3b216 | ||
|
|
dc5d1174c7 | ||
|
|
33a7524cd7 | ||
|
|
0a0e26372b | ||
|
|
a28fb93cec | ||
|
|
365da79783 | ||
|
|
df92c80c27 | ||
|
|
d0caf9f521 | ||
|
|
3f85aedd0b | ||
|
|
6658dc2197 | ||
|
|
9b7a79a01c | ||
|
|
125510c981 | ||
|
|
f3385dafa2 | ||
|
|
327887b87d | ||
|
|
47ef916873 | ||
|
|
5064b06f2c | ||
|
|
73f2de1ea6 | ||
|
|
4df03984bd | ||
|
|
92980ab55b | ||
|
|
3b97d1eec7 | ||
|
|
545c8d5456 | ||
|
|
f79edf866a | ||
|
|
83ea40dff9 | ||
|
|
444ac83697 | ||
|
|
8f725c7911 | ||
|
|
6725d56bb8 | ||
|
|
666b7870b7 | ||
|
|
686ce5b504 | ||
|
|
4373f4d8f9 | ||
|
|
479572fadb | ||
|
|
0757f9d680 | ||
|
|
835b6987a7 | ||
|
|
6e627c4e2e | ||
|
|
0f41e95952 | ||
|
|
2e70c11c74 | ||
|
|
7e889300ef | ||
|
|
c497d3a941 | ||
|
|
fe17c6ba75 | ||
|
|
3a9a929f56 | ||
|
|
88a7432975 | ||
|
|
373dfb0465 | ||
|
|
80a7a9873a | ||
|
|
9c2bb9b3de | ||
|
|
2acf996430 | ||
|
|
f3451bfc2e | ||
|
|
48cdffbc03 | ||
|
|
37d201b6fb | ||
|
|
d1ecb3db44 | ||
|
|
90e2fe60d1 | ||
|
|
09e3721036 | ||
|
|
6354598d48 | ||
|
|
55df377a20 | ||
|
|
c01e229bd7 | ||
|
|
e8ff02b2e7 | ||
|
|
634508a3bc | ||
|
|
b1c8b3d689 | ||
|
|
ec55e8dc9a | ||
|
|
a1bc66b10a | ||
|
|
4485a631cd | ||
|
|
25a4041958 | ||
|
|
e6bf6da381 | ||
|
|
e507b8ff43 | ||
|
|
5e12d4013a | ||
|
|
84af8b76be | ||
|
|
b3669b3001 | ||
|
|
6f41b20caf | ||
|
|
37d391b4fc | ||
|
|
7702ee4f4f | ||
|
|
d0ba623cfa | ||
|
|
ea675f11ee | ||
|
|
7b314e3b25 | ||
|
|
f78383be29 | ||
|
|
17fd06894a | ||
|
|
4e6a3bbace | ||
|
|
3743a328e3 | ||
|
|
6c87d85920 | ||
|
|
5b685ecc64 | ||
|
|
9ea7bd255a | ||
|
|
ae01066fe2 | ||
|
|
fefd1be22c | ||
|
|
bdbf6e9ca6 | ||
|
|
c5193b6d43 | ||
|
|
183c4b25a9 | ||
|
|
933804e836 | ||
|
|
0a59f793bf | ||
|
|
cfa9ac09d7 | ||
|
|
fe8532578d | ||
|
|
420aad0878 | ||
|
|
16944a6140 | ||
|
|
b2d7b65ce9 | ||
|
|
8c8c248ef7 | ||
|
|
6e8cdb30e8 | ||
|
|
3985d2549e | ||
|
|
68a2af0248 | ||
|
|
7231959f81 | ||
|
|
8498d7f788 | ||
|
|
d752389710 | ||
|
|
95ed7aaf27 | ||
|
|
21dc573f3f | ||
|
|
cb0411b180 | ||
|
|
41b12151f6 | ||
|
|
62dbe3acf5 | ||
|
|
bbff543768 | ||
|
|
008a8a78b9 | ||
|
|
c466189007 | ||
|
|
b856c4874e | ||
|
|
407e3143eb | ||
|
|
d60e7501cc | ||
|
|
ac90eb21a6 | ||
|
|
61bffa3d31 | ||
|
|
fca1bccda3 | ||
|
|
8e6fb4c64f | ||
|
|
5229fe7d16 | ||
|
|
bc04a8cbec | ||
|
|
0a34ede61a | ||
|
|
8a4a9ba083 | ||
|
|
61f5dcfd02 | ||
|
|
5cfa2cf577 | ||
|
|
3f8963273b | ||
|
|
1aa65946c2 | ||
|
|
0760583359 | ||
|
|
a757ba6bdc | ||
|
|
44375e72ad | ||
|
|
6454c10e63 | ||
|
|
7a018e09a9 | ||
|
|
2a9546ced1 | ||
|
|
8926ff69b1 | ||
|
|
340169bfb6 | ||
|
|
3a905d3f9a | ||
|
|
c4d01fe63f | ||
|
|
7738ea0c00 | ||
|
|
446f40714d | ||
|
|
3b26aa05b5 | ||
|
|
8e077e0282 | ||
|
|
ae608f0cb8 | ||
|
|
f1c0d0b8a6 | ||
|
|
d613a6be6e | ||
|
|
d9adb750d4 | ||
|
|
2b37d5a642 | ||
|
|
1750cd9081 | ||
|
|
7769d0303e | ||
|
|
6a41d28404 | ||
|
|
9108b63355 | ||
|
|
1b70e59bde | ||
|
|
b48d256ec4 | ||
|
|
9c0e6a307b | ||
|
|
3e5ce72e27 | ||
|
|
b347f03fbb | ||
|
|
f3660c166f | ||
|
|
aaf96bbc2c | ||
|
|
6d84b0e371 | ||
|
|
db4b504e53 | ||
|
|
8d4dbbf5f2 | ||
|
|
b8d2797259 | ||
|
|
d6afc85a8c | ||
|
|
ee21155d1a | ||
|
|
65a7c58441 | ||
|
|
51ec600de2 | ||
|
|
8201085ccb | ||
|
|
c16a8faa3f | ||
|
|
4ce7f55e0c | ||
|
|
574448ff3b | ||
|
|
af5fd5b3ef | ||
|
|
7fcda084ab | ||
|
|
eccdc52342 | ||
|
|
4c192d7e1e | ||
|
|
baf04a4d48 | ||
|
|
f715ceafc9 | ||
|
|
af73dcd722 | ||
|
|
5e3485a8e2 | ||
|
|
1458dbc307 | ||
|
|
a879960a2d | ||
|
|
9ac77af077 | ||
|
|
3e07d18acd | ||
|
|
fa6cc26416 | ||
|
|
a1ca871b24 | ||
|
|
d9066a49c4 | ||
|
|
63ad6dadf2 | ||
|
|
89b096aa65 | ||
|
|
ee0156d35d | ||
|
|
9c17d55e0d | ||
|
|
411a6791b2 | ||
|
|
6f3af7b609 | ||
|
|
43ff1c033e | ||
|
|
09c44d351d | ||
|
|
a22160579d | ||
|
|
81df2ce7fd | ||
|
|
119d0b339d | ||
|
|
d1362c3d74 | ||
|
|
8142dd1ec9 | ||
|
|
2afd6967b4 | ||
|
|
eec5fbb1cc | ||
|
|
fe922ec22e | ||
|
|
30a70f5627 | ||
|
|
65c5f2c559 | ||
|
|
1abca7619d | ||
|
|
b74f0f2982 | ||
|
|
6a85f84565 | ||
|
|
65329398fd | ||
|
|
a2e434a1fb | ||
|
|
d2bbe6a98e | ||
|
|
2c1967d788 | ||
|
|
798aee78c3 | ||
|
|
2807c98c2c | ||
|
|
5e9b976676 | ||
|
|
44ce976ffa | ||
|
|
5ba80fcbdc | ||
|
|
7b77f60458 | ||
|
|
81f59ff776 | ||
|
|
12f4295932 | ||
|
|
d33e5cc766 | ||
|
|
63d9547e7c | ||
|
|
d18fd36ae1 | ||
|
|
2b1ba88983 | ||
|
|
8be867f884 | ||
|
|
cafe480ba4 | ||
|
|
6472c70960 | ||
|
|
56c5a533e7 | ||
|
|
7e3ff1ad03 | ||
|
|
df3aaf961d | ||
|
|
e0d7233b40 | ||
|
|
1b4c4319e1 | ||
|
|
14f29941b0 | ||
|
|
6b57e45e04 | ||
|
|
4389329bfa | ||
|
|
842e11b3a1 | ||
|
|
3a38c32b4c | ||
|
|
e3101fb86b | ||
|
|
c3c6acd37c | ||
|
|
8de0f6a72a | ||
|
|
2799dbee3e | ||
|
|
58eeee825e | ||
|
|
6653dca776 | ||
|
|
77ba15f54c | ||
|
|
943f903646 | ||
|
|
b4a620e74e | ||
|
|
653a0ab104 | ||
|
|
2c26fa51a3 | ||
|
|
dff9911a15 | ||
|
|
3d5818f017 | ||
|
|
efd294dcef | ||
|
|
0eb62a09bc | ||
|
|
73d52fa0d0 | ||
|
|
5b0cc63f73 | ||
|
|
26a591f07f | ||
|
|
fe8851c797 | ||
|
|
511f677ae4 | ||
|
|
1cef0d11ee | ||
|
|
536cabb75b | ||
|
|
cceda03905 | ||
|
|
982f555a21 | ||
|
|
fe70ecb635 | ||
|
|
d3d9f70657 | ||
|
|
5c0bee6031 | ||
|
|
4439bb6abe | ||
|
|
b432204b4b | ||
|
|
9a85a72089 | ||
|
|
a970a78932 | ||
|
|
ed65805d53 | ||
|
|
88ae7e9375 | ||
|
|
0135a4d1b9 | ||
|
|
4af2c4f214 | ||
|
|
89a8f102dc | ||
|
|
c19cc56c55 | ||
|
|
d032fce7ea | ||
|
|
2fdc7fef32 | ||
|
|
1e41d695c5 | ||
|
|
12f91f7d86 | ||
|
|
f75d0f8099 | ||
|
|
07bbe00059 | ||
|
|
be0d363576 | ||
|
|
c2e648c9d5 | ||
|
|
33049a77e7 | ||
|
|
89241623f3 | ||
|
|
40e432dedb | ||
|
|
8434e8f5ce | ||
|
|
9b99debacc | ||
|
|
a23ec33591 | ||
|
|
aaea04fc00 | ||
|
|
b4f0087eef | ||
|
|
7824c52d9f | ||
|
|
f81c452ba5 | ||
|
|
7072674111 | ||
|
|
16e887c917 | ||
|
|
145659b256 | ||
|
|
572033debe | ||
|
|
b85f9102ce | ||
|
|
942aebedd0 | ||
|
|
32d830440a | ||
|
|
4575616961 | ||
|
|
4a0e2ea306 | ||
|
|
14ec9a9089 | ||
|
|
e91b4070aa | ||
|
|
6dd34b0c63 | ||
|
|
ab4639f48f | ||
|
|
aa3cbd881b | ||
|
|
8681c9c3e6 | ||
|
|
9ec9aef632 | ||
|
|
3be7dd753d | ||
|
|
7e84cf897c | ||
|
|
259e84cea5 | ||
|
|
f9014f0e19 | ||
|
|
e59f5c9af8 | ||
|
|
3661c1585e | ||
|
|
771c01c8b4 | ||
|
|
9f72b43826 | ||
|
|
17cdea9beb | ||
|
|
ec3475d834 | ||
|
|
5ea9c587a8 | ||
|
|
228f38617b | ||
|
|
1e38055376 | ||
|
|
0ee9126820 | ||
|
|
9e455e4c1e | ||
|
|
d77b54f27b | ||
|
|
ff36d1efbe | ||
|
|
cbbbaf65cf | ||
|
|
f129b07dc9 | ||
|
|
74ade73476 | ||
|
|
81acd29c73 | ||
|
|
f1caf21deb | ||
|
|
a28ea6be8f | ||
|
|
f36c5e002b | ||
|
|
803289ee1f | ||
|
|
76cdad4fe6 | ||
|
|
1da9c821ee | ||
|
|
d03b30bc00 | ||
|
|
710d9ab8ac | ||
|
|
d008944022 | ||
|
|
f18bce6094 | ||
|
|
31eb00a155 | ||
|
|
a67c969189 | ||
|
|
58e6c6f23a | ||
|
|
f046d75b75 | ||
|
|
30bcfedc86 | ||
|
|
866b4d6cd4 | ||
|
|
a42938fa64 | ||
|
|
e02b0f9bc7 | ||
|
|
2006d885b8 | ||
|
|
049a41f366 | ||
|
|
7f30680fb3 | ||
|
|
167522d322 | ||
|
|
2d4256b239 | ||
|
|
247e3e8d93 | ||
|
|
5951b92668 | ||
|
|
a9ee670eb4 | ||
|
|
3990aaf38f | ||
|
|
48f5880f1d | ||
|
|
3332f58376 | ||
|
|
46ea8fbf72 | ||
|
|
6a21f8e3de | ||
|
|
f02ca4e3d2 | ||
|
|
7f658691bb | ||
|
|
f5307e4bd4 | ||
|
|
5b1a730f11 | ||
|
|
0c14eb17c4 | ||
|
|
7bb0425c81 | ||
|
|
8832c2b234 | ||
|
|
437e202d27 | ||
|
|
d34f5eccb6 | ||
|
|
382d347508 | ||
|
|
c792c0f54e | ||
|
|
73d0f04d45 | ||
|
|
f1d3902e3e | ||
|
|
8b6ef7b325 | ||
|
|
6ad0b47c7c | ||
|
|
96964224f4 | ||
|
|
0ed5e3ebe6 | ||
|
|
925926f542 | ||
|
|
44ddf210ea | ||
|
|
62c6a8775c | ||
|
|
64cd6ee3c9 | ||
|
|
abc4636662 | ||
|
|
ade25b3304 | ||
|
|
b192ad955e | ||
|
|
e9da476b51 | ||
|
|
12719e3049 | ||
|
|
e178d9914d | ||
|
|
6fd728aa2d | ||
|
|
bf26ca4eb9 | ||
|
|
f606d92c5c | ||
|
|
8b850f1410 | ||
|
|
c992e340ca | ||
|
|
06f9db06b0 | ||
|
|
2b96bb3d52 | ||
|
|
1af5ab09e9 | ||
|
|
ebb9452b8f | ||
|
|
196f03b84e | ||
|
|
93e784a0fe | ||
|
|
026194e5e2 | ||
|
|
98341b440a | ||
|
|
417f5805a8 | ||
|
|
094f0b8a91 | ||
|
|
aa22e6951d | ||
|
|
55724acafa | ||
|
|
b89a32025a | ||
|
|
d62919a357 | ||
|
|
1c7d9bf141 | ||
|
|
e3934b96dc | ||
|
|
6d117f44de | ||
|
|
64821e6a64 | ||
|
|
e7c6611c88 | ||
|
|
6220aadb2d | ||
|
|
2959054d0c | ||
|
|
7d960579f9 | ||
|
|
5fd1d05670 | ||
|
|
0e86dea544 | ||
|
|
2221fd8dff | ||
|
|
c92266fd7f | ||
|
|
08aff05a06 | ||
|
|
1195af76a6 | ||
|
|
e4bc0caa51 | ||
|
|
c3783b6498 | ||
|
|
55ef74eabc | ||
|
|
9667e1c269 | ||
|
|
b803731bc5 | ||
|
|
8415e7cddc | ||
|
|
5f16349a19 | ||
|
|
aff5bba4ec | ||
|
|
d79b8c6cb2 | ||
|
|
bbc123c3b8 | ||
|
|
04273d8064 | ||
|
|
3f1fd55a7b | ||
|
|
bfda42a2be | ||
|
|
201f0fd336 | ||
|
|
99ab8db99f | ||
|
|
f604fdaf36 | ||
|
|
c311d4a8df | ||
|
|
1fc7c9974b | ||
|
|
e63fb46c8f | ||
|
|
db6b4e4800 | ||
|
|
2901b5e5d0 | ||
|
|
abd049e715 | ||
|
|
c51e636637 | ||
|
|
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 | ||
|
|
3a486ed973 | ||
|
|
2178da0414 | ||
|
|
33c204dedb | ||
|
|
f8809df59c | ||
|
|
41a34d0f1c | ||
|
|
04bc0c3c64 | ||
|
|
0aa6a057b1 | ||
|
|
f1488077dd | ||
|
|
b34aaab5f5 | ||
|
|
688bfe59e8 | ||
|
|
8f543a29e0 | ||
|
|
ad73a404c4 | ||
|
|
4ed9f4fff4 | ||
|
|
176c8466a4 | ||
|
|
703c319f7f | ||
|
|
ca8a8174c4 | ||
|
|
f8af8f95ac | ||
|
|
98a7aac736 | ||
|
|
d49f907f53 | ||
|
|
164dd399b0 | ||
|
|
f41d3f22c7 | ||
|
|
469c789c14 | ||
|
|
6ec3728280 | ||
|
|
c8cc479358 | ||
|
|
51ad488ce2 | ||
|
|
985411d48f | ||
|
|
53b5f3a0fc | ||
|
|
c0b21a9f7e | ||
|
|
3ec4ef71c6 | ||
|
|
5df02c19dc | ||
|
|
464c9de6fb | ||
|
|
aad609f427 | ||
|
|
7313cf722d | ||
|
|
978a658c1e | ||
|
|
cdad43ff32 | ||
|
|
4c29afd424 | ||
|
|
c98a21b030 | ||
|
|
f5fde34952 | ||
|
|
59f854c075 | ||
|
|
4ae654d54d | ||
|
|
85affae70f | ||
|
|
1bbba663b2 | ||
|
|
44c7b4eab0 | ||
|
|
57d4cc57cd | ||
|
|
7d892d8164 | ||
|
|
84fbc7e464 | ||
|
|
5f77b87b07 | ||
|
|
97ec8f8d3a | ||
|
|
407ad4deee | ||
|
|
1c04aeae39 | ||
|
|
02e7d036d5 | ||
|
|
16ef674910 | ||
|
|
19bcfbe6e9 | ||
|
|
a2c1c2dea6 | ||
|
|
e9b5bfcc53 | ||
|
|
672dd5ea4b | ||
|
|
b101f9989b | ||
|
|
cf9b8c357e | ||
|
|
c71e1d2e8a | ||
|
|
1e462714e4 | ||
|
|
d9f55460dd | ||
|
|
77a670bbc3 | ||
|
|
1a1fe9ac9d | ||
|
|
ae0faf467f | ||
|
|
16df116d1d | ||
|
|
d7075ae551 | ||
|
|
e500cba7e9 | ||
|
|
2d188b3941 | ||
|
|
e88ea69801 | ||
|
|
9aeab0ff5b | ||
|
|
36c700d92d | ||
|
|
eed6105222 | ||
|
|
a5a5f30dd7 | ||
|
|
291e3a8d14 | ||
|
|
edd34b7903 | ||
|
|
345ea71eed | ||
|
|
d89a016ab1 | ||
|
|
b87951855b | ||
|
|
70e37c0119 | ||
|
|
f55bd860ba | ||
|
|
0f960df8cf | ||
|
|
915c562545 | ||
|
|
770d86258f | ||
|
|
a955fe2474 | ||
|
|
4e9130ac29 | ||
|
|
e94a5505d8 | ||
|
|
1ee2cbec1c | ||
|
|
da6b039f10 | ||
|
|
310cc04a2b | ||
|
|
6fce10aea0 | ||
|
|
97a664159b | ||
|
|
c2291d4b5d | ||
|
|
d6de90a296 | ||
|
|
f06bbf9a51 | ||
|
|
90305965ba | ||
|
|
b655009477 | ||
|
|
2e600e52c7 | ||
|
|
b5f617dbe5 | ||
|
|
b3fc23201e | ||
|
|
6f251e6024 | ||
|
|
b356004f68 | ||
|
|
66dc593fa5 | ||
|
|
8b34ba3958 | ||
|
|
2df774d9f5 | ||
|
|
c96c957a34 | ||
|
|
f3d7641ba9 | ||
|
|
83fd85f059 | ||
|
|
308f8339ae | ||
|
|
9697795279 | ||
|
|
9b40610f26 | ||
|
|
1c61508abb | ||
|
|
2a1c452aac | ||
|
|
1dd1c188d6 | ||
|
|
deec1f998c | ||
|
|
8f634099e2 | ||
|
|
7ee48e4c1d | ||
|
|
270705b3cd | ||
|
|
9588a76109 | ||
|
|
ada9e7da31 | ||
|
|
38ffccb903 | ||
|
|
f5023a7c07 | ||
|
|
bc6a0f8e60 | ||
|
|
5ee7d336ef | ||
|
|
24e42daa51 | ||
|
|
4bafd13c55 | ||
|
|
bf404104c2 | ||
|
|
afaee6bc16 | ||
|
|
e44fbb3847 | ||
|
|
ff70c654a2 | ||
|
|
586a26968c | ||
|
|
5fde6561d0 | ||
|
|
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 | ||
|
|
6e9eddeb56 | ||
|
|
b700aee87d | ||
|
|
b926af286a | ||
|
|
9fca85209f | ||
|
|
a9362cc6f9 | ||
|
|
40296dc876 | ||
|
|
ed1e0ceb30 | ||
|
|
55817b0e70 | ||
|
|
6d7d12138c | ||
|
|
f43097f813 | ||
|
|
52f1f79c01 | ||
|
|
991fc4f450 | ||
|
|
04147fb9b9 | ||
|
|
4a8c692d06 | ||
|
|
0609f47cc3 | ||
|
|
0d1e6f2ee7 | ||
|
|
8568aebdbb | ||
|
|
bfb7c1d213 | ||
|
|
e526555748 | ||
|
|
45c4b262a2 | ||
|
|
e1f805b9c9 | ||
|
|
d0c11cd3af | ||
|
|
0c5bce8baf | ||
|
|
e650e00cb8 | ||
|
|
422996f8a7 | ||
|
|
9cad57c607 | ||
|
|
cd4a2b6678 | ||
|
|
2a0f8335ed | ||
|
|
16faf49438 | ||
|
|
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 | ||
|
|
13a39168c4 | ||
|
|
9fdffcc8e9 | ||
|
|
3daff4381f | ||
|
|
5914469b11 | ||
|
|
39e7f2598b | ||
|
|
c8d326d24b | ||
|
|
10d53fbe5f | ||
|
|
21c65cf68f | ||
|
|
d8639a2a71 | ||
|
|
734191424b | ||
|
|
5d4fcfde00 | ||
|
|
54d7e5460a | ||
|
|
43ebe9e0fd | ||
|
|
515bdf5a74 | ||
|
|
018714610a | ||
|
|
00ee165f8e | ||
|
|
68442ae9e6 | ||
|
|
b937bfae04 | ||
|
|
317e7f135e | ||
|
|
5adb083575 | ||
|
|
524bd4e9eb | ||
|
|
9dfd6ce34c | ||
|
|
5d28bc0e3b | ||
|
|
a4e97e0070 | ||
|
|
a6e38ad2ae | ||
|
|
bb0ae4ebc3 | ||
|
|
dd29e02c5c | ||
|
|
75186183eb | ||
|
|
e7f6348d9a | ||
|
|
83f13cbdc8 | ||
|
|
0045d9212e | ||
|
|
dd254c6c23 | ||
|
|
c66d6e00f5 | ||
|
|
ae3be13aa8 | ||
|
|
cd9d1fb674 | ||
|
|
edba670d3a | ||
|
|
fbb1a9647d | ||
|
|
06cf65497f | ||
|
|
62a0a0fedc | ||
|
|
106dce25dd | ||
|
|
af6de6b585 | ||
|
|
d8c7a0a358 | ||
|
|
58f7b09e8d | ||
|
|
ff11399180 | ||
|
|
363c9e4afb | ||
|
|
e745a4073b | ||
|
|
7d1a895b48 | ||
|
|
a8b42bcd50 | ||
|
|
f33bce41ea | ||
|
|
cdefe6133f | ||
|
|
44a5199a31 | ||
|
|
dccad902d6 | ||
|
|
b477f7c2f1 | ||
|
|
540c410957 | ||
|
|
e205344867 | ||
|
|
7b36c2207a | ||
|
|
8b968579b1 | ||
|
|
6ce794ffcc | ||
|
|
6de1fe34e3 | ||
|
|
d5359a96ca | ||
|
|
3eee0b11d2 | ||
|
|
e792afb1fd | ||
|
|
4fb55d0d70 | ||
|
|
b330991855 | ||
|
|
165ad45822 | ||
|
|
295917036b | ||
|
|
ef9a7cfe85 | ||
|
|
ff073b1221 | ||
|
|
4ece4a7ff6 | ||
|
|
761b3c6a16 | ||
|
|
7ace8c52dd | ||
|
|
f6dd0ecdb9 | ||
|
|
1d4eaaa79c | ||
|
|
3b83f064d4 | ||
|
|
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 | ||
|
|
c330020487 | ||
|
|
83d2472a55 | ||
|
|
458d556e51 | ||
|
|
d9aeb8db1e | ||
|
|
85c0352abb | ||
|
|
7a40a645e6 | ||
|
|
7e9921e9e5 | ||
|
|
2f7b3917ed | ||
|
|
02aff1acbe | ||
|
|
e0d66d3083 | ||
|
|
d999e466b7 | ||
|
|
e589163bb7 | ||
|
|
d2b86d100c | ||
|
|
d8996405c4 | ||
|
|
e548125907 | ||
|
|
edbcaba89b | ||
|
|
b39d106c04 | ||
|
|
4894118809 | ||
|
|
933fc27126 | ||
|
|
c6723da780 | ||
|
|
7a8c320560 | ||
|
|
6fbf0fdc10 | ||
|
|
efaefcfaa9 | ||
|
|
73d281b6ee | ||
|
|
03e943f383 | ||
|
|
5854dffd50 | ||
|
|
c727e3e980 | ||
|
|
b385c715ef | ||
|
|
77e550f028 | ||
|
|
9ea8e86bb3 | ||
|
|
273fa8cd59 | ||
|
|
d4c8b5fa16 | ||
|
|
e62e8ca24e | ||
|
|
4497fbcb10 | ||
|
|
c910b6efcb | ||
|
|
633463a184 | ||
|
|
8a15caf42d | ||
|
|
af9efa09f0 | ||
|
|
a5557ca032 | ||
|
|
0bf587bcf5 | ||
|
|
e11b65719e | ||
|
|
c09a85f340 | ||
|
|
f90fe04b2b | ||
|
|
ca55d9c85e | ||
|
|
4ada92071e | ||
|
|
e848946898 | ||
|
|
9da05543c3 | ||
|
|
8a721bf2e0 | ||
|
|
36914abcd4 | ||
|
|
cd2d186599 | ||
|
|
b3bcff094d | ||
|
|
7955fe7aed | ||
|
|
5ac4c58d1e | ||
|
|
2741422c0a | ||
|
|
cde4551eb7 | ||
|
|
7174fccfe4 | ||
|
|
0303292a28 | ||
|
|
f95df06800 | ||
|
|
f93893e007 | ||
|
|
3680d45814 | ||
|
|
dc872647a9 | ||
|
|
7a45d467b7 | ||
|
|
79aa07ff1a | ||
|
|
c4ff099f26 | ||
|
|
ad2b577515 | ||
|
|
5f52801869 | ||
|
|
6bfd9586e0 | ||
|
|
f36713e0be | ||
|
|
8b20169b59 | ||
|
|
42699ac044 | ||
|
|
da03acc394 | ||
|
|
5d1f2d48ae | ||
|
|
bb07652cf8 | ||
|
|
dc47c6d72a | ||
|
|
c96782d7c1 | ||
|
|
5d61dfce68 | ||
|
|
4b12561905 | ||
|
|
5a81a25b7e | ||
|
|
29f323e721 | ||
|
|
c462604833 | ||
|
|
e48f36cd20 | ||
|
|
1ed84059d7 | ||
|
|
5d8b96a749 | ||
|
|
0920bdc3fa | ||
|
|
c3a9b4dda3 | ||
|
|
eab0068428 | ||
|
|
0d1b962b2f | ||
|
|
67b9143dfc | ||
|
|
e7f298e32a | ||
|
|
516f0c259f | ||
|
|
2c87c60328 | ||
|
|
b4fd6e86ed | ||
|
|
a3e71a8b49 | ||
|
|
aa9c992f2e | ||
|
|
ca498b19cc | ||
|
|
ca4ea97768 | ||
|
|
8be2389fd9 | ||
|
|
46988800ef | ||
|
|
881999bcfe | ||
|
|
9c124e8e44 | ||
|
|
3306963c54 | ||
|
|
baa474843a | ||
|
|
6ed1b58321 | ||
|
|
87813b398a | ||
|
|
28266ed9a2 | ||
|
|
cd6bd9ece8 | ||
|
|
fb2f712c16 | ||
|
|
f58fae8d16 | ||
|
|
a10b10a87b | ||
|
|
47007232b8 | ||
|
|
4761e9ce2f | ||
|
|
849262c95e | ||
|
|
e6e184c412 | ||
|
|
9c6d9ecf0a | ||
|
|
65273127f5 | ||
|
|
b5530085bb | ||
|
|
9ec82d52c9 | ||
|
|
7865e08c91 | ||
|
|
93922567fc | ||
|
|
7c0b7a2f17 | ||
|
|
e8055bbc35 | ||
|
|
7fdd9767ba | ||
|
|
1f105999af | ||
|
|
e116d01453 | ||
|
|
f970263c72 | ||
|
|
fecc9178b3 | ||
|
|
08c80b6f58 | ||
|
|
a3995582c4 | ||
|
|
5008eb022f | ||
|
|
353e7be31f | ||
|
|
30c7024663 | ||
|
|
22efe74ec8 | ||
|
|
a6c1443669 | ||
|
|
5792b15cb5 | ||
|
|
168b79a178 | ||
|
|
7062f2a7d9 | ||
|
|
af666458d3 | ||
|
|
184b302273 | ||
|
|
3004f336da | ||
|
|
45bb7696c3 | ||
|
|
199a9f838d | ||
|
|
de604b9f3a | ||
|
|
df5aa3186c | ||
|
|
1c68c3f964 | ||
|
|
1f79708ea3 | ||
|
|
494d67459f | ||
|
|
a15f80e771 | ||
|
|
be18891957 | ||
|
|
a6dae398da | ||
|
|
d8c885bf4e | ||
|
|
f94d8aff9f | ||
|
|
a549cdf0e7 | ||
|
|
b7a2f16246 | ||
|
|
0ec88ecc24 | ||
|
|
9e7c2e7a65 | ||
|
|
39fa9f2097 | ||
|
|
9040cab600 | ||
|
|
e56cb4bc85 | ||
|
|
fc08baf7ae | ||
|
|
7325153da1 | ||
|
|
e12793e7eb | ||
|
|
5fe146ee0a | ||
|
|
0af2987cef | ||
|
|
e266093a4a | ||
|
|
19f0efb654 | ||
|
|
e0b2eab475 | ||
|
|
25f4e2a8b5 | ||
|
|
d338a73794 | ||
|
|
75f2bf8b1b | ||
|
|
1f5ff932b1 | ||
|
|
eb54487d8e | ||
|
|
5cc6bad34b | ||
|
|
acf4456077 | ||
|
|
b45615e6fc | ||
|
|
38609ee25a | ||
|
|
cae2b9cd5a | ||
|
|
4cacc845c0 | ||
|
|
3dfbd23f96 | ||
|
|
21effa654d | ||
|
|
c33dc6fbad | ||
|
|
057caf127a | ||
|
|
767bc8ecb6 | ||
|
|
bdf5c45cda | ||
|
|
3dfe633428 | ||
|
|
efba86a72d | ||
|
|
316da144e1 | ||
|
|
d3ab8f9812 | ||
|
|
0ea7f1852b | ||
|
|
3c341fc583 | ||
|
|
de90504a6d | ||
|
|
75af7b9987 | ||
|
|
b8fbc9b19c | ||
|
|
fc643d28c6 | ||
|
|
f6e2d3b1f3 | ||
|
|
f1973d55c0 | ||
|
|
476070b0a7 | ||
|
|
9d6b9c556d | ||
|
|
510f635bbc | ||
|
|
d1e57340b8 | ||
|
|
9ca36f3eeb | ||
|
|
05f0df4917 | ||
|
|
5f347bbe40 | ||
|
|
3c4f62bd51 | ||
|
|
abd2d424a6 | ||
|
|
89c5f15c1f | ||
|
|
2a597fb3b8 | ||
|
|
2f97c3a1e2 | ||
|
|
ba5174ddfa | ||
|
|
9a900f9ff1 | ||
|
|
2081e25cf5 | ||
|
|
49ab358cf2 | ||
|
|
efb72af1b8 | ||
|
|
16334f67a5 | ||
|
|
ddb78af57d | ||
|
|
9e2226e384 | ||
|
|
a1dd6cf3dc | ||
|
|
2ee023ac22 | ||
|
|
3496ac28f3 | ||
|
|
f6f496f656 | ||
|
|
68147d654f | ||
|
|
3d29cfbed5 | ||
|
|
fbd1097a0c | ||
|
|
0cc34798fb | ||
|
|
b6100cfecd | ||
|
|
d835b113f8 | ||
|
|
4e4d20ad31 | ||
|
|
8167ea8a83 | ||
|
|
42e1b5ca7e | ||
|
|
55285f4c5f | ||
|
|
5565116c34 | ||
|
|
addd8e0f5f | ||
|
|
46dbc89482 | ||
|
|
0e66ebfeef | ||
|
|
05f2e2af1a | ||
|
|
8ff89a5ab4 | ||
|
|
faa4a7c0e2 | ||
|
|
ba4885cb85 | ||
|
|
a8a0f777e2 | ||
|
|
852b5373bc | ||
|
|
bca09023e2 | ||
|
|
4d8efccc73 | ||
|
|
0e539d91fe | ||
|
|
2ff1e67e8a | ||
|
|
35c3d54688 | ||
|
|
50f60b18e7 | ||
|
|
bc5c2ce059 | ||
|
|
21d5f117ee | ||
|
|
319d196e93 | ||
|
|
96863b3196 | ||
|
|
9fde36dca1 | ||
|
|
108daedff5 | ||
|
|
bf786e8872 | ||
|
|
3eb09b66ec | ||
|
|
cb00826f51 | ||
|
|
15ab80a475 | ||
|
|
3e32db74cc | ||
|
|
db07d7a73d | ||
|
|
65899b0ef0 | ||
|
|
63c3d07cb9 | ||
|
|
246e0d76c1 | ||
|
|
1a15d2c039 | ||
|
|
90e32df3fb | ||
|
|
8ef2c4013a | ||
|
|
a460bc25d6 | ||
|
|
bde303ec45 | ||
|
|
6512c465f0 | ||
|
|
59de6b0035 | ||
|
|
3931625133 | ||
|
|
6a0b7d6b7d | ||
|
|
6817c45ddc | ||
|
|
6f216cc1aa | ||
|
|
ad4c383adf | ||
|
|
8864e79db1 | ||
|
|
dd7a7fa796 | ||
|
|
d4422f89aa | ||
|
|
cdff98b109 | ||
|
|
835e16e54d | ||
|
|
d46afab6dd | ||
|
|
05e582793d | ||
|
|
fc62d85c23 | ||
|
|
940af6d367 | ||
|
|
c19717e84c | ||
|
|
080951fb34 | ||
|
|
245c59e942 | ||
|
|
99dc87d715 | ||
|
|
e48924d987 | ||
|
|
2b06a42ac5 | ||
|
|
d8c99221ff | ||
|
|
7c48e53329 | ||
|
|
a0290609f9 | ||
|
|
821fa724e8 | ||
|
|
5adab1885b | ||
|
|
240dc46a23 | ||
|
|
3d621c68cb | ||
|
|
dd47b6c6ad | ||
|
|
37cec4c46f | ||
|
|
108c0a6176 | ||
|
|
ec24d0eaae | ||
|
|
6912c082b1 | ||
|
|
8a6c54c4d5 | ||
|
|
85f21550cb | ||
|
|
fe8ed4e346 | ||
|
|
2d0464c097 | ||
|
|
6dfc43abf1 | ||
|
|
c52900e713 | ||
|
|
4378489d80 | ||
|
|
ca5977db75 | ||
|
|
639720b6fd | ||
|
|
79f4d02350 | ||
|
|
f8afb396ed | ||
|
|
884ab8c783 | ||
|
|
ac055dc2e0 | ||
|
|
32cc86ec99 | ||
|
|
6fae79560e | ||
|
|
f8ce38f11e | ||
|
|
b114a8f8fc | ||
|
|
ad65fcdffc | ||
|
|
0228072c6f | ||
|
|
87747a83b9 | ||
|
|
efec507bf8 | ||
|
|
1d65184241 | ||
|
|
0501deac2d | ||
|
|
d3f9a9c3a0 | ||
|
|
95c7d5bc2c | ||
|
|
91217b6d5e | ||
|
|
af875ab035 | ||
|
|
8ada28775e | ||
|
|
6ebcbc8738 | ||
|
|
42af73cdff | ||
|
|
71f885b899 | ||
|
|
b325bd9b18 | ||
|
|
a0ecd65e70 | ||
|
|
2ef0fc9415 | ||
|
|
ba6eb26e6e | ||
|
|
09380db661 | ||
|
|
b208294185 | ||
|
|
52c1676005 | ||
|
|
448c4546f2 | ||
|
|
9c9f6645d3 | ||
|
|
9102d974a5 | ||
|
|
b2738db441 | ||
|
|
c667118f10 | ||
|
|
218a4a761a | ||
|
|
e1dc58d456 | ||
|
|
e9137fccc7 | ||
|
|
facc3acf31 | ||
|
|
c2d5d475b9 | ||
|
|
e17d90ce5f | ||
|
|
9fed15f88b | ||
|
|
7cef3fe24b | ||
|
|
4a9b30d4d5 | ||
|
|
40d94141c4 | ||
|
|
023badc39c | ||
|
|
e78430db62 | ||
|
|
8ee4768f58 | ||
|
|
c581a8016c | ||
|
|
293692d5c5 | ||
|
|
7b7e6e4db0 | ||
|
|
cd54a2093e | ||
|
|
92099dc763 | ||
|
|
9d27379b25 | ||
|
|
b84546826f | ||
|
|
421aa65e6d | ||
|
|
fac3af6360 | ||
|
|
a9d34dfcc8 | ||
|
|
9bfbf229db | ||
|
|
2d11c0f61e | ||
|
|
fe033d68cf | ||
|
|
64ad07b9db | ||
|
|
e7f8288244 | ||
|
|
496d60c3a7 | ||
|
|
5f92920195 | ||
|
|
907571bd83 | ||
|
|
5bc37379fc | ||
|
|
0d943516a3 | ||
|
|
5f76067190 | ||
|
|
81446fb4ef | ||
|
|
edf2e32c98 | ||
|
|
46af4556a9 | ||
|
|
d349354c9d | ||
|
|
a3c59f1ec3 | ||
|
|
73289148df | ||
|
|
abd7cf090a | ||
|
|
30d035f8c6 | ||
|
|
e8b3419933 | ||
|
|
fd5ace58b4 | ||
|
|
60e5f1ae85 | ||
|
|
61d707482a | ||
|
|
26d0bda8b2 | ||
|
|
ce8d53a513 | ||
|
|
a99e88b46c | ||
|
|
9fd4e6c8f7 | ||
|
|
410dbbc8b1 | ||
|
|
9273a0abcf | ||
|
|
f68cb4ae13 | ||
|
|
e7d8fdf590 | ||
|
|
9ef5fd12e0 | ||
|
|
2c69af2149 | ||
|
|
5dd59c0053 | ||
|
|
ebc943bd70 | ||
|
|
ee3d995117 | ||
|
|
9c527e3fce | ||
|
|
20c2f1950b | ||
|
|
b8c90aa8d5 | ||
|
|
f6c5769d47 | ||
|
|
c06b1badc4 | ||
|
|
94a6b53f4a | ||
|
|
1d60635e3b | ||
|
|
a84f009ad9 | ||
|
|
c6e480e89c | ||
|
|
af53f06eac | ||
|
|
93a6df362c | ||
|
|
9a80a006ce | ||
|
|
a3e9971379 | ||
|
|
ac0d17e57e | ||
|
|
b9f0b0d1d7 | ||
|
|
7340b48b70 | ||
|
|
6146d659cc | ||
|
|
d757ffd641 | ||
|
|
5cd6b6b7c5 | ||
|
|
ec42dad728 | ||
|
|
a97b36ebdd | ||
|
|
723d9cd62c | ||
|
|
717de22f58 | ||
|
|
d91d062c96 | ||
|
|
63d48032c5 | ||
|
|
f329fe21af | ||
|
|
ba2de7ece5 | ||
|
|
77ef86413d | ||
|
|
2860837741 | ||
|
|
e6acf52638 | ||
|
|
4c9dbcb96d | ||
|
|
1de788ea68 | ||
|
|
617892eb93 | ||
|
|
986dc6c1c0 | ||
|
|
a7e7ff61ef | ||
|
|
1031bbbce7 | ||
|
|
9d31120f1a | ||
|
|
4bceaf8a25 | ||
|
|
185daf470d | ||
|
|
723cbcf99c | ||
|
|
c2ebfc72ef | ||
|
|
92ddaa568a | ||
|
|
1845ec8469 | ||
|
|
9bccacea47 | ||
|
|
b06d87d2ee | ||
|
|
fc6eb4be33 | ||
|
|
a3e3c78c5c | ||
|
|
927c859456 | ||
|
|
354152dbb5 | ||
|
|
954309d034 | ||
|
|
91474f1f0c | ||
|
|
d6cb8674f7 | ||
|
|
2a627ef988 | ||
|
|
c76f39c0fa | ||
|
|
cc497b29ab | ||
|
|
8d112d2e93 | ||
|
|
e9f6d6ba4d | ||
|
|
88452ea519 | ||
|
|
ff7be0d637 | ||
|
|
c2ee92878c | ||
|
|
02b1e03611 | ||
|
|
6e0c84ccad | ||
|
|
bd125d2915 | ||
|
|
2cc40cbff9 | ||
|
|
8237eb56ab | ||
|
|
20825d66fe | ||
|
|
28db6fb32e | ||
|
|
583eb10b83 | ||
|
|
c1b99958f4 | ||
|
|
050f48ac2a | ||
|
|
e4ec5b3eb1 | ||
|
|
953ff45085 | ||
|
|
2ebea847c1 | ||
|
|
4e89a95e35 | ||
|
|
ede51872e2 | ||
|
|
d36569d258 | ||
|
|
70f00b6bb4 | ||
|
|
f5617aca1c | ||
|
|
bdaa78b919 | ||
|
|
32ecd52f2b | ||
|
|
f1c21be4a0 | ||
|
|
7a5bf2ffc4 | ||
|
|
f5ea9d0fda | ||
|
|
168b38fc4b | ||
|
|
de41301a44 | ||
|
|
7c0c440df2 | ||
|
|
0d636aa04c | ||
|
|
f4940dceb1 | ||
|
|
28e0d712ab | ||
|
|
447f7d6459 | ||
|
|
d325e6b060 | ||
|
|
0286fa4ed0 | ||
|
|
df74e6ddd1 | ||
|
|
3b3770d6b9 | ||
|
|
e6d931729c | ||
|
|
7165a2159d | ||
|
|
9e03a5f7bd | ||
|
|
1bc988d9d1 | ||
|
|
dafbfb4198 | ||
|
|
e07ff45ae6 | ||
|
|
f41763b0b9 | ||
|
|
59a1c38d34 | ||
|
|
09d624c24b | ||
|
|
81afe28901 | ||
|
|
c88038e95e | ||
|
|
962ebc9ef0 | ||
|
|
1733179bfb | ||
|
|
2108712f2d | ||
|
|
982d57c9ae | ||
|
|
38b8000d2a | ||
|
|
d5387c5d46 | ||
|
|
081c9d068f | ||
|
|
aa503d6a74 | ||
|
|
ed50e2b392 | ||
|
|
ea4a68e06c | ||
|
|
45247e6d59 | ||
|
|
6fd2ad21ec | ||
|
|
b7d5602cce | ||
|
|
53df1a03a3 | ||
|
|
0f81d877ef | ||
|
|
a3ca5a26ae | ||
|
|
dacaef6fa4 | ||
|
|
78e7468715 | ||
|
|
d48add55b6 | ||
|
|
bb3ed4cea4 | ||
|
|
fddcdec81f | ||
|
|
0e7de456f6 | ||
|
|
64408495ee | ||
|
|
228cff3cfd | ||
|
|
f3f2c8485a | ||
|
|
83459b4c78 | ||
|
|
9d041aaa7a | ||
|
|
0c0f9e6ccf | ||
|
|
d6ed860bc3 | ||
|
|
c6443f24b2 | ||
|
|
2b64a49359 | ||
|
|
287fb9b9d6 | ||
|
|
c286f1c5f3 | ||
|
|
977e0c9008 | ||
|
|
497a3104f0 | ||
|
|
adc5e324a7 | ||
|
|
2c6cca6bf6 | ||
|
|
debb33a63d | ||
|
|
5252f45073 | ||
|
|
610c42a1ae | ||
|
|
fcb1bba7fa | ||
|
|
764a20a36b | ||
|
|
b8dbec46bb | ||
|
|
a86fd9cf06 | ||
|
|
8f16e0167c | ||
|
|
60a8f72be8 | ||
|
|
61a5b1a337 | ||
|
|
f8dfa5a6e0 | ||
|
|
9aea091f53 | ||
|
|
4bee4584dc | ||
|
|
5954141c15 | ||
|
|
05754d3e42 | ||
|
|
4f5fd6c463 | ||
|
|
ca27949989 | ||
|
|
06b2a8757e | ||
|
|
8ac74ed397 | ||
|
|
0e2b317eb8 | ||
|
|
15bc3c45a0 | ||
|
|
ded6ee8a65 | ||
|
|
c1af40ff5c | ||
|
|
f06edd723d | ||
|
|
02f1fe48c6 | ||
|
|
87d269ba5c | ||
|
|
6e6d765699 | ||
|
|
a25327d370 | ||
|
|
ed285e9ac5 | ||
|
|
c42d17897c | ||
|
|
be81091698 | ||
|
|
3cba838412 | ||
|
|
38357f7efa | ||
|
|
2c7b814d37 | ||
|
|
e1f7262f2a | ||
|
|
400078dce5 | ||
|
|
6de6ad661d | ||
|
|
7c8e9bb5ec | ||
|
|
d1ff06840e | ||
|
|
a0dfb8afbd | ||
|
|
1d301ac78d | ||
|
|
8875f6d487 | ||
|
|
66bfef28c0 | ||
|
|
f03b8a3a14 | ||
|
|
62ebd0627d | ||
|
|
bb1a4747f5 | ||
|
|
d640859940 | ||
|
|
e660e1e727 | ||
|
|
ad89aea45c | ||
|
|
c73416bdb8 | ||
|
|
6253aaa015 | ||
|
|
0d2d861896 | ||
|
|
0baf4a094a | ||
|
|
353474dacd | ||
|
|
5f38b579fe | ||
|
|
1e2bc29a60 | ||
|
|
fafd162db0 | ||
|
|
ec5e98b934 | ||
|
|
18e3a16299 | ||
|
|
efe4194f9c | ||
|
|
4249a0beb1 | ||
|
|
461132b95e | ||
|
|
19af0b36a2 | ||
|
|
6fc4fc294a | ||
|
|
a6b6295426 | ||
|
|
029dfd4688 | ||
|
|
2587350d1e | ||
|
|
d400ebfda0 | ||
|
|
34c8a73ee5 | ||
|
|
4ecb58cd5c | ||
|
|
c2c8c1719e | ||
|
|
1305335f0a | ||
|
|
15b51921b2 | ||
|
|
54f9b712e4 | ||
|
|
5afd76fb45 | ||
|
|
54c8d5b7b8 | ||
|
|
e4e9267c08 | ||
|
|
655b677961 | ||
|
|
8faa7bd68d | ||
|
|
f618055aab | ||
|
|
54fa4bccf6 | ||
|
|
24d4070667 | ||
|
|
7b1c3665d5 | ||
|
|
7d80c3eda6 | ||
|
|
f7aa313aea | ||
|
|
c9576d98e4 | ||
|
|
5d36ecbbbb | ||
|
|
bcc2abf472 | ||
|
|
933ca3ecca | ||
|
|
acaff825c1 | ||
|
|
9169bfabad | ||
|
|
fb6dc5e8f1 | ||
|
|
d60bb6a19e | ||
|
|
996f238648 | ||
|
|
7c744f0e3d | ||
|
|
2f54a948be | ||
|
|
652d75a939 | ||
|
|
104c9803f7 | ||
|
|
bc93604576 | ||
|
|
a1af1ff3d2 | ||
|
|
47d77a3198 | ||
|
|
82b4643c78 | ||
|
|
0096907368 | ||
|
|
1504127979 | ||
|
|
6f7306cd37 | ||
|
|
063d468836 | ||
|
|
7e88de182e | ||
|
|
bf4319d978 | ||
|
|
c3d89b6509 | ||
|
|
cb731099e6 | ||
|
|
b6073f7e6f | ||
|
|
9b2d74c3ad | ||
|
|
7713848067 | ||
|
|
c8a901d3ff | ||
|
|
d02e1b3640 | ||
|
|
19dbf815e6 | ||
|
|
df70a791fd | ||
|
|
a84036e0c6 | ||
|
|
973070f22b | ||
|
|
5f0df5ed53 | ||
|
|
80be279e11 | ||
|
|
1aaafbf435 | ||
|
|
1dd1283d56 | ||
|
|
184ec5f1c0 | ||
|
|
56e5c33c3e | ||
|
|
37c5278abf | ||
|
|
d1482e38fe | ||
|
|
1cce6137a0 | ||
|
|
27ea0ddd98 | ||
|
|
bd7992a45a | ||
|
|
95a0769eaa | ||
|
|
af9713d5b1 | ||
|
|
ab9f771829 | ||
|
|
fa0c91b22c | ||
|
|
2e5f5b48fa | ||
|
|
a52ae2dba1 | ||
|
|
19cd163b30 | ||
|
|
06b687e899 | ||
|
|
fa42c2202e | ||
|
|
0537544de2 | ||
|
|
be1c119799 | ||
|
|
76f398dd16 | ||
|
|
064d3e5dbb | ||
|
|
c6c3243234 | ||
|
|
60b183b094 | ||
|
|
9b5fb43ad3 | ||
|
|
7c58261b72 | ||
|
|
712ca71656 | ||
|
|
c8ad8a693b | ||
|
|
1be74096f6 | ||
|
|
92b4eec36a | ||
|
|
44038fbdce | ||
|
|
b4c7996d1a | ||
|
|
39fd8a5457 | ||
|
|
24da9b7b5a | ||
|
|
3edb8db94b | ||
|
|
909b4b1c0b | ||
|
|
38f2ab252e | ||
|
|
f0772b147f | ||
|
|
9fc9d5c642 | ||
|
|
73db82042c | ||
|
|
a14f558258 | ||
|
|
97081d46c4 | ||
|
|
60f83b334e | ||
|
|
340ac869ce | ||
|
|
b2b6ba4921 | ||
|
|
3274a06e34 | ||
|
|
e9850bfc56 | ||
|
|
529c42cccf | ||
|
|
2a00227486 | ||
|
|
5252edbf70 | ||
|
|
74c15b4f42 | ||
|
|
d482b9baf6 | ||
|
|
cde216523e | ||
|
|
8aeb815b5a | ||
|
|
3c602268e3 | ||
|
|
9177fb4d77 | ||
|
|
e3f1fafad9 | ||
|
|
32bf923c1a | ||
|
|
d3a0e8067e | ||
|
|
105d5007cf | ||
|
|
bafa486668 | ||
|
|
80a2b34d43 | ||
|
|
a5e1e38e74 | ||
|
|
0486e9e37b | ||
|
|
09722d8678 | ||
|
|
cd22e38660 | ||
|
|
f83fe76280 | ||
|
|
b9e1e6030f | ||
|
|
4874b53c7c | ||
|
|
b1a48f4f27 | ||
|
|
3fee9cbb42 | ||
|
|
a2a460a883 | ||
|
|
6bcd67a906 | ||
|
|
9e2d253fb6 | ||
|
|
a7efc82944 | ||
|
|
25f4bb5557 | ||
|
|
74d6b7edc5 | ||
|
|
1204b5b1a6 | ||
|
|
59ddc965ec | ||
|
|
9cc4ffaf33 | ||
|
|
11ba63d086 | ||
|
|
06d2aba57c | ||
|
|
7ecaad529f | ||
|
|
5ef3aa4153 | ||
|
|
5e83e14637 | ||
|
|
8dbc10efd7 | ||
|
|
592f0540f9 | ||
|
|
0e28f77a1f | ||
|
|
618609dbfa | ||
|
|
c86f1f5546 | ||
|
|
a09a028dc9 | ||
|
|
44da71bcdd | ||
|
|
343ea0c306 | ||
|
|
a60f22ef5c | ||
|
|
d614070f44 | ||
|
|
fd5d81e399 | ||
|
|
841d3ac115 | ||
|
|
3df101a91d | ||
|
|
3124c29052 | ||
|
|
a4a4eda0eb | ||
|
|
7d418b91b4 | ||
|
|
9ef7771adc | ||
|
|
d0a8b678d3 | ||
|
|
6d52cc7c73 | ||
|
|
2acc034fc1 | ||
|
|
51a2cb91f2 | ||
|
|
44d045f546 | ||
|
|
6be206ff28 | ||
|
|
c4badb9daf | ||
|
|
f8ad766695 | ||
|
|
a55d4634b1 | ||
|
|
42bc4c8acb | ||
|
|
efd273bfa5 | ||
|
|
3fb71b74cd | ||
|
|
2bb3e3ea48 | ||
|
|
59ec877ddd | ||
|
|
31e9025df3 | ||
|
|
1b42bc0e75 | ||
|
|
0ad3f12686 | ||
|
|
6de775a1bf | ||
|
|
1d74b9d782 | ||
|
|
933a5f5e7c | ||
|
|
cd280802de | ||
|
|
b53a4e5bf8 | ||
|
|
c20648c33a | ||
|
|
fa32948508 | ||
|
|
9ec349e733 | ||
|
|
4fa2bf1b29 | ||
|
|
3d01a76296 | ||
|
|
f9c0539d68 | ||
|
|
dde1495854 | ||
|
|
065f87c2e8 | ||
|
|
c049cf440f | ||
|
|
1149730962 | ||
|
|
16de8cb91b | ||
|
|
caec05df14 | ||
|
|
e387f4ca1c | ||
|
|
6d7ffe6a25 | ||
|
|
ad0c0f6865 | ||
|
|
dd541e5f70 |
14
.devcontainer/devcontainer.json
Normal file
14
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,14 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||
{
|
||||
"name": "Actual development",
|
||||
"dockerComposeFile": [
|
||||
"../docker-compose.yml",
|
||||
"docker-compose.yml"
|
||||
],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
"service": "actual-development",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"postCreateCommand": "yarn install"
|
||||
}
|
||||
6
.devcontainer/docker-compose.yml
Normal file
6
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
actual-development:
|
||||
volumes:
|
||||
- ..:/workspaces:cached
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
79
.eslintrc.js
79
.eslintrc.js
@@ -1,79 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
const rulesDirPlugin = require('eslint-plugin-rulesdir');
|
||||
rulesDirPlugin.RULES_DIR = path.join(
|
||||
__dirname,
|
||||
'packages',
|
||||
'eslint-plugin-actual',
|
||||
'lib',
|
||||
'rules',
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
plugins: ['prettier', 'import', 'rulesdir', '@typescript-eslint'],
|
||||
extends: ['react-app', 'plugin:@typescript-eslint/recommended'],
|
||||
reportUnusedDisableDirectives: true,
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
|
||||
'no-restricted-globals': ['error'].concat(
|
||||
require('confusing-browser-globals').filter(g => g !== 'self'),
|
||||
),
|
||||
|
||||
'react/jsx-no-useless-fragment': 'error',
|
||||
|
||||
'rulesdir/typography': 'error',
|
||||
|
||||
// https://github.com/eslint/eslint/issues/16954
|
||||
// https://github.com/eslint/eslint/issues/16953
|
||||
'no-loop-func': 'off',
|
||||
|
||||
// TODO: re-enable these rules
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
|
||||
'import/no-useless-path-segments': 'error',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
alphabetize: {
|
||||
caseInsensitive: true,
|
||||
order: 'asc',
|
||||
},
|
||||
groups: [
|
||||
'builtin', // Built-in types are first
|
||||
'external',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index', // Then the index file
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
pathGroups: [
|
||||
// Enforce that React (and react-related packages) is the first import
|
||||
{ group: 'builtin', pattern: 'react?(-*)', position: 'before' },
|
||||
// Separate imports from Actual from "real" external imports
|
||||
{
|
||||
group: 'external',
|
||||
pattern: 'loot-{core,design}/**/*',
|
||||
position: 'after',
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ['react'],
|
||||
},
|
||||
],
|
||||
|
||||
// Rules disable during TS migration
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'prefer-const': 'off',
|
||||
'prefer-spread': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'none',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -8,8 +8,12 @@
|
||||
|
||||
# Declare files that will always have LF line endings on checkout.
|
||||
*.js text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.tsx text eol=lf
|
||||
|
||||
yarn.lock text eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpg binary
|
||||
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Funding policies: https://actualbudget.org/docs/contributing/leadership/funding
|
||||
open_collective: actual
|
||||
github: actualbudget
|
||||
21
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
21
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,13 +1,20 @@
|
||||
name: Bug Report
|
||||
description: File a bug report also known as an issue or problem.
|
||||
title: '[Bug]: '
|
||||
labels: ['bug', 'needs triage']
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
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:
|
||||
@@ -28,12 +35,13 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: errors-received
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 'What error did you receive?'
|
||||
description: 'If you received an error or a message on the screen, please provide that here.'
|
||||
label: How can we reproduce the issue?
|
||||
description: Please give step-by-step instructions on how to reproduce the issue. In most cases this might also require uploading a sample budget/import file.
|
||||
value: 'How can we reproduce the issue?'
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: markdown
|
||||
id: env-info
|
||||
attributes:
|
||||
@@ -47,7 +55,9 @@ body:
|
||||
- Locally via Yarn
|
||||
- Docker
|
||||
- Fly.io
|
||||
- Pikapods
|
||||
- NAS
|
||||
- Desktop App (Electron)
|
||||
- Other
|
||||
validations:
|
||||
required: false
|
||||
@@ -61,6 +71,7 @@ body:
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Desktop App (Electron)
|
||||
- Other
|
||||
- type: dropdown
|
||||
id: operating-system
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,11 @@
|
||||
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.
|
||||
- name: Translations
|
||||
url: https://hosted.weblate.org/projects/actualbudget/actual/
|
||||
about: Found a string that needs a better translation? Add your suggestion or upvote an existing one in Weblate.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Feature request
|
||||
description: Request a missing feature
|
||||
title: '[Feature] '
|
||||
labels: ['feature', 'needs triage']
|
||||
labels: ['feature']
|
||||
body:
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
|
||||
30
.github/actions/bump-package-versions
vendored
Executable file
30
.github/actions/bump-package-versions
vendored
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
version="${1#v}"
|
||||
else
|
||||
version=""
|
||||
fi
|
||||
|
||||
files_to_bump=(
|
||||
packages/api/package.json
|
||||
packages/desktop-client/package.json
|
||||
packages/desktop-electron/package.json
|
||||
)
|
||||
|
||||
for file in "${files_to_bump[@]}"; do
|
||||
if [ -z "$version" ]; then
|
||||
# version format: YY.MM.patch
|
||||
# logic: if before the 25th, bump patch, else set minor/major to next month
|
||||
version="$(jq -r .version "$file" | perl -e '($y,$m,$p)=split/\./,<>;$d=(localtime)[3];$d>25?($p=0,++$m,$m>12&&($m=1,++$y)):$p++;print"$y.$m.$p\n"')"
|
||||
if [ -z "$version" ]; then
|
||||
echo "Error: Failed to calculate new version" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Bumping $file to version $version"
|
||||
jq '.version = "'"$version"'"' "$file" > "$file.tmp"
|
||||
mv "$file.tmp" "$file"
|
||||
done
|
||||
60
.github/actions/check-migrations.js
vendored
Normal file
60
.github/actions/check-migrations.js
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// overview:
|
||||
// 1. Identify the migrations in packages/loot-core/migrations/* on `master` and HEAD
|
||||
// 2. Make sure that any new migrations on HEAD are dated after the latest migration on `master`.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const migrationsDir = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'packages',
|
||||
'loot-core',
|
||||
'migrations',
|
||||
);
|
||||
|
||||
function readMigrations(ref) {
|
||||
const { stdout } = spawnSync('git', [
|
||||
'ls-tree',
|
||||
'--name-only',
|
||||
ref,
|
||||
migrationsDir + '/',
|
||||
]);
|
||||
const files = stdout.toString().split('\n').filter(Boolean);
|
||||
console.log(`Found ${files.length} migrations on ${ref}.`);
|
||||
return files
|
||||
.map(file => path.basename(file))
|
||||
.filter(file => !file.startsWith('.'))
|
||||
.map(name => ({
|
||||
date: parseInt(name.split('_')[0]),
|
||||
name: name.match(/^\d+_(.+?)(\.sql)?$/)?.[1] ?? '***' + name,
|
||||
}));
|
||||
}
|
||||
|
||||
spawnSync('git', ['fetch', 'origin', 'master']);
|
||||
let masterMigrations = readMigrations('origin/master');
|
||||
let headMigrations = readMigrations('HEAD');
|
||||
|
||||
let latestMasterMigration = masterMigrations[masterMigrations.length - 1].date;
|
||||
let newMigrations = headMigrations.filter(
|
||||
migration => !masterMigrations.find(m => m.name === migration.name),
|
||||
);
|
||||
let badMigrations = newMigrations.filter(
|
||||
migration => migration.date <= latestMasterMigration,
|
||||
);
|
||||
|
||||
if (badMigrations.length) {
|
||||
console.error(
|
||||
`The following migrations are dated before the latest migration on master:`,
|
||||
);
|
||||
badMigrations.forEach(migration => {
|
||||
console.error(` ${migration.name}`);
|
||||
});
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`All migrations are dated after the latest migration on master.`);
|
||||
}
|
||||
184
.github/actions/handle-feature-requests.js
vendored
Normal file
184
.github/actions/handle-feature-requests.js
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// overview:
|
||||
// 1. Fetch the issues that are linked to the PR
|
||||
// 2. Filter out the issues that are not feature requests
|
||||
// 3. For each feature request:
|
||||
// 1. Remove the 'help wanted' & 'needs votes' labels
|
||||
// 3. Find the automated comment, hide the comment as 'outdated'
|
||||
// 5. Post a new comment saying that the feature request has been implemented, and will be released in the next version. Link to the PR.
|
||||
|
||||
async function makeAPIRequest(query, variables) {
|
||||
const res = await fetch('https://api.github.com/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function group(name, body) {
|
||||
console.log(`::group::${name}`);
|
||||
const result = body();
|
||||
if (result instanceof Promise) {
|
||||
return result.finally(() => console.log(`::endgroup::`));
|
||||
}
|
||||
console.log(`::endgroup::`);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const featureRequests = await group('Pull Request API Response', async () => {
|
||||
const res = await makeAPIRequest(
|
||||
/* GraphQL */ `
|
||||
query FetchLinkedIssues($pr: Int!) {
|
||||
repository(owner: "actualbudget", name: "actual") {
|
||||
pullRequest(number: $pr) {
|
||||
closingIssuesReferences(first: 10) {
|
||||
nodes {
|
||||
id
|
||||
number
|
||||
labels(first: 10) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ pr: parseInt(process.env.PR_NUMBER) },
|
||||
);
|
||||
|
||||
console.log(JSON.stringify(res, null, 2));
|
||||
|
||||
return res.data.repository.pullRequest.closingIssuesReferences.nodes.filter(
|
||||
issue => issue.labels.nodes.some(label => label.name === 'feature'),
|
||||
);
|
||||
});
|
||||
|
||||
if (featureRequests.length === 0) {
|
||||
console.log('No linked feature requests found');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { id, number, labels } of featureRequests) {
|
||||
await group(`Issue #${number}: Remove labels`, async () => {
|
||||
const toRemove = labels.nodes
|
||||
.filter(
|
||||
label =>
|
||||
label.name === 'help wanted' ||
|
||||
label.name === 'needs votes' ||
|
||||
label.name === 'good first issue',
|
||||
)
|
||||
.map(label => label.id);
|
||||
const res = await makeAPIRequest(
|
||||
/* GraphQL */ `
|
||||
mutation RemoveLabels($issue: ID!, $labels: [ID!]!) {
|
||||
removeLabelsFromLabelable(
|
||||
input: {
|
||||
clientMutationId: "1"
|
||||
labelIds: $labels
|
||||
labelableId: $issue
|
||||
}
|
||||
) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
issue: id,
|
||||
labels: toRemove,
|
||||
},
|
||||
);
|
||||
console.log(JSON.stringify(res, null, 2));
|
||||
});
|
||||
|
||||
await group(`Issue #${number}: Collapse automatic comment`, async () => {
|
||||
const commentRes = await makeAPIRequest(
|
||||
/* GraphQL */ `
|
||||
query FetchComments($issue: Int!) {
|
||||
repository(owner: "actualbudget", name: "actual") {
|
||||
issue(number: $issue) {
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
body
|
||||
author {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ issue: number },
|
||||
);
|
||||
console.log(JSON.stringify(commentRes, null, 2));
|
||||
|
||||
const comments = commentRes.data.repository.issue.comments.nodes.filter(
|
||||
comment => comment.author.login === 'github-actions',
|
||||
);
|
||||
const commentToCollapse =
|
||||
comments.find(comment =>
|
||||
comment.body.includes('<!-- feature-auto-close-comment -->'),
|
||||
) ||
|
||||
comments.find(comment =>
|
||||
comment.body.includes(
|
||||
':sparkles: Thanks for sharing your idea! :sparkles:',
|
||||
),
|
||||
);
|
||||
|
||||
if (!commentToCollapse) {
|
||||
console.log('No comment to collapse found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const res = await makeAPIRequest(
|
||||
/* GraphQL */ `
|
||||
mutation CollapseComment($comment: ID!) {
|
||||
minimizeComment(
|
||||
input: { classifier: OUTDATED, subjectId: $comment }
|
||||
) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ comment: commentToCollapse.id },
|
||||
);
|
||||
console.log(JSON.stringify(res, null, 2));
|
||||
});
|
||||
|
||||
await group(`Issue #${number}: Post comment`, async () => {
|
||||
const res = await makeAPIRequest(
|
||||
/* GraphQL */ `
|
||||
mutation PostComment($issue: ID!, $body: String!) {
|
||||
addComment(
|
||||
input: { subjectId: $issue, body: $body, clientMutationId: "1" }
|
||||
) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
issue: id,
|
||||
body: `:tada: This feature has been implemented in #${process.env.PR_NUMBER} and will be released in the next version. Thanks for sharing your idea! :tada:\n\n<!-- feature-implemented-comment -->`,
|
||||
},
|
||||
);
|
||||
console.log(JSON.stringify(res, null, 2));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -9,8 +9,8 @@ function get_status() {
|
||||
curl --header "Authorization: Bearer $GITHUB_TOKEN" "https://api.github.com/repos/actualbudget/actual/commits/$COMMIT_SHA/statuses" > /tmp/status.json
|
||||
cat /tmp/status.json
|
||||
echo "::endgroup::"
|
||||
netlify=$(jq '[.[] | select(.context == "netlify/actualbudget/deploy-preview")][0]' /tmp/status.json)
|
||||
state=$(jq -r '.state' <<< "$netlify")
|
||||
netlify=$(yarn jq '[.[] | select(.context == "netlify/actualbudget/deploy-preview")][0]' /tmp/status.json)
|
||||
state=$(yarn jq -r '.state' <<< "$netlify")
|
||||
echo "::group::Netlify Status"
|
||||
echo "$netlify"
|
||||
echo "::endgroup::"
|
||||
@@ -32,7 +32,7 @@ done
|
||||
|
||||
if [ "$state" == "success" ]; then
|
||||
echo -e "\033[0;32mNetlify build succeeded!\033[0m"
|
||||
jq -r '"url=" + .target_url' <<< "$netlify" > $GITHUB_OUTPUT
|
||||
yarn jq -r '"url=" + .target_url' <<< "$netlify" > $GITHUB_OUTPUT
|
||||
exit 0
|
||||
else
|
||||
echo -e "\033[0;31mNetlify build failed. Cancelling end-to-end tests.\033[0m"
|
||||
35
.github/actions/setup/action.yml
vendored
35
.github/actions/setup/action.yml
vendored
@@ -1,19 +1,44 @@
|
||||
name: Setup
|
||||
|
||||
inputs:
|
||||
working-directory:
|
||||
description: 'Working directory to run in, default .'
|
||||
required: false
|
||||
default: '.'
|
||||
download-translations:
|
||||
description: 'Whether to download translations as part of setup, default true'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.15.0
|
||||
node-version: 18.16.0
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
shell: bash
|
||||
if: ${{ env.ACT }}
|
||||
- name: Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: yarn-v1-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
key: yarn-v1-${{ runner.os }}-${{ hashFiles(format('{0}/.nvmrc', inputs.working-directory)) }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
- name: Install
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: yarn --immutable
|
||||
shell: bash
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- name: Download translations
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: actualbudget/translations
|
||||
path: ${{ inputs.working-directory }}/packages/desktop-client/locale
|
||||
if: ${{ inputs.download-translations == 'true' }}
|
||||
- name: Remove untranslated languages
|
||||
run: packages/desktop-client/bin/remove-untranslated-languages
|
||||
shell: bash
|
||||
if: ${{ inputs.download-translations == 'true' }}
|
||||
|
||||
69
.github/workflows/build.yml
vendored
69
.github/workflows/build.yml
vendored
@@ -12,46 +12,73 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches: '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
||||
|
||||
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
|
||||
run: cd packages/api && yarn build
|
||||
- name: Create package tgz
|
||||
run: cd packages/api && yarn pack && mv package.tgz actual-api.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-api
|
||||
path: packages/api/actual-api.tgz
|
||||
|
||||
crdt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build CRDT
|
||||
run: cd packages/crdt && yarn build
|
||||
- name: Create package tgz
|
||||
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-crdt
|
||||
path: packages/crdt/actual-crdt.tgz
|
||||
|
||||
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@v4
|
||||
with:
|
||||
name: build-stats
|
||||
path: packages/desktop-client/build-stats
|
||||
|
||||
# TODO: re-enable after solving https://github.com/actualbudget/actual/issues/468
|
||||
# electron:
|
||||
# # As electron builds take longer, we only run them in master.
|
||||
# if: github.event_name != 'pull_request'
|
||||
# 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
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Server
|
||||
run: cd packages/sync-server && yarn build
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sync-server
|
||||
path: packages/sync-server/build
|
||||
|
||||
16
.github/workflows/check-release-notes.yml
vendored
16
.github/workflows/check-release-notes.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: Check release notes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- '!release/*'
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Check release notes
|
||||
uses: actualbudget/actions/release-notes/check@main
|
||||
48
.github/workflows/check.yml
vendored
Normal file
48
.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Test
|
||||
|
||||
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:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
migrations:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19'
|
||||
- name: Check migrations
|
||||
run: node ./.github/actions/check-migrations.js
|
||||
11
.github/workflows/codeql.yml
vendored
11
.github/workflows/codeql.yml
vendored
@@ -4,10 +4,13 @@ on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '23 11 * * 6'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -19,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'
|
||||
|
||||
90
.github/workflows/docker-edge.yml
vendored
Normal file
90
.github/workflows/docker-edge.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: Build Edge Docker Image
|
||||
|
||||
# Edge Docker images are built for every commit, and daily
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/sync-server/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/sync-server/**'
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
IMAGES: |
|
||||
actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual
|
||||
|
||||
# Creates the following tags:
|
||||
# - actual-server:edge
|
||||
TAGS: |
|
||||
type=edge,value=edge
|
||||
type=sha
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.event.repository.fork == false }}
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu, alpine]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# Push to both Docker Hub and Github Container Registry
|
||||
images: ${{ env.IMAGES }}
|
||||
flavor: ${{ matrix.os != 'ubuntu' && format('suffix=-{0}', matrix.os) || '' }}
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download artifacts
|
||||
run: ./packages/sync-server/docker/download-artifacts.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: packages/sync-server/docker/edge-${{ matrix.os }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7${{ matrix.os == 'alpine' && ',linux/arm/v6' || '' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
|
||||
89
.github/workflows/docker-release.yml
vendored
Normal file
89
.github/workflows/docker-release.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: Build Stable Docker Image
|
||||
|
||||
# Stable Docker images are built for every new tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths-ignore:
|
||||
- README.md
|
||||
- LICENSE.txt
|
||||
|
||||
env:
|
||||
IMAGES: |
|
||||
actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual
|
||||
|
||||
# Creates the following tags:
|
||||
# - actual-server:latest (see docker/metadata-action flavor inputs, below)
|
||||
# - actual-server:1.3
|
||||
# - actual-server:1.3.7
|
||||
# - actual-server:sha-90dd603
|
||||
TAGS: |
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# Push to both Docker Hub and Github Container Registry
|
||||
images: ${{ env.IMAGES }}
|
||||
# Automatically update :latest
|
||||
flavor: latest=true
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Docker meta for Alpine image
|
||||
id: alpine-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGES }}
|
||||
# Automatically update :latest
|
||||
flavor: |
|
||||
latest=true
|
||||
suffix=-alpine,onlatest=true
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push ubuntu image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: packages/sync-server/docker/stable-ubuntu.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
- name: Build and push alpine image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: packages/sync-server/docker/stable-alpine.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
tags: ${{ steps.alpine-meta.outputs.tags }}
|
||||
56
.github/workflows/e2e-test.yml
vendored
56
.github/workflows/e2e-test.yml
vendored
@@ -1,33 +1,71 @@
|
||||
name: E2E Tests
|
||||
|
||||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
GITHUB_PR_NUMBER: ${{github.event.pull_request.number}}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run end-to-end tests on Netlify PR preview
|
||||
netlify:
|
||||
name: Wait for Netlify build to finish
|
||||
runs-on: ubuntu-latest
|
||||
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: Setup Playwright
|
||||
run: npx playwright install chromium --with-deps
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./bin/netlify-wait-for-build
|
||||
run: ./.github/actions/netlify-wait-for-build
|
||||
|
||||
functional:
|
||||
name: Functional
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- 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: ${{ steps.netlify.outputs.url }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- 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.41.1-jammy
|
||||
steps:
|
||||
- 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@v4
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
89
.github/workflows/electron-master.yml
vendored
Normal file
89
.github/workflows/electron-master.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
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 update
|
||||
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 for Mac
|
||||
if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
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: Build Electron
|
||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||
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/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
65
.github/workflows/electron-pr.yml
vendored
Normal file
65
.github/workflows/electron-pr.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
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 update
|
||||
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/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
17
.github/workflows/generate-release-notes.yml
vendored
17
.github/workflows/generate-release-notes.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: Generate Release Notes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release/*
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Generate release notes
|
||||
uses: actualbudget/actions/release-notes/generate@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
35
.github/workflows/generate-release-pr.yml
vendored
Normal file
35
.github/workflows/generate-release-pr.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Generate release PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Commit or branch to release'
|
||||
required: true
|
||||
default: 'master'
|
||||
version:
|
||||
description: 'Version number for the release (optional)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
generate-release-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Bump package versions
|
||||
id: bump_package_versions
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/bump-package-versions ${{ github.event.inputs.version }}
|
||||
echo "version=$(jq -r .version packages/desktop-client/package.json)" > $GITHUB_OUTPUT
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
title: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
body: 'Generated by [generate-release-pr.yml](../tree/master/.github/workflows/generate-release-pr.yml)'
|
||||
branch: 'release/v${{ steps.bump_package_versions.outputs.version }}'
|
||||
87
.github/workflows/i18n-string-extract-master.yml
vendored
Normal file
87
.github/workflows/i18n-string-extract-master.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Extract and upload i18n strings
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 4am UTC
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
extract-and-upload-i18n-strings:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'actualbudget/actual'
|
||||
steps:
|
||||
- name: Check out main repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: actual
|
||||
- name: Set up environment
|
||||
uses: ./actual/.github/actions/setup
|
||||
with:
|
||||
working-directory: actual
|
||||
download-translations: false # As we'll manually clone instead
|
||||
- name: Configure Git config
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: Configure i18n client
|
||||
run: |
|
||||
pip install wlc
|
||||
|
||||
- name: Lock translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
lock \
|
||||
actualbudget/actual
|
||||
|
||||
- name: Update VCS with latest translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
push \
|
||||
actualbudget/actual
|
||||
- name: Check out updated translations
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
|
||||
repository: actualbudget/translations
|
||||
path: translations
|
||||
- name: Generate i18n strings
|
||||
working-directory: actual
|
||||
run: |
|
||||
mkdir -p packages/desktop-client/locale/
|
||||
cp ../translations/en.json packages/desktop-client/locale/
|
||||
yarn generate:i18n
|
||||
if [[ ! -f packages/desktop-client/locale/en.json ]]; then
|
||||
echo "File packages/desktop-client/locale/en.json not found. Ensure the file was generated correctly."
|
||||
exit 1
|
||||
fi
|
||||
- name: Check in new i18n strings
|
||||
working-directory: translations
|
||||
run: |
|
||||
cp ../actual/packages/desktop-client/locale/en.json .
|
||||
git add .
|
||||
if git commit -m "Update source strings"; then
|
||||
git push
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
- name: Update Weblate with latest translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
pull \
|
||||
actualbudget/actual
|
||||
|
||||
- name: Unlock translations
|
||||
if: always() # Clean up even on failure
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
unlock \
|
||||
actualbudget/actual
|
||||
@@ -9,8 +9,6 @@ jobs:
|
||||
if: ${{ github.event.label.name == 'feature' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: needs votes
|
||||
@@ -26,12 +24,14 @@ jobs:
|
||||
body: |
|
||||
:sparkles: Thanks for sharing your idea! :sparkles:
|
||||
|
||||
This repository is now using lodash style issue management for enhancements. This means enhancement issues will now be closed instead of leaving them open. This doesn’t mean we don’t accept feature requests, though! We will consider implementing ones that receive many upvotes, and we welcome contributions for any feature requests marked as needing votes (just post a comment first so we can help you make a successful contribution).
|
||||
This repository uses lodash style issue management for enhancements. That means enhancement issues are automatically closed. This doesn’t mean we don’t accept feature requests, though! We will consider implementing ones that receive many upvotes, and we welcome contributions for any feature requests marked as needing votes (just post a comment first so we can help you make a successful contribution).
|
||||
|
||||
The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+
|
||||
|
||||
Don’t forget to upvote the top comment with 👍!
|
||||
|
||||
<!-- feature-auto-close-comment -->
|
||||
- name: Close Issue
|
||||
run: gh issue close "${{ github.event.issue.number }}"
|
||||
run: gh issue close "https://github.com/actualbudget/actual/issues/${{ github.event.issue.number }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
35
.github/workflows/issues-feature-implemented.yml
vendored
Normal file
35
.github/workflows/issues-feature-implemented.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Handle completed feature requests
|
||||
|
||||
##########################################################################################
|
||||
# WARNING! This workflow uses the 'pull_request_target' event. That mans that it will #
|
||||
# always run in the context of the main actualbudget/actual repo, even if the PR is from #
|
||||
# a fork. This is necessary to get access to a GitHub token that can post a comment on #
|
||||
# the PR. Be VERY CAREFUL about adding things to this workflow, since forks can inject #
|
||||
# arbitrary code into their branch, and can pollute the artifacts we download. Arbitrary #
|
||||
# code execution in this workflow could lead to a compromise of the main repo. #
|
||||
##########################################################################################
|
||||
# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests #
|
||||
##########################################################################################
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
handle-feature-requests:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# This is not a security concern because we have approved & merged the PR
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19'
|
||||
- name: Handle feature requests
|
||||
run: node .github/actions/handle-feature-requests.js
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
14
.github/workflows/issues-remove-help-wanted.yml
vendored
Normal file
14
.github/workflows/issues-remove-help-wanted.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Remove 'help wanted' label from closed issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
remove-help-wanted:
|
||||
if: ${{ !contains(github.event.issue.labels.*.name, 'feature') && contains(github.event.issue.labels.*.name, 'help wanted') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: help wanted
|
||||
18
.github/workflows/lint.yml
vendored
18
.github/workflows/lint.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Linter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches: '*'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
43
.github/workflows/netlify-release.yml
vendored
Normal file
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
|
||||
14
.github/workflows/opened-issues-triage.yml
vendored
14
.github/workflows/opened-issues-triage.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: Mark new issue for triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
needs-triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: needs triage
|
||||
21
.github/workflows/release-notes.yml
vendored
Normal file
21
.github/workflows/release-notes.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Release notes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check release notes
|
||||
if: startsWith(github.head_ref, 'release/') == false
|
||||
uses: actualbudget/actions/release-notes/check@main
|
||||
- name: Generate release notes
|
||||
if: startsWith(github.head_ref, 'release/') == true
|
||||
uses: actualbudget/actions/release-notes/generate@main
|
||||
89
.github/workflows/size-compare.yml
vendored
Normal file
89
.github/workflows/size-compare.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: Compare Sizes
|
||||
|
||||
##########################################################################################
|
||||
# WARNING! This workflow uses the 'pull_request_target' event. That mans that it will #
|
||||
# always run in the context of the main actualbudget/actual repo, even if the PR is from #
|
||||
# a fork. This is necessary to get access to a GitHub token that can post a comment on #
|
||||
# the PR. Be VERY CAREFUL about adding things to this workflow, since forks can inject #
|
||||
# arbitrary code into their branch, and can pollute the artifacts we download. Arbitrary #
|
||||
# code execution in this workflow could lead to a compromise of the main repo. #
|
||||
##########################################################################################
|
||||
# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests #
|
||||
##########################################################################################
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- '!packages/sync-server/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
compare:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for ${{github.base_ref}} build to succeed
|
||||
uses: fountainhead/action-wait-for-check@v1.2.0
|
||||
id: master-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: web
|
||||
ref: ${{github.base_ref}}
|
||||
|
||||
- name: Wait for PR build to succeed
|
||||
uses: fountainhead/action-wait-for-check@v1.2.0
|
||||
id: wait-for-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: web
|
||||
ref: ${{github.event.pull_request.head.sha}}
|
||||
|
||||
- name: Report build failure
|
||||
if: steps.wait-for-build.outputs.conclusion == 'failure'
|
||||
run: |
|
||||
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@v6
|
||||
id: pr-build
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
name: build-stats
|
||||
path: base
|
||||
|
||||
- name: Download build artifact from PR
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
name: build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
|
||||
- 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: twk3/rollup-size-compare-action@v1.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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@v2.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
current-stats-json-path: ./head/loot-core-stats.json
|
||||
base-stats-json-path: ./base/loot-core-stats.json
|
||||
title: loot-core
|
||||
32
.github/workflows/stale.yml
vendored
32
.github/workflows/stale.yml
vendored
@@ -1,20 +1,26 @@
|
||||
name: Close inactive issues
|
||||
name: 'Close stale PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v7
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: -1
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "🚧🚨 This issue is being marked as stale due to 90 days of inactivity. 🚧🚨"
|
||||
only-labels: 'needs triage'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
days-before-issue-stale: -1
|
||||
stale-wip:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
|
||||
days-before-stale: 7
|
||||
any-of-labels: ':construction: WIP'
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: -1
|
||||
|
||||
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches: '*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Test
|
||||
run: yarn test
|
||||
18
.github/workflows/typecheck.yml
vendored
18
.github/workflows/typecheck.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Typecheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches: '*'
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
115
.github/workflows/update-vrt.yml
vendored
Normal file
115
.github/workflows/update-vrt.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: /update-vrt
|
||||
on:
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ contains(github.event.comment.body, '/update-vrt') }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-vrt:
|
||||
name: Update VRT
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./.github/actions/netlify-wait-for-build
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
run: yarn vrt --update-snapshots
|
||||
env:
|
||||
E2E_START_URL: ${{ steps.netlify.outputs.url }}
|
||||
- name: Create patch
|
||||
run: |
|
||||
git config --system --add safe.directory "*"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git reset
|
||||
git add "**/*.png"
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update VRT"
|
||||
git format-patch -1 HEAD --stdout > Update-VRT.patch
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: patch
|
||||
path: Update-VRT.patch
|
||||
|
||||
push-patch:
|
||||
runs-on: ubuntu-latest
|
||||
needs: update-vrt
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: patch
|
||||
- name: Apply patch and push
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git apply Update-VRT.patch
|
||||
git add "**/*.png"
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update VRT"
|
||||
git push origin HEAD:${BRANCH_NAME}
|
||||
- name: Add finished reaction
|
||||
uses: dkershner6/reaction-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: "rocket"
|
||||
|
||||
add-starting-reaction:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: React to comment
|
||||
uses: dkershner6/reaction-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: "+1"
|
||||
45
.gitignore
vendored
45
.gitignore
vendored
@@ -1,26 +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
|
||||
|
||||
**/*.log
|
||||
|
||||
# Yarn
|
||||
.pnp.*
|
||||
@@ -30,3 +38,18 @@ export-2020-01-10.csv
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
|
||||
# Misc
|
||||
.#*
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# build output
|
||||
package.tgz
|
||||
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
yarn lint-staged
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
sync_pb.*
|
||||
2
.secret-tokens.example
Normal file
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
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
873
.yarn/releases/yarn-3.4.1.cjs
vendored
873
.yarn/releases/yarn-3.4.1.cjs
vendored
File diff suppressed because one or more lines are too long
894
.yarn/releases/yarn-4.3.1.cjs
vendored
Executable file
894
.yarn/releases/yarn-4.3.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
14
.yarnrc.yml
14
.yarnrc.yml
@@ -1,9 +1,9 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
enableTransparentWorkspaces: 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.4.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.3.1.cjs
|
||||
|
||||
@@ -1,34 +1 @@
|
||||
## Expectations
|
||||
|
||||
For smaller improvements or features - feel free to submit a PR or an issue if you don't have the necessary skills to build it yourself. For larger features we would recommend first opening an issue to discuss it with the team.
|
||||
|
||||
We aren't going to take every single little change. Don't be offended if we close your PR. In order for the project to stay healthy, we need to guard our bandwidth and also only take changes that align with Actual.
|
||||
|
||||
Here are some initial guidelines for how contributions will be treated:
|
||||
|
||||
- The mental health of the maintainers will be prioritized above all else. If this means some things get lost and PRs are unreviewed because maintainers are spending time with family or on themselves, we celebrate that.
|
||||
|
||||
- Multiple maintainers are key to this being a healthy project. Currently a few people have maintainer rights (see list below). We are actively looking for more people to come on as maintainers. If nobody steps up, expect less activity on this project.
|
||||
|
||||
- An open PR does not automatically deserve time for a full review and acceptance. It's up to the PR author to convince the maintainers that the change is good and worth reviewing. This involves a clear description for why the the change is being made, detailing the tradeoffs.
|
||||
|
||||
- We especially welcome improvements in automation: creating github actions to automatically generate builds, making the release process easier, etc.
|
||||
|
||||
## Main contributors
|
||||
|
||||
(sorted alphabetically)
|
||||
|
||||
- @albertogasparin
|
||||
- @j-f1
|
||||
- @jlongster
|
||||
- @MatissJanis
|
||||
- @rich-howell
|
||||
- @trevdor
|
||||
|
||||
## Project ideas
|
||||
|
||||
We welcome all contributions from the community. If you have an idea for a feature you want to build - please go ahead and submit a PR with the implementation or if it's a larger feature - open a new issue so we can discuss it.
|
||||
|
||||
If you do not have ideas what to build: the issue list is always a good starting point. Look for issues labeled with "[help wanted](https://github.com/actualbudget/actual/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)".
|
||||
|
||||
For first time contributions you can also filter the issues labeled with "[good first issue](https://github.com/actualbudget/actual/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)".
|
||||
Please review the contributing documentation on our website: https://actualbudget.org/docs/contributing/
|
||||
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
###################################################
|
||||
# This Dockerfile is used by the docker-compose.yml
|
||||
# file to build the development container.
|
||||
# Do not make any changes here unless you know what
|
||||
# you are doing.
|
||||
###################################################
|
||||
|
||||
FROM node:18-bullseye as dev
|
||||
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y openssl
|
||||
WORKDIR /app
|
||||
CMD ["sh", "./bin/docker-start"]
|
||||
54
README.md
54
README.md
@@ -6,42 +6,74 @@
|
||||
|
||||
Actual is a local-first personal finance tool. It is 100% free and open-source, written in NodeJS, it has a synchronization element so that all your changes can move between devices without any heavy lifting.
|
||||
|
||||
If you are interested in contributing, or want to know how development works, see [CONTRIBUTING.md](https://github.com/actualbudget/actual/blob/master/CONTRIBUTING.md) we would love to have you.
|
||||
If you are interested in contributing, or want to know how development works, see our [contributing](https://actualbudget.org/docs/contributing/) document we would love to have you.
|
||||
|
||||
Want to say thanks? Click the ⭐ at the top of the page.
|
||||
|
||||
## Key Links
|
||||
|
||||
- Actual [discord](https://discord.gg/pRYNYr4W5A) community.
|
||||
- Actual [Community Documentation](https://actualbudget.github.io/docs)
|
||||
- Actual [Community Documentation](https://actualbudget.org/docs)
|
||||
- [Frequently asked questions](https://actualbudget.org/docs/faq)
|
||||
|
||||
## Installation
|
||||
|
||||
If you are only interested in running the latest version and not contributing to the source code, you don't need to clone this repo. You can get the latest version through npm.
|
||||
There are four ways to deploy Actual:
|
||||
|
||||
### The easy way: using a server (recommended)
|
||||
1. One-click deployment [via PikaPods](https://www.pikapods.com/pods?run=actual) (~1.40 $/month) - recommended for non-technical users
|
||||
1. Managed hosting [via Fly.io](https://actualbudget.org/docs/install/fly) (~1.50 $/month)
|
||||
1. Self-hosted by using [a Docker image](https://actualbudget.org/docs/install/docker)
|
||||
1. Local-only apps - [downloadable Windows, Mac and Linux apps](https://actualbudget.org/download/) you can run on your device
|
||||
|
||||
The easiest way to get Actual running is to use the [actual-server](https://github.com/actualbudget/actual-server) project. That is the server for syncing changes across devices, and it comes with the latest version of Actual. The server will provide both the web project and a server for syncing.
|
||||
Learn more in the [installation instructions docs](https://actualbudget.org/docs/install/).
|
||||
|
||||
You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.github.io/docs/Installing/Local/your-own-machine)
|
||||
## Ready to Start Budgeting?
|
||||
|
||||
Read about [Envelope budgeting](https://actualbudget.org/docs/getting-started/envelope-budgeting) to know more about the idea behind Actual Budget.
|
||||
|
||||
### Are you new to budgeting or want to start fresh?
|
||||
|
||||
Check out the community's [Starting Fresh](https://actualbudget.org/docs/getting-started/starting-fresh) guide so you can quickly get up and running!
|
||||
|
||||
### Are you migrating from other budgeting apps?
|
||||
|
||||
Check out the community's [Migration](https://actualbudget.org/docs/migration/) guide to start jumping on the Actual Budget train!
|
||||
|
||||
## Documentation
|
||||
|
||||
We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.github.io/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
|
||||
We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.org/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
|
||||
|
||||
## Code structure
|
||||
## Contributing
|
||||
|
||||
Actual is a community driven product. Learn more about [contributing to Actual](https://actualbudget.org/docs/contributing/).
|
||||
|
||||
### Code structure
|
||||
|
||||
The Actual app is split up into a few packages:
|
||||
|
||||
- loot-core - The core application that runs on any platform
|
||||
- loot-design - The generic design components that make up the UI
|
||||
- desktop-client - The desktop UI
|
||||
- desktop-electron - The desktop app
|
||||
|
||||
More information on the project structure is available in our [community documentation](https://actualbudget.github.io/docs/Developers/project-layout).
|
||||
More information on the project structure is available in our [community documentation](https://actualbudget.org/docs/contributing/project-details).
|
||||
|
||||
### Feature Requests
|
||||
|
||||
Current feature requests can be seen [here](https://github.com/actualbudget/actual/issues?q=is%3Aissue+label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc).
|
||||
Vote for your favorite requests by reacting :+1: to the top comment of the request.
|
||||
|
||||
To add new feature requests, open a new Issue of the "Feature Request" type.
|
||||
|
||||
### Translation
|
||||
|
||||
Make Actual Budget accessible to more people by helping with the [Internationalization](https://actualbudget.org/docs/contributing/i18n/) of Actual. We are using a crowd sourcing tool to manage the translations, see our [Weblate Project](https://hosted.weblate.org/projects/actualbudget/). Weblate proudly supports open-source software projects through their [Libre plan](https://weblate.org/en/hosting/#libre).
|
||||
|
||||
## Repo Activity
|
||||
|
||||

|
||||
|
||||
## Sponsors
|
||||
|
||||
Thanks to our wonderful sponsors who make Actual budget possible!
|
||||
Thanks to our wonderful sponsors who make Actual Budget possible!
|
||||
|
||||
<a href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" /> </a>
|
||||
|
||||
13
bin/docker-start
Normal file
13
bin/docker-start
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
#####################################################
|
||||
# This startup script is used by the docker container
|
||||
# to check if the node_modules folder is empty and
|
||||
# if so, run yarn to install the dependencies.
|
||||
#####################################################
|
||||
|
||||
if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
|
||||
yarn
|
||||
fi
|
||||
|
||||
BROWSER=0 yarn start:browser
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
FILES=$(git diff --cached --name-only --diff-filter=ACMR "*.js" "*.jsx" | sed 's| |\\ |g')
|
||||
[ -z "$FILES" ] && exit 0
|
||||
|
||||
# Prettify all selected files
|
||||
echo "$FILES" | xargs ./node_modules/.bin/prettier --write
|
||||
|
||||
# Add back the modified/prettified files to staging
|
||||
echo "$FILES" | xargs git add
|
||||
|
||||
exit 0
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
ROOT=$(cd "`dirname $0`"; pwd)
|
||||
NPM_NAME="$1"
|
||||
NAME="$2"
|
||||
PACKAGE_DIR="`dirname "$ROOT"`/packages/$NAME"
|
||||
|
||||
if [ -z "$NAME" ] || [ -z "$NPM_NAME" ]; then
|
||||
echo "Usage: `basename $0` <npm-name> <local-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "$PACKAGE_DIR" ]; then
|
||||
read -p "Package exists, remove $PACKAGE_DIR? [y/N] " -r
|
||||
if [ -z "$REPLY" ] || [ "$REPLY" != "y" ]; then
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "$PACKAGE_DIR"
|
||||
URL="`npm view "$NPM_NAME" dist.tarball`"
|
||||
TMPDIR="`mktemp -d`"
|
||||
|
||||
cd "$TMPDIR"
|
||||
wget -O tar.tgz "$URL"
|
||||
tar xvzf tar.tgz
|
||||
mv package "$PACKAGE_DIR"
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
VERSION=""
|
||||
|
||||
POSITIONAL=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}"
|
||||
|
||||
NOTES="$@"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "--version is required";
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Version: $VERSION"
|
||||
echo "Notes: $NOTES"
|
||||
read -p "Make release? [y/N] " -r
|
||||
if [ -z "$REPLY" ] || [ "$REPLY" != "y" ]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
source ./.secret-tokens
|
||||
|
||||
# Tag and push to make windows and linux versions
|
||||
git push origin master
|
||||
git tag -a "$VERSION" -m "$NOTES"
|
||||
git push origin "$VERSION"
|
||||
|
||||
# Make a macOS version
|
||||
./bin/package --release --version "$VERSION"
|
||||
|
||||
# TODO: browser version
|
||||
|
||||
# Finally, update github issues
|
||||
curl -X POST -H "x-release-token: $RELEASE_TOKEN" https://actual-automoto.fly.dev/release/"$VERSION"
|
||||
111
bin/package
111
bin/package
@@ -1,111 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
|
||||
ROOT=`dirname $0`
|
||||
VERSION=""
|
||||
BETA=""
|
||||
RELEASE=""
|
||||
RELEASE_NOTES=""
|
||||
CI=${CI:-false}
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
POSITIONAL=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--beta)
|
||||
RELEASE="beta"
|
||||
shift
|
||||
;;
|
||||
--release)
|
||||
RELEASE="production"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}"
|
||||
|
||||
if [ -z "$VERSION" ] && [ -n "$RELEASE" ]; then
|
||||
echo "Version is required if making a release"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$RELEASE" ]; then
|
||||
if [ -z "$CIRCLE_TAG" ]; then
|
||||
read -p "Make release: $RELEASE v$VERSION? [y/N] " -r
|
||||
if [ -z "$REPLY" ] || [ "$REPLY" != "y" ]; then
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -z "$CIRCLE_TAG" ]; then
|
||||
RELEASE_NOTES=`git tag -l --format="%(contents:subject)" "$VERSION"`
|
||||
else
|
||||
RELEASE_NOTES=`git tag -l --format="%(contents:subject)" "$CIRCLE_TAG"`
|
||||
fi
|
||||
fi
|
||||
|
||||
PACKAGE_VERSION=`node -p -e "require('./packages/desktop-electron/package.json').version"`
|
||||
if [ "$VERSION" != "$PACKAGE_VERSION" ]; then
|
||||
echo "Version in desktop-electron/package.json does not match given version! ($PACKAGE_VERSION)"
|
||||
exit 4
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$OSTYPE" == "msys" ]; then
|
||||
if [ $CI != true ]; then
|
||||
read -s -p "Windows certificate password: " -r CSC_KEY_PASSWORD
|
||||
export CSC_KEY_PASSWORD
|
||||
elif [ -n "$CIRCLE_TAG" ]; then
|
||||
# We only want to run this on CircleCI as Github doesn't have the CSC_KEY_PASSWORD secret set.
|
||||
certutil -f -p ${CSC_KEY_PASSWORD} -importPfx ~/windows-shift-reset-llc.p12
|
||||
fi
|
||||
fi
|
||||
|
||||
# We only need to run linting once (and this doesn't seem to work on
|
||||
# Windows for some reason)
|
||||
if [[ $CI != true && "$OSTYPE" == "darwin"* ]]; then
|
||||
yarn lint
|
||||
fi
|
||||
|
||||
yarn patch-package
|
||||
|
||||
yarn workspace loot-core build:node
|
||||
|
||||
yarn workspace @actual-app/web build
|
||||
|
||||
yarn workspace Actual 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
|
||||
|
||||
echo "\nCreated release $VERSION with release notes \"$RELEASE_NOTES\""
|
||||
elif [ "$RELEASE" == "beta" ]; then
|
||||
yarn build --publish never --arm64 --x64
|
||||
|
||||
echo "\nCreated beta release $VERSION"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build --publish never --x64
|
||||
fi
|
||||
)
|
||||
@@ -1,63 +1,19 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
ROOT=`dirname $0`
|
||||
VERSION=""
|
||||
RELEASE=""
|
||||
CI=${CI:-false}
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
POSITIONAL=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--beta)
|
||||
RELEASE="beta"
|
||||
shift
|
||||
;;
|
||||
--release)
|
||||
RELEASE="production"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}"
|
||||
|
||||
if [ -z "$VERSION" ] && [ -n "$RELEASE" ]; then
|
||||
echo "Version is required if making a release"
|
||||
exit 1
|
||||
echo "Updating translations..."
|
||||
if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
if [ -n "$RELEASE" ]; then
|
||||
read -p "Deploy release for browser: $RELEASE v$VERSION? [y/N] " -r
|
||||
if [ -z "$REPLY" ] || [ "$REPLY" != "y" ]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
PACKAGE_VERSION=`node -p -e "require('./packages/desktop-electron/package.json').version"`
|
||||
if [ "$VERSION" != "$PACKAGE_VERSION" ] && [ "$VERSION-next" != "$PACKAGE_VERSION" ]; then
|
||||
echo "Version in desktop-electron/package.json does not match given version! ($PACKAGE_VERSION)"
|
||||
exit 4
|
||||
fi
|
||||
fi
|
||||
|
||||
# There's no need to check linting in CI as it'll be done in a different step.
|
||||
if [ $CI != true ]; then
|
||||
yarn lint
|
||||
fi
|
||||
|
||||
ACTUAL_RELEASE_TYPE=$RELEASE yarn workspace loot-core build:browser
|
||||
|
||||
REACT_APP_RELEASE_TYPE=$RELEASE yarn workspace @actual-app/web build:browser
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
|
||||
echo "packages/desktop-client/build"
|
||||
|
||||
67
bin/package-electron
Executable file
67
bin/package-electron
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
|
||||
ROOT=`dirname $0`
|
||||
RELEASE=""
|
||||
CI=${CI:-false}
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
POSITIONAL=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
--release)
|
||||
RELEASE="production"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}"
|
||||
|
||||
if [ "$OSTYPE" == "msys" ]; then
|
||||
if [ $CI != true ]; then
|
||||
read -s -p "Windows certificate password: " -r CSC_KEY_PASSWORD
|
||||
export CSC_KEY_PASSWORD
|
||||
elif [ -n "$CIRCLE_TAG" ]; then
|
||||
# We only want to run this on CircleCI as Github doesn't have the CSC_KEY_PASSWORD secret set.
|
||||
certutil -f -p ${CSC_KEY_PASSWORD} -importPfx ~/windows-shift-reset-llc.p12
|
||||
fi
|
||||
fi
|
||||
|
||||
yarn workspace loot-core build:node
|
||||
|
||||
# Get translations
|
||||
echo "Updating translations..."
|
||||
if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
yarn workspace desktop-electron update-client
|
||||
|
||||
(
|
||||
cd packages/desktop-electron;
|
||||
yarn clean;
|
||||
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build
|
||||
|
||||
echo "\nCreated release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build
|
||||
fi
|
||||
)
|
||||
32
bin/run-vrt
Executable file
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"
|
||||
|
||||
MSYS_NO_PATHCONV=1 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"
|
||||
BIN
demo.png
BIN
demo.png
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 107 KiB |
18
docker-compose.yml
Normal file
18
docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
###################################################
|
||||
# This creates and stands up the development
|
||||
# docker container. Depends on the Dockerfile and
|
||||
# docker-start.sh files.
|
||||
###################################################
|
||||
|
||||
services:
|
||||
actual-development:
|
||||
build: .
|
||||
image: actual-development
|
||||
environment:
|
||||
- HTTPS
|
||||
ports:
|
||||
- '3001:3001'
|
||||
volumes:
|
||||
- '.:/app'
|
||||
restart: 'no'
|
||||
|
||||
841
eslint.config.mjs
Normal file
841
eslint.config.mjs
Normal file
@@ -0,0 +1,841 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import globals from 'globals';
|
||||
|
||||
import pluginImport from 'eslint-plugin-import';
|
||||
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
|
||||
import pluginPrettier from 'eslint-plugin-prettier/recommended';
|
||||
import pluginReact from 'eslint-plugin-react';
|
||||
import pluginReactHooks from 'eslint-plugin-react-hooks';
|
||||
import pluginRulesDir from 'eslint-plugin-rulesdir';
|
||||
import pluginTypescript from 'typescript-eslint';
|
||||
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
pluginRulesDir.RULES_DIR = path.join(
|
||||
__dirname,
|
||||
'packages',
|
||||
'eslint-plugin-actual',
|
||||
'lib',
|
||||
'rules',
|
||||
);
|
||||
|
||||
const confusingBrowserGlobals = [
|
||||
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
|
||||
'addEventListener',
|
||||
'blur',
|
||||
'close',
|
||||
'closed',
|
||||
'confirm',
|
||||
'defaultStatus',
|
||||
'defaultstatus',
|
||||
'event',
|
||||
'external',
|
||||
'find',
|
||||
'focus',
|
||||
'frameElement',
|
||||
'frames',
|
||||
'history',
|
||||
'innerHeight',
|
||||
'innerWidth',
|
||||
'length',
|
||||
'location',
|
||||
'locationbar',
|
||||
'menubar',
|
||||
'moveBy',
|
||||
'moveTo',
|
||||
'name',
|
||||
'onblur',
|
||||
'onerror',
|
||||
'onfocus',
|
||||
'onload',
|
||||
'onresize',
|
||||
'onunload',
|
||||
'open',
|
||||
'opener',
|
||||
'opera',
|
||||
'outerHeight',
|
||||
'outerWidth',
|
||||
'pageXOffset',
|
||||
'pageYOffset',
|
||||
'parent',
|
||||
'print',
|
||||
'removeEventListener',
|
||||
'resizeBy',
|
||||
'resizeTo',
|
||||
'screen',
|
||||
'screenLeft',
|
||||
'screenTop',
|
||||
'screenX',
|
||||
'screenY',
|
||||
'scroll',
|
||||
'scrollbars',
|
||||
'scrollBy',
|
||||
'scrollTo',
|
||||
'scrollX',
|
||||
'scrollY',
|
||||
'status',
|
||||
'statusbar',
|
||||
'stop',
|
||||
'toolbar',
|
||||
'top',
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'packages/api/app/bundle.api.js',
|
||||
'packages/api/dist',
|
||||
'packages/api/@types',
|
||||
'packages/api/migrations',
|
||||
'packages/crdt/dist',
|
||||
'packages/desktop-client/bundle.browser.js',
|
||||
'packages/desktop-client/build/',
|
||||
'packages/desktop-client/build-electron/',
|
||||
'packages/desktop-client/build-stats/',
|
||||
'packages/desktop-client/public/kcab/',
|
||||
'packages/desktop-client/public/data/',
|
||||
'packages/desktop-client/**/node_modules/*',
|
||||
'packages/desktop-client/node_modules/',
|
||||
'packages/desktop-client/src/icons/**/*',
|
||||
'packages/desktop-client/test-results/',
|
||||
'packages/desktop-client/playwright-report/',
|
||||
'packages/desktop-electron/client-build/',
|
||||
'packages/desktop-electron/build/',
|
||||
'packages/desktop-electron/dist/',
|
||||
'packages/import-ynab4/**/node_modules/*',
|
||||
'packages/import-ynab5/**/node_modules/*',
|
||||
'packages/loot-core/**/node_modules/*',
|
||||
'packages/loot-core/**/lib-dist/*',
|
||||
'packages/loot-core/**/proto/*',
|
||||
'.yarn/*',
|
||||
'.github/*',
|
||||
],
|
||||
},
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.commonjs,
|
||||
...globals.jest,
|
||||
...globals.node,
|
||||
globalThis: false,
|
||||
vi: true,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginReact.configs.flat.recommended,
|
||||
pluginReact.configs.flat['jsx-runtime'],
|
||||
pluginPrettier,
|
||||
...pluginTypescript.configs.recommended,
|
||||
pluginImport.flatConfigs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': pluginReactHooks,
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
rulesdir: pluginRulesDir,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,ts,jsx,tsx}'],
|
||||
rules: {
|
||||
// http://eslint.org/docs/rules/
|
||||
'array-callback-return': 'warn',
|
||||
|
||||
'default-case': [
|
||||
'warn',
|
||||
{
|
||||
commentPattern: '^no default$',
|
||||
},
|
||||
],
|
||||
|
||||
curly: ['warn', 'multi-line', 'consistent'],
|
||||
'dot-location': ['warn', 'property'],
|
||||
eqeqeq: ['warn', 'smart'],
|
||||
'new-parens': 'warn',
|
||||
'no-array-constructor': 'warn',
|
||||
'no-caller': 'warn',
|
||||
'no-cond-assign': ['warn', 'except-parens'],
|
||||
'no-const-assign': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'no-delete-var': 'warn',
|
||||
'no-dupe-args': 'warn',
|
||||
'no-dupe-class-members': 'warn',
|
||||
'no-dupe-keys': 'warn',
|
||||
'no-duplicate-case': 'warn',
|
||||
'no-empty-character-class': 'warn',
|
||||
'no-empty-pattern': 'warn',
|
||||
'no-eval': 'warn',
|
||||
'no-ex-assign': 'warn',
|
||||
'no-extend-native': 'warn',
|
||||
'no-extra-bind': 'warn',
|
||||
'no-extra-label': 'warn',
|
||||
'no-fallthrough': 'warn',
|
||||
'no-func-assign': 'warn',
|
||||
'no-implied-eval': 'warn',
|
||||
'no-invalid-regexp': 'warn',
|
||||
'no-iterator': 'warn',
|
||||
'no-label-var': 'warn',
|
||||
|
||||
'no-labels': [
|
||||
'warn',
|
||||
{
|
||||
allowLoop: true,
|
||||
allowSwitch: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-lone-blocks': 'warn',
|
||||
|
||||
'no-mixed-operators': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
['&', '|', '^', '~', '<<', '>>', '>>>'],
|
||||
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
|
||||
['&&', '||'],
|
||||
['in', 'instanceof'],
|
||||
],
|
||||
|
||||
allowSamePrecedence: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-multi-str': 'warn',
|
||||
'no-global-assign': 'warn',
|
||||
'no-unsafe-negation': 'warn',
|
||||
'no-new-func': 'warn',
|
||||
'no-new-object': 'warn',
|
||||
'no-new-symbol': 'warn',
|
||||
'no-new-wrappers': 'warn',
|
||||
'no-obj-calls': 'warn',
|
||||
'no-octal': 'warn',
|
||||
'no-octal-escape': 'warn',
|
||||
'no-redeclare': 'warn',
|
||||
'no-regex-spaces': 'warn',
|
||||
'no-script-url': 'warn',
|
||||
'no-self-assign': 'warn',
|
||||
'no-self-compare': 'warn',
|
||||
'no-sequences': 'warn',
|
||||
'no-shadow-restricted-names': 'warn',
|
||||
'no-sparse-arrays': 'warn',
|
||||
'no-template-curly-in-string': 'warn',
|
||||
'no-this-before-super': 'warn',
|
||||
'no-throw-literal': 'warn',
|
||||
'no-undef': 'error',
|
||||
'no-unreachable': 'warn',
|
||||
|
||||
'no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
},
|
||||
],
|
||||
|
||||
'no-unused-labels': 'warn',
|
||||
|
||||
'no-use-before-define': [
|
||||
'warn',
|
||||
{
|
||||
functions: false,
|
||||
classes: false,
|
||||
variables: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-useless-computed-key': 'warn',
|
||||
'no-useless-concat': 'warn',
|
||||
'no-useless-constructor': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
|
||||
'no-useless-rename': [
|
||||
'warn',
|
||||
{
|
||||
ignoreDestructuring: false,
|
||||
ignoreImport: false,
|
||||
ignoreExport: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-with': 'warn',
|
||||
'no-whitespace-before-property': 'warn',
|
||||
|
||||
'require-yield': 'warn',
|
||||
'rest-spread-spacing': ['warn', 'never'],
|
||||
strict: ['warn', 'never'],
|
||||
'unicode-bom': ['warn', 'never'],
|
||||
'use-isnan': 'warn',
|
||||
'valid-typeof': 'warn',
|
||||
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: 'require',
|
||||
property: 'ensure',
|
||||
message:
|
||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
||||
},
|
||||
{
|
||||
object: 'System',
|
||||
property: 'import',
|
||||
message:
|
||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
||||
},
|
||||
],
|
||||
|
||||
'getter-return': 'warn',
|
||||
|
||||
// https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules
|
||||
'import/first': 'error',
|
||||
'import/no-amd': 'error',
|
||||
'import/no-anonymous-default-export': 'warn',
|
||||
'import/no-webpack-loader-syntax': 'error',
|
||||
'import/extensions': [
|
||||
'warn',
|
||||
'never',
|
||||
{
|
||||
json: 'always',
|
||||
},
|
||||
],
|
||||
'import/no-useless-path-segments': 'warn',
|
||||
'import/no-duplicates': [
|
||||
'warn',
|
||||
{
|
||||
'prefer-inline': true,
|
||||
},
|
||||
],
|
||||
'import/order': [
|
||||
'warn',
|
||||
{
|
||||
alphabetize: {
|
||||
caseInsensitive: true,
|
||||
order: 'asc',
|
||||
},
|
||||
|
||||
groups: ['builtin', 'external', 'parent', 'sibling', 'index'],
|
||||
'newlines-between': 'always',
|
||||
|
||||
pathGroups: [
|
||||
{
|
||||
// Enforce that React (and react-related packages) is the first import
|
||||
group: 'builtin',
|
||||
pattern: 'react?(-*)',
|
||||
position: 'before',
|
||||
},
|
||||
{
|
||||
// Separate imports from Actual from "real" external imports
|
||||
group: 'external',
|
||||
pattern: 'loot-{core,design}/**/*',
|
||||
position: 'after',
|
||||
},
|
||||
],
|
||||
|
||||
pathGroupsExcludedImportTypes: ['react'],
|
||||
},
|
||||
],
|
||||
|
||||
// https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
|
||||
'react/forbid-foreign-prop-types': [
|
||||
'warn',
|
||||
{
|
||||
allowInPropTypes: true,
|
||||
},
|
||||
],
|
||||
'react/jsx-no-comment-textnodes': 'warn',
|
||||
'react/jsx-no-duplicate-props': 'warn',
|
||||
'react/jsx-no-target-blank': 'warn',
|
||||
'react/jsx-no-undef': 'error',
|
||||
'react/jsx-pascal-case': [
|
||||
'warn',
|
||||
{
|
||||
allowAllCaps: true,
|
||||
ignore: [],
|
||||
},
|
||||
],
|
||||
'react/no-danger-with-children': 'warn',
|
||||
// Disabled because of undesirable warnings
|
||||
// See https://github.com/facebook/create-react-app/issues/5204 for
|
||||
// blockers until its re-enabled
|
||||
// 'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'react/jsx-no-useless-fragment': 'warn',
|
||||
'react/self-closing-comp': 'warn',
|
||||
'react/jsx-filename-extension': [
|
||||
'warn',
|
||||
{
|
||||
extensions: ['.jsx', '.tsx'],
|
||||
allow: 'as-needed',
|
||||
},
|
||||
],
|
||||
'react/no-unstable-nested-components': [
|
||||
'warn',
|
||||
{
|
||||
allowAsProps: true,
|
||||
customValidators: ['formatter'],
|
||||
},
|
||||
],
|
||||
// Don't need this as we're using TypeScript
|
||||
'react/prop-types': 'off',
|
||||
|
||||
// https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
|
||||
'jsx-a11y/alt-text': 'warn',
|
||||
'jsx-a11y/anchor-has-content': 'warn',
|
||||
'jsx-a11y/anchor-is-valid': [
|
||||
'warn',
|
||||
{
|
||||
aspects: ['noHref', 'invalidHref'],
|
||||
},
|
||||
],
|
||||
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
|
||||
'jsx-a11y/aria-props': 'warn',
|
||||
'jsx-a11y/aria-proptypes': 'warn',
|
||||
'jsx-a11y/aria-role': [
|
||||
'warn',
|
||||
{
|
||||
ignoreNonDOM: true,
|
||||
},
|
||||
],
|
||||
'jsx-a11y/aria-unsupported-elements': 'warn',
|
||||
'jsx-a11y/heading-has-content': 'warn',
|
||||
'jsx-a11y/iframe-has-title': 'warn',
|
||||
'jsx-a11y/img-redundant-alt': 'warn',
|
||||
'jsx-a11y/no-access-key': 'warn',
|
||||
'jsx-a11y/no-distracting-elements': 'warn',
|
||||
'jsx-a11y/no-redundant-roles': 'warn',
|
||||
'jsx-a11y/role-has-required-aria-props': 'warn',
|
||||
'jsx-a11y/role-supports-aria-props': 'warn',
|
||||
'jsx-a11y/scope': 'warn',
|
||||
|
||||
// https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'warn',
|
||||
{
|
||||
additionalHooks: '(useQuery)',
|
||||
},
|
||||
],
|
||||
|
||||
'rulesdir/typography': 'warn',
|
||||
'rulesdir/prefer-if-statement': 'warn',
|
||||
|
||||
// Note: base rule explicitly disabled in favor of the TS one
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
varsIgnorePattern: '^(_|React)',
|
||||
ignoreRestSiblings: true,
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
],
|
||||
|
||||
'no-restricted-globals': ['warn', ...confusingBrowserGlobals],
|
||||
|
||||
// https://github.com/eslint/eslint/issues/16954
|
||||
// https://github.com/eslint/eslint/issues/16953
|
||||
'no-loop-func': 'off',
|
||||
|
||||
// TODO: re-enable these rules
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'no-var': 'warn',
|
||||
'react/jsx-curly-brace-presence': 'warn',
|
||||
'object-shorthand': ['warn', 'properties'],
|
||||
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
{
|
||||
// forbid React.* as they are legacy https://twitter.com/dan_abramov/status/1308739731551858689
|
||||
selector:
|
||||
":matches(MemberExpression[object.name='React'], TSQualifiedName[left.name='React'])",
|
||||
message:
|
||||
'Using default React import is discouraged, please use named exports directly instead.',
|
||||
},
|
||||
{
|
||||
// forbid <a> in favor of <Link>
|
||||
selector: 'JSXOpeningElement[name.name="a"]',
|
||||
message: 'Using <a> is discouraged, please use <Link> instead.',
|
||||
},
|
||||
],
|
||||
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['*.api', '*.web', '*.electron'],
|
||||
message: "Don't directly reference imports from other platforms",
|
||||
},
|
||||
{
|
||||
group: ['uuid'],
|
||||
importNames: ['*'],
|
||||
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
|
||||
},
|
||||
{
|
||||
group: ['**/style', '**/colors'],
|
||||
importNames: ['colors'],
|
||||
message: 'Please use themes instead of colors',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
'ts-ignore': 'allow-with-description',
|
||||
},
|
||||
],
|
||||
|
||||
// Rules disabled during TS migration
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'prefer-const': 'warn',
|
||||
'prefer-spread': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'import/no-default-export': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts?(x)'],
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
project: [path.join(__dirname, './tsconfig.json')],
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
|
||||
// typescript-eslint specific options
|
||||
warnOnUnsupportedTypeScriptVersion: true,
|
||||
},
|
||||
},
|
||||
|
||||
// If adding a typescript-eslint version of an existing ESLint rule,
|
||||
// make sure to disable the ESLint rule here.
|
||||
rules: {
|
||||
// TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
|
||||
'default-case': 'off',
|
||||
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
|
||||
'no-dupe-class-members': 'off',
|
||||
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
|
||||
'no-undef': 'off',
|
||||
|
||||
// Add TypeScript specific rules (and turn off ESLint equivalents)
|
||||
'@typescript-eslint/consistent-type-assertions': 'warn',
|
||||
'no-array-constructor': 'off',
|
||||
'@typescript-eslint/no-array-constructor': 'warn',
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'warn',
|
||||
'no-use-before-define': 'off',
|
||||
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
'warn',
|
||||
{
|
||||
functions: false,
|
||||
classes: false,
|
||||
variables: false,
|
||||
typedefs: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-unused-expressions': 'off',
|
||||
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
},
|
||||
],
|
||||
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/**/*.{ts,tsx}',
|
||||
'packages/loot-core/src/client/**/*.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
// enforce import type
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
{
|
||||
prefer: 'type-imports',
|
||||
fixStyle: 'inline-type-imports',
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/no-restricted-types': [
|
||||
'warn',
|
||||
{
|
||||
types: {
|
||||
// forbid FC as superflous
|
||||
FunctionComponent: {
|
||||
message:
|
||||
'Type the props argument and let TS infer or use ComponentType for a component prop',
|
||||
},
|
||||
|
||||
FC: {
|
||||
message:
|
||||
'Type the props argument and let TS infer or use ComponentType for a component prop',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*'],
|
||||
ignores: ['packages/desktop-client/src/hooks/useNavigate.{ts,tsx}'],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'react-router-dom',
|
||||
importNames: ['useNavigate'],
|
||||
message:
|
||||
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*', 'packages/loot-core/**/*'],
|
||||
ignores: ['packages/desktop-client/src/redux/index.{ts,tsx}'],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useDispatch'],
|
||||
message:
|
||||
"Please import Actual's useDispatch() hook from `src/redux` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useSelector'],
|
||||
message:
|
||||
"Please import Actual's useSelector() hook from `src/redux` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useStore'],
|
||||
message:
|
||||
"Please import Actual's useStore() hook from `src/redux` instead.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/loot-core/src/**/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['*.api', '*.web', '*.electron'],
|
||||
message: "Don't directly reference imports from other platforms",
|
||||
},
|
||||
{
|
||||
group: ['uuid'],
|
||||
importNames: ['*'],
|
||||
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
|
||||
},
|
||||
{
|
||||
group: ['loot-core/**'],
|
||||
message:
|
||||
'Please use relative imports in loot-core instead of importing from `loot-core/*`',
|
||||
},
|
||||
{
|
||||
group: ['@actual-app/web/*'],
|
||||
message: 'Please do not import `@actual-app/web` in `loot-core`',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/loot-core/src/types/**/*',
|
||||
'packages/loot-core/src/client/state-types/**/*',
|
||||
'**/icons/**/*',
|
||||
'**/{mocks,__mocks__}/**/*',
|
||||
// can't correctly resolve usages
|
||||
'**/*.{testing,electron,browser,web,api}.ts',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'import/no-unused-modules': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/src/style/index.*',
|
||||
'packages/desktop-client/src/style/palette.*',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'off',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['**/style', '**/colors'],
|
||||
importNames: ['colors'],
|
||||
message: 'Please use themes instead of colors',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/api/migrations/*', 'packages/loot-core/migrations/*'],
|
||||
|
||||
rules: {
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/api/index.ts'],
|
||||
rules: {
|
||||
'import/no-unresolved': '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/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/envelope/HoldMenu.tsx',
|
||||
'packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx',
|
||||
'packages/component-library/src/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',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'eslint.config.mjs',
|
||||
'**/*.test.js',
|
||||
'**/*.test.ts',
|
||||
'**/*.test.jsx',
|
||||
'**/*.test.tsx',
|
||||
'**/*.spec.js',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'rulesdir/typography': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/**/*.{ts,tsx}',
|
||||
'packages/loot-core/src/client/**/*.{ts,tsx}',
|
||||
],
|
||||
ignores: ['**/**/globals.d.ts'],
|
||||
rules: {
|
||||
// enforce type over interface
|
||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/sync-server/**/*'],
|
||||
// TODO: fix the issues in these files
|
||||
rules: {
|
||||
'import/extensions': 'off',
|
||||
'rulesdir/typography': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/sync-server/src/app-gocardless/banks/*.js'],
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
// can be re-enabled after https://github.com/actualbudget/actual/pull/4253
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
68
package.json
68
package.json
@@ -19,42 +19,70 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn start:browser",
|
||||
"start:desktop": "npm-run-all --parallel 'start:desktop-*'",
|
||||
"start:server": "yarn workspace @actual-app/sync-server start",
|
||||
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
|
||||
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
||||
"start:desktop": "yarn rebuild-electron && npm-run-all --parallel 'start:desktop-*'",
|
||||
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
"start:desktop-electron": "yarn workspace Actual watch",
|
||||
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
||||
"start:electron": "yarn start:desktop",
|
||||
"start:browser": "npm-run-all --parallel 'start:browser-*'",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"test": "yarn workspaces foreach --parallel --verbose run test",
|
||||
"test:debug": "yarn workspaces foreach --verbose run test",
|
||||
"e2e": "yarn workspaces foreach --parallel --verbose run e2e",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"test": "yarn workspaces foreach --all --parallel --verbose run test",
|
||||
"test:debug": "yarn workspaces foreach --all --verbose run test",
|
||||
"e2e": "yarn workspaces foreach --all --parallel --verbose run e2e",
|
||||
"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": "cross-env NODE_ENV=development yarn workspaces foreach --verbose run lint --max-warnings 0",
|
||||
"typecheck": "yarn tsc",
|
||||
"postinstall": "patch-package"
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"lint:verbose": "DEBUG=eslint:cli-engine eslint . --max-warnings 0",
|
||||
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
||||
"typecheck": "yarn tsc && tsc-strict",
|
||||
"jq": "./node_modules/node-jq/bin/jq",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^5.1.5",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"patch-package": "^6.1.2",
|
||||
"prettier": "2.8.2",
|
||||
"react-refresh": "^0.14.0",
|
||||
"globals": "^15.13.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.9",
|
||||
"node-jq": "^4.0.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript": "^5.0.2"
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.18.1",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"react-error-overlay": "6.0.9"
|
||||
"rollup": "4.9.4"
|
||||
},
|
||||
"packageManager": "yarn@3.4.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,json}": "prettier --write"
|
||||
},
|
||||
"packageManager": "yarn@4.3.1",
|
||||
"browserslist": [
|
||||
"electron 12.0",
|
||||
"electron 24.0",
|
||||
"defaults"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
app/bundle.api.js
|
||||
dist
|
||||
2
packages/api/.gitignore
vendored
2
packages/api/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
app/bundle.api.js*
|
||||
app/stats.json
|
||||
migrations
|
||||
default-db.sqlite
|
||||
mocks/budgets/**/*
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
npm install @actual-app/api
|
||||
```
|
||||
|
||||
View docs here: https://actualbudget.github.io/docs/Developers/using-the-API
|
||||
View docs here: https://actualbudget.org/docs/api/
|
||||
|
||||
21
packages/api/__snapshots__/methods.test.ts.snap
Normal file
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,31 +0,0 @@
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import * as bundle from './app/bundle.api';
|
||||
import * as injected from './injected';
|
||||
|
||||
let actualApp;
|
||||
export const internal = bundle.lib;
|
||||
|
||||
export * as methods from './methods';
|
||||
export * as utils from './utils';
|
||||
|
||||
export async function init(config = {}) {
|
||||
if (actualApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
global.fetch = fetch;
|
||||
|
||||
await bundle.init(config);
|
||||
actualApp = bundle.lib;
|
||||
|
||||
injected.override(bundle.lib.send);
|
||||
return bundle.lib;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (actualApp) {
|
||||
await actualApp.send('close-budget');
|
||||
actualApp = null;
|
||||
}
|
||||
}
|
||||
49
packages/api/index.ts
Normal file
49
packages/api/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type {
|
||||
RequestInfo as FetchInfo,
|
||||
RequestInit as FetchInit,
|
||||
} from 'node-fetch';
|
||||
|
||||
// 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;
|
||||
|
||||
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
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,151 +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 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 });
|
||||
}
|
||||
657
packages/api/methods.test.ts
Normal file
657
packages/api/methods.test.ts
Normal file
@@ -0,0 +1,657 @@
|
||||
// @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: false,
|
||||
id: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
is_income: false,
|
||||
name: 'Usual Expenses',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
hidden: false,
|
||||
id: 'a137772f-cf2f-4089-9432-822d2ddc1466',
|
||||
is_income: false,
|
||||
name: 'Investments and Savings',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
hidden: false,
|
||||
id: '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00',
|
||||
is_income: true,
|
||||
name: 'Income',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// 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].id);
|
||||
expect(await api.getRules()).toHaveLength(1);
|
||||
|
||||
await api.deleteRule(rules[0].id);
|
||||
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);
|
||||
});
|
||||
});
|
||||
232
packages/api/methods.ts
Normal file
232
packages/api/methods.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
// @ts-strict-ignore
|
||||
import type { Handlers } from 'loot-core/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 interface ImportTransactionsOpts {
|
||||
defaultCleared?: boolean;
|
||||
}
|
||||
|
||||
export function importTransactions(
|
||||
accountId,
|
||||
transactions,
|
||||
opts: ImportTransactionsOpts = {
|
||||
defaultCleared: true,
|
||||
},
|
||||
) {
|
||||
return send('api/transactions-import', {
|
||||
accountId,
|
||||
transactions,
|
||||
opts,
|
||||
});
|
||||
}
|
||||
|
||||
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 getCommonPayees() {
|
||||
return send('api/common-payees-get');
|
||||
}
|
||||
|
||||
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: string) {
|
||||
return send('api/rule-delete', id);
|
||||
}
|
||||
|
||||
export function holdBudgetForNextMonth(month, amount) {
|
||||
return send('api/budget-hold-for-next-month', { month, amount });
|
||||
}
|
||||
|
||||
export function resetBudgetHold(month) {
|
||||
return send('api/budget-reset-hold', { month });
|
||||
}
|
||||
0
packages/api/mocks/budgets/.gitkeep
Normal file
0
packages/api/mocks/budgets/.gitkeep
Normal file
@@ -1,27 +1,40 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "6.0.0",
|
||||
"version": "25.3.1",
|
||||
"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",
|
||||
"default-db.sqlite",
|
||||
"migrations"
|
||||
"@types"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:node": "tsc --p tsconfig.dist.json",
|
||||
"build": "yarn run build:app && yarn run build:node"
|
||||
"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": "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.2.0",
|
||||
"node-fetch": "^2.6.9",
|
||||
"uuid": "3.3.2"
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"compare-versions": "^6.1.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.2"
|
||||
"@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.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as api from './index';
|
||||
|
||||
async function run() {
|
||||
let app = await api.init({ config: { dataDir: '/tmp' } });
|
||||
await app.send('create-budget', { testMode: true });
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,11 +1,19 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
// Using ES2021 because that’s the newest version where
|
||||
// the latest Node 16.x release supports all of the features
|
||||
"target": "ES2021",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node10",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist"
|
||||
"outDir": "dist",
|
||||
"declarationDir": "@types",
|
||||
"paths": {
|
||||
"loot-core/*": ["./@types/loot-core/*"]
|
||||
}
|
||||
},
|
||||
"include": ["."]
|
||||
"include": ["."],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types"]
|
||||
}
|
||||
|
||||
16
packages/api/validateNodeVersion.js
Normal file
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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
packages/component-library/package.json
Normal file
40
packages/component-library/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@actual-app/components",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.4",
|
||||
"react-aria-components": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"exports": {
|
||||
"./icons/*": "./src/icons/*.tsx",
|
||||
"./aligned-text": "./src/AlignedText.tsx",
|
||||
"./block": "./src/Block.tsx",
|
||||
"./button": "./src/Button.tsx",
|
||||
"./card": "./src/Card.tsx",
|
||||
"./form-error": "./src/FormError.tsx",
|
||||
"./initial-focus": "./src/InitialFocus.ts",
|
||||
"./inline-field": "./src/InlineField.tsx",
|
||||
"./label": "./src/Label.tsx",
|
||||
"./menu": "./src/Menu.tsx",
|
||||
"./paragraph": "./src/Paragraph.tsx",
|
||||
"./popover": "./src/Popover.tsx",
|
||||
"./space-between": "./src/SpaceBetween.tsx",
|
||||
"./stack": "./src/Stack.tsx",
|
||||
"./styles": "./src/styles.ts",
|
||||
"./text": "./src/Text.tsx",
|
||||
"./text-one-line": "./src/TextOneLine.tsx",
|
||||
"./theme": "./src/theme.ts",
|
||||
"./tokens": "./src/tokens.ts",
|
||||
"./toggle": "./src/Toggle.tsx",
|
||||
"./tooltip": "./src/Tooltip.tsx",
|
||||
"./view": "./src/View.tsx"
|
||||
}
|
||||
}
|
||||
55
packages/component-library/src/AlignedText.tsx
Normal file
55
packages/component-library/src/AlignedText.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { type ComponentProps, type ReactNode, type CSSProperties } from 'react';
|
||||
|
||||
import { Block } from './Block';
|
||||
import { View } from './View';
|
||||
|
||||
type AlignedTextProps = ComponentProps<typeof View> & {
|
||||
left: ReactNode;
|
||||
right: ReactNode;
|
||||
style?: CSSProperties;
|
||||
leftStyle?: CSSProperties;
|
||||
rightStyle?: CSSProperties;
|
||||
truncate?: 'left' | 'right';
|
||||
};
|
||||
export function AlignedText({
|
||||
left,
|
||||
right,
|
||||
style,
|
||||
leftStyle,
|
||||
rightStyle,
|
||||
truncate = 'left',
|
||||
...nativeProps
|
||||
}: AlignedTextProps) {
|
||||
const truncateStyle: CSSProperties = {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ flexDirection: 'row', alignItems: 'center', ...style }}
|
||||
{...nativeProps}
|
||||
>
|
||||
<Block
|
||||
style={{
|
||||
marginRight: 10,
|
||||
...(truncate === 'left' && truncateStyle),
|
||||
...leftStyle,
|
||||
}}
|
||||
>
|
||||
{left}
|
||||
</Block>
|
||||
<Block
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
...(truncate === 'right' && truncateStyle),
|
||||
...rightStyle,
|
||||
}}
|
||||
>
|
||||
{right}
|
||||
</Block>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
17
packages/component-library/src/Block.tsx
Normal file
17
packages/component-library/src/Block.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type HTMLProps, type Ref } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
|
||||
type BlockProps = HTMLProps<HTMLDivElement> & {
|
||||
innerRef?: Ref<HTMLDivElement>;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function Block(props: BlockProps) {
|
||||
const { className = '', style, innerRef, ...restProps } = props;
|
||||
return (
|
||||
<div {...restProps} ref={innerRef} className={cx(className, css(style))} />
|
||||
);
|
||||
}
|
||||
238
packages/component-library/src/Button.tsx
Normal file
238
packages/component-library/src/Button.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useMemo,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ReactNode,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
import { Button as ReactAriaButton } from 'react-aria-components';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { AnimatedLoading } from './icons/AnimatedLoading';
|
||||
import { styles } from './styles';
|
||||
import { theme } from './theme';
|
||||
import { View } from './View';
|
||||
|
||||
const backgroundColor: {
|
||||
[key in ButtonVariant | `${ButtonVariant}Disabled`]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalBackground,
|
||||
normalDisabled: theme.buttonNormalDisabledBackground,
|
||||
primary: theme.buttonPrimaryBackground,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledBackground,
|
||||
bare: theme.buttonBareBackground,
|
||||
bareDisabled: theme.buttonBareDisabledBackground,
|
||||
menu: theme.buttonMenuBackground,
|
||||
menuSelected: theme.buttonMenuSelectedBackground,
|
||||
};
|
||||
|
||||
const backgroundColorHover: Record<
|
||||
ButtonVariant | `${ButtonVariant}Disabled`,
|
||||
CSSProperties['backgroundColor']
|
||||
> = {
|
||||
normal: theme.buttonNormalBackgroundHover,
|
||||
primary: theme.buttonPrimaryBackgroundHover,
|
||||
bare: theme.buttonBareBackgroundHover,
|
||||
menu: theme.buttonMenuBackgroundHover,
|
||||
menuSelected: theme.buttonMenuSelectedBackgroundHover,
|
||||
normalDisabled: 'transparent',
|
||||
primaryDisabled: 'transparent',
|
||||
bareDisabled: 'transparent',
|
||||
menuDisabled: 'transparent',
|
||||
menuSelectedDisabled: 'transparent',
|
||||
};
|
||||
|
||||
const borderColor: {
|
||||
[key in
|
||||
| ButtonVariant
|
||||
| `${ButtonVariant}Disabled`]?: CSSProperties['borderColor'];
|
||||
} = {
|
||||
normal: theme.buttonNormalBorder,
|
||||
normalDisabled: theme.buttonNormalDisabledBorder,
|
||||
primary: theme.buttonPrimaryBorder,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledBorder,
|
||||
menu: theme.buttonMenuBorder,
|
||||
menuSelected: theme.buttonMenuSelectedBorder,
|
||||
};
|
||||
|
||||
const textColor: {
|
||||
[key in ButtonVariant | `${ButtonVariant}Disabled`]?: CSSProperties['color'];
|
||||
} = {
|
||||
normal: theme.buttonNormalText,
|
||||
normalDisabled: theme.buttonNormalDisabledText,
|
||||
primary: theme.buttonPrimaryText,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledText,
|
||||
bare: theme.buttonBareText,
|
||||
bareDisabled: theme.buttonBareDisabledText,
|
||||
menu: theme.buttonMenuText,
|
||||
menuSelected: theme.buttonMenuSelectedText,
|
||||
};
|
||||
|
||||
const textColorHover: {
|
||||
[key in ButtonVariant]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalTextHover,
|
||||
primary: theme.buttonPrimaryTextHover,
|
||||
bare: theme.buttonBareTextHover,
|
||||
menu: theme.buttonMenuTextHover,
|
||||
menuSelected: theme.buttonMenuSelectedTextHover,
|
||||
};
|
||||
|
||||
const _getBorder = (
|
||||
variant: ButtonVariant,
|
||||
variantWithDisabled: keyof typeof borderColor,
|
||||
): string => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return 'none';
|
||||
|
||||
default:
|
||||
return '1px solid ' + borderColor[variantWithDisabled];
|
||||
}
|
||||
};
|
||||
|
||||
const _getPadding = (variant: ButtonVariant): string => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return '5px';
|
||||
default:
|
||||
return '5px 10px';
|
||||
}
|
||||
};
|
||||
|
||||
const _getHoveredStyles = (variant: ButtonVariant): CSSProperties => ({
|
||||
...(variant !== 'bare' && styles.shadow),
|
||||
backgroundColor: backgroundColorHover[variant],
|
||||
color: textColorHover[variant],
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const _getActiveStyles = (
|
||||
variant: ButtonVariant,
|
||||
bounce: boolean,
|
||||
): CSSProperties => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return { backgroundColor: theme.buttonBareBackgroundActive };
|
||||
default:
|
||||
return {
|
||||
transform: bounce ? 'translateY(1px)' : undefined,
|
||||
boxShadow: `0 1px 4px 0 ${
|
||||
variant === 'primary'
|
||||
? theme.buttonPrimaryShadow
|
||||
: theme.buttonNormalShadow
|
||||
}`,
|
||||
transition: 'none',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
type ButtonProps = ComponentPropsWithoutRef<typeof ReactAriaButton> & {
|
||||
variant?: ButtonVariant;
|
||||
bounce?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
type ButtonVariant = 'normal' | 'primary' | 'bare' | 'menu' | 'menuSelected';
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(props, ref) => {
|
||||
const { children, variant = 'normal', bounce = true, ...restProps } = props;
|
||||
|
||||
const variantWithDisabled: ButtonVariant | `${ButtonVariant}Disabled` =
|
||||
props.isDisabled ? `${variant}Disabled` : variant;
|
||||
|
||||
const defaultButtonClassName: string = useMemo(
|
||||
() =>
|
||||
String(
|
||||
css({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
padding: _getPadding(variant),
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
borderRadius: 4,
|
||||
backgroundColor: backgroundColor[variantWithDisabled],
|
||||
border: _getBorder(variant, variantWithDisabled),
|
||||
color: textColor[variantWithDisabled],
|
||||
transition: 'box-shadow .25s',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
...styles.smallText,
|
||||
'&[data-hovered]': _getHoveredStyles(variant),
|
||||
'&[data-pressed]': _getActiveStyles(variant, bounce),
|
||||
}),
|
||||
),
|
||||
[bounce, variant, variantWithDisabled],
|
||||
);
|
||||
|
||||
const className = restProps.className;
|
||||
|
||||
return (
|
||||
<ReactAriaButton
|
||||
ref={ref}
|
||||
{...restProps}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps =>
|
||||
`${defaultButtonClassName} ${className(renderProps)}`
|
||||
: `${defaultButtonClassName} ${className || ''}`
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ReactAriaButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
type ButtonWithLoadingProps = ButtonProps & {
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export const ButtonWithLoading = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonWithLoadingProps
|
||||
>((props, ref) => {
|
||||
const { isLoading, children, style, ...buttonProps } = props;
|
||||
return (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
ref={ref}
|
||||
style={buttonRenderProps => ({
|
||||
position: 'relative',
|
||||
...(typeof style === 'function' ? style(buttonRenderProps) : style),
|
||||
})}
|
||||
>
|
||||
{isLoading && (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<AnimatedLoading style={{ width: 20, height: 20 }} />
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
opacity: isLoading ? 0 : 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
ButtonWithLoading.displayName = 'ButtonWithLoading';
|
||||
38
packages/component-library/src/Card.tsx
Normal file
38
packages/component-library/src/Card.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { type ComponentProps, forwardRef } from 'react';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { View } from './View';
|
||||
|
||||
type CardProps = ComponentProps<typeof View>;
|
||||
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<View
|
||||
{...props}
|
||||
ref={ref}
|
||||
style={{
|
||||
marginTop: 15,
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
borderRadius: 6,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderColor: theme.cardBorder,
|
||||
boxShadow: '0 1px 2px #9594A8',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
14
packages/component-library/src/FormError.tsx
Normal file
14
packages/component-library/src/FormError.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type ReactNode, type CSSProperties } from 'react';
|
||||
|
||||
import { View } from './View';
|
||||
|
||||
type FormErrorProps = {
|
||||
style?: CSSProperties;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function FormError({ style, children }: FormErrorProps) {
|
||||
return (
|
||||
<View style={{ color: 'red', fontSize: 13, ...style }}>{children}</View>
|
||||
);
|
||||
}
|
||||
36
packages/component-library/src/InitialFocus.ts
Normal file
36
packages/component-library/src/InitialFocus.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
cloneElement,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
type InitialFocusProps = {
|
||||
children:
|
||||
| ReactElement<{ inputRef: Ref<HTMLInputElement> }>
|
||||
| ((node: Ref<HTMLInputElement>) => ReactElement);
|
||||
};
|
||||
|
||||
export function InitialFocus({ children }: InitialFocusProps) {
|
||||
const node = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (node.current) {
|
||||
// This is needed to avoid a strange interaction with
|
||||
// `ScopeTab`, which doesn't allow it to be focused at first for
|
||||
// some reason. Need to look into it.
|
||||
setTimeout(() => {
|
||||
if (node.current) {
|
||||
node.current.focus();
|
||||
node.current.setSelectionRange(0, 10000);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return children(node);
|
||||
}
|
||||
return cloneElement(children, { inputRef: node });
|
||||
}
|
||||
47
packages/component-library/src/InlineField.tsx
Normal file
47
packages/component-library/src/InlineField.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
|
||||
type InlineFieldProps = {
|
||||
label: ReactNode;
|
||||
labelWidth?: number;
|
||||
children?: ReactNode;
|
||||
width: number | string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function InlineField({
|
||||
label,
|
||||
labelWidth,
|
||||
children,
|
||||
width,
|
||||
style,
|
||||
}: InlineFieldProps) {
|
||||
return (
|
||||
<label
|
||||
className={css([
|
||||
{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
margin: '7px 0',
|
||||
width,
|
||||
},
|
||||
style,
|
||||
])}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: labelWidth || 75,
|
||||
textAlign: 'right',
|
||||
paddingRight: 10,
|
||||
}}
|
||||
>
|
||||
{label}:
|
||||
</div>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
32
packages/component-library/src/Label.tsx
Normal file
32
packages/component-library/src/Label.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { forwardRef, type ReactNode, type CSSProperties } from 'react';
|
||||
|
||||
import { styles } from './styles';
|
||||
import { Text } from './Text';
|
||||
import { theme } from './theme';
|
||||
|
||||
type LabelProps = {
|
||||
title: ReactNode;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export const Label = forwardRef<HTMLSpanElement, LabelProps>(
|
||||
({ title, style }: LabelProps, ref) => {
|
||||
return (
|
||||
<Text
|
||||
ref={ref}
|
||||
style={{
|
||||
...styles.text,
|
||||
color: theme.tableRowHeaderText,
|
||||
textAlign: 'right',
|
||||
fontSize: 14,
|
||||
marginBottom: 2,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Label.displayName = 'Label';
|
||||
241
packages/component-library/src/Menu.tsx
Normal file
241
packages/component-library/src/Menu.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentType,
|
||||
type SVGProps,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import { Text } from './Text';
|
||||
import { theme } from './theme';
|
||||
import { Toggle } from './Toggle';
|
||||
import { View } from './View';
|
||||
|
||||
const MenuLine: unique symbol = Symbol('menu-line');
|
||||
const MenuLabel: unique symbol = Symbol('menu-label');
|
||||
Menu.line = MenuLine;
|
||||
Menu.label = MenuLabel;
|
||||
|
||||
type KeybindingProps = {
|
||||
keyName: ReactNode;
|
||||
};
|
||||
|
||||
function Keybinding({ keyName }: KeybindingProps) {
|
||||
return (
|
||||
<Text style={{ fontSize: 10, color: theme.menuKeybindingText }}>
|
||||
{keyName}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export type MenuItemObject<NameType, Type extends string | symbol = string> = {
|
||||
type?: Type;
|
||||
name: NameType;
|
||||
disabled?: boolean;
|
||||
icon?: ComponentType<SVGProps<SVGSVGElement>>;
|
||||
iconSize?: number;
|
||||
text: string;
|
||||
key?: string;
|
||||
toggle?: boolean;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
export type MenuItem<NameType = string> =
|
||||
| MenuItemObject<NameType>
|
||||
| MenuItemObject<string, typeof Menu.label>
|
||||
| typeof Menu.line;
|
||||
|
||||
function isLabel<T>(
|
||||
item: MenuItemObject<T> | MenuItemObject<string, typeof Menu.label>,
|
||||
): item is MenuItemObject<string, typeof Menu.label> {
|
||||
return item.type === Menu.label;
|
||||
}
|
||||
|
||||
type MenuProps<NameType> = {
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
items: Array<MenuItem<NameType>>;
|
||||
onMenuSelect?: (itemName: NameType) => void;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
getItemStyle?: (item: MenuItemObject<NameType>) => CSSProperties;
|
||||
};
|
||||
|
||||
export function Menu<const NameType = string>({
|
||||
header,
|
||||
footer,
|
||||
items: allItems,
|
||||
onMenuSelect,
|
||||
style,
|
||||
className,
|
||||
getItemStyle,
|
||||
}: MenuProps<NameType>) {
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const items = allItems.filter(x => x);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = elRef.current;
|
||||
el?.focus();
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const filteredItems = items.filter(
|
||||
item => item && item !== Menu.line && item.type !== Menu.label,
|
||||
);
|
||||
const currentIndex = filteredItems.indexOf(items[hoveredIndex || 0]);
|
||||
|
||||
const transformIndex = (idx: number) => items.indexOf(filteredItems[idx]);
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setHoveredIndex(
|
||||
hoveredIndex === null
|
||||
? 0
|
||||
: transformIndex(Math.max(currentIndex - 1, 0)),
|
||||
);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setHoveredIndex(
|
||||
hoveredIndex === null
|
||||
? 0
|
||||
: transformIndex(
|
||||
Math.min(currentIndex + 1, filteredItems.length - 1),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
const item = items[hoveredIndex || 0];
|
||||
if (hoveredIndex !== null && item !== Menu.line && !isLabel(item)) {
|
||||
onMenuSelect?.(item.name);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
el?.addEventListener('keydown', onKeyDown);
|
||||
|
||||
return () => {
|
||||
el?.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [hoveredIndex]);
|
||||
|
||||
return (
|
||||
<View
|
||||
className={className}
|
||||
style={{ outline: 'none', borderRadius: 4, overflow: 'hidden', ...style }}
|
||||
tabIndex={1}
|
||||
innerRef={elRef}
|
||||
>
|
||||
{header}
|
||||
{items.map((item, idx) => {
|
||||
if (item === Menu.line) {
|
||||
return (
|
||||
<View key={idx} style={{ margin: '3px 0px' }}>
|
||||
<View style={{ borderTop: '1px solid ' + theme.menuBorder }} />
|
||||
</View>
|
||||
);
|
||||
} else if (isLabel(item)) {
|
||||
return (
|
||||
<Text
|
||||
key={idx}
|
||||
style={{
|
||||
color: theme.menuItemTextHeader,
|
||||
fontSize: 11,
|
||||
lineHeight: '1em',
|
||||
textTransform: 'uppercase',
|
||||
margin: '3px 9px',
|
||||
marginTop: 5,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<View
|
||||
role="button"
|
||||
key={String(item.name)}
|
||||
style={{
|
||||
cursor: 'default',
|
||||
padding: 10,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: theme.menuItemText,
|
||||
...(item.disabled && { color: theme.buttonBareDisabledText }),
|
||||
...(!item.disabled &&
|
||||
hoveredIndex === idx && {
|
||||
backgroundColor: theme.menuItemBackgroundHover,
|
||||
color: theme.menuItemTextHover,
|
||||
}),
|
||||
...(!isLabel(item) && getItemStyle?.(item)),
|
||||
}}
|
||||
onPointerEnter={() => setHoveredIndex(idx)}
|
||||
onPointerLeave={() => setHoveredIndex(null)}
|
||||
onPointerUp={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (
|
||||
!item.disabled &&
|
||||
item.toggle === undefined &&
|
||||
!isLabel(item)
|
||||
) {
|
||||
onMenuSelect?.(item.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Force it to line up evenly */}
|
||||
{item.toggle === undefined ? (
|
||||
<>
|
||||
{Icon && (
|
||||
<Icon
|
||||
width={item.iconSize || 10}
|
||||
height={item.iconSize || 10}
|
||||
style={{ marginRight: 7, width: item.iconSize || 10 }}
|
||||
/>
|
||||
)}
|
||||
<Text title={item.tooltip}>{item.text}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<label htmlFor={String(item.name)} title={item.tooltip}>
|
||||
{item.text}
|
||||
</label>
|
||||
<Toggle
|
||||
id={String(item.name)}
|
||||
isOn={item.toggle}
|
||||
style={{ marginLeft: 5 }}
|
||||
onToggle={() =>
|
||||
!item.disabled &&
|
||||
!isLabel(item) &&
|
||||
item.toggle !== undefined &&
|
||||
onMenuSelect?.(item.name)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{item.key && <Keybinding keyName={item.key} />}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{footer}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
32
packages/component-library/src/Paragraph.tsx
Normal file
32
packages/component-library/src/Paragraph.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { type HTMLProps } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
|
||||
type ParagraphProps = HTMLProps<HTMLDivElement> & {
|
||||
style?: CSSProperties;
|
||||
isLast?: boolean;
|
||||
};
|
||||
|
||||
export function Paragraph({
|
||||
style,
|
||||
isLast,
|
||||
children,
|
||||
...props
|
||||
}: ParagraphProps) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={css([
|
||||
!isLast && { marginBottom: 15 },
|
||||
style,
|
||||
{
|
||||
lineHeight: '1.5em',
|
||||
},
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
packages/component-library/src/Popover.tsx
Normal file
57
packages/component-library/src/Popover.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { type ComponentProps, useCallback, useEffect, useRef } from 'react';
|
||||
import { Popover as ReactAriaPopover } from 'react-aria-components';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { styles } from './styles';
|
||||
|
||||
type PopoverProps = ComponentProps<typeof ReactAriaPopover>;
|
||||
|
||||
export const Popover = ({
|
||||
style = {},
|
||||
shouldCloseOnInteractOutside,
|
||||
...props
|
||||
}: PopoverProps) => {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(e: FocusEvent) => {
|
||||
if (!ref.current?.contains(e.relatedTarget as Node)) {
|
||||
props.onOpenChange?.(false);
|
||||
}
|
||||
},
|
||||
[props],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.isNonModal) return;
|
||||
if (props.isOpen) {
|
||||
ref.current?.addEventListener('focusout', handleFocus);
|
||||
} else {
|
||||
ref.current?.removeEventListener('focusout', handleFocus);
|
||||
}
|
||||
}, [handleFocus, props.isNonModal, props.isOpen]);
|
||||
|
||||
return (
|
||||
<ReactAriaPopover
|
||||
ref={ref}
|
||||
placement="bottom end"
|
||||
offset={1}
|
||||
className={css({
|
||||
...styles.tooltip,
|
||||
...styles.lightScrollbar,
|
||||
padding: 0,
|
||||
userSelect: 'none',
|
||||
...style,
|
||||
})}
|
||||
shouldCloseOnInteractOutside={element => {
|
||||
if (shouldCloseOnInteractOutside) {
|
||||
return shouldCloseOnInteractOutside(element);
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
31
packages/component-library/src/SpaceBetween.tsx
Normal file
31
packages/component-library/src/SpaceBetween.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { type CSSProperties, type ReactNode } from 'react';
|
||||
|
||||
import { View } from './View';
|
||||
|
||||
type SpaceBetweenProps = {
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
gap?: number;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const SpaceBetween = ({
|
||||
direction = 'horizontal',
|
||||
gap = 15,
|
||||
style,
|
||||
children,
|
||||
}: SpaceBetweenProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: direction === 'horizontal' ? 'row' : 'column',
|
||||
alignItems: 'center',
|
||||
gap,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
98
packages/component-library/src/Stack.tsx
Normal file
98
packages/component-library/src/Stack.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
// @ts-strict-ignore
|
||||
import React, {
|
||||
Children,
|
||||
type ComponentProps,
|
||||
Fragment,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
type ReactNode,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import { Text } from './Text';
|
||||
import { View } from './View';
|
||||
|
||||
function getChildren(key, children) {
|
||||
return Children.toArray(children).reduce(
|
||||
(list, child) => {
|
||||
if (child) {
|
||||
if (
|
||||
typeof child === 'object' &&
|
||||
'type' in child &&
|
||||
child.type === Fragment
|
||||
) {
|
||||
return list.concat(getChildren(child.key, child.props.children));
|
||||
}
|
||||
list.push({ key: key + child['key'], child });
|
||||
return list;
|
||||
}
|
||||
return list;
|
||||
},
|
||||
[] as Array<{ key: string; child: ReactNode }>,
|
||||
);
|
||||
}
|
||||
|
||||
type StackProps = ComponentProps<typeof View> & {
|
||||
direction?: CSSProperties['flexDirection'];
|
||||
align?: string;
|
||||
justify?: string;
|
||||
spacing?: number;
|
||||
debug?: boolean;
|
||||
};
|
||||
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
||||
(
|
||||
{
|
||||
direction = 'column',
|
||||
align,
|
||||
justify,
|
||||
spacing = 3,
|
||||
children,
|
||||
debug,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isReversed = direction.endsWith('reverse');
|
||||
const isHorizontal = direction.startsWith('row');
|
||||
const validChildren = getChildren('', children);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: direction,
|
||||
alignItems: align,
|
||||
justifyContent: justify,
|
||||
...style,
|
||||
}}
|
||||
innerRef={ref}
|
||||
{...props}
|
||||
>
|
||||
{validChildren.map(({ key, child }, index) => {
|
||||
const isLastChild = validChildren.length === index + 1;
|
||||
|
||||
let marginProp;
|
||||
if (isHorizontal) {
|
||||
marginProp = isReversed ? 'marginLeft' : 'marginRight';
|
||||
} else {
|
||||
marginProp = isReversed ? 'marginTop' : 'marginBottom';
|
||||
}
|
||||
|
||||
return cloneElement(
|
||||
typeof child === 'string' ? <Text>{child}</Text> : child,
|
||||
{
|
||||
key,
|
||||
style: {
|
||||
...(debug && { borderWidth: 1, borderColor: 'red' }),
|
||||
...(isLastChild ? null : { [marginProp]: spacing * 5 }),
|
||||
...(child.props ? child.props.style : null),
|
||||
},
|
||||
},
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Stack.displayName = 'Stack';
|
||||
30
packages/component-library/src/Text.tsx
Normal file
30
packages/component-library/src/Text.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, {
|
||||
type HTMLProps,
|
||||
type Ref,
|
||||
type ReactNode,
|
||||
forwardRef,
|
||||
} from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
|
||||
type TextProps = HTMLProps<HTMLSpanElement> & {
|
||||
innerRef?: Ref<HTMLSpanElement>;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export const Text = forwardRef<HTMLSpanElement, TextProps>((props, ref) => {
|
||||
const { className = '', style, innerRef, ...restProps } = props;
|
||||
return (
|
||||
<span
|
||||
{...restProps}
|
||||
ref={innerRef ?? ref}
|
||||
className={cx(className, css(style))}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Text.displayName = 'Text';
|
||||
22
packages/component-library/src/TextOneLine.tsx
Normal file
22
packages/component-library/src/TextOneLine.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { type ComponentProps } from 'react';
|
||||
|
||||
import { Text } from './Text';
|
||||
|
||||
type TextOneLineProps = ComponentProps<typeof Text>;
|
||||
|
||||
export function TextOneLine({ children, ...props }: TextOneLineProps) {
|
||||
return (
|
||||
<Text
|
||||
{...props}
|
||||
style={{
|
||||
...props.style,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
88
packages/component-library/src/Toggle.tsx
Normal file
88
packages/component-library/src/Toggle.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { type CSSProperties } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { View } from './View';
|
||||
|
||||
type ToggleProps = {
|
||||
id: string;
|
||||
isOn: boolean;
|
||||
isDisabled?: boolean;
|
||||
onToggle?: (isOn: boolean) => void;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export const Toggle = ({
|
||||
id,
|
||||
isOn,
|
||||
isDisabled = false,
|
||||
onToggle,
|
||||
className,
|
||||
style,
|
||||
}: ToggleProps) => {
|
||||
return (
|
||||
<View style={style} className={className}>
|
||||
<input
|
||||
id={id}
|
||||
checked={isOn}
|
||||
disabled={isDisabled}
|
||||
onChange={e => onToggle?.(e.target.checked)}
|
||||
className={css({
|
||||
height: 0,
|
||||
width: 0,
|
||||
visibility: 'hidden',
|
||||
})}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
data-toggle-container
|
||||
data-on={isOn}
|
||||
className={String(
|
||||
css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
width: '32px',
|
||||
height: '16px',
|
||||
borderRadius: '100px',
|
||||
position: 'relative',
|
||||
transition: 'background-color .2s',
|
||||
backgroundColor: isOn
|
||||
? theme.checkboxToggleBackgroundSelected
|
||||
: theme.checkboxToggleBackground,
|
||||
}),
|
||||
)}
|
||||
htmlFor={id}
|
||||
>
|
||||
<span
|
||||
data-toggle
|
||||
data-on={isOn}
|
||||
className={css(
|
||||
{
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
content: '" "',
|
||||
position: 'absolute',
|
||||
top: '2px',
|
||||
left: '2px',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '100px',
|
||||
transition: '0.2s',
|
||||
boxShadow: '0 0 2px 0 rgba(10, 10, 10, 0.29)',
|
||||
backgroundColor: isDisabled
|
||||
? theme.checkboxToggleDisabled
|
||||
: '#fff',
|
||||
},
|
||||
isOn && {
|
||||
left: 'calc(100% - 2px)',
|
||||
transform: 'translateX(-100%)',
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
71
packages/component-library/src/Tooltip.tsx
Normal file
71
packages/component-library/src/Tooltip.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { Tooltip as AriaTooltip, TooltipTrigger } from 'react-aria-components';
|
||||
|
||||
import { styles } from './styles';
|
||||
import { View } from './View';
|
||||
|
||||
type TooltipProps = Partial<ComponentProps<typeof AriaTooltip>> & {
|
||||
children: ReactNode;
|
||||
content: ReactNode;
|
||||
triggerProps?: Partial<ComponentProps<typeof TooltipTrigger>>;
|
||||
};
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
content,
|
||||
triggerProps = {},
|
||||
...props
|
||||
}: TooltipProps) => {
|
||||
const triggerRef = useRef(null);
|
||||
const [isHovered, setIsHover] = useState(false);
|
||||
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const handlePointerEnter = useCallback(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setIsHover(true);
|
||||
}, triggerProps.delay ?? 300);
|
||||
|
||||
hoverTimeoutRef.current = timeout;
|
||||
}, [triggerProps.delay]);
|
||||
|
||||
const handlePointerLeave = useCallback(() => {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
|
||||
setIsHover(false);
|
||||
}, []);
|
||||
|
||||
// Force closing the tooltip whenever the disablement state changes
|
||||
useEffect(() => {
|
||||
setIsHover(false);
|
||||
}, [triggerProps.isDisabled]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ minHeight: 'auto', flexShrink: 0, maxWidth: '100%' }}
|
||||
ref={triggerRef}
|
||||
onMouseEnter={handlePointerEnter}
|
||||
onMouseLeave={handlePointerLeave}
|
||||
>
|
||||
<TooltipTrigger
|
||||
isOpen={isHovered && !triggerProps.isDisabled}
|
||||
{...triggerProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
<AriaTooltip triggerRef={triggerRef} style={styles.tooltip} {...props}>
|
||||
{content}
|
||||
</AriaTooltip>
|
||||
</TooltipTrigger>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
31
packages/component-library/src/View.tsx
Normal file
31
packages/component-library/src/View.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { forwardRef, type HTMLProps, type Ref } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
|
||||
type ViewProps = HTMLProps<HTMLDivElement> & {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
nativeStyle?: CSSProperties;
|
||||
innerRef?: Ref<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const View = forwardRef<HTMLDivElement, ViewProps>((props, ref) => {
|
||||
// The default styles are special-cased and pulled out into static
|
||||
// styles, and hardcode the class name here. View is used almost
|
||||
// everywhere and we can avoid any perf penalty that glamor would
|
||||
// incur.
|
||||
|
||||
const { className = '', style, nativeStyle, innerRef, ...restProps } = props;
|
||||
return (
|
||||
<div
|
||||
{...restProps}
|
||||
ref={innerRef ?? ref}
|
||||
style={nativeStyle}
|
||||
className={cx('view', className, css(style))}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
View.displayName = 'View';
|
||||
26
packages/component-library/src/icons/AnimatedLoading.tsx
Normal file
26
packages/component-library/src/icons/AnimatedLoading.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { type SVGProps } from 'react';
|
||||
|
||||
import { css, keyframes } from '@emotion/css';
|
||||
|
||||
import { SvgLoading } from './Loading';
|
||||
|
||||
const rotation = keyframes({
|
||||
'0%': { transform: 'rotate(-90deg)' },
|
||||
'100%': { transform: 'rotate(666deg)' },
|
||||
});
|
||||
|
||||
export function AnimatedLoading(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<span
|
||||
className={css({
|
||||
animationName: rotation,
|
||||
animationDuration: '1.6s',
|
||||
animationTimingFunction: 'cubic-bezier(0.17, 0.67, 0.83, 0.67)',
|
||||
animationIterationCount: 'infinite',
|
||||
lineHeight: 0,
|
||||
})}
|
||||
>
|
||||
<SvgLoading {...props} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
33
packages/component-library/src/icons/Loading.tsx
Normal file
33
packages/component-library/src/icons/Loading.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { type SVGProps, useState } from 'react';
|
||||
|
||||
export const SvgLoading = (props: SVGProps<SVGSVGElement>) => {
|
||||
const { color = 'currentColor' } = props;
|
||||
const [gradientId] = useState('gradient-' + Math.random());
|
||||
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 38 38" style={{ ...props.style }}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
x1="8.042%"
|
||||
y1="0%"
|
||||
x2="65.682%"
|
||||
y2="23.865%"
|
||||
id={gradientId}
|
||||
>
|
||||
<stop stopColor={color} stopOpacity={0} offset="0%" />
|
||||
<stop stopColor={color} stopOpacity={0.631} offset="63.146%" />
|
||||
<stop stopColor={color} offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(1 2)" fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M36 18c0-9.94-8.06-18-18-18"
|
||||
stroke={'url(#' + gradientId + ')'}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
<circle fill={color} cx={36} cy={18} r={1} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
157
packages/component-library/src/styles.ts
Normal file
157
packages/component-library/src/styles.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { keyframes } from '@emotion/css';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { tokens } from './tokens';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type CSSProperties = Record<string, any>;
|
||||
|
||||
const MOBILE_MIN_HEIGHT = 40;
|
||||
|
||||
const shadowLarge = {
|
||||
boxShadow: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const styles: Record<string, any> = {
|
||||
incomeHeaderHeight: 70,
|
||||
cardShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
|
||||
monthRightPadding: 5,
|
||||
menuBorderRadius: 4,
|
||||
mobileMinHeight: MOBILE_MIN_HEIGHT,
|
||||
mobileMenuItem: {
|
||||
fontSize: 17,
|
||||
fontWeight: 400,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
height: MOBILE_MIN_HEIGHT,
|
||||
minHeight: MOBILE_MIN_HEIGHT,
|
||||
},
|
||||
mobileEditingPadding: 12,
|
||||
altMenuMaxHeight: 250,
|
||||
altMenuText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
altMenuHeaderText: {
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
},
|
||||
veryLargeText: {
|
||||
fontSize: 30,
|
||||
fontWeight: 600,
|
||||
},
|
||||
largeText: {
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
mediumText: {
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
},
|
||||
smallText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
verySmallText: {
|
||||
fontSize: 12,
|
||||
},
|
||||
tinyText: {
|
||||
fontSize: 10,
|
||||
},
|
||||
page: {
|
||||
flex: 1,
|
||||
'@media (max-height: 550px)': {
|
||||
minHeight: 700, // ensure we can scroll on small screens
|
||||
},
|
||||
paddingTop: 8, // height of the titlebar
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
paddingTop: 36,
|
||||
},
|
||||
},
|
||||
pageContent: {
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
},
|
||||
},
|
||||
settingsPageContent: {
|
||||
padding: 20,
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
padding: 'inherit',
|
||||
},
|
||||
},
|
||||
staticText: {
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
},
|
||||
shadow: {
|
||||
boxShadow: '0 2px 4px 0 rgba(0,0,0,0.1)',
|
||||
},
|
||||
shadowLarge,
|
||||
tnum: {
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
fontFeatureSettings: '"tnum"',
|
||||
},
|
||||
notFixed: { fontFeatureSettings: '' },
|
||||
text: {
|
||||
fontSize: 16,
|
||||
// lineHeight: 22.4 // TODO: This seems like trouble, but what's the right value?
|
||||
},
|
||||
delayedFadeIn: {
|
||||
animationName: keyframes({
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
}),
|
||||
animationDuration: '1s',
|
||||
animationFillMode: 'both',
|
||||
animationDelay: '0.5s',
|
||||
},
|
||||
underlinedText: {
|
||||
borderBottom: `2px solid`,
|
||||
},
|
||||
noTapHighlight: {
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
lineClamp: (lines: number) => {
|
||||
return {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: lines,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
};
|
||||
},
|
||||
tooltip: {
|
||||
padding: 5,
|
||||
...shadowLarge,
|
||||
borderWidth: 2,
|
||||
borderRadius: 4,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.tooltipBorder,
|
||||
backgroundColor: theme.tooltipBackground,
|
||||
color: theme.tooltipText,
|
||||
overflow: 'auto',
|
||||
},
|
||||
popover: {
|
||||
border: 'none',
|
||||
backgroundColor: theme.menuBackground,
|
||||
color: theme.menuItemText,
|
||||
},
|
||||
// Dynamically set
|
||||
horizontalScrollbar: null as CSSProperties | null,
|
||||
lightScrollbar: null as CSSProperties | null,
|
||||
darkScrollbar: null as CSSProperties | null,
|
||||
scrollbarWidth: null as number | null,
|
||||
editorPill: {
|
||||
color: theme.pillText,
|
||||
backgroundColor: theme.pillBackground,
|
||||
borderRadius: 4,
|
||||
padding: '3px 5px',
|
||||
},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user