Compare commits
621 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1b78cb4cb4
|
|||
| b32f805cbd | |||
|
3110fd347e
|
|||
|
5612ac4e95
|
|||
|
69ab4913aa
|
|||
|
39e01e4558
|
|||
|
7afc75dbff
|
|||
|
f5f74ae27f
|
|||
|
55f7baecf6
|
|||
|
bd199fcf26
|
|||
| 0777c213ba | |||
|
071922d979
|
|||
|
fc044df9fd
|
|||
|
dd2bdd5332
|
|||
|
0cd05d14bc
|
|||
|
3ee140723c
|
|||
|
49b3825af4
|
|||
|
af309ff682
|
|||
|
53f13e4a63
|
|||
|
bcdd033f34
|
|||
|
b9e7737589
|
|||
|
57b9bccb4a
|
|||
|
5c9fbf5043
|
|||
|
205d2d2afc
|
|||
| 895bf569ff | |||
|
dbedf1dbe8
|
|||
| 26f2f2afe9 | |||
|
6c456b1184
|
|||
|
03e7f14d1c
|
|||
|
e94e747ba1
|
|||
|
3679412b3c
|
|||
|
ea15e69d79
|
|||
|
8cc3da40b7
|
|||
|
bdeab512b6
|
|||
| 97ebebe28c | |||
|
d5edb93ec5
|
|||
|
0ecc4079c2
|
|||
|
3f080cf553
|
|||
|
0bdb4d2021
|
|||
|
1e7d8491f9
|
|||
|
4356288497
|
|||
|
87bf8ff2df
|
|||
|
26975636a0
|
|||
| dad56a7b31 | |||
|
d069fa02ab
|
|||
|
d219abc358
|
|||
|
13989bf973
|
|||
|
b3ab69d599
|
|||
|
fe3816501c
|
|||
| 854402aa73 | |||
|
abc19c715b
|
|||
|
b6434ac8d2
|
|||
|
4788b8c51c
|
|||
|
6a72a5b418
|
|||
|
d90ddd464a
|
|||
|
2626543f3e
|
|||
| 088db24534 | |||
| f7b7daa024 | |||
| bb11e2267b | |||
| 7cd023a21b | |||
|
1095bf0218
|
|||
|
138cec0389
|
|||
|
a93be41e08
|
|||
|
89f6fa0b5c
|
|||
|
e2a80eafd1
|
|||
|
662e76979b
|
|||
|
6c7de97e38
|
|||
|
fe27b010da
|
|||
|
ff716c68ea
|
|||
|
84dded2f10
|
|||
|
ee664c06c1
|
|||
|
62b618b657
|
|||
| 348dc03429 | |||
|
d159fca816
|
|||
|
a582f41fde
|
|||
|
2082b51c5b
|
|||
|
e543708b42
|
|||
|
0b0dea095f
|
|||
|
c7eb81450c
|
|||
|
ca1ccf6d93
|
|||
| 0b79d48be9 | |||
|
f8cc453d7e
|
|||
|
f86190030f
|
|||
|
08bedda288
|
|||
| 7c8ca45b27 | |||
|
ff3fe49dd0
|
|||
|
92056517a6
|
|||
|
11f19c8344
|
|||
|
0f8d7d84ca
|
|||
| e1780109aa | |||
|
8c2f6aa38b
|
|||
|
afbce03a4a
|
|||
|
1f2dd9d198
|
|||
|
e4814923f9
|
|||
|
ad015f1cee
|
|||
|
0c5aebf7b4
|
|||
| 52f7e86fc9 | |||
|
fb66bd4f0d
|
|||
|
19553b9777
|
|||
| 441dc21894 | |||
| 0732911e13 | |||
| 57934ae7a3 | |||
| 1e240b6539 | |||
| 2c773c147a | |||
|
e05449a7ff
|
|||
|
44b631c688
|
|||
|
6cd62d3764
|
|||
|
4c749bca35
|
|||
|
466354cff7
|
|||
|
721c330f3c
|
|||
| d07963ad6b | |||
|
eef9fa1aba
|
|||
|
77dbde8bb4
|
|||
| 64bd2477c7 | |||
|
aed29ff982
|
|||
| f3f158ff26 | |||
| 2a8fd3b001 | |||
|
087cb706d2
|
|||
|
b58870a03c
|
|||
|
ccda6ec354
|
|||
|
2045b94951
|
|||
|
be16a57043
|
|||
|
ae06385039
|
|||
|
7b1c3c813c
|
|||
|
ec50e7deef
|
|||
|
c3b1bd2f4c
|
|||
|
93e2a23ac7
|
|||
|
a41d1b1dbd
|
|||
|
c0ccf83061
|
|||
|
4ab5c3c372
|
|||
|
5d4fb1c423
|
|||
|
361ccb103d
|
|||
|
2bf590886c
|
|||
|
c7b6f9e3e7
|
|||
|
c1eb93c596
|
|||
|
d85235ff88
|
|||
|
63675a1e13
|
|||
|
87bc8f5cb5
|
|||
|
4abc1593cb
|
|||
|
25b2a8b0e5
|
|||
|
b5700092a8
|
|||
|
7288d75237
|
|||
|
444ec62a66
|
|||
|
326d46b1a1
|
|||
|
083c873631
|
|||
|
37dc91da57
|
|||
|
7c86edcaaa
|
|||
|
e4d2fdfce4
|
|||
|
4722064337
|
|||
|
9f35add07f
|
|||
|
e86541574d
|
|||
|
5f90bc23bc
|
|||
|
1dd63b2c7a
|
|||
|
f1bd20a6f4
|
|||
|
ec6b72008d
|
|||
|
dec71c6f4f
|
|||
|
52172ea065
|
|||
|
f63d14d7b4
|
|||
|
ef0fc11edf
|
|||
|
083334ed18
|
|||
|
69a6c850bf
|
|||
|
8074d2d0e3
|
|||
|
11c3078b55
|
|||
|
fb267813fb
|
|||
|
a96cca879d
|
|||
|
4849a755dc
|
|||
|
eaac245f4e
|
|||
|
95743330e4
|
|||
|
4a9dd5ff09
|
|||
|
3b127b0e81
|
|||
|
769317777c
|
|||
| 174073fc42 | |||
| 733e5e3683 | |||
| 090d1d0856 | |||
| e047cc45ba | |||
| c64a503546 | |||
| 13021d665d | |||
| b5dbb2449d | |||
| 302c3315a3 | |||
| bac4c1c425 | |||
| c54d754c89 | |||
| 63dd5430bc | |||
| eb2c613ac9 | |||
| bf6d9dbc31 | |||
| c491f10ec3 | |||
| d4b546bfcd | |||
| 69141a31c4 | |||
| e7c9620bbe | |||
| 832de3c06f | |||
| 50e1fc728c | |||
| 871731110b | |||
| f996c89dfe | |||
| 1d8e9ddf5d | |||
| 352c9fe3ab | |||
| e2a944ed5b | |||
| c57d4162d7 | |||
| 41cc37d3eb | |||
| 081584a508 | |||
| 3cc01cce4e | |||
| cedbe3cb3d | |||
| b8da04c145 | |||
| 009caa53e5 | |||
| 9a1f543921 | |||
| 22300c2980 | |||
| 018132034d | |||
| f5d7916556 | |||
| 12df28f7fd | |||
| 2f4f350a93 | |||
| 4946b3311d | |||
| cc2f91afce | |||
| aa351cbe17 | |||
| 065c635580 | |||
| 67e3c9a7c3 | |||
| 4f4b3c5c17 | |||
| 4f6bafa31e | |||
| 94846c6e00 | |||
| 45d553866e | |||
| 8ac6e3ad3c | |||
| 215ad1d640 | |||
| 3f4cc39067 | |||
| 0a6b6180b9 | |||
| 536868002f | |||
| 04a7f9139d | |||
| 863b542e3e | |||
| 23d86f1502 | |||
| 314cf58c29 | |||
| 9c063b82f1 | |||
| b397e4faf6 | |||
| 256197094e | |||
| 9898d4bb1d | |||
| c321dc51b0 | |||
| 8c759601ca | |||
| 15e5d3c2fb | |||
| 47b7a6501b | |||
| 42304a8e27 | |||
| 9a4db6daf6 | |||
| 26dc78f553 | |||
| 5a618f7ef9 | |||
| 5d29bd9604 | |||
| 97cc71024f | |||
| 58f1382df1 | |||
| 6c5406c0b9 | |||
| b2457421be | |||
| 134eb7d355 | |||
| 1dc1a9767e | |||
| 243a41db88 | |||
| 523d4524b5 | |||
| c21613f20a | |||
| 12b127c75d | |||
| a7dd058d21 | |||
| de6606b29d | |||
| ec701b3130 | |||
| 8e7430ebb9 | |||
| 1deecafee7 | |||
| 51d8b6c8c1 | |||
| b28f6e6bd7 | |||
| 4ab28016a3 | |||
| 6995b67a22 | |||
| 0076d59a42 | |||
| 36b92d97ca | |||
| f272bcfcbc | |||
| 985e1673ac | |||
| fe4af46852 | |||
| 7b9659e22f | |||
| 8be18fc27c | |||
| 7e518780c5 | |||
| 89391c1543 | |||
| 05e3118a0c | |||
| 494b5d9bdd | |||
| f8f1ad644c | |||
| 5a5051bac6 | |||
| 304c0ac8d0 | |||
| cae13ed662 | |||
| 729110f8d1 | |||
| 11f66de6e6 | |||
| 9bf2ae2d7f | |||
| 2a384f3eed | |||
| 6b3666def9 | |||
| 74c8e32cc6 | |||
| 5dde978499 | |||
| 0625ee9d79 | |||
| dc5dfe19f2 | |||
| c518b7f565 | |||
| d13242b210 | |||
| ada029586a | |||
| 7bf5fa444a | |||
| d08d5f9e8d | |||
| a2bd3f76b8 | |||
| a4ca37d882 | |||
| 143317d909 | |||
| d8bcc8ff67 | |||
| e4a39ec67c | |||
| 5b47ba9e11 | |||
| d8f565cd9e | |||
| a9480a09f1 | |||
| 60ba019139 | |||
| 413bcddb1e | |||
| c2273d1778 | |||
| 780afc9bc6 | |||
| a8ae2b0156 | |||
| d6d2b1a61c | |||
| 4c70c0d233 | |||
| 725d36bed6 | |||
| 0a71d7ad91 | |||
| 6a176b5904 | |||
| c7d046aa46 | |||
| aa9c3648c0 | |||
| 9c18593529 | |||
| 52fe7ff026 | |||
| b02967c5ef | |||
| 7236b2d2b3 | |||
| fd1ae0a612 | |||
| 525a39479b | |||
| 612b76300c | |||
| 42fe01b349 | |||
| 80af572742 | |||
| d7a0817ac6 | |||
| c65880972a | |||
| c94319e6a8 | |||
| f9dbd47922 | |||
| 636ac30622 | |||
| 791943bb95 | |||
| d9ae654f42 | |||
| 8a156f964b | |||
| 743a4cc3ac | |||
| 5f1ec4887e | |||
| fe16a605c1 | |||
| 9cf743b02a | |||
| d7294d9d7f | |||
| d8a9886442 | |||
| d83d671580 | |||
| 0bbca079c5 | |||
| 8bd7a77d44 | |||
| a73a4da575 | |||
| 8cc1b02d19 | |||
| 6b49ca26c0 | |||
| ffe555e2ee | |||
| aa98b35796 | |||
| b2bbbfd010 | |||
| edd7ffd3c5 | |||
| abb7d95804 | |||
| 0e0b1afe3a | |||
| 29a6d79d38 | |||
| 07c579850e | |||
| 091ebf4b6a | |||
| b4e89492b4 | |||
| f5c89fd567 | |||
| 1fbf8f0769 | |||
| cc8952f844 | |||
| 25ea20e63b | |||
| e60651858d | |||
| 24390c6503 | |||
| c1d5c90745 | |||
| 3d20bce5e3 | |||
| 3fe8043c76 | |||
| bad86232b4 | |||
| 7eb14ac4e2 | |||
| 8b0cd8c986 | |||
| 5064a472f2 | |||
| afc5a5fe50 | |||
| f9f6786f95 | |||
| 03f31e2d20 | |||
| 1c1f772ff5 | |||
| 58bd729acb | |||
| 1ea5305e6e | |||
| e84a1cbc4e | |||
| 29115d9b16 | |||
| 409d5e33f9 | |||
| 883b5dc415 | |||
| 16ee3b6b6b | |||
| 4efd6f8f88 | |||
| af9951ca5a | |||
| c30c6c2b36 | |||
| 8bd4c6e6e1 | |||
| 9a98b37bb6 | |||
| fad10ec1ea | |||
| cec87f6caf | |||
| 22a9786168 | |||
| 18be60da9b | |||
| 4b6c79345b | |||
| f07100b7ea | |||
| 7083da367d | |||
| 8720a0faab | |||
| d8b1b3d777 | |||
| 4c2d95f8f1 | |||
| 9386c30d09 | |||
| 6638691c9f | |||
| d2bb70e8a1 | |||
| ef45d55521 | |||
| f86c53de89 | |||
| 82b275caaa | |||
| e524e5558d | |||
| 6593005566 | |||
| cf77f20d41 | |||
| 67ee2d9a74 | |||
| 48e5ddbc02 | |||
| e1737392a7 | |||
| 00b7b380b9 | |||
| 9821c8b2ea | |||
| c6a37f7e94 | |||
| f25c74c7e9 | |||
| 1c49d56b19 | |||
| 109ffd2898 | |||
| 7d5b93a62c | |||
| 6c0cb1a29c | |||
| d444b86ca8 | |||
| 185d7c58c2 | |||
| 283ec1d48a | |||
| f32a34a702 | |||
| afa09f4b8f | |||
| 24a3d8beb4 | |||
| 2a7ec1073f | |||
| d02fb4716a | |||
| 49bfad51b6 | |||
| 25ead15014 | |||
| 85a9d1c73b | |||
| 247fc8785a | |||
| efd1b371f9 | |||
| 3c73ddd443 | |||
| 29e9df97fb | |||
| fa95376fda | |||
| 028391386b | |||
| de937aceee | |||
| 2f4807cdda | |||
| d1791cadfe | |||
| 83fd9a94b0 | |||
| 562a8d159b | |||
| 1000dd12bc | |||
| 6f4afaf84a | |||
| 5de33a18f5 | |||
| 2aae2a8f90 | |||
| 6dcbd1efbf | |||
| a2ddb2313d | |||
| 6de7f64d3d | |||
| d0cae59070 | |||
| 6bd87ae0de | |||
| 0380ea3e98 | |||
| 9dba432542 | |||
| da608a0012 | |||
| 0af78e3dbd | |||
| 5db628311e | |||
| aa28a14d04 | |||
| 9c4e431e3b | |||
| 8984b1ecb7 | |||
| f05b0bbd8b | |||
| bff611f866 | |||
| b17e4c0a63 | |||
| f0b75d96db | |||
| 190975b981 | |||
| e4c9a88eca | |||
| 1f8fa367a5 | |||
| 5202fe0f8e | |||
| f202b34d28 | |||
| 07fa07eb22 | |||
| 137f9ae8bf | |||
| e44a8efa4a | |||
| a7b288c25f | |||
| 3dbcba5eb2 | |||
| bd2a640be0 | |||
| 889f2c05fc | |||
| c3f2afcaf5 | |||
| 32c85ea56b | |||
| 97210df0ba | |||
| 88be3525b5 | |||
| 7da7de1edc | |||
| 7592d67d69 | |||
| b75ac5c8b9 | |||
| 2954955e39 | |||
| 1fb20b1d08 | |||
| bfa0b350ac | |||
| 487013f301 | |||
| b16bb2d93a | |||
| 64997d74e9 | |||
| 297ed4ee45 | |||
| b4a094bc89 | |||
| e43706e2a6 | |||
| 2befc91d90 | |||
| 7a35f5177c | |||
| 6efc3ea790 | |||
| ef0f3ffbfb | |||
| 1e14cc695e | |||
| c074ccf460 | |||
| 5436469128 | |||
| 32714efeb9 | |||
| cf777571dc | |||
| 44b4340a91 | |||
| 24e3171311 | |||
| 575c1a11c2 | |||
| 47b8421d35 | |||
| 38d853a272 | |||
| 0320d6149f | |||
| ba9b3530fa | |||
| e78832e2c8 | |||
| 804060ad34 | |||
| 840519a437 | |||
| 7c54ebcbd0 | |||
| d07c0d67bf | |||
| a0ee2f7bb8 | |||
| d070288e17 | |||
| 681f80f6ab | |||
| eb5c87308a | |||
| 3e7fe1b5be | |||
| 552ca3026d | |||
| ba50e5a878 | |||
| acd1ce28f4 | |||
| 1b2b1bd447 | |||
| 7083d2369d | |||
| 8fed06b494 | |||
| 75650c19ab | |||
| ade47dadde | |||
| eaec617756 | |||
| 2199394abc | |||
| 93a18a8a20 | |||
| 2190b7af68 | |||
| 7da4956f1e | |||
| 31e27865bb | |||
| 967f213cba | |||
| 0e556aa794 | |||
| e2ac6099e3 | |||
| a4088e7acf | |||
| 550bd48fdb | |||
| f0f82a96fb | |||
| 8d303ea5af | |||
| c0b5c36115 | |||
| ba3c707da3 | |||
| b09f19f399 | |||
| 539f32911d | |||
| e009caaf25 | |||
| 1761ffbd3b | |||
| 33d6499dd4 | |||
| 3839eb2be4 | |||
| a04ab09ed6 | |||
| 89d6b920b1 | |||
| d425ed6b9e | |||
| e4215557c8 | |||
| fcc4045a4e | |||
| 5bcce71b36 | |||
| c9abec8a46 | |||
| 62e195d186 | |||
| e5f69a5e9a | |||
| e31ff492d5 | |||
| 99bbcda709 | |||
| 8aa941e704 | |||
| fdda30da67 | |||
| 399ee27af9 | |||
| d2c43b9ef1 | |||
| 7e6c6a037d | |||
| e1f19a1832 | |||
| 7b5cea3613 | |||
| 5d875d23e2 | |||
| e24fc6c81d | |||
| 72c6266474 | |||
| 4a711788f6 | |||
| c373c901a2 | |||
| 32846194b6 | |||
| 1a5d33b1f0 | |||
| 0e330a6529 | |||
| e9f0638ca2 | |||
| ce8fef79dc | |||
| b9fa0e2d60 | |||
| 41a04d6b53 | |||
| e527099b00 | |||
| 7e04cef964 | |||
| cb1208acaa | |||
| f985ce6402 | |||
| f54a36f240 | |||
| 4182f0993c | |||
| 28f7466595 | |||
| 6cfd4240de | |||
| 6e24015f76 | |||
| 5390c5cea8 | |||
| adcc81ba52 | |||
| 31a1f21dcd | |||
| b7316e15a2 | |||
| ad92ffa447 | |||
| 45044c0f61 | |||
| cc7c47cbab | |||
| 27f4006f05 | |||
| 576313a7b0 | |||
| 13c3feaa69 | |||
| a0dd8b925b | |||
| 21f187cac2 | |||
| d8951caddb | |||
| 206c657b98 | |||
| 6e262537a4 | |||
| 0587559939 | |||
| 939baec1a8 | |||
| 429f670c12 | |||
| b21d2ad864 | |||
| 8f841522a3 | |||
| fa7db66f4e | |||
| 499b22af15 | |||
| f992fb10bd | |||
| 603224e906 | |||
| bbea07b2a7 | |||
| b9d78efaff | |||
| 1a58099413 | |||
| e2d02d357c | |||
| 0812ff3467 | |||
| 6dafc26deb | |||
| a418ddbe52 | |||
| 6a5eb5a313 | |||
| 6354b03717 | |||
| 5d6b4a5255 | |||
| 3f0b1ccbcd | |||
| f00ecfd9ab | |||
| ee0d3370b1 | |||
| 73d7ed52dd | |||
| 6a31b0d579 | |||
| 869c4c0ae9 | |||
| 4be696ea62 | |||
| 83a0d56314 | |||
| 0d1418e595 | |||
| 7db521a72a | |||
| e7ff606891 | |||
| 60de3d8a31 | |||
| f12740dfa8 | |||
| 020a1c5682 | |||
| 576e2741a9 | |||
| bc19363bf9 | |||
| 72cf4b495e |
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
|||||||
+1
-1
@@ -5,5 +5,5 @@
|
|||||||
|
|
||||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||||
*/
|
*/
|
||||||
"disableAnalytics": false
|
"disableAnalytics": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# unconventional js
|
||||||
|
/blueprints/*/files/
|
||||||
|
/vendor/
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist/
|
||||||
|
/tmp/
|
||||||
|
/release/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/bower_components/
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/coverage/
|
||||||
|
!.*
|
||||||
|
|
||||||
|
# ember-try
|
||||||
|
/.node_modules.ember-try/
|
||||||
|
/bower.json.ember-try
|
||||||
|
/package.json.ember-try
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@babel/eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
requireConfigFile: false,
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
legacyDecorators: true
|
||||||
|
},
|
||||||
|
babelOptions: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
plugins: [
|
||||||
|
['@babel/plugin-proposal-decorators', { legacy: true }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
console: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'ember'
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:ember/recommended'
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
browser: true
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'ember/avoid-leaking-state-in-ember-objects': 'warn',
|
||||||
|
'no-console': 'off',
|
||||||
|
'ember/no-jquery': 'error',
|
||||||
|
'ember/require-computed-property-dependencies': 'warn',
|
||||||
|
'ember/no-computed-properties-in-native-classes': 'warn',
|
||||||
|
'ember/no-observers': 'warn',
|
||||||
|
'ember/no-classic-classes': 'warn',
|
||||||
|
'ember/no-classic-components': 'warn',
|
||||||
|
'ember/no-controller-access-in-routes': 'warn',
|
||||||
|
'ember/no-actions-hash': 'warn',
|
||||||
|
'ember/require-tagless-components': 'warn'
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
// node files
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'.eslintrc.js',
|
||||||
|
'.template-lintrc.js',
|
||||||
|
'ember-cli-build.js',
|
||||||
|
'testem.js',
|
||||||
|
'blueprints/*/index.js',
|
||||||
|
'config/**/*.js',
|
||||||
|
'lib/*/index.js',
|
||||||
|
'server/**/*.js'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'script'
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: false,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
plugins: ['node'],
|
||||||
|
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
|
||||||
|
// add your custom rules and overrides for node files here
|
||||||
|
|
||||||
|
// this can be removed once the following is fixed
|
||||||
|
// https://github.com/mysticatea/eslint-plugin-node/issues/77
|
||||||
|
'node/no-unpublished-require': 'off'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [16.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm install
|
||||||
|
- run: npm test
|
||||||
+16
-7
@@ -1,17 +1,26 @@
|
|||||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
/dist
|
/dist/
|
||||||
/tmp
|
/tmp/
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/bower_components/
|
||||||
/bower_components
|
/node_modules/
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
|
/.env*
|
||||||
|
/.pnp*
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
/connect.lock
|
/connect.lock
|
||||||
/coverage/*
|
/coverage/
|
||||||
/libpeerconnection.log
|
/libpeerconnection.log
|
||||||
npm-debug.log*
|
/npm-debug.log*
|
||||||
testem.log
|
/testem.log
|
||||||
|
/yarn-error.log
|
||||||
|
.tm_properties
|
||||||
|
|
||||||
|
# ember-try
|
||||||
|
/.node_modules.ember-try/
|
||||||
|
/bower.json.ember-try
|
||||||
|
/package.json.ember-try
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"predef": [
|
|
||||||
"document",
|
|
||||||
"window",
|
|
||||||
"-Promise"
|
|
||||||
],
|
|
||||||
"browser": true,
|
|
||||||
"boss": true,
|
|
||||||
"curly": true,
|
|
||||||
"debug": false,
|
|
||||||
"devel": true,
|
|
||||||
"eqeqeq": true,
|
|
||||||
"evil": true,
|
|
||||||
"forin": false,
|
|
||||||
"immed": false,
|
|
||||||
"laxbreak": false,
|
|
||||||
"newcap": true,
|
|
||||||
"noarg": true,
|
|
||||||
"noempty": false,
|
|
||||||
"nonew": false,
|
|
||||||
"nomen": false,
|
|
||||||
"onevar": false,
|
|
||||||
"plusplus": false,
|
|
||||||
"regexp": false,
|
|
||||||
"undef": true,
|
|
||||||
"sub": true,
|
|
||||||
"strict": false,
|
|
||||||
"white": false,
|
|
||||||
"eqnull": true,
|
|
||||||
"esversion": 6,
|
|
||||||
"unused": true
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extends: 'octane',
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'simple-unless': false,
|
||||||
|
'no-nested-interactive': false,
|
||||||
|
'no-html-comments': false
|
||||||
|
},
|
||||||
|
|
||||||
|
ignore: [
|
||||||
|
'kredits-web/templates/components/**',
|
||||||
|
'app/templates/components/**'
|
||||||
|
],
|
||||||
|
|
||||||
|
pending: [
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/dashboard",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/add-contribution/template",
|
||||||
|
"only": [
|
||||||
|
"no-action",
|
||||||
|
"no-curly-component-invocation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/add-contributor/template",
|
||||||
|
"only": [
|
||||||
|
"no-action",
|
||||||
|
"no-curly-component-invocation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/contribution-list/template",
|
||||||
|
"only": [
|
||||||
|
"no-action",
|
||||||
|
"no-curly-component-invocation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/contributor-list/template",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/expense-list/template",
|
||||||
|
"only": [
|
||||||
|
"no-invalid-role"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/topbar-account-panel/template",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/contributions/new",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/contributions/resubmit",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/contributors/edit",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/contributors/new",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/signup/eth-account",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/signup/index",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "6"
|
|
||||||
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- $HOME/.npm
|
|
||||||
- $HOME/.cache # includes bowers cache
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- npm config set spin false
|
|
||||||
- npm install -g bower
|
|
||||||
- bower --version
|
|
||||||
- npm install phantomjs-prebuilt
|
|
||||||
- node_modules/phantomjs-prebuilt/bin/phantomjs --version
|
|
||||||
|
|
||||||
install:
|
|
||||||
- npm install
|
|
||||||
- bower install
|
|
||||||
|
|
||||||
script:
|
|
||||||
- npm test
|
|
||||||
@@ -1,77 +1,123 @@
|
|||||||
[](https://travis-ci.org/67P/kredits-web)
|
[](https://github.com/67P/kredits-web/actions/workflows/ci.yml?query=branch%3Amaster)
|
||||||
|
|
||||||
# kredits-web
|
# Kredits Web
|
||||||
|
|
||||||
This README outlines the details of collaborating on this Ember application.
|
This is the main Web UI for Kosmos [Kredits](https://wiki.kosmos.org/Kredits).
|
||||||
A short introduction of this app could easily go here.
|
It provides an overview of contributors and contributions, as well as the
|
||||||
|
community's budget, expenses, and reimbursements.
|
||||||
|
|
||||||
## Prerequisites
|
It is an unhosted Web app, fetching all data from
|
||||||
|
[Rootstock](https://rootstock.io/) and [IPFS](https://ipfs.tech/), and caching
|
||||||
|
it in the browser's local database. As such, you can run the app directly from
|
||||||
|
the `/release` directory on any Web server that can serve static assets.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
You will need the following things properly installed on your computer.
|
You will need the following things properly installed on your computer.
|
||||||
|
|
||||||
* [Git](https://git-scm.com/)
|
* [Git](https://git-scm.com/)
|
||||||
* [Node.js](https://nodejs.org/) (with NPM)
|
* [Node.js](https://nodejs.org/) (with npm)
|
||||||
* [Bower](https://bower.io/)
|
|
||||||
* [Ember CLI](https://ember-cli.com/)
|
* [Ember CLI](https://ember-cli.com/)
|
||||||
* [PhantomJS](http://phantomjs.org/)
|
* [Google Chrome](https://google.com/chrome/) (only for running tests)
|
||||||
|
|
||||||
## Installation
|
### Installation
|
||||||
|
|
||||||
* `git clone <repository-url>` this repository
|
* `git clone git@github.com:67P/kredits-web.git` this repository
|
||||||
* `cd kredits-web`
|
* `cd kredits-web`
|
||||||
* `npm install`
|
* `npm install`
|
||||||
* `bower install`
|
|
||||||
|
|
||||||
## Running / Development
|
### Building/running for development
|
||||||
|
|
||||||
* `ember serve`
|
* `npm start` - by default Kredits Web connects to the Rootstock testnet network
|
||||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
* Visit the app at [http://localhost:4200](http://localhost:4200).
|
||||||
|
* Visit the tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
||||||
|
|
||||||
### Code Generators
|
See [working with locally deployed contracts](https://github.com/67P/kredits-web#working-with-locally-deployed-contracts) for details on how to develop with locally deployed contracts.
|
||||||
|
|
||||||
|
### Code generators
|
||||||
|
|
||||||
Make use of the many generators for code, try `ember help generate` for more details
|
Make use of the many generators for code, try `ember help generate` for more details
|
||||||
|
|
||||||
### Running Tests
|
### Running tests
|
||||||
|
|
||||||
* `ember test`
|
* `ember test`
|
||||||
* `ember test --server`
|
* `ember test --server`
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
* `npm run lint:hbs`
|
||||||
|
* `npm run lint:js`
|
||||||
|
* `npm run lint:js -- --fix`
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
* `ember build` (development)
|
* `ember build` (development)
|
||||||
* `ember build --environment production` (production)
|
* `npm run build` (production)
|
||||||
|
|
||||||
### Deploying
|
### Deploying
|
||||||
|
|
||||||
Specify what it takes to deploy your app.
|
_(You need collaborator permissions on the 5apps Deploy project.)_
|
||||||
|
|
||||||
|
`npm run deploy`
|
||||||
|
|
||||||
## Working with locally deployed contracts
|
### Working with locally deployed contracts
|
||||||
|
|
||||||
For development you might want to run your own development chain with your own development contracts.
|
The smart contracts and their JavaScript wrapper library are developed in the
|
||||||
The kredits-contracts helper scripts are available in kredits-web. See [kredits-contracts](https://github.com/67P/kredits-contracts) for more information.
|
[kredits-contracts](https://github.com/67P/kredits-contracts) repo/package.
|
||||||
|
|
||||||
Start your parity node with a KreditsChain:
|
You can run `kredits-web` on your machine, against a local, simulated
|
||||||
|
blockchain. [kredits-contracts](https://github.com/67P/kredits-contracts)
|
||||||
|
contains all the tools to start and set up such a simulated network, as well as
|
||||||
|
to deploy the Kredits smart contracts to it.
|
||||||
|
|
||||||
npm run kredits-parity
|
These are the basic steps to get up and running:
|
||||||
|
|
||||||
Deploy the contracts to your local chain:
|
#### 1. IPFS
|
||||||
|
|
||||||
npm run kredits-deploy
|
Run a local IPFS deamon.
|
||||||
|
|
||||||
Run kredits-web with your localy deployed contracts:
|
* Make sure CORS headers are configured. See [IPFS](#ipfs) for more info.
|
||||||
|
* `ipfs daemon`
|
||||||
|
|
||||||
KREDITS_CONTRACT_ADDR=`kredits-inspect -c Kredits -r -w address` \
|
#### 2. kredits-contracts
|
||||||
TOKEN_CONTRACT_ADDR=`kredits-inspect -c Token -r -w address` \
|
|
||||||
npm start
|
|
||||||
|
|
||||||
You need to have `./node_modules/.bin` in your path for this to work. Otherwise simply copy the contract addresses and set the environment variables manually.
|
Run a local devchain with test data. (See [kredits-contracts
|
||||||
|
README](https://github.com/67P/kredits-contracts) for details.
|
||||||
|
|
||||||
|
* Clone [kredits-contracts](https://github.com/67P/kredits-contracts)
|
||||||
|
* `npm install`
|
||||||
|
* `npm run devchain` - runs a local development chain
|
||||||
|
* `npm run bootstrap` - deploys all contracts and seeds test data
|
||||||
|
* `npm link` - makes the `kredits-contracts` module linkable as `kredits-contracts` on your machine
|
||||||
|
|
||||||
|
#### 3. kredits-web
|
||||||
|
|
||||||
|
With IPFS and the local devchain running, you can now link the contracts and
|
||||||
|
start the Ember app:
|
||||||
|
|
||||||
|
* `npm link kredits-contracts` - links the local `kredits-contracts` package (has to be done again after every `npm install`)
|
||||||
|
* `npm run start:local` - runs the Ember app with WEB3_PROVIDER_URL=http://localhost:8545 set
|
||||||
|
|
||||||
|
#### 4. Metamask network
|
||||||
|
|
||||||
|
If you want to interact with the local contracts via a Web3 wallet, switch the
|
||||||
|
network to a "Custom RPC" one, with the RPC URL `http://localhost:8545`.
|
||||||
|
|
||||||
|
#### IPFS
|
||||||
|
|
||||||
|
If you haven't configured your IPFS node for CORS yet, you can do so by running
|
||||||
|
the following commands:
|
||||||
|
|
||||||
|
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]'
|
||||||
|
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]'
|
||||||
|
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
|
||||||
|
ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/8080
|
||||||
|
|
||||||
## Further Reading / Useful Links
|
## Further Reading / Useful Links
|
||||||
|
|
||||||
* [ember.js](http://emberjs.com/)
|
* [ember.js](https://emberjs.com/)
|
||||||
* [ember-cli](https://ember-cli.com/)
|
* [ember-cli](https://ember-cli.com/)
|
||||||
* Development Browser Extensions
|
* Development Browser Extensions
|
||||||
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
|
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
|
||||||
|
|||||||
+7
-13
@@ -1,18 +1,12 @@
|
|||||||
import Ember from 'ember';
|
import Application from '@ember/application';
|
||||||
import Resolver from './resolver';
|
import Resolver from 'ember-resolver';
|
||||||
import loadInitializers from 'ember-load-initializers';
|
import loadInitializers from 'ember-load-initializers';
|
||||||
import config from './config/environment';
|
import config from './config/environment';
|
||||||
|
|
||||||
let App;
|
export default class App extends Application {
|
||||||
|
modulePrefix = config.modulePrefix;
|
||||||
Ember.MODEL_FACTORY_INJECTIONS = true;
|
podModulePrefix = config.podModulePrefix;
|
||||||
|
Resolver = Resolver;
|
||||||
App = Ember.Application.extend({
|
}
|
||||||
modulePrefix: config.modulePrefix,
|
|
||||||
podModulePrefix: config.podModulePrefix,
|
|
||||||
Resolver
|
|
||||||
});
|
|
||||||
|
|
||||||
loadInitializers(App, config.modulePrefix);
|
loadInitializers(App, config.modulePrefix);
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import Component from '@ember/component';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
import { and, notEmpty } from '@ember/object/computed';
|
||||||
|
import { assign } from '@ember/polyfills';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
attributes: null,
|
||||||
|
|
||||||
|
contributors: computed('kredits.contributorsSorted.[]', function() {
|
||||||
|
return this.kredits.contributorsSorted.map(c => {
|
||||||
|
return {
|
||||||
|
id: c.id.toString(),
|
||||||
|
name: c.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
isValidContributor: notEmpty('contributorId'),
|
||||||
|
isValidKind: notEmpty('kind'),
|
||||||
|
isValidAmount: computed('amount', function() {
|
||||||
|
return parseInt(this.amount, 10) > 0;
|
||||||
|
}),
|
||||||
|
isValidDescription: notEmpty('description'),
|
||||||
|
isValidUrl: notEmpty('url'),
|
||||||
|
isValid: and('isValidContributor',
|
||||||
|
'isValidKind',
|
||||||
|
'isValidAmount',
|
||||||
|
'isValidDescription'),
|
||||||
|
|
||||||
|
init () {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.set('defaultDate', moment().startOf('hour').toDate());
|
||||||
|
this.set('defaultAttr', {
|
||||||
|
contributorId: null,
|
||||||
|
kind: null,
|
||||||
|
date: this.defaultDate,
|
||||||
|
amount: null,
|
||||||
|
description: null,
|
||||||
|
url: null,
|
||||||
|
details: null
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set('attributes', assign({}, this.defaultAttr, this.attributes));
|
||||||
|
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
reset () {
|
||||||
|
this.setProperties(this.attributes);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
|
||||||
|
submit (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
if (!this.isValid) {
|
||||||
|
alert('Invalid data. Please review and try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = this.getProperties(Object.keys(this.attributes));
|
||||||
|
|
||||||
|
attributes.contributorId = parseInt(this.contributorId);
|
||||||
|
|
||||||
|
let dateInput = (attributes.date instanceof Array) ?
|
||||||
|
attributes.date[0] : attributes.date;
|
||||||
|
|
||||||
|
const [ date, time ] = dateInput.toISOString().split('T');
|
||||||
|
[ attributes.date, attributes.time ] = [ date, time ];
|
||||||
|
|
||||||
|
this.set('inProgress', true);
|
||||||
|
|
||||||
|
this.save(attributes)
|
||||||
|
.then((/*contribution*/) => {
|
||||||
|
this.reset();
|
||||||
|
}, err => {
|
||||||
|
console.warn(err);
|
||||||
|
window.alert('Something went wrong. Check the browser console for details.');
|
||||||
|
})
|
||||||
|
.finally(() => this.set('inProgress', false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<form onsubmit={{action "submit"}}>
|
||||||
|
<label>
|
||||||
|
<p class="label">Contributor:</p>
|
||||||
|
<p>
|
||||||
|
<select required onchange={{action (mut this.contributorId) value="target.value"}}>
|
||||||
|
<option value="" selected disabled hidden></option>
|
||||||
|
{{#each this.contributors as |contributor|}}
|
||||||
|
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Kind:</p>
|
||||||
|
<p>
|
||||||
|
<select required onchange={{action (mut this.kind) value="target.value"}}>
|
||||||
|
<option value="" selected disabled hidden></option>
|
||||||
|
<option value="bureaucracy" selected={{eq this.kind "bureaucracy"}}>Bureaucracy</option>
|
||||||
|
<option value="community" selected={{eq this.kind "community"}}>Community</option>
|
||||||
|
<option value="design" selected={{eq this.kind "design"}}>Design</option>
|
||||||
|
<option value="dev" selected={{eq this.kind "dev"}}>Development</option>
|
||||||
|
<option value="docs" selected={{eq this.kind "docs"}}>Documentation</option>
|
||||||
|
<option value="ops" selected={{eq this.kind "ops"}}>IT Operations</option>
|
||||||
|
<option value="outreach" selected={{eq this.kind "outreach"}}>Outreach</option>
|
||||||
|
<option value="qa" selected={{eq this.kind "qa"}}>Quality Assurance</option>
|
||||||
|
<option value="special" selected={{eq this.kind "special"}}>Special</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Date:</p>
|
||||||
|
<p>
|
||||||
|
{{ember-flatpickr
|
||||||
|
date=this.date
|
||||||
|
defaultDate=this.defaultDate
|
||||||
|
maxDate=this.defaultDate
|
||||||
|
enableTime=true
|
||||||
|
time_24hr=true
|
||||||
|
onChange=(action (mut this.date))
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Amount:</p>
|
||||||
|
<p>
|
||||||
|
{{input type="text"
|
||||||
|
placeholder="500"
|
||||||
|
value=this.amount
|
||||||
|
class=(if this.isValidAmount "valid" "")}}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Description:</p>
|
||||||
|
<p>
|
||||||
|
{{input type="text"
|
||||||
|
value=this.description
|
||||||
|
class=(if this.isValidDescription "valid" "")}}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">URL (optional):</p>
|
||||||
|
<p>
|
||||||
|
{{input type="text"
|
||||||
|
value=this.url
|
||||||
|
class=(if this.isValidUrl "valid" "")}}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{{#if this.details}}
|
||||||
|
<label>
|
||||||
|
<p class="label">Details:</p>
|
||||||
|
<p>
|
||||||
|
<pre>
|
||||||
|
{{this.details}}
|
||||||
|
</pre>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<p class="actions">
|
||||||
|
{{input type="submit"
|
||||||
|
disabled=this.inProgress
|
||||||
|
value=(if this.inProgress "Processing" "Save")}}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
@@ -1,79 +1,91 @@
|
|||||||
import Ember from 'ember';
|
import Component from '@ember/component';
|
||||||
import Contributor from 'kredits-web/models/contributor';
|
import { computed } from '@ember/object';
|
||||||
|
import { and, notEmpty } from '@ember/object/computed';
|
||||||
const {
|
import { inject as service } from '@ember/service';
|
||||||
Component,
|
import { isPresent } from '@ember/utils';
|
||||||
isPresent,
|
import { isAddress } from 'web3-utils';
|
||||||
inject: {
|
|
||||||
service
|
|
||||||
},
|
|
||||||
computed
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
|
||||||
|
router: service(),
|
||||||
kredits: service(),
|
kredits: service(),
|
||||||
|
|
||||||
newContributor: null,
|
attributes: null,
|
||||||
inProgress: false,
|
|
||||||
|
|
||||||
isValidAddress: function() {
|
isValidAccount: computed('account', function() {
|
||||||
return this.get('kredits.web3')
|
return isAddress(this.account);
|
||||||
.isAddress(this.get('newContributor.address'));
|
}),
|
||||||
}.property('kredits.web3', 'newContributor.address'),
|
|
||||||
|
|
||||||
isValidName: function() {
|
isValidName: notEmpty('name'),
|
||||||
return isPresent(this.get('newContributor.name'));
|
isValidURL: notEmpty('url'),
|
||||||
}.property('newContributor.name'),
|
isValidGithubUID: notEmpty('github_uid'),
|
||||||
|
isValidGithubUsername: notEmpty('github_username'),
|
||||||
|
isValidGiteaUsername: notEmpty('gitea_username'),
|
||||||
|
isValidWikiUsername: notEmpty('wiki_username'),
|
||||||
|
isValidZoomDisplayName: notEmpty('zoom_display_name'),
|
||||||
|
|
||||||
isValidURL: function() {
|
isValid: and(
|
||||||
return isPresent(this.get('newContributor.url'));
|
'isValidAccount',
|
||||||
}.property('newContributor.url'),
|
|
||||||
|
|
||||||
isValidGithubUID: function() {
|
|
||||||
return isPresent(this.get('newContributor.github_uid'));
|
|
||||||
}.property('newContributor.github_uid'),
|
|
||||||
|
|
||||||
isValidGithubUsername: function() {
|
|
||||||
return isPresent(this.get('newContributor.github_username'));
|
|
||||||
}.property('newContributor.github_username'),
|
|
||||||
|
|
||||||
isValidWikiUsername: function() {
|
|
||||||
return isPresent(this.get('newContributor.wiki_username'));
|
|
||||||
}.property('newContributor.wiki_username'),
|
|
||||||
|
|
||||||
isValid: computed.and(
|
|
||||||
'isValidAddress',
|
|
||||||
'isValidName',
|
'isValidName',
|
||||||
'isValidGithubUID'
|
'isValidGithubUID'
|
||||||
),
|
),
|
||||||
|
|
||||||
reset: function() {
|
inProgress: false,
|
||||||
this.setProperties({
|
|
||||||
newContributor: Contributor.create({ kind: 'person' }),
|
init () {
|
||||||
inProgress: false
|
this._super(...arguments);
|
||||||
|
this.setDefaultAttributes();
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
setDefaultAttributes () {
|
||||||
|
if (isPresent(this.attributes)) { return; }
|
||||||
|
|
||||||
|
this.set('attributes', {
|
||||||
|
account: null,
|
||||||
|
name: null,
|
||||||
|
kind: 'person',
|
||||||
|
url: null,
|
||||||
|
github_username: null,
|
||||||
|
github_uid: null,
|
||||||
|
gitea_username: null,
|
||||||
|
wiki_username: null,
|
||||||
|
zoom_display_name: null
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reset: function() {
|
||||||
|
this.setProperties(this.attributes);
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
save() {
|
submit (evt) {
|
||||||
if (!this.get('contractInteractionEnabled')) {
|
evt.preventDefault();
|
||||||
alert('Only core team members can add new contributors. Please ask someone to set you up.');
|
|
||||||
|
if (!this.kredits.currentUserIsCore) {
|
||||||
|
window.alert('Only core team members can edit profiles. Please ask someone to set you up.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.isValid) {
|
||||||
|
window.alert('Invalid data. Please review and try again.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.get('isValid')) {
|
const attributes = Object.keys(this.attributes);
|
||||||
|
const contributor = this.getProperties(attributes);
|
||||||
|
|
||||||
this.set('inProgress', true);
|
this.set('inProgress', true);
|
||||||
|
|
||||||
this.get('kredits').addContributor(this.get('newContributor')).then(contributor => {
|
this.save(contributor).then(() => {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.get('contributors').pushObject(contributor);
|
}).catch(err => {
|
||||||
window.scroll(0,0);
|
console.warn(err);
|
||||||
|
window.alert('Something went wrong. Please check the browser console.');
|
||||||
|
}).finally(() => {
|
||||||
|
this.set('inProgress', false);
|
||||||
|
this.router.transitionTo('dashboard');
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
alert('Invalid data. Please review and try again.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,56 @@
|
|||||||
<form {{action "save" on="submit"}}>
|
<form onsubmit={{action "submit"}}>
|
||||||
<p>
|
<p>
|
||||||
{{input type="checkbox" name="is-core" id="is-core" checked=newContributor.isCore}}
|
<label for="c-account">Rootstock account</label>
|
||||||
<label for="is-core" class="checkbox">
|
<Input @type="text"
|
||||||
Core team member (can add contributors)
|
@value={{this.account}}
|
||||||
</label>
|
name="account" id="c-account"
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{input name="address"
|
|
||||||
type="text"
|
|
||||||
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
||||||
value=newContributor.address
|
class={{if this.isValidAccount "valid" ""}} />
|
||||||
class=(if isValidAddress 'valid' '')}}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut newContributor.kind) value="target.value"}}>
|
<label for="c-kind">Kind</label>
|
||||||
<option value="person" selected={{eq newContributor.kind "person"}}>Person</option>
|
<select required onchange={{action (mut this.kind) value="target.value"}} id="c-kind">
|
||||||
<option value="organization" selected={{eq newContributor.kind "organization"}}>Organization</option>
|
<option value="person" selected={{eq this.kind "person"}}>Person</option>
|
||||||
|
<option value="organization" selected={{eq this.kind "organization"}}>Organization</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input name="name"
|
<label for="c-name">Name</label>
|
||||||
type="text"
|
{{input name="name" type="text" value=this.name placeholder="Zero Cool"
|
||||||
placeholder="Name"
|
class=(if this.isValidName "valid" "") id="c-name"}}
|
||||||
value=newContributor.name
|
|
||||||
class=(if isValidName 'valid' '')}}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input name="url"
|
<label for="c-url">URL</label>
|
||||||
type="text"
|
{{input name="url" type="text" value=this.url placeholder="http://zerocool.bit"
|
||||||
placeholder="URL"
|
class=(if this.isValidURL "valid" "") id="c-url"}}
|
||||||
value=newContributor.url
|
|
||||||
class=(if isValidURL 'valid' '')}}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input name="github_uid"
|
<label for="c-github-uid">GitHub UID</label>
|
||||||
type="text"
|
{{input name="github_uid" type="text" value=this.github_uid placeholder="2342"
|
||||||
placeholder="GitHub UID (123)"
|
class=(if this.isValidGithubUID "valid" "") id="c-github-uid"}}
|
||||||
value=newContributor.github_uid
|
|
||||||
class=(if isValidGithubUID 'valid' '')}}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input name="github_username"
|
<label for="c-github-username">GitHub username</label>
|
||||||
type="text"
|
{{input name="github_username" type="text" value=this.github_username placeholder="zerocool"
|
||||||
placeholder="GitHub username"
|
class=(if this.isValidGithubUsername "valid" "") id="c-github-username"}}
|
||||||
value=newContributor.github_username
|
|
||||||
class=(if isValidGithubUsername 'valid' '')}}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input name="wiki_username"
|
<label for="c-gitea-username">Gitea username</label>
|
||||||
type="text"
|
{{input name="gitea_username" type="text" value=this.gitea_username placeholder="zerocool"
|
||||||
placeholder="Wiki Username"
|
class=(if this.isValidGiteaUsername "valid" "") id="c-gitea-username"}}
|
||||||
value=newContributor.wiki_username
|
</p>
|
||||||
class=(if isValidWikiUsername 'valid' '')}}
|
<p>
|
||||||
|
<label for="c-wiki-username">Wiki username</label>
|
||||||
|
{{input name="wiki_username" type="text" value=this.wiki_username placeholder="ZeroCool"
|
||||||
|
class=(if this.isValidWikiUsername "valid" "") id="c-wiki-username"}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="c-zoom-display-name">Zoom display name</label>
|
||||||
|
{{input name="zoom_display_name" type="text" value=this.zoom_display_name placeholder="Zero Cool"
|
||||||
|
class=(if this.isValidZoomDisplayName "valid" "") id="c-zoom-display-name"}}
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
{{input type="submit" disabled=this.inProgress
|
||||||
|
value=(if this.inProgress "Processing" "Save")}}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import moment from 'moment';
|
||||||
|
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
||||||
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
|
export default class AddExpenseItemComponent extends Component {
|
||||||
|
@tracked amount = '0';
|
||||||
|
@tracked currency = 'EUR';
|
||||||
|
@tracked date = moment().startOf('hour').toDate();
|
||||||
|
@tracked title = '';
|
||||||
|
@tracked description = '';
|
||||||
|
@tracked url = '';
|
||||||
|
@tracked tags = '';
|
||||||
|
|
||||||
|
defaultDate = moment().startOf('hour').toDate();
|
||||||
|
|
||||||
|
currencies = [
|
||||||
|
{ code: 'EUR' },
|
||||||
|
{ code: 'USD' }
|
||||||
|
];
|
||||||
|
|
||||||
|
get isValidAmount () {
|
||||||
|
return isValidAmount(this.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
get amountInputClass () {
|
||||||
|
return this.isValidTotal ? 'valid' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
validateForm () {
|
||||||
|
const formEl = document.querySelector('form#add-expense-item');
|
||||||
|
const inputFields = formEl.querySelectorAll('input');
|
||||||
|
inputFields.forEach(i => i.classList.remove('invalid'));
|
||||||
|
let validity = true;
|
||||||
|
|
||||||
|
if (!this.isValidAmount) {
|
||||||
|
document.querySelector('input[name=expense-amount]').classList.add('invalid');
|
||||||
|
validity = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formEl.checkValidity()) {
|
||||||
|
inputFields.forEach(i => {
|
||||||
|
if (!i.validity.valid) {
|
||||||
|
i.classList.add('invalid');
|
||||||
|
validity = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return validity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateCurrency(event) {
|
||||||
|
this.currency = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
submit (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let dateInput = (this.date instanceof Array) ?
|
||||||
|
this.date[0] : this.date;
|
||||||
|
|
||||||
|
const [ date ] = moment(dateInput).utcOffset(0, true)
|
||||||
|
.toISOString()
|
||||||
|
.split('T');
|
||||||
|
|
||||||
|
const isValid = this.validateForm();
|
||||||
|
if (!isValid) return false;
|
||||||
|
|
||||||
|
const expense = {
|
||||||
|
amount: parseFloat(this.amount),
|
||||||
|
currency: this.currency,
|
||||||
|
date: date,
|
||||||
|
title: this.title,
|
||||||
|
description: isPresent(this.description) ? this.description : undefined,
|
||||||
|
url: isPresent(this.url) ? this.url : undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPresent(this.tags)) {
|
||||||
|
expense.tags = this.tags.split(',')
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(t => t.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.args.addExpenseItem(expense);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<form id="add-expense-item" {{on "submit" this.submit}} novalidate>
|
||||||
|
<fieldset class="horizontal">
|
||||||
|
<label>
|
||||||
|
<p class="label">Amount:</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-amount"
|
||||||
|
@type="text"
|
||||||
|
@placeholder="10"
|
||||||
|
@value={{this.amount}}
|
||||||
|
@required={{true}}
|
||||||
|
@pattern="([0-9]*[.])?[0-9]+"
|
||||||
|
@class={{this.amountInputClass}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Currency:</p>
|
||||||
|
<p>
|
||||||
|
<select required name="expense-currency" {{on "change" this.updateCurrency}}>
|
||||||
|
<option value="" selected disabled hidden></option>
|
||||||
|
{{#each this.currencies as |currency|}}
|
||||||
|
<option value={{currency.code}} selected={{eq this.currency currency.code}}>{{currency.code}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<label>
|
||||||
|
<p class="label">Date:</p>
|
||||||
|
<p>
|
||||||
|
<EmberFlatpickr @date={{this.date}}
|
||||||
|
@defaultDate={{this.defaultDate}}
|
||||||
|
@maxDate={{this.defaultDate}}
|
||||||
|
@enableTime={{false}}
|
||||||
|
@onChange={{fn (mut this.date)}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Title:</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-title"
|
||||||
|
@type="text"
|
||||||
|
@value={{this.title}}
|
||||||
|
@required={{true}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Description (optional):</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-description" @type="text" @value={{this.description}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">URL (optional):</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-url" @type="url" @value={{this.url}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Tags (comma-separated, optional):</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-tags" @type="text" @value={{this.tags}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p class="actions">
|
||||||
|
<Input @type="submit" @value="Add" @class="green"
|
||||||
|
@title="Add item to reimbursement" />
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
|
|
||||||
const {
|
|
||||||
Component,
|
|
||||||
isPresent,
|
|
||||||
inject: {
|
|
||||||
service
|
|
||||||
},
|
|
||||||
computed
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
proposal: null,
|
|
||||||
contributors: null,
|
|
||||||
inProgress: false,
|
|
||||||
|
|
||||||
isValidRecipient: computed('proposal.recipientAddress', function() {
|
|
||||||
return this.get('kredits.web3').isAddress(this.get('proposal.recipientAddress'));
|
|
||||||
}),
|
|
||||||
|
|
||||||
isValidAmount: computed('proposal.amount', function() {
|
|
||||||
return parseInt(this.get('proposal.amount'), 10) > 0;
|
|
||||||
}),
|
|
||||||
|
|
||||||
isValidUrl: computed('proposal.url', function() {
|
|
||||||
return isPresent(this.get('proposal.url'));
|
|
||||||
}),
|
|
||||||
|
|
||||||
isValidDescription: computed('proposal.description', function() {
|
|
||||||
return isPresent(this.get('proposal.description'));
|
|
||||||
}),
|
|
||||||
|
|
||||||
isValid: computed.and('isValidRecipient',
|
|
||||||
'isValidAmount',
|
|
||||||
'isValidDescription'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
save() {
|
|
||||||
if (! this.get('isValid')) {
|
|
||||||
alert('Invalid data. Please review and try again.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.set('inProgress', true);
|
|
||||||
let proposal = this.get('proposal');
|
|
||||||
|
|
||||||
// Set the recipient's IPFS profile hash so it can be used in the
|
|
||||||
// contribution object (which is to be stored in IPFS as well)
|
|
||||||
let contributor = this.get('contributors').findBy('address', proposal.get('recipientAddress'));
|
|
||||||
proposal.set('recipientProfile', contributor.get('ipfsHash'));
|
|
||||||
|
|
||||||
this.get('kredits').addProposal(proposal)
|
|
||||||
.then(() => {
|
|
||||||
this.attrs.onSave();
|
|
||||||
}).catch((error) => {
|
|
||||||
Ember.Logger.error('[add-proposal] error creating the proposal', error);
|
|
||||||
alert('Something went wrong.');
|
|
||||||
}).finally(() => {
|
|
||||||
this.set('inProgress', false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<form {{action "save" on="submit"}}>
|
|
||||||
<p>
|
|
||||||
<select required onchange={{action (mut proposal.recipientAddress) value="target.value"}}>
|
|
||||||
<option value="" selected disabled hidden>Contributor</option>
|
|
||||||
{{#each contributors as |contributor|}}
|
|
||||||
<option value={{contributor.address}} selected={{eq proposal.recipientAddress contributor.address}}>{{contributor.github_username}}</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<select required onchange={{action (mut proposal.kind) value="target.value"}}>
|
|
||||||
<option value="community" selected={{eq proposal.kind "community"}}>Community</option>
|
|
||||||
<option value="design" selected={{eq proposal.kind "design"}}>Design</option>
|
|
||||||
<option value="dev" selected={{eq proposal.kind "dev"}}>Development</option>
|
|
||||||
<option value="docs" selected={{eq proposal.kind "docs"}}>Documentation</option>
|
|
||||||
<option value="ops" selected={{eq proposal.kind "ops"}}>IT Operations</option>
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
placeholder="100"
|
|
||||||
value=proposal.amount
|
|
||||||
class=(if isValidAmount 'valid' '')}}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
placeholder="Description"
|
|
||||||
value=proposal.description
|
|
||||||
class=(if isValidDescription 'valid' '')}}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
placeholder="URL (optional)"
|
|
||||||
value=proposal.url
|
|
||||||
class=(if isValidUrl 'valid' '')}}
|
|
||||||
</p>
|
|
||||||
<p class="actions">
|
|
||||||
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
|
||||||
{{#link-to 'index'}}Back{{/link-to}}
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { A } from '@ember/array';
|
||||||
|
import { scheduleOnce } from '@ember/runloop';
|
||||||
|
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
||||||
|
import readFileContent from 'kredits-web/utils/read-file-content';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default class AddReimbursementComponent extends Component {
|
||||||
|
@service router;
|
||||||
|
@service kredits;
|
||||||
|
@service exchangeRates;
|
||||||
|
|
||||||
|
@alias('kredits.contributorsSorted') contributors;
|
||||||
|
|
||||||
|
@tracked recipientId = null;
|
||||||
|
@tracked title = '';
|
||||||
|
@tracked total = '0';
|
||||||
|
@tracked expenses = A([]);
|
||||||
|
@tracked expenseFormVisible = true;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.exchangeRates.fetchRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
get contributorId () {
|
||||||
|
return this.recipientId || this.kredits.currentUser?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isValidTotal () {
|
||||||
|
return isValidAmount(this.total);
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalInputClass () {
|
||||||
|
return this.isValidTotal ? 'valid' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalEUR () {
|
||||||
|
const expenses = this.expenses.filterBy('currency', 'EUR');
|
||||||
|
if (expenses.length > 0) {
|
||||||
|
return expenses.mapBy('amount')
|
||||||
|
.reduce((summation, current) => summation + current);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalUSD () {
|
||||||
|
const expenses = this.expenses.filterBy('currency', 'USD');
|
||||||
|
if (expenses.length > 0) {
|
||||||
|
return expenses.mapBy('amount')
|
||||||
|
.reduce((summation, current) => summation + current);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitButtonEnabled () {
|
||||||
|
return this.isValidTotal &&
|
||||||
|
(this.expenses.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitButtonDisabled () {
|
||||||
|
return !this.submitButtonEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToExpenseItemForm () {
|
||||||
|
const anchor = document.getElementById('new-expense-item');
|
||||||
|
anchor.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTotalAmountFromFiat() {
|
||||||
|
let btcAmount = 0;
|
||||||
|
|
||||||
|
if (this.exchangeRates.btceur > 0 && this.totalEUR > 0) {
|
||||||
|
btcAmount += (this.totalEUR / this.exchangeRates.btceur);
|
||||||
|
}
|
||||||
|
if (this.exchangeRates.btcusd > 0 && this.totalUSD > 0) {
|
||||||
|
btcAmount += (this.totalUSD / this.exchangeRates.btcusd);
|
||||||
|
}
|
||||||
|
if (this.totalUSD === 0 && this.totalEUR === 0) {
|
||||||
|
btcAmount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.total = btcAmount.toFixed(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use ember-concurrency here
|
||||||
|
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
|
||||||
|
@action
|
||||||
|
async addExpensesFromFile (evt) {
|
||||||
|
const content = await readFileContent(evt.target.files[0]);
|
||||||
|
const expenses = JSON.parse(content);
|
||||||
|
|
||||||
|
if (expenses instanceof Array) {
|
||||||
|
for (const item of expenses) {
|
||||||
|
this.addExpenseItem(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Expenses in file must be a list of items:");
|
||||||
|
console.debug(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateContributor (event) {
|
||||||
|
this.recipientId = parseInt(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
showExpenseForm () {
|
||||||
|
this.expenseFormVisible = true;
|
||||||
|
scheduleOnce('afterRender', this, this.scrollToExpenseItemForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addExpenseItem (expenseItem) {
|
||||||
|
this.expenses.pushObject(expenseItem);
|
||||||
|
this.updateTotalAmountFromFiat();
|
||||||
|
this.expenseFormVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeExpenseItem (expenseItem) {
|
||||||
|
this.expenses.removeObject(expenseItem);
|
||||||
|
this.updateTotalAmountFromFiat();
|
||||||
|
|
||||||
|
if (this.expenses.length === 0) {
|
||||||
|
this.expenseFormVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
submit (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.kredits.currentUser) { window.alert('You need to connect your RSK account first.'); return false }
|
||||||
|
if (!this.kredits.currentUserIsCore) { window.alert('Only core contributors can submit reimbursements.'); return false }
|
||||||
|
|
||||||
|
const contributor = this.contributors.findBy('id', this.contributorId);
|
||||||
|
|
||||||
|
const attributes = {
|
||||||
|
amount: parseInt(parseFloat(this.total) * 100000000), // convert to sats
|
||||||
|
token: config.tokens['BTC'],
|
||||||
|
recipientId: this.contributorId,
|
||||||
|
title: `Expenses covered by ${contributor.name}`,
|
||||||
|
description: this.description,
|
||||||
|
url: this.url,
|
||||||
|
expenses: JSON.parse(JSON.stringify((this.expenses)))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inProgress = true;
|
||||||
|
|
||||||
|
this.kredits.addReimbursement(attributes)
|
||||||
|
.then((/* reimbursement */) => {
|
||||||
|
this.router.transitionTo('budget');
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error('Could not add reimbursement:', e);
|
||||||
|
window.alert('Something went wrong. Please check the browser console.')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.inProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<form {{on "submit" this.submit}} novalidate>
|
||||||
|
<label>
|
||||||
|
<p class="label">Contributor:</p>
|
||||||
|
<p>
|
||||||
|
<select id="contributor" required {{on "change" this.updateContributor}}>
|
||||||
|
{{#each this.contributors as |contributor|}}
|
||||||
|
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<fieldset class="horizontal thirds total-amounts">
|
||||||
|
<label>
|
||||||
|
<p class="label">Total amount (BTC):</p>
|
||||||
|
<p>
|
||||||
|
<Input @type="text"
|
||||||
|
@name="total-btc"
|
||||||
|
@placeholder="0.0015"
|
||||||
|
@value={{this.total}}
|
||||||
|
@required={{true}}
|
||||||
|
@pattern="([0-9]*[.])?[0-9]+"
|
||||||
|
@class={{this.totalInputClass}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">EUR total</p>
|
||||||
|
<p>
|
||||||
|
<Input @type="text"
|
||||||
|
@name="total-eur"
|
||||||
|
@value={{this.totalEUR}}
|
||||||
|
@disabled={{true}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">USD total</p>
|
||||||
|
<p>
|
||||||
|
<Input @type="text"
|
||||||
|
@name="total-usd"
|
||||||
|
@value={{this.totalUSD}}
|
||||||
|
@disabled={{true}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<h3>Expense items</h3>
|
||||||
|
{{#if this.expenses}}
|
||||||
|
<ExpenseList @expenses={{this.expenses}}
|
||||||
|
@removeExpenseItem={{this.removeExpenseItem}}
|
||||||
|
@deletable={{true}} />
|
||||||
|
|
||||||
|
<p class="actions">
|
||||||
|
<button {{on "click" this.showExpenseForm}}
|
||||||
|
id="add-another-item" class="green small" type="button">
|
||||||
|
+ Add another item
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p>No line items yet.</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<p class="actions">
|
||||||
|
{{#if this.inProgress}}
|
||||||
|
<Input @type="submit" @value="Submitting..." @disabled={{true}}
|
||||||
|
@title="Submit/propose this reimbursement" />
|
||||||
|
{{else}}
|
||||||
|
<Input @type="submit" @value="Submit" @disabled={{this.submitButtonDisabled}}
|
||||||
|
@title="Submit/propose this reimbursement" />
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{#if this.expenseFormVisible}}
|
||||||
|
<h3 id="new-expense-item">New expense item</h3>
|
||||||
|
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
|
||||||
|
{{/if}}
|
||||||
|
</form>
|
||||||
|
<form id="add-expenses-from-file">
|
||||||
|
<h3>Add expense items from file</h3>
|
||||||
|
<input type="file" multiple="false"
|
||||||
|
onchange={{fn this.addExpensesFromFile}}
|
||||||
|
accept="application/json" />
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default class BudgetBalancesComponent extends Component {
|
||||||
|
@service communityFunds;
|
||||||
|
|
||||||
|
get balancesSorted () {
|
||||||
|
return this.communityFunds.balances
|
||||||
|
.sortBy('confirmed_balance').reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
get loading () {
|
||||||
|
return !this.communityFunds.balancesLoaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<table class="token-balances {{if this.loading 'loading'}}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Token</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Fiat value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each this.balancesSorted as |balance|}}
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<img src={{balance.token.icon}}
|
||||||
|
alt={{balance.token.description}}
|
||||||
|
title={{balance.token.description}} />
|
||||||
|
</th>
|
||||||
|
<td class="amount">
|
||||||
|
{{fmt-number balance.confirmed_balance}} <span class="unit">sats</span>
|
||||||
|
</td>
|
||||||
|
<td class="fiat-amount">
|
||||||
|
~{{balance.balanceUSD}} USD
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import Component from '@ember/component';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
|
||||||
|
let categoryColors = {
|
||||||
|
community: "#fb6868",
|
||||||
|
design: "#fbe468",
|
||||||
|
dev: "#e068fb",
|
||||||
|
docs: "#97fb68",
|
||||||
|
ops: "#8f68fb",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
|
||||||
|
contributions: null,
|
||||||
|
chartOptions: Object.freeze({
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
chartData: computed('contributions', function() {
|
||||||
|
let kredits = this.contributions
|
||||||
|
.filterBy('vetoed', false)
|
||||||
|
.map(c => {
|
||||||
|
return { kind: c.kind, amount: c.amount }
|
||||||
|
})
|
||||||
|
.reduce(function (kinds, c) {
|
||||||
|
if (c.kind in kinds) {
|
||||||
|
kinds[c.kind] = kinds[c.kind] + c.amount
|
||||||
|
} else {
|
||||||
|
kinds[c.kind] = c.amount;
|
||||||
|
}
|
||||||
|
return kinds;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
datasets: [{
|
||||||
|
data: [
|
||||||
|
kredits['community'],
|
||||||
|
kredits['design'],
|
||||||
|
kredits['dev'],
|
||||||
|
kredits['ops'],
|
||||||
|
kredits['docs'],
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
categoryColors.community,
|
||||||
|
categoryColors.design,
|
||||||
|
categoryColors.dev,
|
||||||
|
categoryColors.ops,
|
||||||
|
categoryColors.docs,
|
||||||
|
],
|
||||||
|
borderWidth: 1
|
||||||
|
}],
|
||||||
|
labels: [
|
||||||
|
'Community',
|
||||||
|
'Design',
|
||||||
|
'Development',
|
||||||
|
'Operations & Infrastructure',
|
||||||
|
'Documentation'
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="chart">
|
||||||
|
<EmberChart @type="doughnut"
|
||||||
|
@data={{this.chartData}}
|
||||||
|
@options={{this.chartOptions}}
|
||||||
|
@width="200" @height="200" />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default class ConfirmedInComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
get confirmedInBlocks () {
|
||||||
|
return this.args.confirmedAtBlock - this.kredits.currentBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
get confirmedInSeconds () {
|
||||||
|
// A new block is mined every 30 seconds on average
|
||||||
|
return this.confirmedInBlocks * 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
get confirmedInHumanTime () {
|
||||||
|
return moment.duration(this.confirmedInSeconds, "seconds").humanize();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConfirmed () {
|
||||||
|
return this.confirmedInBlocks <= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{{#if this.isConfirmed}}
|
||||||
|
Confirmed at block <strong>{{@confirmedAtBlock}}</strong> (~ {{this.confirmedInHumanTime}} ago)
|
||||||
|
{{else}}
|
||||||
|
Confirming in <strong>{{this.confirmedInBlocks}}</strong> blocks (~ {{this.confirmedInHumanTime}})
|
||||||
|
{{/if}}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import Component from '@ember/component';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
import { sort } from '@ember/object/computed';
|
||||||
|
import { isPresent } from '@ember/utils';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
|
||||||
|
router: service(),
|
||||||
|
|
||||||
|
tagName: 'div',
|
||||||
|
classNames: ['contributions'],
|
||||||
|
|
||||||
|
selectedContribution: null,
|
||||||
|
|
||||||
|
showQuickFilter: false,
|
||||||
|
hideSmallContributions: false,
|
||||||
|
contributorId: null,
|
||||||
|
contributionKind: null,
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
contributorsSorting: Object.freeze(['name:asc']),
|
||||||
|
contributors: sort('kredits.contributors', 'contributorsSorting'),
|
||||||
|
|
||||||
|
contributorsActive: computed('contributors.[]', 'contributions', function() {
|
||||||
|
const activeIds = new Set(this.contributions.mapBy('contributorId'));
|
||||||
|
|
||||||
|
return this.contributors.filter(c => activeIds.has(c.id));
|
||||||
|
}),
|
||||||
|
|
||||||
|
contributionKinds: computed('contributions.[]', function() {
|
||||||
|
return this.contributions.mapBy('kind').uniq();
|
||||||
|
}),
|
||||||
|
|
||||||
|
contributionsFiltered: computed('contributions.[]', 'hideSmallContributions', 'contributorId', 'contributionKind', function() {
|
||||||
|
return this.contributions.filter(c => {
|
||||||
|
let included = true;
|
||||||
|
|
||||||
|
if (this.hideSmallContributions &&
|
||||||
|
c.amount <= 500) { included = false; }
|
||||||
|
|
||||||
|
if (isPresent(this.contributorId) &&
|
||||||
|
c.contributorId !== parseInt(this.contributorId)) { included = false; }
|
||||||
|
|
||||||
|
if (isPresent(this.contributionKind) &&
|
||||||
|
c.kind !== this.contributionKind) { included = false; }
|
||||||
|
|
||||||
|
return included;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
|
||||||
|
veto (contributionId) {
|
||||||
|
if (this.contractInteractionEnabled) {
|
||||||
|
this.vetoContribution(contributionId);
|
||||||
|
} else {
|
||||||
|
window.alert('Only members can veto contributions. Please ask someone to set you up.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openContributionDetails(contribution) {
|
||||||
|
this.router.transitionTo('dashboard.contributions.show', contribution);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{{#if @showQuickFilter}}
|
||||||
|
<div class="quick-filter">
|
||||||
|
<p>
|
||||||
|
<label class="filter-contributor">
|
||||||
|
Contributor:
|
||||||
|
<select onchange={{action (mut this.contributorId) value="target.value"}}>
|
||||||
|
<option value="" selected>all</option>
|
||||||
|
{{#each this.contributorsActive as |contributor|}}
|
||||||
|
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="filter-contribution-kind">
|
||||||
|
Kind:
|
||||||
|
<select onchange={{action (mut this.contributionKind) value="target.value"}}>
|
||||||
|
<option value="" selected>all</option>
|
||||||
|
{{#each this.contributionKinds as |kind|}}
|
||||||
|
<option value={{kind}} selected={{eq this.contributionKind kind}}>{{capitalize-string kind}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="filter-contribution-size">
|
||||||
|
<Input @type="checkbox" @checked={{this.hideSmallContributions}} />
|
||||||
|
Hide small contributions
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<ul class="item-list contribution-list {{if @loading 'loading'}}">
|
||||||
|
{{#each this.contributionsFiltered as |contribution|}}
|
||||||
|
<li role="button" data-contribution-id={{contribution.id}}
|
||||||
|
{{action "openContributionDetails" contribution}}
|
||||||
|
class="{{item-status contribution}}{{if (eq contribution.id @selectedContributionId) " selected"}}">
|
||||||
|
<p class="meta">
|
||||||
|
<span class="recipient"><UserAvatar @contributor={{contribution.contributor}} /></span>
|
||||||
|
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
|
||||||
|
<span class="title">{{contribution.description}}</span>
|
||||||
|
</p>
|
||||||
|
<p class="kredits-amount">
|
||||||
|
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
|
||||||
|
</p>
|
||||||
|
{{#unless contribution.vetoed}}
|
||||||
|
{{#unless (is-confirmed-contribution contribution)}}
|
||||||
|
<p class="voting">
|
||||||
|
{{input type="button" class="button small danger" value="veto"
|
||||||
|
click=(action "veto" contribution.id)
|
||||||
|
disabled=contribution.hasPendingChanges}}
|
||||||
|
</p>
|
||||||
|
{{/unless}}
|
||||||
|
{{/unless}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
@@ -1,22 +1,18 @@
|
|||||||
import Ember from 'ember';
|
import Component from '@ember/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
|
||||||
tagName: 'table',
|
router: service(),
|
||||||
classNames: 'contributor-list',
|
|
||||||
selectedContributor: null,
|
selectedContributorId: null,
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
toggleContributorInfo(contributor) {
|
openContributorDetails(contributor) {
|
||||||
if (contributor.get('showMetadata')) {
|
this.router.transitionTo('dashboard.contributors.show', contributor);
|
||||||
contributor.set('showMetadata', false);
|
|
||||||
} else {
|
|
||||||
this.get('contributors').setEach('showMetadata', false);
|
|
||||||
contributor.set('showMetadata', true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
<tbody>
|
<table class="contributor-list {{if @loading 'loading'}}">
|
||||||
{{#each contributors as |contributor|}}
|
<thead>
|
||||||
<tr class="{{if contributor.isCurrentUser 'current-user'}}" {{action "toggleContributorInfo" contributor}}>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each @contributorList as |c|}}
|
||||||
|
<tr role="button"
|
||||||
|
onclick={{action "openContributorDetails" c.contributor}}
|
||||||
|
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
|
||||||
<td class="person">
|
<td class="person">
|
||||||
<img class="avatar" src={{contributor.avatarURL}}>
|
<UserAvatar @contributor={{c.contributor}} /> {{c.contributor.name}}
|
||||||
{{contributor.name}}
|
|
||||||
</td>
|
</td>
|
||||||
<td class="kredits">
|
<td class="kredits">
|
||||||
<span class="amount">{{contributor.kredits}}</span>
|
<span class="amount">
|
||||||
|
{{#if @showUnconfirmedKredits}}
|
||||||
|
{{c.amountTotal}}
|
||||||
|
{{else}}
|
||||||
|
{{c.amountConfirmed}}
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
<span class="symbol">₭S</span>
|
<span class="symbol">₭S</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="metadata {{if contributor.isCurrentUser 'current-user'}} {{if contributor.showMetadata 'visible'}}">
|
|
||||||
<td colspan="2">
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://testnet.etherscan.io/address/{{contributor.address}}">Inspect Ethereum transactions</a></li>
|
|
||||||
{{#if contributor.ipfsHash}}
|
|
||||||
<li><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">Inspect IPFS profile</a></li>
|
|
||||||
{{/if}}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
|
||||||
|
export default class ExpenseListComponent extends Component {
|
||||||
|
|
||||||
|
get showDeleteButton () {
|
||||||
|
return !!this.args.deletable;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<ul class="expense-list">
|
||||||
|
{{#each @expenses as |expense|}}
|
||||||
|
<li class="expense-item">
|
||||||
|
<h4>
|
||||||
|
<span class="date">{{fmt-date-localized expense.date}}:</span>
|
||||||
|
<span class="title">{{expense.title}}</span>
|
||||||
|
</h4>
|
||||||
|
<div class="amount">
|
||||||
|
{{fmt-fiat-currency expense.amount expense.currency}}
|
||||||
|
</div>
|
||||||
|
<p class="description">
|
||||||
|
{{expense.description}}
|
||||||
|
</p>
|
||||||
|
<p class="tags">
|
||||||
|
{{#each expense.tags as |tag|}}
|
||||||
|
<button type="button" class="small yellow" role="none">
|
||||||
|
<IconTag />{{tag}}
|
||||||
|
</button>
|
||||||
|
{{/each}}
|
||||||
|
</p>
|
||||||
|
{{#if this.showDeleteButton}}
|
||||||
|
<div class="actions">
|
||||||
|
<button {{on "click" (fn @removeExpenseItem expense)}}
|
||||||
|
class="danger small" type="button">delete</button>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Component from '@ember/component';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: "",
|
||||||
|
account: null,
|
||||||
|
|
||||||
|
iconComponentName: computed('account.site', function() {
|
||||||
|
if (this.account.site.match(/github|gitea|wiki|zoom/)) {
|
||||||
|
return 'icon-account-' + this.account.site.replace(/\./g, '-');
|
||||||
|
} else {
|
||||||
|
return 'icon-web-globe';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<a href={{this.account.url}} target="_blank" rel="noopener noreferrer"
|
||||||
|
title="{{this.account.username}} on {{this.account.site}}">
|
||||||
|
<span class="site">{{this.account.site}}</span>
|
||||||
|
{{component this.iconComponentName}}
|
||||||
|
</a>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Ember from 'ember';
|
import Component from '@ember/component';
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Component.extend({
|
||||||
|
|
||||||
classNames: ['loading-spinner']
|
classNames: ['loading-spinner']
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,5 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Loading data from Ethereum...
|
Loading data...
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<nav id="main-menu">
|
||||||
|
<ul>
|
||||||
|
<li><LinkTo @route="dashboard">Dashboard</LinkTo></li>
|
||||||
|
<li><LinkTo @route="budget">Budget</LinkTo></li>
|
||||||
|
<li><LinkTo @route="about">About</LinkTo></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
|
||||||
|
|
||||||
tagName: 'ul',
|
|
||||||
classNames: ['proposal-list'],
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
confirm(proposalId) {
|
|
||||||
if (this.get('contractInteractionEnabled')) {
|
|
||||||
this.sendAction('confirmAction', proposalId);
|
|
||||||
} else {
|
|
||||||
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{{#each proposals as |proposal|}}
|
|
||||||
<li data-proposal-id={{proposal.id}} title="({{proposal.kind}}) {{proposal.description}}">
|
|
||||||
<span class="category {{proposal.kind}}">♥</span>
|
|
||||||
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
|
|
||||||
for <span class="recipient">{{proposal.recipientName}}</span>
|
|
||||||
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
|
|
||||||
|
|
||||||
{{#unless proposal.executed}}<button {{action "confirm" proposal.id}}>+1</button>{{/unless}}
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
import fmtDateLocalized from 'kredits-web/helpers/fmt-date-localized';
|
||||||
|
|
||||||
|
export default class ReimbursementItemComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
@tracked showExpenseDetails = false;
|
||||||
|
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
if (this.isUnconfirmed && !this.isVetoed) {
|
||||||
|
this.showExpenseDetails = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get ipfsGatewayUrl () {
|
||||||
|
return config.ipfs.gatewayUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConfirmed () {
|
||||||
|
return (this.args.reimbursement.confirmedAt - this.kredits.currentBlock) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUnconfirmed () {
|
||||||
|
return !this.isConfirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isVetoed () {
|
||||||
|
return this.args.reimbursement.vetoed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showVetoButton () {
|
||||||
|
return this.isUnconfirmed && this.kredits.currentUserIsCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showConfirmedIn () {
|
||||||
|
return !this.isVetoed &&
|
||||||
|
(this.showExpenseDetails || this.isUnconfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
get expenses () {
|
||||||
|
return this.args.reimbursement.expenses;
|
||||||
|
}
|
||||||
|
|
||||||
|
get expensesDateRange () {
|
||||||
|
const dates = this.expenses.map(e => e.date).uniq().sort();
|
||||||
|
let out = fmtDateLocalized.compute(dates.firstObject)
|
||||||
|
if (dates.length > 1) {
|
||||||
|
out += ' - ' + fmtDateLocalized.compute(dates.lastObject)
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleExpenseDetails () {
|
||||||
|
this.showExpenseDetails = !this.showExpenseDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
veto (id) {
|
||||||
|
this.kredits.vetoReimbursement(id).then(transaction => {
|
||||||
|
console.debug('[controllers:budget] Veto submitted to chain: '+transaction.hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<li data-reimbursement-id={{@reimbursement.id}}
|
||||||
|
class="{{item-status @reimbursement}}"
|
||||||
|
role="button" {{on "click" this.toggleExpenseDetails}}>
|
||||||
|
<p class="meta">
|
||||||
|
<span class="recipient">
|
||||||
|
<UserAvatar @contributor={{@reimbursement.contributor}} />
|
||||||
|
</span>
|
||||||
|
<span class="title">
|
||||||
|
Expenses covered by {{@reimbursement.contributor.name}}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="token-amount">
|
||||||
|
<span class="amount">
|
||||||
|
{{sats-to-btc @reimbursement.amount}}</span> <span class="symbol">BTC</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{#if this.showExpenseDetails}}
|
||||||
|
<ExpenseList @expenses={{@reimbursement.expenses}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<p>
|
||||||
|
{{#if this.showConfirmedIn}}
|
||||||
|
<ConfirmedIn @confirmedAtBlock={{@reimbursement.confirmedAt}} />
|
||||||
|
{{else}}
|
||||||
|
{{#unless this.isVetoed}}{{this.expensesDateRange}}{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
<p class="actions">
|
||||||
|
<a href="{{this.ipfsGatewayUrl}}/{{@reimbursement.ipfsHash}}"
|
||||||
|
class="button small" target="_blank" rel="noopener noreferrer">
|
||||||
|
Inspect IPFS data
|
||||||
|
</a>
|
||||||
|
{{#if this.showVetoButton}}
|
||||||
|
<button {{on "click" (fn this.veto @reimbursement.id)}}
|
||||||
|
disabled={{@reimbursement.vetoed}}
|
||||||
|
class="button small danger" type="button">veto</button>
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { sort } from '@ember/object/computed';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default class ReimbursementListComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
||||||
|
@sort('args.items', 'itemSorting') itemsSorted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<ul class="item-list collapsible spaced reimbursement-list {{if @loading 'loading'}}">
|
||||||
|
{{#each this.itemsSorted as |item|}}
|
||||||
|
<ReimbursementItem @reimbursement={{item}} />
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
|
export default class TopbarAccountPanelComponent extends Component {
|
||||||
|
@service router;
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
@tracked setupInProgress = false;
|
||||||
|
|
||||||
|
get userHasWallet () {
|
||||||
|
return isPresent(window.ethereum);
|
||||||
|
}
|
||||||
|
|
||||||
|
get walletConnected () {
|
||||||
|
return this.userHasWallet && this.kredits.hasAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
get walletDisconnected () {
|
||||||
|
return this.userHasWallet && !this.kredits.hasAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
signup () {
|
||||||
|
this.router.transitionTo('signup');
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async connectWallet () {
|
||||||
|
this.setupInProgress = true;
|
||||||
|
await this.kredits.connectWallet();
|
||||||
|
this.setupInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<section id="user-account">
|
||||||
|
{{#if this.setupInProgress}}
|
||||||
|
Connecting account...
|
||||||
|
{{else}}
|
||||||
|
{{#if (and this.walletConnected this.kredits.currentUser)}}
|
||||||
|
{{this.kredits.currentUser.name}}
|
||||||
|
{{else}}
|
||||||
|
<span class="anonymous">Anonymous</span>
|
||||||
|
<button onclick={{action "signup"}} id="signup"
|
||||||
|
class="small" type="button">Sign up</button>
|
||||||
|
{{#if this.walletDisconnected}}
|
||||||
|
<button onclick={{action "connectWallet"}} id="connect"
|
||||||
|
class="small green" type="button">Connect wallet</button>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import Component from '@ember/component';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
|
||||||
|
const SIZES = {
|
||||||
|
'small': '128', // pixels
|
||||||
|
'medium': '256',
|
||||||
|
'large': '512'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
contributor: null,
|
||||||
|
tagName: 'img',
|
||||||
|
classNames: ['avatar'],
|
||||||
|
classNameBindings: ['size'],
|
||||||
|
attributeBindings: ['src', 'title'],
|
||||||
|
size: 'small',
|
||||||
|
|
||||||
|
src: alias('avatarURL'),
|
||||||
|
title: alias('contributor.name'),
|
||||||
|
|
||||||
|
avatarURL: computed('contributor.github_uid', 'size', function() {
|
||||||
|
const github_uid = this.contributor.github_uid;
|
||||||
|
|
||||||
|
if (github_uid) {
|
||||||
|
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=${SIZES[this.size]}`;
|
||||||
|
} else {
|
||||||
|
// TODO use custom avatar
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import Ember from 'ember';
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default class ApplicationController extends Controller {
|
||||||
});
|
@service kredits;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
|
export default class BudgetController extends Controller {
|
||||||
|
@service kredits;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
@alias('kredits.reimbursementsUnconfirmed') reimbursementsUnconfirmed;
|
||||||
|
@alias('kredits.reimbursementsConfirmed') reimbursementsConfirmed;
|
||||||
|
@alias('kredits.currentUserIsCore') currentUserIsCore;
|
||||||
|
|
||||||
|
@action
|
||||||
|
addReimbursement () {
|
||||||
|
if (!this.kredits.currentUser) {
|
||||||
|
window.alert('You need to connect your RSK account first.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.kredits.currentUserIsCore) {
|
||||||
|
window.alert('Only core contributors can submit reimbursements.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.router.transitionTo('reimbursements.new');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { alias, filterBy, sort } from '@ember/object/computed';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
queryParams: ['contributorId', 'kind', 'amount'],
|
||||||
|
|
||||||
|
contributors: alias('kredits.contributors'),
|
||||||
|
minedContributors: filterBy('contributors', 'id'),
|
||||||
|
|
||||||
|
contributorsSorting: Object.freeze(['name:asc']),
|
||||||
|
sortedContributors: sort('minedContributors', 'contributorsSorting'),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
|
||||||
|
save (contribution) {
|
||||||
|
const contributor = this.contributors.findBy('id', contribution.contributorId);
|
||||||
|
contribution.contributorIpfsHash = contributor.ipfsHash;
|
||||||
|
|
||||||
|
return this.kredits.addContribution(contribution)
|
||||||
|
.then(contribution => {
|
||||||
|
this.transitionToRoute('index');
|
||||||
|
return contribution;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import ContributionsNewController from 'kredits-web/controllers/contributions/new';
|
||||||
|
|
||||||
|
export default ContributionsNewController.extend({
|
||||||
|
|
||||||
|
attributes: null,
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
|
||||||
|
save (attributes) {
|
||||||
|
return this.kredits.updateContributor(this.model.id, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
|
||||||
|
save (contributor) {
|
||||||
|
return this.kredits.addContributor(contributor);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
import { alias, gt, not, sort } from '@ember/object/computed';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
showDetailsPane: false,
|
||||||
|
selectedContributorId: null,
|
||||||
|
selectedContributionId: null,
|
||||||
|
|
||||||
|
currentBlock: alias('kredits.currentBlock'),
|
||||||
|
|
||||||
|
contributions: alias('kredits.contributions'),
|
||||||
|
contributionsConfirmed: alias('kredits.contributionsConfirmed'),
|
||||||
|
contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'),
|
||||||
|
|
||||||
|
contributionsSorting: Object.freeze(['date:desc', 'time:desc', 'id:desc']),
|
||||||
|
contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'),
|
||||||
|
contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'),
|
||||||
|
|
||||||
|
kreditsByContributor: alias('kredits.kreditsByContributor'),
|
||||||
|
|
||||||
|
kreditsToplistSorting: computed('showUnconfirmedKredits', function(){
|
||||||
|
return this.showUnconfirmedKredits ? ['amountTotal:desc'] : ['amountConfirmed:desc'];
|
||||||
|
}),
|
||||||
|
kreditsToplist: sort('kreditsByContributor', 'kreditsToplistSorting'),
|
||||||
|
|
||||||
|
showUnconfirmedKredits: true,
|
||||||
|
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
|
||||||
|
|
||||||
|
showQuickFilterUnconfirmed: false,
|
||||||
|
showQuickFilterConfirmed: false,
|
||||||
|
|
||||||
|
showFullContributionSync: gt('kredits.missingHistoricContributionsCount', 0),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
|
||||||
|
vetoContribution (contributionId) {
|
||||||
|
this.kredits.veto(contributionId).then(transaction => {
|
||||||
|
console.debug('[controllers:index] Veto submitted to chain: '+transaction.hash);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleQuickFilterUnconfirmed () {
|
||||||
|
this.toggleProperty('showQuickFilterUnconfirmed');
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleQuickFilterConfirmed () {
|
||||||
|
this.toggleProperty('showQuickFilterConfirmed');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
|
||||||
|
ipfsGatewayUrl: computed(function() {
|
||||||
|
return config.ipfs.gatewayUrl;
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
|
||||||
|
roleName: computed('model.isCore', 'totalKreditsEarned', function() {
|
||||||
|
if (this.model.isCore) return 'Core Contributor';
|
||||||
|
if (this.model.totalKreditsEarned <= 5000) return 'Newcomer';
|
||||||
|
return 'Contributor';
|
||||||
|
}),
|
||||||
|
|
||||||
|
ipfsGatewayUrl: computed(function() {
|
||||||
|
return config.ipfs.gatewayUrl;
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
import Proposal from 'kredits-web/models/proposal';
|
|
||||||
|
|
||||||
const {
|
|
||||||
computed,
|
|
||||||
inject: {
|
|
||||||
service
|
|
||||||
}
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
contractInteractionEnabled: computed.alias('kredits.web3Provided'),
|
|
||||||
|
|
||||||
findContributorByAddress(address) {
|
|
||||||
return this.get('model.contributors')
|
|
||||||
.findBy('address', address);
|
|
||||||
},
|
|
||||||
|
|
||||||
proposalsOpen: function() {
|
|
||||||
let proposals = this.get('model.proposals')
|
|
||||||
.filterBy('executed', false)
|
|
||||||
.map(p => {
|
|
||||||
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
return proposals;
|
|
||||||
}.property('model.proposals.[]', 'model.proposals.@each.executed', 'model.contributors.[]'),
|
|
||||||
|
|
||||||
proposalsClosed: function() {
|
|
||||||
let proposals = this.get('model.proposals')
|
|
||||||
.filterBy('executed', true)
|
|
||||||
.map(p => {
|
|
||||||
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
return proposals;
|
|
||||||
}.property('model.proposals.[]', 'model.proposals.@each.executed', 'model.contributors.[]'),
|
|
||||||
|
|
||||||
proposalsSorting: ['id:desc'],
|
|
||||||
proposalsClosedSorted: Ember.computed.sort('proposalsClosed', 'proposalsSorting'),
|
|
||||||
proposalsOpenSorted: Ember.computed.sort('proposalsOpen', 'proposalsSorting'),
|
|
||||||
|
|
||||||
contributorsWithKredits: function() {
|
|
||||||
return this.get('model.contributors').filter(c => {
|
|
||||||
return c.get('kredits') !== 0;
|
|
||||||
});
|
|
||||||
}.property('model.contributors.@each.kredits'),
|
|
||||||
|
|
||||||
contributorsSorting: ['kredits:desc'],
|
|
||||||
contributorsSorted: Ember.computed.sort('contributorsWithKredits', 'contributorsSorting'),
|
|
||||||
|
|
||||||
watchContractEvents: function() {
|
|
||||||
let events = this.get('kredits.kreditsContract')
|
|
||||||
.allEvents(/* [additionalFilterObject], */);
|
|
||||||
|
|
||||||
events.watch((error, data) => {
|
|
||||||
Ember.Logger.debug('[index] Received contract event', data);
|
|
||||||
|
|
||||||
switch (data.event) {
|
|
||||||
case 'ProposalCreated':
|
|
||||||
this._handleProposalCreated(data);
|
|
||||||
break;
|
|
||||||
case 'ProposalExecuted':
|
|
||||||
this._handleProposalExecuted(data);
|
|
||||||
break;
|
|
||||||
case 'ProposalVoted':
|
|
||||||
this._handleProposalVoted(data);
|
|
||||||
break;
|
|
||||||
case 'Transfer':
|
|
||||||
this._handleTransfer(data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}.on('init'),
|
|
||||||
|
|
||||||
_handleProposalCreated(data) {
|
|
||||||
if (Ember.isPresent(this.get('model.proposals')
|
|
||||||
.findBy('id', data.args.id.toNumber()))) {
|
|
||||||
Ember.Logger.debug('[index] proposal exists, not adding from event');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let proposal = Proposal.create({
|
|
||||||
id: data.args.id.toNumber(),
|
|
||||||
creatorAddress: data.args.creator,
|
|
||||||
recipientAddress: data.args.recipient,
|
|
||||||
recipientName: null,
|
|
||||||
votesCount: 0,
|
|
||||||
votesNeeded: 2,
|
|
||||||
amount: data.args.amount.toNumber(),
|
|
||||||
executed: false,
|
|
||||||
url: data.args.url,
|
|
||||||
ipfsHash: data.args.ipfsHash
|
|
||||||
});
|
|
||||||
|
|
||||||
this.get('model.proposals').pushObject(proposal);
|
|
||||||
},
|
|
||||||
|
|
||||||
_handleProposalExecuted(data) {
|
|
||||||
if (this.get('model.proposals')
|
|
||||||
.findBy('id', data.args.id.toNumber())
|
|
||||||
.get('executed')) {
|
|
||||||
Ember.Logger.debug('[index] proposal already executed, not adding from event');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.get('model.proposals')
|
|
||||||
.findBy('id', data.args.id.toNumber())
|
|
||||||
.setProperties({
|
|
||||||
'executed': true,
|
|
||||||
'votesCount': 2 // TODO use real count
|
|
||||||
});
|
|
||||||
|
|
||||||
this.get('model.contributors')
|
|
||||||
.findBy('address', data.args.recipient)
|
|
||||||
.incrementProperty('kredits', data.args.amount.toNumber());
|
|
||||||
},
|
|
||||||
|
|
||||||
_handleProposalVoted(data) {
|
|
||||||
this.get('model.proposals')
|
|
||||||
.findBy('id', data.args.id.toNumber())
|
|
||||||
.incrementProperty('votesCount', 1);
|
|
||||||
},
|
|
||||||
|
|
||||||
_handleTransfer(data) {
|
|
||||||
this.get('model.contributors')
|
|
||||||
.findBy('address', data.args.from)
|
|
||||||
.incrementProperty('kredits', - data.args.value.toNumber());
|
|
||||||
this.get('model.contributors')
|
|
||||||
.findBy('address', data.args.to)
|
|
||||||
.incrementProperty('kredits', data.args.value.toNumber());
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
confirmProposal(proposalId) {
|
|
||||||
this.get('kredits').vote(proposalId).then(transactionId => {
|
|
||||||
window.confirm('Vote submitted to Ethereum blockhain: '+transactionId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
import QueryParams from 'ember-parachute';
|
|
||||||
|
|
||||||
export const queryParams = new QueryParams({
|
|
||||||
recipient: {
|
|
||||||
defaultValue: ''
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
defaultValue: ''
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
defaultValue: ''
|
|
||||||
},
|
|
||||||
ipfsHash: {
|
|
||||||
defaultValue: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Ember.Controller.extend(queryParams.Mixin, {
|
|
||||||
|
|
||||||
contributors: null,
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
onSave() {
|
|
||||||
this.transitionToRoute('index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
import { isAddress } from 'web3-utils';
|
||||||
|
|
||||||
|
export default class AccountController extends Controller {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
@tracked accountAddress = null;
|
||||||
|
|
||||||
|
get isValidEthAccount () {
|
||||||
|
return isAddress(this.accountAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
get signupButtonDisabled () {
|
||||||
|
return !this.isValidEthAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
completeSignup () {
|
||||||
|
const payload = {
|
||||||
|
accessToken: this.kredits.githubAccessToken,
|
||||||
|
account: this.accountAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(config.githubSignupUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
alert('Creating profile failed. We have been notified about this error and will take a look soon. Sorry!');
|
||||||
|
console.warn('Creating contributor profile failed:',
|
||||||
|
JSON.parse(data.error.body).error.message)
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.log('[signup/account] Created contributor:', data);
|
||||||
|
|
||||||
|
this.kredits.githubAccessToken = null;
|
||||||
|
this.accountAddress = null;
|
||||||
|
|
||||||
|
this.transitionToRoute('signup.complete');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default class IndexController extends Controller {
|
||||||
|
|
||||||
|
@action
|
||||||
|
connectGithub () {
|
||||||
|
window.location = config.githubConnectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Helper from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default Helper.extend({
|
||||||
|
compute([string]) {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function fmtCryptoCurrency(params/*, hash*/) {
|
||||||
|
let fmtAmount;
|
||||||
|
const amount = params[0];
|
||||||
|
const code = params[1];
|
||||||
|
|
||||||
|
switch(code) {
|
||||||
|
case 'RBTC':
|
||||||
|
fmtAmount = amount / 1000000000000000000;
|
||||||
|
break;
|
||||||
|
case 'BTC':
|
||||||
|
fmtAmount = amount / 100000000;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmtAmount;
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
import getLocale from 'kredits-web/utils/get-locale';
|
||||||
|
|
||||||
|
export default helper(function fmtDateLocalized(dateStr) {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const locale = getLocale();
|
||||||
|
return new Intl.DateTimeFormat(locale).format(date);
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function fmtFiatCurrency(params) {
|
||||||
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
|
const formatter = new Intl.NumberFormat(lang, {
|
||||||
|
style: 'currency',
|
||||||
|
currency: params[1] || 'EUR',
|
||||||
|
currencyDisplay: 'code'
|
||||||
|
})
|
||||||
|
return formatter.format(params[0]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function fmtNumber(number) {
|
||||||
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
|
return number.toLocaleString(lang);
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Helper from '@ember/component/helper';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
|
||||||
|
export default Helper.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
currentBlock: alias('kredits.currentBlock'),
|
||||||
|
|
||||||
|
compute([contribution]) {
|
||||||
|
return !contribution.vetoed &&
|
||||||
|
(contribution.confirmedAt <= this.currentBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Helper from '@ember/component/helper';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
|
export default Helper.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
currentUser: alias('kredits.currentUser'),
|
||||||
|
|
||||||
|
compute([contributor]) {
|
||||||
|
return isPresent(contributor) && isPresent(this.currentUser) &&
|
||||||
|
contributor.account === this.currentUser.account;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Helper from '@ember/component/helper';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import { once } from '@ember/runloop';
|
||||||
|
|
||||||
|
export default Helper.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
currentBlock: alias('kredits.currentBlock'),
|
||||||
|
|
||||||
|
compute([item]) {
|
||||||
|
this.setupRecompute(item);
|
||||||
|
|
||||||
|
let status = [];
|
||||||
|
|
||||||
|
if (item.vetoed) {
|
||||||
|
status.push('vetoed');
|
||||||
|
} else if (item.confirmedAt > this.currentBlock) {
|
||||||
|
status.push('unconfirmed');
|
||||||
|
} else {
|
||||||
|
status.push('confirmed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.hasPendingChanges) {
|
||||||
|
status.push('pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.join(' ');
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
if (this.teardown) this.teardown();
|
||||||
|
this._super(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
setupRecompute (item) {
|
||||||
|
if (this.teardown) this.teardown();
|
||||||
|
|
||||||
|
item.addObserver('vetoed' , this, this.triggerRecompute);
|
||||||
|
item.addObserver('confirmedAt' , this, this.triggerRecompute);
|
||||||
|
item.addObserver('currentBlock' , this, this.triggerRecompute);
|
||||||
|
item.addObserver('hasPendingChanges' , this, this.triggerRecompute);
|
||||||
|
|
||||||
|
this.teardown = () => {
|
||||||
|
item.removeObserver('vetoed', this, this.triggerRecompute);
|
||||||
|
item.removeObserver('confirmedAt', this, this.triggerRecompute);
|
||||||
|
item.removeObserver('currentBlock', this, this.triggerRecompute);
|
||||||
|
item.removeObserver('hasPendingChanges', this, this.triggerRecompute);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
triggerRecompute () {
|
||||||
|
once(this, this.recompute);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function satsToBtc(amount/*, hash*/) {
|
||||||
|
return amount / 100000000;
|
||||||
|
});
|
||||||
+2
-2
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
{{content-for "head"}}
|
{{content-for "head"}}
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
||||||
<link rel="stylesheet" href="{{rootURL}}assets/kredits-web.css">
|
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/kredits-web.css">
|
||||||
|
|
||||||
{{content-for "head-footer"}}
|
{{content-for "head-footer"}}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* This pauses the app's boot process until the window load event is fired.
|
|
||||||
* It allows the user provided Web3 instance (e.g. Metamask, Mist Browser)
|
|
||||||
* to be inserted before we try to use it.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
name: 'defer-loading',
|
|
||||||
initialize: function(application) {
|
|
||||||
// Load event already fired, so web3 should be loaded if available
|
|
||||||
if (window.windowLoadComplete) { return true; }
|
|
||||||
|
|
||||||
// Pause app loading
|
|
||||||
application.deferReadiness();
|
|
||||||
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
// Continue app loading
|
|
||||||
application.advanceReadiness();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import EmberObject, { computed } from '@ember/object';
|
||||||
|
import { isEmpty, isPresent } from '@ember/utils';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default EmberObject.extend({
|
||||||
|
// Contract
|
||||||
|
id: null,
|
||||||
|
contributorId: null,
|
||||||
|
amount: null,
|
||||||
|
confirmedAt: null,
|
||||||
|
vetoed: null,
|
||||||
|
ipfsHash: null,
|
||||||
|
|
||||||
|
// contributor model instance
|
||||||
|
contributor: null,
|
||||||
|
|
||||||
|
// TODO contributor who submitted the contribution
|
||||||
|
// submittedBy: null,
|
||||||
|
|
||||||
|
// IPFS
|
||||||
|
kind: null,
|
||||||
|
description: null,
|
||||||
|
details: null,
|
||||||
|
url: null,
|
||||||
|
date: null,
|
||||||
|
time: null,
|
||||||
|
|
||||||
|
pendingTx: null,
|
||||||
|
|
||||||
|
init () {
|
||||||
|
this._super(...arguments);
|
||||||
|
if (isEmpty(this.details)) this.set('details', {});
|
||||||
|
},
|
||||||
|
|
||||||
|
iso8601Date: computed('date', 'time', function() {
|
||||||
|
return this.time ? `${this.date}T${this.time}` : this.date;
|
||||||
|
}),
|
||||||
|
|
||||||
|
jsDate: computed('iso8601Date', function() {
|
||||||
|
return moment(this.iso8601Date).toDate();
|
||||||
|
}),
|
||||||
|
|
||||||
|
hasPendingChanges: computed('pendingTx', function() {
|
||||||
|
return isPresent(this.pendingTx);
|
||||||
|
}),
|
||||||
|
|
||||||
|
serialize () {
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
+15
-109
@@ -1,119 +1,25 @@
|
|||||||
import Ember from 'ember';
|
import EmberObject from '@ember/object';
|
||||||
|
|
||||||
const {
|
export default EmberObject.extend({
|
||||||
isPresent,
|
// Contract
|
||||||
} = Ember;
|
id: null,
|
||||||
|
account: null,
|
||||||
|
balance: 0,
|
||||||
|
totalKreditsEarned: 0,
|
||||||
|
contributionsCount: 0,
|
||||||
|
isCore: false,
|
||||||
|
ipfsHash: null,
|
||||||
|
|
||||||
export default Ember.Object.extend({
|
// IPFS
|
||||||
|
|
||||||
address: null,
|
|
||||||
name: null,
|
|
||||||
kind: null,
|
kind: null,
|
||||||
|
name: null,
|
||||||
url: null,
|
url: null,
|
||||||
github_username: null,
|
github_username: null,
|
||||||
github_uid: null,
|
github_uid: null,
|
||||||
wiki_username: null,
|
wiki_username: null,
|
||||||
ipfsHash: null,
|
zoom_display_name: null,
|
||||||
kredits: null,
|
|
||||||
isCore: false,
|
|
||||||
isCurrentUser: false,
|
|
||||||
|
|
||||||
avatarURL: function() {
|
serialize () {
|
||||||
return `https\:\/\/avatars2.githubusercontent.com/u/${this.get('github_uid')}?v=3&s=128`;
|
return JSON.stringify(this);
|
||||||
}.property('github_uid'),
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the contributor's profile data from IPFS and sets local instance
|
|
||||||
* properties from it
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
loadProfile(ipfs) {
|
|
||||||
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
|
||||||
ipfs.getFile(this.get('ipfsHash')).then(content => {
|
|
||||||
let profileJSON = JSON.parse(content);
|
|
||||||
let profile = Ember.Object.create(profileJSON);
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
name: profile.get('name'),
|
|
||||||
kind: profile.get('kind')
|
|
||||||
});
|
|
||||||
|
|
||||||
let accounts = profile.get('accounts');
|
|
||||||
let github = accounts.findBy('site', 'github.com');
|
|
||||||
let wiki = accounts.findBy('site', 'wiki.kosmos.org');
|
|
||||||
|
|
||||||
if (isPresent(github)) {
|
|
||||||
this.setProperties({
|
|
||||||
github_username: github.username,
|
|
||||||
github_uid: github.uid,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (isPresent(wiki)) {
|
|
||||||
this.setProperties({
|
|
||||||
wiki_username: wiki.username
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ember.Logger.debug('[contributor] loaded contributor profile', profile);
|
|
||||||
resolve();
|
|
||||||
}).catch((err) => {
|
|
||||||
Ember.Logger.error('[contributor] error trying to load contributor profile', this.get('ipfsHash'), err);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a JSON-LD object of the contributor, according to
|
|
||||||
* https://github.com/67P/kosmos-schemas/blob/master/schemas/contributor.json
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
toJSON() {
|
|
||||||
let contributor = {
|
|
||||||
"@context": "https://schema.kosmos.org",
|
|
||||||
"@type": "Contributor",
|
|
||||||
"kind": this.get('kind'),
|
|
||||||
"name": this.get('name'),
|
|
||||||
"accounts": []
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Ember.isPresent(this.get('url'))) {
|
|
||||||
contributor["url"] = this.get('url');
|
|
||||||
}
|
|
||||||
if (Ember.isPresent(this.get('github_uid'))) {
|
|
||||||
contributor.accounts.push({
|
|
||||||
"site": "github.com",
|
|
||||||
"uid": this.get('github_uid'),
|
|
||||||
"username": this.get('github_username'),
|
|
||||||
"url": `https://github.com/${this.get('github_username')}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (Ember.isPresent(this.get('wiki_username'))) {
|
|
||||||
contributor.accounts.push({
|
|
||||||
"site": "wiki.kosmos.org",
|
|
||||||
"username": this.get('wiki_username'),
|
|
||||||
"url": `https://wiki.kosmos.org/User:${this.get('wiki_username')}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return contributor;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JSON-LD representation of the model as a string
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
serialize() {
|
|
||||||
return JSON.stringify(this.toJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
|
|
||||||
const {
|
|
||||||
isPresent,
|
|
||||||
isEmpty,
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Ember.Object.extend({
|
|
||||||
|
|
||||||
id: null,
|
|
||||||
creatorAddress: null,
|
|
||||||
recipientAddress: null,
|
|
||||||
recipientName: null,
|
|
||||||
recipientProfile: null,
|
|
||||||
votesCount: null,
|
|
||||||
votesNeeded: null,
|
|
||||||
amount: null,
|
|
||||||
executed: null,
|
|
||||||
contribution: null,
|
|
||||||
kind: null,
|
|
||||||
description: null,
|
|
||||||
url: null,
|
|
||||||
details: null,
|
|
||||||
ipfsHash: null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the contribution details from IPFS and sets local instance
|
|
||||||
* properties from it
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
loadContribution(ipfs) {
|
|
||||||
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
|
||||||
ipfs.getFile(this.get('ipfsHash')).then(content => {
|
|
||||||
let contributionJSON = JSON.parse(content);
|
|
||||||
let contribution = Ember.Object.create(contributionJSON);
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
kind: contribution.get('kind'),
|
|
||||||
description: contribution.get('description'),
|
|
||||||
url: contribution.get('url')
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO load details
|
|
||||||
// let details = profile.get('accounts');
|
|
||||||
|
|
||||||
Ember.Logger.debug('[proposal] loaded contribution details', contributionJSON);
|
|
||||||
resolve();
|
|
||||||
}).catch((err) => {
|
|
||||||
Ember.Logger.error('[proposal] error trying to load contribution details', this.get('ipfsHash'), err);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a JSON-LD object of the contribution, according to
|
|
||||||
* https://github.com/67P/kosmos-schemas/blob/master/schemas/contribution.json
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
contributionToJSON() {
|
|
||||||
if (isEmpty(this.get('recipientProfile'))) {
|
|
||||||
throw new Error('IPFS hash for recipient profile missing from proposal object');
|
|
||||||
}
|
|
||||||
if (isEmpty(this.get('kind')) || isEmpty(this.get('description'))) {
|
|
||||||
throw new Error('Missing one or more required properties: kind, description');
|
|
||||||
}
|
|
||||||
|
|
||||||
let contribution = {
|
|
||||||
"@context": "https://schema.kosmos.org",
|
|
||||||
"@type": "Contribution",
|
|
||||||
"contributor": {
|
|
||||||
"ipfs": this.get('recipientProfile')
|
|
||||||
},
|
|
||||||
"kind": this.get('kind'),
|
|
||||||
"description": this.get('description'),
|
|
||||||
"details": {}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isPresent(this.get('url'))) {
|
|
||||||
contribution["url"] = this.get('url');
|
|
||||||
}
|
|
||||||
|
|
||||||
return contribution;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JSON-LD representation of the contribution as a string
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
serializeContribution() {
|
|
||||||
return JSON.stringify(this.contributionToJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import EmberObject, { computed } from '@ember/object';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
|
export default EmberObject.extend({
|
||||||
|
|
||||||
|
// Contract
|
||||||
|
id: null,
|
||||||
|
recipientId: null,
|
||||||
|
token: null,
|
||||||
|
amount: null,
|
||||||
|
confirmedAt: null,
|
||||||
|
vetoed: null,
|
||||||
|
ipfsHash: null,
|
||||||
|
|
||||||
|
// contributor model instance
|
||||||
|
recipient: null,
|
||||||
|
|
||||||
|
// TODO contributor who submitted the reimbursement
|
||||||
|
// recordedBy: null,
|
||||||
|
|
||||||
|
// IPFS
|
||||||
|
expenses: null, // Array of expense objects
|
||||||
|
|
||||||
|
pendingTx: null,
|
||||||
|
|
||||||
|
iso8601Date: computed('date', 'time', function() {
|
||||||
|
return this.time ? `${this.date}T${this.time}` : this.date;
|
||||||
|
}),
|
||||||
|
|
||||||
|
jsDate: computed('iso8601Date', function() {
|
||||||
|
return moment(this.iso8601Date).toDate();
|
||||||
|
}),
|
||||||
|
|
||||||
|
hasPendingChanges: computed('pendingTx', function() {
|
||||||
|
return isPresent(this.pendingTx);
|
||||||
|
}),
|
||||||
|
|
||||||
|
pendingStatus: computed('pendingTx', function() {
|
||||||
|
return isPresent(this.pendingTx) ? 'isPending' : 'notPending';
|
||||||
|
}),
|
||||||
|
|
||||||
|
serialize () {
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Resolver from 'ember-resolver';
|
|
||||||
|
|
||||||
export default Resolver;
|
|
||||||
+32
-9
@@ -1,16 +1,39 @@
|
|||||||
import Ember from 'ember';
|
import EmberRouter from '@ember/routing/router';
|
||||||
import config from './config/environment';
|
import config from './config/environment';
|
||||||
|
|
||||||
const Router = Ember.Router.extend({
|
export default class Router extends EmberRouter {
|
||||||
location: config.locationType,
|
location = config.locationType;
|
||||||
rootURL: config.rootURL
|
rootURL = config.rootURL;
|
||||||
});
|
}
|
||||||
|
|
||||||
Router.map(function() {
|
Router.map(function() {
|
||||||
this.route('spinner');
|
this.route('dashboard', function() {
|
||||||
this.route('proposals', function() {
|
this.route('contributors', function() {
|
||||||
|
this.route('show', { path: ':id' });
|
||||||
|
});
|
||||||
|
this.route('contributions', function() {
|
||||||
|
this.route('show', { path: ':id' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.route('contributions', function() {
|
||||||
|
this.route('new', { queryParams: ['contributorId', 'kind', 'amount'] });
|
||||||
|
this.route('resubmit', { path: ':id/resubmit' });
|
||||||
|
});
|
||||||
|
this.route('contributors', function() {
|
||||||
|
this.route('new');
|
||||||
|
this.route('edit', { path: ':id/edit' });
|
||||||
|
});
|
||||||
|
this.route('signup', function() {
|
||||||
|
this.route('github');
|
||||||
|
this.route('account');
|
||||||
|
this.route('complete');
|
||||||
|
});
|
||||||
|
this.route('budget', function() {
|
||||||
|
this.route('expenses');
|
||||||
|
this.route('reimbursements', function() {});
|
||||||
|
});
|
||||||
|
this.route('reimbursements', function() {
|
||||||
this.route('new');
|
this.route('new');
|
||||||
});
|
});
|
||||||
|
this.route('about');
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Router;
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class AboutRoute extends Route {
|
||||||
|
}
|
||||||
@@ -1,4 +1,35 @@
|
|||||||
import Ember from 'ember';
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { schedule } from '@ember/runloop';
|
||||||
|
|
||||||
export default Ember.Route.extend({
|
export default class ApplicationRoute extends Route {
|
||||||
});
|
@service kredits;
|
||||||
|
@service communityFunds;
|
||||||
|
|
||||||
|
beforeModel(/* transition */) {
|
||||||
|
return this.kredits.setup().then(() => {
|
||||||
|
this.kredits.kredits.preflightChecks().catch(error => {
|
||||||
|
console.error('Kredits preflight check failed!');
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.log('Error initializing Kredits', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return this.kredits.loadInitialData().then(() => {
|
||||||
|
this.kredits.addContractEventHandlers()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterModel() {
|
||||||
|
if (this.kredits.contributorsNeedSync) {
|
||||||
|
schedule('afterRender', this.kredits.syncContributors,
|
||||||
|
this.kredits.syncContributors.perform);
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule('afterRender', this.communityFunds.fetchBalances,
|
||||||
|
this.communityFunds.fetchBalances.perform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { isEmpty, isPresent } from '@ember/utils';
|
||||||
|
import { schedule } from '@ember/runloop';
|
||||||
|
|
||||||
|
export default class BudgetRoute extends Route {
|
||||||
|
@service browserCache;
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
async model () {
|
||||||
|
if (isPresent(this.kredits.reimbursements) &&
|
||||||
|
isEmpty(this.kredits.reimbursementsPending)) {
|
||||||
|
console.debug('[route:budget] Reimbursements loaded before, no need to sync or load');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const numCachedReimbursements = await this.browserCache.reimbursements.length();
|
||||||
|
if (numCachedReimbursements > 0) {
|
||||||
|
await this.kredits.loadObjectsFromCache('Reimbursement');
|
||||||
|
this.kredits.set('reimbursementsNeedSync', true);
|
||||||
|
} else {
|
||||||
|
await this.kredits.fetchObjects('Reimbursement', { page: { size: 10 } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterModel() {
|
||||||
|
if (this.kredits.reimbursementsNeedSync) {
|
||||||
|
schedule('afterRender', this.kredits.syncReimbursements,
|
||||||
|
this.kredits.syncReimbursements.perform);
|
||||||
|
}
|
||||||
|
schedule('afterRender', this.kredits.fetchMissingReimbursements,
|
||||||
|
this.kredits.fetchMissingReimbursements.perform);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class BudgetExpensesRoute extends Route {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class ContributionsNewRoute extends Route {
|
||||||
|
|
||||||
|
model (params) {
|
||||||
|
return { params };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
model(params) {
|
||||||
|
return this.kredits.contributions.findBy('id', parseInt(params.id));
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController (controller, model) {
|
||||||
|
this._super(controller, model);
|
||||||
|
|
||||||
|
controller.set('attributes', model.getProperties([
|
||||||
|
'contributorId', 'kind', 'amount', 'description', 'url', 'details'
|
||||||
|
]));
|
||||||
|
controller.set('attributes.date', model.jsDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
contributors: alias('kredits.contributors'),
|
||||||
|
|
||||||
|
model(params) {
|
||||||
|
return this.kredits.contributors.findBy('id', params.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController (controller, model) {
|
||||||
|
this._super(controller, model);
|
||||||
|
|
||||||
|
controller.set('attributes', {
|
||||||
|
account: model.account,
|
||||||
|
name: model.name,
|
||||||
|
kind: model.kind,
|
||||||
|
url: model.url,
|
||||||
|
github_username: model.github_username,
|
||||||
|
github_uid: model.github_uid,
|
||||||
|
gitea_username: model.gitea_username,
|
||||||
|
wiki_username: model.wiki_username,
|
||||||
|
zoom_display_name: model.zoom_display_name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { schedule } from '@ember/runloop';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default class DashboardRoute extends Route {
|
||||||
|
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
afterModel() {
|
||||||
|
if (this.kredits.contributionsNeedSync) {
|
||||||
|
schedule('afterRender', this.kredits.syncContributions,
|
||||||
|
this.kredits.syncContributions.perform);
|
||||||
|
}
|
||||||
|
// TODO fetch automatically under a certain threshold
|
||||||
|
// The browser might delete cached data and we don't need manual re-syncs
|
||||||
|
// depending on how little is missing
|
||||||
|
// schedule('afterRender', this.kredits.fetchMissingContributions,
|
||||||
|
// this.kredits.fetchMissingContributions.perform);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
contributions: alias('kredits.contributions'),
|
||||||
|
|
||||||
|
model (params) {
|
||||||
|
return this.contributions.findBy('id', parseInt(params.id));
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController (controller, model) {
|
||||||
|
this._super(controller, model);
|
||||||
|
|
||||||
|
this.controllerFor('dashboard')
|
||||||
|
.setProperties({
|
||||||
|
showDetailsPane: true,
|
||||||
|
selectedContributionId: model.id
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
this.controllerFor('dashboard')
|
||||||
|
.setProperties({
|
||||||
|
showDetailsPane: false,
|
||||||
|
selectedContributionId: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
contributors: alias('kredits.contributors'),
|
||||||
|
|
||||||
|
model (params) {
|
||||||
|
const contributor = this.contributors.findBy('id', params.id);
|
||||||
|
|
||||||
|
if (contributor) {
|
||||||
|
return contributor;
|
||||||
|
} else {
|
||||||
|
return this.kredits.fetchContributor(params.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController (controller, model) {
|
||||||
|
this._super(controller, model);
|
||||||
|
|
||||||
|
this.controllerFor('dashboard')
|
||||||
|
.setProperties({
|
||||||
|
showDetailsPane: true,
|
||||||
|
selectedContributorId: model.id
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
this.controllerFor('dashboard')
|
||||||
|
.setProperties({
|
||||||
|
showDetailsPane: false,
|
||||||
|
selectedContributorId: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
+4
-28
@@ -1,33 +1,9 @@
|
|||||||
import Ember from 'ember';
|
import Route from '@ember/routing/route';
|
||||||
import Contributor from 'kredits-web/models/contributor';
|
|
||||||
|
|
||||||
export default Ember.Route.extend({
|
export default Route.extend({
|
||||||
|
|
||||||
kredits: Ember.inject.service(),
|
redirect () {
|
||||||
|
this.transitionTo('dashboard');
|
||||||
beforeModel(transition) {
|
|
||||||
const kredits = this.get('kredits');
|
|
||||||
|
|
||||||
if (kredits.get('web3') && kredits.get('web3Provided')) {
|
|
||||||
kredits.get('web3').eth.getAccounts((error, accounts) => {
|
|
||||||
if (error || accounts.length === 0) {
|
|
||||||
if (confirm('It looks like you have an Ethereum wallet available. Please unlock your account.')) {
|
|
||||||
transition.retry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
model() {
|
|
||||||
let kredits = this.get('kredits');
|
|
||||||
|
|
||||||
return Ember.RSVP.hash({
|
|
||||||
contributors: kredits.getContributors(),
|
|
||||||
totalSupply: kredits.getValueFromContract('tokenContract', 'totalSupply'),
|
|
||||||
proposals: kredits.getProposals(),
|
|
||||||
newContributor: Contributor.create({ kind: 'person' })
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
import Proposal from 'kredits-web/models/proposal';
|
|
||||||
|
|
||||||
const {
|
|
||||||
Route,
|
|
||||||
RSVP,
|
|
||||||
inject: {
|
|
||||||
service
|
|
||||||
}
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
model(params) {
|
|
||||||
const proposal = Proposal.create({
|
|
||||||
recipientAddress: params.recipient,
|
|
||||||
amount: params.amount,
|
|
||||||
url: params.url,
|
|
||||||
kind: params.kind || 'dev',
|
|
||||||
ipfsHash: params.ipfsHash
|
|
||||||
});
|
|
||||||
|
|
||||||
return RSVP.hash({
|
|
||||||
proposal,
|
|
||||||
contributors: this.get('kredits').getContributors()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setupController(controller, model) {
|
|
||||||
this._super(controller, model.proposal);
|
|
||||||
|
|
||||||
controller.set('contributors', model.contributors);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class ReimbursementsNewRoute extends Route {
|
||||||
|
|
||||||
|
model (params) {
|
||||||
|
return { params };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { isEmpty } from '@ember/utils';
|
||||||
|
|
||||||
|
export default class SignupAccountRoute extends Route {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
async setupController (controller) {
|
||||||
|
if (!window.ethereum) return;
|
||||||
|
|
||||||
|
if (this.kredits.hasAccounts) {
|
||||||
|
controller.accountAddress = this.kredits.currentUserAccounts.firstObject;
|
||||||
|
} else {
|
||||||
|
return this.kredits.connectWallet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect () {
|
||||||
|
if (isEmpty(this.kredits.githubAccessToken)) {
|
||||||
|
this.transitionTo('signup.index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { isEmpty } from '@ember/utils';
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
redirect () {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
accessToken = window.location.hash.match(/access_token=(.+)/)[1];
|
||||||
|
} catch (error) { /* ignore */ }
|
||||||
|
|
||||||
|
if (isEmpty(accessToken) || accessToken === 'undefined') {
|
||||||
|
console.error('No GitHub access token found.');
|
||||||
|
this.transitionTo('signup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.kredits.set('githubAccessToken', accessToken);
|
||||||
|
|
||||||
|
this.transitionTo('signup.account');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
|
|
||||||
export default Ember.Route.extend({
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
import * as localforage from 'localforage';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
function createStore(name) {
|
||||||
|
let networkName;
|
||||||
|
if (config.web3NetworkName) {
|
||||||
|
networkName = config.web3NetworkName.toLocaleLowerCase().replace(' ', '-');
|
||||||
|
} else {
|
||||||
|
networkName = 'custom';
|
||||||
|
}
|
||||||
|
return localforage.createInstance({ name: `kredits:${networkName}:${name}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BrowserCacheService extends Service {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.stores = {
|
||||||
|
contributors: createStore('contributors'),
|
||||||
|
contributions: createStore('contributions'),
|
||||||
|
reimbursements: createStore('reimbursements')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get contributors() {
|
||||||
|
return this.stores.contributors;
|
||||||
|
}
|
||||||
|
|
||||||
|
get contributions() {
|
||||||
|
return this.stores.contributions;
|
||||||
|
}
|
||||||
|
|
||||||
|
get reimbursements() {
|
||||||
|
return this.stores.reimbursements;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { A } from '@ember/array';
|
||||||
|
import { task } from 'ember-concurrency-decorators';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import Service, { inject as service } from '@ember/service';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default class CommunityFundsService extends Service {
|
||||||
|
@service exchangeRates;
|
||||||
|
|
||||||
|
@tracked balancesLoaded = false;
|
||||||
|
@tracked balances = A([]);
|
||||||
|
|
||||||
|
@task
|
||||||
|
*fetchBalances () {
|
||||||
|
const promises = [];
|
||||||
|
const balances = config.communityFundsAPI.balances;
|
||||||
|
|
||||||
|
for (const item of Object.keys(balances)) {
|
||||||
|
const c = balances[item];
|
||||||
|
promises.push(
|
||||||
|
this.fetchBalance(c.url)
|
||||||
|
.then(res => { return this.processBalance(res, c) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
yield Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
this.balancesLoaded = true;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(`[community-funds] Fetching balances failed:`);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchBalance(url) {
|
||||||
|
return fetch(url).then(res => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async processBalance (res, config) {
|
||||||
|
await this.exchangeRates.fetchRates();
|
||||||
|
|
||||||
|
// Format and round the approximate USD value
|
||||||
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
|
const balanceUSD = (res.confirmed_balance / 100000000) * this.exchangeRates.btcusd;
|
||||||
|
res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang);
|
||||||
|
|
||||||
|
this.balances.pushObject({
|
||||||
|
...res,
|
||||||
|
...{ token: { icon: `/img/${config.icon}`, symbol: config.symbol, description: config.description } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user