mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
767 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd2e6e1b24 | ||
|
|
99b8d24db3 | ||
|
|
8116b569c1 | ||
|
|
da791c5fed | ||
|
|
fbd9c59bfd | ||
|
|
3a1b3d19c5 | ||
|
|
238076f534 | ||
|
|
214d74ae11 | ||
|
|
30324f6113 | ||
|
|
68f0a25873 | ||
|
|
cd5bc4e930 | ||
|
|
1d8f729c95 | ||
|
|
f0d2fb53d4 | ||
|
|
0445c680ba | ||
|
|
12453942c8 | ||
|
|
b18a5be940 | ||
|
|
c2234747e8 | ||
|
|
d6c9ab43ec | ||
|
|
db16a87f74 | ||
|
|
5b0d2ec97b | ||
|
|
b906db3b24 | ||
|
|
df0af2a11f | ||
|
|
706dd3e616 | ||
|
|
bbec58e049 | ||
|
|
381b7d85f4 | ||
|
|
c17c056af3 | ||
|
|
a26a85cd1f | ||
|
|
9b68b1d327 | ||
|
|
6a6631052e | ||
|
|
d4ad3a953a | ||
|
|
7bb63a78c5 | ||
|
|
26c859f14c | ||
|
|
dd5c9bf3f6 | ||
|
|
3db40fea31 | ||
|
|
8f1c198406 | ||
|
|
ac65a3c86e | ||
|
|
79abcd90f6 | ||
|
|
d614abdec6 | ||
|
|
44d754c59d | ||
|
|
247d1db5d5 | ||
|
|
a52cfb2cd2 | ||
|
|
4e5d923388 | ||
|
|
f9598dd619 | ||
|
|
00f1c62646 | ||
|
|
8997728cbc | ||
|
|
99a0704516 | ||
|
|
434e361d5d | ||
|
|
f112b0bf03 | ||
|
|
0ce67ccd35 | ||
|
|
c947feadeb | ||
|
|
2fa381980d | ||
|
|
4e2c42f65c | ||
|
|
38b7d60af7 | ||
|
|
4714593d52 | ||
|
|
7729ed4f72 | ||
|
|
7107777df3 | ||
|
|
53989b918e | ||
|
|
fdd0c84441 | ||
|
|
8f0789bc6d | ||
|
|
2fdea6cd61 | ||
|
|
ed5f3a6202 | ||
|
|
d98e909256 | ||
|
|
6e0def310f | ||
|
|
823da07a5e | ||
|
|
40d2960562 | ||
|
|
6799f45352 | ||
|
|
1209a044ce | ||
|
|
9a8760c120 | ||
|
|
8d2a7716f3 | ||
|
|
5f240ada59 | ||
|
|
1c86dea4be | ||
|
|
92fd7ac09c | ||
|
|
90b490c28b | ||
|
|
8fd03a09de | ||
|
|
041232fbdd | ||
|
|
f595121ab3 | ||
|
|
bd733312f4 | ||
|
|
70db2f78da | ||
|
|
0298aba86f | ||
|
|
60b344eea8 | ||
|
|
eb63b743b1 | ||
|
|
d09ffc9dd8 | ||
|
|
f1b2643941 | ||
|
|
9e9d3ddc57 | ||
|
|
99a134494c | ||
|
|
d1156f963d | ||
|
|
2a6a2694d4 | ||
|
|
cc24e6b801 | ||
|
|
9c85719270 | ||
|
|
394e86f765 | ||
|
|
6bb9366ec8 | ||
|
|
8a9d4df6c7 | ||
|
|
fdc2e91170 | ||
|
|
40bf026e82 | ||
|
|
975e82710e | ||
|
|
1763dbcff1 | ||
|
|
908281356e | ||
|
|
123d5638cf | ||
|
|
79d0006f3f | ||
|
|
240b5daf3e | ||
|
|
cd0bede2f2 | ||
|
|
c5d984732a | ||
|
|
7358e68394 | ||
|
|
27b09e5b73 | ||
|
|
1705511b10 | ||
|
|
e17fa6d0ed | ||
|
|
9091976337 | ||
|
|
58d098503b | ||
|
|
33fe4f5295 | ||
|
|
0646c4f8bd | ||
|
|
960469f07c | ||
|
|
665f81ac9c | ||
|
|
26f05b343e | ||
|
|
f50968f992 | ||
|
|
4c486c399b | ||
|
|
9f4dd909a8 | ||
|
|
2b85aa1b88 | ||
|
|
8e4c3a3b21 | ||
|
|
a4160d2994 | ||
|
|
27e0252ccd | ||
|
|
0a707b3f02 | ||
|
|
6fc421810f | ||
|
|
22cf7443f4 | ||
|
|
8e7b4d2444 | ||
|
|
54437cec19 | ||
|
|
40fc63ea0c | ||
|
|
6bd81fe12a | ||
|
|
519ea1a33f | ||
|
|
f07f309393 | ||
|
|
7132e9ff24 | ||
|
|
34ae3cd704 | ||
|
|
fba972c98e | ||
|
|
a391ac682d | ||
|
|
4ee49d5991 | ||
|
|
0d573651a3 | ||
|
|
52efc23984 | ||
|
|
aefb84df3b | ||
|
|
ba374e08ff | ||
|
|
33a11ac2e5 | ||
|
|
357c4a382d | ||
|
|
d7e8f26ace | ||
|
|
5c312c1939 | ||
|
|
5163ab134e | ||
|
|
73dd0db529 | ||
|
|
8921db89ab | ||
|
|
8d624459d4 | ||
|
|
ec96021b00 | ||
|
|
25f50b8cdf | ||
|
|
0127d5143a | ||
|
|
ffe3b689c4 | ||
|
|
ff123be895 | ||
|
|
8178ec5671 | ||
|
|
67dd089e67 | ||
|
|
8d96368ea6 | ||
|
|
eb163ef03c | ||
|
|
5558403358 | ||
|
|
db3a4d0f01 | ||
|
|
d01fe62757 | ||
|
|
87cfc8f1de | ||
|
|
3a8bef26d3 | ||
|
|
b550c0e9e3 | ||
|
|
8f1ee30553 | ||
|
|
458174a5f5 | ||
|
|
c5414aadd1 | ||
|
|
eacd01e77e | ||
|
|
089b919a68 | ||
|
|
2e5945642d | ||
|
|
68a5b6fc50 | ||
|
|
88538257ac | ||
|
|
fb8041fb4b | ||
|
|
e685c4302d | ||
|
|
103b56ea3d | ||
|
|
0490b115ad | ||
|
|
5e1dd4a9ad | ||
|
|
93dd97a14a | ||
|
|
52d065a38d | ||
|
|
ad33e9f32d | ||
|
|
b9f00c6971 | ||
|
|
dba5ea156a | ||
|
|
631c86865f | ||
|
|
324b8fc74a | ||
|
|
282ca3ea2a | ||
|
|
17223db3ea | ||
|
|
cff3fdae6e | ||
|
|
108e83a402 | ||
|
|
fc237848c8 | ||
|
|
92b86bfa0b | ||
|
|
b0fd17047c | ||
|
|
bf8f82fbd8 | ||
|
|
5468c60eaa | ||
|
|
8736d87b95 | ||
|
|
dd0440519b | ||
|
|
b7400553fc | ||
|
|
9d2f570515 | ||
|
|
7eeba0c082 | ||
|
|
2e6bb21fda | ||
|
|
e30cfdf942 | ||
|
|
b07fb92e5c | ||
|
|
5729125fbb | ||
|
|
4577038abd | ||
|
|
7649c1df9e | ||
|
|
033bd111ad | ||
|
|
2cbe07b373 | ||
|
|
f6ec5c67a7 | ||
|
|
6bb78d3216 | ||
|
|
fa717a357d | ||
|
|
eed6bcc044 | ||
|
|
a01fd739bd | ||
|
|
3b7ed5ffd7 | ||
|
|
b3d9beea6d | ||
|
|
e256c7a7d9 | ||
|
|
ef866f957a | ||
|
|
58d25415db | ||
|
|
15f51a4064 | ||
|
|
7fbe456e79 | ||
|
|
9d8daac4cf | ||
|
|
ce199374d5 | ||
|
|
4af8615624 | ||
|
|
b6bb438507 | ||
|
|
49acd8a4f3 | ||
|
|
3e699a99d5 | ||
|
|
e4238f9283 | ||
|
|
9cd6d6d4c1 | ||
|
|
49a4b5feb4 | ||
|
|
c6eff157de | ||
|
|
80d16233e7 | ||
|
|
65e1a39027 | ||
|
|
d73b567bd4 | ||
|
|
787bb0a9e6 | ||
|
|
7faf0efb20 | ||
|
|
ed70cb8e3d | ||
|
|
2544b5b821 | ||
|
|
90613b7cc8 | ||
|
|
173640d0b4 | ||
|
|
e70d09ebbf | ||
|
|
1ff33378e1 | ||
|
|
286739f770 | ||
|
|
45b1cd3942 | ||
|
|
b2d34ab95d | ||
|
|
f628ed55cf | ||
|
|
70ec9f50ab | ||
|
|
d6d130b8f9 | ||
|
|
40410eb10f | ||
|
|
a6e3dbd825 | ||
|
|
3882df41f1 | ||
|
|
6c67b96e30 | ||
|
|
b6d34472fe | ||
|
|
69d11b5cd0 | ||
|
|
f36f2029e5 | ||
|
|
af7476f79a | ||
|
|
890207daee | ||
|
|
9a5653f1e3 | ||
|
|
4f45ba1680 | ||
|
|
0ccfd36a83 | ||
|
|
8452a95114 | ||
|
|
9b98035ee7 | ||
|
|
6dc75afdc8 | ||
|
|
922ae7a274 | ||
|
|
2d46d12628 | ||
|
|
44f270f408 | ||
|
|
9ff70c4aef | ||
|
|
8c6a54397f | ||
|
|
ce915df2b2 | ||
|
|
84dd772b7c | ||
|
|
959f6c2c58 | ||
|
|
793b2b20e9 | ||
|
|
1c224fedc5 | ||
|
|
4299baec5d | ||
|
|
e931d3efbc | ||
|
|
36d81a6ec9 | ||
|
|
22c2d09d88 | ||
|
|
558659b54d | ||
|
|
eb1a0ba49f | ||
|
|
45212e7e14 | ||
|
|
c691af9712 | ||
|
|
0c2226b3fc | ||
|
|
eeda6b0208 | ||
|
|
d9096424c5 | ||
|
|
d171078ca4 | ||
|
|
cb18b8f347 | ||
|
|
4fdb72f93c | ||
|
|
549930b48c | ||
|
|
409a3de901 | ||
|
|
431ac5a403 | ||
|
|
999d4633ab | ||
|
|
5a26fc812d | ||
|
|
eb210e9072 | ||
|
|
dbfd25bb8e | ||
|
|
4292583d73 | ||
|
|
7c346d62eb | ||
|
|
14abbee3fb | ||
|
|
903979b3f2 | ||
|
|
913e649e5d | ||
|
|
7f8733796e | ||
|
|
1a98afee92 | ||
|
|
60daafe136 | ||
|
|
c7ad06a1f7 | ||
|
|
d07c795511 | ||
|
|
89d1071dc1 | ||
|
|
dfe34a9f22 | ||
|
|
93d3ea70fc | ||
|
|
89a76d9ead | ||
|
|
dc6bd1aae8 | ||
|
|
3412d70737 | ||
|
|
ce52d5cf42 | ||
|
|
da19066fb8 | ||
|
|
be20c2c800 | ||
|
|
c59638aaae | ||
|
|
00691f1225 | ||
|
|
735b79a4e0 | ||
|
|
443b529667 | ||
|
|
c3e62f58ab | ||
|
|
9869545777 | ||
|
|
04e5034f5f | ||
|
|
e99e3eb6c4 | ||
|
|
508615b600 | ||
|
|
4d3d416ecb | ||
|
|
be2897806a | ||
|
|
6ebde04d22 | ||
|
|
9deaa8694e | ||
|
|
5dff50ff92 | ||
|
|
ba4d90765a | ||
|
|
187ccaa95e | ||
|
|
18b6d8289f | ||
|
|
c851f8f006 | ||
|
|
9d60704c9b | ||
|
|
365d7a1afd | ||
|
|
a3d5f23861 | ||
|
|
330ea986d4 | ||
|
|
652c1aae73 | ||
|
|
c91adcd165 | ||
|
|
293c286d22 | ||
|
|
70244f79ba | ||
|
|
6ba49ad294 | ||
|
|
acf8cbfe0e | ||
|
|
f904fd00e5 | ||
|
|
831bec5baf | ||
|
|
f0257b0f87 | ||
|
|
66856d1229 | ||
|
|
27c22a4b09 | ||
|
|
1e7cce9056 | ||
|
|
219af76679 | ||
|
|
dcf773fd88 | ||
|
|
df51e6d429 | ||
|
|
6ea66b85e0 | ||
|
|
10aae90ae2 | ||
|
|
ea8e31561e | ||
|
|
45c627b0a5 | ||
|
|
1d2ca469fc | ||
|
|
e6a0c86f4e | ||
|
|
039429d452 | ||
|
|
46df9398e8 | ||
|
|
744907ac38 | ||
|
|
084677cdca | ||
|
|
891cd3124f | ||
|
|
b9fd3b8b7d | ||
|
|
f6208c1324 | ||
|
|
3a117c0f09 | ||
|
|
a0c83f33ca | ||
|
|
99b9fadd74 | ||
|
|
cf023847ad | ||
|
|
59357b274d | ||
|
|
1582184223 | ||
|
|
db9bcafb82 | ||
|
|
50b9838eec | ||
|
|
ff958b7cd6 | ||
|
|
33bc2aa220 | ||
|
|
03b331e5d5 | ||
|
|
86cc9fb7d8 | ||
|
|
f98efe0b97 | ||
|
|
6c5345df64 | ||
|
|
2d97661d28 | ||
|
|
3efe0c1ce2 | ||
|
|
6099c91216 | ||
|
|
934e4d9607 | ||
|
|
1198f6112e | ||
|
|
ac7e08ae2c | ||
|
|
9c5cbd348b | ||
|
|
8600710d23 | ||
|
|
d06d52deda | ||
|
|
948377ba93 | ||
|
|
d878028dcc | ||
|
|
65c969a321 | ||
|
|
777f7f9305 | ||
|
|
43fb37ab1d | ||
|
|
06734ec886 | ||
|
|
0b8ae93727 | ||
|
|
24cea97e08 | ||
|
|
18536e5db0 | ||
|
|
8c236cb5cb | ||
|
|
797c8ad7fa | ||
|
|
ba8d2dcb8b | ||
|
|
78a095d958 | ||
|
|
627172f6df | ||
|
|
a39f25961c | ||
|
|
e738ae5c8c | ||
|
|
749e85e8e6 | ||
|
|
cfa251b158 | ||
|
|
1ca38b8741 | ||
|
|
d7bc5a7088 | ||
|
|
3b9a2c3ee1 | ||
|
|
260ad77d39 | ||
|
|
5c508a0cd9 | ||
|
|
fd3d1607a4 | ||
|
|
abbc0fbcf1 | ||
|
|
00be41608d | ||
|
|
499c3f2e13 | ||
|
|
627845f6e4 | ||
|
|
07eea76057 | ||
|
|
fecc4e9b79 | ||
|
|
58e9302f15 | ||
|
|
86f649fab1 | ||
|
|
9ac0becfb2 | ||
|
|
eda547b868 | ||
|
|
f75e872415 | ||
|
|
aef0712165 | ||
|
|
bed4b7fd27 | ||
|
|
b53ff5daf3 | ||
|
|
bb0872b4fc | ||
|
|
b65101f4be | ||
|
|
593d242a4c | ||
|
|
db7f339c34 | ||
|
|
9f3575a874 | ||
|
|
1c9c59c512 | ||
|
|
127202b831 | ||
|
|
4f8a04ed21 | ||
|
|
63b2e0560b | ||
|
|
07291d71f2 | ||
|
|
d1ca1ec4d9 | ||
|
|
6907cf9972 | ||
|
|
d4f8d1498d | ||
|
|
0952e4a664 | ||
|
|
6a4e8c95ea | ||
|
|
983bfb7adf | ||
|
|
d7aaf5e210 | ||
|
|
50281132ad | ||
|
|
6a2b22015e | ||
|
|
2f90890f50 | ||
|
|
0fe83a0583 | ||
|
|
ce74e69480 | ||
|
|
ddea2aeb22 | ||
|
|
7bbe69cce9 | ||
|
|
e921e30d64 | ||
|
|
cd4f9d8bb4 | ||
|
|
a0553788b6 | ||
|
|
1a183d78af | ||
|
|
cabcaa892c | ||
|
|
01c9d62a2b | ||
|
|
ba76df863c | ||
|
|
81441a0895 | ||
|
|
da0222f213 | ||
|
|
fb8a2eb2e0 | ||
|
|
cde2e27e04 | ||
|
|
3758ea2cf4 | ||
|
|
e62fc11328 | ||
|
|
3cbfae83c1 | ||
|
|
57667654ef | ||
|
|
eadd66fa91 | ||
|
|
75cd94a39a | ||
|
|
7872bfe19d | ||
|
|
af008e69c2 | ||
|
|
a549abc20f | ||
|
|
116344737a | ||
|
|
93c03f4e88 | ||
|
|
445332c27c | ||
|
|
c42e1892d0 | ||
|
|
b6b526dd57 | ||
|
|
3ef7f19ffc | ||
|
|
9d0d851c2e | ||
|
|
adb35b5bef | ||
|
|
acead09377 | ||
|
|
714cf43f6a | ||
|
|
5df0755252 | ||
|
|
c14827b234 | ||
|
|
ff9ef2af41 | ||
|
|
91ef5edcc3 | ||
|
|
27302c6fcc | ||
|
|
4d975da176 | ||
|
|
5b58d8a1e8 | ||
|
|
3105958afb | ||
|
|
a505227d01 | ||
|
|
673503b76f | ||
|
|
384682421d | ||
|
|
2ddd6e6321 | ||
|
|
86739aa1ac | ||
|
|
45a46cbc7a | ||
|
|
567f453232 | ||
|
|
890f654971 | ||
|
|
572a0ac266 | ||
|
|
d26ffdbe1a | ||
|
|
0bfc9236ed | ||
|
|
32e6394b3f | ||
|
|
09735b7f47 | ||
|
|
ee280d5c7b | ||
|
|
c1b56e4cb6 | ||
|
|
6698d15f20 | ||
|
|
ef35fd02e5 | ||
|
|
8e70e20f9e | ||
|
|
9632bf5b93 | ||
|
|
dde0cab04b | ||
|
|
c8337c7287 | ||
|
|
15560a3bce | ||
|
|
2e3a60cf6e | ||
|
|
08b0c43382 | ||
|
|
4e0e11a611 | ||
|
|
ef41dfca4c | ||
|
|
cfbca4b0fd | ||
|
|
fdea9a68a1 | ||
|
|
47169e19aa | ||
|
|
0b03c8360b | ||
|
|
62f8af1455 | ||
|
|
0934d452bb | ||
|
|
f2f31790b4 | ||
|
|
cf6ecc17cc | ||
|
|
931f9bdce0 | ||
|
|
bec0528a3a | ||
|
|
670f2b1fc3 | ||
|
|
f2f6de717b | ||
|
|
f8ad2eddf3 | ||
|
|
c36a46cad6 | ||
|
|
00360c77d2 | ||
|
|
8a62cd386e | ||
|
|
450327f093 | ||
|
|
e87ec04058 | ||
|
|
f9d41de8f1 | ||
|
|
f80a1a5f6b | ||
|
|
f81caf962d | ||
|
|
d18fcf0a18 | ||
|
|
0187217c86 | ||
|
|
b820bdec09 | ||
|
|
adace2954e | ||
|
|
6eeb8eeba6 | ||
|
|
dd2a8202ef | ||
|
|
d1cfd627bc | ||
|
|
fb97b7443d | ||
|
|
48fcd45d7d | ||
|
|
5cfc418d77 | ||
|
|
f3fbe38247 | ||
|
|
a0a1c84db1 | ||
|
|
54d563f49e | ||
|
|
e8ee8b8a16 | ||
|
|
c6ac44ba14 | ||
|
|
e4d8438801 | ||
|
|
f9539ab50a | ||
|
|
59f83c2432 | ||
|
|
cd789136c0 | ||
|
|
54b5bc441e | ||
|
|
2537b6ba09 | ||
|
|
013a1b4f51 | ||
|
|
d2377bd7c3 | ||
|
|
c17314125e | ||
|
|
09a59480f3 | ||
|
|
63cc2ce70a | ||
|
|
4642e050ba | ||
|
|
27a442ed2e | ||
|
|
325ae00eeb | ||
|
|
152e4129b2 | ||
|
|
2ddcf84625 | ||
|
|
13314700cd | ||
|
|
a7a499a2b1 | ||
|
|
b646313b58 | ||
|
|
f3ce4ca803 | ||
|
|
93d99c0c47 | ||
|
|
ae1fc7572a | ||
|
|
1a527cca10 | ||
|
|
c625513924 | ||
|
|
3f58302a14 | ||
|
|
63b199c9c2 | ||
|
|
fc64c565db | ||
|
|
91e60fa82b | ||
|
|
0cc52c2206 | ||
|
|
2ffe4ba70b | ||
|
|
2afd7e3687 | ||
|
|
a0f8d13c4f | ||
|
|
2571ea021a | ||
|
|
6950e05b6a | ||
|
|
7eb767a268 | ||
|
|
8e64abc4bc | ||
|
|
52df793a74 | ||
|
|
8e44a421a2 | ||
|
|
7f4ccdcac8 | ||
|
|
03e8de2f62 | ||
|
|
8b04eecc90 | ||
|
|
16bcd86792 | ||
|
|
be3c519a57 | ||
|
|
8776cb1cea | ||
|
|
4c94503f9a | ||
|
|
48f57376d3 | ||
|
|
958469f526 | ||
|
|
2a774a7bb6 | ||
|
|
a872ad9d8b | ||
|
|
2499a05473 | ||
|
|
6b66893ea4 | ||
|
|
529c27aed5 | ||
|
|
70fc0afbc4 | ||
|
|
09f81fd0d6 | ||
|
|
af7f2d4d5e | ||
|
|
3bd5d6b9f6 | ||
|
|
57912b5a5a | ||
|
|
a05f5b9737 | ||
|
|
1963b586ac | ||
|
|
3b9ad59849 | ||
|
|
79e0e5668d | ||
|
|
0e8edf0c72 | ||
|
|
24e2544544 | ||
|
|
f3732c76ea | ||
|
|
a4c72a9a86 | ||
|
|
455610e586 | ||
|
|
634d58b3ca | ||
|
|
27bbd77e8c | ||
|
|
d8ae77ded7 | ||
|
|
0648c04728 | ||
|
|
57c26e3b4a | ||
|
|
b03afff994 | ||
|
|
77f9e60177 | ||
|
|
35bb792496 | ||
|
|
8a87304800 | ||
|
|
64bbe053f8 | ||
|
|
d3f420bf6d | ||
|
|
7fcaaa297a | ||
|
|
7c2d2044a9 | ||
|
|
aa32f59dc6 | ||
|
|
182af99e7c | ||
|
|
5b520a7a81 | ||
|
|
364917c910 | ||
|
|
ca7b9c786a | ||
|
|
15c2363098 | ||
|
|
1a11095121 | ||
|
|
2b384b1d15 | ||
|
|
a1d61edb9c | ||
|
|
96a8687896 | ||
|
|
0448773682 | ||
|
|
57998ba727 | ||
|
|
de83447cb3 | ||
|
|
eba19468d5 | ||
|
|
65c78df671 | ||
|
|
a7096aa89f | ||
|
|
15a50ef452 | ||
|
|
04036e5c87 | ||
|
|
2bbb5ef74e | ||
|
|
91eb7feb3c | ||
|
|
978d77142c | ||
|
|
e36478b9ac | ||
|
|
e1fe4dd693 | ||
|
|
b1ee949b1c | ||
|
|
a0e5f8e97e | ||
|
|
e9cfb2c4ee | ||
|
|
190b6edfb1 | ||
|
|
80a0c59f87 | ||
|
|
823fdec705 | ||
|
|
fe87dcced7 | ||
|
|
137eb44516 | ||
|
|
f60d957102 | ||
|
|
8f0b04504f | ||
|
|
2c39d8b1c8 | ||
|
|
d4d1c32288 | ||
|
|
e4f39d2b6a | ||
|
|
e5a2bfbcbd | ||
|
|
de3b76b31d | ||
|
|
53455496bf | ||
|
|
cc2a2f6dfb | ||
|
|
ee4ac7371c | ||
|
|
d5265407b9 | ||
|
|
954b3e9fc5 | ||
|
|
7d9894bef7 | ||
|
|
3b34698e8b | ||
|
|
263cb581c4 | ||
|
|
1c9cb4516c | ||
|
|
ac4ceccb4f | ||
|
|
e731b7882d | ||
|
|
84e0728ff3 | ||
|
|
666bc18e91 | ||
|
|
8f83124a0d | ||
|
|
ee91daad7e | ||
|
|
ee78c0d33b | ||
|
|
1318abd37e | ||
|
|
76a031a8c9 | ||
|
|
09482ebcf3 | ||
|
|
67424f2d3a | ||
|
|
51f530ffbe | ||
|
|
013f96a754 | ||
|
|
df6a018fb6 | ||
|
|
409eaf54c1 | ||
|
|
7e04fd342c | ||
|
|
1fe15bc6a5 | ||
|
|
ff1bffbb55 | ||
|
|
b28b18a19a | ||
|
|
bbc3c85212 | ||
|
|
26a08fac06 | ||
|
|
da9d7a4336 | ||
|
|
46c6555f94 | ||
|
|
3e980fd2d4 | ||
|
|
fb1462f669 | ||
|
|
41e1630aac | ||
|
|
ef84c4e3da | ||
|
|
61cc44cc83 | ||
|
|
c20cbe7d66 | ||
|
|
2f4af3223b | ||
|
|
e4b2c42897 | ||
|
|
746df9277c | ||
|
|
8428588a4c | ||
|
|
83a8f4b911 | ||
|
|
2736024cb7 | ||
|
|
9a32ca893e | ||
|
|
59d3c6c94f | ||
|
|
388027b731 | ||
|
|
8abdedc11d | ||
|
|
9758f5baa8 | ||
|
|
248262a597 | ||
|
|
cc0f2c7c7f | ||
|
|
72f6468d12 | ||
|
|
522c0edd90 | ||
|
|
d338f217fe | ||
|
|
ca79857386 | ||
|
|
60e551e273 | ||
|
|
954e148be3 | ||
|
|
3d0b79f674 | ||
|
|
d9442aa23c | ||
|
|
ba0daf4452 | ||
|
|
8d9cd5bbd1 | ||
|
|
186b877c09 | ||
|
|
5ed2dfccd1 | ||
|
|
911cfd8642 | ||
|
|
3539bd1e79 | ||
|
|
f56df7c16d | ||
|
|
c507dfa6c4 | ||
|
|
f6d2e898dc | ||
|
|
326c7a93fb | ||
|
|
58381b8062 | ||
|
|
0899cea4b4 | ||
|
|
7459e937b5 | ||
|
|
55db0bebbb | ||
|
|
0bdb8142c6 | ||
|
|
88ee94d4b6 | ||
|
|
1df4ed0fe9 | ||
|
|
2a339a2935 | ||
|
|
a1810e6023 | ||
|
|
832ca3347c | ||
|
|
9d2b64e82b | ||
|
|
9a5e4b3f54 | ||
|
|
e5e8032ba1 | ||
|
|
5356e68b51 | ||
|
|
cd94c625a7 | ||
|
|
972a3746a1 | ||
|
|
a9e12e4384 | ||
|
|
1690e6420f | ||
|
|
acdf61f7ab | ||
|
|
2e4fc557ea | ||
|
|
979dcead49 | ||
|
|
116ddf345d | ||
|
|
366805a64f | ||
|
|
1fee2a846a | ||
|
|
3f54eb52b2 | ||
|
|
a3847ce1c9 | ||
|
|
1e7415b692 | ||
|
|
ff950ef28a | ||
|
|
51bd12c6cf | ||
|
|
5fa37dbffb | ||
|
|
c6307e4ad3 | ||
|
|
06a54d451c | ||
|
|
e317075815 | ||
|
|
45541a255b | ||
|
|
3ab423d695 | ||
|
|
345d7b427a | ||
|
|
de6d6b692e | ||
|
|
b2845e2284 | ||
|
|
47383c347c |
14
.babelrc
Normal file
14
.babelrc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"presets": ["react", "es2015"],
|
||||||
|
"env": {
|
||||||
|
"development": {
|
||||||
|
"presets": ["react-hmre"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"presets": ["react", "es2015"],
|
||||||
|
"plugins": [
|
||||||
|
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,6 +1,10 @@
|
|||||||
build/
|
.DS_Store
|
||||||
node_modules/
|
|
||||||
electron_build/
|
|
||||||
.env
|
.env
|
||||||
dist/
|
Desktop.ini
|
||||||
vendor/
|
Thumbs.db
|
||||||
|
node_modules/*
|
||||||
|
!node_modules/boost
|
||||||
|
/dist
|
||||||
|
/compiled
|
||||||
|
/secret
|
||||||
|
*.log
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "browser/ace"]
|
|
||||||
path = browser/ace
|
|
||||||
url = https://github.com/ajaxorg/ace-builds.git
|
|
||||||
18
LICENSE
Normal file
18
LICENSE
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
GPL-3.0
|
||||||
|
|
||||||
|
Boostnote - the simplest note app
|
||||||
|
|
||||||
|
Copyright (C) 2016 MAISIN&CO.
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
11
appdmg.json
Normal file
11
appdmg.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"title": "Boostnote",
|
||||||
|
"icon": "resources/dmg.icns",
|
||||||
|
"background": "resources/boostnote-install.png",
|
||||||
|
"icon-size": 80,
|
||||||
|
"contents": [
|
||||||
|
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" },
|
||||||
|
{ "x": 192, "y": 344, "type": "file", "path": "dist/Boostnote-darwin-x64/Boostnote.app" }
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
11
bower.json
11
bower.json
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "codexen-app",
|
|
||||||
"dependencies": {
|
|
||||||
"react": "~0.13.3",
|
|
||||||
"fontawesome": "~4.3.0",
|
|
||||||
"react-router": "~0.13.3",
|
|
||||||
"reflux": "~0.2.8",
|
|
||||||
"moment": "~2.10.3",
|
|
||||||
"markdown-it": "~4.3.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Submodule browser/ace deleted from 0982db4853
201
browser/components/CodeEditor.js
Normal file
201
browser/components/CodeEditor.js
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import CodeMirror from 'codemirror'
|
||||||
|
|
||||||
|
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||||
|
|
||||||
|
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
|
|
||||||
|
function pass (name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'ejs':
|
||||||
|
return 'Embedded Javascript'
|
||||||
|
case 'html_ruby':
|
||||||
|
return 'Embedded Ruby'
|
||||||
|
case 'objectivec':
|
||||||
|
return 'Objective C'
|
||||||
|
case 'text':
|
||||||
|
return 'Plain Text'
|
||||||
|
default:
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CodeEditor extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.changeHandler = (e) => this.handleChange(e)
|
||||||
|
this.blurHandler = (editor, e) => {
|
||||||
|
if (e == null) return null
|
||||||
|
let el = e.relatedTarget
|
||||||
|
while (el != null) {
|
||||||
|
if (el === this.refs.root) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
el = el.parentNode
|
||||||
|
}
|
||||||
|
this.props.onBlur != null && this.props.onBlur(e)
|
||||||
|
}
|
||||||
|
this.loadStyleHandler = (e) => {
|
||||||
|
this.editor.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.value = this.props.value
|
||||||
|
this.editor = CodeMirror(this.refs.root, {
|
||||||
|
value: this.props.value,
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true,
|
||||||
|
theme: this.props.theme,
|
||||||
|
indentUnit: this.props.indentSize,
|
||||||
|
tabSize: this.props.indentSize,
|
||||||
|
indentWithTabs: this.props.indentType !== 'space',
|
||||||
|
keyMap: 'sublime',
|
||||||
|
inputStyle: 'textarea',
|
||||||
|
extraKeys: {
|
||||||
|
Tab: function (cm) {
|
||||||
|
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||||
|
else {
|
||||||
|
if (cm.getOption('indentWithTabs')) {
|
||||||
|
cm.execCommand('insertTab')
|
||||||
|
} else {
|
||||||
|
cm.execCommand('insertSoftTab')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Cmd-T': function (cm) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setMode(this.props.mode)
|
||||||
|
|
||||||
|
this.editor.on('blur', this.blurHandler)
|
||||||
|
this.editor.on('change', this.changeHandler)
|
||||||
|
|
||||||
|
let editorTheme = document.getElementById('editorTheme')
|
||||||
|
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.editor.off('blur', this.blurHandler)
|
||||||
|
this.editor.off('change', this.changeHandler)
|
||||||
|
let editorTheme = document.getElementById('editorTheme')
|
||||||
|
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps, prevState) {
|
||||||
|
let needRefresh = false
|
||||||
|
if (prevProps.mode !== this.props.mode) {
|
||||||
|
this.setMode(this.props.mode)
|
||||||
|
}
|
||||||
|
if (prevProps.theme !== this.props.theme) {
|
||||||
|
this.editor.setOption('theme', this.props.theme)
|
||||||
|
// editor should be refreshed after css loaded
|
||||||
|
}
|
||||||
|
if (prevProps.fontSize !== this.props.fontSize) {
|
||||||
|
needRefresh = true
|
||||||
|
}
|
||||||
|
if (prevProps.fontFamily !== this.props.fontFamily) {
|
||||||
|
needRefresh = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.indentSize !== this.props.indentSize) {
|
||||||
|
this.editor.setOption('indentUnit', this.props.indentSize)
|
||||||
|
this.editor.setOption('tabSize', this.props.indentSize)
|
||||||
|
}
|
||||||
|
if (prevProps.indentType !== this.props.indentType) {
|
||||||
|
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needRefresh) {
|
||||||
|
this.editor.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMode (mode) {
|
||||||
|
let syntax = CodeMirror.findModeByName(pass(mode))
|
||||||
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
|
|
||||||
|
this.editor.setOption('mode', syntax.mime)
|
||||||
|
CodeMirror.autoLoadMode(this.editor, syntax.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange (e) {
|
||||||
|
this.value = this.editor.getValue()
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveCursorTo (row, col) {
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToLine (num) {
|
||||||
|
}
|
||||||
|
|
||||||
|
focus () {
|
||||||
|
this.editor.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
blur () {
|
||||||
|
this.editor.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
reload () {
|
||||||
|
// Change event shouldn't be fired when switch note
|
||||||
|
this.editor.off('change', this.changeHandler)
|
||||||
|
this.value = this.props.value
|
||||||
|
this.editor.setValue(this.props.value)
|
||||||
|
this.editor.clearHistory()
|
||||||
|
this.editor.on('change', this.changeHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue (value) {
|
||||||
|
let cursor = this.editor.getCursor()
|
||||||
|
this.editor.setValue(value)
|
||||||
|
this.editor.setCursor(cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { className, fontFamily, fontSize } = this.props
|
||||||
|
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||||
|
? [fontFamily].concat(defaultEditorFontFamily)
|
||||||
|
: defaultEditorFontFamily
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className == null
|
||||||
|
? 'CodeEditor'
|
||||||
|
: `CodeEditor ${className}`
|
||||||
|
}
|
||||||
|
ref='root'
|
||||||
|
tabIndex='-1'
|
||||||
|
style={{
|
||||||
|
fontFamily: fontFamily.join(', '),
|
||||||
|
fontSize: fontSize
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeEditor.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
onBlur: PropTypes.func,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
readOnly: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeEditor.defaultProps = {
|
||||||
|
readOnly: false,
|
||||||
|
theme: 'xcode',
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Monaco, Consolas',
|
||||||
|
indentSize: 4,
|
||||||
|
indentType: 'space'
|
||||||
|
}
|
||||||
208
browser/components/MarkdownEditor.js
Normal file
208
browser/components/MarkdownEditor.js
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './MarkdownEditor.styl'
|
||||||
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
|
|
||||||
|
class MarkdownEditor extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
status: 'PREVIEW',
|
||||||
|
renderValue: props.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.value = this.refs.code.value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
this.value = this.refs.code.value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (props) {
|
||||||
|
if (props.value !== this.props.value) {
|
||||||
|
this.queueRendering(props.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.cancelQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
queueRendering (value) {
|
||||||
|
clearTimeout(this.renderTimer)
|
||||||
|
this.renderTimer = setTimeout(() => {
|
||||||
|
this.renderPreview(value)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelQueue () {
|
||||||
|
clearTimeout(this.renderTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPreview (value) {
|
||||||
|
this.setState({
|
||||||
|
renderValue: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange (e) {
|
||||||
|
this.value = this.refs.code.value
|
||||||
|
this.props.onChange(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextMenu (e) {
|
||||||
|
let { config } = this.props
|
||||||
|
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||||
|
let newStatus = this.state.status === 'PREVIEW'
|
||||||
|
? 'CODE'
|
||||||
|
: 'PREVIEW'
|
||||||
|
this.setState({
|
||||||
|
status: newStatus
|
||||||
|
}, () => {
|
||||||
|
if (newStatus === 'CODE') {
|
||||||
|
this.refs.code.focus()
|
||||||
|
} else {
|
||||||
|
this.refs.code.blur()
|
||||||
|
this.refs.preview.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur (e) {
|
||||||
|
let { config } = this.props
|
||||||
|
if (config.editor.switchPreview === 'BLUR') {
|
||||||
|
let cursorPosition = this.refs.code.editor.getCursor()
|
||||||
|
this.setState({
|
||||||
|
status: 'PREVIEW'
|
||||||
|
}, () => {
|
||||||
|
this.refs.preview.focus()
|
||||||
|
this.refs.preview.scrollTo(cursorPosition.line)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePreviewMouseDown (e) {
|
||||||
|
this.previewMouseDownedAt = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePreviewMouseUp (e) {
|
||||||
|
let { config } = this.props
|
||||||
|
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
|
||||||
|
this.setState({
|
||||||
|
status: 'CODE'
|
||||||
|
}, () => {
|
||||||
|
this.refs.code.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheckboxClick (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
let idMatch = /checkbox-([0-9]+)/
|
||||||
|
let checkedMatch = /\[x\]/i
|
||||||
|
let uncheckedMatch = /\[ \]/
|
||||||
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
|
let lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
|
let lines = this.refs.code.value
|
||||||
|
.split('\n')
|
||||||
|
|
||||||
|
let targetLine = lines[lineIndex]
|
||||||
|
|
||||||
|
if (targetLine.match(checkedMatch)) {
|
||||||
|
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||||
|
}
|
||||||
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
|
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||||
|
}
|
||||||
|
this.refs.code.setValue(lines.join('\n'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus () {
|
||||||
|
if (this.state.status === 'PREVIEW') {
|
||||||
|
this.setState({
|
||||||
|
status: 'CODE'
|
||||||
|
}, () => {
|
||||||
|
this.refs.code.focus()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.refs.code.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reload () {
|
||||||
|
this.refs.code.reload()
|
||||||
|
this.cancelQueue()
|
||||||
|
this.renderPreview(this.props.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { className, value, config } = this.props
|
||||||
|
|
||||||
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
|
|
||||||
|
let previewStyle = {}
|
||||||
|
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className == null
|
||||||
|
? 'MarkdownEditor'
|
||||||
|
: `MarkdownEditor ${className}`
|
||||||
|
}
|
||||||
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
|
tabIndex='-1'
|
||||||
|
>
|
||||||
|
<CodeEditor styleName='codeEditor'
|
||||||
|
ref='code'
|
||||||
|
mode='GitHub Flavored Markdown'
|
||||||
|
value={value}
|
||||||
|
theme={config.editor.theme}
|
||||||
|
fontFamily={config.editor.fontFamily}
|
||||||
|
fontSize={editorFontSize}
|
||||||
|
indentType={config.editor.indentType}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
|
onChange={(e) => this.handleChange(e)}
|
||||||
|
onBlur={(e) => this.handleBlur(e)}
|
||||||
|
/>
|
||||||
|
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||||
|
? 'preview'
|
||||||
|
: 'preview--hide'
|
||||||
|
}
|
||||||
|
style={previewStyle}
|
||||||
|
theme={config.ui.theme}
|
||||||
|
fontSize={config.preview.fontSize}
|
||||||
|
fontFamily={config.preview.fontFamily}
|
||||||
|
codeBlockTheme={config.preview.codeBlockTheme}
|
||||||
|
codeBlockFontFamily={config.editor.fontFamily}
|
||||||
|
lineNumber={config.preview.lineNumber}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
|
ref='preview'
|
||||||
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
|
tabIndex='0'
|
||||||
|
value={this.state.renderValue}
|
||||||
|
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||||
|
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
|
||||||
|
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownEditor.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
ignorePreviewPointerEvents: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(MarkdownEditor, styles)
|
||||||
23
browser/components/MarkdownEditor.styl
Normal file
23
browser/components/MarkdownEditor.styl
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.root
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.codeEditor
|
||||||
|
absolute top bottom left right
|
||||||
|
|
||||||
|
.codeEditor--hide
|
||||||
|
@extend .codeEditor
|
||||||
|
|
||||||
|
.preview
|
||||||
|
display block
|
||||||
|
absolute top bottom left right
|
||||||
|
z-index 100
|
||||||
|
background-color white
|
||||||
|
height 100%
|
||||||
|
width 100%
|
||||||
|
|
||||||
|
.preview--hide
|
||||||
|
@extend .preview
|
||||||
|
z-index 0
|
||||||
|
opacity 0
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
300
browser/components/MarkdownPreview.js
Normal file
300
browser/components/MarkdownPreview.js
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import markdown from 'browser/lib/markdown'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import CodeMirror from 'codemirror'
|
||||||
|
import consts from 'browser/lib/consts'
|
||||||
|
import Raphael from 'raphael'
|
||||||
|
import flowchart from 'flowchart'
|
||||||
|
import SequenceDiagram from 'sequence-diagram'
|
||||||
|
|
||||||
|
function decodeHTMLEntities (text) {
|
||||||
|
var entities = [
|
||||||
|
['apos', '\''],
|
||||||
|
['amp', '&'],
|
||||||
|
['lt', '<'],
|
||||||
|
['gt', '>']
|
||||||
|
]
|
||||||
|
|
||||||
|
for (var i = 0, max = entities.length; i < max; ++i) {
|
||||||
|
text = text.replace(new RegExp('&' + entities[i][0] + ';', 'g'), entities[i][1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const { remote } = require('electron')
|
||||||
|
const { app } = remote
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||||
|
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
||||||
|
? app.getAppPath()
|
||||||
|
: path.resolve())
|
||||||
|
|
||||||
|
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
|
||||||
|
return `
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lato';
|
||||||
|
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
||||||
|
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
||||||
|
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
${markdownStyle}
|
||||||
|
body {
|
||||||
|
font-family: ${fontFamily.join(', ')};
|
||||||
|
font-size: ${fontSize}px;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: ${codeBlockFontFamily.join(', ')};
|
||||||
|
}
|
||||||
|
.lineNumber {
|
||||||
|
${lineNumber && 'display: block !important;'}
|
||||||
|
font-family: ${codeBlockFontFamily.join(', ')};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const { shell } = require('electron')
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||||
|
if (!OSX) {
|
||||||
|
defaultFontFamily.unshift('\'Microsoft YaHei\'')
|
||||||
|
defaultFontFamily.unshift('meiryo')
|
||||||
|
}
|
||||||
|
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
|
|
||||||
|
export default class MarkdownPreview extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||||
|
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||||
|
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||||
|
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||||
|
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePreviewAnchorClick (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
let anchor = e.target.closest('a')
|
||||||
|
let href = anchor.getAttribute('href')
|
||||||
|
if (_.isString(href) && href.match(/^#/)) {
|
||||||
|
let targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
|
||||||
|
if (targetElement != null) {
|
||||||
|
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shell.openExternal(href)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheckboxClick (e) {
|
||||||
|
this.props.onCheckboxClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextMenu (e) {
|
||||||
|
this.props.onContextMenu(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown (e) {
|
||||||
|
if (e.target != null) {
|
||||||
|
switch (e.target.tagName) {
|
||||||
|
case 'A':
|
||||||
|
case 'INPUT':
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp (e) {
|
||||||
|
if (e.target != null && e.target.tagName === 'A') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||||
|
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||||
|
|
||||||
|
this.refs.root.contentWindow.document.head.innerHTML = `
|
||||||
|
<style id='style'></style>
|
||||||
|
<link rel="stylesheet" href="${appPath}/compiled/katex-style.css">
|
||||||
|
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
|
||||||
|
<link rel="stylesheet" id="codeTheme">
|
||||||
|
`
|
||||||
|
this.rewriteIframe()
|
||||||
|
this.applyStyle()
|
||||||
|
|
||||||
|
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||||
|
if (prevProps.fontFamily !== this.props.fontFamily ||
|
||||||
|
prevProps.fontSize !== this.props.fontSize ||
|
||||||
|
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||||
|
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||||
|
prevProps.lineNumber !== this.props.lineNumber ||
|
||||||
|
prevProps.theme !== this.props.theme) {
|
||||||
|
this.applyStyle()
|
||||||
|
this.rewriteIframe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyStyle () {
|
||||||
|
let { fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
|
||||||
|
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||||
|
? [fontFamily].concat(defaultFontFamily)
|
||||||
|
: defaultFontFamily
|
||||||
|
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||||
|
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
|
||||||
|
: defaultCodeBlockFontFamily
|
||||||
|
|
||||||
|
this.setCodeTheme(codeBlockTheme)
|
||||||
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCodeTheme (theme) {
|
||||||
|
theme = consts.THEMES.some((_theme) => _theme === theme)
|
||||||
|
? theme
|
||||||
|
: 'default'
|
||||||
|
this.getWindow().document.getElementById('codeTheme').href = `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||||
|
}
|
||||||
|
|
||||||
|
rewriteIframe () {
|
||||||
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||||
|
el.removeEventListener('click', this.anchorClickHandler)
|
||||||
|
})
|
||||||
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||||
|
el.removeEventListener('click', this.checkboxClickHandler)
|
||||||
|
})
|
||||||
|
|
||||||
|
let { value, theme, indentSize, codeBlockTheme } = this.props
|
||||||
|
|
||||||
|
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||||
|
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
||||||
|
|
||||||
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||||
|
el.addEventListener('click', this.anchorClickHandler)
|
||||||
|
})
|
||||||
|
|
||||||
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||||
|
el.addEventListener('click', this.checkboxClickHandler)
|
||||||
|
})
|
||||||
|
|
||||||
|
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||||
|
? codeBlockTheme
|
||||||
|
: 'default'
|
||||||
|
|
||||||
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
||||||
|
let syntax = CodeMirror.findModeByName(el.className)
|
||||||
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
|
CodeMirror.requireMode(syntax.mode, () => {
|
||||||
|
let content = el.innerHTML
|
||||||
|
el.innerHTML = ''
|
||||||
|
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
|
||||||
|
CodeMirror.runMode(content, syntax.mime, el, {
|
||||||
|
tabSize: indentSize
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
let opts = {}
|
||||||
|
// if (this.props.theme === 'dark') {
|
||||||
|
// opts['font-color'] = '#DDD'
|
||||||
|
// opts['line-color'] = '#DDD'
|
||||||
|
// opts['element-color'] = '#DDD'
|
||||||
|
// opts['fill'] = '#3A404C'
|
||||||
|
// }
|
||||||
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
|
||||||
|
Raphael.setWindow(this.getWindow())
|
||||||
|
try {
|
||||||
|
let diagram = flowchart.parse(decodeHTMLEntities(el.innerHTML))
|
||||||
|
el.innerHTML = ''
|
||||||
|
diagram.drawSVG(el, opts)
|
||||||
|
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||||
|
el.addEventListener('click', this.anchorClickHandler)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
el.className = 'flowchart-error'
|
||||||
|
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
|
||||||
|
Raphael.setWindow(this.getWindow())
|
||||||
|
try {
|
||||||
|
let diagram = SequenceDiagram.parse(decodeHTMLEntities(el.innerHTML))
|
||||||
|
el.innerHTML = ''
|
||||||
|
diagram.drawSVG(el, {theme: 'simple'})
|
||||||
|
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||||
|
el.addEventListener('click', this.anchorClickHandler)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
el.className = 'sequence-error'
|
||||||
|
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
focus () {
|
||||||
|
this.refs.root.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
getWindow () {
|
||||||
|
return this.refs.root.contentWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTo (targetRow) {
|
||||||
|
let blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
||||||
|
|
||||||
|
for (let index = 0; index < blocks.length; index++) {
|
||||||
|
let block = blocks[index]
|
||||||
|
let row = parseInt(block.getAttribute('data-line'))
|
||||||
|
if (row > targetRow || index === blocks.length - 1) {
|
||||||
|
block = blocks[index - 1]
|
||||||
|
block != null && this.getWindow().scrollTo(0, block.offsetTop)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { className, style, tabIndex } = this.props
|
||||||
|
return (
|
||||||
|
<iframe className={className != null
|
||||||
|
? 'MarkdownPreview ' + className
|
||||||
|
: 'MarkdownPreview'
|
||||||
|
}
|
||||||
|
style={style}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
ref='root'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownPreview.propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onDoubleClick: PropTypes.func,
|
||||||
|
onMouseUp: PropTypes.func,
|
||||||
|
onMouseDown: PropTypes.func,
|
||||||
|
className: PropTypes.string,
|
||||||
|
value: PropTypes.string
|
||||||
|
}
|
||||||
24
browser/components/ProfileImage.js
Normal file
24
browser/components/ProfileImage.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import md5 from 'md5'
|
||||||
|
|
||||||
|
export default class ProfileImage extends React.Component {
|
||||||
|
render () {
|
||||||
|
let className = this.props.className == null ? 'ProfileImage' : 'ProfileImage ' + this.props.className
|
||||||
|
let email = this.props.email != null ? this.props.email : ''
|
||||||
|
let src = 'http://www.gravatar.com/avatar/' + md5(email.trim().toLowerCase()) + '?s=' + this.props.size
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
width={this.props.size}
|
||||||
|
height={this.props.size}
|
||||||
|
src={src}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileImage.propTypes = {
|
||||||
|
email: PropTypes.string,
|
||||||
|
size: PropTypes.string,
|
||||||
|
className: PropTypes.string
|
||||||
|
}
|
||||||
317
browser/components/markdown.styl
Normal file
317
browser/components/markdown.styl
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
global-reset()
|
||||||
|
|
||||||
|
borderColor = #D0D0D0 // using
|
||||||
|
highlightenBorderColor = darken(borderColor, 20%)
|
||||||
|
invBorderColor = #404849
|
||||||
|
brandBorderColor = #3FB399
|
||||||
|
|
||||||
|
focusBorderColor = #369DCD
|
||||||
|
|
||||||
|
buttonBorderColor = #4C4C4C
|
||||||
|
|
||||||
|
lightButtonColor = #898989
|
||||||
|
|
||||||
|
hoverBackgroundColor= transparentify(#444, 4%) // using
|
||||||
|
|
||||||
|
inactiveTextColor = #888 // using
|
||||||
|
textColor = #4D4D4D // using
|
||||||
|
backgroundColor= white
|
||||||
|
fontSize= 14px // using
|
||||||
|
|
||||||
|
shadowColor= #C5C5C5
|
||||||
|
|
||||||
|
invBackgroundColor = #4C4C4C
|
||||||
|
invTextColor = white
|
||||||
|
|
||||||
|
btnColor = #888
|
||||||
|
btnHighlightenColor = #000
|
||||||
|
|
||||||
|
brandColor = #2BAC8F
|
||||||
|
|
||||||
|
popupShadow = 0 0 5px 0 #888
|
||||||
|
|
||||||
|
|
||||||
|
tableHeadBgColor = white
|
||||||
|
tableOddBgColor = #F9F9F9
|
||||||
|
tableEvenBgColor = white
|
||||||
|
|
||||||
|
facebookColor= #3b5998
|
||||||
|
githubBtn= #201F1F
|
||||||
|
|
||||||
|
// using
|
||||||
|
successBackgroundColor= #E0F0D9
|
||||||
|
successTextColor= #3E753F
|
||||||
|
errorBackgroundColor= #F2DEDE
|
||||||
|
errorTextColor= #A64444
|
||||||
|
infoBackgroundColor= #D9EDF7
|
||||||
|
infoTextColor= #34708E
|
||||||
|
|
||||||
|
popupZIndex= 500
|
||||||
|
|
||||||
|
body
|
||||||
|
font-size 16px
|
||||||
|
padding 15px
|
||||||
|
font-family helvetica, arial, sans-serif
|
||||||
|
line-height 1.6
|
||||||
|
overflow-x hidden
|
||||||
|
user-select all
|
||||||
|
.katex
|
||||||
|
font 400 1.2em 'KaTeX_Main'
|
||||||
|
line-height 1.2em
|
||||||
|
white-space nowrap
|
||||||
|
text-indent 0
|
||||||
|
.katex .mfrac>.vlist>span:nth-child(2)
|
||||||
|
top 0 !important
|
||||||
|
.katex-error
|
||||||
|
background-color errorBackgroundColor
|
||||||
|
color errorTextColor
|
||||||
|
padding 5px
|
||||||
|
margin -5px
|
||||||
|
border-radius 5px
|
||||||
|
.flowchart-error, .sequence-error
|
||||||
|
background-color errorBackgroundColor
|
||||||
|
color errorTextColor
|
||||||
|
padding 5px
|
||||||
|
border-radius 5px
|
||||||
|
justify-content left
|
||||||
|
li
|
||||||
|
label.taskListItem
|
||||||
|
margin-left -2em
|
||||||
|
background-color white
|
||||||
|
div.math-rendered
|
||||||
|
text-align center
|
||||||
|
.math-failed
|
||||||
|
background-color alpha(red, 0.1)
|
||||||
|
color darken(red, 15%)
|
||||||
|
padding 5px
|
||||||
|
margin 5px 0
|
||||||
|
border-radius 5px
|
||||||
|
sup
|
||||||
|
position relative
|
||||||
|
top -.4em
|
||||||
|
font-size 0.8em
|
||||||
|
vertical-align top
|
||||||
|
sub
|
||||||
|
position relative
|
||||||
|
bottom -.4em
|
||||||
|
font-size 0.8em
|
||||||
|
vertical-align top
|
||||||
|
a
|
||||||
|
color brandColor
|
||||||
|
text-decoration none
|
||||||
|
padding 5px
|
||||||
|
border-radius 5px
|
||||||
|
margin -5px
|
||||||
|
transition .1s
|
||||||
|
display inline-block
|
||||||
|
img
|
||||||
|
vertical-align sub
|
||||||
|
&:hover
|
||||||
|
color lighten(brandColor, 5%)
|
||||||
|
text-decoration underline
|
||||||
|
background-color alpha(#FFC95C, 0.3)
|
||||||
|
&:visited
|
||||||
|
color brandColor
|
||||||
|
hr
|
||||||
|
border-top none
|
||||||
|
border-bottom solid 1px borderColor
|
||||||
|
margin 15px 0
|
||||||
|
h1, h2, h3, h4, h5, h6
|
||||||
|
font-weight bold
|
||||||
|
h1
|
||||||
|
font-size 2.25em
|
||||||
|
padding-bottom 0.3em
|
||||||
|
line-height 1.2em
|
||||||
|
border-bottom solid 1px borderColor
|
||||||
|
margin 1em 0 0.44em
|
||||||
|
&:first-child
|
||||||
|
margin-top 0
|
||||||
|
h2
|
||||||
|
font-size 1.75em
|
||||||
|
padding-bottom 0.3em
|
||||||
|
line-height 1.225em
|
||||||
|
border-bottom solid 1px borderColor
|
||||||
|
margin 1em 0 0.57em
|
||||||
|
&:first-child
|
||||||
|
margin-top 0
|
||||||
|
h3
|
||||||
|
font-size 1.5em
|
||||||
|
line-height 1.43em
|
||||||
|
margin 1em 0 0.66em
|
||||||
|
h4
|
||||||
|
font-size 1.25em
|
||||||
|
line-height 1.4em
|
||||||
|
margin 1em 0 0.8em
|
||||||
|
h5
|
||||||
|
font-size 1em
|
||||||
|
line-height 1.4em
|
||||||
|
margin 1em 0 1em
|
||||||
|
h6
|
||||||
|
font-size 1em
|
||||||
|
line-height 1.4em
|
||||||
|
margin 1em 0 1em
|
||||||
|
color #777
|
||||||
|
p
|
||||||
|
line-height 1.6em
|
||||||
|
margin 0 0 1em
|
||||||
|
white-space pre-line
|
||||||
|
img
|
||||||
|
max-width 100%
|
||||||
|
strong, b
|
||||||
|
font-weight bold
|
||||||
|
em, i
|
||||||
|
font-style italic
|
||||||
|
s, del, strike
|
||||||
|
text-decoration line-through
|
||||||
|
u
|
||||||
|
text-decoration underline
|
||||||
|
blockquote
|
||||||
|
border-left solid 4px brandBorderColor
|
||||||
|
margin 0 0 1em
|
||||||
|
padding 0 25px
|
||||||
|
ul
|
||||||
|
list-style-type disc
|
||||||
|
padding-left 2em
|
||||||
|
margin-bottom 1em
|
||||||
|
li
|
||||||
|
display list-item
|
||||||
|
p
|
||||||
|
margin 0
|
||||||
|
&>li>ul, &>li>ol
|
||||||
|
margin 0
|
||||||
|
&>li>ul
|
||||||
|
list-style-type circle
|
||||||
|
&>li>ul
|
||||||
|
list-style-type square
|
||||||
|
ol
|
||||||
|
list-style-type decimal
|
||||||
|
padding-left 2em
|
||||||
|
margin-bottom 1em
|
||||||
|
li
|
||||||
|
display list-item
|
||||||
|
p
|
||||||
|
margin 0
|
||||||
|
&>li>ul, &>li>ol
|
||||||
|
margin 0
|
||||||
|
code
|
||||||
|
padding 0.2em 0.4em
|
||||||
|
background-color #f7f7f7
|
||||||
|
border-radius 3px
|
||||||
|
font-size 1em
|
||||||
|
text-decoration none
|
||||||
|
margin-right 2px
|
||||||
|
pre
|
||||||
|
padding 0.5em !important
|
||||||
|
border solid 1px #D1D1D1
|
||||||
|
border-radius 5px
|
||||||
|
overflow-x auto
|
||||||
|
margin 0 0 1em
|
||||||
|
display flex
|
||||||
|
line-height 1.4em
|
||||||
|
&.flowchart, &.sequence
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
background-color white
|
||||||
|
&.CodeMirror
|
||||||
|
height initial
|
||||||
|
&>code
|
||||||
|
flex 1
|
||||||
|
overflow-x auto
|
||||||
|
code
|
||||||
|
background-color inherit
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
border none
|
||||||
|
border-radius 0
|
||||||
|
&>span.lineNumber
|
||||||
|
display none
|
||||||
|
font-size 1em
|
||||||
|
padding 0.5em 0
|
||||||
|
margin -0.5em 0.5em -0.5em -0.5em
|
||||||
|
border-right 1px solid
|
||||||
|
text-align right
|
||||||
|
border-top-left-radius 4px
|
||||||
|
border-bottom-left-radius 4px
|
||||||
|
&.CodeMirror-gutters
|
||||||
|
position initial
|
||||||
|
top initial
|
||||||
|
left initial
|
||||||
|
min-height 0 !important
|
||||||
|
&>span
|
||||||
|
display block
|
||||||
|
padding 0 .5em 0
|
||||||
|
table
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
margin 0 0 1em
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
background-color tableHeadBgColor
|
||||||
|
th
|
||||||
|
border-style solid
|
||||||
|
padding 6px 13px
|
||||||
|
line-height 1.6
|
||||||
|
border-width 1px 0 2px 1px
|
||||||
|
border-color borderColor
|
||||||
|
font-weight bold
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px borderColor
|
||||||
|
tbody
|
||||||
|
tr:nth-child(2n + 1)
|
||||||
|
background-color tableOddBgColor
|
||||||
|
tr:nth-child(2n)
|
||||||
|
background-color tableEvenBgColor
|
||||||
|
td
|
||||||
|
border-style solid
|
||||||
|
padding 6px 13px
|
||||||
|
line-height 1.6
|
||||||
|
border-width 0 0 1px 1px
|
||||||
|
border-color borderColor
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px borderColor
|
||||||
|
|
||||||
|
themeDarkBackground = darken(#21252B, 10%)
|
||||||
|
themeDarkText = #DDDDDD
|
||||||
|
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||||
|
themeDarkPreview = #282C34
|
||||||
|
themeDarkTableOdd = themeDarkPreview
|
||||||
|
themeDarkTableEven = darken(themeDarkPreview, 10%)
|
||||||
|
themeDarkTableHead = themeDarkTableEven
|
||||||
|
themeDarkTableBorder = themeDarkBorder
|
||||||
|
themeDarkModalButtonDefault = themeDarkPreview
|
||||||
|
themeDarkModalButtonDanger = #BF360C
|
||||||
|
body[data-theme="dark"]
|
||||||
|
color themeDarkText
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color themeDarkPreview
|
||||||
|
a:hover
|
||||||
|
background-color alpha(lighten(brandColor, 30%), 0.2) !important
|
||||||
|
|
||||||
|
code
|
||||||
|
border-color darken(themeDarkBorder, 10%)
|
||||||
|
background-color lighten(themeDarkPreview, 10%)
|
||||||
|
|
||||||
|
pre
|
||||||
|
border-color lighten(#21252B, 20%)
|
||||||
|
code
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
label.taskListItem
|
||||||
|
background-color themeDarkPreview
|
||||||
|
table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
background-color themeDarkTableHead
|
||||||
|
th
|
||||||
|
border-color themeDarkTableBorder
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px themeDarkTableBorder
|
||||||
|
tbody
|
||||||
|
tr:nth-child(2n + 1)
|
||||||
|
background-color themeDarkTableOdd
|
||||||
|
tr:nth-child(2n)
|
||||||
|
background-color themeDarkTableEven
|
||||||
|
td
|
||||||
|
border-color themeDarkTableBorder
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px themeDarkTableBorder
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var CodeViewer = require('../../main/Components/CodeViewer')
|
|
||||||
|
|
||||||
var Markdown = require('../../main/Mixins/Markdown')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [Markdown],
|
|
||||||
propTypes: {
|
|
||||||
currentArticle: React.PropTypes.object
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var article = this.props.currentArticle
|
|
||||||
|
|
||||||
if (article != null) {
|
|
||||||
if (article.type === 'code') {
|
|
||||||
return (
|
|
||||||
<div className='FinderDetail'>
|
|
||||||
<div className='header'><i className='fa fa-code fa-fw'/> {article.description}</div>
|
|
||||||
<div className='content'>
|
|
||||||
<CodeViewer code={article.content} mode={article.mode}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else if (article.type === 'note') {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='FinderDetail'>
|
|
||||||
<div className='header'><i className='fa fa-file-text-o fa-fw'/> {article.title}</div>
|
|
||||||
<div className='content'>
|
|
||||||
<div className='marked' dangerouslySetInnerHTML={{__html: ' ' + this.markdown(article.content)}}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className='FinderDetail'>
|
|
||||||
<div className='nothing'>Nothing selected</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
onChange: React.PropTypes.func,
|
|
||||||
search: React.PropTypes.string
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='FinderInput'>
|
|
||||||
<input value={this.props.search} onChange={this.props.onChange} type='text'/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
articles: React.PropTypes.arrayOf,
|
|
||||||
currentArticle: React.PropTypes.shape({
|
|
||||||
id: React.PropTypes.number,
|
|
||||||
type: React.PropTypes.string
|
|
||||||
}),
|
|
||||||
selectArticle: React.PropTypes.func
|
|
||||||
},
|
|
||||||
componentDidUpdate: function () {
|
|
||||||
var index = this.props.articles.indexOf(this.props.currentArticle)
|
|
||||||
var el = React.findDOMNode(this)
|
|
||||||
var li = el.querySelectorAll('li')[index]
|
|
||||||
|
|
||||||
if (li == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
|
|
||||||
if (overflowBelow) {
|
|
||||||
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
|
|
||||||
}
|
|
||||||
var overflowAbove = el.scrollTop > li.offsetTop
|
|
||||||
if (overflowAbove) {
|
|
||||||
el.scrollTop = li.offsetTop
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleArticleClick: function (article) {
|
|
||||||
return function () {
|
|
||||||
this.props.selectArticle(article)
|
|
||||||
}.bind(this)
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var list = this.props.articles.map(function (article) {
|
|
||||||
if (article == null) {
|
|
||||||
return (
|
|
||||||
<li className={isActive ? 'active' : ''}>
|
|
||||||
<div className='articleItem'>Undefined</div>
|
|
||||||
<div className='divider'/>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var isActive = this.props.currentArticle != null && (article.type === this.props.currentArticle.type && article.id === this.props.currentArticle.id)
|
|
||||||
if (article.type === 'code') {
|
|
||||||
return (
|
|
||||||
<li onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
|
|
||||||
<div className='articleItem'><i className='fa fa-code fa-fw'/> {article.description}</div>
|
|
||||||
<div className='divider'/>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (article.type === 'note') {
|
|
||||||
return (
|
|
||||||
<li onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
|
|
||||||
<div className='articleItem'><i className='fa fa-file-text-o fa-fw'/> {article.title}</div>
|
|
||||||
<div className='divider'/>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<li className={isActive ? 'active' : ''}>
|
|
||||||
<div className='articleItem'>Undefined</div>
|
|
||||||
<div className='divider'/>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='FinderList'>
|
|
||||||
<ul>
|
|
||||||
{list}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
110
browser/finder/FinderMain.styl
Normal file
110
browser/finder/FinderMain.styl
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
$search-height = 50px
|
||||||
|
$nav-width = 175px
|
||||||
|
$list-width = 250px
|
||||||
|
|
||||||
|
.root
|
||||||
|
absolute top left right bottom
|
||||||
|
|
||||||
|
.search
|
||||||
|
height $search-height
|
||||||
|
padding 10px
|
||||||
|
box-sizing border-box
|
||||||
|
border-bottom $ui-border
|
||||||
|
text-align center
|
||||||
|
|
||||||
|
.search-input
|
||||||
|
height 30px
|
||||||
|
width 100%
|
||||||
|
margin 0 auto
|
||||||
|
font-size 18px
|
||||||
|
border none
|
||||||
|
outline none
|
||||||
|
text-align center
|
||||||
|
background-color transparent
|
||||||
|
.result
|
||||||
|
absolute left right bottom
|
||||||
|
top $search-height
|
||||||
|
|
||||||
|
.result-nav
|
||||||
|
user-select none
|
||||||
|
absolute left top bottom
|
||||||
|
width $nav-width
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
|
||||||
|
.result-nav-filter
|
||||||
|
margin-bottom 5px
|
||||||
|
|
||||||
|
.result-nav-filter-option
|
||||||
|
height 25px
|
||||||
|
line-height 25px
|
||||||
|
padding 0 10px
|
||||||
|
label
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
.result-nav-menu
|
||||||
|
navButtonColor()
|
||||||
|
height 32px
|
||||||
|
padding 0 10px
|
||||||
|
font-size 14px
|
||||||
|
width 100%
|
||||||
|
outline none
|
||||||
|
text-align left
|
||||||
|
line-height 32px
|
||||||
|
box-sizing border-box
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
.result-nav-menu--active
|
||||||
|
@extend .result-nav-menu
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
color $ui-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
|
||||||
|
.result-nav-storageList
|
||||||
|
absolute bottom left right
|
||||||
|
top 80px + 32px + 10px
|
||||||
|
overflow-y auto
|
||||||
|
|
||||||
|
.result-list
|
||||||
|
user-select none
|
||||||
|
absolute top bottom
|
||||||
|
left $nav-width
|
||||||
|
width $list-width
|
||||||
|
border-width 0 1px
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-borderColor
|
||||||
|
box-sizing border-box
|
||||||
|
overflow-y auto
|
||||||
|
|
||||||
|
.result-detail
|
||||||
|
absolute top bottom right
|
||||||
|
left $nav-width + $list-width
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
.search
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
.search-input
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.result-nav
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
label
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.result-nav-menu
|
||||||
|
navDarkButtonColor()
|
||||||
|
|
||||||
|
.result-nav-menu--active
|
||||||
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
color $ui-dark-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
|
||||||
|
.result-list
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.result-detail
|
||||||
|
absolute top bottom right
|
||||||
|
left $nav-width + $list-width
|
||||||
204
browser/finder/NoteDetail.js
Normal file
204
browser/finder/NoteDetail.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './NoteDetail.styl'
|
||||||
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
|
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||||
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
|
import CodeMirror from 'codemirror'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const { clipboard } = electron
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
function pass (name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'ejs':
|
||||||
|
return 'Embedded Javascript'
|
||||||
|
case 'html_ruby':
|
||||||
|
return 'Embedded Ruby'
|
||||||
|
case 'objectivec':
|
||||||
|
return 'Objective C'
|
||||||
|
case 'text':
|
||||||
|
return 'Plain Text'
|
||||||
|
default:
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function notify (title, options) {
|
||||||
|
if (global.process.platform === 'win32') {
|
||||||
|
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||||
|
}
|
||||||
|
return new window.Notification(title, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoteDetail extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
snippetIndex: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.note !== this.props.note) {
|
||||||
|
this.setState({
|
||||||
|
snippetIndex: 0
|
||||||
|
}, () => {
|
||||||
|
if (nextProps.note.type === 'SNIPPET_NOTE') {
|
||||||
|
nextProps.note.snippets.forEach((snippet, index) => {
|
||||||
|
this.refs['code-' + index].reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPriorSnippet () {
|
||||||
|
let { note } = this.props
|
||||||
|
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||||
|
this.setState({
|
||||||
|
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNextSnippet () {
|
||||||
|
let { note } = this.props
|
||||||
|
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||||
|
this.setState({
|
||||||
|
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToClipboard () {
|
||||||
|
let { note } = this.props
|
||||||
|
|
||||||
|
if (note.type === 'MARKDOWN_NOTE') {
|
||||||
|
clipboard.writeText(note.content)
|
||||||
|
} else {
|
||||||
|
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
|
||||||
|
}
|
||||||
|
|
||||||
|
notify('Saved to Clipboard!', {
|
||||||
|
body: 'Paste it wherever you want!',
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabButtonClick (e, index) {
|
||||||
|
this.setState({
|
||||||
|
snippetIndex: index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { note, config } = this.props
|
||||||
|
if (note == null) {
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
|
|
||||||
|
if (note.type === 'SNIPPET_NOTE') {
|
||||||
|
let tabList = note.snippets.map((snippet, index) => {
|
||||||
|
let isActive = this.state.snippetIndex === index
|
||||||
|
return <div styleName={isActive
|
||||||
|
? 'tabList-item--active'
|
||||||
|
: 'tabList-item'
|
||||||
|
}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<button styleName='tabList-item-button'
|
||||||
|
onClick={(e) => this.handleTabButtonClick(e, index)}
|
||||||
|
>
|
||||||
|
{snippet.name.trim().length > 0
|
||||||
|
? snippet.name
|
||||||
|
: <span styleName='tabList-item-unnamed'>
|
||||||
|
Unnamed
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
let viewList = note.snippets.map((snippet, index) => {
|
||||||
|
let isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
|
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
||||||
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
|
|
||||||
|
return <div styleName='tabView'
|
||||||
|
key={index}
|
||||||
|
style={{zIndex: isActive ? 5 : 4}}
|
||||||
|
>
|
||||||
|
{snippet.mode === 'markdown'
|
||||||
|
? <MarkdownEditor styleName='tabView-content'
|
||||||
|
config={config}
|
||||||
|
value={snippet.content}
|
||||||
|
ref={'code-' + index}
|
||||||
|
/>
|
||||||
|
: <CodeEditor styleName='tabView-content'
|
||||||
|
mode={snippet.mode}
|
||||||
|
value={snippet.content}
|
||||||
|
theme={config.editor.theme}
|
||||||
|
fontFamily={config.editor.fontFamily}
|
||||||
|
fontSize={editorFontSize}
|
||||||
|
indentType={config.editor.indentType}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
|
readOnly
|
||||||
|
ref={'code-' + index}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
<div styleName='description'>
|
||||||
|
<textarea styleName='description-textarea'
|
||||||
|
style={{
|
||||||
|
fontFamily: config.preview.fontFamily,
|
||||||
|
fontSize: parseInt(config.preview.fontSize, 10)
|
||||||
|
}}
|
||||||
|
ref='description'
|
||||||
|
placeholder='Description...'
|
||||||
|
value={note.description}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div styleName='tabList'>
|
||||||
|
{tabList}
|
||||||
|
</div>
|
||||||
|
{viewList}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarkdownPreview styleName='root'
|
||||||
|
theme={config.ui.theme}
|
||||||
|
fontSize={config.preview.fontSize}
|
||||||
|
fontFamily={config.preview.fontFamily}
|
||||||
|
codeBlockTheme={config.preview.codeBlockTheme}
|
||||||
|
codeBlockFontFamily={config.editor.fontFamily}
|
||||||
|
lineNumber={config.preview.lineNumber}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
|
value={note.content}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteDetail.propTypes = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(NoteDetail, styles)
|
||||||
109
browser/finder/NoteDetail.styl
Normal file
109
browser/finder/NoteDetail.styl
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
.root
|
||||||
|
absolute top bottom left right
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
|
||||||
|
.description
|
||||||
|
absolute top left right
|
||||||
|
height 80px
|
||||||
|
border-bottom $ui-border
|
||||||
|
box-sizing border-box
|
||||||
|
|
||||||
|
.description-textarea
|
||||||
|
display block
|
||||||
|
height 100%
|
||||||
|
width 100%
|
||||||
|
resize none
|
||||||
|
border none
|
||||||
|
padding 10px
|
||||||
|
line-height 1.6
|
||||||
|
box-sizing border-box
|
||||||
|
|
||||||
|
.tabList
|
||||||
|
absolute left right
|
||||||
|
top 80px
|
||||||
|
box-sizing border-box
|
||||||
|
height 30px
|
||||||
|
border-bottom $ui-border
|
||||||
|
display flex
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
|
||||||
|
.tabList-item
|
||||||
|
position relative
|
||||||
|
flex 1
|
||||||
|
border-right $ui-border
|
||||||
|
|
||||||
|
.tabList-item--active
|
||||||
|
@extend .tabList-item
|
||||||
|
.tabList-item-button
|
||||||
|
border-color $brand-color
|
||||||
|
|
||||||
|
.tabList-item-button
|
||||||
|
width 100%
|
||||||
|
height 29px
|
||||||
|
navButtonColor()
|
||||||
|
outline none
|
||||||
|
border-left 4px solid transparent
|
||||||
|
|
||||||
|
.tabList-item-deleteButton
|
||||||
|
position absolute
|
||||||
|
top 5px
|
||||||
|
height 20px
|
||||||
|
right 5px
|
||||||
|
width 20px
|
||||||
|
text-align center
|
||||||
|
border none
|
||||||
|
padding 0
|
||||||
|
color transparent
|
||||||
|
background-color transparent
|
||||||
|
border-radius 2px
|
||||||
|
.tabList-plusButton
|
||||||
|
navButtonColor()
|
||||||
|
width 30px
|
||||||
|
|
||||||
|
.tabView
|
||||||
|
absolute left right bottom
|
||||||
|
top 110px
|
||||||
|
|
||||||
|
.tabView-content
|
||||||
|
absolute top left right bottom
|
||||||
|
box-sizing border-box
|
||||||
|
height 100%
|
||||||
|
width 100%
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.description
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.description-textarea
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
color white
|
||||||
|
|
||||||
|
.tabList
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
border-bottom-color $ui-dark-borderColor
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
.tabList-item
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
.tabList-item--active
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
.tabList-item-button
|
||||||
|
border-color $brand-color
|
||||||
|
.tabList-item-button
|
||||||
|
navDarkButtonColor()
|
||||||
|
border-left 4px solid transparent
|
||||||
|
.tabList-plusButton
|
||||||
|
navDarkButtonColor()
|
||||||
|
|
||||||
|
.tabView-top
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
.tabView-top-name
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
color $ui-dark-text-color
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
.tabView-top-mode
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
background-color $dark-default-button-background
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
85
browser/finder/NoteItem.js
Normal file
85
browser/finder/NoteItem.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './NoteItem.styl'
|
||||||
|
import moment from 'moment'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
class NoteItem extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick (e) {
|
||||||
|
this.props.onClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { note, folder, storage, isActive } = this.props
|
||||||
|
|
||||||
|
let tagList = _.isArray(note.tags)
|
||||||
|
? note.tags.map((tag) => {
|
||||||
|
return (
|
||||||
|
<span styleName='bottom-tagList-item'
|
||||||
|
key={tag}>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
return (
|
||||||
|
<div styleName={isActive
|
||||||
|
? 'root--active'
|
||||||
|
: 'root'
|
||||||
|
}
|
||||||
|
key={note.storage + '-' + note.key}
|
||||||
|
onClick={(e) => this.handleClick(e)}
|
||||||
|
>
|
||||||
|
<div styleName='info'>
|
||||||
|
<div styleName='info-left'>
|
||||||
|
<span styleName='info-left-folder'
|
||||||
|
style={{borderColor: folder.color}}
|
||||||
|
>
|
||||||
|
{folder.name}
|
||||||
|
<span styleName='info-left-folder-surfix'>in {storage.name}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='title'>
|
||||||
|
{note.type === 'SNIPPET_NOTE'
|
||||||
|
? <i styleName='title-icon' className='fa fa-fw fa-code' />
|
||||||
|
: <i styleName='title-icon' className='fa fa-fw fa-file-text-o' />
|
||||||
|
}
|
||||||
|
{note.title.trim().length > 0
|
||||||
|
? note.title
|
||||||
|
: <span styleName='title-empty'>Empty</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='bottom'>
|
||||||
|
<i styleName='bottom-tagIcon'
|
||||||
|
className='fa fa-tags fa-fw'
|
||||||
|
/>
|
||||||
|
<div styleName='bottom-tagList'>
|
||||||
|
{tagList.length > 0
|
||||||
|
? tagList
|
||||||
|
: <span styleName='bottom-tagList-empty'>Not tagged yet</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='bottom-time'>
|
||||||
|
{moment(note.updatedAt).fromNow()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteItem.propTypes = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(NoteItem, styles)
|
||||||
179
browser/finder/NoteItem.styl
Normal file
179
browser/finder/NoteItem.styl
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
.root
|
||||||
|
position relative
|
||||||
|
border-bottom $ui-border
|
||||||
|
padding 2px 5px
|
||||||
|
user-select none
|
||||||
|
cursor pointer
|
||||||
|
transition background-color 0.15s
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-active-color, 20%)
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
@extend .root
|
||||||
|
background-color $ui-active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-active-color
|
||||||
|
color white
|
||||||
|
.info-left-folder
|
||||||
|
.info-left-folder-surfix
|
||||||
|
.title
|
||||||
|
.title-icon
|
||||||
|
.title-empty
|
||||||
|
.bottom-tagIcon
|
||||||
|
.bottom-tagList-item
|
||||||
|
.bottom-tagList-empty
|
||||||
|
.bottom-time
|
||||||
|
color white
|
||||||
|
.bottom-tagList-item
|
||||||
|
color white
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
.border
|
||||||
|
absolute top bottom left right
|
||||||
|
border-style solid
|
||||||
|
border-width 2px
|
||||||
|
border-color transparent
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
|
.info
|
||||||
|
height 20px
|
||||||
|
clearfix()
|
||||||
|
font-size 12px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
line-height 20px
|
||||||
|
overflow-y hidden
|
||||||
|
|
||||||
|
.info-left
|
||||||
|
float left
|
||||||
|
overflow ellipsis
|
||||||
|
|
||||||
|
.info-left-folder
|
||||||
|
border-left 4px solid transparent
|
||||||
|
padding 2px 5px
|
||||||
|
color $ui-text-color
|
||||||
|
.info-left-folder-surfix
|
||||||
|
font-size 10px
|
||||||
|
margin-left 5px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
.info-right
|
||||||
|
float right
|
||||||
|
|
||||||
|
.title
|
||||||
|
height 24px
|
||||||
|
box-sizing border-box
|
||||||
|
line-height 24px
|
||||||
|
height 20px
|
||||||
|
line-height 20px
|
||||||
|
padding 0 5px 0 0
|
||||||
|
overflow ellipsis
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
.title-icon
|
||||||
|
font-size 12px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
padding-right 3px
|
||||||
|
|
||||||
|
.title-empty
|
||||||
|
font-weight normal
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.bottom
|
||||||
|
margin-top 2px
|
||||||
|
height 20px
|
||||||
|
font-size 12px
|
||||||
|
line-height 20px
|
||||||
|
overflow ellipsis
|
||||||
|
display flex
|
||||||
|
|
||||||
|
.bottom-tagIcon
|
||||||
|
vertical-align middle
|
||||||
|
color $ui-button-color
|
||||||
|
height 20px
|
||||||
|
line-height 20px
|
||||||
|
|
||||||
|
.bottom-tagList
|
||||||
|
flex 1
|
||||||
|
overflow ellipsis
|
||||||
|
line-height 20px
|
||||||
|
|
||||||
|
.bottom-tagList-item
|
||||||
|
margin 0 4px
|
||||||
|
padding 0 4px
|
||||||
|
height 20px
|
||||||
|
box-sizing border-box
|
||||||
|
border-radius 3px
|
||||||
|
vertical-align middle
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
border-width 0 0 0 3px
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
color $ui-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
|
.bottom-tagList-empty
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
vertical-align middle
|
||||||
|
font-size 10px
|
||||||
|
margin-left 5px
|
||||||
|
|
||||||
|
.bottom-time
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
margin-left 5px
|
||||||
|
font-size 10px
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
@extend .root
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color $ui-active-color
|
||||||
|
.info-left-folder
|
||||||
|
.info-left-folder-surfix
|
||||||
|
.title
|
||||||
|
.title-icon
|
||||||
|
.title-empty
|
||||||
|
.bottom-tagIcon
|
||||||
|
.bottom-tagList-item
|
||||||
|
.bottom-tagList-empty
|
||||||
|
.bottom-time
|
||||||
|
color white
|
||||||
|
.bottom-tagList-item
|
||||||
|
color white
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
.info
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
.info-left-folder
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.info-left-folder-surfix
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
.title
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.title-icon
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
.title-empty
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
.tagList-empty
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
.bottom-tagIcon
|
||||||
|
color $ui-dark-button-color
|
||||||
|
|
||||||
|
.bottom-tagList-item
|
||||||
|
color $ui-dark-text-color
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
|
||||||
|
.bottom-tagList-empty
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
.bottom-time
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
89
browser/finder/NoteList.js
Normal file
89
browser/finder/NoteList.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import NoteItem from './NoteItem'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
class NoteList extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
range: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (this.props.search !== nextProps.search) {
|
||||||
|
this.resetScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
let { index } = this.props
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
let list = this.refs.root
|
||||||
|
let item = list.childNodes[index]
|
||||||
|
if (item == null) return null
|
||||||
|
|
||||||
|
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||||
|
if (overflowBelow) {
|
||||||
|
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||||
|
}
|
||||||
|
let overflowAbove = list.scrollTop > item.offsetTop
|
||||||
|
if (overflowAbove) {
|
||||||
|
list.scrollTop = item.offsetTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetScroll () {
|
||||||
|
this.refs.root.scrollTop = 0
|
||||||
|
this.setState({
|
||||||
|
range: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
let { notes } = this.props
|
||||||
|
|
||||||
|
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
|
||||||
|
this.setState({
|
||||||
|
range: this.state.range + 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { storageMap, notes, index } = this.props
|
||||||
|
|
||||||
|
let notesList = notes
|
||||||
|
.slice(0, 10 + 10 * this.state.range)
|
||||||
|
.map((note, _index) => {
|
||||||
|
let storage = storageMap[note.storage]
|
||||||
|
let folder = _.find(storage.folders, {key: note.folder})
|
||||||
|
return (
|
||||||
|
<NoteItem
|
||||||
|
note={note}
|
||||||
|
key={`${note.storage}-${note.key}`}
|
||||||
|
storage={storage}
|
||||||
|
folder={folder}
|
||||||
|
isActive={index === _index}
|
||||||
|
onClick={(e) => this.props.handleNoteClick(e, _index)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div className={this.props.className}
|
||||||
|
onScroll={(e) => this.handleScroll(e)}
|
||||||
|
ref='root'
|
||||||
|
>
|
||||||
|
{notesList}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteList.propTypes = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoteList
|
||||||
79
browser/finder/StorageSection.js
Normal file
79
browser/finder/StorageSection.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './StorageSection.styl'
|
||||||
|
|
||||||
|
class StorageSection extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isOpen: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleButtonClick (e) {
|
||||||
|
this.setState({
|
||||||
|
isOpen: !this.state.isOpen
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick (e) {
|
||||||
|
let { storage } = this.props
|
||||||
|
this.props.handleStorageButtonClick(e, storage.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFolderClick (e, folder) {
|
||||||
|
let { storage } = this.props
|
||||||
|
this.props.handleFolderButtonClick(e, storage.key, folder.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { storage, filter } = this.props
|
||||||
|
let folderList = storage.folders
|
||||||
|
.map((folder) => {
|
||||||
|
return (
|
||||||
|
<button styleName={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key
|
||||||
|
? 'folderList-item--active'
|
||||||
|
: 'folderList-item'
|
||||||
|
}
|
||||||
|
style={{borderColor: folder.color}}
|
||||||
|
key={folder.key}
|
||||||
|
onClick={(e) => this.handleFolderClick(e, folder)}
|
||||||
|
>
|
||||||
|
{folder.name}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
<div styleName='header'>
|
||||||
|
<button styleName='header-toggleButton'
|
||||||
|
onClick={(e) => this.handleToggleButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className={this.state.isOpen
|
||||||
|
? 'fa fa-caret-down'
|
||||||
|
: 'fa fa-caret-right'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button styleName={filter.type === 'STORAGE' && filter.storage === storage.key
|
||||||
|
? 'header-name--active'
|
||||||
|
: 'header-name'
|
||||||
|
}
|
||||||
|
onClick={(e) => this.handleHeaderClick(e)}
|
||||||
|
>{storage.name}</button>
|
||||||
|
</div>
|
||||||
|
{this.state.isOpen &&
|
||||||
|
<div styleName='folderList'>
|
||||||
|
{folderList}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StorageSection.propTypes = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(StorageSection, styles)
|
||||||
85
browser/finder/StorageSection.styl
Normal file
85
browser/finder/StorageSection.styl
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
.root
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.header
|
||||||
|
height 26px
|
||||||
|
.header-toggleButton
|
||||||
|
absolute top left
|
||||||
|
width 25px
|
||||||
|
height 26px
|
||||||
|
navButtonColor()
|
||||||
|
border none
|
||||||
|
outline none
|
||||||
|
.header-name
|
||||||
|
display block
|
||||||
|
height 26px
|
||||||
|
navButtonColor()
|
||||||
|
padding 0 10px 0 25px
|
||||||
|
font-size 14px
|
||||||
|
width 100%
|
||||||
|
text-align left
|
||||||
|
line-height 26px
|
||||||
|
box-sizing border-box
|
||||||
|
cursor pointer
|
||||||
|
outline none
|
||||||
|
|
||||||
|
.header-name--active
|
||||||
|
@extend .header-name
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
color $ui-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
|
||||||
|
.folderList-item
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
height 26px
|
||||||
|
navButtonColor()
|
||||||
|
padding 0 10px 0 25px
|
||||||
|
font-size 14px
|
||||||
|
width 100%
|
||||||
|
text-align left
|
||||||
|
line-height 26px
|
||||||
|
box-sizing border-box
|
||||||
|
cursor pointer
|
||||||
|
outline none
|
||||||
|
padding 0 10px
|
||||||
|
margin 2px 0
|
||||||
|
height 26px
|
||||||
|
line-height 26px
|
||||||
|
border-width 0 0 0 6px
|
||||||
|
border-style solid
|
||||||
|
border-color transparent
|
||||||
|
|
||||||
|
.folderList-item--active
|
||||||
|
@extend .folderList-item
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
color $ui-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.header-toggleButton
|
||||||
|
navDarkButtonColor()
|
||||||
|
.header-name
|
||||||
|
navDarkButtonColor()
|
||||||
|
|
||||||
|
.header-name--active
|
||||||
|
@extend .header-name
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
color $ui-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
|
||||||
|
.folderList-item
|
||||||
|
navDarkButtonColor()
|
||||||
|
border-width 0 0 0 6px
|
||||||
|
border-style solid
|
||||||
|
border-color transparent
|
||||||
|
|
||||||
|
.folderList-item--active
|
||||||
|
@extend .folderList-item
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
color $ui-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
|
|
||||||
<title>CodeXen Popup</title>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" title="no title" charset="utf-8">
|
|
||||||
<link rel="shortcut icon" href="favicon.ico">
|
|
||||||
<script>
|
|
||||||
document.addEventListener('mousewheel', function(e) {
|
|
||||||
if(e.deltaY % 1 !== 0) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!Object.assign) {
|
|
||||||
Object.defineProperty(Object, 'assign', {
|
|
||||||
enumerable: false,
|
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
value: function(target) {
|
|
||||||
'use strict';
|
|
||||||
if (target === undefined || target === null) {
|
|
||||||
throw new TypeError('Cannot convert first argument to object');
|
|
||||||
}
|
|
||||||
|
|
||||||
var to = Object(target);
|
|
||||||
for (var i = 1; i < arguments.length; i++) {
|
|
||||||
var nextSource = arguments[i];
|
|
||||||
if (nextSource === undefined || nextSource === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
nextSource = Object(nextSource);
|
|
||||||
|
|
||||||
var keysArray = Object.keys(Object(nextSource));
|
|
||||||
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
|
|
||||||
var nextKey = keysArray[nextIndex];
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
|
|
||||||
if (desc !== undefined && desc.enumerable) {
|
|
||||||
to[nextKey] = nextSource[nextKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return to;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
require('electron-stylus')(__dirname + '/../styles/finder/index.styl')
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="content"></div>
|
|
||||||
<script src="../ace/src-min/ace.js"></script>
|
|
||||||
<script>
|
|
||||||
require('node-jsx').install({ harmony: true, extension: '.jsx' })
|
|
||||||
require('./index.jsx')
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
363
browser/finder/index.js
Normal file
363
browser/finder/index.js
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { connect, Provider } from 'react-redux'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import ipc from './ipcClient'
|
||||||
|
import store from './store'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './FinderMain.styl'
|
||||||
|
import StorageSection from './StorageSection'
|
||||||
|
import NoteList from './NoteList'
|
||||||
|
import NoteDetail from './NoteDetail'
|
||||||
|
require('!!style!css!stylus?sourceMap!../main/global.styl')
|
||||||
|
require('../lib/customMeta')
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const { remote } = electron
|
||||||
|
const { Menu } = remote
|
||||||
|
|
||||||
|
function hideFinder () {
|
||||||
|
let finderWindow = remote.getCurrentWindow()
|
||||||
|
if (global.process.platform === 'win32') {
|
||||||
|
finderWindow.blur()
|
||||||
|
finderWindow.hide()
|
||||||
|
}
|
||||||
|
if (global.process.platform === 'darwin') {
|
||||||
|
Menu.sendActionToFirstResponder('hide:')
|
||||||
|
}
|
||||||
|
remote.getCurrentWindow().hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
|
||||||
|
|
||||||
|
class FinderMain extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
search: '',
|
||||||
|
index: 0,
|
||||||
|
filter: {
|
||||||
|
includeSnippet: true,
|
||||||
|
includeMarkdown: false,
|
||||||
|
type: 'ALL',
|
||||||
|
storage: null,
|
||||||
|
folder: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.focusHandler = (e) => this.handleWindowFocus(e)
|
||||||
|
this.blurHandler = (e) => this.handleWindowBlur(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.refs.search.focus()
|
||||||
|
window.addEventListener('focus', this.focusHandler)
|
||||||
|
window.addEventListener('blur', this.blurHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
window.removeEventListener('focus', this.focusHandler)
|
||||||
|
window.removeEventListener('blur', this.blurHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWindowFocus (e) {
|
||||||
|
this.refs.search.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWindowBlur (e) {
|
||||||
|
let { filter } = this.state
|
||||||
|
filter.type = 'ALL'
|
||||||
|
this.setState({
|
||||||
|
search: '',
|
||||||
|
filter,
|
||||||
|
index: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
this.refs.search.focus()
|
||||||
|
if (e.keyCode === 9) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
this.refs.detail.selectPriorSnippet()
|
||||||
|
} else {
|
||||||
|
this.refs.detail.selectNextSnippet()
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
if (e.keyCode === 38) {
|
||||||
|
this.selectPrevious()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 40) {
|
||||||
|
this.selectNext()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.refs.detail.saveToClipboard()
|
||||||
|
hideFinder()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
hideFinder()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
if (e.keyCode === 91 || e.metaKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchChange (e) {
|
||||||
|
this.setState({
|
||||||
|
search: e.target.value,
|
||||||
|
index: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectArticle (article) {
|
||||||
|
this.setState({currentArticle: article})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPrevious () {
|
||||||
|
if (this.state.index > 0) {
|
||||||
|
this.setState({
|
||||||
|
index: this.state.index - 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNext () {
|
||||||
|
if (this.state.index < this.noteCount - 1) {
|
||||||
|
this.setState({
|
||||||
|
index: this.state.index + 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnlySnippetCheckboxChange (e) {
|
||||||
|
let { filter } = this.state
|
||||||
|
filter.includeSnippet = e.target.checked
|
||||||
|
this.setState({
|
||||||
|
filter: filter,
|
||||||
|
index: 0
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnlyMarkdownCheckboxChange (e) {
|
||||||
|
let { filter } = this.state
|
||||||
|
filter.includeMarkdown = e.target.checked
|
||||||
|
this.refs.list.resetScroll()
|
||||||
|
this.setState({
|
||||||
|
filter: filter,
|
||||||
|
index: 0
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAllNotesButtonClick (e) {
|
||||||
|
let { filter } = this.state
|
||||||
|
filter.type = 'ALL'
|
||||||
|
this.refs.list.resetScroll()
|
||||||
|
this.setState({
|
||||||
|
filter,
|
||||||
|
index: 0
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStarredButtonClick (e) {
|
||||||
|
let { filter } = this.state
|
||||||
|
filter.type = 'STARRED'
|
||||||
|
this.refs.list.resetScroll()
|
||||||
|
this.setState({
|
||||||
|
filter,
|
||||||
|
index: 0
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStorageButtonClick (e, storage) {
|
||||||
|
let { filter } = this.state
|
||||||
|
filter.type = 'STORAGE'
|
||||||
|
filter.storage = storage
|
||||||
|
this.refs.list.resetScroll()
|
||||||
|
this.setState({
|
||||||
|
filter,
|
||||||
|
index: 0
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFolderButtonClick (e, storage, folder) {
|
||||||
|
let { filter } = this.state
|
||||||
|
filter.type = 'FOLDER'
|
||||||
|
filter.storage = storage
|
||||||
|
filter.folder = folder
|
||||||
|
this.refs.list.resetScroll()
|
||||||
|
this.setState({
|
||||||
|
filter,
|
||||||
|
index: 0
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNoteClick (e, index) {
|
||||||
|
this.setState({
|
||||||
|
index
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { data, config } = this.props
|
||||||
|
let { filter, search } = this.state
|
||||||
|
let storageList = []
|
||||||
|
for (let key in data.storageMap) {
|
||||||
|
let storage = data.storageMap[key]
|
||||||
|
let item = (
|
||||||
|
<StorageSection
|
||||||
|
filter={filter}
|
||||||
|
storage={storage}
|
||||||
|
key={storage.key}
|
||||||
|
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
|
||||||
|
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
storageList.push(item)
|
||||||
|
}
|
||||||
|
let notes = []
|
||||||
|
let noteIds
|
||||||
|
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'STORAGE':
|
||||||
|
noteIds = data.storageNoteMap[filter.storage]
|
||||||
|
break
|
||||||
|
case 'FOLDER':
|
||||||
|
noteIds = data.folderNoteMap[filter.storage + '-' + filter.folder]
|
||||||
|
break
|
||||||
|
case 'STARRED':
|
||||||
|
noteIds = data.starredSet
|
||||||
|
}
|
||||||
|
if (noteIds != null) {
|
||||||
|
noteIds.forEach((id) => {
|
||||||
|
notes.push(data.noteMap[id])
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
for (let key in data.noteMap) {
|
||||||
|
notes.push(data.noteMap[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter.includeSnippet && filter.includeMarkdown) {
|
||||||
|
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
|
||||||
|
} else if (filter.includeSnippet && !filter.includeMarkdown) {
|
||||||
|
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search.trim().length > 0) {
|
||||||
|
let needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
|
||||||
|
notes = notes.filter((note) => note.title.match(needle))
|
||||||
|
}
|
||||||
|
notes = notes
|
||||||
|
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||||
|
|
||||||
|
let activeNote = notes[this.state.index]
|
||||||
|
this.noteCount = notes.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='Finder'
|
||||||
|
styleName='root'
|
||||||
|
ref='-1'
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||||
|
>
|
||||||
|
<div styleName='search'>
|
||||||
|
<input
|
||||||
|
styleName='search-input'
|
||||||
|
ref='search'
|
||||||
|
value={search}
|
||||||
|
placeholder='Search...'
|
||||||
|
onChange={(e) => this.handleSearchChange(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div styleName='result'>
|
||||||
|
<div styleName='result-nav'>
|
||||||
|
<div styleName='result-nav-filter'>
|
||||||
|
<div styleName='result-nav-filter-option'>
|
||||||
|
<label>
|
||||||
|
<input type='checkbox'
|
||||||
|
checked={filter.includeSnippet}
|
||||||
|
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
|
||||||
|
/> Only Snippets</label>
|
||||||
|
</div>
|
||||||
|
<div styleName='result-nav-filter-option'>
|
||||||
|
<label>
|
||||||
|
<input type='checkbox'
|
||||||
|
checked={filter.includeMarkdown}
|
||||||
|
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
|
||||||
|
/> Only Markdown</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button styleName={filter.type === 'ALL'
|
||||||
|
? 'result-nav-menu--active'
|
||||||
|
: 'result-nav-menu'
|
||||||
|
}
|
||||||
|
onClick={(e) => this.handleAllNotesButtonClick(e)}
|
||||||
|
><i className='fa fa-files-o fa-fw'/> All Notes</button>
|
||||||
|
<button styleName={filter.type === 'STARRED'
|
||||||
|
? 'result-nav-menu--active'
|
||||||
|
: 'result-nav-menu'
|
||||||
|
}
|
||||||
|
onClick={(e) => this.handleStarredButtonClick(e)}
|
||||||
|
><i className='fa fa-star fa-fw'/> Starred</button>
|
||||||
|
<div styleName='result-nav-storageList'>
|
||||||
|
{storageList}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NoteList styleName='result-list'
|
||||||
|
storageMap={data.storageMap}
|
||||||
|
notes={notes}
|
||||||
|
ref='list'
|
||||||
|
search={search}
|
||||||
|
index={this.state.index}
|
||||||
|
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
|
||||||
|
/>
|
||||||
|
<div styleName='result-detail'>
|
||||||
|
<NoteDetail
|
||||||
|
note={activeNote}
|
||||||
|
config={config}
|
||||||
|
ref='detail'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FinderMain.propTypes = {
|
||||||
|
dispatch: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
|
||||||
|
|
||||||
|
function refreshData () {
|
||||||
|
// let data = dataStore.getData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.render((
|
||||||
|
<Provider store={store}>
|
||||||
|
<Finder/>
|
||||||
|
</Provider>
|
||||||
|
), document.getElementById('content'), function () {
|
||||||
|
refreshData()
|
||||||
|
})
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
var remote = require('remote')
|
|
||||||
var hideFinder = remote.getGlobal('hideFinder')
|
|
||||||
var clipboard = require('clipboard')
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var ArticleFilter = require('../main/Mixins/ArticleFilter')
|
|
||||||
|
|
||||||
var FinderInput = require('./Components/FinderInput')
|
|
||||||
var FinderList = require('./Components/FinderList')
|
|
||||||
var FinderDetail = require('./Components/FinderDetail')
|
|
||||||
|
|
||||||
// Filter end
|
|
||||||
|
|
||||||
function fetchArticles () {
|
|
||||||
var user = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
if (user == null) {
|
|
||||||
console.log('need to login')
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
var articles = []
|
|
||||||
user.Planets.forEach(function (planet) {
|
|
||||||
var _planet = JSON.parse(localStorage.getItem('planet-' + planet.id))
|
|
||||||
articles = articles.concat(_planet.Codes, _planet.Notes)
|
|
||||||
})
|
|
||||||
user.Teams.forEach(function (team) {
|
|
||||||
team.Planets.forEach(function (planet) {
|
|
||||||
var _planet = JSON.parse(localStorage.getItem('planet-' + planet.id))
|
|
||||||
articles = articles.concat(_planet.Codes, _planet.Notes)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return articles
|
|
||||||
}
|
|
||||||
|
|
||||||
var Finder = React.createClass({
|
|
||||||
mixins: [ArticleFilter],
|
|
||||||
getInitialState: function () {
|
|
||||||
var articles = fetchArticles()
|
|
||||||
return {
|
|
||||||
articles: articles,
|
|
||||||
currentArticle: articles[0],
|
|
||||||
search: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
document.addEventListener('keydown', this.handleKeyDown)
|
|
||||||
document.addEventListener('click', this.handleClick)
|
|
||||||
window.addEventListener('focus', this.handleFinderFocus)
|
|
||||||
this.handleFinderFocus()
|
|
||||||
},
|
|
||||||
componentWillUnmount: function () {
|
|
||||||
document.removeEventListener('keydown', this.handleKeyDown)
|
|
||||||
document.removeEventListener('click', this.handleClick)
|
|
||||||
window.removeEventListener('focus', this.handleFinderFocus)
|
|
||||||
},
|
|
||||||
handleFinderFocus: function () {
|
|
||||||
console.log('focusseeddddd')
|
|
||||||
this.focusInput()
|
|
||||||
var articles = fetchArticles()
|
|
||||||
this.setState({
|
|
||||||
articles: articles,
|
|
||||||
search: ''
|
|
||||||
}, function () {
|
|
||||||
var firstArticle = this.refs.finderList.props.articles[0]
|
|
||||||
if (firstArticle) {
|
|
||||||
this.setState({
|
|
||||||
currentArticle: firstArticle
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleKeyDown: function (e) {
|
|
||||||
if (e.keyCode === 38) {
|
|
||||||
this.selectPrevious()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === 40) {
|
|
||||||
this.selectNext()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
var article = this.state.currentArticle
|
|
||||||
clipboard.writeText(article.content)
|
|
||||||
hideFinder()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
if (e.keyCode === 27) {
|
|
||||||
hideFinder()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
focusInput: function () {
|
|
||||||
React.findDOMNode(this.refs.finderInput).querySelector('input').focus()
|
|
||||||
},
|
|
||||||
handleClick: function () {
|
|
||||||
this.focusInput()
|
|
||||||
},
|
|
||||||
selectPrevious: function () {
|
|
||||||
var index = this.refs.finderList.props.articles.indexOf(this.state.currentArticle)
|
|
||||||
if (index > 0) {
|
|
||||||
this.setState({currentArticle: this.refs.finderList.props.articles[index - 1]})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectNext: function () {
|
|
||||||
var index = this.refs.finderList.props.articles.indexOf(this.state.currentArticle)
|
|
||||||
if (index > -1 && index < this.refs.finderList.props.articles.length - 1) {
|
|
||||||
this.setState({currentArticle: this.refs.finderList.props.articles[index + 1]})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectArticle: function (article) {
|
|
||||||
this.setState({currentArticle: article})
|
|
||||||
},
|
|
||||||
handleChange: function (e) {
|
|
||||||
this.setState({search: e.target.value}, function () {
|
|
||||||
this.setState({currentArticle: this.refs.finderList.props.articles[0]})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var articles = this.searchArticle(this.state.search, this.state.articles)
|
|
||||||
return (
|
|
||||||
<div className='Finder'>
|
|
||||||
<FinderInput ref='finderInput' onChange={this.handleChange} search={this.state.search}/>
|
|
||||||
<FinderList ref='finderList' currentArticle={this.state.currentArticle} articles={articles} selectArticle={this.selectArticle}/>
|
|
||||||
<FinderDetail currentArticle={this.state.currentArticle}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
React.render(<Finder/>, document.getElementById('content'))
|
|
||||||
122
browser/finder/ipcClient.js
Normal file
122
browser/finder/ipcClient.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
const nodeIpc = require('node-ipc')
|
||||||
|
const { remote, ipcRenderer } = require('electron')
|
||||||
|
const { app, Menu } = remote
|
||||||
|
const path = require('path')
|
||||||
|
const store = require('./store')
|
||||||
|
const consts = require('browser/lib/consts')
|
||||||
|
|
||||||
|
nodeIpc.config.id = 'finder'
|
||||||
|
nodeIpc.config.retry = 1500
|
||||||
|
nodeIpc.config.silent = true
|
||||||
|
|
||||||
|
function killFinder () {
|
||||||
|
let finderWindow = remote.getCurrentWindow()
|
||||||
|
finderWindow.removeAllListeners()
|
||||||
|
if (global.process.platform === 'darwin') {
|
||||||
|
// Only OSX has another app process.
|
||||||
|
nodeIpc.of.node.emit('quit-from-finder')
|
||||||
|
} else {
|
||||||
|
finderWindow.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFinder () {
|
||||||
|
let finderWindow = remote.getCurrentWindow()
|
||||||
|
if (global.process.platform === 'darwin') {
|
||||||
|
if (finderWindow.isVisible()) {
|
||||||
|
finderWindow.hide()
|
||||||
|
Menu.sendActionToFirstResponder('hide:')
|
||||||
|
} else {
|
||||||
|
nodeIpc.of.node.emit('request-data-from-finder')
|
||||||
|
finderWindow.show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (finderWindow.isVisible()) {
|
||||||
|
finderWindow.blur()
|
||||||
|
finderWindow.hide()
|
||||||
|
} else {
|
||||||
|
nodeIpc.of.node.emit('request-data-from-finder')
|
||||||
|
finderWindow.show()
|
||||||
|
finderWindow.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeIpc.connectTo(
|
||||||
|
'node',
|
||||||
|
path.join(app.getPath('userData'), 'boostnote.service'),
|
||||||
|
function () {
|
||||||
|
nodeIpc.of.node.on('error', function (err) {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
nodeIpc.of.node.on('connect', function () {
|
||||||
|
console.log('Conncted successfully')
|
||||||
|
})
|
||||||
|
nodeIpc.of.node.on('disconnect', function () {
|
||||||
|
console.log('disconnected')
|
||||||
|
})
|
||||||
|
|
||||||
|
nodeIpc.of.node.on('open-finder', function () {
|
||||||
|
toggleFinder()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcRenderer.on('open-finder-from-tray', function () {
|
||||||
|
toggleFinder()
|
||||||
|
})
|
||||||
|
ipcRenderer.on('open-main-from-tray', function () {
|
||||||
|
nodeIpc.of.node.emit('open-main-from-finder')
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcRenderer.on('quit-from-tray', function () {
|
||||||
|
nodeIpc.of.node.emit('quit-from-finder')
|
||||||
|
killFinder()
|
||||||
|
})
|
||||||
|
|
||||||
|
nodeIpc.of.node.on('throttle-data', function (payload) {
|
||||||
|
console.log('Received data from Main renderer')
|
||||||
|
store.default.dispatch({
|
||||||
|
type: 'THROTTLE_DATA',
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
nodeIpc.of.node.on('config-renew', function (payload) {
|
||||||
|
const { config } = payload
|
||||||
|
if (config.ui.theme === 'dark') {
|
||||||
|
document.body.setAttribute('data-theme', 'dark')
|
||||||
|
} else {
|
||||||
|
document.body.setAttribute('data-theme', 'default')
|
||||||
|
}
|
||||||
|
|
||||||
|
let editorTheme = document.getElementById('editorTheme')
|
||||||
|
if (editorTheme == null) {
|
||||||
|
editorTheme = document.createElement('link')
|
||||||
|
editorTheme.setAttribute('id', 'editorTheme')
|
||||||
|
editorTheme.setAttribute('rel', 'stylesheet')
|
||||||
|
document.head.appendChild(editorTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
||||||
|
? config.editor.theme
|
||||||
|
: 'default'
|
||||||
|
|
||||||
|
if (config.editor.theme !== 'default') {
|
||||||
|
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
||||||
|
}
|
||||||
|
|
||||||
|
store.default.dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config: config
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
nodeIpc.of.node.on('quit-finder-app', function () {
|
||||||
|
nodeIpc.of.node.emit('quit-finder-app-confirm')
|
||||||
|
killFinder()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const ipc = {}
|
||||||
|
|
||||||
|
module.exports = ipc
|
||||||
51
browser/finder/store.js
Normal file
51
browser/finder/store.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { combineReducers, createStore } from 'redux'
|
||||||
|
import { routerReducer } from 'react-router-redux'
|
||||||
|
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
|
||||||
|
|
||||||
|
let defaultData = {
|
||||||
|
storageMap: {},
|
||||||
|
noteMap: {},
|
||||||
|
starredSet: [],
|
||||||
|
storageNoteMap: {},
|
||||||
|
folderNoteMap: {},
|
||||||
|
tagNoteMap: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function data (state = defaultData, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'THROTTLE_DATA':
|
||||||
|
console.log(action)
|
||||||
|
state = action.data
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
function config (state = DEFAULT_CONFIG, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'INIT_CONFIG':
|
||||||
|
case 'SET_CONFIG':
|
||||||
|
return Object.assign({}, state, action.config)
|
||||||
|
case 'SET_IS_SIDENAV_FOLDED':
|
||||||
|
state.isSideNavFolded = action.isFolded
|
||||||
|
return Object.assign({}, state)
|
||||||
|
case 'SET_ZOOM':
|
||||||
|
state.zoom = action.zoom
|
||||||
|
return Object.assign({}, state)
|
||||||
|
case 'SET_LIST_WIDTH':
|
||||||
|
state.listWidth = action.listWidth
|
||||||
|
return Object.assign({}, state)
|
||||||
|
case 'SET_UI':
|
||||||
|
return Object.assign({}, state, action.config)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
let reducer = combineReducers({
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
routing: routerReducer
|
||||||
|
})
|
||||||
|
|
||||||
|
let store = createStore(reducer)
|
||||||
|
|
||||||
|
export default store
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a href="/main">Go Main</a>
|
|
||||||
<a href="/main">Go Popup</a>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
5
browser/lib/CSSModules.js
Normal file
5
browser/lib/CSSModules.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import CSSModules from 'react-css-modules'
|
||||||
|
|
||||||
|
export default function (component, styles) {
|
||||||
|
return CSSModules(component, styles, {errorWhenNotFound: false})
|
||||||
|
}
|
||||||
106
browser/lib/Mutable.js
Normal file
106
browser/lib/Mutable.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
class MutableMap {
|
||||||
|
constructor (iterable) {
|
||||||
|
this._map = new Map(iterable)
|
||||||
|
Object.defineProperty(this, 'size', {
|
||||||
|
get: () => this._map.size,
|
||||||
|
set: function (value) {
|
||||||
|
this['size'] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get (...args) {
|
||||||
|
return this._map.get(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
set (...args) {
|
||||||
|
return this._map.set(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete (...args) {
|
||||||
|
return this._map.delete(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
has (...args) {
|
||||||
|
return this._map.has(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear (...args) {
|
||||||
|
return this._map.clear(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach (...args) {
|
||||||
|
return this._map.forEach(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator] () {
|
||||||
|
return this._map[Symbol.iterator]()
|
||||||
|
}
|
||||||
|
|
||||||
|
map (cb) {
|
||||||
|
let result = []
|
||||||
|
for (let [key, value] of this._map) {
|
||||||
|
result.push(cb(value, key))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
toJS () {
|
||||||
|
let result = {}
|
||||||
|
for (let [key, value] of this._map) {
|
||||||
|
if (value instanceof MutableSet || value instanceof MutableMap) {
|
||||||
|
value = value.toJS()
|
||||||
|
}
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MutableSet {
|
||||||
|
constructor (iterable) {
|
||||||
|
this._set = new Set(iterable)
|
||||||
|
Object.defineProperty(this, 'size', {
|
||||||
|
get: () => this._set.size,
|
||||||
|
set: function (value) {
|
||||||
|
this['size'] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
add (...args) {
|
||||||
|
return this._set.add(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete (...args) {
|
||||||
|
return this._set.delete(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach (...args) {
|
||||||
|
return this._set.forEach(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator] () {
|
||||||
|
return this._set[Symbol.iterator]()
|
||||||
|
}
|
||||||
|
|
||||||
|
map (cb) {
|
||||||
|
let result = []
|
||||||
|
this._set.forEach(function (value, key) {
|
||||||
|
result.push(cb(value, key))
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
toJS () {
|
||||||
|
return Array.from(this._set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Mutable = {
|
||||||
|
Map: MutableMap,
|
||||||
|
Set: MutableSet
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Mutable
|
||||||
21
browser/lib/api.js
Normal file
21
browser/lib/api.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import superagent from 'superagent'
|
||||||
|
import superagentPromise from 'superagent-promise'
|
||||||
|
|
||||||
|
export const SERVER_URL = 'https://b00st.io/'
|
||||||
|
// export const SERVER_URL = 'http://localhost:3333/'
|
||||||
|
|
||||||
|
export const request = superagentPromise(superagent, Promise)
|
||||||
|
|
||||||
|
export function shareViaPublicURL (input) {
|
||||||
|
return request
|
||||||
|
.post(SERVER_URL + 'apis/share')
|
||||||
|
// .set({
|
||||||
|
// Authorization: 'Bearer ' + auth.token()
|
||||||
|
// })
|
||||||
|
.send(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
SERVER_URL,
|
||||||
|
shareViaPublicURL
|
||||||
|
}
|
||||||
36
browser/lib/consts.js
Normal file
36
browser/lib/consts.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const fs = require('sander')
|
||||||
|
const { remote } = require('electron')
|
||||||
|
const { app } = remote
|
||||||
|
|
||||||
|
const themePath = process.env.NODE_ENV === 'production'
|
||||||
|
? path.join(app.getAppPath(), './node_modules/codemirror/theme')
|
||||||
|
: require('path').resolve('./node_modules/codemirror/theme')
|
||||||
|
const themes = fs.readdirSync(themePath)
|
||||||
|
.map((themePath) => {
|
||||||
|
return themePath.substring(0, themePath.lastIndexOf('.'))
|
||||||
|
})
|
||||||
|
|
||||||
|
const consts = {
|
||||||
|
FOLDER_COLORS: [
|
||||||
|
'#E10051',
|
||||||
|
'#FF8E00',
|
||||||
|
'#E8D252',
|
||||||
|
'#3FD941',
|
||||||
|
'#30D5C8',
|
||||||
|
'#2BA5F7',
|
||||||
|
'#B013A4'
|
||||||
|
],
|
||||||
|
FOLDER_COLOR_NAMES: [
|
||||||
|
'Razzmatazz (Red)',
|
||||||
|
'Pizazz (Orange)',
|
||||||
|
'Confetti (Yellow)',
|
||||||
|
'Emerald (Green)',
|
||||||
|
'Turquoise',
|
||||||
|
'Dodger Blue',
|
||||||
|
'Violet Eggplant'
|
||||||
|
],
|
||||||
|
THEMES: ['default'].concat(themes)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = consts
|
||||||
17
browser/lib/context.js
Normal file
17
browser/lib/context.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const { remote } = require('electron')
|
||||||
|
const { Menu, MenuItem } = remote
|
||||||
|
|
||||||
|
function popup (templates) {
|
||||||
|
let menu = new Menu()
|
||||||
|
templates.forEach((item) => {
|
||||||
|
menu.append(new MenuItem(item))
|
||||||
|
})
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
popup
|
||||||
|
}
|
||||||
|
|
||||||
|
module.export = context
|
||||||
|
export default context
|
||||||
4
browser/lib/customMeta.js
Normal file
4
browser/lib/customMeta.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import CodeMirror from 'codemirror'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
||||||
7
browser/lib/keygen.js
Normal file
7
browser/lib/keygen.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const crypto = require('crypto')
|
||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
|
module.exports = function (length) {
|
||||||
|
if (!_.isFinite(length)) length = 10
|
||||||
|
return crypto.randomBytes(length).toString('hex')
|
||||||
|
}
|
||||||
164
browser/lib/markdown.js
Normal file
164
browser/lib/markdown.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import markdownit from 'markdown-it'
|
||||||
|
import emoji from 'markdown-it-emoji'
|
||||||
|
import math from '@rokt33r/markdown-it-math'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
const katex = window.katex
|
||||||
|
|
||||||
|
function createGutter (str) {
|
||||||
|
let lc = (str.match(/\n/g) || []).length
|
||||||
|
let lines = []
|
||||||
|
for (let i = 1; i <= lc; i++) {
|
||||||
|
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||||
|
}
|
||||||
|
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||||
|
}
|
||||||
|
|
||||||
|
var md = markdownit({
|
||||||
|
typographer: true,
|
||||||
|
linkify: true,
|
||||||
|
html: true,
|
||||||
|
xhtmlOut: true,
|
||||||
|
highlight: function (str, lang) {
|
||||||
|
if (lang === 'flowchart') {
|
||||||
|
return `<pre class="flowchart">${str}</pre>`
|
||||||
|
}
|
||||||
|
if (lang === 'sequence') {
|
||||||
|
return `<pre class="sequence">${str}</pre>`
|
||||||
|
}
|
||||||
|
return '<pre class="code">' +
|
||||||
|
createGutter(str) +
|
||||||
|
'<code class="' + lang + '">' +
|
||||||
|
str +
|
||||||
|
'</code></pre>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
md.use(emoji, {
|
||||||
|
shortcuts: {}
|
||||||
|
})
|
||||||
|
md.use(math, {
|
||||||
|
inlineRenderer: function (str) {
|
||||||
|
let output = ''
|
||||||
|
try {
|
||||||
|
output = katex.renderToString(str.trim())
|
||||||
|
} catch (err) {
|
||||||
|
output = `<span class="katex-error">${err.message}</span>`
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
},
|
||||||
|
blockRenderer: function (str) {
|
||||||
|
let output = ''
|
||||||
|
try {
|
||||||
|
output = katex.renderToString(str.trim(), {displayMode: true})
|
||||||
|
} catch (err) {
|
||||||
|
output = `<div class="katex-error">${err.message}</div>`
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
})
|
||||||
|
md.use(require('markdown-it-footnote'))
|
||||||
|
// Override task item
|
||||||
|
md.block.ruler.at('paragraph', function (state, startLine/*, endLine*/) {
|
||||||
|
let content, terminate, i, l, token
|
||||||
|
let nextLine = startLine + 1
|
||||||
|
let terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||||
|
let endLine = state.lineMax
|
||||||
|
|
||||||
|
// jump line-by-line until empty one or EOF
|
||||||
|
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||||
|
// this would be a code block normally, but after paragraph
|
||||||
|
// it's considered a lazy continuation regardless of what's there
|
||||||
|
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
||||||
|
|
||||||
|
// quirk for blockquotes, this line should already be checked by that rule
|
||||||
|
if (state.sCount[nextLine] < 0) { continue }
|
||||||
|
|
||||||
|
// Some tags can terminate paragraph without empty line.
|
||||||
|
terminate = false
|
||||||
|
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||||
|
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||||
|
terminate = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (terminate) { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
||||||
|
|
||||||
|
state.line = nextLine
|
||||||
|
|
||||||
|
token = state.push('paragraph_open', 'p', 1)
|
||||||
|
token.map = [ startLine, state.line ]
|
||||||
|
|
||||||
|
if (state.parentType === 'list') {
|
||||||
|
let match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||||
|
if (match) {
|
||||||
|
content = `<label class='taskListItem' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token = state.push('inline', '', 0)
|
||||||
|
token.content = content
|
||||||
|
token.map = [ startLine, state.line ]
|
||||||
|
token.children = []
|
||||||
|
|
||||||
|
token = state.push('paragraph_close', 'p', -1)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add line number attribute for scrolling
|
||||||
|
let originalRender = md.renderer.render
|
||||||
|
md.renderer.render = function render (tokens, options, env) {
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
switch (token.type) {
|
||||||
|
case 'heading_open':
|
||||||
|
case 'paragraph_open':
|
||||||
|
case 'blockquote_open':
|
||||||
|
case 'table_open':
|
||||||
|
token.attrPush(['data-line', token.map[0]])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let result = originalRender.call(md.renderer, tokens, options, env)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
window.md = md
|
||||||
|
|
||||||
|
function strip (input) {
|
||||||
|
var output = input
|
||||||
|
try {
|
||||||
|
output = output
|
||||||
|
.replace(/^([\s\t]*)([\*\-\+]|\d\.)\s+/gm, '$1')
|
||||||
|
.replace(/\n={2,}/g, '\n')
|
||||||
|
.replace(/~~/g, '')
|
||||||
|
.replace(/`{3}.*\n/g, '')
|
||||||
|
.replace(/<(.*?)>/g, '$1')
|
||||||
|
.replace(/^[=\-]{2,}\s*$/g, '')
|
||||||
|
.replace(/\[\^.+?\](: .*?$)?/g, '')
|
||||||
|
.replace(/\s{0,2}\[.*?\]: .*?$/g, '')
|
||||||
|
.replace(/!\[.*?\][\[\(].*?[\]\)]/g, '')
|
||||||
|
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
|
||||||
|
.replace(/>/g, '')
|
||||||
|
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
|
||||||
|
.replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
|
||||||
|
.replace(/([\*_]{1,3})(\S.*?\S)\1/g, '$2')
|
||||||
|
.replace(/(`{3,})(.*?)\1/gm, '$2')
|
||||||
|
.replace(/^-{3,}\s*$/g, '')
|
||||||
|
.replace(/`(.+?)`/g, '$1')
|
||||||
|
.replace(/\n{2,}/g, '\n\n')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const markdown = {
|
||||||
|
render: function markdown (content) {
|
||||||
|
if (!_.isString(content)) content = ''
|
||||||
|
return md.render(content)
|
||||||
|
},
|
||||||
|
strip
|
||||||
|
}
|
||||||
|
export default markdown
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
var remote = require('remote')
|
|
||||||
var version = remote.getGlobal('version')
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var ExternalLink = require('../Mixins/ExternalLink')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [ExternalLink, KeyCaster('aboutModal')],
|
|
||||||
propTypes: {
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='AboutModal modal'>
|
|
||||||
<div className='about1'>
|
|
||||||
<img className='logo' src='resources/favicon-230x230.png'/>
|
|
||||||
<div className='appInfo'>Boost {version == null || version.length === 0 ? 'DEV version' : 'v' + version}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='about2'>
|
|
||||||
<div className='externalLabel'>External links</div>
|
|
||||||
<ul className='externalList'>
|
|
||||||
<li><a onClick={this.openExternal} href='http://b00st.io'>Boost Homepage <i className='fa fa-external-link'/></a></li>
|
|
||||||
<li><a>Regulation <i className='fa fa-external-link'/></a></li>
|
|
||||||
<li><a>Private policy <i className='fa fa-external-link'/></a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var Select = require('react-select')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var UserStore = require('../Stores/UserStore')
|
|
||||||
|
|
||||||
var getOptions = function (input, callback) {
|
|
||||||
Hq.searchUser(input)
|
|
||||||
.then(function (res) {
|
|
||||||
callback(null, {
|
|
||||||
options: res.body.map(function (user) {
|
|
||||||
return {
|
|
||||||
label: user.name,
|
|
||||||
value: user.name
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
complete: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, KeyCaster('addMemberModal')],
|
|
||||||
propTypes: {
|
|
||||||
team: React.PropTypes.object,
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
userName: '',
|
|
||||||
role: 'member'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
case 'submitAddMemberModal':
|
|
||||||
this.handleSubmit()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSubmit: function () {
|
|
||||||
this.setState({errorMessage: null}, function () {
|
|
||||||
Hq
|
|
||||||
.addMember(this.props.team.name, {
|
|
||||||
userName: this.state.userName,
|
|
||||||
role: this.state.role
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
console.log(res.body)
|
|
||||||
UserStore.Actions.addMember(res.body)
|
|
||||||
this.props.close()
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
if (err.status === 403) {
|
|
||||||
this.setState({errorMessage: err.response.body.message})
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleChange: function (value) {
|
|
||||||
this.setState({userName: value})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='AddMemberModal modal'>
|
|
||||||
<Select
|
|
||||||
name='userName'
|
|
||||||
value={this.state.userName}
|
|
||||||
placeholder='Username to add'
|
|
||||||
asyncOptions={getOptions}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
className='userNameSelect'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='formField'>
|
|
||||||
Add member as
|
|
||||||
<select valueLink={this.linkState('role')}>
|
|
||||||
<option value={'member'}>Member</option>
|
|
||||||
<option value={'owner'}>Owner</option>
|
|
||||||
</select>
|
|
||||||
role
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.errorMessage != null ? (<p className='errorAlert'>{this.state.errorMessage}</p>) : null}
|
|
||||||
|
|
||||||
<button onClick={this.handleSubmit} className='submitButton'><i className='fa fa-check'/></button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
var React = require('react')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var PlanetStore = require('../Stores/PlanetStore')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [KeyCaster('codeDeleteModal')],
|
|
||||||
propTypes: {
|
|
||||||
planet: React.PropTypes.object,
|
|
||||||
code: React.PropTypes.object,
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'submitCodeDeleteModal':
|
|
||||||
this.submit()
|
|
||||||
break
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submit: function () {
|
|
||||||
var planet = this.props.planet
|
|
||||||
Hq.destroyCode(planet.userName, planet.name, this.props.code.localId)
|
|
||||||
.then(function (res) {
|
|
||||||
PlanetStore.Actions.destroyCode(res.body)
|
|
||||||
this.props.close()
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='CodeDeleteModal modal'>
|
|
||||||
<div className='modal-header'>
|
|
||||||
<h1>Delete Code</h1>
|
|
||||||
</div>
|
|
||||||
<div className='modal-body'>
|
|
||||||
<p>Are you sure to delete it?</p>
|
|
||||||
</div>
|
|
||||||
<div className='modal-footer'>
|
|
||||||
<div className='modal-control'>
|
|
||||||
<button onClick={this.props.close} className='btn-default'>Cancel</button>
|
|
||||||
<button ref='submit' onClick={this.submit} className='btn-primary'>Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
var React = require('react')
|
|
||||||
var CodeForm = require('./CodeForm')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
close: React.PropTypes.func,
|
|
||||||
code: React.PropTypes.object,
|
|
||||||
planet: React.PropTypes.object
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
// TODO: Hacked!! should fix later
|
|
||||||
setTimeout(function () {
|
|
||||||
React.findDOMNode(this.refs.form.refs.description).focus()
|
|
||||||
}.bind(this), 1)
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='CodeEditModal modal'>
|
|
||||||
<div className='modal-header'>
|
|
||||||
<h1>Edit Code</h1>
|
|
||||||
</div>
|
|
||||||
<CodeForm ref='form' code={this.props.code} planet={this.props.planet} close={this.props.close}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var ace = window.ace
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
code: React.PropTypes.string,
|
|
||||||
mode: React.PropTypes.string,
|
|
||||||
className: React.PropTypes.string,
|
|
||||||
onChange: React.PropTypes.func
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
var el = React.findDOMNode(this.refs.target)
|
|
||||||
var editor = ace.edit(el)
|
|
||||||
editor.$blockScrolling = Infinity
|
|
||||||
editor.setValue(this.props.code)
|
|
||||||
editor.renderer.setShowGutter(true)
|
|
||||||
editor.setTheme('ace/theme/xcode')
|
|
||||||
editor.clearSelection()
|
|
||||||
|
|
||||||
var session = editor.getSession()
|
|
||||||
if (this.props.mode != null && this.props.mode.length > 0) {
|
|
||||||
session.setMode('ace/mode/' + this.props.mode)
|
|
||||||
} else {
|
|
||||||
session.setMode('ace/mode/text')
|
|
||||||
}
|
|
||||||
session.setUseSoftTabs(true)
|
|
||||||
session.setOption('useWorker', false)
|
|
||||||
session.setUseWrapMode(true)
|
|
||||||
|
|
||||||
session.on('change', function (e) {
|
|
||||||
if (this.props.onChange != null) {
|
|
||||||
var value = editor.getValue()
|
|
||||||
this.props.onChange(e, value)
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
this.setState({editor: editor})
|
|
||||||
},
|
|
||||||
componentDidUpdate: function (prevProps) {
|
|
||||||
if (this.state.editor.getValue() !== this.props.code) {
|
|
||||||
this.state.editor.setValue(this.props.code)
|
|
||||||
this.state.editor.clearSelection()
|
|
||||||
}
|
|
||||||
if (prevProps.mode !== this.props.mode) {
|
|
||||||
var session = this.state.editor.getSession()
|
|
||||||
if (this.props.mode != null && this.props.mode.length > 0) {
|
|
||||||
session.setMode('ace/mode/' + this.props.mode)
|
|
||||||
} else {
|
|
||||||
session.setMode('ace/mode/text')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div ref='target' className={this.props.className}></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var CodeEditor = require('./CodeEditor')
|
|
||||||
var Select = require('react-select')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var PlanetStore = require('../Stores/PlanetStore')
|
|
||||||
|
|
||||||
var aceModes = require('../../../modules/ace-modes')
|
|
||||||
|
|
||||||
var getOptions = function (input, callback) {
|
|
||||||
Hq.searchTag(input)
|
|
||||||
.then(function (res) {
|
|
||||||
callback(null, {
|
|
||||||
options: res.body.map(function (tag) {
|
|
||||||
return {
|
|
||||||
label: tag.name,
|
|
||||||
value: tag.name
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
complete: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, KeyCaster('codeForm')],
|
|
||||||
propTypes: {
|
|
||||||
planet: React.PropTypes.object,
|
|
||||||
close: React.PropTypes.func,
|
|
||||||
transitionTo: React.PropTypes.func,
|
|
||||||
code: React.PropTypes.object
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
var code = Object.assign({
|
|
||||||
description: '',
|
|
||||||
mode: '',
|
|
||||||
content: '',
|
|
||||||
Tags: []
|
|
||||||
}, this.props.code)
|
|
||||||
|
|
||||||
code.Tags = code.Tags.map(function (tag) {
|
|
||||||
return {
|
|
||||||
label: tag.name,
|
|
||||||
value: tag.name
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: code
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'submitCodeForm':
|
|
||||||
this.submit()
|
|
||||||
break
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleModeChange: function (selected) {
|
|
||||||
var code = this.state.code
|
|
||||||
code.mode = selected
|
|
||||||
this.setState({code: code})
|
|
||||||
},
|
|
||||||
handleTagsChange: function (selected, all) {
|
|
||||||
var code = this.state.code
|
|
||||||
code.Tags = all
|
|
||||||
this.setState({code: code})
|
|
||||||
},
|
|
||||||
handleContentChange: function (e, value) {
|
|
||||||
var code = this.state.code
|
|
||||||
code.content = value
|
|
||||||
this.setState({code: code})
|
|
||||||
},
|
|
||||||
submit: function () {
|
|
||||||
var planet = this.props.planet
|
|
||||||
var code = this.state.code
|
|
||||||
code.Tags = code.Tags.map(function (tag) {
|
|
||||||
return tag.value
|
|
||||||
})
|
|
||||||
if (this.props.code == null) {
|
|
||||||
Hq.createCode(planet.userName, planet.name, this.state.code)
|
|
||||||
.then(function (res) {
|
|
||||||
var code = res.body
|
|
||||||
PlanetStore.Actions.updateCode(code)
|
|
||||||
this.props.close()
|
|
||||||
this.props.transitionTo('codes', {userName: planet.userName, planetName: planet.name, localId: code.localId})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Hq.updateCode(planet.userName, planet.name, this.props.code.localId, this.state.code)
|
|
||||||
.then(function (res) {
|
|
||||||
var code = res.body
|
|
||||||
PlanetStore.Actions.updateCode(code)
|
|
||||||
this.props.close()
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleKeyDown: function (e) {
|
|
||||||
if (e.keyCode === 13 && e.metaKey) {
|
|
||||||
this.submit()
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var modeOptions = aceModes.map(function (mode) {
|
|
||||||
return {
|
|
||||||
label: mode,
|
|
||||||
value: mode
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div className='CodeForm'>
|
|
||||||
<div className='modal-body'>
|
|
||||||
<div className='form-group'>
|
|
||||||
<textarea ref='description' className='codeDescription block-input' valueLink={this.linkState('code.description')} placeholder='Description'/>
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<Select
|
|
||||||
name='mode'
|
|
||||||
className='modeSelect'
|
|
||||||
value={this.state.code.mode}
|
|
||||||
placeholder='Select Language'
|
|
||||||
options={modeOptions}
|
|
||||||
onChange={this.handleModeChange}/>
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<CodeEditor onChange={this.handleContentChange} code={this.state.code.content} mode={this.state.code.mode}/>
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<Select
|
|
||||||
name='Tags'
|
|
||||||
multi={true}
|
|
||||||
allowCreate={true}
|
|
||||||
value={this.state.code.Tags}
|
|
||||||
placeholder='Tags...'
|
|
||||||
asyncOptions={getOptions}
|
|
||||||
onChange={this.handleTagsChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='modal-footer'>
|
|
||||||
<div className='modal-control'>
|
|
||||||
<button onClick={this.props.close} className='btn-default'>Cancel</button>
|
|
||||||
<button onClick={this.submit} className='btn-primary'>{this.props.code == null ? 'Launch' : 'Relaunch'}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var ace = window.ace
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
code: React.PropTypes.string,
|
|
||||||
mode: React.PropTypes.string,
|
|
||||||
className: React.PropTypes.string
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
var el = React.findDOMNode(this.refs.target)
|
|
||||||
var editor = ace.edit(el)
|
|
||||||
editor.$blockScrolling = Infinity
|
|
||||||
editor.setValue(this.props.code)
|
|
||||||
editor.renderer.setShowGutter(false)
|
|
||||||
editor.setReadOnly(true)
|
|
||||||
editor.setTheme('ace/theme/xcode')
|
|
||||||
editor.setHighlightActiveLine(false)
|
|
||||||
editor.clearSelection()
|
|
||||||
|
|
||||||
var session = editor.getSession()
|
|
||||||
if (this.props.mode != null && this.props.mode.length > 0) {
|
|
||||||
session.setMode('ace/mode/' + this.props.mode)
|
|
||||||
} else {
|
|
||||||
session.setMode('ace/mode/text')
|
|
||||||
}
|
|
||||||
session.setUseSoftTabs(true)
|
|
||||||
session.setOption('useWorker', false)
|
|
||||||
session.setUseWrapMode(true)
|
|
||||||
|
|
||||||
this.setState({editor: editor})
|
|
||||||
},
|
|
||||||
componentDidUpdate: function (prevProps) {
|
|
||||||
if (this.state.editor.getValue() !== this.props.code) {
|
|
||||||
this.state.editor.setValue(this.props.code)
|
|
||||||
this.state.editor.clearSelection()
|
|
||||||
}
|
|
||||||
if (prevProps.mode !== this.props.mode) {
|
|
||||||
var session = this.state.editor.getSession()
|
|
||||||
if (this.props.mode != null && this.props.mode.length > 0) {
|
|
||||||
session.setMode('ace/mode/' + this.props.mode)
|
|
||||||
} else {
|
|
||||||
session.setMode('ace/mode/text')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div ref='target' className={this.props.className}></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
var React = require('react')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, KeyCaster('contactModal')],
|
|
||||||
propTypes: {
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
isSent: false,
|
|
||||||
mail: {
|
|
||||||
title: '',
|
|
||||||
content: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
case 'submitContactModal':
|
|
||||||
if (this.state.isSent) {
|
|
||||||
this.props.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.sendEmail()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
React.findDOMNode(this.refs.title).focus()
|
|
||||||
},
|
|
||||||
sendEmail: function () {
|
|
||||||
Hq.sendEmail(this.state.mail)
|
|
||||||
.then(function (res) {
|
|
||||||
this.setState({isSent: !this.state.isSent})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='ContactModal modal'>
|
|
||||||
<div className='modal-header'><h1>Contact form</h1></div>
|
|
||||||
|
|
||||||
{!this.state.isSent ? (
|
|
||||||
<div className='contactForm'>
|
|
||||||
<div className='modal-body'>
|
|
||||||
<div className='formField'>
|
|
||||||
<input ref='title' valueLink={this.linkState('mail.title')} placeholder='Title'/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<textarea valueLink={this.linkState('mail.content')} placeholder='Content'/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='modal-footer'>
|
|
||||||
<div className='formControl'>
|
|
||||||
<button onClick={this.sendEmail} className='sendButton'>Send</button>
|
|
||||||
<button onClick={this.props.close}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='confirmation'>
|
|
||||||
<div className='confirmationMessage'>Thanks for sharing your opinion!</div>
|
|
||||||
<button className='doneButton' onClick={this.props.close}>Done</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var UserStore = require('../Stores/UserStore')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, KeyCaster('editProfileModal')],
|
|
||||||
propTypes: {
|
|
||||||
user: React.PropTypes.shape({
|
|
||||||
name: React.PropTypes.string,
|
|
||||||
profileName: React.PropTypes.string,
|
|
||||||
email: React.PropTypes.string
|
|
||||||
}),
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
var user = this.props.user
|
|
||||||
return {
|
|
||||||
currentTab: 'userInfo',
|
|
||||||
user: {
|
|
||||||
profileName: user.profileName,
|
|
||||||
email: user.email
|
|
||||||
},
|
|
||||||
userSubmitStatus: null,
|
|
||||||
password: {
|
|
||||||
currentPassword: '',
|
|
||||||
newPassword: '',
|
|
||||||
passwordConfirmation: ''
|
|
||||||
},
|
|
||||||
passwordSubmitStatus: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectTab: function (tabName) {
|
|
||||||
return function () {
|
|
||||||
this.setState({currentTab: tabName})
|
|
||||||
}.bind(this)
|
|
||||||
},
|
|
||||||
saveUserInfo: function () {
|
|
||||||
this.setState({
|
|
||||||
userSubmitStatus: 'sending'
|
|
||||||
}, function () {
|
|
||||||
Hq.updateUser(this.props.user.name, this.state.user)
|
|
||||||
.then(function (res) {
|
|
||||||
this.setState({userSubmitStatus: 'done'}, function () {
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(res.body))
|
|
||||||
UserStore.Actions.update(res.body)
|
|
||||||
})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({userSubmitStatus: 'error'})
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
savePassword: function () {
|
|
||||||
this.setState({
|
|
||||||
passwordSubmitStatus: 'sending'
|
|
||||||
}, function () {
|
|
||||||
console.log(this.state.password)
|
|
||||||
Hq.changePassword(this.state.password)
|
|
||||||
.then(function (res) {
|
|
||||||
this.setState({
|
|
||||||
passwordSubmitStatus: 'done',
|
|
||||||
currentPassword: '',
|
|
||||||
newPassword: '',
|
|
||||||
passwordConfirmation: ''
|
|
||||||
})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({
|
|
||||||
passwordSubmitStatus: 'error',
|
|
||||||
currentPassword: '',
|
|
||||||
newPassword: '',
|
|
||||||
passwordConfirmation: ''
|
|
||||||
})
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var content
|
|
||||||
|
|
||||||
switch (this.state.currentTab) {
|
|
||||||
case 'userInfo':
|
|
||||||
content = this.renderUserInfoTab()
|
|
||||||
break
|
|
||||||
case 'password':
|
|
||||||
content = this.renderPasswordTab()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='EditProfileModal modal tabModal'>
|
|
||||||
<div className='leftPane'>
|
|
||||||
<div className='tabLabel'>Edit profile</div>
|
|
||||||
<div className='tabList'>
|
|
||||||
<button className={this.state.currentTab === 'userInfo' ? 'active' : ''} onClick={this.selectTab('userInfo')}><i className='fa fa-user fa-fw'/> User Info</button>
|
|
||||||
<button className={this.state.currentTab === 'password' ? 'active' : ''} onClick={this.selectTab('password')}><i className='fa fa-lock fa-fw'/> Password</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='rightPane'>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
renderUserInfoTab: function () {
|
|
||||||
return (
|
|
||||||
<div className='userInfoTab'>
|
|
||||||
<div className='formField'>
|
|
||||||
<label>Profile Name</label>
|
|
||||||
<input valueLink={this.linkState('user.profileName')}/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<label>E-mail</label>
|
|
||||||
<input valueLink={this.linkState('user.email')}/>
|
|
||||||
</div>
|
|
||||||
<div className='formConfirm'>
|
|
||||||
<button disabled={this.state.userSubmitStatus === 'sending'} onClick={this.saveUserInfo}>Save</button>
|
|
||||||
|
|
||||||
<div className={'alertInfo' + (this.state.userSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
|
||||||
|
|
||||||
<div className={'alertError' + (this.state.userSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
|
||||||
|
|
||||||
<div className={'alertSuccess' + (this.state.userSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
renderPasswordTab: function () {
|
|
||||||
return (
|
|
||||||
<div className='passwordTab'>
|
|
||||||
<div className='formField'>
|
|
||||||
<label>Current password</label>
|
|
||||||
<input valueLink={this.linkState('password.currentPassword')}/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<label>New password</label>
|
|
||||||
<input valueLink={this.linkState('password.newPassword')}/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<label>Confirmation</label>
|
|
||||||
<input valueLink={this.linkState('password.passwordConfirmation')}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='formConfirm'>
|
|
||||||
<button disabled={this.state.password.newPassword.length === 0 || this.state.password.newPassword !== this.state.password.passwordConfirmation || this.state.passwordSubmitStatus === 'sending'} onClick={this.savePassword}>Save</button>
|
|
||||||
|
|
||||||
<div className={'alertInfo' + (this.state.passwordSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
|
||||||
|
|
||||||
<div className={'alertError' + (this.state.passwordSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
|
||||||
|
|
||||||
<div className={'alertSuccess' + (this.state.passwordSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var Navigation = ReactRouter.Navigation
|
|
||||||
var State = ReactRouter.State
|
|
||||||
var Link = ReactRouter.Link
|
|
||||||
var Reflux = require('reflux')
|
|
||||||
|
|
||||||
var Modal = require('../Mixins/Modal')
|
|
||||||
|
|
||||||
var UserStore = require('../Stores/UserStore')
|
|
||||||
|
|
||||||
var AboutModal = require('./AboutModal')
|
|
||||||
var PlanetCreateModal = require('./PlanetCreateModal')
|
|
||||||
var TeamCreateModal = require('./TeamCreateModal')
|
|
||||||
var LogoutModal = require('./LogoutModal')
|
|
||||||
var ProfileImage = require('./ProfileImage')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [Navigation, State, Reflux.listenTo(UserStore, 'onUserChange'), Modal],
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
isPlanetCreateModalOpen: false,
|
|
||||||
currentUser: JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUserChange: function (res) {
|
|
||||||
switch (res.status) {
|
|
||||||
case 'userUpdated':
|
|
||||||
var user = res.data
|
|
||||||
var currentUser = this.state.currentUser
|
|
||||||
if (currentUser.id === user.id) {
|
|
||||||
this.setState({currentUser: user})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.userType === 'team') {
|
|
||||||
var isMyTeam = user.Members.some(function (member) {
|
|
||||||
if (currentUser.id === member.id) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isMyTeam) {
|
|
||||||
var isNew = !currentUser.Teams.some(function (team, index) {
|
|
||||||
if (user.id === team.id) {
|
|
||||||
currentUser.Teams.splice(index, 1, user)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isNew) {
|
|
||||||
currentUser.Teams.push(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({currentUser: currentUser})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openTeamCreateModal: function () {
|
|
||||||
this.openModal(TeamCreateModal, {user: this.state.currentUser, transitionTo: this.transitionTo})
|
|
||||||
},
|
|
||||||
openAboutModal: function () {
|
|
||||||
this.openModal(AboutModal)
|
|
||||||
},
|
|
||||||
openPlanetCreateModal: function () {
|
|
||||||
this.openModal(PlanetCreateModal, {transitionTo: this.transitionTo})
|
|
||||||
},
|
|
||||||
toggleProfilePopup: function () {
|
|
||||||
this.openProfilePopup()
|
|
||||||
},
|
|
||||||
openProfilePopup: function () {
|
|
||||||
this.setState({isProfilePopupOpen: true}, function () {
|
|
||||||
document.addEventListener('click', this.closeProfilePopup)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeProfilePopup: function () {
|
|
||||||
document.removeEventListener('click', this.closeProfilePopup)
|
|
||||||
this.setState({isProfilePopupOpen: false})
|
|
||||||
},
|
|
||||||
handleLogoutClick: function () {
|
|
||||||
this.openModal(LogoutModal, {transitionTo: this.transitionTo})
|
|
||||||
},
|
|
||||||
switchPlanetByIndex: function (index) {
|
|
||||||
var planetProps = this.refs.planets.props.children[index - 1].props
|
|
||||||
this.transitionTo('planet', {userName: planetProps.userName, planetName: planetProps.planetName})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var params = this.getParams()
|
|
||||||
|
|
||||||
if (this.state.currentUser == null) {
|
|
||||||
return (
|
|
||||||
<div className='HomeNavigator'>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var planets = (this.state.currentUser.Planets.concat(this.state.currentUser.Teams.reduce(function (planets, team) {
|
|
||||||
return team.Planets == null ? planets : planets.concat(team.Planets)
|
|
||||||
}, []))).map(function (planet, index) {
|
|
||||||
return (
|
|
||||||
<li userName={planet.userName} planetName={planet.name} key={planet.id} className={params.userName === planet.userName && params.planetName === planet.name ? 'active' : ''}>
|
|
||||||
<Link to='planet' params={{userName: planet.userName, planetName: planet.name}}>
|
|
||||||
{planet.name[0]}
|
|
||||||
<div className='planetTooltip'>{planet.userName}/{planet.name}</div>
|
|
||||||
</Link>
|
|
||||||
{index < 9 ? (<div className='shortCut'>⌘{index + 1}</div>) : null}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
var popup = this.renderPopup()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='HomeNavigator'>
|
|
||||||
<button onClick={this.toggleProfilePopup} className='profileButton'>
|
|
||||||
<ProfileImage size='55' email={this.state.currentUser.email}/>
|
|
||||||
</button>
|
|
||||||
{popup}
|
|
||||||
<ul ref='planets' className='planetList'>
|
|
||||||
{planets}
|
|
||||||
</ul>
|
|
||||||
<button onClick={this.openPlanetCreateModal} className='newPlanet'>
|
|
||||||
<i className='fa fa-plus'/>
|
|
||||||
<div className='tooltip'>Create new planet</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
renderPopup: function () {
|
|
||||||
var teams = this.state.currentUser.Teams == null ? [] : this.state.currentUser.Teams.map(function (team) {
|
|
||||||
return (
|
|
||||||
<li key={'user-' + team.id}>
|
|
||||||
<Link to='userHome' params={{userName: team.name}} className='userName'>{team.profileName} ({team.name})</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref='profilePopup' className={'profilePopup' + (this.state.isProfilePopupOpen ? '' : ' close')}>
|
|
||||||
<div className='profileGroup'>
|
|
||||||
<div className='profileGroupLabel'>
|
|
||||||
<span>You</span>
|
|
||||||
</div>
|
|
||||||
<ul className='profileGroupList'>
|
|
||||||
<li>
|
|
||||||
<Link to='userHome' params={{userName: this.state.currentUser.name}} className='userName'>Profile ({this.state.currentUser.name})</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='profileGroup'>
|
|
||||||
<div className='profileGroupLabel'>
|
|
||||||
<span>Team</span>
|
|
||||||
</div>
|
|
||||||
<ul className='profileGroupList'>
|
|
||||||
{teams}
|
|
||||||
<li>
|
|
||||||
<button onClick={this.openTeamCreateModal} className='createNewTeam'><i className='fa fa-plus-square-o'/> create new team</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className='controlGroup'>
|
|
||||||
<li>
|
|
||||||
<button onClick={this.openAboutModal}><i className='fa fa-info-circle fa-fw'/> About this app</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button onClick={this.handleLogoutClick}><i className='fa fa-sign-out fa-fw'/> Log out</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var CodeForm = require('./CodeForm')
|
|
||||||
var NoteForm = require('./NoteForm')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
planet: React.PropTypes.object,
|
|
||||||
transitionTo: React.PropTypes.func,
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
currentTab: 'code'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
var codeButton = React.findDOMNode(this.refs.codeButton)
|
|
||||||
codeButton.addEventListener('keydown', this.handleKeyDown)
|
|
||||||
React.findDOMNode(this.refs.noteButton).addEventListener('keydown', this.handleKeyDown)
|
|
||||||
codeButton.focus()
|
|
||||||
},
|
|
||||||
componentWillUnmount: function () {
|
|
||||||
React.findDOMNode(this.refs.codeButton).removeEventListener('keydown', this.handleKeyDown)
|
|
||||||
React.findDOMNode(this.refs.noteButton).removeEventListener('keydown', this.handleKeyDown)
|
|
||||||
},
|
|
||||||
handleKeyDown: function (e) {
|
|
||||||
if (e.keyCode === 37 && e.metaKey) {
|
|
||||||
this.selectCodeTab()
|
|
||||||
e.stopPropagation()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.keyCode === 39 && e.metaKey) {
|
|
||||||
this.selectNoteTab()
|
|
||||||
e.stopPropagation()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.keyCode === 9) {
|
|
||||||
if (this.state.currentTab === 'code') React.findDOMNode(this.refs.form.refs.description).focus()
|
|
||||||
else React.findDOMNode(this.refs.form.refs.title).focus()
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectCodeTab: function () {
|
|
||||||
this.setState({currentTab: 'code'}, function () {
|
|
||||||
React.findDOMNode(this.refs.codeButton).focus()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectNoteTab: function () {
|
|
||||||
this.setState({currentTab: 'note'}, function () {
|
|
||||||
React.findDOMNode(this.refs.noteButton).focus()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var modalBody
|
|
||||||
if (this.state.currentTab === 'code') {
|
|
||||||
modalBody = (
|
|
||||||
<CodeForm ref='form' planet={this.props.planet} transitionTo={this.props.transitionTo} close={this.props.close}/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
modalBody = (
|
|
||||||
<NoteForm ref='form' planet={this.props.planet} transitionTo={this.props.transitionTo} close={this.props.close}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='LaunchModal modal'>
|
|
||||||
<div className='modal-header'>
|
|
||||||
<div className='modal-tab'>
|
|
||||||
<button ref='codeButton' className={this.state.currentTab === 'code' ? 'btn-primary active' : 'btn-default'} onClick={this.selectCodeTab}>Code</button>
|
|
||||||
<button ref='noteButton' className={this.state.currentTab === 'note' ? 'btn-primary active' : 'btn-default'} onClick={this.selectNoteTab}>Note</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{modalBody}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react')
|
|
||||||
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [KeyCaster('logoutModal')],
|
|
||||||
propTypes: {
|
|
||||||
transitionTo: React.PropTypes.func,
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
case 'submitLogoutModal':
|
|
||||||
this.logout()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logout: function () {
|
|
||||||
localStorage.removeItem('currentUser')
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
this.props.transitionTo('login')
|
|
||||||
this.props.close()
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='LogoutModal modal'>
|
|
||||||
<div className='messageLabel'>Are you sure to log out?</div>
|
|
||||||
<div className='formControl'>
|
|
||||||
<button onClick={this.props.close}>Cancel</button>
|
|
||||||
<button className='logoutButton' onClick={this.logout}>Log out</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
var React = require('react')
|
|
||||||
|
|
||||||
var Markdown = require('../Mixins/Markdown')
|
|
||||||
var ExternalLink = require('../Mixins/ExternalLink')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [Markdown, ExternalLink],
|
|
||||||
propTypes: {
|
|
||||||
className: React.PropTypes.string,
|
|
||||||
content: React.PropTypes.string
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
this.addListener()
|
|
||||||
},
|
|
||||||
componentDidUpdate: function () {
|
|
||||||
this.addListener()
|
|
||||||
},
|
|
||||||
componentWillUnmount: function () {
|
|
||||||
this.removeListener()
|
|
||||||
},
|
|
||||||
componentWillUpdate: function () {
|
|
||||||
this.removeListener()
|
|
||||||
},
|
|
||||||
addListener: function () {
|
|
||||||
var anchors = React.findDOMNode(this).querySelectorAll('a')
|
|
||||||
|
|
||||||
for (var i = 0; i < anchors.length; i++) {
|
|
||||||
anchors[i].addEventListener('click', this.openExternal)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeListener: function () {
|
|
||||||
var anchors = React.findDOMNode(this).querySelectorAll('a')
|
|
||||||
|
|
||||||
for (var i = 0; i < anchors.length; i++) {
|
|
||||||
anchors[i].removeEventListener('click', this.openExternal)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '')} dangerouslySetInnerHTML={{__html: ' ' + this.markdown(this.props.content)}}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
var React = require('react')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var PlanetStore = require('../Stores/PlanetStore')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [KeyCaster('noteDeleteModal')],
|
|
||||||
propTypes: {
|
|
||||||
planet: React.PropTypes.object,
|
|
||||||
note: React.PropTypes.object,
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'submitNoteDeleteModal':
|
|
||||||
this.submit()
|
|
||||||
break
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submit: function () {
|
|
||||||
var planet = this.props.planet
|
|
||||||
Hq.destroyNote(planet.userName, planet.name, this.props.note.localId)
|
|
||||||
.then(function (res) {
|
|
||||||
PlanetStore.Actions.destroyNote(res.body)
|
|
||||||
this.props.close()
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='NoteDeleteModal modal'>
|
|
||||||
<div className='modal-header'>
|
|
||||||
<h1>Delete Note</h1>
|
|
||||||
</div>
|
|
||||||
<div className='modal-body'>
|
|
||||||
<p>Are you sure to delete it?</p>
|
|
||||||
</div>
|
|
||||||
<div className='modal-footer'>
|
|
||||||
<div className='modal-control'>
|
|
||||||
<button onClick={this.props.close} className='btn-default'>Cancel</button>
|
|
||||||
<button ref='submit' onClick={this.submit} className='btn-primary'>Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
var React = require('react')
|
|
||||||
|
|
||||||
var NoteForm = require('./NoteForm')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
close: React.PropTypes.func,
|
|
||||||
note: React.PropTypes.object,
|
|
||||||
planet: React.PropTypes.object
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
// TODO: Hacked!! should fix later
|
|
||||||
setTimeout(function () {
|
|
||||||
React.findDOMNode(this.refs.form.refs.title).focus()
|
|
||||||
}.bind(this), 1)
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='NoteEditModal modal'>
|
|
||||||
<div className='modal-header'>
|
|
||||||
<h1>Edit Note</h1>
|
|
||||||
</div>
|
|
||||||
<NoteForm ref='form' note={this.props.note} planet={this.props.planet} close={this.props.close}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var Select = require('react-select')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var Markdown = require('../Mixins/Markdown')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var PlanetStore = require('../Stores/PlanetStore')
|
|
||||||
|
|
||||||
var CodeEditor = require('./CodeEditor')
|
|
||||||
var MarkdownPreview = require('./MarkdownPreview')
|
|
||||||
|
|
||||||
var getOptions = function (input, callback) {
|
|
||||||
Hq.searchTag(input)
|
|
||||||
.then(function (res) {
|
|
||||||
callback(null, {
|
|
||||||
options: res.body.map(function (tag) {
|
|
||||||
return {
|
|
||||||
label: tag.name,
|
|
||||||
value: tag.name
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
complete: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var EDIT_MODE = 0
|
|
||||||
var PREVIEW_MODE = 1
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, Markdown, KeyCaster('noteForm')],
|
|
||||||
propTypes: {
|
|
||||||
planet: React.PropTypes.object,
|
|
||||||
close: React.PropTypes.func,
|
|
||||||
transitionTo: React.PropTypes.func,
|
|
||||||
note: React.PropTypes.object
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
var note = Object.assign({
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
Tags: []
|
|
||||||
}, this.props.note)
|
|
||||||
note.Tags = note.Tags.map(function (tag) {
|
|
||||||
return {
|
|
||||||
label: tag.name,
|
|
||||||
value: tag.name
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
note: note,
|
|
||||||
mode: EDIT_MODE
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'submitNoteForm':
|
|
||||||
this.submit()
|
|
||||||
break
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleTagsChange: function (selected, all) {
|
|
||||||
var note = this.state.note
|
|
||||||
note.Tags = all
|
|
||||||
this.setState({note: note})
|
|
||||||
},
|
|
||||||
handleContentChange: function (e, value) {
|
|
||||||
var note = this.state.note
|
|
||||||
note.content = value
|
|
||||||
this.setState({note: note})
|
|
||||||
},
|
|
||||||
togglePreview: function () {
|
|
||||||
this.setState({mode: this.state.mode === EDIT_MODE ? PREVIEW_MODE : EDIT_MODE})
|
|
||||||
},
|
|
||||||
submit: function () {
|
|
||||||
var planet = this.props.planet
|
|
||||||
var note = this.state.note
|
|
||||||
note.Tags = note.Tags.map(function (tag) {
|
|
||||||
return tag.value
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.props.note == null) {
|
|
||||||
Hq.createNote(planet.userName, planet.name, this.state.note)
|
|
||||||
.then(function (res) {
|
|
||||||
var note = res.body
|
|
||||||
PlanetStore.Actions.updateNote(note)
|
|
||||||
this.props.close()
|
|
||||||
this.props.transitionTo('notes', {userName: planet.userName, planetName: planet.name, localId: note.localId})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Hq.updateNote(planet.userName, planet.name, this.props.note.localId, this.state.note)
|
|
||||||
.then(function (res) {
|
|
||||||
var note = res.body
|
|
||||||
PlanetStore.Actions.updateNote(note)
|
|
||||||
this.props.close()
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var content = this.state.mode === EDIT_MODE ? (
|
|
||||||
<div className='form-group'>
|
|
||||||
<CodeEditor onChange={this.handleContentChange} code={this.state.note.content} mode={'markdown'}/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='form-group relative'>
|
|
||||||
<div className='previewMode'>Preview mode</div>
|
|
||||||
<MarkdownPreview className='marked' content={this.state.note.content}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='NoteForm'>
|
|
||||||
<div className='modal-body'>
|
|
||||||
<div className='form-group'>
|
|
||||||
<input ref='title' className='block-input' valueLink={this.linkState('note.title')} placeholder='Title'/>
|
|
||||||
</div>
|
|
||||||
{content}
|
|
||||||
<div className='form-group'>
|
|
||||||
<Select
|
|
||||||
name='Tags'
|
|
||||||
multi={true}
|
|
||||||
allowCreate={true}
|
|
||||||
value={this.state.note.Tags}
|
|
||||||
placeholder='Tags...'
|
|
||||||
asyncOptions={getOptions}
|
|
||||||
onChange={this.handleTagsChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='modal-footer'>
|
|
||||||
<button onClick={this.togglePreview} className={'btn-default' + (this.state.mode === PREVIEW_MODE ? ' active' : '')}>Preview mode</button>
|
|
||||||
<div className='modal-control'>
|
|
||||||
<button onClick={this.props.close} className='btn-default'>Cancel</button>
|
|
||||||
<button onClick={this.submit} className='btn-primary'>Launch</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var moment = require('moment')
|
|
||||||
|
|
||||||
var CodeViewer = require('./CodeViewer')
|
|
||||||
var CodeEditModal = require('./CodeEditModal')
|
|
||||||
var CodeDeleteModal = require('./CodeDeleteModal')
|
|
||||||
var NoteEditModal = require('./NoteEditModal')
|
|
||||||
var NoteDeleteModal = require('./NoteDeleteModal')
|
|
||||||
var MarkdownPreview = require('./MarkdownPreview')
|
|
||||||
var ProfileImage = require('./ProfileImage')
|
|
||||||
|
|
||||||
var Modal = require('../Mixins/Modal')
|
|
||||||
var ForceUpdate = require('../Mixins/ForceUpdate')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [ForceUpdate(60000), Modal],
|
|
||||||
propTypes: {
|
|
||||||
article: React.PropTypes.object,
|
|
||||||
showOnlyWithTag: React.PropTypes.func,
|
|
||||||
planet: React.PropTypes.object
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
isEditModalOpen: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openEditModal: function () {
|
|
||||||
if (this.props.article == null) return
|
|
||||||
switch (this.props.article.type) {
|
|
||||||
case 'code' :
|
|
||||||
this.openModal(CodeEditModal, {code: this.props.article, planet: this.props.planet})
|
|
||||||
break
|
|
||||||
case 'note' :
|
|
||||||
this.openModal(NoteEditModal, {note: this.props.article, planet: this.props.planet})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openDeleteModal: function () {
|
|
||||||
if (this.props.article == null) return
|
|
||||||
switch (this.props.article.type) {
|
|
||||||
case 'code' :
|
|
||||||
this.openModal(CodeDeleteModal, {code: this.props.article, planet: this.props.planet})
|
|
||||||
break
|
|
||||||
case 'note' :
|
|
||||||
this.openModal(NoteDeleteModal, {note: this.props.article, planet: this.props.planet})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var article = this.props.article
|
|
||||||
if (article == null) {
|
|
||||||
return (
|
|
||||||
<div className='PlanetArticleDetail'>
|
|
||||||
Nothing selected
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var tags = article.Tags.length > 0 ? article.Tags.map(function (tag) {
|
|
||||||
return (
|
|
||||||
<a onClick={this.props.showOnlyWithTag(tag.name)} key={tag.id}>#{tag.name}</a>
|
|
||||||
)
|
|
||||||
}.bind(this)) : (
|
|
||||||
<a className='noTag'>Not tagged yet</a>
|
|
||||||
)
|
|
||||||
if (article.type === 'code') {
|
|
||||||
return (
|
|
||||||
<div className='PlanetArticleDetail codeDetail'>
|
|
||||||
<div className='detailHeader'>
|
|
||||||
<div className='itemLeft'>
|
|
||||||
<ProfileImage className='profileImage' size='25' email={article.User.email}/>
|
|
||||||
<i className='fa fa-file-text-o fa-fw'></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='itemRight'>
|
|
||||||
<div className='itemInfo'>{moment(article.updatedAt).fromNow()} by <span className='userProfileName'>{article.User.profileName}</span></div>
|
|
||||||
<div className='description'>{article.description}</div>
|
|
||||||
<div className='tags'><i className='fa fa-tags'/>{tags}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className='itemControl'>
|
|
||||||
<button id='articleEditButton' onClick={this.openEditModal} className='editButton'>
|
|
||||||
<i className='fa fa-edit fa-fw'></i>
|
|
||||||
<div className='tooltip'>Edit</div>
|
|
||||||
</button>
|
|
||||||
<button onClick={this.openDeleteModal} className='deleteButton'>
|
|
||||||
<i className='fa fa-trash fa-fw'></i>
|
|
||||||
<div className='tooltip'>Delete</div>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='detailBody'>
|
|
||||||
<CodeViewer className='content' code={article.content} mode={article.mode}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className='PlanetArticleDetail noteDetail'>
|
|
||||||
<div className='detailHeader'>
|
|
||||||
<div className='itemLeft'>
|
|
||||||
<ProfileImage className='profileImage' size='25' email={article.User.email}/>
|
|
||||||
<i className='fa fa-file-text-o fa-fw'></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='itemRight'>
|
|
||||||
<div className='itemInfo'>{moment(article.updatedAt).fromNow()} by <span className='userProfileName'>{article.User.profileName}</span></div>
|
|
||||||
<div className='description'>{article.title}</div>
|
|
||||||
<div className='tags'><i className='fa fa-tags'/>{tags}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className='itemControl'>
|
|
||||||
<button id='articleEditButton' onClick={this.openEditModal} className='editButton'>
|
|
||||||
<i className='fa fa-edit fa-fw'></i>
|
|
||||||
<div className='tooltip'>Edit</div>
|
|
||||||
</button>
|
|
||||||
<button onClick={this.openDeleteModal} className='deleteButton'>
|
|
||||||
<i className='fa fa-trash fa-fw'></i>
|
|
||||||
<div className='tooltip'>Delete</div>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='detailBody'>
|
|
||||||
<MarkdownPreview className='content' content={article.content}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var moment = require('moment')
|
|
||||||
|
|
||||||
var ForceUpdate = require('../Mixins/ForceUpdate')
|
|
||||||
var Markdown = require('../Mixins/Markdown')
|
|
||||||
|
|
||||||
var ProfileImage = require('../Components/ProfileImage')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [ReactRouter.Navigation, ReactRouter.State, ForceUpdate(60000), Markdown],
|
|
||||||
propTypes: {
|
|
||||||
articles: React.PropTypes.array,
|
|
||||||
showOnlyWithTag: React.PropTypes.func
|
|
||||||
},
|
|
||||||
handleArticleClikck: function (article) {
|
|
||||||
if (article.type === 'code') {
|
|
||||||
return function (e) {
|
|
||||||
var params = this.getParams()
|
|
||||||
|
|
||||||
document.getElementById('articleEditButton').focus()
|
|
||||||
this.transitionTo('codes', {
|
|
||||||
userName: params.userName,
|
|
||||||
planetName: params.planetName,
|
|
||||||
localId: article.localId
|
|
||||||
})
|
|
||||||
}.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (article.type === 'note') {
|
|
||||||
return function (e) {
|
|
||||||
var params = this.getParams()
|
|
||||||
|
|
||||||
document.getElementById('articleEditButton').focus()
|
|
||||||
this.transitionTo('notes', {
|
|
||||||
userName: params.userName,
|
|
||||||
planetName: params.planetName,
|
|
||||||
localId: article.localId
|
|
||||||
})
|
|
||||||
}.bind(this)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var articles = this.props.articles.map(function (article) {
|
|
||||||
var tags = article.Tags.length > 0 ? article.Tags.map(function (tag) {
|
|
||||||
return (
|
|
||||||
<a onClick={this.props.showOnlyWithTag(tag.name)} key={tag.id}>#{tag.name}</a>
|
|
||||||
)
|
|
||||||
}.bind(this)) : (
|
|
||||||
<a className='noTag'>Not tagged yet</a>
|
|
||||||
)
|
|
||||||
var params = this.getParams()
|
|
||||||
var isActive = article.type === 'code' ? this.isActive('codes') && parseInt(params.localId, 10) === article.localId : this.isActive('notes') && parseInt(params.localId, 10) === article.localId
|
|
||||||
|
|
||||||
if (article.type === 'code') {
|
|
||||||
return (
|
|
||||||
<li onClick={this.handleArticleClikck(article)} key={'code-' + article.id}>
|
|
||||||
<div className={'articleItem' + (isActive ? ' active' : '')}>
|
|
||||||
<div className='itemLeft'>
|
|
||||||
<ProfileImage className='profileImage' size='25' email={article.User.email}/>
|
|
||||||
<i className='fa fa-code fa-fw'></i>
|
|
||||||
</div>
|
|
||||||
<div className='itemRight'>
|
|
||||||
<div className='itemInfo'>{moment(article.updatedAt).fromNow()} by <span className='userProfileName'>{article.User.profileName}</span></div>
|
|
||||||
<div className='description'>{article.description}</div>
|
|
||||||
<div className='tags'><i className='fa fa-tags'/>{tags}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='divider'></div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li onClick={this.handleArticleClikck(article)} key={'note-' + article.id}>
|
|
||||||
<div className={'articleItem blueprintItem' + (isActive ? ' active' : '')}>
|
|
||||||
<div className='itemLeft'>
|
|
||||||
<ProfileImage className='profileImage' size='25' email={article.User.email}/>
|
|
||||||
<i className='fa fa-file-text-o fa-fw'></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='itemRight'>
|
|
||||||
<div className='itemInfo'>{moment(article.updatedAt).fromNow()} by <span className='userProfileName'>{article.User.profileName}</span></div>
|
|
||||||
<div className='description'>{article.title}</div>
|
|
||||||
<div className='tags'><i className='fa fa-tags'/>{tags}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='divider'></div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='PlanetArticleList'>
|
|
||||||
<ul ref='articles'>
|
|
||||||
{articles}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var PlanetStore = require('../Stores/PlanetStore')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, KeyCaster('planetCreateModal')],
|
|
||||||
propTypes: {
|
|
||||||
ownerName: React.PropTypes.string,
|
|
||||||
transitionTo: React.PropTypes.func,
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
var ownerName = this.props.ownerName != null ? this.props.ownerName : currentUser.name
|
|
||||||
return {
|
|
||||||
user: currentUser,
|
|
||||||
planet: {
|
|
||||||
name: '',
|
|
||||||
public: true
|
|
||||||
},
|
|
||||||
ownerName: ownerName
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
React.findDOMNode(this.refs.name).focus()
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
case 'submitPlanetCreateModal':
|
|
||||||
this.handleSubmit()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSubmit: function () {
|
|
||||||
this.setState({errorMessage: null}, function () {
|
|
||||||
Hq.createPlanet(this.state.ownerName, this.state.planet)
|
|
||||||
.then(function (res) {
|
|
||||||
var planet = res.body
|
|
||||||
|
|
||||||
PlanetStore.Actions.update(planet)
|
|
||||||
|
|
||||||
if (this.props.transitionTo != null) {
|
|
||||||
this.props.transitionTo('planetHome', {userName: planet.userName, planetName: planet.name})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.close()
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
if (err.status === 403) {
|
|
||||||
this.setState({errorMessage: err.response.body.message})
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var teamOptions = this.state.user.Teams.map(function (team) {
|
|
||||||
return (
|
|
||||||
<option key={'user-' + team.id} value={team.name}>{team.profileName} ({team.name})</option>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div className='PlanetCreateModal modal'>
|
|
||||||
<input ref='name' valueLink={this.linkState('planet.name')} className='nameInput stripInput' placeholder='Crate new Planet'/>
|
|
||||||
|
|
||||||
<div className='formField'>
|
|
||||||
of
|
|
||||||
<select valueLink={this.linkState('ownerName')}>
|
|
||||||
<option value={this.state.user.name}>Me({this.state.user.name})</option>
|
|
||||||
{teamOptions}
|
|
||||||
</select>
|
|
||||||
as
|
|
||||||
<select valueLink={this.linkState('planet.public')}>
|
|
||||||
<option value={true}>Public</option>
|
|
||||||
<option value={false}>Private</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.errorMessage != null ? (<p className='errorAlert'>{this.state.errorMessage}</p>) : null}
|
|
||||||
|
|
||||||
<button onClick={this.handleSubmit} className='submitButton'>
|
|
||||||
<i className='fa fa-check'/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var Link = ReactRouter.Link
|
|
||||||
|
|
||||||
var Modal = require('../Mixins/Modal')
|
|
||||||
var ExternalLink = require('../Mixins/ExternalLink')
|
|
||||||
|
|
||||||
var PlanetSettingModal = require('./PlanetSettingModal')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [ReactRouter.State, Modal, ExternalLink],
|
|
||||||
propTypes: {
|
|
||||||
search: React.PropTypes.string,
|
|
||||||
fetchPlanet: React.PropTypes.func,
|
|
||||||
onSearchChange: React.PropTypes.func,
|
|
||||||
currentPlanet: React.PropTypes.object
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
search: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
var search = React.findDOMNode(this.refs.search)
|
|
||||||
search.addEventListener('keydown', this.handleSearchKeyDown)
|
|
||||||
},
|
|
||||||
componentWillUnmount: function () {
|
|
||||||
var search = React.findDOMNode(this.refs.search)
|
|
||||||
search.removeEventListener('keydown', this.handleSearchKeyDown)
|
|
||||||
},
|
|
||||||
handleSearchKeyDown: function (e) {
|
|
||||||
if (e.keyCode === 38 || e.keyCode === 40) {
|
|
||||||
var search = React.findDOMNode(this.refs.search)
|
|
||||||
search.blur()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
if (e.keyCode !== 27 && (e.keyCode !== 13 || !e.metaKey)) {
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openPlanetSettingModal: function () {
|
|
||||||
this.openModal(PlanetSettingModal, {planet: this.props.currentPlanet})
|
|
||||||
},
|
|
||||||
refresh: function () {
|
|
||||||
this.props.fetchPlanet()
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var currentPlanetName = this.props.currentPlanet.name
|
|
||||||
var currentUserName = this.props.currentPlanet.userName
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='PlanetHeader'>
|
|
||||||
<div className='headerLabel'>
|
|
||||||
<Link to='userHome' params={{userName: currentUserName}} className='userName'>{currentUserName}</Link>
|
|
||||||
<span className='planetName'>{currentPlanetName}</span>
|
|
||||||
|
|
||||||
{this.props.currentPlanet.public ? null : (
|
|
||||||
<div className='private'>
|
|
||||||
<i className='fa fa-lock'/>
|
|
||||||
<div className='tooltip'>Private planet</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button onClick={this.openPlanetSettingModal} className='planetSettingButton'>
|
|
||||||
<i className='fa fa-chevron-down'></i>
|
|
||||||
<div className='tooltip'>Planet setting</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='headerControl'>
|
|
||||||
<div className='searchInput'>
|
|
||||||
<i className='fa fa-search'/>
|
|
||||||
<input onChange={this.props.onSearchChange} value={this.props.search} ref='search' type='text' className='inline-input circleInput' placeholder='Search...'/>
|
|
||||||
</div>
|
|
||||||
<button onClick={this.refresh} className='refreshButton'>
|
|
||||||
<i className='fa fa-refresh'/>
|
|
||||||
<div className='tooltip'>Refresh planet</div>
|
|
||||||
</button>
|
|
||||||
<a onClick={this.openExternal} href='http://b00st.io' className='logo'>
|
|
||||||
<img width='44' height='44' src='resources/favicon-230x230.png'/>
|
|
||||||
<div className='tooltip'>Boost official page</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var Navigation = ReactRouter.Navigation
|
|
||||||
|
|
||||||
var Modal = require('../Mixins/Modal')
|
|
||||||
|
|
||||||
var LaunchModal = require('../Components/LaunchModal')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [Modal, Navigation],
|
|
||||||
propTypes: {
|
|
||||||
planet: React.PropTypes.shape({
|
|
||||||
name: React.PropTypes.string
|
|
||||||
}),
|
|
||||||
search: React.PropTypes.string,
|
|
||||||
toggleCodeFilter: React.PropTypes.func,
|
|
||||||
toggleNoteFilter: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
isLaunchModalOpen: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openLaunchModal: function () {
|
|
||||||
this.openModal(LaunchModal, {planet: this.props.planet, transitionTo: this.transitionTo})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var keywords = this.props.search.split(' ')
|
|
||||||
var usingCodeFilter = keywords.some(function (keyword) {
|
|
||||||
if (keyword === '$c') return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
var usingNoteFilter = keywords.some(function (keyword) {
|
|
||||||
if (keyword === '$n') return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='PlanetNavigator'>
|
|
||||||
<button onClick={this.openLaunchModal} className='launchButton btn-primary btn-block'>
|
|
||||||
<i className='fa fa-rocket fa-fw'/> Launch
|
|
||||||
</button>
|
|
||||||
<nav className='articleFilters'>
|
|
||||||
<a className={usingCodeFilter && !usingNoteFilter ? 'active' : ''} onClick={this.props.toggleCodeFilter}>
|
|
||||||
<i className='fa fa-code fa-fw'/> Codes
|
|
||||||
</a>
|
|
||||||
<a className={!usingCodeFilter && usingNoteFilter ? 'active' : ''} onClick={this.props.toggleNoteFilter}>
|
|
||||||
<i className='fa fa-file-text-o fa-fw'/> Notes
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var PlanetStore = require('../Stores/PlanetStore')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, KeyCaster('planetSettingModal')],
|
|
||||||
propTypes: {
|
|
||||||
close: React.PropTypes.func,
|
|
||||||
planet: React.PropTypes.shape({
|
|
||||||
name: React.PropTypes.string,
|
|
||||||
public: React.PropTypes.bool,
|
|
||||||
userName: React.PropTypes.string
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
var deleteTextCandidates = [
|
|
||||||
'Confirm',
|
|
||||||
'Exterminatus',
|
|
||||||
'Avada Kedavra'
|
|
||||||
]
|
|
||||||
var random = Math.round(Math.random() * 10) % 10
|
|
||||||
var randomDeleteText = random > 1 ? deleteTextCandidates[0] : random === 1 ? deleteTextCandidates[1] : deleteTextCandidates[2]
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentTab: 'profile',
|
|
||||||
planet: {
|
|
||||||
name: this.props.planet.name,
|
|
||||||
public: this.props.planet.public
|
|
||||||
},
|
|
||||||
randomDeleteText: randomDeleteText,
|
|
||||||
deleteConfirmation: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activePlanetProfile: function () {
|
|
||||||
this.setState({currentTab: 'profile'})
|
|
||||||
},
|
|
||||||
activePlanetDelete: function () {
|
|
||||||
this.setState({currentTab: 'delete'})
|
|
||||||
},
|
|
||||||
handlePublicChange: function (value) {
|
|
||||||
return function () {
|
|
||||||
this.state.planet.public = value
|
|
||||||
this.setState({planet: this.state.planet})
|
|
||||||
}.bind(this)
|
|
||||||
},
|
|
||||||
handleSavePlanetProfile: function (e) {
|
|
||||||
var planet = this.props.planet
|
|
||||||
|
|
||||||
this.setState({profileSubmitStatus: 'sending'}, function () {
|
|
||||||
Hq.updatePlanet(planet.userName, planet.name, this.state.planet)
|
|
||||||
.then(function (res) {
|
|
||||||
var planet = res.body
|
|
||||||
|
|
||||||
this.setState({profileSubmitStatus: 'done'})
|
|
||||||
|
|
||||||
PlanetStore.Actions.update(planet)
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
this.setState({profileSubmitStatus: 'error'})
|
|
||||||
console.error(err)
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleDeletePlanetClick: function () {
|
|
||||||
var planet = this.props.planet
|
|
||||||
|
|
||||||
this.setState({deleteSubmitStatus: 'sending'}, function () {
|
|
||||||
Hq.destroyPlanet(planet.userName, planet.name)
|
|
||||||
.then(function (res) {
|
|
||||||
var planet = res.body
|
|
||||||
|
|
||||||
PlanetStore.Actions.destroy(planet)
|
|
||||||
this.setState({deleteSubmitStatus: 'done'}, function () {
|
|
||||||
this.props.close()
|
|
||||||
})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
this.setState({deleteSubmitStatus: 'error'})
|
|
||||||
console.error(err)
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var content
|
|
||||||
|
|
||||||
content = this.state.currentTab === 'profile' ? this.renderPlanetProfileTab() : this.renderPlanetDeleteTab()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='PlanetSettingModal modal tabModal'>
|
|
||||||
<div className='leftPane'>
|
|
||||||
<h1 className='tabLabel'>Planet setting</h1>
|
|
||||||
<nav className='tabList'>
|
|
||||||
<button onClick={this.activePlanetProfile} className={this.state.currentTab === 'profile' ? 'active' : ''}><i className='fa fa-globe fa-fw'/> Planet profile</button>
|
|
||||||
<button onClick={this.activePlanetDelete} className={this.state.currentTab === 'delete' ? 'active' : ''}><i className='fa fa-trash fa-fw'/> Delete Planet</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='rightPane'>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
renderPlanetProfileTab: function () {
|
|
||||||
return (
|
|
||||||
<div className='planetProfileTab'>
|
|
||||||
<div className='formField'>
|
|
||||||
<label>Planet name </label>
|
|
||||||
<input valueLink={this.linkState('planet.name')}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='formRadioField'>
|
|
||||||
<input id='publicOption' checked={this.state.planet.public} onChange={this.handlePublicChange(true)} name='public' type='radio'/> <label htmlFor='publicOption'>Public</label>
|
|
||||||
|
|
||||||
<input id='privateOption' checked={!this.state.planet.public} onChange={this.handlePublicChange(false)} name='public' type='radio'/> <label htmlFor='privateOption'>Private</label>
|
|
||||||
</div>
|
|
||||||
<div className='formConfirm'>
|
|
||||||
<button onClick={this.handleSavePlanetProfile} className='saveButton btn-primary'>Save</button>
|
|
||||||
|
|
||||||
<div className={'alertInfo' + (this.state.profileSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
|
||||||
|
|
||||||
<div className={'alertError' + (this.state.profileSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
|
||||||
|
|
||||||
<div className={'alertSuccess' + (this.state.profileSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
renderPlanetDeleteTab: function () {
|
|
||||||
var disabled = !this.state.deleteConfirmation.match(new RegExp('^' + this.props.planet.userName + '/' + this.props.planet.name + '$'))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='planetDeleteTab'>
|
|
||||||
<p>Are you sure to destroy <strong>'{this.props.planet.userName + '/' + this.props.planet.name}'</strong>?</p>
|
|
||||||
<p>If you are sure, write <strong>'{this.props.planet.userName + '/' + this.props.planet.name}'</strong> to input below and click <strong>'{this.state.randomDeleteText}'</strong> button.</p>
|
|
||||||
<input valueLink={this.linkState('deleteConfirmation')} placeholder='userName/planetName'/>
|
|
||||||
<div className='formConfirm'>
|
|
||||||
<button disabled={disabled} onClick={this.handleDeletePlanetClick}>{this.state.randomDeleteText}</button>
|
|
||||||
|
|
||||||
<div className={'alertInfo' + (this.state.deleteSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
|
||||||
|
|
||||||
<div className={'alertError' + (this.state.deleteSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
|
||||||
|
|
||||||
<div className={'alertSuccess' + (this.state.deleteSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var md5 = require('md5')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
email: React.PropTypes.string,
|
|
||||||
size: React.PropTypes.string,
|
|
||||||
className: React.PropTypes.string
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<img className={this.props.className} width={this.props.size} height={this.props.size} src={'http://www.gravatar.com/avatar/' + md5(this.props.email.trim().toLowerCase()) + '?s=' + this.props.size}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var UserStore = require('../Stores/UserStore')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, KeyCaster('teamCreateModal')],
|
|
||||||
propTypes: {
|
|
||||||
user: React.PropTypes.shape({
|
|
||||||
name: React.PropTypes.string
|
|
||||||
}),
|
|
||||||
transitionTo: React.PropTypes.func,
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
team: {
|
|
||||||
name: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
React.findDOMNode(this.refs.teamName).focus()
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
case 'submitTeamCreateModal':
|
|
||||||
this.handleSubmit()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSubmit: function () {
|
|
||||||
Hq.createTeam(this.props.user.name, this.state.team)
|
|
||||||
.then(function (res) {
|
|
||||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
var team = res.body
|
|
||||||
|
|
||||||
currentUser.Teams.push(team)
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(currentUser))
|
|
||||||
UserStore.Actions.update(currentUser)
|
|
||||||
|
|
||||||
if (this.props.transitionTo != null) {
|
|
||||||
this.props.transitionTo('userHome', {userName: team.name})
|
|
||||||
}
|
|
||||||
this.props.close()
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='TeamCreateModal modal'>
|
|
||||||
<input ref='teamName' valueLink={this.linkState('team.name')} className='nameInput stripInput' placeholder='Create new team'/>
|
|
||||||
|
|
||||||
<button onClick={this.handleSubmit} className='submitButton'>
|
|
||||||
<i className='fa fa-check'/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
var Reflux = require('reflux')
|
|
||||||
var Select = require('react-select')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var Helper = require('../Mixins/Helper')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var UserStore = require('../Stores/UserStore')
|
|
||||||
|
|
||||||
var getOptions = function (input, callback) {
|
|
||||||
Hq.searchUser(input)
|
|
||||||
.then(function (res) {
|
|
||||||
callback(null, {
|
|
||||||
options: res.body.map(function (user) {
|
|
||||||
return {
|
|
||||||
label: user.name,
|
|
||||||
value: user.name
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
complete: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, Reflux.listenTo(UserStore, 'onUserChange'), Helper, KeyCaster('teamSettingsModal')],
|
|
||||||
propTypes: {
|
|
||||||
team: React.PropTypes.shape({
|
|
||||||
id: React.PropTypes.number,
|
|
||||||
name: React.PropTypes.string,
|
|
||||||
profileName: React.PropTypes.string,
|
|
||||||
email: React.PropTypes.string,
|
|
||||||
Members: React.PropTypes.array
|
|
||||||
}),
|
|
||||||
close: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
var team = this.props.team
|
|
||||||
return {
|
|
||||||
currentTab: 'teamInfo',
|
|
||||||
team: {
|
|
||||||
profileName: team.profileName
|
|
||||||
},
|
|
||||||
userSubmitStatus: null,
|
|
||||||
member: {
|
|
||||||
name: '',
|
|
||||||
role: 'member'
|
|
||||||
},
|
|
||||||
updatingMember: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUserChange: function (res) {
|
|
||||||
var member
|
|
||||||
switch (res.status) {
|
|
||||||
case 'memberAdded':
|
|
||||||
member = res.data
|
|
||||||
if (member.TeamMember.TeamId === this.props.team.id) {
|
|
||||||
this.forceUpdate()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'memberRemoved':
|
|
||||||
member = res.data
|
|
||||||
if (member.TeamMember.TeamId === this.props.team.id) {
|
|
||||||
this.forceUpdate()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectTab: function (tabName) {
|
|
||||||
return function () {
|
|
||||||
this.setState({currentTab: tabName})
|
|
||||||
}.bind(this)
|
|
||||||
},
|
|
||||||
saveUserInfo: function () {
|
|
||||||
this.setState({
|
|
||||||
userSubmitStatus: 'sending'
|
|
||||||
}, function () {
|
|
||||||
Hq.updateUser(this.props.team.name, this.state.team)
|
|
||||||
.then(function (res) {
|
|
||||||
this.setState({userSubmitStatus: 'done'}, function () {
|
|
||||||
UserStore.Actions.update(res.body)
|
|
||||||
this.forceUpdate()
|
|
||||||
})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({userSubmitStatus: 'error'})
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleMemberNameChange: function (value) {
|
|
||||||
var member = this.state.member
|
|
||||||
member.name = value
|
|
||||||
this.setState({member: member})
|
|
||||||
},
|
|
||||||
addMember: function () {
|
|
||||||
this.setState({updatingMember: true}, function () {
|
|
||||||
Hq
|
|
||||||
.addMember(this.props.team.name, {
|
|
||||||
userName: this.state.member.name,
|
|
||||||
role: this.state.member.role
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
UserStore.Actions.addMember(res.body)
|
|
||||||
this.setState({updatingMember: false})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({updatingMember: false})
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
roleChange: function (memberName) {
|
|
||||||
return function (e) {
|
|
||||||
var role = e.target.value
|
|
||||||
this.setState({updatingMember: true}, function () {
|
|
||||||
Hq
|
|
||||||
.addMember(this.props.team.name, {
|
|
||||||
userName: memberName,
|
|
||||||
role: role
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
UserStore.Actions.addMember(res.body)
|
|
||||||
this.setState({updatingMember: false})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({updatingMember: false})
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
}.bind(this)
|
|
||||||
},
|
|
||||||
removeMember: function (memberName) {
|
|
||||||
return function () {
|
|
||||||
this.setState({updatingMember: true}, function () {
|
|
||||||
Hq
|
|
||||||
.removeMember(this.props.team.name, {
|
|
||||||
userName: memberName
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
UserStore.Actions.removeMember(res.body)
|
|
||||||
this.setState({updatingMember: false})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({updatingMember: false})
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
}.bind(this)
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var content
|
|
||||||
|
|
||||||
switch (this.state.currentTab) {
|
|
||||||
case 'teamInfo':
|
|
||||||
content = this.renderTeamInfoTab()
|
|
||||||
break
|
|
||||||
case 'members':
|
|
||||||
content = this.renderMembersTab()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='TeamSettingsModal modal tabModal'>
|
|
||||||
<div className='leftPane'>
|
|
||||||
<div className='tabLabel'>Team settings</div>
|
|
||||||
<div className='tabList'>
|
|
||||||
<button className={this.state.currentTab === 'teamInfo' ? 'active' : ''} onClick={this.selectTab('teamInfo')}><i className='fa fa-info-circle fa-fw'/> Team Info</button>
|
|
||||||
<button className={this.state.currentTab === 'members' ? 'active' : ''} onClick={this.selectTab('members')}><i className='fa fa-users fa-fw'/> Members</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='rightPane'>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
renderTeamInfoTab: function () {
|
|
||||||
return (
|
|
||||||
<div className='userInfoTab'>
|
|
||||||
<div className='formField'>
|
|
||||||
<label>Profile Name</label>
|
|
||||||
<input valueLink={this.linkState('team.profileName')}/>
|
|
||||||
</div>
|
|
||||||
<div className='formConfirm'>
|
|
||||||
<button disabled={this.state.userSubmitStatus === 'sending'} onClick={this.saveUserInfo}>Save</button>
|
|
||||||
|
|
||||||
<div className={'alertInfo' + (this.state.userSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
|
||||||
|
|
||||||
<div className={'alertError' + (this.state.userSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
|
||||||
|
|
||||||
<div className={'alertSuccess' + (this.state.userSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
renderMembersTab: function () {
|
|
||||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
|
|
||||||
var members = this.props.team.Members.map(function (member) {
|
|
||||||
var isCurrentUser = currentUser.id === member.id
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td>{member.profileName}({member.name})</td>
|
|
||||||
<td>
|
|
||||||
{isCurrentUser ? (
|
|
||||||
'Owner'
|
|
||||||
) : (
|
|
||||||
<select disabled={this.state.updatingMember} onChange={this.roleChange(member.name)} className='roleSelect' value={member.TeamMember.role}>
|
|
||||||
<option value='owner'>Owner</option>
|
|
||||||
<option value='member'>Member</option>
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{isCurrentUser ? '-' : (
|
|
||||||
<button disabled={this.state.updatingMember} onClick={this.removeMember(member.name)}><i className='fa fa-close fa-fw'/></button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
var belowLimit = members.length < 5
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='membersTab'>
|
|
||||||
<table className='memberTable'>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Control</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{members}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{belowLimit ? (
|
|
||||||
<div className='addMemberForm'>
|
|
||||||
<div className='formLabel'>Add Member</div>
|
|
||||||
<div className='formGroup'>
|
|
||||||
<Select
|
|
||||||
name='userName'
|
|
||||||
value={this.state.member.name}
|
|
||||||
placeholder='Username to add'
|
|
||||||
asyncOptions={getOptions}
|
|
||||||
onChange={this.handleMemberNameChange}
|
|
||||||
className='userNameSelect'
|
|
||||||
/>
|
|
||||||
<select valueLink={this.linkState('member.role')} className='roleSelect'>
|
|
||||||
<option value={'member'}>Member</option>
|
|
||||||
<option value={'owner'}>Owner</option>
|
|
||||||
</select>
|
|
||||||
<button disabled={this.state.updatingMember} onClick={this.addMember} className='confirmButton'>Add Member</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
Maximum number of members is 5 on Beta version. Please contact us if you want futher use.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var RouteHandler = ReactRouter.RouteHandler
|
|
||||||
var State = ReactRouter.State
|
|
||||||
var Navigation = ReactRouter.Navigation
|
|
||||||
|
|
||||||
var AuthFilter = require('../Mixins/AuthFilter')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var HomeNavigator = require('../Components/HomeNavigator')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [AuthFilter.OnlyUser, State, Navigation, KeyCaster('homeContainer')],
|
|
||||||
componentDidMount: function () {
|
|
||||||
if (this.isActive('homeEmpty')) {
|
|
||||||
var user = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
if (user.Planets != null && user.Planets.length > 0) {
|
|
||||||
this.transitionTo('planet', {userName: user.name, planetName: user.Planets[0].name})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.transitionTo('userHome', {userName: user.name})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'switchPlanet':
|
|
||||||
this.refs.navigator.switchPlanetByIndex(e.data)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='HomeContainer'>
|
|
||||||
<HomeNavigator ref='navigator'/>
|
|
||||||
<RouteHandler/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var Link = ReactRouter.Link
|
|
||||||
|
|
||||||
var AuthFilter = require('../Mixins/AuthFilter')
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, ReactRouter.Navigation, AuthFilter.OnlyGuest],
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
user: {},
|
|
||||||
authenticationFailed: false,
|
|
||||||
connectionFailed: false,
|
|
||||||
isSending: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onListen: function (res) {
|
|
||||||
if (res.status === 'failedToLogIn') {
|
|
||||||
if (res.data.status === 401) {
|
|
||||||
// Wrong E-mail or Password
|
|
||||||
this.setState({
|
|
||||||
authenticationFailed: true,
|
|
||||||
connectionFailed: false,
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Connection Failed or Whatever
|
|
||||||
this.setState({
|
|
||||||
authenticationFailed: false,
|
|
||||||
connectionFailed: true,
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSubmit: function (e) {
|
|
||||||
this.setState({
|
|
||||||
authenticationFailed: false,
|
|
||||||
connectionFailed: false,
|
|
||||||
isSending: true
|
|
||||||
}, function () {
|
|
||||||
Hq.login(this.state.user)
|
|
||||||
.then(function (res) {
|
|
||||||
localStorage.setItem('token', res.body.token)
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(res.body.user))
|
|
||||||
|
|
||||||
this.transitionTo('userHome', {userName: res.body.user.name})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
if (err.status === 401) {
|
|
||||||
this.setState({
|
|
||||||
authenticationFailed: true,
|
|
||||||
connectionFailed: false,
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
authenticationFailed: false,
|
|
||||||
connectionFailed: true,
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='LoginContainer'>
|
|
||||||
<img className='logo' src='resources/favicon-230x230.png'/>
|
|
||||||
|
|
||||||
<nav className='authNavigator text-center'><Link to='login'>Log In</Link> / <Link to='signup'>Sign Up</Link></nav>
|
|
||||||
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
<div className='form-group'>
|
|
||||||
<input className='stripInput' valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<input className='stripInput' valueLink={this.linkState('user.password')} onChange={this.handleChange} type='password' placeholder='Password'/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.isSending ? (
|
|
||||||
<p className='alertInfo'>Logging in...</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.state.connectionFailed ? (
|
|
||||||
<p className='alertError'>Please try again.</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.state.authenticationFailed ? (
|
|
||||||
<p className='alertError'>Wrong E-mail or Password.</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className='form-group'>
|
|
||||||
<button className='logInButton' type='submit'>Log In</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var ipc = require('ipc')
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var RouteHandler = ReactRouter.RouteHandler
|
|
||||||
var Navigation = ReactRouter.Navigation
|
|
||||||
var State = ReactRouter.State
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var Modal = require('../Mixins/Modal')
|
|
||||||
|
|
||||||
var UserStore = require('../Stores/UserStore')
|
|
||||||
|
|
||||||
var ContactModal = require('../Components/ContactModal')
|
|
||||||
|
|
||||||
function fetchPlanet (userName, planetName) {
|
|
||||||
Hq.fetchPlanet(userName, planetName)
|
|
||||||
.then(function (res) {
|
|
||||||
var planet = res.body
|
|
||||||
|
|
||||||
planet.Codes.forEach(function (code) {
|
|
||||||
code.type = 'code'
|
|
||||||
})
|
|
||||||
|
|
||||||
planet.Notes.forEach(function (note) {
|
|
||||||
note.type = 'note'
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('planet-' + planet.id + ' fetched!')
|
|
||||||
localStorage.setItem('planet-' + planet.id, JSON.stringify(planet))
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [State, Navigation, Modal],
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
updateAvailable: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
ipc.on('update-available', function (message) {
|
|
||||||
this.setState({updateAvailable: true})
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
if (this.isActive('root')) {
|
|
||||||
if (localStorage.getItem('currentUser') == null) {
|
|
||||||
this.transitionTo('login')
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
this.transitionTo('home')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Hq.getUser()
|
|
||||||
.then(function (res) {
|
|
||||||
var user = res.body
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(user))
|
|
||||||
UserStore.Actions.update(user)
|
|
||||||
|
|
||||||
user.Planets.forEach(function (planet) {
|
|
||||||
fetchPlanet(planet.userName, planet.name)
|
|
||||||
})
|
|
||||||
user.Teams.forEach(function (team) {
|
|
||||||
team.Planets.forEach(function (planet) {
|
|
||||||
fetchPlanet(planet.userName, planet.name)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
if (err.status === 401) {
|
|
||||||
console.log('Not logged in yet')
|
|
||||||
localStorage.removeItem('currentUser')
|
|
||||||
this.transitionTo('login')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error(err)
|
|
||||||
}.bind(this))
|
|
||||||
},
|
|
||||||
updateApp: function () {
|
|
||||||
ipc.send('update-app', 'Deal with it.')
|
|
||||||
},
|
|
||||||
openContactModal: function () {
|
|
||||||
this.openModal(ContactModal)
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='Main'>
|
|
||||||
{this.state.updateAvailable ? (
|
|
||||||
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
|
|
||||||
) : null}
|
|
||||||
<button onClick={this.openContactModal} className='contactButton'>
|
|
||||||
<i className='fa fa-paper-plane-o'/>
|
|
||||||
<div className='tooltip'>Contact us</div>
|
|
||||||
</button>
|
|
||||||
<RouteHandler/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
/* global localStorage*/
|
|
||||||
'strict'
|
|
||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var Reflux = require('reflux')
|
|
||||||
|
|
||||||
var PlanetHeader = require('../Components/PlanetHeader')
|
|
||||||
var PlanetNavigator = require('../Components/PlanetNavigator')
|
|
||||||
var PlanetArticleList = require('../Components/PlanetArticleList')
|
|
||||||
var PlanetArticleDetail = require('../Components/PlanetArticleDetail')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var Modal = require('../Mixins/Modal')
|
|
||||||
var ArticleFilter = require('../Mixins/ArticleFilter')
|
|
||||||
var Helper = require('../Mixins/Helper')
|
|
||||||
var KeyCaster = require('../Mixins/KeyCaster')
|
|
||||||
|
|
||||||
var UserStore = require('../Stores/UserStore')
|
|
||||||
var PlanetStore = require('../Stores/PlanetStore')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [ReactRouter.Navigation, ReactRouter.State, Modal, Reflux.listenTo(UserStore, 'onUserChange'), Reflux.listenTo(PlanetStore, 'onPlanetChange'), ArticleFilter, Helper, KeyCaster('planetContainer')],
|
|
||||||
propTypes: {
|
|
||||||
params: React.PropTypes.object,
|
|
||||||
planetName: React.PropTypes.string
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
currentUser: JSON.parse(localStorage.getItem('currentUser')),
|
|
||||||
planet: null,
|
|
||||||
search: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
this.fetchPlanet(this.props.params.userName, this.props.params.planetName)
|
|
||||||
},
|
|
||||||
componentDidUpdate: function () {
|
|
||||||
if (this.isActive('planetHome') && this.refs.list != null && this.refs.list.props.articles.length > 0) {
|
|
||||||
var article = this.refs.list.props.articles[0]
|
|
||||||
var planet = this.state.planet
|
|
||||||
switch (article.type) {
|
|
||||||
case 'code':
|
|
||||||
this.transitionTo('codes', {userName: planet.userName, planetName: planet.name, localId: article.localId})
|
|
||||||
break
|
|
||||||
case 'note':
|
|
||||||
this.transitionTo('notes', {userName: planet.userName, planetName: planet.name, localId: article.localId})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillReceiveProps: function (nextProps) {
|
|
||||||
if (this.state.planet == null) {
|
|
||||||
this.fetchPlanet(nextProps.params.userName, nextProps.params.planetName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.params.userName !== this.state.planet.userName || nextProps.params.planetName !== this.state.planet.name) {
|
|
||||||
this.setState({
|
|
||||||
planet: null
|
|
||||||
}, function () {
|
|
||||||
this.fetchPlanet(nextProps.params.userName, nextProps.params.planetName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyCast: function (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'openLaunchModal':
|
|
||||||
this.refs.navigator.openLaunchModal()
|
|
||||||
break
|
|
||||||
case 'selectNextArticle':
|
|
||||||
this.selectNextArticle()
|
|
||||||
break
|
|
||||||
case 'selectPriorArticle':
|
|
||||||
this.selectPriorArticle()
|
|
||||||
break
|
|
||||||
case 'toggleFocusSearchInput':
|
|
||||||
this.toggleFocusSearchInput()
|
|
||||||
break
|
|
||||||
case 'openEditModal':
|
|
||||||
this.refs.detail.openEditModal()
|
|
||||||
break
|
|
||||||
case 'openDeleteModal':
|
|
||||||
this.refs.detail.openDeleteModal()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPlanetChange: function (res) {
|
|
||||||
if (this.state.planet == null) return
|
|
||||||
|
|
||||||
var planet, code, note, articleIndex, articlesCount
|
|
||||||
switch (res.status) {
|
|
||||||
case 'updated':
|
|
||||||
planet = res.data
|
|
||||||
if (this.state.planet.id === planet.id) {
|
|
||||||
if (this.state.planet.name === planet.name) {
|
|
||||||
this.setState({planet: planet})
|
|
||||||
} else {
|
|
||||||
this.transitionTo('planetHome', {userName: planet.userName, planetName: planet.name})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'destroyed':
|
|
||||||
planet = res.data
|
|
||||||
if (this.state.planet.id === planet.id) {
|
|
||||||
this.transitionTo('userHome', {userName: this.state.planet.userName})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'codeUpdated':
|
|
||||||
code = res.data
|
|
||||||
if (code.PlanetId === this.state.planet.id) {
|
|
||||||
this.state.planet.Codes = this.updateItemToTargetArray(code, this.state.planet.Codes)
|
|
||||||
|
|
||||||
this.setState({planet: this.state.planet})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'noteUpdated':
|
|
||||||
note = res.data
|
|
||||||
if (note.PlanetId === this.state.planet.id) {
|
|
||||||
this.state.planet.Notes = this.updateItemToTargetArray(note, this.state.planet.Notes)
|
|
||||||
|
|
||||||
this.setState({planet: this.state.planet})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'codeDestroyed':
|
|
||||||
code = res.data
|
|
||||||
if (code.PlanetId === this.state.planet.id) {
|
|
||||||
this.state.planet.Codes = this.deleteItemFromTargetArray(code, this.state.planet.Codes)
|
|
||||||
|
|
||||||
if (this.refs.detail.props.article != null && this.refs.detail.props.article.type === code.type && this.refs.detail.props.article.localId === code.localId) {
|
|
||||||
articleIndex = this.getFilteredIndexOfCurrentArticle()
|
|
||||||
articlesCount = this.refs.list.props.articles.length
|
|
||||||
|
|
||||||
this.setState({planet: this.state.planet}, function () {
|
|
||||||
if (articlesCount > 1) {
|
|
||||||
if (articleIndex > 0) {
|
|
||||||
this.selectArticleByListIndex(articleIndex - 1)
|
|
||||||
} else {
|
|
||||||
this.selectArticleByListIndex(articleIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({planet: this.state.planet})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'noteDestroyed':
|
|
||||||
note = res.data
|
|
||||||
if (note.PlanetId === this.state.planet.id) {
|
|
||||||
this.state.planet.Notes = this.deleteItemFromTargetArray(note, this.state.planet.Notes)
|
|
||||||
|
|
||||||
if (this.refs.detail.props.article != null && this.refs.detail.props.article.type === note.type && this.refs.detail.props.article.localId === note.localId) {
|
|
||||||
articleIndex = this.getFilteredIndexOfCurrentArticle()
|
|
||||||
articlesCount = this.refs.list.props.articles.length
|
|
||||||
|
|
||||||
this.setState({planet: this.state.planet}, function () {
|
|
||||||
if (articlesCount > 1) {
|
|
||||||
if (articleIndex > 0) {
|
|
||||||
this.selectArticleByListIndex(articleIndex - 1)
|
|
||||||
} else {
|
|
||||||
this.selectArticleByListIndex(articleIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({planet: this.state.planet})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUserChange: function () {
|
|
||||||
|
|
||||||
},
|
|
||||||
fetchPlanet: function (userName, planetName) {
|
|
||||||
if (userName == null) userName = this.props.params.userName
|
|
||||||
if (planetName == null) planetName = this.props.params.planetName
|
|
||||||
|
|
||||||
Hq.fetchPlanet(userName, planetName)
|
|
||||||
.then(function (res) {
|
|
||||||
var planet = res.body
|
|
||||||
|
|
||||||
planet.Codes.forEach(function (code) {
|
|
||||||
code.type = 'code'
|
|
||||||
})
|
|
||||||
|
|
||||||
planet.Notes.forEach(function (note) {
|
|
||||||
note.type = 'note'
|
|
||||||
})
|
|
||||||
|
|
||||||
localStorage.setItem('planet-' + planet.id, JSON.stringify(planet))
|
|
||||||
|
|
||||||
this.setState({planet: planet})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getFilteredIndexOfCurrentArticle: function () {
|
|
||||||
var params = this.props.params
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
if (this.isActive('codes')) {
|
|
||||||
this.refs.list.props.articles.some(function (_article, _index) {
|
|
||||||
if (_article.type === 'code' && _article.localId === parseInt(params.localId, 10)) {
|
|
||||||
index = _index
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (this.isActive('notes')) {
|
|
||||||
this.refs.list.props.articles.some(function (_article, _index) {
|
|
||||||
if (_article.type === 'note' && _article.localId === parseInt(params.localId, 10)) {
|
|
||||||
index = _index
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return index
|
|
||||||
},
|
|
||||||
selectArticleByListIndex: function (index) {
|
|
||||||
var article = this.refs.list.props.articles[index]
|
|
||||||
var params = this.props.params
|
|
||||||
|
|
||||||
if (article == null) {
|
|
||||||
this.transitionTo('planetHome', params)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var listElement = this.refs.list.refs.articles.getDOMNode()
|
|
||||||
var articleElement = listElement.querySelectorAll('li')[index]
|
|
||||||
|
|
||||||
var overflowBelow = listElement.clientHeight + listElement.scrollTop < articleElement.offsetTop + articleElement.clientHeight
|
|
||||||
if (overflowBelow) {
|
|
||||||
listElement.scrollTop = articleElement.offsetTop + articleElement.clientHeight - listElement.clientHeight
|
|
||||||
}
|
|
||||||
var overflowAbove = listElement.scrollTop > articleElement.offsetTop
|
|
||||||
if (overflowAbove) {
|
|
||||||
listElement.scrollTop = articleElement.offsetTop
|
|
||||||
}
|
|
||||||
|
|
||||||
if (article.type === 'code') {
|
|
||||||
params.localId = article.localId
|
|
||||||
this.transitionTo('codes', params)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (article.type === 'note') {
|
|
||||||
params.localId = article.localId
|
|
||||||
this.transitionTo('notes', params)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectNextArticle: function () {
|
|
||||||
if (this.state.planet == null) return
|
|
||||||
|
|
||||||
var index = this.getFilteredIndexOfCurrentArticle()
|
|
||||||
|
|
||||||
if (index < this.refs.list.props.articles.length - 1) {
|
|
||||||
this.selectArticleByListIndex(index + 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectPriorArticle: function () {
|
|
||||||
if (this.state.planet == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var index = this.getFilteredIndexOfCurrentArticle()
|
|
||||||
|
|
||||||
if (index > 0) {
|
|
||||||
this.selectArticleByListIndex(index - 1)
|
|
||||||
} else {
|
|
||||||
React.findDOMNode(this.refs.header.refs.search).focus()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleFocusSearchInput: function () {
|
|
||||||
var search = React.findDOMNode(this.refs.header.refs.search)
|
|
||||||
if (document.activeElement === search) {
|
|
||||||
React.findDOMNode(this.refs.header.refs.search).blur()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
React.findDOMNode(this.refs.header.refs.search).focus()
|
|
||||||
},
|
|
||||||
handleSearchChange: function (e) {
|
|
||||||
this.setState({search: e.target.value}, function () {
|
|
||||||
this.selectArticleByListIndex(0)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
showAll: function () {
|
|
||||||
this.setState({search: ''})
|
|
||||||
},
|
|
||||||
toggleCodeFilter: function () {
|
|
||||||
var keywords = typeof this.state.search === 'string' ? this.state.search.split(' ') : []
|
|
||||||
|
|
||||||
var usingCodeFilter = false
|
|
||||||
var usingNoteFilter = false
|
|
||||||
keywords = keywords.filter(function (keyword) {
|
|
||||||
if (keyword === '$n') {
|
|
||||||
usingNoteFilter = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (keyword === '$c') usingCodeFilter = true
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (usingCodeFilter && !usingNoteFilter) {
|
|
||||||
keywords = keywords.filter(function (keyword) {
|
|
||||||
return keyword !== '$c'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usingCodeFilter) {
|
|
||||||
keywords.unshift('$c')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({search: keywords.join(' ')}, function () {
|
|
||||||
this.selectArticleByListIndex(0)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
toggleNoteFilter: function () {
|
|
||||||
var keywords = typeof this.state.search === 'string' ? this.state.search.split(' ') : []
|
|
||||||
|
|
||||||
var usingCodeFilter = false
|
|
||||||
var usingNoteFilter = false
|
|
||||||
keywords = keywords.filter(function (keyword) {
|
|
||||||
if (keyword === '$c') {
|
|
||||||
usingCodeFilter = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (keyword === '$n') usingNoteFilter = true
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (usingNoteFilter && !usingCodeFilter) {
|
|
||||||
keywords = keywords.filter(function (keyword) {
|
|
||||||
return keyword !== '$n'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usingNoteFilter) {
|
|
||||||
keywords.unshift('$n')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({search: keywords.join(' ')}, function () {
|
|
||||||
this.selectArticleByListIndex(0)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
applyTagFilter: function (tag) {
|
|
||||||
return function () {
|
|
||||||
this.setState({search: '#' + tag})
|
|
||||||
}.bind(this)
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
if (this.state.planet == null) return (<div/>)
|
|
||||||
|
|
||||||
var localId = parseInt(this.props.params.localId, 10)
|
|
||||||
|
|
||||||
var codes = this.state.planet.Codes
|
|
||||||
var notes = this.state.planet.Notes
|
|
||||||
|
|
||||||
var article
|
|
||||||
if (this.isActive('codes')) {
|
|
||||||
codes.some(function (_article) {
|
|
||||||
if (localId === _article.localId) {
|
|
||||||
article = _article
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
} else if (this.isActive('notes')) {
|
|
||||||
notes.some(function (_article) {
|
|
||||||
if (localId === _article.localId) {
|
|
||||||
article = _article
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var articles = codes.concat(notes)
|
|
||||||
|
|
||||||
var filteredArticles = this.searchArticle(this.state.search, articles)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='PlanetContainer'>
|
|
||||||
<PlanetHeader
|
|
||||||
ref='header'
|
|
||||||
search={this.state.search}
|
|
||||||
fetchPlanet={this.fetchPlanet}
|
|
||||||
onSearchChange={this.handleSearchChange}
|
|
||||||
currentPlanet={this.state.planet}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PlanetNavigator
|
|
||||||
ref='navigator'
|
|
||||||
search={this.state.search}
|
|
||||||
showAll={this.showAll}
|
|
||||||
toggleCodeFilter={this.toggleCodeFilter}
|
|
||||||
toggleNoteFilter={this.toggleNoteFilter}
|
|
||||||
planet={this.state.planet}/>
|
|
||||||
|
|
||||||
<PlanetArticleList showOnlyWithTag={this.applyTagFilter} ref='list' articles={filteredArticles}/>
|
|
||||||
|
|
||||||
<PlanetArticleDetail
|
|
||||||
ref='detail'
|
|
||||||
article={article}
|
|
||||||
planet={this.state.planet}
|
|
||||||
showOnlyWithTag={this.applyTagFilter}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var Link = ReactRouter.Link
|
|
||||||
|
|
||||||
var AuthFilter = require('../Mixins/AuthFilter')
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, ReactRouter.Navigation, AuthFilter.OnlyGuest],
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
user: {},
|
|
||||||
connectionFailed: false,
|
|
||||||
emailConflicted: false,
|
|
||||||
nameConflicted: false,
|
|
||||||
validationFailed: false,
|
|
||||||
isSending: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSubmit: function (e) {
|
|
||||||
this.setState({
|
|
||||||
connectionFailed: false,
|
|
||||||
emailConflicted: false,
|
|
||||||
nameConflicted: false,
|
|
||||||
validationFailed: false,
|
|
||||||
isSending: true
|
|
||||||
}, function () {
|
|
||||||
Hq.signup(this.state.user)
|
|
||||||
.then(function (res) {
|
|
||||||
localStorage.setItem('token', res.body.token)
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(res.body.user))
|
|
||||||
|
|
||||||
this.transitionTo('userHome', {userName: res.body.user.name})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
var res = err.response
|
|
||||||
if (err.status === 409) {
|
|
||||||
// Confliction
|
|
||||||
var emailConflicted = res.body.errors[0].path === 'email'
|
|
||||||
var nameConflicted = res.body.errors[0].path === 'name'
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
connectionFailed: false,
|
|
||||||
emailConflicted: emailConflicted,
|
|
||||||
nameConflicted: nameConflicted,
|
|
||||||
validationFailed: false,
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.status === 422) {
|
|
||||||
// Validation Failed
|
|
||||||
this.setState({
|
|
||||||
connectionFailed: false,
|
|
||||||
emailConflicted: false,
|
|
||||||
nameConflicted: false,
|
|
||||||
validationFailed: {
|
|
||||||
errors: res.body.errors.map(function (error) {
|
|
||||||
return error.path
|
|
||||||
})
|
|
||||||
},
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection Failed or Whatever
|
|
||||||
this.setState({
|
|
||||||
connectionFailed: true,
|
|
||||||
emailConflicted: false,
|
|
||||||
nameConflicted: false,
|
|
||||||
validationFailed: false,
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}.bind(this))
|
|
||||||
})
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div className='SignupContainer'>
|
|
||||||
<img className='logo' src='resources/favicon-230x230.png'/>
|
|
||||||
|
|
||||||
<nav className='authNavigator text-center'><Link to='login'>Log In</Link> / <Link to='signup'>Sign Up</Link></nav>
|
|
||||||
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
<div className='form-group'>
|
|
||||||
<input className='stripInput' valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<input className='stripInput' valueLink={this.linkState('user.password')} type='password' placeholder='Password'/>
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<input className='stripInput' valueLink={this.linkState('user.name')} type='text' placeholder='name'/>
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<input className='stripInput' valueLink={this.linkState('user.profileName')} type='text' placeholder='Profile name'/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.isSending ? (
|
|
||||||
<p className='alertInfo'>Signing up...</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.state.connectionFailed ? (
|
|
||||||
<p className='alertError'>Please try again.</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.state.emailConflicted ? (
|
|
||||||
<p className='alertError'>E-mail already exists.</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.state.nameConflicted ? (
|
|
||||||
<p className='alertError'>Username already exists.</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.state.validationFailed ? (
|
|
||||||
<p className='alertError'>Please fill every field correctly: {this.state.validationFailed.errors.join(', ')}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className='form-group'>
|
|
||||||
<button className='logInButton' type='submit'>Sign Up</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p className='alert'>会員登録することで、当サイトの利用規約及びCookieの使用を含むデータに関するポリシーに同意するものとします。</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var React = require('react/addons')
|
|
||||||
var ReactRouter = require('react-router')
|
|
||||||
var Navigation = ReactRouter.Navigation
|
|
||||||
var State = ReactRouter.State
|
|
||||||
var RouteHandler = ReactRouter.RouteHandler
|
|
||||||
var Link = ReactRouter.Link
|
|
||||||
var Reflux = require('reflux')
|
|
||||||
|
|
||||||
var LinkedState = require('../Mixins/LinkedState')
|
|
||||||
var Modal = require('../Mixins/Modal')
|
|
||||||
var Helper = require('../Mixins/Helper')
|
|
||||||
|
|
||||||
var Hq = require('../Services/Hq')
|
|
||||||
|
|
||||||
var ProfileImage = require('../Components/ProfileImage')
|
|
||||||
var EditProfileModal = require('../Components/EditProfileModal')
|
|
||||||
var TeamSettingsModal = require('../Components/TeamSettingsModal')
|
|
||||||
var PlanetCreateModal = require('../Components/PlanetCreateModal')
|
|
||||||
var AddMemberModal = require('../Components/AddMemberModal')
|
|
||||||
var TeamCreateModal = require('../Components/TeamCreateModal')
|
|
||||||
|
|
||||||
var UserStore = require('../Stores/UserStore')
|
|
||||||
var PlanetStore = require('../Stores/PlanetStore')
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
mixins: [LinkedState, State, Navigation, Modal, Reflux.listenTo(UserStore, 'onUserChange'), Reflux.listenTo(PlanetStore, 'onPlanetChange'), Helper],
|
|
||||||
propTypes: {
|
|
||||||
params: React.PropTypes.shape({
|
|
||||||
userName: React.PropTypes.string,
|
|
||||||
planetName: React.PropTypes.string
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
user: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
this.fetchUser()
|
|
||||||
},
|
|
||||||
componentWillReceiveProps: function (nextProps) {
|
|
||||||
if (this.state.user == null) {
|
|
||||||
this.fetchUser(nextProps.params.userName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.params.userName !== this.state.user.name) {
|
|
||||||
this.setState({
|
|
||||||
user: null
|
|
||||||
}, function () {
|
|
||||||
this.fetchUser(nextProps.params.userName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUserChange: function (res) {
|
|
||||||
if (this.state.user == null) return
|
|
||||||
|
|
||||||
var member
|
|
||||||
switch (res.status) {
|
|
||||||
case 'userUpdated':
|
|
||||||
if (this.state.user.id === res.data.id) {
|
|
||||||
this.setState({user: res.data})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'memberAdded':
|
|
||||||
member = res.data
|
|
||||||
if (this.state.user.userType === 'team' && member.TeamMember.TeamId === this.state.user.id) {
|
|
||||||
this.state.user.Members = this.updateItemToTargetArray(member, this.state.user.Members)
|
|
||||||
|
|
||||||
this.setState({user: this.state.user})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'memberRemoved':
|
|
||||||
member = res.data
|
|
||||||
if (this.state.user.userType === 'team' && member.TeamMember.TeamId === this.state.user.id) {
|
|
||||||
this.state.user.Members = this.deleteItemFromTargetArray(member, this.state.user.Members)
|
|
||||||
|
|
||||||
this.setState({user: this.state.user})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPlanetChange: function (res) {
|
|
||||||
if (this.state.user == null) return
|
|
||||||
|
|
||||||
var currentUser, planet, isOwner, team
|
|
||||||
switch (res.status) {
|
|
||||||
case 'updated':
|
|
||||||
// if state.user is currentUser, planet will be fetched by UserStore
|
|
||||||
currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
if (currentUser.id === this.state.user.id) return
|
|
||||||
|
|
||||||
planet = res.data
|
|
||||||
isOwner = planet.Owner.id === this.state.user.id
|
|
||||||
if (isOwner) {
|
|
||||||
this.state.user.Planets = this.updateItemToTargetArray(planet, this.state.user.Planets)
|
|
||||||
this.setState({user: this.state.user})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if team of user has this planet
|
|
||||||
team = null
|
|
||||||
this.state.user.userType !== 'team' && this.state.user.Teams.some(function (_team) {
|
|
||||||
if (planet.Owner.id === _team.id) {
|
|
||||||
team = _team
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (team != null) {
|
|
||||||
team.Planets = this.updateItemToTargetArray(planet, team.Planets)
|
|
||||||
this.setState({user: this.state.user})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'destroyed':
|
|
||||||
// if state.user is currentUser, planet will be fetched by UserStore
|
|
||||||
currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
if (currentUser.id === this.state.user.id) return
|
|
||||||
|
|
||||||
planet = res.data
|
|
||||||
isOwner = planet.Owner.id === this.state.user.id
|
|
||||||
if (isOwner) {
|
|
||||||
this.state.user.Planets = this.deleteItemFromTargetArray(planet, this.state.user.Planets)
|
|
||||||
this.setState({user: this.state.user})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if team of user has this planet
|
|
||||||
team = null
|
|
||||||
this.state.user.userType !== 'team' && this.state.user.Teams.some(function (_team) {
|
|
||||||
if (planet.Owner.id === _team.id) {
|
|
||||||
team = _team
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (team != null) {
|
|
||||||
team.Planets = this.deleteItemFromTargetArray(planet, team.Planets)
|
|
||||||
this.setState({user: this.state.user})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchUser: function (userName) {
|
|
||||||
if (userName == null) userName = this.props.params.userName
|
|
||||||
|
|
||||||
Hq.fetchUser(userName)
|
|
||||||
.then(function (res) {
|
|
||||||
this.setState({user: res.body})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
openEditProfileModal: function () {
|
|
||||||
this.openModal(EditProfileModal, {user: this.state.user})
|
|
||||||
},
|
|
||||||
openTeamSettingsModal: function () {
|
|
||||||
this.openModal(TeamSettingsModal, {team: this.state.user})
|
|
||||||
},
|
|
||||||
openAddUserModal: function () {
|
|
||||||
this.openModal(AddMemberModal, {team: this.state.user})
|
|
||||||
},
|
|
||||||
openTeamCreateModal: function () {
|
|
||||||
this.openModal(TeamCreateModal, {user: this.state.user})
|
|
||||||
},
|
|
||||||
openPlanetCreateModalWithOwnerName: function (name) {
|
|
||||||
return function () {
|
|
||||||
this.openModal(PlanetCreateModal, {ownerName: name})
|
|
||||||
}.bind(this)
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var user = this.state.user
|
|
||||||
|
|
||||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
|
|
||||||
if (this.isActive('userHome')) {
|
|
||||||
if (user == null) {
|
|
||||||
return (
|
|
||||||
<div className='UserContainer'>
|
|
||||||
User Loading...
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else if (user.userType === 'team') {
|
|
||||||
return this.renderTeamHome(currentUser)
|
|
||||||
} else {
|
|
||||||
return this.renderUserHome(currentUser)
|
|
||||||
}
|
|
||||||
} else if (this.isActive('planet') && user != null && user.userType === 'team') {
|
|
||||||
var members = user.Members.map(function (member) {
|
|
||||||
return (
|
|
||||||
<li key={'user-' + member.id}><Link to='userHome' params={{userName: member.name}}>
|
|
||||||
<ProfileImage className='memberImage' size='22' email={member.email}/>
|
|
||||||
<div className='memberInfo'>
|
|
||||||
<div className='memberProfileName'>{member.profileName}</div>
|
|
||||||
<div className='memberName'>@{member.name}</div>
|
|
||||||
</div>
|
|
||||||
</Link></li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div className='UserContainer'>
|
|
||||||
<RouteHandler/>
|
|
||||||
<div className='memberPopup'>
|
|
||||||
<div className='label'>Members</div>
|
|
||||||
<ul className='members'>
|
|
||||||
{members}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className='UserContainer'>
|
|
||||||
<RouteHandler/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderTeamHome: function (currentUser) {
|
|
||||||
var user = this.state.user
|
|
||||||
|
|
||||||
var isOwner = user.Members == null ? false : user.Members.some(function (member) {
|
|
||||||
return member.id === currentUser.id && member.TeamMember.role === 'owner'
|
|
||||||
})
|
|
||||||
|
|
||||||
var userPlanets = user.Planets.map(function (planet) {
|
|
||||||
return (
|
|
||||||
<li key={'planet-' + planet.id}>
|
|
||||||
<Link to='planet' params={{userName: planet.userName, planetName: planet.name}}>{planet.userName}/{planet.name}</Link>
|
|
||||||
{!planet.public ? (<i className='fa fa-lock'/>) : null}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
var members = user.Members == null ? [] : user.Members.map(function (member) {
|
|
||||||
return (
|
|
||||||
<li key={'user-' + member.id}>
|
|
||||||
<Link to='userHome' params={{userName: member.name}}>
|
|
||||||
<ProfileImage size='22' className='memberImage' email={member.email}/>
|
|
||||||
<div className='memberInfo'>
|
|
||||||
<div className='memberProfileName'>{member.profileName} <span className='memberRole'>({member.TeamMember.role})</span></div>
|
|
||||||
<div className='memberName'>@{member.name}</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
<div className='role'></div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div className='UserContainer'>
|
|
||||||
<div className='userProfile'>
|
|
||||||
<ProfileImage className='userPhoto' size='75' email={user.email}/>
|
|
||||||
<div className='userInfo'>
|
|
||||||
<div className='userProfileName'>{user.profileName}</div>
|
|
||||||
<div className='userName'>{user.name}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isOwner ? (<button onClick={this.openTeamSettingsModal} className='editProfileButton'>Team settings</button>) : null}
|
|
||||||
</div>
|
|
||||||
<div className='memberList'>
|
|
||||||
<div className='memberLabel'>{members.length} {members.length > 1 ? 'Members' : 'Member'}</div>
|
|
||||||
<ul className='members'>
|
|
||||||
{members}
|
|
||||||
{isOwner ? (<li><button onClick={this.openAddUserModal} className='addMemberButton'><i className='fa fa-plus-square-o'/> add Member</button></li>) : null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className='planetList'>
|
|
||||||
<div className='planetLabel'>{userPlanets.length} {userPlanets.length > 0 ? 'Planets' : 'Planet'}</div>
|
|
||||||
<div className='planetGroup'>
|
|
||||||
<ul className='planets'>
|
|
||||||
{userPlanets}
|
|
||||||
{isOwner ? (<li><button onClick={this.openPlanetCreateModalWithOwnerName(user.name)} className='createPlanetButton'><i className='fa fa-plus-square-o'/> Create new planet</button></li>) : null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
renderUserHome: function (currentUser) {
|
|
||||||
var user = this.state.user
|
|
||||||
|
|
||||||
var isOwner = currentUser.id === user.id
|
|
||||||
|
|
||||||
var userPlanets = user.Planets.map(function (planet) {
|
|
||||||
return (
|
|
||||||
<li key={'planet-' + planet.id}>
|
|
||||||
<Link to='planet' params={{userName: planet.userName, planetName: planet.name}}>{planet.userName}/{planet.name}</Link>
|
|
||||||
{!planet.public ? (<i className='fa fa-lock'/>) : null}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
var teams = user.Teams == null ? [] : user.Teams.map(function (team) {
|
|
||||||
return (
|
|
||||||
<li key={'user-' + team.id}>
|
|
||||||
<Link to='userHome' params={{userName: team.name}}>
|
|
||||||
<div className='teamInfo'>
|
|
||||||
<div className='teamProfileName'>{team.profileName}</div>
|
|
||||||
<div className='teamName'>@{team.name}</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
var teamPlanets = user.Teams == null ? [] : user.Teams.map(function (team) {
|
|
||||||
var planets = (team.Planets == null ? [] : team.Planets).map(function (planet) {
|
|
||||||
return (
|
|
||||||
<li key={'planet-' + planet.id}>
|
|
||||||
<Link to='planet' params={{userName: planet.userName, planetName: planet.name}}>{planet.userName}/{planet.name}</Link>
|
|
||||||
{!planet.public ? (<i className='fa fa-lock'/>) : null}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div key={'user-' + team.id} className='planetGroup'>
|
|
||||||
<div className='planetGroupLabel'>{team.name}</div>
|
|
||||||
<ul className='planets'>
|
|
||||||
{planets}
|
|
||||||
{isOwner ? (<li><button onClick={this.openPlanetCreateModalWithOwnerName(team.name)} className='createPlanetButton'><i className='fa fa-plus-square-o'/> Create new planet</button></li>) : null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
var planetCount = userPlanets.length + user.Teams.reduce(function (sum, team) {
|
|
||||||
return sum + (team.Planets != null ? team.Planets.length : 0)
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='UserContainer'>
|
|
||||||
<div className='userProfile'>
|
|
||||||
<ProfileImage className='userPhoto' size='75' email={user.email}/>
|
|
||||||
<div className='userInfo'>
|
|
||||||
<div className='userProfileName'>{user.profileName}</div>
|
|
||||||
<div className='userName'>{user.name}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isOwner ? (
|
|
||||||
<button onClick={this.openEditProfileModal} className='editProfileButton'>Edit profile</button>) : null}
|
|
||||||
</div>
|
|
||||||
<div className='teamList'>
|
|
||||||
<div className='teamLabel'>{teams.length} {teams.length > 1 ? 'Teams' : 'Team'}</div>
|
|
||||||
<ul className='teams'>
|
|
||||||
{teams}
|
|
||||||
{isOwner ? (<li><button onClick={this.openTeamCreateModal} className='createTeamButton'><i className='fa fa-plus-square-o'/> Create new team</button></li>) : null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className='planetList'>
|
|
||||||
<div className='planetLabel'>{planetCount} {planetCount > 1 ? 'Planets' : 'Planet'}</div>
|
|
||||||
<div className='planetGroup'>
|
|
||||||
<div className='planetGroupLabel'>{user.profileName}</div>
|
|
||||||
<ul className='planets'>
|
|
||||||
{userPlanets}
|
|
||||||
{isOwner ? (<li><button onClick={this.openPlanetCreateModalWithOwnerName(user.name)} className='createPlanetButton'><i className='fa fa-plus-square-o'/> Create new planet</button></li>) : null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{teamPlanets}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
24
browser/main/Detail/Detail.styl
Normal file
24
browser/main/Detail/Detail.styl
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
.root
|
||||||
|
absolute top bottom right
|
||||||
|
border-width 1px 0
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-borderColor
|
||||||
|
|
||||||
|
.empty
|
||||||
|
height 320px
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
|
||||||
|
.empty-message
|
||||||
|
width 100%
|
||||||
|
font-size 42px
|
||||||
|
line-height 72px
|
||||||
|
text-align center
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
.empty-message
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
291
browser/main/Detail/FolderSelect.js
Normal file
291
browser/main/Detail/FolderSelect.js
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './FolderSelect.styl'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
class FolderSelect extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
status: 'IDLE',
|
||||||
|
search: '',
|
||||||
|
optionIndex: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.value = this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
this.value = this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick (e) {
|
||||||
|
this.setState({
|
||||||
|
status: 'SEARCH',
|
||||||
|
optionIndex: -1
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFocus (e) {
|
||||||
|
if (this.state.status === 'IDLE') {
|
||||||
|
this.setState({
|
||||||
|
status: 'FOCUS'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur (e) {
|
||||||
|
if (this.state.status === 'FOCUS') {
|
||||||
|
this.setState({
|
||||||
|
status: 'IDLE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 13:
|
||||||
|
if (this.state.status === 'FOCUS') {
|
||||||
|
this.setState({
|
||||||
|
status: 'SEARCH',
|
||||||
|
optionIndex: -1
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 40:
|
||||||
|
case 38:
|
||||||
|
if (this.state.status === 'FOCUS') {
|
||||||
|
this.setState({
|
||||||
|
status: 'SEARCH',
|
||||||
|
optionIndex: 0
|
||||||
|
}, () => {
|
||||||
|
this.refs.search.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 9:
|
||||||
|
if (e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
let tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
|
||||||
|
let previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
|
||||||
|
if (previousEl != null) previousEl.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchInputBlur (e) {
|
||||||
|
if (e.relatedTarget !== this.refs.root) {
|
||||||
|
this.setState({
|
||||||
|
status: 'IDLE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchInputChange (e) {
|
||||||
|
let { folders } = this.props
|
||||||
|
let search = this.refs.search.value
|
||||||
|
let optionIndex = search.length > 0
|
||||||
|
? _.findIndex(folders, (folder) => {
|
||||||
|
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
|
||||||
|
})
|
||||||
|
: -1
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
search: this.refs.search.value,
|
||||||
|
optionIndex: optionIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchInputKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 40:
|
||||||
|
e.stopPropagation()
|
||||||
|
this.nextOption()
|
||||||
|
break
|
||||||
|
case 38:
|
||||||
|
e.stopPropagation()
|
||||||
|
this.previousOption()
|
||||||
|
break
|
||||||
|
case 13:
|
||||||
|
e.stopPropagation()
|
||||||
|
this.selectOption()
|
||||||
|
break
|
||||||
|
case 27:
|
||||||
|
e.stopPropagation()
|
||||||
|
this.setState({
|
||||||
|
status: 'FOCUS'
|
||||||
|
}, () => {
|
||||||
|
this.refs.root.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextOption () {
|
||||||
|
let { storages } = this.props
|
||||||
|
let { optionIndex } = this.state
|
||||||
|
|
||||||
|
optionIndex++
|
||||||
|
if (optionIndex >= folders.length) optionIndex = 0
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
optionIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
previousOption () {
|
||||||
|
let { folders } = this.props
|
||||||
|
let { optionIndex } = this.state
|
||||||
|
|
||||||
|
optionIndex--
|
||||||
|
if (optionIndex < 0) optionIndex = folders.length - 1
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
optionIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOption () {
|
||||||
|
let { folders } = this.props
|
||||||
|
let optionIndex = this.state.optionIndex
|
||||||
|
|
||||||
|
let folder = folders[optionIndex]
|
||||||
|
if (folder != null) {
|
||||||
|
this.setState({
|
||||||
|
status: 'FOCUS'
|
||||||
|
}, () => {
|
||||||
|
this.setValue(folder.key)
|
||||||
|
this.refs.root.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOptionClick (storageKey, folderKey) {
|
||||||
|
return (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.setState({
|
||||||
|
status: 'FOCUS'
|
||||||
|
}, () => {
|
||||||
|
this.setValue(storageKey + '-' + folderKey)
|
||||||
|
this.refs.root.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue (value) {
|
||||||
|
this.value = value
|
||||||
|
this.props.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { className, data, value } = this.props
|
||||||
|
let splitted = value.split('-')
|
||||||
|
let storageKey = splitted.shift()
|
||||||
|
let folderKey = splitted.shift()
|
||||||
|
let options = []
|
||||||
|
data.storageMap.forEach((storage, index) => {
|
||||||
|
storage.folders.forEach((folder) => {
|
||||||
|
options.push({
|
||||||
|
storage: storage,
|
||||||
|
folder: folder
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
||||||
|
|
||||||
|
if (this.state.search.trim().length > 0) {
|
||||||
|
let filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
|
||||||
|
options = options.filter((option) => filter.test(option.folder.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
let optionList = options
|
||||||
|
.map((option, index) => {
|
||||||
|
return (
|
||||||
|
<div styleName={index === this.state.optionIndex
|
||||||
|
? 'search-optionList-item--active'
|
||||||
|
: 'search-optionList-item'
|
||||||
|
}
|
||||||
|
key={option.storage.key + '-' + option.folder.key}
|
||||||
|
onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)}
|
||||||
|
>
|
||||||
|
<span styleName='search-optionList-item-name'
|
||||||
|
style={{borderColor: option.folder.color}}
|
||||||
|
>
|
||||||
|
{option.folder.name}
|
||||||
|
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={_.isString(className)
|
||||||
|
? 'FolderSelect ' + className
|
||||||
|
: 'FolderSelect'
|
||||||
|
}
|
||||||
|
styleName={this.state.status === 'SEARCH'
|
||||||
|
? 'root--search'
|
||||||
|
: this.state.status === 'FOCUS'
|
||||||
|
? 'root--focus'
|
||||||
|
: 'root'
|
||||||
|
}
|
||||||
|
ref='root'
|
||||||
|
tabIndex='0'
|
||||||
|
onClick={(e) => this.handleClick(e)}
|
||||||
|
onFocus={(e) => this.handleFocus(e)}
|
||||||
|
onBlur={(e) => this.handleBlur(e)}
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||||
|
>
|
||||||
|
{this.state.status === 'SEARCH'
|
||||||
|
? <div styleName='search'>
|
||||||
|
<input styleName='search-input'
|
||||||
|
ref='search'
|
||||||
|
value={this.state.search}
|
||||||
|
placeholder='Folder...'
|
||||||
|
onChange={(e) => this.handleSearchInputChange(e)}
|
||||||
|
onBlur={(e) => this.handleSearchInputBlur(e)}
|
||||||
|
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
||||||
|
/>
|
||||||
|
<div styleName='search-optionList'
|
||||||
|
ref='optionList'
|
||||||
|
>
|
||||||
|
{optionList}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <div styleName='idle'>
|
||||||
|
<div styleName='idle-label'>
|
||||||
|
<span styleName='idle-label-name'
|
||||||
|
style={{borderColor: currentOption.folder.color}}
|
||||||
|
>
|
||||||
|
{currentOption.folder.name}
|
||||||
|
<span styleName='idle-label-name-surfix'>in {currentOption.storage.name}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<i styleName='idle-caret' className='fa fa-fw fa-caret-down'/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderSelect.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
value: PropTypes.string,
|
||||||
|
folders: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
key: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
color: PropTypes.string
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(FolderSelect, styles)
|
||||||
132
browser/main/Detail/FolderSelect.styl
Normal file
132
browser/main/Detail/FolderSelect.styl
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
.root
|
||||||
|
position relative
|
||||||
|
border solid 1px transparent
|
||||||
|
line-height 34px
|
||||||
|
vertical-align middle
|
||||||
|
border-radius 2px
|
||||||
|
transition 0.15s
|
||||||
|
user-select none
|
||||||
|
&:hover
|
||||||
|
background-color white
|
||||||
|
border-color $ui-borderColor
|
||||||
|
|
||||||
|
.root--search, .root--focus
|
||||||
|
@extend .root
|
||||||
|
background-color white
|
||||||
|
border-color $ui-input--focus-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color white
|
||||||
|
border-color $ui-input--focus-borderColor
|
||||||
|
|
||||||
|
.idle
|
||||||
|
position relative
|
||||||
|
cursor pointer
|
||||||
|
.idle-label
|
||||||
|
absolute left top
|
||||||
|
padding 0 0 0 5px
|
||||||
|
right 20px
|
||||||
|
overflow ellipsis
|
||||||
|
|
||||||
|
.idle-label-name
|
||||||
|
border-left solid 4px transparent
|
||||||
|
padding 2px 5px
|
||||||
|
.idle-label-name-surfix
|
||||||
|
font-size 10px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
margin-left 5px
|
||||||
|
.idle-caret
|
||||||
|
absolute right top
|
||||||
|
height 34px
|
||||||
|
width 20px
|
||||||
|
line-height 34px
|
||||||
|
|
||||||
|
.search
|
||||||
|
absolute top left right bottom
|
||||||
|
line-height 34px
|
||||||
|
|
||||||
|
.search-input
|
||||||
|
vertical-align middle
|
||||||
|
position relative
|
||||||
|
top -2px
|
||||||
|
outline none
|
||||||
|
border none
|
||||||
|
height 20px
|
||||||
|
line-height 20px
|
||||||
|
background-color transparent
|
||||||
|
padding 0 10px
|
||||||
|
|
||||||
|
.search-optionList
|
||||||
|
position fixed
|
||||||
|
max-height 450px
|
||||||
|
overflow auto
|
||||||
|
z-index 200
|
||||||
|
background-color white
|
||||||
|
border-radius 2px
|
||||||
|
box-shadow 2px 2px 10px gray
|
||||||
|
|
||||||
|
.search-optionList-item
|
||||||
|
height 34px
|
||||||
|
width 250px
|
||||||
|
box-sizing border-box
|
||||||
|
padding 0 5px
|
||||||
|
overflow ellipsis
|
||||||
|
cursor pointer
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--hover-backgroundColor
|
||||||
|
|
||||||
|
.search-optionList-item--active
|
||||||
|
@extend .search-optionList-item
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
color $ui-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
color $ui-button--active-color
|
||||||
|
.search-optionList-item-name
|
||||||
|
border-left solid 4px transparent
|
||||||
|
padding 2px 5px
|
||||||
|
.search-optionList-item-name-surfix
|
||||||
|
font-size 10px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
margin-left 5px
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
color $ui-dark-text-color
|
||||||
|
&:hover
|
||||||
|
color white
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.root--search, .root--focus
|
||||||
|
@extend .root
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
border-color $ui-input--focus-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
border-color $ui-input--focus-borderColor
|
||||||
|
|
||||||
|
.idle-label-name-surfix
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
.search-input
|
||||||
|
color white
|
||||||
|
background-color transparent
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.search-optionList
|
||||||
|
color white
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
box-shadow 2px 2px 10px black
|
||||||
|
|
||||||
|
.search-optionList-item
|
||||||
|
&:hover
|
||||||
|
background-color lighten($ui-dark-button--hover-backgroundColor, 15%)
|
||||||
|
|
||||||
|
.search-optionList-item--active
|
||||||
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
color $ui-dark-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
color $ui-dark-button--active-color
|
||||||
|
.search-optionList-item-name-surfix
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
305
browser/main/Detail/MarkdownNoteDetail.js
Normal file
305
browser/main/Detail/MarkdownNoteDetail.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './MarkdownNoteDetail.styl'
|
||||||
|
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||||
|
import StarButton from './StarButton'
|
||||||
|
import TagSelect from './TagSelect'
|
||||||
|
import FolderSelect from './FolderSelect'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import { hashHistory } from 'react-router'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
import markdown from 'browser/lib/markdown'
|
||||||
|
import StatusBar from '../StatusBar'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const { remote } = electron
|
||||||
|
const { Menu, MenuItem, dialog } = remote
|
||||||
|
|
||||||
|
class MarkdownNoteDetail extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isMovingNote: false,
|
||||||
|
note: Object.assign({
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
}, props.note)
|
||||||
|
}
|
||||||
|
this.dispatchTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
focus () {
|
||||||
|
this.refs.content.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
|
||||||
|
if (this.saveQueue != null) this.saveNow()
|
||||||
|
this.setState({
|
||||||
|
note: Object.assign({}, nextProps.note)
|
||||||
|
}, () => {
|
||||||
|
this.refs.content.reload()
|
||||||
|
this.refs.tags.reset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (this.saveQueue != null) this.saveNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
findTitle (value) {
|
||||||
|
let splitted = value.split('\n')
|
||||||
|
let title = null
|
||||||
|
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
let trimmedLine = splitted[i].trim()
|
||||||
|
if (trimmedLine.match(/^# .+/)) {
|
||||||
|
title = trimmedLine.substring(1, trimmedLine.length).trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title == null) {
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
let trimmedLine = splitted[i].trim()
|
||||||
|
if (trimmedLine.length > 0) {
|
||||||
|
title = trimmedLine
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (title == null) {
|
||||||
|
title = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title = markdown.strip(title)
|
||||||
|
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange (e) {
|
||||||
|
let { note } = this.state
|
||||||
|
|
||||||
|
note.content = this.refs.content.value
|
||||||
|
note.tags = this.refs.tags.value
|
||||||
|
note.title = this.findTitle(note.content)
|
||||||
|
note.updatedAt = new Date()
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
note
|
||||||
|
}, () => {
|
||||||
|
this.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
clearTimeout(this.saveQueue)
|
||||||
|
this.saveQueue = setTimeout(() => {
|
||||||
|
this.saveNow()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveNow () {
|
||||||
|
let { note, dispatch } = this.props
|
||||||
|
clearTimeout(this.saveQueue)
|
||||||
|
this.saveQueue = null
|
||||||
|
|
||||||
|
dataApi
|
||||||
|
.updateNote(note.storage, note.key, this.state.note)
|
||||||
|
.then((note) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFolderChange (e) {
|
||||||
|
let { note } = this.state
|
||||||
|
let value = this.refs.folder.value
|
||||||
|
let splitted = value.split('-')
|
||||||
|
let newStorageKey = splitted.shift()
|
||||||
|
let newFolderKey = splitted.shift()
|
||||||
|
|
||||||
|
dataApi
|
||||||
|
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||||
|
.then((newNote) => {
|
||||||
|
this.setState({
|
||||||
|
isMovingNote: true,
|
||||||
|
note: Object.assign({}, newNote)
|
||||||
|
}, () => {
|
||||||
|
let { dispatch, location } = this.props
|
||||||
|
dispatch({
|
||||||
|
type: 'MOVE_NOTE',
|
||||||
|
originNote: note,
|
||||||
|
note: newNote
|
||||||
|
})
|
||||||
|
hashHistory.replace({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {
|
||||||
|
key: newNote.storage + '-' + newNote.key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
isMovingNote: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStarButtonClick (e) {
|
||||||
|
let { note } = this.state
|
||||||
|
|
||||||
|
note.isStarred = !note.isStarred
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
note
|
||||||
|
}, () => {
|
||||||
|
this.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exportAsFile () {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShareButtonClick (e) {
|
||||||
|
let menu = new Menu()
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Export as a File',
|
||||||
|
click: (e) => this.handlePreferencesButtonClick(e)
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Export to Web',
|
||||||
|
disabled: true,
|
||||||
|
click: (e) => this.handlePreferencesButtonClick(e)
|
||||||
|
}))
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextButtonClick (e) {
|
||||||
|
let menu = new Menu()
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Delete',
|
||||||
|
click: (e) => this.handleDeleteMenuClick(e)
|
||||||
|
}))
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteMenuClick (e) {
|
||||||
|
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Delete a note',
|
||||||
|
detail: 'This work cannot be undone.',
|
||||||
|
buttons: ['Confirm', 'Cancel']
|
||||||
|
})
|
||||||
|
if (index === 0) {
|
||||||
|
let { note, dispatch } = this.props
|
||||||
|
dataApi
|
||||||
|
.deleteNote(note.storage, note.key)
|
||||||
|
.then((data) => {
|
||||||
|
let dispatchHandler = () => {
|
||||||
|
dispatch({
|
||||||
|
type: 'DELETE_NOTE',
|
||||||
|
storageKey: data.storageKey,
|
||||||
|
noteKey: data.noteKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ee.once('list:moved', dispatchHandler)
|
||||||
|
ee.emit('list:next')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteKeyDown (e) {
|
||||||
|
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { data, config } = this.props
|
||||||
|
let { note } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='NoteDetail'
|
||||||
|
style={this.props.style}
|
||||||
|
styleName='root'
|
||||||
|
>
|
||||||
|
<div styleName='info'>
|
||||||
|
<div styleName='info-left'>
|
||||||
|
<div styleName='info-left-top'>
|
||||||
|
<FolderSelect styleName='info-left-top-folderSelect'
|
||||||
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
|
ref='folder'
|
||||||
|
data={data}
|
||||||
|
onChange={(e) => this.handleFolderChange(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div styleName='info-left-bottom'>
|
||||||
|
<TagSelect
|
||||||
|
styleName='info-left-bottom-tagSelect'
|
||||||
|
ref='tags'
|
||||||
|
value={this.state.note.tags}
|
||||||
|
onChange={(e) => this.handleChange(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='info-right'>
|
||||||
|
<StarButton styleName='info-right-button'
|
||||||
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
|
isActive={note.isStarred}
|
||||||
|
/>
|
||||||
|
<button styleName='info-right-button'
|
||||||
|
onClick={(e) => this.handleShareButtonClick(e)}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<i className='fa fa-share-alt fa-fw' />
|
||||||
|
<span styleName='info-right-button-tooltip'
|
||||||
|
style={{right: 20}}
|
||||||
|
>Share Note</span>
|
||||||
|
</button>
|
||||||
|
<button styleName='info-right-button'
|
||||||
|
onClick={(e) => this.handleContextButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-ellipsis-v' />
|
||||||
|
<span styleName='info-right-button-tooltip'
|
||||||
|
style={{right: 5}}
|
||||||
|
>More Options</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='body'>
|
||||||
|
<MarkdownEditor
|
||||||
|
ref='content'
|
||||||
|
styleName='body-noteEditor'
|
||||||
|
config={config}
|
||||||
|
value={this.state.note.content}
|
||||||
|
onChange={(e) => this.handleChange(e)}
|
||||||
|
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusBar
|
||||||
|
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownNoteDetail.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
repositories: PropTypes.array,
|
||||||
|
note: PropTypes.shape({
|
||||||
|
|
||||||
|
}),
|
||||||
|
style: PropTypes.shape({
|
||||||
|
left: PropTypes.number
|
||||||
|
}),
|
||||||
|
ignorePreviewPointerEvents: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(MarkdownNoteDetail, styles)
|
||||||
102
browser/main/Detail/MarkdownNoteDetail.styl
Normal file
102
browser/main/Detail/MarkdownNoteDetail.styl
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
$info-height = 75px
|
||||||
|
|
||||||
|
.root
|
||||||
|
absolute top right bottom
|
||||||
|
border-width 0 0 1px
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-borderColor
|
||||||
|
|
||||||
|
.info
|
||||||
|
absolute top left right
|
||||||
|
height $info-height
|
||||||
|
border-bottom $ui-border
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
|
||||||
|
.info-left
|
||||||
|
float left
|
||||||
|
padding 0 5px
|
||||||
|
|
||||||
|
.info-left-top
|
||||||
|
height 40px
|
||||||
|
line-height 40px
|
||||||
|
|
||||||
|
.info-left-top-folderSelect
|
||||||
|
display inline-block
|
||||||
|
height 34px
|
||||||
|
width 200px
|
||||||
|
vertical-align middle
|
||||||
|
|
||||||
|
.info-left-bottom
|
||||||
|
height 30px
|
||||||
|
|
||||||
|
.info-left-bottom-tagSelect
|
||||||
|
height 30px
|
||||||
|
line-height 30px
|
||||||
|
|
||||||
|
.info-right
|
||||||
|
float right
|
||||||
|
|
||||||
|
.info-right-button
|
||||||
|
width 34px
|
||||||
|
height 34px
|
||||||
|
border-radius 17px
|
||||||
|
navButtonColor()
|
||||||
|
border $ui-border
|
||||||
|
font-size 14px
|
||||||
|
margin 8px 2px
|
||||||
|
padding 0
|
||||||
|
&:active
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
&:hover .info-right-button-tooltip
|
||||||
|
opacity 1
|
||||||
|
&:focus
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
|
||||||
|
.info-right-button-tooltip
|
||||||
|
tooltip()
|
||||||
|
position fixed
|
||||||
|
top 45px
|
||||||
|
padding 5px
|
||||||
|
opacity 0
|
||||||
|
border-radius 2px
|
||||||
|
|
||||||
|
.body
|
||||||
|
absolute left right
|
||||||
|
top $info-height
|
||||||
|
bottom $statusBar-height
|
||||||
|
|
||||||
|
.body-noteEditor
|
||||||
|
absolute top bottom left right
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.info
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
|
||||||
|
.info-delete
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.info-delete-confirmButton
|
||||||
|
colorDarkDangerButton()
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.info-delete-cancelButton
|
||||||
|
colorDarkDefaultButton()
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.info-right-button
|
||||||
|
navDarkButtonColor()
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:active
|
||||||
|
border-color $ui-dark-button--focus-borderColor
|
||||||
|
&:hover .info-right-button-tooltip
|
||||||
|
opacity 1
|
||||||
|
&:focus
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
|
||||||
|
.info-right-button-tooltip
|
||||||
|
darkTooltip()
|
||||||
653
browser/main/Detail/SnippetNoteDetail.js
Normal file
653
browser/main/Detail/SnippetNoteDetail.js
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './SnippetNoteDetail.styl'
|
||||||
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
|
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||||
|
import StarButton from './StarButton'
|
||||||
|
import TagSelect from './TagSelect'
|
||||||
|
import FolderSelect from './FolderSelect'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import { hashHistory } from 'react-router'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
import CodeMirror from 'codemirror'
|
||||||
|
import SnippetTab from './SnippetTab'
|
||||||
|
import StatusBar from '../StatusBar'
|
||||||
|
import context from 'browser/lib/context'
|
||||||
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
|
||||||
|
function pass (name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'ejs':
|
||||||
|
return 'Embedded Javascript'
|
||||||
|
case 'html_ruby':
|
||||||
|
return 'Embedded Ruby'
|
||||||
|
case 'objectivec':
|
||||||
|
return 'Objective C'
|
||||||
|
case 'text':
|
||||||
|
return 'Plain Text'
|
||||||
|
default:
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const { remote } = electron
|
||||||
|
const { Menu, MenuItem, dialog } = remote
|
||||||
|
|
||||||
|
class SnippetNoteDetail extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isMovingNote: false,
|
||||||
|
snippetIndex: 0,
|
||||||
|
note: Object.assign({
|
||||||
|
description: ''
|
||||||
|
}, props.note, {
|
||||||
|
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.note.key !== this.props.note.key) {
|
||||||
|
if (this.saveQueue != null) this.saveNow()
|
||||||
|
let nextNote = Object.assign({
|
||||||
|
description: ''
|
||||||
|
}, nextProps.note, {
|
||||||
|
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
snippetIndex: 0,
|
||||||
|
note: nextNote
|
||||||
|
}, () => {
|
||||||
|
let { snippets } = this.state.note
|
||||||
|
snippets.forEach((snippet, index) => {
|
||||||
|
this.refs['code-' + index].reload()
|
||||||
|
})
|
||||||
|
this.refs.tags.reset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (this.saveQueue != null) this.saveNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
findTitle (value) {
|
||||||
|
let splitted = value.split('\n')
|
||||||
|
let title = null
|
||||||
|
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
let trimmedLine = splitted[i].trim()
|
||||||
|
if (trimmedLine.match(/^# .+/)) {
|
||||||
|
title = trimmedLine.substring(1, trimmedLine.length).trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title == null) {
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
let trimmedLine = splitted[i].trim()
|
||||||
|
if (trimmedLine.length > 0) {
|
||||||
|
title = trimmedLine
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (title == null) {
|
||||||
|
title = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange (e) {
|
||||||
|
let { note } = this.state
|
||||||
|
|
||||||
|
note.tags = this.refs.tags.value
|
||||||
|
note.description = this.refs.description.value
|
||||||
|
note.updatedAt = new Date()
|
||||||
|
note.title = this.findTitle(note.description)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
note
|
||||||
|
}, () => {
|
||||||
|
this.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
clearTimeout(this.saveQueue)
|
||||||
|
this.saveQueue = setTimeout(() => {
|
||||||
|
this.saveNow()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveNow () {
|
||||||
|
let { note, dispatch } = this.props
|
||||||
|
clearTimeout(this.saveQueue)
|
||||||
|
this.saveQueue = null
|
||||||
|
|
||||||
|
dataApi
|
||||||
|
.updateNote(note.storage, note.key, this.state.note)
|
||||||
|
.then((note) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFolderChange (e) {
|
||||||
|
let { note } = this.state
|
||||||
|
let value = this.refs.folder.value
|
||||||
|
let splitted = value.split('-')
|
||||||
|
let newStorageKey = splitted.shift()
|
||||||
|
let newFolderKey = splitted.shift()
|
||||||
|
|
||||||
|
dataApi
|
||||||
|
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||||
|
.then((newNote) => {
|
||||||
|
this.setState({
|
||||||
|
isMovingNote: true,
|
||||||
|
note: Object.assign({}, newNote)
|
||||||
|
}, () => {
|
||||||
|
let { dispatch, location } = this.props
|
||||||
|
dispatch({
|
||||||
|
type: 'MOVE_NOTE',
|
||||||
|
originNote: note,
|
||||||
|
note: newNote
|
||||||
|
})
|
||||||
|
hashHistory.replace({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {
|
||||||
|
key: newNote.storage + '-' + newNote.key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
isMovingNote: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStarButtonClick (e) {
|
||||||
|
let { note } = this.state
|
||||||
|
|
||||||
|
note.isStarred = !note.isStarred
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
note
|
||||||
|
}, () => {
|
||||||
|
this.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exportAsFile () {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShareButtonClick (e) {
|
||||||
|
let menu = new Menu()
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Export as a File',
|
||||||
|
disabled: true,
|
||||||
|
click: (e) => this.handlePreferencesButtonClick(e)
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Export to Web',
|
||||||
|
disabled: true,
|
||||||
|
click: (e) => this.handlePreferencesButtonClick(e)
|
||||||
|
}))
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextButtonClick (e) {
|
||||||
|
context.popup([{
|
||||||
|
label: 'Delete',
|
||||||
|
click: (e) => this.handleDeleteMenuClick(e)
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteMenuClick (e) {
|
||||||
|
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Delete a note',
|
||||||
|
detail: 'This work cannot be undone.',
|
||||||
|
buttons: ['Confirm', 'Cancel']
|
||||||
|
})
|
||||||
|
if (index === 0) {
|
||||||
|
let { note, dispatch } = this.props
|
||||||
|
dataApi
|
||||||
|
.deleteNote(note.storage, note.key)
|
||||||
|
.then((data) => {
|
||||||
|
let dispatchHandler = () => {
|
||||||
|
dispatch({
|
||||||
|
type: 'DELETE_NOTE',
|
||||||
|
storageKey: data.storageKey,
|
||||||
|
noteKey: data.noteKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ee.once('list:moved', dispatchHandler)
|
||||||
|
ee.emit('list:next')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabPlusButtonClick (e) {
|
||||||
|
this.addSnippet()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabButtonClick (e, index) {
|
||||||
|
this.setState({
|
||||||
|
snippetIndex: index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabDeleteButtonClick (e, index) {
|
||||||
|
if (this.state.note.snippets.length > 1) {
|
||||||
|
if (this.state.note.snippets[index].content.trim().length > 0) {
|
||||||
|
let dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Delete a snippet',
|
||||||
|
detail: 'This work cannot be undone.',
|
||||||
|
buttons: ['Confirm', 'Cancel']
|
||||||
|
})
|
||||||
|
if (dialogIndex === 0) {
|
||||||
|
this.deleteSnippetByIndex(index)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.deleteSnippetByIndex(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSnippetByIndex (index) {
|
||||||
|
let snippets = this.state.note.snippets.slice()
|
||||||
|
snippets.splice(index, 1)
|
||||||
|
this.state.note.snippets = snippets
|
||||||
|
let snippetIndex = this.state.snippetIndex >= snippets.length
|
||||||
|
? snippets.length - 1
|
||||||
|
: this.state.snippetIndex
|
||||||
|
this.setState({
|
||||||
|
note: this.state.note,
|
||||||
|
snippetIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renameSnippetByIndex (index, name) {
|
||||||
|
let snippets = this.state.note.snippets.slice()
|
||||||
|
snippets[index].name = name
|
||||||
|
let syntax = CodeMirror.findModeByFileName(name.trim())
|
||||||
|
let mode = syntax != null ? syntax.name : null
|
||||||
|
if (mode != null) snippets[index].mode = mode
|
||||||
|
this.state.note.snippets = snippets
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
note: this.state.note
|
||||||
|
}, () => {
|
||||||
|
this.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModeOptionClick (index, name) {
|
||||||
|
return (e) => {
|
||||||
|
let snippets = this.state.note.snippets.slice()
|
||||||
|
snippets[index].mode = name
|
||||||
|
this.state.note.snippets = snippets
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
note: this.state.note
|
||||||
|
}, () => {
|
||||||
|
this.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCodeChange (index) {
|
||||||
|
return (e) => {
|
||||||
|
let snippets = this.state.note.snippets.slice()
|
||||||
|
snippets[index].content = this.refs['code-' + index].value
|
||||||
|
this.state.note.snippets = snippets
|
||||||
|
this.setState({
|
||||||
|
note: this.state.note
|
||||||
|
}, () => {
|
||||||
|
this.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 9:
|
||||||
|
if (e.ctrlKey && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.jumpNextTab()
|
||||||
|
} else if (e.ctrlKey && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.jumpPrevTab()
|
||||||
|
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.focusEditor()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 76:
|
||||||
|
{
|
||||||
|
let isSuper = global.process.platform === 'darwin'
|
||||||
|
? e.metaKey
|
||||||
|
: e.ctrlKey
|
||||||
|
if (isSuper) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 84:
|
||||||
|
{
|
||||||
|
let isSuper = global.process.platform === 'darwin'
|
||||||
|
? e.metaKey
|
||||||
|
: e.ctrlKey
|
||||||
|
if (isSuper) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.addSnippet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModeButtonClick (e, index) {
|
||||||
|
let menu = new Menu()
|
||||||
|
CodeMirror.modeInfo.forEach((mode) => {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: mode.name,
|
||||||
|
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIndentTypeButtonClick (e) {
|
||||||
|
context.popup([
|
||||||
|
{
|
||||||
|
label: 'tab',
|
||||||
|
click: (e) => this.handleIndentTypeItemClick(e, 'tab')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'space',
|
||||||
|
click: (e) => this.handleIndentTypeItemClick(e, 'space')
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIndentSizeButtonClick (e) {
|
||||||
|
context.popup([
|
||||||
|
{
|
||||||
|
label: '2',
|
||||||
|
click: (e) => this.handleIndentSizeItemClick(e, 2)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '4',
|
||||||
|
click: (e) => this.handleIndentSizeItemClick(e, 4)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '8',
|
||||||
|
click: (e) => this.handleIndentSizeItemClick(e, 8)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIndentSizeItemClick (e, indentSize) {
|
||||||
|
let { config, dispatch } = this.props
|
||||||
|
let editor = Object.assign({}, config.editor, {
|
||||||
|
indentSize
|
||||||
|
})
|
||||||
|
ConfigManager.set({
|
||||||
|
editor
|
||||||
|
})
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config: {
|
||||||
|
editor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIndentTypeItemClick (e, indentType) {
|
||||||
|
let { config, dispatch } = this.props
|
||||||
|
let editor = Object.assign({}, config.editor, {
|
||||||
|
indentType
|
||||||
|
})
|
||||||
|
ConfigManager.set({
|
||||||
|
editor
|
||||||
|
})
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config: {
|
||||||
|
editor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
focus () {
|
||||||
|
this.refs.description.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
addSnippet () {
|
||||||
|
let { note } = this.state
|
||||||
|
|
||||||
|
note.snippets = note.snippets.concat([{
|
||||||
|
name: '',
|
||||||
|
mode: 'Plain Text',
|
||||||
|
content: ''
|
||||||
|
}])
|
||||||
|
let snippetIndex = note.snippets.length - 1
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
note,
|
||||||
|
snippetIndex
|
||||||
|
}, () => {
|
||||||
|
this.refs['tab-' + snippetIndex].startRenaming()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpNextTab () {
|
||||||
|
this.setState({
|
||||||
|
snippetIndex: (this.state.snippetIndex + 1) % this.state.note.snippets.length
|
||||||
|
}, () => {
|
||||||
|
this.focusEditor()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpPrevTab () {
|
||||||
|
this.setState({
|
||||||
|
snippetIndex: (this.state.snippetIndex - 1 + this.state.note.snippets.length) % this.state.note.snippets.length
|
||||||
|
}, () => {
|
||||||
|
this.focusEditor()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
focusEditor () {
|
||||||
|
console.log('code-' + this.state.snippetIndex)
|
||||||
|
this.refs['code-' + this.state.snippetIndex].focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { data, config } = this.props
|
||||||
|
let { note } = this.state
|
||||||
|
|
||||||
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
|
|
||||||
|
let tabList = note.snippets.map((snippet, index) => {
|
||||||
|
let isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
|
return <SnippetTab
|
||||||
|
key={index}
|
||||||
|
ref={'tab-' + index}
|
||||||
|
snippet={snippet}
|
||||||
|
isActive={isActive}
|
||||||
|
onClick={(e) => this.handleTabButtonClick(e, index)}
|
||||||
|
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
||||||
|
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
||||||
|
isDeletable={note.snippets.length > 1}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
|
||||||
|
let viewList = note.snippets.map((snippet, index) => {
|
||||||
|
let isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
|
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
||||||
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
|
|
||||||
|
return <div styleName='tabView'
|
||||||
|
key={index}
|
||||||
|
style={{zIndex: isActive ? 5 : 4}}
|
||||||
|
>
|
||||||
|
{snippet.mode === 'Markdown' || snippet.mode === 'GitHub Flavored Markdown'
|
||||||
|
? <MarkdownEditor styleName='tabView-content'
|
||||||
|
value={snippet.content}
|
||||||
|
config={config}
|
||||||
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
|
ref={'code-' + index}
|
||||||
|
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||||
|
/>
|
||||||
|
: <CodeEditor styleName='tabView-content'
|
||||||
|
mode={snippet.mode}
|
||||||
|
value={snippet.content}
|
||||||
|
theme={config.editor.theme}
|
||||||
|
fontFamily={config.editor.fontFamily}
|
||||||
|
fontSize={editorFontSize}
|
||||||
|
indentType={config.editor.indentType}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
|
ref={'code-' + index}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='NoteDetail'
|
||||||
|
style={this.props.style}
|
||||||
|
styleName='root'
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||||
|
>
|
||||||
|
<div styleName='info'>
|
||||||
|
<div styleName='info-left'>
|
||||||
|
<div styleName='info-left-top'>
|
||||||
|
<FolderSelect styleName='info-left-top-folderSelect'
|
||||||
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
|
ref='folder'
|
||||||
|
data={data}
|
||||||
|
onChange={(e) => this.handleFolderChange(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div styleName='info-left-bottom'>
|
||||||
|
<TagSelect
|
||||||
|
styleName='info-left-bottom-tagSelect'
|
||||||
|
ref='tags'
|
||||||
|
value={this.state.note.tags}
|
||||||
|
onChange={(e) => this.handleChange(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='info-right'>
|
||||||
|
<StarButton styleName='info-right-button'
|
||||||
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
|
isActive={note.isStarred}
|
||||||
|
/>
|
||||||
|
<button styleName='info-right-button'
|
||||||
|
onClick={(e) => this.handleShareButtonClick(e)}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<i className='fa fa-share-alt fa-fw'/>
|
||||||
|
<span styleName='info-right-button-tooltip'
|
||||||
|
style={{right: 20}}
|
||||||
|
>Share Note</span>
|
||||||
|
</button>
|
||||||
|
<button styleName='info-right-button'
|
||||||
|
onClick={(e) => this.handleContextButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-ellipsis-v'/>
|
||||||
|
<span styleName='info-right-button-tooltip'
|
||||||
|
style={{right: 5}}
|
||||||
|
>More Options</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='body'>
|
||||||
|
<div styleName='description'>
|
||||||
|
<textarea
|
||||||
|
style={{
|
||||||
|
fontFamily: config.preview.fontFamily,
|
||||||
|
fontSize: parseInt(config.preview.fontSize, 10)
|
||||||
|
}}
|
||||||
|
ref='description'
|
||||||
|
placeholder='Description...'
|
||||||
|
value={this.state.note.description}
|
||||||
|
onChange={(e) => this.handleChange(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div styleName='tabList'>
|
||||||
|
<div styleName='list'>
|
||||||
|
{tabList}
|
||||||
|
</div>
|
||||||
|
<button styleName='plusButton'
|
||||||
|
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-plus'/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{viewList}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='override'>
|
||||||
|
<button
|
||||||
|
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
||||||
|
>
|
||||||
|
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
||||||
|
? 'Select Syntax...'
|
||||||
|
: this.state.note.snippets[this.state.snippetIndex].mode
|
||||||
|
}
|
||||||
|
<i className='fa fa-caret-down'/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => this.handleIndentTypeButtonClick(e)}
|
||||||
|
>
|
||||||
|
Indent: {config.editor.indentType}
|
||||||
|
<i className='fa fa-caret-down'/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => this.handleIndentSizeButtonClick(e)}
|
||||||
|
>
|
||||||
|
size: {config.editor.indentSize}
|
||||||
|
<i className='fa fa-caret-down'/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusBar
|
||||||
|
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SnippetNoteDetail.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
repositories: PropTypes.array,
|
||||||
|
note: PropTypes.shape({
|
||||||
|
|
||||||
|
}),
|
||||||
|
style: PropTypes.shape({
|
||||||
|
left: PropTypes.number
|
||||||
|
}),
|
||||||
|
ignorePreviewPointerEvents: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(SnippetNoteDetail, styles)
|
||||||
170
browser/main/Detail/SnippetNoteDetail.styl
Normal file
170
browser/main/Detail/SnippetNoteDetail.styl
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
$info-height = 75px
|
||||||
|
|
||||||
|
.root
|
||||||
|
absolute top bottom right
|
||||||
|
border-width 0 0 1px
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-borderColor
|
||||||
|
|
||||||
|
.info
|
||||||
|
absolute top left right
|
||||||
|
height $info-height
|
||||||
|
border-bottom $ui-border
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
|
||||||
|
.info-left
|
||||||
|
float left
|
||||||
|
padding 0 5px
|
||||||
|
|
||||||
|
.info-left-top
|
||||||
|
height 40px
|
||||||
|
line-height 40px
|
||||||
|
|
||||||
|
.info-left-top-folderSelect
|
||||||
|
display inline-block
|
||||||
|
height 34px
|
||||||
|
width 200px
|
||||||
|
vertical-align middle
|
||||||
|
|
||||||
|
.info-left-bottom
|
||||||
|
height 30px
|
||||||
|
|
||||||
|
.info-left-bottom-tagSelect
|
||||||
|
height 30px
|
||||||
|
line-height 30px
|
||||||
|
|
||||||
|
.info-right
|
||||||
|
float right
|
||||||
|
|
||||||
|
.info-right-button
|
||||||
|
width 34px
|
||||||
|
height 34px
|
||||||
|
border-radius 17px
|
||||||
|
navButtonColor()
|
||||||
|
border $ui-border
|
||||||
|
font-size 14px
|
||||||
|
margin 8px 2px
|
||||||
|
padding 0
|
||||||
|
&:active
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
&:hover .info-right-button-tooltip
|
||||||
|
opacity 1
|
||||||
|
&:focus
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
.info-right-button-tooltip
|
||||||
|
tooltip()
|
||||||
|
position fixed
|
||||||
|
top 45px
|
||||||
|
padding 5px
|
||||||
|
opacity 0
|
||||||
|
|
||||||
|
.body
|
||||||
|
absolute left right
|
||||||
|
top $info-height
|
||||||
|
bottom $statusBar-height
|
||||||
|
|
||||||
|
.body .description
|
||||||
|
absolute top left right
|
||||||
|
height 80px
|
||||||
|
border-bottom $ui-border
|
||||||
|
|
||||||
|
.body .description textarea
|
||||||
|
display block
|
||||||
|
height 100%
|
||||||
|
width 100%
|
||||||
|
resize none
|
||||||
|
border none
|
||||||
|
padding 10px
|
||||||
|
line-height 1.6
|
||||||
|
|
||||||
|
.tabList
|
||||||
|
absolute left right
|
||||||
|
top 80px
|
||||||
|
height 30px
|
||||||
|
border-bottom $ui-border
|
||||||
|
display flex
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
|
||||||
|
.tabList .list
|
||||||
|
flex 1
|
||||||
|
display flex
|
||||||
|
overflow hidden
|
||||||
|
border-right $ui-border
|
||||||
|
|
||||||
|
.tabList .plusButton
|
||||||
|
navButtonColor()
|
||||||
|
width 30px
|
||||||
|
|
||||||
|
.tabView
|
||||||
|
absolute left right bottom
|
||||||
|
top 110px
|
||||||
|
|
||||||
|
.tabView-content
|
||||||
|
absolute top left right bottom
|
||||||
|
|
||||||
|
.override
|
||||||
|
absolute bottom left
|
||||||
|
height 23px
|
||||||
|
z-index 1
|
||||||
|
button
|
||||||
|
navButtonColor()
|
||||||
|
height 24px
|
||||||
|
border-width 0 1px 0 0
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-borderColor
|
||||||
|
&:active .update-icon
|
||||||
|
color white
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.info
|
||||||
|
border-bottom-color $ui-dark-borderColor
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
|
||||||
|
.info-delete
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.info-delete-confirmButton
|
||||||
|
colorDarkDangerButton()
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.info-delete-cancelButton
|
||||||
|
colorDarkDefaultButton()
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.info-right-button
|
||||||
|
navDarkButtonColor()
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:active
|
||||||
|
border-color $ui-dark-button--focus-borderColor
|
||||||
|
&:hover .info-right-button-tooltip
|
||||||
|
opacity 1
|
||||||
|
&:focus
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
|
||||||
|
.info-right-button-tooltip
|
||||||
|
darkTooltip()
|
||||||
|
|
||||||
|
.body .description
|
||||||
|
border-bottom-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.body .description textarea
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
color white
|
||||||
|
|
||||||
|
.tabList
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
border-bottom-color $ui-dark-borderColor
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
|
||||||
|
.tabList .list
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.tabList .plusButton
|
||||||
|
navDarkButtonColor()
|
||||||
|
.override
|
||||||
|
button
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
131
browser/main/Detail/SnippetTab.js
Normal file
131
browser/main/Detail/SnippetTab.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './SnippetTab.styl'
|
||||||
|
import context from 'browser/lib/context'
|
||||||
|
|
||||||
|
class SnippetTab extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isRenaming: false,
|
||||||
|
name: props.snippet.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUpdate (nextProps) {
|
||||||
|
if (nextProps.snippet.name !== this.props.snippet.name) {
|
||||||
|
this.setState({
|
||||||
|
name: nextProps.snippet.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick (e) {
|
||||||
|
this.props.onClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextMenu (e) {
|
||||||
|
context.popup([
|
||||||
|
{
|
||||||
|
label: 'Rename',
|
||||||
|
click: (e) => this.handleRenameClick(e)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenameClick (e) {
|
||||||
|
this.startRenaming()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNameInputBlur (e) {
|
||||||
|
this.handleRename()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNameInputChange (e) {
|
||||||
|
this.setState({
|
||||||
|
name: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNameInputKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 13:
|
||||||
|
this.handleRename()
|
||||||
|
break
|
||||||
|
case 27:
|
||||||
|
this.setState({
|
||||||
|
name: this.props.snippet.name,
|
||||||
|
isRenaming: false
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRename () {
|
||||||
|
this.setState({
|
||||||
|
isRenaming: false
|
||||||
|
}, () => {
|
||||||
|
if (this.props.snippet.name !== this.state.name) {
|
||||||
|
this.props.onRename(this.state.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteButtonClick (e) {
|
||||||
|
this.props.onDelete(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
startRenaming () {
|
||||||
|
this.setState({
|
||||||
|
isRenaming: true
|
||||||
|
}, () => {
|
||||||
|
this.refs.name.focus()
|
||||||
|
this.refs.name.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { isActive, snippet, isDeletable } = this.props
|
||||||
|
return (
|
||||||
|
<div styleName={isActive
|
||||||
|
? 'root--active'
|
||||||
|
: 'root'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!this.state.isRenaming
|
||||||
|
? <button styleName='button'
|
||||||
|
onClick={(e) => this.handleClick(e)}
|
||||||
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
|
>
|
||||||
|
{snippet.name.trim().length > 0
|
||||||
|
? snippet.name
|
||||||
|
: <span styleName='button-unnamed'>
|
||||||
|
Unnamed
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
: <input styleName='input'
|
||||||
|
ref='name'
|
||||||
|
value={this.state.name}
|
||||||
|
onChange={(e) => this.handleNameInputChange(e)}
|
||||||
|
onBlur={(e) => this.handleNameInputBlur(e)}
|
||||||
|
onKeyDown={(e) => this.handleNameInputKeyDown(e)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{isDeletable &&
|
||||||
|
<button styleName='deleteButton'
|
||||||
|
onClick={(e) => this.handleDeleteButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-times'/>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SnippetTab.propTypes = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(SnippetTab, styles)
|
||||||
102
browser/main/Detail/SnippetTab.styl
Normal file
102
browser/main/Detail/SnippetTab.styl
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
.root
|
||||||
|
position relative
|
||||||
|
flex 1
|
||||||
|
overflow hidden
|
||||||
|
border-right $ui-border
|
||||||
|
&:last-child
|
||||||
|
border-right none
|
||||||
|
&:hover
|
||||||
|
.deleteButton
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
background-color darken($ui-backgroundColor, 15%)
|
||||||
|
&:active
|
||||||
|
color white
|
||||||
|
background-color $ui-active-color
|
||||||
|
.root--active
|
||||||
|
@extend .root
|
||||||
|
min-width 100px
|
||||||
|
.button
|
||||||
|
border-color $brand-color
|
||||||
|
|
||||||
|
.button
|
||||||
|
width 100%
|
||||||
|
height 29px
|
||||||
|
overflow ellipsis
|
||||||
|
text-align left
|
||||||
|
padding-right 30px
|
||||||
|
border none
|
||||||
|
background-color transparent
|
||||||
|
transition 0.15s
|
||||||
|
border-left 4px solid transparent
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--hover-backgroundColor
|
||||||
|
&:active, &:active:hover
|
||||||
|
border-color $brand-color
|
||||||
|
|
||||||
|
.deleteButton
|
||||||
|
position absolute
|
||||||
|
top 5px
|
||||||
|
height 20px
|
||||||
|
right 5px
|
||||||
|
width 20px
|
||||||
|
text-align center
|
||||||
|
border none
|
||||||
|
padding 0
|
||||||
|
color transparent
|
||||||
|
background-color transparent
|
||||||
|
border-radius 2px
|
||||||
|
|
||||||
|
.input
|
||||||
|
height 29px
|
||||||
|
border $ui-active-color
|
||||||
|
padding 0 5px
|
||||||
|
width 100%
|
||||||
|
outline none
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
color $ui-dark-text-color
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||||
|
&:active
|
||||||
|
color white
|
||||||
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
color $ui-dark-text-color
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
.button
|
||||||
|
border-color $brand-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||||
|
&:active
|
||||||
|
color white
|
||||||
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
|
||||||
|
.button
|
||||||
|
border none
|
||||||
|
color $ui-dark-text-color
|
||||||
|
background-color transparent
|
||||||
|
transition color background-color 0.15s
|
||||||
|
border-left 4px solid transparent
|
||||||
|
&:hover
|
||||||
|
color white
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
&:active
|
||||||
|
color $ui-dark-button--active-color
|
||||||
|
&:active, &:active:hover
|
||||||
|
color $ui-dark-button--active-color
|
||||||
|
|
||||||
|
.input
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
color white
|
||||||
68
browser/main/Detail/StarButton.js
Normal file
68
browser/main/Detail/StarButton.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './StarButton.styl'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
class StarButton extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isActive: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave (e) {
|
||||||
|
this.setState({
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { className } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={_.isString(className)
|
||||||
|
? 'StarButton ' + className
|
||||||
|
: 'StarButton'
|
||||||
|
}
|
||||||
|
styleName={this.state.isActive || this.props.isActive
|
||||||
|
? 'root--active'
|
||||||
|
: 'root'
|
||||||
|
}
|
||||||
|
onMouseDown={(e) => this.handleMouseDown(e)}
|
||||||
|
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||||
|
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
>
|
||||||
|
<i styleName='icon'
|
||||||
|
className={this.state.isActive || this.props.isActive
|
||||||
|
? 'fa fa-star'
|
||||||
|
: 'fa fa-star-o'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span styleName='tooltip'>Star Note</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StarButton.propTypes = {
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
className: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(StarButton, styles)
|
||||||
27
browser/main/Detail/StarButton.styl
Normal file
27
browser/main/Detail/StarButton.styl
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
.root
|
||||||
|
position relative
|
||||||
|
padding 0
|
||||||
|
&:hover
|
||||||
|
.icon
|
||||||
|
transform rotate(-72deg)
|
||||||
|
.tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
@extend .root
|
||||||
|
color $brand-color
|
||||||
|
&:hover
|
||||||
|
color $brand-color !important
|
||||||
|
.icon
|
||||||
|
transform rotate(-72deg)
|
||||||
|
.icon
|
||||||
|
transition transform 0.15s
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position fixed
|
||||||
|
top 45px
|
||||||
|
right 65px
|
||||||
|
padding 5px
|
||||||
|
opacity 0
|
||||||
|
border-radius 2px
|
||||||
152
browser/main/Detail/TagSelect.js
Normal file
152
browser/main/Detail/TagSelect.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './TagSelect.styl'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
class TagSelect extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
newTag: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.value = this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
this.value = this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewTagInputKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 9:
|
||||||
|
e.preventDefault()
|
||||||
|
this.submitTag()
|
||||||
|
break
|
||||||
|
case 13:
|
||||||
|
this.submitTag()
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
if (this.refs.newTag.value.length === 0) {
|
||||||
|
this.removeLastTag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLastTag () {
|
||||||
|
let { value } = this.props
|
||||||
|
|
||||||
|
value = _.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: []
|
||||||
|
value.pop()
|
||||||
|
value = _.uniq(value)
|
||||||
|
|
||||||
|
this.value = value
|
||||||
|
this.props.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
reset () {
|
||||||
|
this.setState({
|
||||||
|
newTag: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
submitTag () {
|
||||||
|
let { value } = this.props
|
||||||
|
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||||
|
|
||||||
|
if (newTag.length <= 0) {
|
||||||
|
this.setState({
|
||||||
|
newTag: ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value = _.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: []
|
||||||
|
value.push(newTag)
|
||||||
|
value = _.uniq(value)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
newTag: ''
|
||||||
|
}, () => {
|
||||||
|
this.value = value
|
||||||
|
this.props.onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewTagInputChange (e) {
|
||||||
|
this.setState({
|
||||||
|
newTag: this.refs.newTag.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTagRemoveButtonClick (tag) {
|
||||||
|
return (e) => {
|
||||||
|
let { value } = this.props
|
||||||
|
|
||||||
|
value.splice(value.indexOf(tag), 1)
|
||||||
|
value = _.uniq(value)
|
||||||
|
|
||||||
|
this.value = value
|
||||||
|
this.props.onChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { value, className } = this.props
|
||||||
|
|
||||||
|
let tagList = _.isArray(value)
|
||||||
|
? value.map((tag) => {
|
||||||
|
return (
|
||||||
|
<span styleName='tag'
|
||||||
|
key={tag}
|
||||||
|
>
|
||||||
|
<button styleName='tag-removeButton'
|
||||||
|
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-times fa-fw'/>
|
||||||
|
</button>
|
||||||
|
<span styleName='tag-label'>{tag}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={_.isString(className)
|
||||||
|
? 'TagSelect ' + className
|
||||||
|
: 'TagSelect'
|
||||||
|
}
|
||||||
|
styleName='root'
|
||||||
|
>
|
||||||
|
<i styleName='icon'
|
||||||
|
className='fa fa-tags'
|
||||||
|
/>
|
||||||
|
{tagList}
|
||||||
|
<input styleName='newTag'
|
||||||
|
ref='newTag'
|
||||||
|
value={this.state.newTag}
|
||||||
|
placeholder='Add tag...'
|
||||||
|
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||||
|
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagSelect.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
onChange: PropTypes.func
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(TagSelect, styles)
|
||||||
87
browser/main/Detail/TagSelect.styl
Normal file
87
browser/main/Detail/TagSelect.styl
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
.root
|
||||||
|
position relative
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
.icon
|
||||||
|
display inline-block
|
||||||
|
width 30px
|
||||||
|
vertical-align middle
|
||||||
|
text-align center
|
||||||
|
color $ui-button-color
|
||||||
|
|
||||||
|
.tag
|
||||||
|
display inline-block
|
||||||
|
margin 0 2px
|
||||||
|
vertical-align middle
|
||||||
|
height 20px
|
||||||
|
background-color white
|
||||||
|
border-radius 3px
|
||||||
|
overflow hidden
|
||||||
|
clearfix()
|
||||||
|
|
||||||
|
.tag-removeButton
|
||||||
|
float left
|
||||||
|
height 20px
|
||||||
|
width 18px
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
border-width 0 0 0 3px
|
||||||
|
line-height 18px
|
||||||
|
background-color transparent
|
||||||
|
color $ui-button-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--hover-backgroundColor
|
||||||
|
&:active, &:active:hover
|
||||||
|
color $ui-button--active-color
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
&:focus
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
|
||||||
|
.tag-label
|
||||||
|
float left
|
||||||
|
height 20px
|
||||||
|
line-height 20px
|
||||||
|
padding 0 6px
|
||||||
|
|
||||||
|
.newTag
|
||||||
|
display inline-block
|
||||||
|
margin 0 2px
|
||||||
|
vertical-align middle
|
||||||
|
height 24px
|
||||||
|
box-sizing borde-box
|
||||||
|
border none
|
||||||
|
border-bottom $ui-border
|
||||||
|
background-color transparent
|
||||||
|
outline none
|
||||||
|
padding 0 4px
|
||||||
|
&:focus
|
||||||
|
border-color $ui-input--focus-borderColor = #369DCD
|
||||||
|
&:disabled
|
||||||
|
background-color $ui-input--disabled-backgroundColor = #DDD
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.icon
|
||||||
|
color $ui-dark-button-color
|
||||||
|
|
||||||
|
.tag
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
|
||||||
|
.tag-removeButton
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
background-color transparent
|
||||||
|
color $ui-button-color
|
||||||
|
|
||||||
|
.tag-label
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.newTag
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
background-color transparent
|
||||||
|
color $ui-dark-text-color
|
||||||
|
&:focus
|
||||||
|
border-color $ui-input--focus-borderColor = #369DCD
|
||||||
|
&:disabled
|
||||||
|
background-color $ui-input--disabled-backgroundColor = #DDD
|
||||||
103
browser/main/Detail/index.js
Normal file
103
browser/main/Detail/index.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './Detail.styl'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import MarkdownNoteDetail from './MarkdownNoteDetail'
|
||||||
|
import SnippetNoteDetail from './SnippetNoteDetail'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
import StatusBar from '../StatusBar'
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
class Detail extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.focusHandler = () => {
|
||||||
|
this.refs.root != null && this.refs.root.focus()
|
||||||
|
}
|
||||||
|
this.deleteHandler = () => {
|
||||||
|
this.refs.root != null && this.refs.root.handleDeleteMenuClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
ee.on('detail:focus', this.focusHandler)
|
||||||
|
ee.on('detail:delete', this.deleteHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ee.off('detail:focus', this.focusHandler)
|
||||||
|
ee.off('detail:delete', this.deleteHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { location, data, config } = this.props
|
||||||
|
let note = null
|
||||||
|
if (location.query.key != null) {
|
||||||
|
let splitted = location.query.key.split('-')
|
||||||
|
let storageKey = splitted.shift()
|
||||||
|
let noteKey = splitted.shift()
|
||||||
|
|
||||||
|
note = data.noteMap.get(storageKey + '-' + noteKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note == null) {
|
||||||
|
return (
|
||||||
|
<div styleName='root'
|
||||||
|
style={this.props.style}
|
||||||
|
tabIndex='0'
|
||||||
|
>
|
||||||
|
<div styleName='empty'>
|
||||||
|
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new post</div>
|
||||||
|
</div>
|
||||||
|
<StatusBar
|
||||||
|
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.type === 'SNIPPET_NOTE') {
|
||||||
|
return (
|
||||||
|
<SnippetNoteDetail
|
||||||
|
note={note}
|
||||||
|
config={config}
|
||||||
|
ref='root'
|
||||||
|
{..._.pick(this.props, [
|
||||||
|
'dispatch',
|
||||||
|
'data',
|
||||||
|
'style',
|
||||||
|
'ignorePreviewPointerEvents',
|
||||||
|
'location'
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarkdownNoteDetail
|
||||||
|
note={note}
|
||||||
|
config={config}
|
||||||
|
ref='root'
|
||||||
|
{..._.pick(this.props, [
|
||||||
|
'dispatch',
|
||||||
|
'data',
|
||||||
|
'style',
|
||||||
|
'ignorePreviewPointerEvents',
|
||||||
|
'location'
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Detail.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
style: PropTypes.shape({
|
||||||
|
left: PropTypes.number
|
||||||
|
}),
|
||||||
|
ignorePreviewPointerEvents: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(Detail, styles)
|
||||||
230
browser/main/Main.js
Normal file
230
browser/main/Main.js
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './Main.styl'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import SideNav from './SideNav'
|
||||||
|
import TopBar from './TopBar'
|
||||||
|
import NoteList from './NoteList'
|
||||||
|
import Detail from './Detail'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
import modal from 'browser/main/lib/modal'
|
||||||
|
import InitModal from 'browser/main/modals/InitModal'
|
||||||
|
import mixpanel from 'browser/main/lib/mixpanel'
|
||||||
|
|
||||||
|
function focused () {
|
||||||
|
mixpanel.track('MAIN_FOCUSED')
|
||||||
|
}
|
||||||
|
|
||||||
|
class Main extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
let { config } = props
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isRightSliderFocused: false,
|
||||||
|
listWidth: config.listWidth,
|
||||||
|
navWidth: config.listWidth,
|
||||||
|
isLeftSliderFocused: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildContext () {
|
||||||
|
let { status, config } = this.props
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
let { dispatch, config } = this.props
|
||||||
|
|
||||||
|
if (config.ui.theme === 'dark') {
|
||||||
|
document.body.setAttribute('data-theme', 'dark')
|
||||||
|
} else {
|
||||||
|
document.body.setAttribute('data-theme', 'default')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload all data
|
||||||
|
dataApi.init()
|
||||||
|
.then((data) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'INIT_ALL',
|
||||||
|
storages: data.storages,
|
||||||
|
notes: data.notes
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.storages.length < 1) {
|
||||||
|
modal.open(InitModal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('focus', focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
window.removeEventListener('focus', focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLeftSlideMouseDown (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.setState({
|
||||||
|
isLeftSliderFocused: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRightSlideMouseDown (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.setState({
|
||||||
|
isRightSliderFocused: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp (e) {
|
||||||
|
if (this.state.isRightSliderFocused) {
|
||||||
|
this.setState({
|
||||||
|
isRightSliderFocused: false
|
||||||
|
}, () => {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
let newListWidth = this.state.listWidth
|
||||||
|
// TODO: ConfigManager should dispatch itself.
|
||||||
|
ConfigManager.set({listWidth: newListWidth})
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_LIST_WIDTH',
|
||||||
|
listWidth: newListWidth
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.state.isLeftSliderFocused) {
|
||||||
|
this.setState({
|
||||||
|
isLeftSliderFocused: false
|
||||||
|
}, () => {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
let navWidth = this.state.navWidth
|
||||||
|
// TODO: ConfigManager should dispatch itself.
|
||||||
|
ConfigManager.set({listWidth: navWidth})
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_NAV_WIDTH',
|
||||||
|
listWidth: navWidth
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove (e) {
|
||||||
|
if (this.state.isRightSliderFocused) {
|
||||||
|
let offset = this.refs.body.getBoundingClientRect().left
|
||||||
|
let newListWidth = e.pageX - offset
|
||||||
|
if (newListWidth < 10) {
|
||||||
|
newListWidth = 10
|
||||||
|
} else if (newListWidth > 600) {
|
||||||
|
newListWidth = 600
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
listWidth: newListWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.state.isLeftSliderFocused) {
|
||||||
|
let navWidth = e.pageX
|
||||||
|
if (navWidth < 80) {
|
||||||
|
navWidth = 80
|
||||||
|
} else if (navWidth > 600) {
|
||||||
|
navWidth = 600
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
navWidth: navWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { config } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='Main'
|
||||||
|
styleName='root'
|
||||||
|
onMouseMove={(e) => this.handleMouseMove(e)}
|
||||||
|
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||||
|
>
|
||||||
|
<SideNav
|
||||||
|
{..._.pick(this.props, [
|
||||||
|
'dispatch',
|
||||||
|
'data',
|
||||||
|
'config',
|
||||||
|
'location'
|
||||||
|
])}
|
||||||
|
width={this.state.navWidth}
|
||||||
|
/>
|
||||||
|
{!config.isSideNavFolded &&
|
||||||
|
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
|
||||||
|
style={{left: this.state.navWidth - 1}}
|
||||||
|
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
|
||||||
|
draggable='false'
|
||||||
|
>
|
||||||
|
<div styleName='slider-hitbox'/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||||
|
ref='body'
|
||||||
|
style={{left: config.isSideNavFolded ? 44 : this.state.navWidth}}
|
||||||
|
>
|
||||||
|
<TopBar style={{width: this.state.listWidth}}
|
||||||
|
{..._.pick(this.props, [
|
||||||
|
'dispatch',
|
||||||
|
'config',
|
||||||
|
'data',
|
||||||
|
'params',
|
||||||
|
'location'
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
<NoteList style={{width: this.state.listWidth}}
|
||||||
|
{..._.pick(this.props, [
|
||||||
|
'dispatch',
|
||||||
|
'data',
|
||||||
|
'config',
|
||||||
|
'params',
|
||||||
|
'location'
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
<div styleName={this.state.isRightSliderFocused ? 'slider--active' : 'slider'}
|
||||||
|
style={{left: this.state.listWidth}}
|
||||||
|
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
|
||||||
|
draggable='false'
|
||||||
|
>
|
||||||
|
<div styleName='slider-hitbox' />
|
||||||
|
</div>
|
||||||
|
<Detail
|
||||||
|
style={{left: this.state.listWidth + 1}}
|
||||||
|
{..._.pick(this.props, [
|
||||||
|
'dispatch',
|
||||||
|
'data',
|
||||||
|
'config',
|
||||||
|
'params',
|
||||||
|
'location'
|
||||||
|
])}
|
||||||
|
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Main.childContextTypes = {
|
||||||
|
status: PropTypes.shape({
|
||||||
|
updateReady: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
config: PropTypes.shape({}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
Main.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
data: PropTypes.shape({}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect((x) => x)(CSSModules(Main, styles))
|
||||||
41
browser/main/Main.styl
Normal file
41
browser/main/Main.styl
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.root
|
||||||
|
absolute top left bottom right
|
||||||
|
|
||||||
|
.body
|
||||||
|
absolute right top bottom
|
||||||
|
left $sideNav-width
|
||||||
|
|
||||||
|
.body--expanded
|
||||||
|
@extend .body
|
||||||
|
left $sideNav--folded-width
|
||||||
|
|
||||||
|
.slider
|
||||||
|
absolute top bottom
|
||||||
|
width 1px
|
||||||
|
background-color $ui-borderColor
|
||||||
|
border-width 0
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-borderColor
|
||||||
|
|
||||||
|
.slider--active
|
||||||
|
@extend .slider
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
|
||||||
|
.slider-hitbox
|
||||||
|
absolute top bottom left right
|
||||||
|
width 7px
|
||||||
|
left -3px
|
||||||
|
z-index 10
|
||||||
|
cursor col-resize
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
absolute top left bottom right
|
||||||
|
|
||||||
|
.slider
|
||||||
|
background-color $ui-dark-borderColor
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
|
.slider--active
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
function basicFilter (keyword, articles) {
|
|
||||||
if (keyword === '' || keyword == null) return articles
|
|
||||||
var firstFiltered = articles.filter(function (article) {
|
|
||||||
|
|
||||||
var first = article.type === 'code' ? article.description : article.title
|
|
||||||
if (first.match(new RegExp(keyword, 'i'))) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
var secondFiltered = articles.filter(function (article) {
|
|
||||||
var second = article.type === 'code' ? article.content : article.content
|
|
||||||
if (second.match(new RegExp(keyword, 'i'))) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
return firstFiltered.concat(secondFiltered).filter(function (value, index, self) {
|
|
||||||
return self.indexOf(value) === index
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function codeFilter (articles) {
|
|
||||||
return articles.filter(function (article) {
|
|
||||||
return article.type === 'code'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function noteFilter (articles) {
|
|
||||||
return articles.filter(function (article) {
|
|
||||||
return article.type === 'note'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function tagFilter (keyword, articles) {
|
|
||||||
return articles.filter(function (article) {
|
|
||||||
return article.Tags.some(function (tag) {
|
|
||||||
return tag.name.match(new RegExp('^' + keyword, 'i'))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchArticle (search, articles) {
|
|
||||||
var keywords = search.split(' ')
|
|
||||||
|
|
||||||
for (var keyword of keywords) {
|
|
||||||
if (keyword.match(/^\$c/, 'i')) {
|
|
||||||
articles = codeFilter(articles)
|
|
||||||
continue
|
|
||||||
} else if (keyword.match(/^\$n/, 'i')) {
|
|
||||||
articles = noteFilter(articles)
|
|
||||||
continue
|
|
||||||
} else if (keyword.match(/^#[A-Za-z0-9]+/)) {
|
|
||||||
articles = tagFilter(keyword.substring(1, keyword.length), articles)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
articles = basicFilter(keyword, articles)
|
|
||||||
}
|
|
||||||
|
|
||||||
return articles.sort(function (a, b) {
|
|
||||||
return new Date(b.updatedAt) - new Date(a.updatedAt)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
searchArticle: searchArticle
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/* global localStorage*/
|
|
||||||
|
|
||||||
var mixin = {}
|
|
||||||
|
|
||||||
mixin.OnlyGuest = {
|
|
||||||
componentDidMount: function () {
|
|
||||||
var currentUser = localStorage.getItem('currentUser')
|
|
||||||
|
|
||||||
if (currentUser == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.transitionTo('userHome', {userName: currentUser.name})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin.OnlyUser = {
|
|
||||||
componentDidMount: function () {
|
|
||||||
var currentUser = localStorage.getItem('currentUser')
|
|
||||||
|
|
||||||
if (currentUser == null) {
|
|
||||||
this.transitionTo('login')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = mixin
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
var shell = require('shell')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
openExternal: function (e) {
|
|
||||||
shell.openExternal(e.currentTarget.href)
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
var ForceUpdate = function (interval) {
|
|
||||||
return {
|
|
||||||
componentDidMount: function () {
|
|
||||||
this.refreshTimer = setInterval(function () {
|
|
||||||
this.forceUpdate()
|
|
||||||
}.bind(this), interval)
|
|
||||||
},
|
|
||||||
componentWillUnmount: function () {
|
|
||||||
clearInterval(this.refreshTimer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ForceUpdate
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
function deleteItemFromTargetArray (item, targetArray) {
|
|
||||||
if (targetArray == null) targetArray = []
|
|
||||||
targetArray.some(function (_item, index) {
|
|
||||||
if (_item.id === item.id) {
|
|
||||||
targetArray.splice(index, 1)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
return targetArray
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateItemToTargetArray (item, targetArray) {
|
|
||||||
if (targetArray == null) targetArray = []
|
|
||||||
|
|
||||||
var isNew = !targetArray.some(function (_item, index) {
|
|
||||||
if (_item.id === item.id) {
|
|
||||||
targetArray.splice(index, 1, item)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isNew) targetArray.push(item)
|
|
||||||
|
|
||||||
return targetArray
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
deleteItemFromTargetArray: deleteItemFromTargetArray,
|
|
||||||
updateItemToTargetArray: updateItemToTargetArray
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
var Reflux = require('reflux')
|
|
||||||
|
|
||||||
var state = {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyDown = Reflux.createAction()
|
|
||||||
|
|
||||||
var KeyStore = Reflux.createStore({
|
|
||||||
init: function () {
|
|
||||||
this.listenTo(keyDown, this.onKeyDown)
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
keyDown(e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setState: function (newState, cb) {
|
|
||||||
for (var key in newState) {
|
|
||||||
state[key] = newState[key]
|
|
||||||
}
|
|
||||||
if (typeof cb === 'function') cb()
|
|
||||||
},
|
|
||||||
onKeyDown: function (e) {
|
|
||||||
/*
|
|
||||||
Modals
|
|
||||||
*/
|
|
||||||
if (state.codeForm || state.noteForm || state.noteDeleteModal || state.codeDeleteModal || state.addMemberModal || state.aboutModal || state.editProfileModal || state.contactModal || state.teamCreateModal || state.planetCreateModal || state.planetSettingModal || state.teamSettingsModal || state.logoutModal) {
|
|
||||||
// ESC
|
|
||||||
if (e.keyCode === 27) this.cast('closeModal')
|
|
||||||
|
|
||||||
// Cmd + Enter
|
|
||||||
if (e.keyCode === 13 && e.metaKey) {
|
|
||||||
if (state.codeForm) this.cast('submitCodeForm')
|
|
||||||
if (state.noteForm) this.cast('submitNoteForm')
|
|
||||||
if (state.codeDeleteModal) this.cast('submitCodeDeleteModal')
|
|
||||||
if (state.noteDeleteModal) this.cast('submitNoteDeleteModal')
|
|
||||||
if (state.addMemberModal) this.cast('submitAddMemberModal')
|
|
||||||
if (state.contactModal) this.cast('submitContactModal')
|
|
||||||
if (state.teamCreateModal) this.cast('submitTeamCreateModal')
|
|
||||||
if (state.planetCreateModal) this.cast('submitPlanetCreateModal')
|
|
||||||
if (state.logoutModal) this.cast('submitLogoutModal')
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
PlanetContainer
|
|
||||||
*/
|
|
||||||
if (state.planetContainer) {
|
|
||||||
// Cmd + Enter, A
|
|
||||||
if ((e.keyCode === 13 && e.metaKey) || e.keyCode === 65) this.cast('openLaunchModal')
|
|
||||||
|
|
||||||
// Esc
|
|
||||||
if (e.keyCode === 27) this.cast('toggleFocusSearchInput')
|
|
||||||
|
|
||||||
// Up
|
|
||||||
if (e.keyCode === 38) this.cast('selectPriorArticle')
|
|
||||||
|
|
||||||
// Down
|
|
||||||
if (e.keyCode === 40) this.cast('selectNextArticle')
|
|
||||||
|
|
||||||
// E
|
|
||||||
if (e.keyCode === 69) this.cast('openEditModal')
|
|
||||||
|
|
||||||
// D
|
|
||||||
if (e.keyCode === 68) this.cast('openDeleteModal')
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
HomeContainer
|
|
||||||
*/
|
|
||||||
if (state.homeContainer) {
|
|
||||||
if (e.keyCode > 48 && e.keyCode < 58 && e.metaKey) {
|
|
||||||
this.cast('switchPlanet', e.keyCode - 48)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cast: function (status, data) {
|
|
||||||
this.trigger({
|
|
||||||
status: status,
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = function (stateKey) {
|
|
||||||
return {
|
|
||||||
mixins: [Reflux.listenTo(KeyStore, 'onKeyCast')],
|
|
||||||
componentDidMount: function () {
|
|
||||||
var newState = {}
|
|
||||||
newState[stateKey] = true
|
|
||||||
KeyStore.setState(newState)
|
|
||||||
},
|
|
||||||
componentWillUnmount: function () {
|
|
||||||
var newState = {}
|
|
||||||
newState[stateKey] = false
|
|
||||||
KeyStore.setState(newState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
function getIn (object, path) {
|
|
||||||
var stack = path.split('.')
|
|
||||||
while (stack.length > 1) {
|
|
||||||
object = object[stack.shift()]
|
|
||||||
}
|
|
||||||
return object[stack.shift()]
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateIn (object, path, value) {
|
|
||||||
var current = object
|
|
||||||
var stack = path.split('.')
|
|
||||||
while (stack.length > 1) {
|
|
||||||
current = current[stack.shift()]
|
|
||||||
}
|
|
||||||
current[stack.shift()] = value
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPartialState (component, path, value) {
|
|
||||||
component.setState(
|
|
||||||
updateIn(component.state, path, value))
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
linkState: function (path) {
|
|
||||||
return {
|
|
||||||
value: getIn(this.state, path),
|
|
||||||
requestChange: setPartialState.bind(null, this, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
var markdownit = require('markdown-it')
|
|
||||||
var md = markdownit({
|
|
||||||
typographer: true,
|
|
||||||
linkify: true
|
|
||||||
})
|
|
||||||
|
|
||||||
var Markdown = {
|
|
||||||
markdown: function (content) {
|
|
||||||
return md.render(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Markdown
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
var React = require('react/addons')
|
|
||||||
var ModalBase = React.createClass({
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
component: null,
|
|
||||||
componentProps: {},
|
|
||||||
isHidden: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
close: function () {
|
|
||||||
this.setState({component: null, componentProps: null, isHidden: true})
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var componentProps = this.state.componentProps
|
|
||||||
return (
|
|
||||||
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
|
|
||||||
<div onClick={this.close} className='modalBack'/>
|
|
||||||
{this.state.component == null ? null : (
|
|
||||||
<this.state.component {...componentProps} close={this.close}/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var modalBase = null
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
componentDidMount: function () {
|
|
||||||
if (modalBase == null) {
|
|
||||||
var el = document.createElement('div')
|
|
||||||
document.body.appendChild(el)
|
|
||||||
modalBase = React.render(<ModalBase/>, el)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openModal: function (component, props) {
|
|
||||||
modalBase.setState({component: component, componentProps: props, isHidden: false})
|
|
||||||
},
|
|
||||||
closeModal: function () {
|
|
||||||
modalBase.setState({isHidden: true})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
168
browser/main/NoteDetail/ShareButton.js
Normal file
168
browser/main/NoteDetail/ShareButton.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import api from 'browser/lib/api'
|
||||||
|
import clientKey from 'browser/lib/clientKey'
|
||||||
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
|
const clipboard = require('electron').clipboard
|
||||||
|
|
||||||
|
function notify (...args) {
|
||||||
|
return new window.Notification(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefault () {
|
||||||
|
return {
|
||||||
|
openDropdown: false,
|
||||||
|
isSharing: false,
|
||||||
|
// Fetched url
|
||||||
|
url: null,
|
||||||
|
// for tooltip Copy -> Copied!
|
||||||
|
copied: false,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ShareButton extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.state = getDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
this.setState(getDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.dropdownInterceptor = e => {
|
||||||
|
this.dropdownClicked = true
|
||||||
|
}
|
||||||
|
ReactDOM.findDOMNode(this.refs.dropdown).addEventListener('click', this.dropdownInterceptor)
|
||||||
|
this.shareViaPublicURLHandler = e => {
|
||||||
|
this.handleShareViaPublicURLClick(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
document.removeEventListener('click', this.dropdownHandler)
|
||||||
|
ReactDOM.findDOMNode(this.refs.dropdown).removeEventListener('click', this.dropdownInterceptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenButtonClick (e) {
|
||||||
|
this.openDropdown()
|
||||||
|
if (this.dropdownHandler == null) {
|
||||||
|
this.dropdownHandler = e => {
|
||||||
|
if (!this.dropdownClicked) {
|
||||||
|
this.closeDropdown()
|
||||||
|
} else {
|
||||||
|
this.dropdownClicked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.removeEventListener('click', this.dropdownHandler)
|
||||||
|
document.addEventListener('click', this.dropdownHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
openDropdown () {
|
||||||
|
this.setState({openDropdown: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDropdown () {
|
||||||
|
document.removeEventListener('click', this.dropdownHandler)
|
||||||
|
this.setState({openDropdown: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClipboardButtonClick (e) {
|
||||||
|
activityRecord.emit('MAIN_DETAIL_COPY')
|
||||||
|
clipboard.writeText(this.props.article.content)
|
||||||
|
notify('Saved to Clipboard!', {
|
||||||
|
body: 'Paste it wherever you want!'
|
||||||
|
})
|
||||||
|
this.setState({openDropdown: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShareViaPublicURLClick (e) {
|
||||||
|
let { user } = this.props
|
||||||
|
let input = Object.assign({}, this.props.article, {
|
||||||
|
clientKey: clientKey.get(),
|
||||||
|
writerName: user.name
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
isSharing: true,
|
||||||
|
failed: false
|
||||||
|
}, () => {
|
||||||
|
api.shareViaPublicURL(input)
|
||||||
|
.then(res => {
|
||||||
|
let url = res.body.url
|
||||||
|
this.setState({url: url, isSharing: false})
|
||||||
|
activityRecord.emit('ARTICLE_SHARE')
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
this.setState({isSharing: false, failed: true})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCopyURLClick () {
|
||||||
|
clipboard.writeText(this.state.url)
|
||||||
|
this.setState({copied: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore copy url tooltip
|
||||||
|
handleCopyURLMouseLeave () {
|
||||||
|
this.setState({copied: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let hasPublicURL = this.state.url != null
|
||||||
|
return (
|
||||||
|
<div className='ShareButton'>
|
||||||
|
<button ref='openButton' onClick={e => this.handleOpenButtonClick(e)} className='ShareButton-open-button'>
|
||||||
|
<i className='fa fa-fw fa-share-alt'/>
|
||||||
|
{
|
||||||
|
this.state.openDropdown ? null : (
|
||||||
|
<span className='tooltip'>Share</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div ref='dropdown' className={'ShareButton-dropdown' + (this.state.openDropdown ? '' : ' hide')}>
|
||||||
|
{
|
||||||
|
!hasPublicURL ? (
|
||||||
|
<button
|
||||||
|
onClick={e => this.shareViaPublicURLHandler(e)}
|
||||||
|
ref='sharePublicURL'
|
||||||
|
disabled={this.state.isSharing}>
|
||||||
|
<i className='fa fa-fw fa-external-link'/> {this.state.failed ? 'Failed : Click to Try again' : !this.state.isSharing ? 'Share via public URL' : 'Sharing...'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className='ShareButton-url'>
|
||||||
|
<input className='ShareButton-url-input' value={this.state.url} readOnly/>
|
||||||
|
<button
|
||||||
|
onClick={e => this.handleCopyURLClick(e)}
|
||||||
|
className='ShareButton-url-button'
|
||||||
|
onMouseLeave={e => this.handleCopyURLMouseLeave(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-fw fa-clipboard'/>
|
||||||
|
<div className='ShareButton-url-button-tooltip'>{this.state.copied ? 'Copied!' : 'Copy URL'}</div>
|
||||||
|
</button>
|
||||||
|
<div className='ShareButton-url-alert'>This url is valid for 7 days.</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<button onClick={e => this.handleClipboardButtonClick(e)}>
|
||||||
|
<i className='fa fa-fw fa-clipboard'/> Copy to clipboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShareButton.propTypes = {
|
||||||
|
article: PropTypes.shape({
|
||||||
|
publicURL: PropTypes.string,
|
||||||
|
content: PropTypes.string
|
||||||
|
}),
|
||||||
|
user: PropTypes.shape({
|
||||||
|
name: PropTypes.string
|
||||||
|
})
|
||||||
|
}
|
||||||
225
browser/main/NoteList/NoteList.styl
Normal file
225
browser/main/NoteList/NoteList.styl
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
.root
|
||||||
|
absolute left bottom
|
||||||
|
border-top $ui-border
|
||||||
|
top $topBar-height - 1
|
||||||
|
|
||||||
|
.control
|
||||||
|
absolute top left right
|
||||||
|
user-select none
|
||||||
|
height 25px
|
||||||
|
font-size 10px
|
||||||
|
border-bottom $ui-border
|
||||||
|
line-height 25px
|
||||||
|
display flex
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.control-sortBy
|
||||||
|
flex 1
|
||||||
|
padding-left 5px
|
||||||
|
|
||||||
|
.control-sortBy-select
|
||||||
|
margin-left 5px
|
||||||
|
padding 0
|
||||||
|
border none
|
||||||
|
background-color transparent
|
||||||
|
font-size 10px
|
||||||
|
|
||||||
|
.control-button
|
||||||
|
width 25px
|
||||||
|
padding 0
|
||||||
|
background-color transparent
|
||||||
|
border none
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
transition 0.15s
|
||||||
|
&:active, &:active:hover
|
||||||
|
color $ui-active-color
|
||||||
|
&:hover
|
||||||
|
color $ui-text-color
|
||||||
|
.control-button-tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.control-button--active
|
||||||
|
@extend .control-button
|
||||||
|
color $ui-active-color
|
||||||
|
&:hover
|
||||||
|
color $ui-active-color
|
||||||
|
|
||||||
|
.control-button-tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
top 20px
|
||||||
|
right 5px
|
||||||
|
padding 5px
|
||||||
|
opacity 0
|
||||||
|
white-space nowrap
|
||||||
|
border-radius 2px
|
||||||
|
z-index 1
|
||||||
|
|
||||||
|
.list
|
||||||
|
absolute left right bottom
|
||||||
|
top 24px
|
||||||
|
overflow auto
|
||||||
|
|
||||||
|
.item
|
||||||
|
position relative
|
||||||
|
border-bottom $ui-border
|
||||||
|
padding 2px 5px
|
||||||
|
user-select none
|
||||||
|
cursor pointer
|
||||||
|
transition background-color 0.15s
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-active-color, 20%)
|
||||||
|
&:active
|
||||||
|
background-color $ui-active-color
|
||||||
|
color white
|
||||||
|
.item-title
|
||||||
|
.item-title-empty
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-tagIcon
|
||||||
|
.item-bottom-tagList-empty
|
||||||
|
.item-bottom-time
|
||||||
|
color white
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
|
||||||
|
.item--active
|
||||||
|
@extend .item
|
||||||
|
background-color $ui-active-color
|
||||||
|
color white
|
||||||
|
.item-title
|
||||||
|
.item-title-empty
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-tagIcon
|
||||||
|
.item-bottom-tagList-empty
|
||||||
|
.item-bottom-time
|
||||||
|
color white
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
&:hover
|
||||||
|
background-color $ui-active-color
|
||||||
|
|
||||||
|
.item-border
|
||||||
|
absolute top bottom left right
|
||||||
|
border-style solid
|
||||||
|
border-width 2px
|
||||||
|
border-color transparent
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
|
.item-title
|
||||||
|
height 24px
|
||||||
|
box-sizing border-box
|
||||||
|
line-height 24px
|
||||||
|
padding 0
|
||||||
|
overflow ellipsis
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
.item-title-icon
|
||||||
|
font-size 12px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
padding-right 3px
|
||||||
|
|
||||||
|
.item-title-empty
|
||||||
|
font-weight normal
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-bottom
|
||||||
|
margin-top 2px
|
||||||
|
height 20px
|
||||||
|
font-size 12px
|
||||||
|
line-height 20px
|
||||||
|
overflow ellipsis
|
||||||
|
display flex
|
||||||
|
|
||||||
|
.item-bottom-tagIcon
|
||||||
|
vertical-align middle
|
||||||
|
color $ui-button-color
|
||||||
|
height 20px
|
||||||
|
line-height 20px
|
||||||
|
|
||||||
|
.item-bottom-tagList
|
||||||
|
flex 1
|
||||||
|
overflow ellipsis
|
||||||
|
line-height 20px
|
||||||
|
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
margin 0 4px
|
||||||
|
padding 0 4px
|
||||||
|
height 20px
|
||||||
|
box-sizing border-box
|
||||||
|
border-radius 3px
|
||||||
|
vertical-align middle
|
||||||
|
border-style solid
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
border-width 0 0 0 3px
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
.item-bottom-tagList-empty
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
vertical-align middle
|
||||||
|
font-size 10px
|
||||||
|
margin-left 5px
|
||||||
|
|
||||||
|
.item-bottom-time
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
margin-left 5px
|
||||||
|
font-size 10px
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
|
||||||
|
.item
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-active-color, 20%)
|
||||||
|
|
||||||
|
.item--active
|
||||||
|
@extend .item
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
.item-title
|
||||||
|
color white
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
.item-bottom-tagList-empty
|
||||||
|
color white
|
||||||
|
&:hover
|
||||||
|
background-color $ui-active-color
|
||||||
|
|
||||||
|
.item-title
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.item-title-icon
|
||||||
|
color $ui-darkinactive-text-color
|
||||||
|
|
||||||
|
.item-title-empty
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
.item-bottom-tagIcon
|
||||||
|
color $ui-dark-button-color
|
||||||
|
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
border-color $ui-dark-button--focus-borderColor
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.item-bottom-tagList-empty
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
vertical-align middle
|
||||||
|
|
||||||
|
.control
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
.control-sortBy-select
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.control-button
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
434
browser/main/NoteList/index.js
Normal file
434
browser/main/NoteList/index.js
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './NoteList.styl'
|
||||||
|
import moment from 'moment'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
|
||||||
|
const { remote } = require('electron')
|
||||||
|
const { Menu, MenuItem, dialog } = remote
|
||||||
|
|
||||||
|
function sortByCreatedAt (a, b) {
|
||||||
|
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByAlphabetical (a, b) {
|
||||||
|
return a.title.localeCompare(b.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByUpdatedAt (a, b) {
|
||||||
|
return new Date(b.updatedAt) - new Date(a.updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoteList extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.selectNextNoteHandler = () => {
|
||||||
|
console.log('fired next')
|
||||||
|
this.selectNextNote()
|
||||||
|
}
|
||||||
|
this.selectPriorNoteHandler = () => {
|
||||||
|
this.selectPriorNote()
|
||||||
|
}
|
||||||
|
this.focusHandler = () => {
|
||||||
|
this.refs.list.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||||
|
ee.on('list:next', this.selectNextNoteHandler)
|
||||||
|
ee.on('list:prior', this.selectPriorNoteHandler)
|
||||||
|
ee.on('list:focus', this.focusHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.location.pathname !== this.props.location.pathname) {
|
||||||
|
this.resetScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetScroll () {
|
||||||
|
this.refs.list.scrollTop = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
clearInterval(this.refreshTimer)
|
||||||
|
|
||||||
|
ee.off('list:next', this.selectNextNoteHandler)
|
||||||
|
ee.off('list:prior', this.selectPriorNoteHandler)
|
||||||
|
ee.off('list:focus', this.focusHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
let { location } = this.props
|
||||||
|
|
||||||
|
if (this.notes.length > 0 && location.query.key == null) {
|
||||||
|
let { router } = this.context
|
||||||
|
router.replace({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {
|
||||||
|
key: this.notes[0].storage + '-' + this.notes[0].key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto scroll
|
||||||
|
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
|
||||||
|
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||||
|
return note != null && note.storage + '-' + note.key === location.query.key
|
||||||
|
})
|
||||||
|
if (targetIndex > -1) {
|
||||||
|
let list = this.refs.list
|
||||||
|
let item = list.childNodes[targetIndex]
|
||||||
|
|
||||||
|
if (item == null) return false
|
||||||
|
|
||||||
|
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||||
|
if (overflowBelow) {
|
||||||
|
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||||
|
}
|
||||||
|
let overflowAbove = list.scrollTop > item.offsetTop
|
||||||
|
if (overflowAbove) {
|
||||||
|
list.scrollTop = item.offsetTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPriorNote () {
|
||||||
|
if (this.notes == null || this.notes.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let { router } = this.context
|
||||||
|
let { location } = this.props
|
||||||
|
|
||||||
|
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||||
|
return note.storage + '-' + note.key === location.query.key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (targetIndex === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetIndex--
|
||||||
|
if (targetIndex < 0) targetIndex = 0
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {
|
||||||
|
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNextNote () {
|
||||||
|
if (this.notes == null || this.notes.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let { router } = this.context
|
||||||
|
let { location } = this.props
|
||||||
|
|
||||||
|
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||||
|
return note.storage + '-' + note.key === location.query.key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (targetIndex === this.notes.length - 1) {
|
||||||
|
targetIndex = 0
|
||||||
|
} else {
|
||||||
|
targetIndex++
|
||||||
|
if (targetIndex < 0) targetIndex = 0
|
||||||
|
else if (targetIndex > this.notes.length - 1) targetIndex === this.notes.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {
|
||||||
|
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ee.emit('list:moved')
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNoteListKeyDown (e) {
|
||||||
|
if (e.metaKey || e.ctrlKey) return true
|
||||||
|
|
||||||
|
if (e.keyCode === 65 && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
ee.emit('top:new-note')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 68) {
|
||||||
|
e.preventDefault()
|
||||||
|
ee.emit('detail:delete')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 69) {
|
||||||
|
e.preventDefault()
|
||||||
|
ee.emit('detail:focus')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 38) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.selectPriorNote()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 40) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.selectNextNote()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotes () {
|
||||||
|
let { data, params, location } = this.props
|
||||||
|
|
||||||
|
if (location.pathname.match(/\/home/)) {
|
||||||
|
return data.noteMap.map((note) => note)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.pathname.match(/\/starred/)) {
|
||||||
|
return data.starredSet.toJS()
|
||||||
|
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
let storageKey = params.storageKey
|
||||||
|
let folderKey = params.folderKey
|
||||||
|
let storage = data.storageMap.get(storageKey)
|
||||||
|
if (storage == null) return []
|
||||||
|
|
||||||
|
let folder = _.find(storage.folders, {key: folderKey})
|
||||||
|
if (folder == null) {
|
||||||
|
let storageNoteSet = data.storageNoteMap
|
||||||
|
.get(storage.key)
|
||||||
|
if (storageNoteSet == null) storageNoteSet = []
|
||||||
|
return storageNoteSet
|
||||||
|
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
let folderNoteKeyList = data.folderNoteMap
|
||||||
|
.get(storage.key + '-' + folder.key)
|
||||||
|
|
||||||
|
return folderNoteKeyList != null
|
||||||
|
? folderNoteKeyList
|
||||||
|
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNoteClick (e, uniqueKey) {
|
||||||
|
let { router } = this.context
|
||||||
|
let { location } = this.props
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {
|
||||||
|
key: uniqueKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNoteContextMenu (e, uniqueKey) {
|
||||||
|
let menu = new Menu()
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Delete Note',
|
||||||
|
click: (e) => this.handleDeleteNote(e, uniqueKey)
|
||||||
|
}))
|
||||||
|
menu.popup()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteNote (e, uniqueKey) {
|
||||||
|
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Delete a note',
|
||||||
|
detail: 'This work cannot be undone.',
|
||||||
|
buttons: ['Confirm', 'Cancel']
|
||||||
|
})
|
||||||
|
if (index === 0) {
|
||||||
|
let { dispatch, location } = this.props
|
||||||
|
let splitted = uniqueKey.split('-')
|
||||||
|
let storageKey = splitted.shift()
|
||||||
|
let noteKey = splitted.shift()
|
||||||
|
|
||||||
|
dataApi
|
||||||
|
.deleteNote(storageKey, noteKey)
|
||||||
|
.then((data) => {
|
||||||
|
let dispatchHandler = () => {
|
||||||
|
dispatch({
|
||||||
|
type: 'DELETE_NOTE',
|
||||||
|
storageKey: data.storageKey,
|
||||||
|
noteKey: data.noteKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.query.key === uniqueKey) {
|
||||||
|
ee.once('list:moved', dispatchHandler)
|
||||||
|
ee.emit('list:next')
|
||||||
|
} else {
|
||||||
|
dispatchHandler()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSortByChange (e) {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
sortBy: e.target.value
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigManager.set(config)
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleListStyleButtonClick (e, style) {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
listStyle: style
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigManager.set(config)
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { location, notes, config } = this.props
|
||||||
|
let sortFunc = config.sortBy === 'CREATED_AT'
|
||||||
|
? sortByCreatedAt
|
||||||
|
: config.sortBy === 'ALPHABETICAL'
|
||||||
|
? sortByAlphabetical
|
||||||
|
: sortByUpdatedAt
|
||||||
|
this.notes = notes = this.getNotes()
|
||||||
|
.sort(sortFunc)
|
||||||
|
|
||||||
|
let noteList = notes
|
||||||
|
.map((note) => {
|
||||||
|
if (note == null) return null
|
||||||
|
let tagElements = _.isArray(note.tags)
|
||||||
|
? note.tags.map((tag) => {
|
||||||
|
return (
|
||||||
|
<span styleName='item-bottom-tagList-item'
|
||||||
|
key={tag}>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
let isActive = location.query.key === note.storage + '-' + note.key
|
||||||
|
return (
|
||||||
|
<div styleName={isActive
|
||||||
|
? 'item--active'
|
||||||
|
: 'item'
|
||||||
|
}
|
||||||
|
key={note.storage + '-' + note.key}
|
||||||
|
onClick={(e) => this.handleNoteClick(e, note.storage + '-' + note.key)}
|
||||||
|
onContextMenu={(e) => this.handleNoteContextMenu(e, note.storage + '-' + note.key)}
|
||||||
|
>
|
||||||
|
<div styleName='item-title'>
|
||||||
|
{note.type === 'SNIPPET_NOTE'
|
||||||
|
? <i styleName='item-title-icon' className='fa fa-fw fa-code'/>
|
||||||
|
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o'/>
|
||||||
|
}
|
||||||
|
{note.title.trim().length > 0
|
||||||
|
? note.title
|
||||||
|
: <span styleName='item-title-empty'>Empty</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.listStyle === 'DEFAULT' &&
|
||||||
|
<div styleName='item-bottom'>
|
||||||
|
<i styleName='item-bottom-tagIcon'
|
||||||
|
className='fa fa-tags fa-fw'
|
||||||
|
/>
|
||||||
|
<div styleName='item-bottom-tagList'>
|
||||||
|
{tagElements.length > 0
|
||||||
|
? tagElements
|
||||||
|
: <span styleName='item-bottom-tagList-empty'>Not tagged yet</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='item-bottom-time'>
|
||||||
|
{moment(config.sortBy === 'CREATED_AT' ? note.createdAt : note.updatedAt).fromNow()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='NoteList'
|
||||||
|
styleName='root'
|
||||||
|
style={this.props.style}
|
||||||
|
>
|
||||||
|
<div styleName='control'>
|
||||||
|
<div styleName='control-sortBy'>
|
||||||
|
Sort by
|
||||||
|
<select styleName='control-sortBy-select'
|
||||||
|
value={config.sortBy}
|
||||||
|
onChange={(e) => this.handleSortByChange(e)}
|
||||||
|
>
|
||||||
|
<option value='UPDATED_AT'>Updated Time</option>
|
||||||
|
<option value='CREATED_AT'>Created Time</option>
|
||||||
|
<option value='ALPHABETICAL'>Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button styleName={config.listStyle === 'DEFAULT'
|
||||||
|
? 'control-button--active'
|
||||||
|
: 'control-button'
|
||||||
|
}
|
||||||
|
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
||||||
|
>
|
||||||
|
<i className='fa fa-th-list'/>
|
||||||
|
<span styleName='control-button-tooltip'>
|
||||||
|
Default Size
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button styleName={config.listStyle === 'SMALL'
|
||||||
|
? 'control-button--active'
|
||||||
|
: 'control-button'
|
||||||
|
}
|
||||||
|
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
||||||
|
>
|
||||||
|
<i className='fa fa-list'/>
|
||||||
|
<span styleName='control-button-tooltip'>
|
||||||
|
Small Size
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div styleName='list'
|
||||||
|
ref='list'
|
||||||
|
tabIndex='-1'
|
||||||
|
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
||||||
|
>
|
||||||
|
{noteList}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NoteList.contextTypes = {
|
||||||
|
router: PropTypes.shape([])
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteList.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
repositories: PropTypes.array,
|
||||||
|
style: PropTypes.shape({
|
||||||
|
width: PropTypes.number
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(NoteList, styles)
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
/* global localStorage */
|
|
||||||
|
|
||||||
var request = require('superagent-promise')(require('superagent'), Promise)
|
|
||||||
var apiUrl = require('../../../config').apiUrl
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
// Auth
|
|
||||||
login: function (input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'auth')
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
signup: function (input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'auth/signup')
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
getUser: function () {
|
|
||||||
return request
|
|
||||||
.get(apiUrl + 'auth/user')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
changePassword: function (input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'auth/password')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Resources
|
|
||||||
fetchUser: function (userName) {
|
|
||||||
return request
|
|
||||||
.get(apiUrl + 'resources/' + userName)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateUser: function (userName, input) {
|
|
||||||
return request
|
|
||||||
.put(apiUrl + 'resources/' + userName)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
createTeam: function (userName, input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'resources/' + userName + '/teams')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
addMember: function (userName, input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'resources/' + userName + '/members')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
removeMember: function (userName, input) {
|
|
||||||
return request
|
|
||||||
.del(apiUrl + 'resources/' + userName + '/members')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
createPlanet: function (userName, input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'resources/' + userName + '/planets')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
fetchPlanet: function (userName, planetName) {
|
|
||||||
return request
|
|
||||||
.get(apiUrl + 'resources/' + userName + '/planets/' + planetName)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updatePlanet: function (userName, planetName, input) {
|
|
||||||
return request
|
|
||||||
.put(apiUrl + 'resources/' + userName + '/planets/' + planetName)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
destroyPlanet: function (userName, planetName) {
|
|
||||||
return request
|
|
||||||
.del(apiUrl + 'resources/' + userName + '/planets/' + planetName)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createCode: function (userName, planetName, input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/codes')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
updateCode: function (userName, planetName, localId, input) {
|
|
||||||
return request
|
|
||||||
.put(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/codes/' + localId)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
destroyCode: function (userName, planetName, localId) {
|
|
||||||
return request
|
|
||||||
.del(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/codes/' + localId)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createNote: function (userName, planetName, input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/notes')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
updateNote: function (userName, planetName, localId, input) {
|
|
||||||
return request
|
|
||||||
.put(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/notes/' + localId)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
},
|
|
||||||
destroyNote: function (userName, planetName, localId) {
|
|
||||||
return request
|
|
||||||
.del(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/notes/' + localId)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// Search
|
|
||||||
searchTag: function (tagName) {
|
|
||||||
return request
|
|
||||||
.get(apiUrl + 'search/tags')
|
|
||||||
.query({name: tagName})
|
|
||||||
},
|
|
||||||
searchUser: function (userName) {
|
|
||||||
return request
|
|
||||||
.get(apiUrl + 'search/users')
|
|
||||||
.query({name: userName})
|
|
||||||
},
|
|
||||||
|
|
||||||
// Mail
|
|
||||||
sendEmail: function (input) {
|
|
||||||
return request
|
|
||||||
.post(apiUrl + 'mail')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user