Compare commits
961 Commits
v12.0.3-pr
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
388a28c00e | ||
|
c0e252d53b | ||
|
ea580c6569 | ||
|
281b469b78 | ||
|
f46da268ff | ||
|
5fc9b4877c | ||
|
2006ff57d6 | ||
|
371dd4a057 | ||
|
09ea1ce72d | ||
|
11734fe181 | ||
|
52f71ffae1 | ||
|
66d923ccca | ||
|
3488ea86b0 | ||
|
29fdd70f99 | ||
|
6549f48998 | ||
|
04aec222ce | ||
|
dda7d350be | ||
|
726e34d47c | ||
|
065ecb0b1f | ||
|
0b00f08e3f | ||
|
b43ad4132b | ||
|
2ec7bc4da9 | ||
|
75cde31370 | ||
|
bffdcdfa7e | ||
|
f2952cb6fb | ||
|
f10c51c461 | ||
|
c6afca8eba | ||
|
8e2be63d3f | ||
|
444e690255 | ||
|
0cd14ab806 | ||
|
1a4d7038a9 | ||
|
bfc2812a4f | ||
|
1ba5c44839 | ||
|
45c9411973 | ||
|
36af8862b1 | ||
|
145f2d17db | ||
|
e68a442a3e | ||
|
6874c2a40c | ||
|
52e020e227 | ||
|
d7e4866fc0 | ||
|
7ff4b52005 | ||
|
1cf26c3d2c | ||
|
84e763cddf | ||
|
63f51d6d2a | ||
|
8cc5815107 | ||
|
0b8aa00f4d | ||
|
4c3cc1ae19 | ||
|
920b7fe430 | ||
|
e11dd9e34c | ||
|
f24516e88a | ||
|
38c6ea36ae | ||
|
e20902279e | ||
|
62f31eee77 | ||
|
cb05c2390e | ||
|
de09b014a0 | ||
|
d733bbccc8 | ||
|
8cb5ecf83c | ||
|
b3602c6fa5 | ||
|
d7c3b459d2 | ||
|
d988c3f0cb | ||
|
a524757a92 | ||
|
a3df5ddd0e | ||
|
b398c493c2 | ||
|
759189ae78 | ||
|
ca4aa8b0cb | ||
|
344cd865b2 | ||
|
e157f8675e | ||
|
ef8eb9137f | ||
|
c23a153818 | ||
|
31e0b24e07 | ||
|
da86a8e1b0 | ||
|
1ec93e6f5a | ||
|
6e6f49beed | ||
|
1f77db68e5 | ||
|
9249e4da17 | ||
|
7a7079ed48 | ||
|
39f12f1279 | ||
|
59c719e64f | ||
|
17a4f2c637 | ||
|
19d555880b | ||
|
8d837c8195 | ||
|
8a18ada09a | ||
|
8ebf4e7225 | ||
|
034f1e2a04 | ||
|
c9984a8319 | ||
|
05f5d13c20 | ||
|
6b51e54596 | ||
|
1134469495 | ||
|
466e8f3a40 | ||
|
885b60f566 | ||
|
272010d55f | ||
|
7fb3b29e3a | ||
|
8e36f412da | ||
|
efb1ac8286 | ||
|
c3fab488f6 | ||
|
b599bed239 | ||
|
72f7e68bdc | ||
|
3ec8c646b1 | ||
|
951698555d | ||
|
618f75ab13 | ||
|
2e5b2797b3 | ||
|
2be71dd00b | ||
|
1be01e5186 | ||
|
e67fc4a3d9 | ||
|
cb69c6662e | ||
|
53bedae485 | ||
|
a865aecb6f | ||
|
ef4fbafa04 | ||
|
a1686622b3 | ||
|
cf262f09db | ||
|
ee61bbd7c5 | ||
|
b28e97970f | ||
|
baddc4bad7 | ||
|
641eb1fca5 | ||
|
dfa86ebc45 | ||
|
ee09f7b0f8 | ||
|
538bf10d18 | ||
|
04f948f7e4 | ||
|
b58e48db0c | ||
|
b27bbf6cce | ||
|
09581a961a | ||
|
247a5b06ae | ||
|
6d32b4409b | ||
|
b27198bf02 | ||
|
acd1a4a957 | ||
|
56d4ebc785 | ||
|
894679436c | ||
|
073af84027 | ||
|
cc99ce2e90 | ||
|
e332873d8e | ||
|
75460d1d7c | ||
|
792135a224 | ||
|
f6ede9c8a7 | ||
|
6614fd20a6 | ||
|
efe9ecce29 | ||
|
d58584e0d7 | ||
|
13098287d3 | ||
|
6d9d952079 | ||
|
dd3f25e216 | ||
|
c39c7c1f34 | ||
|
700e933863 | ||
|
71da2e685e | ||
|
4ef9496557 | ||
|
2ed3b81934 | ||
|
8eb1c65ae3 | ||
|
da16173f35 | ||
|
a4835a9d7b | ||
|
beef890a47 | ||
|
7723ef6fb7 | ||
|
5eb303c76b | ||
|
c8026ddb6c | ||
|
872ae595b8 | ||
|
796600eef6 | ||
|
281a81cbd2 | ||
|
ebe1c54153 | ||
|
29c8c12fa8 | ||
|
f7aaa0bff4 | ||
|
f06981267d | ||
|
30036bf83f | ||
|
005ddb4dca | ||
|
0abb343b4b | ||
|
0666b7a303 | ||
|
3ec456fff5 | ||
|
055bd575b9 | ||
|
28fb4f4887 | ||
|
8d4377f85b | ||
|
18c72b9391 | ||
|
e872b4eff8 | ||
|
27c5d0294f | ||
|
5b63ef83f0 | ||
|
db0e17b645 | ||
|
2152dca689 | ||
|
7b60cc38aa | ||
|
213927e1a7 | ||
|
ff7fd0f0d5 | ||
|
ef9be3fed1 | ||
|
7d46c7aaa2 | ||
|
5e456c569a | ||
|
75034bd9f1 | ||
|
8151f7cf1c | ||
|
3c9394ced4 | ||
|
61cfae3d08 | ||
|
1841d799a2 | ||
|
b93e47b606 | ||
|
41a12fa90e | ||
|
d27ea6be9f | ||
|
e9c61f2bec | ||
|
baa144cf80 | ||
|
cacef99651 | ||
|
03e326d56f | ||
|
67e78e76e3 | ||
|
a998903f52 | ||
|
39aac08f3d | ||
|
0aa2d11f36 | ||
|
592354b3cf | ||
|
db5a8b0695 | ||
|
34992f7ee7 | ||
|
7365db5295 | ||
|
ec9e513699 | ||
|
fa79c2b3a3 | ||
|
61426b04d9 | ||
|
9a8b731780 | ||
|
f6f39685a7 | ||
|
a25bb24b05 | ||
|
abf09b4593 | ||
|
618e25d1ab | ||
|
88de887f82 | ||
|
4cc3250c4a | ||
|
742d7eb311 | ||
|
34b63a771d | ||
|
0dda9864d0 | ||
|
0b698bf13c | ||
|
63c3b36b80 | ||
|
5c9edac317 | ||
|
020a04d6ba | ||
|
20866e43cc | ||
|
9263a445c5 | ||
|
acfc971ddd | ||
|
2c57086489 | ||
|
61ae1a34fb | ||
|
cba13592d0 | ||
|
ae9e109c46 | ||
|
339a149c0c | ||
|
a53d4978d1 | ||
|
de14104af6 | ||
|
e60eb83c93 | ||
|
f44e6eea7d | ||
|
19a5de3328 | ||
|
3f0228e829 | ||
|
9e2d9c49bb | ||
|
ca5225f6cc | ||
|
f1050952dd | ||
|
f46070e76d | ||
|
94553e0ef1 | ||
|
83be683b99 | ||
|
de2032e59c | ||
|
0c29734c5f | ||
|
d3c0a1c6ec | ||
|
bab0d71302 | ||
|
cb074ea304 | ||
|
b34596accf | ||
|
47852c1b4a | ||
|
c12696b7c5 | ||
|
796d2c3144 | ||
|
d4af41cbce | ||
|
40bd6b004b | ||
|
68fa0a311e | ||
|
5f412d7046 | ||
|
db178150ae | ||
|
d8156bbd25 | ||
|
bddbf3663b | ||
|
8dc9d0cdd1 | ||
|
8f99a7a651 | ||
|
571aad3f53 | ||
|
e33d14cbf7 | ||
|
bd92e7e5ef | ||
|
cc3cb2f7a8 | ||
|
a2ee1fede2 | ||
|
d58878ec39 | ||
|
890183fe6c | ||
|
b9be45d151 | ||
|
75401f88f6 | ||
|
752622f35e | ||
|
43cd67f8b1 | ||
|
9bd4f15d94 | ||
|
a205d62cd7 | ||
|
578ede3e87 | ||
|
c10575b730 | ||
|
79fae81959 | ||
|
0e492fad2a | ||
|
634646bb36 | ||
|
d180e6e662 | ||
|
63830e1edc | ||
|
b78e6767e9 | ||
|
33aa6d3c6b | ||
|
a58f877b94 | ||
|
f664bed3a1 | ||
|
8ba1eedb47 | ||
|
5fe0275432 | ||
|
0e7cc9f3c5 | ||
|
e4b9430ce1 | ||
|
770cc7e049 | ||
|
23e04a7431 | ||
|
60d48c1767 | ||
|
102273b5d5 | ||
|
0e2627cde3 | ||
|
eabb0faed5 | ||
|
ed018320d5 | ||
|
7f77c99ba0 | ||
|
5aefc8373c | ||
|
699da59aff | ||
|
4fb7552622 | ||
|
55719ccbdf | ||
|
00707d1d05 | ||
|
0cafb5b3f8 | ||
|
7b293d0771 | ||
|
5b1e3af9cb | ||
|
b857a42c9a | ||
|
9f36050ed9 | ||
|
587f709568 | ||
|
2a78a8e8c3 | ||
|
56901e2ac1 | ||
|
98e01baa8e | ||
|
ebeafd74ab | ||
|
884b4b1ed2 | ||
|
b2f2dc4d0b | ||
|
65ee3f8a8c | ||
|
0414a3f4bc | ||
|
3697c777f1 | ||
|
2912a422bf | ||
|
a83adb7929 | ||
|
df4e04d427 | ||
|
a8727ae408 | ||
|
e8de9ba848 | ||
|
a8785ec879 | ||
|
4a4d28222b | ||
|
e551580e67 | ||
|
5014855e79 | ||
|
9c32fe6833 | ||
|
801a4a86cf | ||
|
1230dda8bd | ||
|
cc0d3a281c | ||
|
8d779db4a5 | ||
|
fe331d95d2 | ||
|
6cb0522a4c | ||
|
a5dbce0f8b | ||
|
870b7a6434 | ||
|
8ae5eedf03 | ||
|
c9b82c70cd | ||
|
74c2b61b39 | ||
|
98ca860a16 | ||
|
c98fd74f66 | ||
|
f942992b81 | ||
|
dc60da99c5 | ||
|
27dae8ce74 | ||
|
c59e7aeb2d | ||
|
0fadf04686 | ||
|
b9acd92b8a | ||
|
bbe090bdb6 | ||
|
afd5d42abc | ||
|
890880a5c8 | ||
|
67083238c2 | ||
|
21f96dd369 | ||
|
36dd38ffa0 | ||
|
7cda15e19c | ||
|
43c28a8466 | ||
|
e7f43e16f2 | ||
|
3899577631 | ||
|
2fb65580e4 | ||
|
bbd4f15cca | ||
|
32a1639aca | ||
|
b499dd87cc | ||
|
c9e2dea463 | ||
|
a7fb61a4d4 | ||
|
ea3a7798b9 | ||
|
314512d353 | ||
|
295f694baa | ||
|
d4b2e75b7e | ||
|
33c4c76608 | ||
|
a3a609d89d | ||
|
e44962be9a | ||
|
47f016e6e9 | ||
|
88bb921c91 | ||
|
0035fd0f04 | ||
|
9bcbab1761 | ||
|
96fd59b54e | ||
|
453deac354 | ||
|
966fc65a9d | ||
|
30d0a34afa | ||
|
49896f53ef | ||
|
1d8df18ca0 | ||
|
d1ac836de6 | ||
|
617a8fab72 | ||
|
ec68ff1bcd | ||
|
669c423b07 | ||
|
4385020918 | ||
|
67dd7e5eed | ||
|
a8a2fd6382 | ||
|
d66695954d | ||
|
13912eea7e | ||
|
b461005c18 | ||
|
0a11ce8282 | ||
|
b4ed45225b | ||
|
2f4b1e76e5 | ||
|
cd5f4511ab | ||
|
78cb08d5a1 | ||
|
5bca4562fb | ||
|
7d421b9afd | ||
|
89c37f7656 | ||
|
ebc97ad6ac | ||
|
35f30ab1cd | ||
|
bdc9aa24ec | ||
|
9807cbd752 | ||
|
b5a52668e9 | ||
|
3f17045e47 | ||
|
4f87a9e477 | ||
|
1fb671ad31 | ||
|
3bde4555d0 | ||
|
7d1e9e11ea | ||
|
ef77d90280 | ||
|
f4558a0f46 | ||
|
b1c44da366 | ||
|
631320c889 | ||
|
0f77313761 | ||
|
27dcce694b | ||
|
f6f606cb8f | ||
|
f00b81f8d8 | ||
|
175803baba | ||
|
a39765072c | ||
|
f4be8814c5 | ||
|
e1eb01b439 | ||
|
d91cdad79e | ||
|
83d615d3ed | ||
|
30ecd7a385 | ||
|
2b297f4f77 | ||
|
15e9d678f0 | ||
|
6d8a61627e | ||
|
90a4cf40ea | ||
|
7a94d66511 | ||
|
27abc38e7e | ||
|
cebadb9769 | ||
|
481365fe64 | ||
|
5a76c6b2ae | ||
|
71966dd729 | ||
|
5ca5acc277 | ||
|
659ada62d3 | ||
|
d9019be22f | ||
|
9692c5e398 | ||
|
bc2c7c80b7 | ||
|
e09ce188aa | ||
|
367fa05a21 | ||
|
c957dd238f | ||
|
680006a883 | ||
|
e242c77202 | ||
|
02c2b98299 | ||
|
d8c6c0c684 | ||
|
2b74a02a67 | ||
|
b386240f28 | ||
|
9dce302e1e | ||
|
2e0fdf1049 | ||
|
cdd059fdb9 | ||
|
1cb4e37207 | ||
|
ef8cd06f72 | ||
|
79413cb4fd | ||
|
9c992dbfc2 | ||
|
06410d217d | ||
|
91b9a7840f | ||
|
21162095fb | ||
|
df2d8def00 | ||
|
23c400a437 | ||
|
e85866b7a9 | ||
|
236ecf987a | ||
|
95a1df9d37 | ||
|
e8bfdd6057 | ||
|
e2e2e98c43 | ||
|
ccab6f1552 | ||
|
508e77d125 | ||
|
1ac807e6f7 | ||
|
82b4fb1ea3 | ||
|
ad5ddebaa6 | ||
|
49dd313899 | ||
|
3ff13909cd | ||
|
90030a8d11 | ||
|
8504134ad6 | ||
|
24147256b4 | ||
|
ef7ac1dd8b | ||
|
93ecea748d | ||
|
9b43c32866 | ||
|
f9e0c467ef | ||
|
4c15cafb72 | ||
|
89382b4efb | ||
|
25bf640426 | ||
|
c81d79b8bc | ||
|
cb3af6e66a | ||
|
65ef9f704b | ||
|
3998d3be44 | ||
|
2b08b68f7a | ||
|
3472206e83 | ||
|
dad238c9a5 | ||
|
92f27a1601 | ||
|
3d7b8b3a6b | ||
|
d65e86731e | ||
|
5b5423c63f | ||
|
db1ea4f07d | ||
|
10770501a4 | ||
|
ae700f7fa9 | ||
|
5abb623306 | ||
|
a87e662f55 | ||
|
2033dbc19c | ||
|
6cbfe0efd4 | ||
|
7e70826a3b | ||
|
b8a1e0db0b | ||
|
6eb86005f7 | ||
|
a96990ebbc | ||
|
fef0d63712 | ||
|
a2ea9c5383 | ||
|
94914009c1 | ||
|
01791be348 | ||
|
94e19c58c0 | ||
|
f8112c0306 | ||
|
bddd73411e | ||
|
0e11cdf7b9 | ||
|
b7d9f781fd | ||
|
076d147d4e | ||
|
0b0e471c90 | ||
|
cccd3a494a | ||
|
7626ea3d89 | ||
|
f590e7741d | ||
|
689b5cba83 | ||
|
ec507d07eb | ||
|
2467734990 | ||
|
9cf1e9564a | ||
|
a4738295db | ||
|
c48aa43ead | ||
|
efee8f88ec | ||
|
12e2151c11 | ||
|
b07f33e07c | ||
|
cfe507f178 | ||
|
55c1d366cb | ||
|
b51c26c4fa | ||
|
a49b940ae8 | ||
|
16b361f932 | ||
|
b606f65f4e | ||
|
9900ffc43a | ||
|
798384a638 | ||
|
458d783195 | ||
|
2b6e0522b1 | ||
|
cc597b331c | ||
|
3a71b7d80b | ||
|
22c08df773 | ||
|
fae2040680 | ||
|
9d2047a4be | ||
|
32406114d9 | ||
|
a13f101e40 | ||
|
41c09b95c6 | ||
|
0076228ef5 | ||
|
4d8601a5f5 | ||
|
137a46eabd | ||
|
1c8955723d | ||
|
9760eca25c | ||
|
41efd64872 | ||
|
859074c5d1 | ||
|
07cec873e6 | ||
|
7d4a9f4575 | ||
|
cf4856d597 | ||
|
fdbf48fed3 | ||
|
724c96bdf8 | ||
|
5a5005f486 | ||
|
178ff372b7 | ||
|
3250ca151c | ||
|
fdf720ffd5 | ||
|
3a743f2d70 | ||
|
9d2367b35b | ||
|
ca045d269e | ||
|
88232cf064 | ||
|
e48a9cad97 | ||
|
76dc9a71a8 | ||
|
492546dc65 | ||
|
4c25402e9e | ||
|
7fc2f193e6 | ||
|
5574252dca | ||
|
019cf75a67 | ||
|
3a4ce4e59f | ||
|
61215e4376 | ||
|
1400730944 | ||
|
fb850e2a16 | ||
|
a6950355ea | ||
|
256b1ec70d | ||
|
bb48f9dd57 | ||
|
88c99173f1 | ||
|
2bc89a67d9 | ||
|
20c81de6cf | ||
|
dd3003a693 | ||
|
de75571ae6 | ||
|
b0f56adc61 | ||
|
c21bf10031 | ||
|
55afd3689c | ||
|
e22eb47f2d | ||
|
a27e9a49c0 | ||
|
c45889b854 | ||
|
404f0b8a82 | ||
|
02055139b5 | ||
|
b395771d4c | ||
|
bab5a0f770 | ||
|
d06d54946a | ||
|
595487eeb0 | ||
|
13efff8eb0 | ||
|
733bb314ba | ||
|
125ce2430c | ||
|
ed7f501d4d | ||
|
e63fd15785 | ||
|
7b7e4cf61a | ||
|
d6e0bd29d2 | ||
|
f7f69aa910 | ||
|
6133db43a0 | ||
|
d9c50d2568 | ||
|
14e56ee8b9 | ||
|
d1e0347194 | ||
|
5a8c9149b9 | ||
|
e8f261e0a9 | ||
|
cd9780dac1 | ||
|
ff44d97405 | ||
|
606e41a5ce | ||
|
39328ea23f | ||
|
89b5100102 | ||
|
86d5d564da | ||
|
4d703eb910 | ||
|
a372b7fc84 | ||
|
57fc2c6a1b | ||
|
aacd5da1b4 | ||
|
7e7b0dbd8f | ||
|
206ecec2aa | ||
|
4b33271e5b | ||
|
50b6acbcfb | ||
|
9bedcc120a | ||
|
b69821357c | ||
|
b4e56ce006 | ||
|
5bd42ddb67 | ||
|
c260d7f443 | ||
|
2cc16bdf11 | ||
|
0c0f07014b | ||
|
100ec123d5 | ||
|
a76e2064b6 | ||
|
dc99e8514c | ||
|
b848703ada | ||
|
b4cda98556 | ||
|
14790ecd36 | ||
|
b1d74560c0 | ||
|
4b4bfb83da | ||
|
89924320cb | ||
|
f6a40c778c | ||
|
9f52046a66 | ||
|
a6f967d597 | ||
|
8b579d5bac | ||
|
2733211cc9 | ||
|
f0ee34399b | ||
|
015ec7a2d8 | ||
|
4088771757 | ||
|
1a42eafbe6 | ||
|
e6ef8d76e4 | ||
|
28bb333364 | ||
|
dd177f450c | ||
|
27a881a020 | ||
|
155d303a12 | ||
|
0504d5b03a | ||
|
db287ccf9d | ||
|
3795c054ea | ||
|
d5acdb8653 | ||
|
b7219ac7f4 | ||
|
af8dd37f97 | ||
|
d263e1018f | ||
|
e34b0768ba | ||
|
f3a68445e0 | ||
|
bcfcfe2578 | ||
|
037c3f3a3d | ||
|
1b89e8e2f1 | ||
|
9c1dd6d8a5 | ||
|
59563965f4 | ||
|
5164284763 | ||
|
6344fffe9e | ||
|
01509b076e | ||
|
e6008aacf6 | ||
|
bcde649294 | ||
|
888c7c28a8 | ||
|
7bd6ad587d | ||
|
50887d21dc | ||
|
324d981420 | ||
|
5d5f2c31f0 | ||
|
ac862f1c0d | ||
|
2652a0c389 | ||
|
8fabf1eee4 | ||
|
2ec770a90e | ||
|
ca42f80d57 | ||
|
b83e272f94 | ||
|
1058cf2151 | ||
|
76695e5c90 | ||
|
9d63b3cbfc | ||
|
58e9d1423f | ||
|
df825cdc6b | ||
|
ecb9cb2b98 | ||
|
96219ccfaa | ||
|
c5bafecad3 | ||
|
10ae44f884 | ||
|
24d9ce03f6 | ||
|
0b60e30dbe | ||
|
bef98588f7 | ||
|
d85dcea40e | ||
|
bd6a35c374 | ||
|
c807d09819 | ||
|
69541fae7e | ||
|
e8e55cb687 | ||
|
1bf3350c8c | ||
|
7b87b755a0 | ||
|
066cf942d3 | ||
|
a59280997a | ||
|
90befdf0a6 | ||
|
ce075f3c4e | ||
|
9ca75133c4 | ||
|
aed76073d6 | ||
|
4fbdbeb954 | ||
|
b2153af9d7 | ||
|
b7b42b5854 | ||
|
356fd64d75 | ||
|
57f56a18f1 | ||
|
dc09360634 | ||
|
4f8047f498 | ||
|
9b8b7c9324 | ||
|
b139c235f7 | ||
|
336e1fa193 | ||
|
9c06206584 | ||
|
08abceb14b | ||
|
bbf567562e | ||
|
aac29a7d71 | ||
|
46096dca8a | ||
|
8dd627bf1f | ||
|
5cf3029030 | ||
|
2af78338bf | ||
|
544c4edbf2 | ||
|
6770711901 | ||
|
350d06068e | ||
|
4667c33434 | ||
|
db08b8bdc9 | ||
|
066c43dec9 | ||
|
46f9dfb1bd | ||
|
1d7521f8ed | ||
|
3f82f6e72f | ||
|
6247a27ef6 | ||
|
49bd39f7df | ||
|
9fb6870679 | ||
|
a50b5d8f5a | ||
|
274229679a | ||
|
4af42afc89 | ||
|
d3f8d512a8 | ||
|
895f3ad670 | ||
|
df69034928 | ||
|
aa921af380 | ||
|
a4002c23c1 | ||
|
8ef2c35331 | ||
|
bdc09c0e4e | ||
|
cb73865b21 | ||
|
bde5ec063e | ||
|
7eaa2b7fef | ||
|
3a783dac7c | ||
|
5512820ecb | ||
|
b27a0170c7 | ||
|
6d17ce3093 | ||
|
b3aaba6ea2 | ||
|
bd013174a9 | ||
|
e90e9d1658 | ||
|
3061e1f88d | ||
|
118e041b55 | ||
|
fc139a83fd | ||
|
68f791bb32 | ||
|
08d25a9e1c | ||
|
dfd6550c1a | ||
|
dd49dc1e41 | ||
|
a9753d876c | ||
|
34c415ac1b | ||
|
a0c928fa88 | ||
|
4b4e03f19f | ||
|
52209a3d71 | ||
|
3ef2be6804 | ||
|
e645b29d87 | ||
|
d3d2335fe2 | ||
|
616fafc0c4 | ||
|
759bb6a05f | ||
|
ae0d94e618 | ||
|
c9c60b7a62 | ||
|
0dcc30ec2f | ||
|
8708ab1c61 | ||
|
d19d22c69d | ||
|
34cf5beb0d | ||
|
757a334112 | ||
|
302e55fc13 | ||
|
034d756f06 | ||
|
156de7fd6d | ||
|
f6fb7ce715 | ||
|
13c9525df1 | ||
|
9219b49d8c | ||
|
919cd2eff7 | ||
|
cbc84274b1 | ||
|
f07dfc65cb | ||
|
de925b1e21 | ||
|
1366bc30f9 | ||
|
2470f6e5a0 | ||
|
36e8f641c2 | ||
|
3b9f22e79d | ||
|
aa1272ac54 | ||
|
0a92044b14 | ||
|
bc8d0d5c48 | ||
|
3a11aceb6b | ||
|
977aa7971e | ||
|
d7f11265ad | ||
|
6ae8f5d3c3 | ||
|
e87e1a0870 | ||
|
981885670d | ||
|
6599ab8a8d | ||
|
4c231ea2c3 | ||
|
7d3646396e | ||
|
c8390efcc3 | ||
|
fc05e4827d | ||
|
d9c9779327 | ||
|
00e8d7b65a | ||
|
ac124d20ba | ||
|
3f6c4f3d26 | ||
|
3918b093fa | ||
|
58ef1f285b | ||
|
7fa74ebda3 | ||
|
ff68c1df64 | ||
|
5e6d6e860c | ||
|
87a9cd0731 | ||
|
f9fa216531 | ||
|
915f2f0053 | ||
|
4bdf546f88 | ||
|
300a0a4487 | ||
|
c33de6b16b | ||
|
81ad75e4ef | ||
|
1f5ea9fcf2 | ||
|
7e2bbdac8e | ||
|
f4d15fb574 | ||
|
b484b45f1a | ||
|
21c9be56a8 | ||
|
cfdc87c54e | ||
|
9283c2cb1d | ||
|
25351e48f5 | ||
|
37cfac8813 | ||
|
19ea6d7dd7 | ||
|
1c6fbc5c93 | ||
|
becf34fe9c | ||
|
f344d26841 | ||
|
90764f9520 | ||
|
3a4db4bf70 | ||
|
2addf8d01f | ||
|
a771e1ddb1 | ||
|
c9f9fe070b | ||
|
89213daeed | ||
|
1d117bf647 | ||
|
44d0c2c3db | ||
|
c3af1fd214 | ||
|
b87c529b2e | ||
|
10d7082d90 | ||
|
c60dcf817a | ||
|
453a17e51d | ||
|
39f06ce855 | ||
|
5f34ff95ea | ||
|
faf213856e | ||
|
54a18ed4ea | ||
|
9a581fde52 | ||
|
fd5d998044 | ||
|
49cd661163 | ||
|
6a514d87eb | ||
|
b831b25263 | ||
|
d63b68e8ab | ||
|
51d5706aa6 | ||
|
429a2f39d3 | ||
|
8ba060bcdd | ||
|
9048035920 | ||
|
61fd67b8bb | ||
|
2223561985 | ||
|
7d206bc691 | ||
|
af0ad3e81c | ||
|
19257fb177 | ||
|
b91d2404b7 | ||
|
0e546a91dd | ||
|
acf730283a | ||
|
d6256c7bed | ||
|
7b7fb111fd | ||
|
1a74f9dabf | ||
|
ab8d16e2f8 | ||
|
72816df9cd | ||
|
07e75b3900 | ||
|
0ba3a66ed5 | ||
|
7ad7f1d9c6 | ||
|
dce7f1ac98 | ||
|
8edd4c8505 | ||
|
ed62007228 | ||
|
6f8389e70c | ||
|
ffc75b77c3 | ||
|
b083ecad29 | ||
|
4aa91aebc6 | ||
|
90b668258d | ||
|
6543dd3bb8 | ||
|
0d8e198ab1 | ||
|
ece36d36f5 | ||
|
03fd3b8f55 | ||
|
d407c9fd63 | ||
|
4093c27ed7 | ||
|
224a6c4373 | ||
|
c16e560ff7 | ||
|
d28285babc | ||
|
78b829834b | ||
|
4da1b47664 | ||
|
bacc9e2aa5 | ||
|
e7a4e29239 | ||
|
4db4711421 | ||
|
ababb20566 | ||
|
1a6c7e7e89 | ||
|
aea996a5c1 | ||
|
b0a26cdf92 | ||
|
af56e31860 | ||
|
3643966ee8 | ||
|
f0292e5bd3 | ||
|
9db4c52c93 | ||
|
187b5df97e | ||
|
3a90f736dd | ||
|
e022266792 | ||
|
943d05a545 | ||
|
8f1c97ff6d | ||
|
e8a3812812 | ||
|
25860d1ab8 | ||
|
0f6911bf50 | ||
|
1dc572d86e | ||
|
24e25248c0 | ||
|
c11c990e1d | ||
|
2ff5e91f98 | ||
|
0317bb2f54 | ||
|
168e0569fa | ||
|
a770fad73d | ||
|
6199763e6f | ||
|
be62042e0e | ||
|
8535af4b7e | ||
|
4e5c0938fa | ||
|
ddf3f91bbc | ||
|
9c178886ef | ||
|
a207f33e60 | ||
|
5d31590e9b | ||
|
f07f357a86 | ||
|
2edba35cf2 | ||
|
2494d33073 | ||
|
d73b7d836c | ||
|
22cdda0e35 | ||
|
b09c520dff | ||
|
5151c6e82e | ||
|
7f507f44ab | ||
|
a452a5990d | ||
|
84928d6701 | ||
|
717f5d21f1 | ||
|
5f6de21b19 | ||
|
a004e65b10 | ||
|
d06667462e | ||
|
1429051186 | ||
|
a426c96bd9 | ||
|
98769ca5bd | ||
|
e999be4320 | ||
|
2531c48f2d | ||
|
3b384be610 | ||
|
29d2d18a0b | ||
|
559d8c52ca | ||
|
9c3e828bf6 | ||
|
f40ea0540c | ||
|
d4fc55b76d | ||
|
edaf88f6cc | ||
|
95d99e10fd | ||
|
3fffdf9eac | ||
|
4007c0c10d | ||
|
5a7e25740f | ||
|
3045a7bcd7 | ||
|
a629d1c07c | ||
|
160dc2b447 | ||
|
0b30d895f3 | ||
|
b3da26a2ad |
5
.github/workflows/dist.yml
vendored
@ -4,12 +4,15 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- "12.0"
|
||||
- "13.0"
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
22
.github/workflows/test.yml
vendored
@ -6,13 +6,23 @@ on:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
REDIS_HOST: localhost
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 3
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.8"
|
||||
|
||||
@ -25,6 +35,7 @@ jobs:
|
||||
sudo apt-get install -y libfuse-dev cmake re2c flex sqlite3
|
||||
sudo apt-get install -y libssl-dev libsasl2-dev libldap2-dev libonig-dev
|
||||
sudo apt-get install -y libxml2 libxml2-dev libjwt-dev
|
||||
sudo apt-get install -y libhiredis-dev
|
||||
|
||||
- name: clone and build
|
||||
run: |
|
||||
@ -39,9 +50,14 @@ jobs:
|
||||
pip install -r test-requirements.txt
|
||||
sudo rm -rf /usr/lib/python3/dist-packages/pytz/
|
||||
|
||||
- name: Set REDIS_HOST environment variable
|
||||
run: |
|
||||
echo "REDIS_HOST=localhost" >> $GITHUB_ENV
|
||||
|
||||
- name: run pytest
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
rm -r tests/seahub/repo_metadata
|
||||
export CCNET_CONF_DIR=/tmp/ccnet SEAFILE_CONF_DIR=/tmp/seafile-data TRAVIS=1 SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=seahub
|
||||
if ./tests/test_seahub_changes.sh; then ./tests/seahubtests.sh init && ./tests/seahubtests.sh runserver && ./tests/seahubtests.sh test; else true; fi
|
||||
|
||||
@ -49,7 +65,7 @@ jobs:
|
||||
with:
|
||||
node-version: "20.x"
|
||||
|
||||
- name: run npm lint
|
||||
- name: run npm lint and npm test
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/tests/
|
||||
if chmod +x test_frontend_changes.sh && ./test_frontend_changes.sh; then chmod +x github_actions_npm_lint.sh && ./github_actions_npm_lint.sh; else true; fi
|
||||
|
@ -28,8 +28,8 @@ minimum_perc = 0
|
||||
resource_name = sdoc-editor
|
||||
|
||||
[o:haiwen:p:seahub:r:seafile-editor]
|
||||
file_filter = media/locales/<lang>/seafile-editor.json
|
||||
source_file = media/locales/en/seafile-editor.json
|
||||
file_filter = media/seafile-editor/locales/<lang>/seafile-editor.json
|
||||
source_file = media/seafile-editor/locales/en/seafile-editor.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
|
@ -52,5 +52,16 @@ Then open your browser, and input `http://localhost:8000/`, there should be a Lo
|
||||
Internationalization (I18n)
|
||||
==========
|
||||
|
||||
Please refer to https://github.com/haiwen/seafile/wiki/Seahub-Translation
|
||||
Please submit translations via Transifex:
|
||||
|
||||
Steps:
|
||||
|
||||
1. Visit the webpage of Transifex ([https://explore.transifex.com/haiwen/seahub/](https://explore.transifex.com/haiwen/seahub/)).
|
||||
|
||||
2. Click the "Join this project" button in the bottom right corner.
|
||||
|
||||
3. Use an email or GitHub account(recommended) to create an account.
|
||||
|
||||
4. Select a language and click 'Join project' to join the language translation.
|
||||
|
||||
5. After accepted by the project maintainer, then you can upload your file or translate online.
|
||||
|
@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@ -22,8 +22,10 @@ const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
|
||||
// );
|
||||
|
||||
// reset by custom
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
const PORT = process.env.PORT || '3000';
|
||||
const CONFIG_HOST = process.env.HOST;
|
||||
const isRunInDocker = CONFIG_HOST === '0.0.0.0';
|
||||
const HOST = isRunInDocker ? '127.0.0.1' : CONFIG_HOST;
|
||||
const PORT = process.env.PORT || '3001';
|
||||
const publicPath = process.env.PUBLIC_PATH || '/assets/bundles/';
|
||||
const publicUrlOrPath = `http://${HOST}:${PORT}${publicPath}`;
|
||||
|
||||
@ -79,5 +81,4 @@ module.exports = {
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports.moduleFileExtensions = moduleFileExtensions;
|
||||
|
@ -23,7 +23,6 @@ const modules = require('./modules');
|
||||
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
|
||||
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
|
||||
const webpackBundleTracker = require('webpack-bundle-tracker');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
const ForkTsCheckerWebpackPlugin =
|
||||
process.env.TSC_COMPILE_ON_ERROR === 'true'
|
||||
@ -106,6 +105,7 @@ const excludedChunkNames = [
|
||||
'sharedFileViewMarkdown',
|
||||
'markdownEditor',
|
||||
'plainMarkdownEditor',
|
||||
'tldrawEditor',
|
||||
];
|
||||
|
||||
// This is the production and development configuration.
|
||||
@ -357,6 +357,7 @@ module.exports = function (webpackEnv) {
|
||||
.map(ext => `.${ext}`)
|
||||
.filter(ext => useTypeScript || !ext.includes('ts')),
|
||||
alias: {
|
||||
'@': path.resolve(process.cwd(), 'src'),
|
||||
// Support React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
'react-native': 'react-native-web',
|
||||
@ -437,12 +438,17 @@ module.exports = function (webpackEnv) {
|
||||
ref: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
{ loader: 'svgo-loader',
|
||||
options: {
|
||||
name: 'static/media/[name].[hash].[ext]',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
'removeTitle',
|
||||
'removeStyleElement',
|
||||
'cleanupIDs',
|
||||
'inlineStyles',
|
||||
'removeXMLProcInst',
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
issuer: {
|
||||
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
|
||||
@ -595,10 +601,19 @@ module.exports = function (webpackEnv) {
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'svg-sprite-loader', options: {}
|
||||
loader: require.resolve('@svgr/webpack'),
|
||||
options: {
|
||||
prettier: false,
|
||||
svgo: false,
|
||||
svgoConfig: {
|
||||
plugins: [{ removeViewBox: false }],
|
||||
},
|
||||
titleProp: true,
|
||||
ref: true,
|
||||
},
|
||||
},
|
||||
{ loader: 'svgo-loader', options: {
|
||||
plugins:[
|
||||
plugins: [
|
||||
'removeTitle',
|
||||
'removeStyleElement',
|
||||
'cleanupIDs',
|
||||
@ -626,6 +641,12 @@ module.exports = function (webpackEnv) {
|
||||
// Make sure to add the new loader(s) before the "file" loader.
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false
|
||||
}
|
||||
}
|
||||
].filter(Boolean),
|
||||
},
|
||||
plugins: [
|
||||
@ -828,7 +849,6 @@ module.exports = function (webpackEnv) {
|
||||
filename: isEnvProduction ? './webpack-stats.pro.json' : './webpack-stats.dev.json',
|
||||
publicPath: isEnvProduction ? '' : paths.publicUrlOrPath
|
||||
}),
|
||||
// new BundleAnalyzerPlugin(),
|
||||
].filter(Boolean),
|
||||
// Turn off performance processing because we utilize
|
||||
// our own hints via the FileSizeReporter
|
||||
|
@ -1,6 +1,8 @@
|
||||
const paths = require('./paths');
|
||||
|
||||
const entryFiles = {
|
||||
tldrawEditor: '/tldrawEditor.js',
|
||||
excalidrawEditor: '/excalidraw-editor.js',
|
||||
markdownEditor: '/index.js',
|
||||
plainMarkdownEditor: '/pages/plain-markdown-editor/index.js',
|
||||
TCAccept: '/tc-accept.js',
|
||||
@ -23,6 +25,7 @@ const entryFiles = {
|
||||
sharedFileViewAudio: '/shared-file-view-audio.js',
|
||||
sharedFileViewDocument: '/shared-file-view-document.js',
|
||||
sharedFileViewSpreadsheet: '/shared-file-view-spreadsheet.js',
|
||||
sharedFileViewExdraw: '/shared-file-view-exdraw.js',
|
||||
sharedFileViewSdoc: '/shared-file-view-sdoc.js',
|
||||
sharedFileViewUnknown: '/shared-file-view-unknown.js',
|
||||
historyTrashFileView: '/history-trash-file-view.js',
|
||||
@ -39,7 +42,6 @@ const entryFiles = {
|
||||
repoFolderTrash: '/repo-folder-trash.js',
|
||||
orgAdmin: '/pages/org-admin',
|
||||
sysAdmin: '/pages/sys-admin',
|
||||
search: '/pages/search',
|
||||
uploadLink: '/pages/upload-link',
|
||||
subscription: '/subscription.js',
|
||||
institutionAdmin: '/pages/institution-admin/index.js'
|
||||
|
17397
frontend/package-lock.json
generated
@ -8,18 +8,19 @@
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@gatsbyjs/reach-router": "1.3.9",
|
||||
"@seafile/react-image-lightbox": "3.0.1",
|
||||
"@excalidraw/excalidraw": "^0.18.0",
|
||||
"@gatsbyjs/reach-router": "2.0.1",
|
||||
"@seafile/react-image-lightbox": "4.0.2",
|
||||
"@seafile/resumablejs": "1.1.16",
|
||||
"@seafile/sdoc-editor": "1.0.135",
|
||||
"@seafile/sdoc-editor": "2.0.54",
|
||||
"@seafile/seafile-calendar": "0.0.28",
|
||||
"@seafile/seafile-editor": "^1.0.122",
|
||||
"@seafile/sf-metadata-ui-component": "^0.0.53",
|
||||
"@seafile/seafile-editor": "2.0.2",
|
||||
"@seafile/stldraw-editor": "1.0.1",
|
||||
"@uiw/codemirror-extensions-langs": "^4.19.4",
|
||||
"@uiw/codemirror-themes": "^4.23.5",
|
||||
"@uiw/react-codemirror": "^4.19.4",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "3.6.0",
|
||||
"axios": "^1.8.2",
|
||||
"chart.js": "4.4.7",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^6.0.1",
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
@ -32,24 +33,24 @@
|
||||
"i18next-xhr-backend": "^3.1.2",
|
||||
"is-hotkey": "0.2.0",
|
||||
"MD5": "^1.3.0",
|
||||
"mdast-util-gfm-autolink-literal": "2.0.0",
|
||||
"object-assign": "4.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "17.0.2",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-app-polyfill": "^2.0.0",
|
||||
"react-chartjs-2": "4.0.0",
|
||||
"react-chartjs-2": "5.3.0",
|
||||
"react-cookies": "^0.1.0",
|
||||
"react-dnd": "^2.6.0",
|
||||
"react-dnd-html5-backend": "^2.6.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^10.12.2",
|
||||
"react-responsive": "9.0.2",
|
||||
"react-select": "5.7.0",
|
||||
"react-mentions": "4.4.10",
|
||||
"react-responsive": "10.0.0",
|
||||
"react-select": "5.9.0",
|
||||
"react-transition-group": "4.4.5",
|
||||
"reactstrap": "8.9.0",
|
||||
"seafile-js": "0.2.237",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"reactstrap": "9.2.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"svgo-loader": "^3.0.1",
|
||||
"unified": "^7.0.0",
|
||||
"url-parse": "^1.4.3",
|
||||
@ -63,7 +64,7 @@
|
||||
"start": "node scripts/start.js",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "node scripts/test.js --env=jsdom",
|
||||
"dev": "export NODE_ENV=development && node config/server.js"
|
||||
"dev": "export NODE_ENV=development && node --max-old-space-size=4096 config/server.js"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@ -168,7 +169,6 @@
|
||||
"url-loader": "4.1.1",
|
||||
"web-vitals": "2.1.4",
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-bundle-tracker": "1.7.0",
|
||||
"webpack-dev-server": "^4.6.0",
|
||||
"webpack-manifest-plugin": "^4.0.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
import { Router, navigate } from '@gatsbyjs/reach-router';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Router, navigate, LocationProvider, globalHistory } from '@gatsbyjs/reach-router';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { Modal } from 'reactstrap';
|
||||
import { siteRoot, siteTitle, mediaUrl, faviconPath } from './utils/constants';
|
||||
@ -8,6 +8,7 @@ import { Utils, isMobile } from './utils/utils';
|
||||
import SystemNotification from './components/system-notification';
|
||||
import EventBus from './components/common/event-bus';
|
||||
import Header from './components/header';
|
||||
import SystemUserNotification from './components/system-user-notification';
|
||||
import SidePanel from './components/side-panel';
|
||||
import ResizeBar from './components/resize-bar';
|
||||
import {
|
||||
@ -24,7 +25,7 @@ import ShareAdminLibraries from './pages/share-admin/libraries';
|
||||
import ShareAdminFolders from './pages/share-admin/folders';
|
||||
import ShareAdminShareLinks from './pages/share-admin/share-links';
|
||||
import ShareAdminUploadLinks from './pages/share-admin/upload-links';
|
||||
import SharedLibraries from './pages/shared-libs/shared-libraries';
|
||||
import SharedLibraries from './pages/shared-libs';
|
||||
import ShareWithOCM from './pages/share-with-ocm/shared-with-ocm';
|
||||
import OCMViaWebdav from './pages/ocm-via-webdav/ocm-via-webdav';
|
||||
import OCMRepoDir from './pages/share-with-ocm/remote-dir-view';
|
||||
@ -85,7 +86,7 @@ class App extends Component {
|
||||
let splitUrlArray = window.location.hash.split('/');
|
||||
let repoID = splitUrlArray[splitUrlArray.length - 2];
|
||||
let url = siteRoot + 'library/' + repoID + '/';
|
||||
navigate(url, { repalce: true });
|
||||
navigate(url, { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
@ -144,7 +145,7 @@ class App extends Component {
|
||||
if (selectedItem.is_dir === true) {
|
||||
this.setState({ currentTab: '', pathPrefix: [] });
|
||||
let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path;
|
||||
navigate(url, { repalce: true });
|
||||
navigate(url, { replace: true });
|
||||
} else {
|
||||
let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
|
||||
let isWeChat = Utils.isWeChat();
|
||||
@ -157,19 +158,6 @@ class App extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onGroupChanged = (groupID) => {
|
||||
setTimeout(function () {
|
||||
let url;
|
||||
if (groupID) {
|
||||
url = siteRoot + 'group/' + groupID + '/';
|
||||
}
|
||||
else {
|
||||
url = siteRoot + 'libraries/';
|
||||
}
|
||||
window.location = url.toString();
|
||||
}, 1);
|
||||
};
|
||||
|
||||
tabItemClick = (tabName, groupID) => {
|
||||
let pathPrefix = [];
|
||||
if (groupID || this.dirViewPanels.indexOf(tabName) > -1) {
|
||||
@ -291,6 +279,7 @@ class App extends Component {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SystemNotification />
|
||||
<SystemUserNotification />
|
||||
<Header
|
||||
isSidePanelClosed={isSidePanelClosed}
|
||||
onCloseSidePanel={this.onCloseSidePanel}
|
||||
@ -343,7 +332,7 @@ class App extends Component {
|
||||
<InvitationsView path={siteRoot + 'invitations/'} />
|
||||
<FilesActivities path={siteRoot + 'dashboard'} />
|
||||
<MyFileActivities path={siteRoot + 'my-activities'} />
|
||||
<GroupView path={siteRoot + 'group/:groupID'} onGroupChanged={this.onGroupChanged} />
|
||||
<GroupView path={siteRoot + 'group/:groupID'} />
|
||||
<LinkedDevices path={siteRoot + 'linked-devices'} />
|
||||
<ShareAdminLibraries path={siteRoot + 'share-admin-libs'} />
|
||||
<ShareAdminFolders path={siteRoot + 'share-admin-folders'} />
|
||||
@ -374,4 +363,9 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
ReactDom.render(<App />, document.getElementById('wrapper'));
|
||||
const root = createRoot(document.getElementById('wrapper'));
|
||||
root.render(
|
||||
<LocationProvider history={globalHistory}>
|
||||
<App />
|
||||
</LocationProvider>
|
||||
);
|
||||
|
21
frontend/src/assets/icons/ai.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#999999;}
|
||||
</style>
|
||||
<title>ai</title>
|
||||
<g id="ai">
|
||||
<g id="ai-assistant">
|
||||
<path id="形状" class="st0" d="M16.1,0C24.9,0,32,6.8,32,15s-7.1,15-15.9,15c-0.1,0-0.3,0-0.6,0c-0.4,0-3.7,0.2-4.9,0.6
|
||||
c-1,0.2-2.8,0.9-3.9,1.2C6.6,31.9,6.2,32,6,32c-0.5,0-0.9-0.4-1-1c-0.2-1.1-0.2-3,0.4-4.7C2.1,23.5,0,19.6,0,15
|
||||
C0.1,6.8,7.2,0,16.1,0z M16,3C8.8,3,3,8.6,3,15.4c0,3.5,1.7,6.6,4.5,9.1l1.2,1.1c-0.4,0.8-0.7,1.3-0.8,1.7
|
||||
c-0.2,0.5-0.3,1.3-0.2,1.7c0.8-0.4,2-1,2.9-1.2c1.7-0.4,4.2-0.6,5.4-0.6c5.7,0,13-5,13-11.8S23.2,3,16,3z M13.1,6.9
|
||||
C10.3,6.9,8,9.1,8,11.8v10c0,0.1,0.1,0.3,0.3,0.3h2.8V11.9c0-1,0.9-1.9,1.9-1.9s1.9,0.9,1.9,1.9v2.3H12c-0.1,0-0.3,0-0.4,0.1
|
||||
c-0.1,0.1-0.1,0.3-0.1,0.4v1.9c0,0.4,0.3,0.6,0.6,0.6H15v4.9h2.8c0.1,0,0.3-0.1,0.3-0.3v-10C18.1,9.1,15.9,6.9,13.1,6.9L13.1,6.9z
|
||||
M22.4,11h-1.8c-0.4,0-0.6,0.2-0.6,0.6v9.8c0,0.4,0.2,0.6,0.6,0.6h1.8c0.4,0,0.6-0.2,0.6-0.6v-9.8C23,11.2,22.8,11,22.4,11z
|
||||
M21.5,10c0.8,0,1.5-0.7,1.5-1.5S22.3,7,21.5,7S20,7.7,20,8.5S20.7,10,21.5,10z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 683 B After Width: | Height: | Size: 683 B |
15
frontend/src/assets/icons/filter-circled.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#999999;}
|
||||
</style>
|
||||
<title>filter-circled</title>
|
||||
<g id="filter-circled">
|
||||
<path id="形状结合" class="st0" d="M16,1c8.3,0,15,6.7,15,15s-6.7,15-15,15S1,24.3,1,16S7.7,1,16,1z M16,4C9.4,4,4,9.4,4,16
|
||||
s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z M20,20c0.6,0,1,0.4,1,1s-0.4,1-1,1h-8c-0.6,0-1-0.4-1-1s0.4-1,1-1H20z M22,15
|
||||
c0.6,0,1,0.4,1,1s-0.4,1-1,1H10c-0.6,0-1-0.4-1-1s0.4-1,1-1H22z M24,10c0.6,0,1,0.4,1,1s-0.4,1-1,1H8c-0.6,0-1-0.4-1-1s0.4-1,1-1
|
||||
H24z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 846 B |
2
frontend/src/assets/icons/left_arrow.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" xmlns="http://www.w3.org/2000/svg" aria-labelledby="title" viewBox="0 0 32 32" preserveAspectRatio="xMidYMid meet" fill="currentColor" width="48" height="48" title="view-back"><title id="title">view-back</title><g><path d="M19.768,23.89c0.354,-0.424 0.296,-1.055 -0.128,-1.408c-1.645,-1.377 -5.465,-4.762 -6.774,-6.482c1.331,-1.749 5.1,-5.085 6.774,-6.482c0.424,-0.353 0.482,-0.984 0.128,-1.408c-0.353,-0.425 -0.984,-0.482 -1.409,-0.128c-1.839,1.532 -5.799,4.993 -7.2,6.964c-0.219,0.312 -0.409,0.664 -0.409,1.054c0,0.39 0.19,0.742 0.409,1.053c1.373,1.932 5.399,5.462 7.2,6.964l0.001,0.001c0.424,0.354 1.055,0.296 1.408,-0.128Z"></path></g></svg>
|
After Width: | Height: | Size: 780 B |
1
frontend/src/assets/icons/link.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1730788276176" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15486" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M108.8 768h816c38.4 0 67.2 35.2 67.2 80s-28.8 76.8-67.2 80H108.8c-38.4 0-67.2-35.2-67.2-80s32-80 67.2-80zM924.8 256H108.8C70.4 256 41.6 220.8 41.6 176S73.6 96 108.8 96h816C963.2 96 992 131.2 992 176S963.2 256 924.8 256z m-432 176h409.6c38.4 0 67.2 35.2 67.2 80s-28.8 80-67.2 80H492.8c-38.4 0-67.2-35.2-67.2-80s28.8-80 67.2-80z m-195.2 105.6L128 630.4c-28.8 19.2-67.2-6.4-67.2-38.4v-185.6c0-35.2 38.4-54.4 67.2-38.4l169.6 92.8c28.8 12.8 28.8 60.8 0 76.8z" p-id="15487"></path></svg>
|
After Width: | Height: | Size: 815 B |
20
frontend/src/assets/icons/map.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#999999;}
|
||||
</style>
|
||||
<title>map</title>
|
||||
<g id="map">
|
||||
<g id="形状结合" transform="translate(1.000000, 1.000000)">
|
||||
<path class="st0" d="M29.5,6.8C29.8,7,30,7.4,30,7.8v17.6c0,0.5-0.3,1-0.8,1.2l-8.8,3.3c-0.3,0.1-0.6,0.1-0.8,0L10,26.7l-8.3,3.2
|
||||
c-0.4,0.1-0.8,0.1-1.2-0.1c-0.3-0.2-0.5-0.6-0.5-1V11.2c0-0.5,0.3-1,0.8-1.2l4-1.5c-0.2,1.1-0.2,2.3,0,3.4l-2,0.9v13.4l5.6-2.4
|
||||
l0-5.4l2.8,3.5v2.7l7.5,2.5v-5.2l2.8-3.5l0,8l5.6-2.2V10.4l-1.9,0.8c0.1-1.1,0.1-2.2-0.2-3.3l3.2-1.2C28.7,6.5,29.1,6.6,29.5,6.8z
|
||||
M8.3,2.7c3.7-3.6,9.6-3.6,13.3,0c3.3,3.1,3.7,8,1.1,11.7l-6.2,7.7c-0.1,0.1-0.2,0.2-0.3,0.3c-0.9,0.7-2.1,0.6-2.8-0.3l-6.1-7.6
|
||||
C4.7,10.8,5.2,5.9,8.3,2.7z M19.4,4.8c-2.4-2.2-6.4-2.2-8.8,0c-2.2,2-2.4,5.1-0.7,7.4l4.1,5.5c0.5,0.7,1.5,0.7,2,0l4.1-5.5
|
||||
C21.8,9.9,21.6,6.8,19.4,4.8z M15,5c2.2,0,4,1.8,4,4c0,1.4-0.8,2.7-2,3.5s-2.8,0.7-4,0s-2-2-2-3.5C11,6.8,12.8,5,15,5z M15,8
|
||||
c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S15.6,8,15,8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
2
frontend/src/assets/icons/right_arrow.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" xmlns="http://www.w3.org/2000/svg" aria-labelledby="title" viewBox="0 0 32 32" preserveAspectRatio="xMidYMid meet" fill="currentColor" width="48" height="48" title="view-forward"><title id="title">view-forward</title><g><path d="M12.982,23.89c-0.354,-0.424 -0.296,-1.055 0.128,-1.408c1.645,-1.377 5.465,-4.762 6.774,-6.482c-1.331,-1.749 -5.1,-5.085 -6.774,-6.482c-0.424,-0.353 -0.482,-0.984 -0.128,-1.408c0.353,-0.425 0.984,-0.482 1.409,-0.128c1.839,1.532 5.799,4.993 7.2,6.964c0.219,0.312 0.409,0.664 0.409,1.054c0,0.39 -0.19,0.742 -0.409,1.053c-1.373,1.932 -5.399,5.462 -7.2,6.964l-0.001,0.001c-0.424,0.354 -1.055,0.296 -1.408,-0.128Z"></path></g></svg>
|
After Width: | Height: | Size: 790 B |
14
frontend/src/assets/icons/rotate.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-rotate</title>
|
||||
<g id="seafile" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="icon-rotate" transform="translate(8, 8) scale(-1, 1) translate(-8, -8)translate(-0, 0)">
|
||||
<rect id="image2" transform="translate(8, 8) scale(-1, 1) translate(-8, -8)" x="0" y="0" width="16" height="16"></rect>
|
||||
<path d="M13.8526316,16 L4.54736842,16 C3.36370614,16 2.4,15.057073 2.4,13.9003426 L2.4,6.89965737 C2.4,5.74109969 3.36370614,4.8 4.54736842,4.8 L13.8526316,4.8 C15.0362938,4.8 16,5.74292707 16,6.89965737 L16,13.9003426 C16,15.057073 15.0362938,16 13.8526316,16 Z M4.69333334,6.4 C4.31055555,6.4 4,6.69932565 4,7.06652165 L4,13.7334783 C4,14.1006743 4.31055555,14.4 4.69333334,14.4 L13.7066666,14.4 C14.0894445,14.4 14.4,14.1006743 14.4,13.7334783 L14.4,7.06652165 C14.4,6.69932565 14.0894445,6.4 13.7066666,6.4 L4.69333334,6.4 Z" id="image" fill="#666666" fill-rule="nonzero"></path>
|
||||
<g id="group" transform="translate(4.8173, 3.52) scale(-1, 1) translate(-4.8173, -3.52)translate(0.0347, 0)" fill="#666666" fill-rule="nonzero">
|
||||
<path d="M8.87810624,7.04 C8.61343976,7.04 8.36129136,6.88219178 8.24862928,6.61795476 C6.78581083,3.19571838 3.28255715,2.68559414 1.32438325,2.68559414 C0.945266472,2.68559414 0.637681159,2.3699777 0.637681159,1.98279707 C0.637681159,1.59561644 0.945266472,1.28 1.32438325,1.28 C5.22642483,1.28 8.20928696,3.02139535 9.50758312,6.0564511 C9.65958752,6.41243708 9.50043,6.8271424 9.1535024,6.98311564 C9.064088,7.02165021 8.9693088,7.04 8.87810624,7.04 Z" id="path2"></path>
|
||||
<path d="M1.42164886,3.84 C1.29607734,3.84 1.1692245,3.77584105 1.07312386,3.64578912 L0.144150966,2.39035448 C-0.0480503219,2.13025062 -0.0480503219,1.71061639 0.144150966,1.45051253 L1.07312386,0.195077896 C1.26532514,-0.0650259653 1.57669123,-0.0650259653 1.76889251,0.195077896 C1.96109379,0.455181757 1.96109379,0.874815984 1.76889251,1.13491985 L1.18716329,1.9204335 L1.76889251,2.70594717 C1.96109379,2.96605102 1.96109379,3.38568526 1.76889251,3.64578912 C1.67407323,3.77410702 1.54722036,3.84 1.42164886,3.84 Z" id="path1"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721786317709" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22672" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 304c115.2 0 208 92.8 208 208s-92.8 208-208 208-208-92.8-208-208 92.8-208 208-208z m0 128c-44.8 0-80 35.2-80 80s35.2 80 80 80 80-35.2 80-80-35.2-80-80-80z" p-id="22673"></path><path d="M432 57.6c-9.6-19.2-32-28.8-51.2-22.4-80 19.2-150.4 60.8-208 112-12.8 16-16 35.2-9.6 54.4 6.4 12.8 9.6 25.6 9.6 41.6 0 51.2-41.6 92.8-89.6 92.8-19.2 0-38.4 12.8-41.6 32C22.4 409.6 16 460.8 16 512c0 35.2 3.2 67.2 9.6 99.2 3.2 22.4 25.6 38.4 48 35.2h9.6c51.2 0 89.6 41.6 89.6 92.8 0 22.4-9.6 41.6-22.4 57.6-16 16-12.8 41.6 3.2 60.8 57.6 60.8 131.2 105.6 214.4 131.2 22.4 6.4 48-6.4 54.4-28.8 12.8-38.4 44.8-64 86.4-64s73.6 25.6 86.4 64c6.4 22.4 32 35.2 54.4 28.8 83.2-25.6 156.8-70.4 214.4-131.2 16-16 16-41.6 3.2-60.8-12.8-16-22.4-35.2-22.4-57.6 0-51.2 41.6-92.8 89.6-92.8h9.6c22.4 3.2 44.8-12.8 48-35.2 6.4-32 9.6-67.2 9.6-99.2 0-51.2-6.4-102.4-22.4-150.4-6.4-19.2-22.4-32-41.6-32-51.2 0-89.6-41.6-89.6-92.8 0-16 3.2-28.8 9.6-41.6 9.6-16 3.2-38.4-9.6-51.2-57.6-54.4-128-92.8-204.8-115.2-19.2 0-41.6 9.6-51.2 28.8-16 28.8-44.8 51.2-80 51.2s-67.2-22.4-80-51.2zM288 262.4c0-16-3.2-28.8-6.4-41.6 32-25.6 67.2-44.8 105.6-60.8 28.8 38.4 73.6 60.8 124.8 60.8s96-22.4 124.8-60.8c38.4 12.8 73.6 35.2 105.6 60.8-3.2 12.8-6.4 28.8-6.4 41.6 0 80 57.6 150.4 134.4 163.2 6.4 28.8 9.6 57.6 9.6 86.4 0 16 0 28.8-3.2 44.8-80 9.6-140.8 80-140.8 163.2 0 25.6 6.4 51.2 16 73.6-32 28.8-67.2 51.2-105.6 67.2-28.8-48-80-76.8-137.6-76.8s-108.8 32-137.6 76.8c-38.4-16-73.6-38.4-105.6-67.2 9.6-22.4 16-48 16-73.6 0-83.2-60.8-153.6-140.8-163.2 3.2-12.8 3.2-28.8 3.2-41.6 0-28.8 3.2-57.6 9.6-86.4C230.4 412.8 288 345.6 288 262.4z" p-id="22674"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733301494152" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6527" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 320c105.6 0 192 86.4 192 192s-86.4 192-192 192-192-86.4-192-192 86.4-192 192-192z m0 96c-54.4 0-96 41.6-96 96s41.6 96 96 96 96-41.6 96-96-41.6-96-96-96z" p-id="6528"></path><path d="M432 60.8c-9.6-19.2-32-28.8-54.4-22.4-83.2 19.2-153.6 60.8-214.4 112-12.8 16-16 35.2-9.6 54.4 6.4 12.8 9.6 25.6 9.6 41.6 0 51.2-44.8 92.8-92.8 92.8-19.2 0-38.4 12.8-44.8 32-19.2 41.6-25.6 92.8-25.6 144 0 35.2 3.2 67.2 9.6 99.2 3.2 22.4 25.6 38.4 51.2 35.2h9.6c54.4 0 92.8 41.6 92.8 92.8 0 22.4-9.6 41.6-22.4 57.6-16 16-12.8 41.6 3.2 60.8 60.8 60.8 137.6 105.6 224 131.2 22.4 6.4 51.2-6.4 57.6-28.8 12.8-38.4 48-64 89.6-64s76.8 25.6 89.6 64c6.4 22.4 32 35.2 57.6 28.8 86.4-25.6 163.2-70.4 224-131.2 16-16 16-41.6 3.2-60.8-12.8-16-22.4-35.2-22.4-57.6 0-51.2 44.8-92.8 92.8-92.8h9.6c22.4 3.2 48-12.8 51.2-35.2 6.4-32 9.6-67.2 9.6-99.2 0-51.2-6.4-102.4-22.4-150.4-6.4-19.2-22.4-32-44.8-32-54.4 0-92.8-41.6-92.8-92.8 0-16 3.2-28.8 9.6-41.6 3.2-16-3.2-38.4-16-51.2-60.8-54.4-134.4-92.8-211.2-115.2-19.2 0-44.8 9.6-54.4 28.8-16 28.8-48 51.2-83.2 51.2s-70.4-22.4-83.2-51.2zM262.4 240c0-16-3.2-32-6.4-44.8 35.2-28.8 76.8-48 118.4-67.2 32 41.6 83.2 67.2 140.8 67.2 57.6 0 108.8-25.6 140.8-67.2 44.8 12.8 83.2 38.4 118.4 67.2-3.2 12.8-6.4 32-6.4 44.8 0 86.4 64 166.4 150.4 179.2 6.4 32 9.6 64 9.6 96 0 16 0 32-3.2 48-89.6 9.6-160 86.4-160 179.2 0 28.8 6.4 57.6 19.2 80-35.2 32-76.8 57.6-118.4 73.6-32-51.2-89.6-83.2-153.6-83.2s-121.6 35.2-153.6 83.2c-44.8-16-83.2-41.6-118.4-73.6 6.4-25.6 16-51.2 16-80 0-89.6-67.2-169.6-160-179.2 3.2-12.8 3.2-32 3.2-44.8 0-32 3.2-64 9.6-96 89.6-16 153.6-89.6 153.6-182.4z" p-id="6529"></path></svg>
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@ -1 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M13.0123,19.3233l7.07107,-7.07107l-9.40841,-9.40842l-6.84754,-0.297719c-0.0144722,-0.00062923 -0.028965,-0.00062923 -0.0434372,0c-0.275882,0.0119949 -0.489804,0.245365 -0.477809,0.521247l0.297719,6.84754l9.40842,9.40842Zm-10.7052,-16.2125c-0.0359846,-0.827645 0.605783,-1.52776 1.43343,-1.56374c0.0434167,-0.00188768 0.086895,-0.00188768 0.130312,0l7.23616,0.314616l10.3906,10.3906l-8.48528,8.48528l-10.3906,-10.3906l-0.314616,-7.23616Zm5.75544,4.1917c-0.585786,0.585786 -1.53553,0.585786 -2.12132,0c-0.585786,-0.585786 -0.585786,-1.53553 0,-2.12132c0.585786,-0.585786 1.53553,-0.585786 2.12132,0c0.585786,0.585786 0.585786,1.53553 0,2.12132Zm-0.707107,-0.707107c0.195262,-0.195262 0.195262,-0.511845 0,-0.707107c-0.195262,-0.195262 -0.511845,-0.195262 -0.707107,0c-0.195262,0.195262 -0.195262,0.511845 0,0.707107c0.195262,0.195262 0.511845,0.195262 0.707107,0Z" fill="#979797" stroke="none"></path></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="15px" height="15px" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M13.7515136,5.93572645 L8.28976642,0.621080351 C7.879571,0.222593643 7.32333684,-0.000847143079 6.7436592,2.41376939e-06 L2.18654926,2.41376939e-06 C0.979680972,2.41376939e-06 0,0.950120356 0,2.12526321 L0,6.55780208 C0,7.12087054 0.230271291,7.66193631 0.64044202,8.06098482 L6.10527317,13.3776312 C6.95834688,14.2074603 8.34132992,14.2074603 9.19440364,13.3776312 L13.7515136,8.94309205 C14.6047509,8.11299113 14.6047509,6.76682748 13.7515136,5.93672657 L13.7515136,5.93572645 Z M12.1129482,8.06825704 L8.34330379,11.7695888 C7.91766113,12.1834794 7.22778724,12.1834794 6.80214458,11.7695888 L2.11187052,7.05109716 C1.90701239,6.85210202 1.79175754,6.58211021 1.79144107,6.30047085 L1.79144107,2.74606218 C1.79169751,2.46489086 1.9068179,2.19534855 2.11144974,1.99679547 C2.31608157,1.79824239 2.59344239,1.68696248 2.88245014,1.68746078 L6.64338195,1.68746078 C6.93283334,1.68746078 7.20969987,1.79859659 7.41396155,1.99731949 L12.1119801,6.56794622 C12.5379286,6.98234468 12.5379286,7.65385856 12.1119801,8.06825704 L12.1129482,8.06825704 Z" id="image2"></path>
|
||||
<path d="M5.19151264,2.9941275 C4.08690365,2.9941275 3.19144113,3.86530858 3.19144113,4.93996491 C3.19144113,6.01462124 4.08690365,6.88580232 5.19151264,6.88580232 C6.29612164,6.88580232 7.19158415,6.01462124 7.19158415,4.93996491 C7.19158415,3.86530858 6.29612164,2.9941275 5.19151264,2.9941275 L5.19151264,2.9941275 Z M4.88150156,5.43615345 C4.69675086,5.322975 4.58976949,5.12109806 4.60216374,4.9090349 C4.61455799,4.69697174 4.74437157,4.50819414 4.94111847,4.41611989 C5.13786537,4.32404563 5.37049132,4.34320761 5.54852541,4.4661535 C5.81754925,4.64750572 5.88755949,5.00514843 5.70588358,5.2700074 C5.52420766,5.53486636 5.15787671,5.60921928 4.88250159,5.43712636 L4.88150156,5.43615345 Z" id="image"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.9 KiB |
14
frontend/src/assets/icons/time.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#999999;}
|
||||
</style>
|
||||
<title>time</title>
|
||||
<g id="time">
|
||||
<path id="形状" class="st0" d="M28,16c0-6.6-5.4-12-12-12S4,9.4,4,16s5.4,12,12,12S28,22.6,28,16 M31,16c0,8.2-6.8,15-15,15
|
||||
S1,24.2,1,16S7.8,1,16,1S31,7.8,31,16 M25,17.5c0,0.8-0.7,1.5-1.5,1.5h-6c-1.7,0-3-1.3-3-3V8.5C14.5,7.7,15.2,7,16,7
|
||||
s1.5,0.7,1.5,1.5v6c0,0.8,0.7,1.5,1.5,1.5h4.5C24.3,16,25,16.7,25,17.5"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 759 B |
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Button, Popover, PopoverBody } from 'reactstrap';
|
||||
import { gettext } from '../utils/constants';
|
||||
|
||||
@ -16,8 +16,7 @@ class ButtonQR extends React.Component {
|
||||
this.state = {
|
||||
isPopoverOpen: false
|
||||
};
|
||||
|
||||
this.btnID = 'btn-' + Math.random().toString().substr(2, 5);
|
||||
this.btn = null;
|
||||
}
|
||||
|
||||
togglePopover = () => {
|
||||
@ -30,14 +29,16 @@ class ButtonQR extends React.Component {
|
||||
const { link } = this.props;
|
||||
const { isPopoverOpen } = this.state;
|
||||
return (
|
||||
<div className="ml-2">
|
||||
<Button outline color="primary" className="btn-icon btn-qr-code-icon sf3-font sf3-font-qr-code" id={this.btnID} onClick={this.togglePopover} type="button"></Button>
|
||||
<Popover placement="bottom" isOpen={isPopoverOpen} target={this.btnID} toggle={this.togglePopover}>
|
||||
<PopoverBody>
|
||||
<QRCode value={link} size={128} />
|
||||
<p className="m-0 mt-1 text-center" style={{ 'maxWidth': '128px' }}>{gettext('Scan the QR code to view the shared content directly')}</p>
|
||||
</PopoverBody>
|
||||
</Popover>
|
||||
<div className="ml-2" ref={ref => this.btn = ref}>
|
||||
<Button outline color="primary" className="btn-icon btn-qr-code-icon sf3-font sf3-font-qr-code" onClick={this.togglePopover} type="button"></Button>
|
||||
{this.btn && (
|
||||
<Popover placement="bottom" isOpen={isPopoverOpen} target={this.btn} toggle={this.togglePopover}>
|
||||
<PopoverBody>
|
||||
<QRCodeSVG value={link} size={128} />
|
||||
<p className="m-0 mt-1 text-center" style={{ 'maxWidth': '128px' }}>{gettext('Scan the QR code to view the shared content directly')}</p>
|
||||
</PopoverBody>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
7
frontend/src/components/centered-loading/index.css
Normal file
@ -0,0 +1,7 @@
|
||||
.sf-centered-loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
20
frontend/src/components/centered-loading/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Loading from '../loading';
|
||||
|
||||
import './index.css';
|
||||
|
||||
function CenteredLoading(props) {
|
||||
return (
|
||||
<div className={classnames('sf-centered-loading', props.className)}>
|
||||
<Loading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CenteredLoading.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CenteredLoading;
|
43
frontend/src/components/click-outside.js
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ClickOutside extends React.Component {
|
||||
|
||||
isClickedInside = false;
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mousedown', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mousedown', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
handleDocumentClick = (e) => {
|
||||
if (this.isClickedInside) {
|
||||
this.isClickedInside = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onClickOutside(e);
|
||||
};
|
||||
|
||||
handleMouseDown = () => {
|
||||
this.isClickedInside = true;
|
||||
};
|
||||
|
||||
render() {
|
||||
return React.cloneElement(
|
||||
React.Children.only(this.props.children), {
|
||||
onMouseDownCapture: this.handleMouseDown
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClickOutside.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
onClickOutside: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ClickOutside;
|
@ -21,7 +21,7 @@
|
||||
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid #eb8205;
|
||||
border: 4px solid #EC8000;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
|
@ -1,33 +1,30 @@
|
||||
.add-item-btn {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-top: 1px solid #dedede;
|
||||
background: #fff;
|
||||
padding: 0 1rem;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
position: relative;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.add-item-btn:hover {
|
||||
background-color: #f5f5f5;
|
||||
cursor: pointer;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.add-item-btn .dtable-icon-add-table {
|
||||
.add-item-btn .seafile-multicolor-icon-add-table {
|
||||
margin-right: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transform: none;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.add-item-btn .add-new-option {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.add-item-btn .description {
|
||||
flex: 1;
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import '../../css/common-add-tool.css';
|
||||
import Icon from '../icon';
|
||||
|
||||
function CommonAddTool(props) {
|
||||
const { callBack, footerName, className, addIconClassName } = props;
|
||||
import './index.css';
|
||||
|
||||
function CommonAddTool({ callBack, footerName, className, addIconClassName, hideIcon, style }) {
|
||||
return (
|
||||
<div className={`add-item-btn ${className ? className : ''}`} onClick={(e) => {callBack(e);}}>
|
||||
<span className={`sf3-font sf3-font-enlarge mr-2 ${addIconClassName || ''}`}></span>
|
||||
<span className='add-new-option' title={footerName}>{footerName}</span>
|
||||
<div className={`add-item-btn ${className ? className : ''}`} style={style} onClick={(e) => {callBack(e);}}>
|
||||
{!hideIcon && <Icon symbol="add-table" className={addIconClassName} />}
|
||||
<span className="description text-truncate">{footerName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { siteRoot, isPro, gettext, appAvatarURL, enableSSOToThirdpartWebsite } from '../../utils/constants';
|
||||
@ -35,10 +34,6 @@ class Account extends Component {
|
||||
this.handleProps();
|
||||
}
|
||||
|
||||
getContainer = () => {
|
||||
return ReactDOM.findDOMNode(this);
|
||||
};
|
||||
|
||||
handleProps = () => {
|
||||
if (this.state.showInfo) {
|
||||
this.addEvents();
|
||||
@ -61,9 +56,7 @@ class Account extends Component {
|
||||
|
||||
handleDocumentClick = (e) => {
|
||||
if (e && (e.which === 3 || (e.type === 'keyup' && e.which !== Utils.keyCodes.tab))) return;
|
||||
const container = this.getContainer();
|
||||
|
||||
if (container.contains(e.target) && container !== e.target && (e.type !== 'keyup' || e.which === Utils.keyCodes.tab)) {
|
||||
if (this.accountDOM && this.accountDOM.contains(e.target) && this.accountDOM !== e.target && (e.type !== 'keyup' || e.which === Utils.keyCodes.tab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -101,8 +94,9 @@ class Account extends Component {
|
||||
renderMenu = () => {
|
||||
let data;
|
||||
const { isStaff, isOrgStaff, isInstAdmin } = this.state;
|
||||
const { isAdminPanel = false } = this.props;
|
||||
|
||||
if (this.props.isAdminPanel) {
|
||||
if (isAdminPanel) {
|
||||
if (isStaff) {
|
||||
data = {
|
||||
url: siteRoot,
|
||||
@ -148,7 +142,7 @@ class Account extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="account">
|
||||
<div id="account" ref={ref => this.accountDOM = ref}>
|
||||
<a id="my-info" href="#" onClick={this.onClickAccount} className="account-toggle no-deco d-none d-md-block" aria-label={gettext('View profile and more')}>
|
||||
{this.renderAvatar()}
|
||||
</a>
|
||||
@ -180,10 +174,6 @@ class Account extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
Account.defaultProps = {
|
||||
isAdminPanel: false
|
||||
};
|
||||
|
||||
Account.propTypes = propTypes;
|
||||
|
||||
export default Account;
|
||||
|
22
frontend/src/components/common/common-undo-tool.js
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
function CommonUndoTool(props) {
|
||||
const style = {
|
||||
color: 'rgb(71, 184, 129)',
|
||||
marginLeft: '8px',
|
||||
paddingBottom: '1px',
|
||||
borderBottom: '1px solid rgb(71, 184, 129)',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
return (
|
||||
<span onClick={(e) => {e.stopPropagation(); props.onUndoOperation(e);}} style={style}>{gettext('Undo')}</span>
|
||||
);
|
||||
}
|
||||
|
||||
CommonUndoTool.propTypes = {
|
||||
onUndoOperation: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CommonUndoTool;
|
@ -3,6 +3,21 @@ export const EVENT_BUS_TYPE = {
|
||||
CURRENT_LIBRARY_CHANGED: 'current_library_changed',
|
||||
SEARCH_LIBRARY_CONTENT: 'search_library_content',
|
||||
|
||||
// group
|
||||
ADD_NEW_GROUP: 'add_new_group',
|
||||
ADD_SHARED_REPO_INTO_GROUP: 'add_shared_repo_into_group',
|
||||
UNSHARE_REPO_TO_GROUP: 'unshare_repo_to_group',
|
||||
|
||||
RESTORE_IMAGE: 'restore_image',
|
||||
OPEN_MARKDOWN_DIALOG: 'open_markdown_dialog',
|
||||
OPEN_MARKDOWN: 'open_markdown',
|
||||
|
||||
// migrate tags
|
||||
OPEN_TREE_PANEL: 'open_tree_panel',
|
||||
OPEN_LIBRARY_SETTINGS_TAGS: 'open_library_settings_tags',
|
||||
|
||||
// tags
|
||||
TAG_STATUS: 'tag_status',
|
||||
TAGS_DATA: 'tags_data',
|
||||
SELECT_TAG: 'select_tag',
|
||||
UPDATE_SELECTED_TAG: 'update_selected_tag',
|
||||
};
|
||||
|
@ -1,5 +1,16 @@
|
||||
class EventBus {
|
||||
subscribers = {};
|
||||
|
||||
constructor() {
|
||||
this.instance = null;
|
||||
this.subscribers = {};
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (this.instance) return this.instance;
|
||||
this.instance = new EventBus();
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
|
||||
subscribe(type, handler) {
|
||||
if (!this.subscribers[type]) {
|
||||
|
59
frontend/src/components/common/fixed-width-table.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FixedWidthTable = ({ className, headers, theadOptions = {}, children }) => {
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
const fixedWidth = useMemo(() => headers.reduce((pre, cur) => cur.isFixed ? cur.width + pre : pre, 0), [headers]);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const handleResize = () => {
|
||||
if (!container) return;
|
||||
setContainerWidth(container.offsetWidth);
|
||||
};
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
container && resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
container && resizeObserver.unobserve(container);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<table ref={containerRef} className={className}>
|
||||
<thead { ...theadOptions }>
|
||||
<tr>
|
||||
{headers.map((header, index) => {
|
||||
const { width, isFixed, className, onClick = () => {}, title = '', ariaLabel = '' } = header;
|
||||
return (
|
||||
<th
|
||||
key={index}
|
||||
style={{ width: isFixed ? width : (containerWidth - fixedWidth) * width }}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{header.children}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
FixedWidthTable.propTypes = {
|
||||
className: PropTypes.string,
|
||||
headers: PropTypes.array,
|
||||
theadOptions: PropTypes.object,
|
||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default FixedWidthTable;
|
96
frontend/src/components/common/group-select/index.css
Normal file
@ -0,0 +1,96 @@
|
||||
.group-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.group-select.custom-select {
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 900px;
|
||||
user-select: none;
|
||||
text-align: left;
|
||||
border-color: 1px solid rgba(0, 40, 100, 0.12);
|
||||
height: auto;
|
||||
min-height: 38px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-select.custom-select:focus,
|
||||
.group-select.custom-select.focus {
|
||||
border-color: #1991eb !important;
|
||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
||||
}
|
||||
|
||||
.group-select.custom-select.disabled:focus,
|
||||
.group-select.custom-select.focus.disabled,
|
||||
.group-select.custom-select.disabled:hover {
|
||||
border-color: rgba(0, 40, 100, 0.12) !important;
|
||||
box-shadow: unset;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.group-select .sf3-font-down {
|
||||
display: inline-block;
|
||||
color: #999;
|
||||
transform: translateY(2px);
|
||||
transition: all 0.1s;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.group-select .sf3-font-down:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.group-select .selected-option {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.group-select.selector-collaborator .option-group .option-group-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.group-select.custom-select.selector-collaborator .option-group .option-group-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.group-select.custom-select.selector-collaborator .option {
|
||||
padding: 5px 0 5px 10px !important;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.group-select .select-placeholder {
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group-select .selected-option-show {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.group-select .selected-option-show .selected-option-item {
|
||||
background-color: rgb(240, 240, 240);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group-select .selected-option-show .selected-option-item .selected-option-item-name {
|
||||
font-size: 13px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.group-select .selected-option-show .selected-option-item .sf2-icon-close {
|
||||
cursor: pointer;
|
||||
color: rgb(103, 103, 103);
|
||||
}
|
138
frontend/src/components/common/group-select/index.js
Normal file
@ -0,0 +1,138 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ModalPortal from '../../modal-portal';
|
||||
import SelectOptionGroup from './select-option-group.js';
|
||||
|
||||
import './index.css';
|
||||
|
||||
class GroupSelect extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowSelectOptions: false
|
||||
};
|
||||
}
|
||||
|
||||
onSelectToggle = (event) => {
|
||||
event.preventDefault();
|
||||
if (this.state.isShowSelectOptions) event.stopPropagation();
|
||||
let eventClassName = event.target.className;
|
||||
if (eventClassName.indexOf('sf2-icon-close') > -1 || eventClassName === 'option-group-search') return;
|
||||
if (event.target.value === '') return;
|
||||
this.setState({
|
||||
isShowSelectOptions: !this.state.isShowSelectOptions
|
||||
});
|
||||
};
|
||||
|
||||
onClickOutside = (event) => {
|
||||
if (this.props.isShowSelected && event.target.className.includes('icon-fork-number')) {
|
||||
return;
|
||||
}
|
||||
if (!this.selector.contains(event.target)) {
|
||||
this.closeSelect();
|
||||
}
|
||||
};
|
||||
|
||||
closeSelect = () => {
|
||||
this.setState({ isShowSelectOptions: false });
|
||||
};
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.selectedOptions.length !== this.props.selectedOptions.length) {
|
||||
// when selectedOptions change and dom rendered, calculate top
|
||||
setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedOptionTop = () => {
|
||||
if (!this.selector) return 38;
|
||||
const { height } = this.selector.getBoundingClientRect();
|
||||
return height;
|
||||
};
|
||||
|
||||
getFilterOptions = (searchValue) => {
|
||||
const { options } = this.props;
|
||||
const validSearchVal = searchValue.trim().toLowerCase();
|
||||
if (!validSearchVal) return options || [];
|
||||
return options.filter(option => option.name.toLowerCase().includes(validSearchVal));
|
||||
};
|
||||
|
||||
render() {
|
||||
let { className, selectedOptions, options, placeholder, searchPlaceholder, noOptionsPlaceholder, isInModal } = this.props;
|
||||
return (
|
||||
<div
|
||||
ref={(node) => this.selector = node}
|
||||
className={classnames('group-select custom-select',
|
||||
{ 'focus': this.state.isShowSelectOptions },
|
||||
className
|
||||
)}
|
||||
onClick={this.onSelectToggle}>
|
||||
<div className="selected-option">
|
||||
{selectedOptions.length > 0 ?
|
||||
<span className="selected-option-show">
|
||||
{selectedOptions.map(item =>
|
||||
<span key={item.id} className="selected-option-item mr-1 px-1">
|
||||
<span className='selected-option-item-name'>{item.name}</span>
|
||||
<i className="sf2-icon-close ml-1" onClick={() => {this.props.onDeleteOption(item);}}></i>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
:
|
||||
<span className="select-placeholder">{placeholder}</span>
|
||||
}
|
||||
<i className="sf3-font-down sf3-font"></i>
|
||||
</div>
|
||||
{this.state.isShowSelectOptions && !isInModal && (
|
||||
<SelectOptionGroup
|
||||
selectedOptions={selectedOptions}
|
||||
top={this.getSelectedOptionTop()}
|
||||
options={options}
|
||||
onSelectOption={this.props.onSelectOption}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
noOptionsPlaceholder={noOptionsPlaceholder}
|
||||
onClickOutside={this.onClickOutside}
|
||||
closeSelect={this.closeSelect}
|
||||
getFilterOptions={this.getFilterOptions}
|
||||
/>
|
||||
)}
|
||||
{this.state.isShowSelectOptions && isInModal && (
|
||||
<ModalPortal>
|
||||
<SelectOptionGroup
|
||||
className={className}
|
||||
selectedOptions={selectedOptions}
|
||||
position={this.selector.getBoundingClientRect()}
|
||||
isInModal={isInModal}
|
||||
top={this.getSelectedOptionTop()}
|
||||
options={options}
|
||||
onSelectOption={this.props.onSelectOption}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
noOptionsPlaceholder={noOptionsPlaceholder}
|
||||
onClickOutside={this.onClickOutside}
|
||||
closeSelect={this.closeSelect}
|
||||
getFilterOptions={this.getFilterOptions}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
selectedOptions: PropTypes.array,
|
||||
options: PropTypes.array,
|
||||
placeholder: PropTypes.string,
|
||||
onSelectOption: PropTypes.func,
|
||||
onDeleteOption: PropTypes.func,
|
||||
searchable: PropTypes.bool,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
noOptionsPlaceholder: PropTypes.string,
|
||||
isInModal: PropTypes.bool, // if select component in a modal (option group need ModalPortal to show)
|
||||
};
|
||||
|
||||
export default GroupSelect;
|
46
frontend/src/components/common/group-select/option.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Option extends Component {
|
||||
|
||||
onSelectOption = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.onSelectOption(this.props.option);
|
||||
};
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (!this.props.disableHover) {
|
||||
this.props.changeIndex(this.props.index);
|
||||
}
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
if (!this.props.disableHover) {
|
||||
this.props.changeIndex(-1);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={this.props.isActive ? 'option option-active' : 'option'}
|
||||
onClick={this.onSelectOption}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Option.propTypes = {
|
||||
index: PropTypes.number,
|
||||
isActive: PropTypes.bool,
|
||||
changeIndex: PropTypes.func,
|
||||
option: PropTypes.object,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
onSelectOption: PropTypes.func,
|
||||
disableHover: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Option;
|
@ -0,0 +1,86 @@
|
||||
.option-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
min-height: 60px;
|
||||
max-height: 300px;
|
||||
min-width: 100%;
|
||||
max-width: 15rem;
|
||||
padding: 0.5rem 0;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border-radius: 3px;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.option-group .option-group-search {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.option-group-search .form-control {
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.option-group .none-search-result {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.option-group .option-group-content {
|
||||
max-height: 252px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
padding: 6px 10px;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
text-align: inherit;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.option.option-active {
|
||||
background-color: #20a0ff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option.option-active .sf2-icon-tick,
|
||||
.option.option-active .select-option-name {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.option .select-option-name .single-select-option {
|
||||
margin: 0 0 0 12px;
|
||||
}
|
||||
|
||||
.option .select-option-name .multiple-select-option {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.option-group-selector-single-select .select-option-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.option-group-selector-single-select .option:hover,
|
||||
.option-group-selector-single-select .option.option-active,
|
||||
.option-group-selector-multiple-select .option:hover,
|
||||
.option-group-selector-multiple-select .option.option-active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import SearchInput from '../../search-input';
|
||||
import Option from './option';
|
||||
import KeyCodes from '../../../constants/keyCodes';
|
||||
|
||||
import './select-option-group.css';
|
||||
|
||||
const OPTION_HEIGHT = 32;
|
||||
|
||||
class SelectOptionGroup extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchVal: '',
|
||||
activeIndex: -1,
|
||||
disableHover: false,
|
||||
};
|
||||
this.filterOptions = null;
|
||||
this.timer = null;
|
||||
this.searchInputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.onHotKey);
|
||||
document.addEventListener('mousedown', this.handleDocumentClick);
|
||||
setTimeout(() => {
|
||||
this.resetMenuStyle();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.filterOptions = null;
|
||||
this.timer && clearTimeout(this.timer);
|
||||
window.removeEventListener('keydown', this.onHotKey);
|
||||
document.removeEventListener('mousedown', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
handleDocumentClick = (e) => {
|
||||
this.props.onClickOutside(e);
|
||||
};
|
||||
|
||||
resetMenuStyle = () => {
|
||||
const { isInModal, position } = this.props;
|
||||
const { top, height } = this.optionGroupRef.getBoundingClientRect();
|
||||
if (isInModal) {
|
||||
if (position.y + position.height + height > window.innerHeight) {
|
||||
this.optionGroupRef.style.top = (position.y - height) + 'px';
|
||||
}
|
||||
this.optionGroupRef.style.opacity = 1;
|
||||
}
|
||||
else {
|
||||
if (height + top > window.innerHeight) {
|
||||
const borderWidth = 2;
|
||||
this.optionGroupRef.style.top = -1 * (height + borderWidth) + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onHotKey = (event) => {
|
||||
const keyCode = event.keyCode;
|
||||
if (keyCode === KeyCodes.UpArrow) {
|
||||
this.onPressUp();
|
||||
} else if (keyCode === KeyCodes.DownArrow) {
|
||||
this.onPressDown();
|
||||
} else if (keyCode === KeyCodes.Enter) {
|
||||
let option = this.filterOptions && this.filterOptions[this.state.activeIndex];
|
||||
if (option) {
|
||||
this.props.onSelectOption(option);
|
||||
}
|
||||
} else if (keyCode === KeyCodes.Tab || keyCode === KeyCodes.Escape) {
|
||||
this.props.closeSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onPressUp = () => {
|
||||
if (this.state.activeIndex > 0) {
|
||||
this.setState({ activeIndex: this.state.activeIndex - 1 }, () => {
|
||||
this.scrollContent();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onPressDown = () => {
|
||||
if (this.filterOptions && this.state.activeIndex < this.filterOptions.length - 1) {
|
||||
this.setState({ activeIndex: this.state.activeIndex + 1 }, () => {
|
||||
this.scrollContent();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (e) => {
|
||||
const { isInModal } = this.props;
|
||||
// prevent event propagation when click option or search input
|
||||
if (isInModal) {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
scrollContent = () => {
|
||||
const { offsetHeight, scrollTop } = this.optionGroupContentRef;
|
||||
this.setState({ disableHover: true });
|
||||
this.timer = setTimeout(() => {
|
||||
this.setState({ disableHover: false });
|
||||
}, 500);
|
||||
if (this.state.activeIndex * OPTION_HEIGHT === 0) {
|
||||
this.optionGroupContentRef.scrollTop = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.activeIndex * OPTION_HEIGHT < scrollTop) {
|
||||
this.optionGroupContentRef.scrollTop = scrollTop - OPTION_HEIGHT;
|
||||
}
|
||||
else if (this.state.activeIndex * OPTION_HEIGHT > offsetHeight + scrollTop) {
|
||||
this.optionGroupContentRef.scrollTop = scrollTop + OPTION_HEIGHT;
|
||||
}
|
||||
};
|
||||
|
||||
changeIndex = (index) => {
|
||||
this.setState({ activeIndex: index });
|
||||
};
|
||||
|
||||
onChangeSearch = (searchVal) => {
|
||||
this.setState({ searchVal: searchVal || '', activeIndex: -1, });
|
||||
};
|
||||
|
||||
clearValue = () => {
|
||||
this.setState({ searchVal: '', activeIndex: -1, });
|
||||
};
|
||||
|
||||
renderOptGroup = (searchVal) => {
|
||||
let { noOptionsPlaceholder, onSelectOption, selectedOptions } = this.props;
|
||||
this.filterOptions = this.props.getFilterOptions(searchVal);
|
||||
if (this.filterOptions.length === 0) {
|
||||
return (
|
||||
<div className="none-search-result">{noOptionsPlaceholder}</div>
|
||||
);
|
||||
}
|
||||
return this.filterOptions.map((option, index) => {
|
||||
const isSelected = selectedOptions.some(item => item.id === option.id);
|
||||
return (
|
||||
<Option
|
||||
key={`${option.id}-${index}`}
|
||||
index={index}
|
||||
isActive={this.state.activeIndex === index}
|
||||
option={option}
|
||||
onSelectOption={onSelectOption}
|
||||
changeIndex={this.changeIndex}
|
||||
disableHover={this.state.disableHover}
|
||||
>
|
||||
<div className='option-label'>{option.label}</div>
|
||||
{isSelected && <i className="sf2-icon-tick text-gray font-weight-bold"></i>}
|
||||
</Option>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { searchPlaceholder, top, left, minWidth, isInModal, position, className } = this.props;
|
||||
let { searchVal } = this.state;
|
||||
let style = { top: top || 0, left: left || 0 };
|
||||
if (minWidth) {
|
||||
style = { top: top || 0, left: left || 0, minWidth };
|
||||
}
|
||||
if (isInModal) {
|
||||
style = {
|
||||
position: 'fixed',
|
||||
left: position.x,
|
||||
top: position.y + position.height,
|
||||
minWidth: position.width,
|
||||
opacity: 0,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classnames('pt-0 option-group', className ? 'option-group-' + className : '')}
|
||||
ref={(ref) => this.optionGroupRef = ref}
|
||||
style={style}
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
<div className="option-group-search position-relative">
|
||||
<SearchInput
|
||||
className="option-search-control"
|
||||
autoFocus={isInModal}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={this.onChangeSearch}
|
||||
ref={this.searchInputRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="option-group-content" ref={(ref) => this.optionGroupContentRef = ref}>
|
||||
{this.renderOptGroup(searchVal)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectOptionGroup.propTypes = {
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
minWidth: PropTypes.number,
|
||||
options: PropTypes.array,
|
||||
onSelectOption: PropTypes.func,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
noOptionsPlaceholder: PropTypes.string,
|
||||
onClickOutside: PropTypes.func.isRequired,
|
||||
closeSelect: PropTypes.func.isRequired,
|
||||
getFilterOptions: PropTypes.func.isRequired,
|
||||
selectedOptions: PropTypes.array,
|
||||
isInModal: PropTypes.bool,
|
||||
position: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SelectOptionGroup;
|
@ -4,6 +4,8 @@ import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { processor } from '@seafile/seafile-editor';
|
||||
import '../../css/notice-item.css';
|
||||
|
||||
const propTypes = {
|
||||
noticeItem: PropTypes.object.isRequired,
|
||||
@ -17,12 +19,16 @@ const MSG_TYPE_REPO_SHARE_TO_GROUP = 'repo_share_to_group';
|
||||
const MSG_TYPE_REPO_TRANSFER = 'repo_transfer';
|
||||
const MSG_TYPE_FILE_UPLOADED = 'file_uploaded';
|
||||
const MSG_TYPE_FOLDER_UPLOADED = 'folder_uploaded';
|
||||
const MSG_TYPE_FILE_COMMENT = 'file_comment';
|
||||
// const MSG_TYPE_GUEST_INVITATION_ACCEPTED = 'guest_invitation_accepted';
|
||||
const MSG_TYPE_REPO_MONITOR = 'repo_monitor';
|
||||
const MSG_TYPE_DELETED_FILES = 'deleted_files';
|
||||
const MSG_TYPE_SAML_SSO_FAILED = 'saml_sso_failed';
|
||||
const MSG_TYPE_REPO_SHARE_PERM_CHANGE = 'repo_share_perm_change';
|
||||
const MSG_TYPE_REPO_SHARE_PERM_DELETE = 'repo_share_perm_delete';
|
||||
const MSG_TYPE_FACE_CLUSTER = 'face_cluster';
|
||||
const MSG_TYPE_SEADOC_REPLY = 'reply';
|
||||
const MSG_TYPE_SEADOC_COMMENT = 'comment';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@ -33,36 +39,44 @@ class NoticeItem extends React.Component {
|
||||
let noticeType = noticeItem.type;
|
||||
let detail = noticeItem.detail;
|
||||
|
||||
if (noticeType === MSG_TYPE_ADD_USER_TO_GROUP) {
|
||||
|
||||
let avatar_url = detail.group_staff_avatar_url;
|
||||
|
||||
let groupStaff = detail.group_staff_name;
|
||||
|
||||
// group name does not support special characters
|
||||
let userHref = siteRoot + 'profile/' + detail.group_staff_email + '/';
|
||||
let groupHref = siteRoot + 'group/' + detail.group_id + '/';
|
||||
let groupName = detail.group_name;
|
||||
|
||||
let notice = gettext('User {user_link} has added you to {group_link}');
|
||||
let userLink = '<a href=' + userHref + '>' + groupStaff + '</a>';
|
||||
let groupLink = '<a href=' + groupHref + '>' + groupName + '</a>';
|
||||
|
||||
notice = notice.replace('{user_link}', userLink);
|
||||
notice = notice.replace('{group_link}', groupLink);
|
||||
|
||||
if (noticeType === MSG_TYPE_FILE_COMMENT) {
|
||||
let avatar_url = detail.author_avatar_url;
|
||||
let author = detail.author_name;
|
||||
let fileName = detail.file_name;
|
||||
let fileUrl = siteRoot + 'lib/' + detail.repo_id + '/' + 'file' + detail.file_path;
|
||||
// 1. handle translate
|
||||
let notice = gettext('File {file_link} has a new comment form user {author}.');
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{file_link}', `{tagA}${fileName}{/tagA}`);
|
||||
notice = notice.replace('{author}', author);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(fileUrl)}>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
return { avatar_url, notice };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_ADD_USER_TO_GROUP) {
|
||||
let avatar_url = detail.group_staff_avatar_url;
|
||||
let groupStaff = detail.group_staff_name;
|
||||
// group name does not support special characters
|
||||
let userHref = siteRoot + 'profile/' + encodeURIComponent(detail.group_staff_email) + '/';
|
||||
let groupHref = siteRoot + 'group/' + detail.group_id + '/';
|
||||
let groupName = detail.group_name;
|
||||
let username = detail.group_staff_name;
|
||||
let notice = gettext('User {user_link} has added you to {group_link}');
|
||||
let userLink = '<a href=' + userHref + '>' + Utils.HTMLescape(groupStaff) + '</a>';
|
||||
let groupLink = '<a href=' + groupHref + '>' + Utils.HTMLescape(groupName) + '</a>';
|
||||
notice = notice.replace('{user_link}', userLink);
|
||||
notice = notice.replace('{group_link}', groupLink);
|
||||
return { avatar_url, notice, username };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_SHARE) {
|
||||
|
||||
let avatar_url = detail.share_from_user_avatar_url;
|
||||
|
||||
let shareFrom = detail.share_from_user_name;
|
||||
|
||||
let repoName = detail.repo_name;
|
||||
let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/';
|
||||
|
||||
let path = detail.path;
|
||||
let notice = '';
|
||||
// 1. handle translate
|
||||
@ -71,21 +85,17 @@ class NoticeItem extends React.Component {
|
||||
} else { // share folder
|
||||
notice = gettext('{share_from} has shared a folder named {repo_link} to you.');
|
||||
}
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{share_from}', shareFrom);
|
||||
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: shareFrom };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_SHARE_PERM_CHANGE) {
|
||||
|
||||
let avatar_url = detail.share_from_user_avatar_url;
|
||||
let shareFrom = detail.share_from_user_name;
|
||||
let permission = detail.permission;
|
||||
@ -99,22 +109,18 @@ class NoticeItem extends React.Component {
|
||||
} else { // share folder
|
||||
notice = gettext('{share_from} has changed the permission of folder {repo_link} to {permission}.');
|
||||
}
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{share_from}', shareFrom);
|
||||
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
|
||||
notice = notice.replace('{permission}', permission);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: shareFrom };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_SHARE_PERM_DELETE) {
|
||||
|
||||
let avatar_url = detail.share_from_user_avatar_url;
|
||||
let shareFrom = detail.share_from_user_name;
|
||||
let repoName = detail.repo_name;
|
||||
@ -126,26 +132,20 @@ class NoticeItem extends React.Component {
|
||||
} else { // share folder
|
||||
notice = gettext('{share_from} has cancelled the sharing of folder {repo_name}.');
|
||||
}
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{share_from}', shareFrom);
|
||||
notice = notice.replace('{repo_name}', repoName);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: shareFrom };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_SHARE_TO_GROUP) {
|
||||
|
||||
let avatar_url = detail.share_from_user_avatar_url;
|
||||
|
||||
let shareFrom = detail.share_from_user_name;
|
||||
|
||||
let repoName = detail.repo_name;
|
||||
let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/';
|
||||
|
||||
let groupUrl = siteRoot + 'group/' + detail.group_id + '/';
|
||||
let groupName = detail.group_name;
|
||||
|
||||
let path = detail.path;
|
||||
let notice = '';
|
||||
// 1. handle translate
|
||||
@ -154,60 +154,50 @@ class NoticeItem extends React.Component {
|
||||
} else {
|
||||
notice = gettext('{share_from} has shared a folder named {repo_link} to group {group_link}.');
|
||||
}
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{share_from}', shareFrom);
|
||||
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
|
||||
notice = notice.replace('{group_link}', `{tagB}${groupName}{/tagB}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
notice = notice.replace('{tagB}', `<a href='${Utils.encodePath(groupUrl)}'>`);
|
||||
notice = notice.replace('{/tagB}', '</a>');
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: shareFrom };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_TRANSFER) {
|
||||
|
||||
let avatar_url = detail.transfer_from_user_avatar_url;
|
||||
|
||||
let repoOwner = detail.transfer_from_user_name;
|
||||
|
||||
let repoName = detail.repo_name;
|
||||
let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/';
|
||||
// 1. handle translate
|
||||
let notice = gettext('{user} has transfered a library named {repo_link} to you.');
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{user}', repoOwner);
|
||||
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(repoUrl)}>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: repoOwner };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_FILE_UPLOADED) {
|
||||
let avatar_url = detail.uploaded_user_avatar_url;
|
||||
let fileName = detail.file_name;
|
||||
let fileLink = siteRoot + 'lib/' + detail.repo_id + '/' + 'file' + detail.file_path;
|
||||
|
||||
let folderName = detail.folder_name;
|
||||
let folderLink = siteRoot + 'library/' + detail.repo_id + '/' + detail.repo_name + detail.folder_path;
|
||||
let notice = '';
|
||||
if (detail.repo_id) { // todo is repo exist ?
|
||||
// 1. handle translate
|
||||
notice = gettext('A file named {upload_file_link} is uploaded to {uploaded_link}.');
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{upload_file_link}', `{tagA}${fileName}{/tagA}`);
|
||||
notice = notice.replace('{uploaded_link}', `{tagB}${folderName}{/tagB}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(fileLink)}>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
@ -216,7 +206,6 @@ class NoticeItem extends React.Component {
|
||||
} else {
|
||||
// 1. handle translate
|
||||
notice = gettext('A file named {upload_file_link} is uploaded.');
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{upload_file_link}', `${fileName}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
@ -340,15 +329,22 @@ class NoticeItem extends React.Component {
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_DELETED_FILES) {
|
||||
const {
|
||||
repo_id,
|
||||
repo_name,
|
||||
} = detail;
|
||||
const { repo_id, repo_name } = detail;
|
||||
const repoURL = `${siteRoot}library/${repo_id}/${encodeURIComponent(repo_name)}/`;
|
||||
const repoLink = `<a href=${repoURL} target="_blank">${Utils.HTMLescape(repo_name)}</a>`;
|
||||
let notice = gettext('Your library {libraryName} has recently deleted a large number of files.');
|
||||
notice = notice.replace('{libraryName}', repoLink);
|
||||
return { avatar_url: null, notice };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_FACE_CLUSTER) {
|
||||
let repo_id = detail.repo_id;
|
||||
let repo_name = detail.repo_name;
|
||||
|
||||
const repoURL = `${siteRoot}library/${repo_id}/${encodeURIComponent(repo_name)}/`;
|
||||
const repoLink = `<a href=${repoURL} target="_blank">${Utils.HTMLescape(repo_name)}</a>`;
|
||||
|
||||
let notice = gettext('Your library {libraryName} has recently deleted a large number of files.');
|
||||
let notice = gettext('Face recognition is done for library {libraryName}.');
|
||||
notice = notice.replace('{libraryName}', repoLink);
|
||||
|
||||
return { avatar_url: null, notice };
|
||||
@ -357,15 +353,70 @@ class NoticeItem extends React.Component {
|
||||
if (noticeType === MSG_TYPE_SAML_SSO_FAILED) {
|
||||
const { error_msg } = detail;
|
||||
let notice = gettext(error_msg);
|
||||
|
||||
return { avatar_url: null, notice };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_SEADOC_COMMENT) {
|
||||
let avatar_url = detail.avatar_url;
|
||||
let notice = detail.comment;
|
||||
let username = detail.user_name;
|
||||
let is_resolved = detail.is_resolved;
|
||||
let sdoc_name = detail.sdoc_name;
|
||||
const repo_id = detail.repo_id;
|
||||
const sdoc_path = detail.sdoc_path;
|
||||
const sdoc_href = siteRoot + 'lib/' + repo_id + '/file' + sdoc_path;
|
||||
let sdoc_link = '<a href=' + sdoc_href + '>' + sdoc_name + '</a>';
|
||||
processor.process(notice, (error, vfile) => {
|
||||
notice = String(vfile);
|
||||
});
|
||||
if (is_resolved) {
|
||||
if (detail.resolve_comment && detail.resolve_comment !== '\u200B') {
|
||||
notice = gettext('Marked "{resolve_comment}" as resolved in document {sdoc_link}');
|
||||
notice = notice.replace('{resolve_comment}', detail.resolve_comment);
|
||||
notice = notice.replace('{sdoc_link}', sdoc_link);
|
||||
} else {
|
||||
notice = gettext('Marked as resolved in document {sdoc_link}');
|
||||
notice = notice.replace('{sdoc_link}', sdoc_link);
|
||||
}
|
||||
} else {
|
||||
notice = gettext('Added a new comment in document {sdoc_link}:').replace('{sdoc_link}', sdoc_link) + notice;
|
||||
}
|
||||
return { avatar_url, username, notice };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_SEADOC_REPLY) {
|
||||
let avatar_url = detail.avatar_url;
|
||||
let notice = detail.reply;
|
||||
let username = detail.user_name;
|
||||
let is_resolved = detail.is_resolved;
|
||||
let sdoc_name = detail.sdoc_name;
|
||||
const repo_id = detail.repo_id;
|
||||
const sdoc_path = detail.sdoc_path;
|
||||
const sdoc_href = siteRoot + 'lib/' + repo_id + '/file' + sdoc_path;
|
||||
let sdoc_link = '<a href=' + sdoc_href + '>' + sdoc_name + '</a>';
|
||||
processor.process(notice, (error, vfile) => {
|
||||
notice = String(vfile);
|
||||
});
|
||||
if (is_resolved) {
|
||||
if (detail.resolve_comment && detail.resolve_comment !== '\u200B') {
|
||||
notice = gettext('Marked "{resolve_comment}" as resolved in document {sdoc_link}');
|
||||
notice = notice.replace('{resolve_comment}', detail.resolve_comment);
|
||||
notice = notice.replace('{sdoc_link}', sdoc_link);
|
||||
} else {
|
||||
notice = gettext('Marked as resolved in document {sdoc_link}');
|
||||
notice = notice.replace('{sdoc_link}', sdoc_link);
|
||||
}
|
||||
} else {
|
||||
notice = gettext('Added a new reply in document {sdoc_link}:').replace('{sdoc_link}', sdoc_link) + notice;
|
||||
}
|
||||
return { avatar_url, username, notice };
|
||||
}
|
||||
|
||||
// if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) {
|
||||
|
||||
// }
|
||||
|
||||
return { avatar_url: null, notice: null };
|
||||
return { avatar_url: null, notice: null, username: null };
|
||||
}
|
||||
|
||||
onNoticeItemClick = () => {
|
||||
@ -378,16 +429,19 @@ class NoticeItem extends React.Component {
|
||||
|
||||
render() {
|
||||
let noticeItem = this.props.noticeItem;
|
||||
let { avatar_url, notice } = this.generatorNoticeInfo();
|
||||
|
||||
let { avatar_url, username, notice } = this.generatorNoticeInfo();
|
||||
if (!avatar_url && !notice) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.props.tr ? (
|
||||
<tr className={noticeItem.seen ? 'read' : 'unread font-weight-bold'}>
|
||||
<tr className='notification-item'>
|
||||
<td className="text-center">
|
||||
{!noticeItem.seen && <span className="notification-point" onClick={this.onMarkNotificationRead}></span>}
|
||||
</td>
|
||||
<td>
|
||||
<img src={avatar_url} width="32" height="32" className="avatar" alt="" />
|
||||
<span className="ml-2 notification-user-name">{username || gettext('System')}</span>
|
||||
</td>
|
||||
<td className="pr-1 pr-md-8">
|
||||
<p className="m-0" dangerouslySetInnerHTML={{ __html: notice }}></p>
|
||||
@ -397,13 +451,21 @@ class NoticeItem extends React.Component {
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<li onClick={this.onNoticeItemClick} className={noticeItem.seen ? 'read' : 'unread'}>
|
||||
<div className="notice-item">
|
||||
<div className="main-info">
|
||||
<img src={avatar_url} width="32" height="32" className="avatar" alt=""/>
|
||||
<p className="brief" dangerouslySetInnerHTML={{ __html: notice }}></p>
|
||||
<li className='notification-item' onClick={this.onNoticeItemClick}>
|
||||
<div className="notification-item-header">
|
||||
{!noticeItem.seen &&
|
||||
<span className="notification-point" onClick={this.onMarkNotificationRead}></span>
|
||||
}
|
||||
<div className="notification-header-info">
|
||||
<div className="notification-user-detail">
|
||||
<img className="notification-user-avatar" src={avatar_url} alt="" />
|
||||
<span className="ml-2 notification-user-name">{username || gettext('System')}</span>
|
||||
</div>
|
||||
<span className="notification-time">{dayjs(noticeItem.time).fromNow()}</span>
|
||||
</div>
|
||||
<p className="time">{dayjs(noticeItem.time).fromNow()}</p>
|
||||
</div>
|
||||
<div className="notification-content-wrapper">
|
||||
<div dangerouslySetInnerHTML={{ __html: notice }}></div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
@ -5,7 +5,7 @@
|
||||
.notification-container {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
width: 320px;
|
||||
width: 400px;
|
||||
right: -16px;
|
||||
top: -1px;
|
||||
border-radius: 3px;
|
||||
@ -27,22 +27,12 @@
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-container .notification-header .notification-close-icon {
|
||||
.notification-container .notification-header .seahub-modal-btn {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
opacity: 0.5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.notification-container .notification-header .notification-close-icon:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.notification-container .notification-body {
|
||||
@ -65,21 +55,15 @@
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .mark-notifications {
|
||||
color: #b4b4b4;
|
||||
.notification-container .mark-all-read {
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #ededed;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .mark-notifications:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-body .notification-list-container {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
@ -190,3 +174,33 @@
|
||||
.notification-body .notification-footer:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .mark-notifications {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #ededed;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .mark-notifications .mark-all-read:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .nav .nav-item .nav-link {
|
||||
height: 46px;
|
||||
margin-right: 15px;
|
||||
margin-left: 15px;
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .nav .nav-item .nav-link.active {
|
||||
color: #ED7109;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notification-container {
|
||||
right: -60px;
|
||||
width: 360px;
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Popover } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import SeahubModalCloseIcon from '../seahub-modal-close';
|
||||
|
||||
import './index.css';
|
||||
|
||||
export default class NotificationPopover extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
headerText: PropTypes.string.isRequired,
|
||||
bodyText: PropTypes.string.isRequired,
|
||||
footerText: PropTypes.string.isRequired,
|
||||
onNotificationListToggle: PropTypes.func,
|
||||
onNotificationDialogToggle: PropTypes.func,
|
||||
listNotifications: PropTypes.func,
|
||||
onMarkAllNotifications: PropTypes.func,
|
||||
children: PropTypes.any,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
headerText: '',
|
||||
bodyText: '',
|
||||
footerText: '',
|
||||
};
|
||||
class NotificationPopover extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mousedown', this.handleOutsideClick, true);
|
||||
@ -47,8 +33,12 @@ export default class NotificationPopover extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
tabItemClick = (tab) => {
|
||||
this.props.tabItemClick(tab);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { headerText, bodyText, footerText } = this.props;
|
||||
const { headerText = '', bodyText = '', footerText = '', currentTab, generalNoticeListUnseen, discussionNoticeListUnseen } = this.props;
|
||||
return (
|
||||
<Popover
|
||||
className="notification-wrapper"
|
||||
@ -59,17 +49,44 @@ export default class NotificationPopover extends React.Component {
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="notification-container" ref={ref => this.notificationContainerRef = ref}>
|
||||
<div className="notification-header">
|
||||
<div className="notification-header modal">
|
||||
{headerText}
|
||||
<span className="sf3-font sf3-font-x-01 notification-close-icon" onClick={this.props.onNotificationListToggle}></span>
|
||||
<SeahubModalCloseIcon toggle={this.props.onNotificationListToggle} />
|
||||
</div>
|
||||
<div className="notification-body">
|
||||
<div className="mark-notifications" onClick={this.props.onMarkAllNotifications}>{bodyText}</div>
|
||||
<div className="mark-notifications">
|
||||
<ul className="nav">
|
||||
<li className="nav-item" onClick={() => this.tabItemClick('general')}>
|
||||
<span className={`nav-link ${currentTab === 'general' ? 'active' : ''}`}>
|
||||
{gettext('General')}
|
||||
{generalNoticeListUnseen > 0 && <span>({generalNoticeListUnseen})</span>}
|
||||
</span>
|
||||
</li>
|
||||
<li className="nav-item" onClick={() => this.tabItemClick('discussion')}>
|
||||
<span className={`nav-link ${currentTab === 'discussion' ? 'active' : ''}`}>
|
||||
{gettext('Discussion')}
|
||||
{discussionNoticeListUnseen > 0 && <span>({discussionNoticeListUnseen})</span>}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<span className="mark-all-read" onClick={this.props.onMarkAllNotifications}>
|
||||
{bodyText}
|
||||
</span>
|
||||
</div>
|
||||
{currentTab === 'general' &&
|
||||
<div className="notification-list-container" onScroll={this.onHandleScroll} ref={ref => this.notificationListRef = ref}>
|
||||
<div ref={ref => this.notificationsWrapperRef = ref}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{currentTab === 'discussion' &&
|
||||
<div className="notification-list-container" onScroll={this.onHandleScroll} ref={ref => this.notificationListRef = ref}>
|
||||
<div ref={ref => this.notificationsWrapperRef = ref}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="notification-footer" onClick={this.onNotificationDialogToggle}>{footerText}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,3 +94,20 @@ export default class NotificationPopover extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationPopover.propTypes = {
|
||||
headerText: PropTypes.string.isRequired,
|
||||
bodyText: PropTypes.string.isRequired,
|
||||
footerText: PropTypes.string.isRequired,
|
||||
onNotificationListToggle: PropTypes.func,
|
||||
onNotificationDialogToggle: PropTypes.func,
|
||||
listNotifications: PropTypes.func,
|
||||
onMarkAllNotifications: PropTypes.func,
|
||||
tabItemClick: PropTypes.func,
|
||||
children: PropTypes.any,
|
||||
currentTab: PropTypes.string,
|
||||
generalNoticeListUnseen: PropTypes.number,
|
||||
discussionNoticeListUnseen: PropTypes.number,
|
||||
};
|
||||
|
||||
export default NotificationPopover;
|
||||
|
@ -12,15 +12,21 @@ class Notification extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
showNotice: false,
|
||||
unseenCount: 0,
|
||||
noticeList: [],
|
||||
totalUnseenCount: 0,
|
||||
generalNoticeList: [],
|
||||
discussionNoticeList: [],
|
||||
currentTab: 'general',
|
||||
isShowNotificationDialog: this.getInitDialogState(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.getUnseenNotificationCount().then(res => {
|
||||
this.setState({ unseenCount: res.data.unseen_count });
|
||||
seafileAPI.listAllNotifications().then(res => {
|
||||
this.setState({
|
||||
totalUnseenCount: res.data.total_unseen_count,
|
||||
generalNoticeListUnseen: res.data.general.unseen_count,
|
||||
discussionNoticeListUnseen: res.data.discussion.unseen_count
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -30,7 +36,7 @@ class Notification extends React.Component {
|
||||
seafileAPI.updateNotifications();
|
||||
this.setState({
|
||||
showNotice: false,
|
||||
unseenCount: 0
|
||||
totalUnseenCount: 0
|
||||
});
|
||||
} else {
|
||||
this.loadNotices();
|
||||
@ -38,28 +44,65 @@ class Notification extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
tabItemClick = (tab) => {
|
||||
const { currentTab } = this.state;
|
||||
if (currentTab === tab) return;
|
||||
this.setState({
|
||||
showNotice: true,
|
||||
currentTab: tab
|
||||
});
|
||||
};
|
||||
|
||||
loadNotices = () => {
|
||||
let page = 1;
|
||||
let perPage = 5;
|
||||
seafileAPI.listNotifications(page, perPage).then(res => {
|
||||
let noticeList = res.data.notification_list;
|
||||
this.setState({ noticeList: noticeList });
|
||||
let perPage = 25;
|
||||
seafileAPI.listAllNotifications(page, perPage).then(res => {
|
||||
let generalNoticeList = res.data.general.notification_list;
|
||||
let discussionNoticeList = res.data.discussion.notification_list;
|
||||
let generalNoticeListUnseen = res.data.general.unseen_count;
|
||||
let discussionNoticeListUnseen = res.data.discussion.unseen_count;
|
||||
this.setState({
|
||||
generalNoticeList: generalNoticeList,
|
||||
discussionNoticeList: discussionNoticeList,
|
||||
generalNoticeListUnseen: generalNoticeListUnseen,
|
||||
discussionNoticeListUnseen: discussionNoticeListUnseen
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onNoticeItemClick = (noticeItem) => {
|
||||
let noticeList = this.state.noticeList.map(item => {
|
||||
if (item.id === noticeItem.id) {
|
||||
item.seen = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
seafileAPI.markNoticeAsRead(noticeItem.id);
|
||||
let unseenCount = this.state.unseenCount === 0 ? 0 : this.state.unseenCount - 1;
|
||||
this.setState({
|
||||
noticeList: noticeList,
|
||||
unseenCount: unseenCount,
|
||||
});
|
||||
if (this.state.currentTab === 'general') {
|
||||
let noticeList = this.state.generalNoticeList.map(item => {
|
||||
if (item.id === noticeItem.id) {
|
||||
item.seen = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
let totalUnseenCount = this.state.totalUnseenCount === 0 ? 0 : this.state.totalUnseenCount - 1;
|
||||
let generalNoticeListUnseen = this.state.generalNoticeListUnseen === 0 ? 0 : this.state.generalNoticeListUnseen - 1;
|
||||
this.setState({
|
||||
generalNoticeList: noticeList,
|
||||
totalUnseenCount: totalUnseenCount,
|
||||
generalNoticeListUnseen: generalNoticeListUnseen
|
||||
});
|
||||
seafileAPI.markNoticeAsRead(noticeItem.id);
|
||||
}
|
||||
if (this.state.currentTab === 'discussion') {
|
||||
let noticeList = this.state.discussionNoticeList.map(item => {
|
||||
if (item.id === noticeItem.id) {
|
||||
item.seen = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
let totalUnseenCount = this.state.totalUnseenCount === 0 ? 0 : this.state.totalUnseenCount - 1;
|
||||
let discussionNoticeListUnseen = this.state.discussionNoticeListUnseen === 0 ? 0 : this.state.discussionNoticeListUnseen - 1;
|
||||
this.setState({
|
||||
discussionNoticeList: noticeList,
|
||||
totalUnseenCount: totalUnseenCount,
|
||||
discussionNoticeListUnseen: discussionNoticeListUnseen
|
||||
});
|
||||
seafileAPI.markSdocNoticeAsRead(noticeItem.id);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -79,43 +122,103 @@ class Notification extends React.Component {
|
||||
};
|
||||
|
||||
onMarkAllNotifications = () => {
|
||||
seafileAPI.updateNotifications().then(() => {
|
||||
this.setState({
|
||||
unseenCount: 0,
|
||||
let generalNoticeListUnseen = this.state.generalNoticeListUnseen;
|
||||
let discussionNoticeListUnseen = this.state.discussionNoticeListUnseen;
|
||||
if (this.state.currentTab === 'general') {
|
||||
seafileAPI.updateNotifications().then((res) => {
|
||||
this.setState({
|
||||
generalNoticeList: this.state.generalNoticeList.map(item => {
|
||||
item.seen = true;
|
||||
return item;
|
||||
}),
|
||||
generalNoticeListUnseen: 0,
|
||||
totalUnseenCount: discussionNoticeListUnseen
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
errorMsg: Utils.getErrorMsg(error, true)
|
||||
} else if (this.state.currentTab === 'discussion') {
|
||||
seafileAPI.updateSdocNotifications().then((res) => {
|
||||
this.setState({
|
||||
discussionNoticeList: this.state.discussionNoticeList.map(item => {
|
||||
item.seen = true;
|
||||
return item;
|
||||
}),
|
||||
discussionNoticeListUnseen: 0,
|
||||
totalUnseenCount: generalNoticeListUnseen
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateTotalUnseenCount = (noticeType) => {
|
||||
if (noticeType === 'general') {
|
||||
this.setState({
|
||||
generalNoticeListUnseen: 0,
|
||||
totalUnseenCount: this.state.discussionNoticeListUnseen
|
||||
});
|
||||
} else if (noticeType === 'discussion') {
|
||||
this.setState({
|
||||
discussionNoticeListUnseen: 0,
|
||||
totalUnseenCount: this.state.generalNoticeListUnseen
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { unseenCount } = this.state;
|
||||
const { totalUnseenCount, currentTab, generalNoticeList, discussionNoticeList, generalNoticeListUnseen, discussionNoticeListUnseen } = this.state;
|
||||
return (
|
||||
<div id="notifications">
|
||||
<a href="#" onClick={this.onClick} className="no-deco" id="notice-icon" title={gettext('Notifications')} aria-label={gettext('Notifications')}>
|
||||
<span className="sf2-icon-bell" id="notification-popover"></span>
|
||||
<span className={`num ${unseenCount ? '' : 'hide'}`}>{unseenCount}</span>
|
||||
<span className={`num ${totalUnseenCount ? '' : 'hide'}`}>{totalUnseenCount}</span>
|
||||
</a>
|
||||
{this.state.showNotice &&
|
||||
<NotificationPopover
|
||||
headerText={gettext('Notification')}
|
||||
bodyText={gettext('Mark all as read')}
|
||||
footerText={gettext('View all notifications')}
|
||||
currentTab={currentTab}
|
||||
onNotificationListToggle={this.onNotificationListToggle}
|
||||
onNotificationDialogToggle={this.onNotificationDialogToggle}
|
||||
onMarkAllNotifications={this.onMarkAllNotifications}
|
||||
tabItemClick={this.tabItemClick}
|
||||
generalNoticeListUnseen={generalNoticeListUnseen}
|
||||
discussionNoticeListUnseen={discussionNoticeListUnseen}
|
||||
>
|
||||
<ul className="notice-list list-unstyled" id="notice-popover">
|
||||
{this.state.noticeList.map(item => {
|
||||
return (<NoticeItem key={item.id} noticeItem={item} onNoticeItemClick={this.onNoticeItemClick}/>);
|
||||
})}
|
||||
</ul>
|
||||
{currentTab === 'general' &&
|
||||
<ul className="notice-list list-unstyled" id="notice-popover">
|
||||
{generalNoticeList.map(item => {
|
||||
return (
|
||||
<NoticeItem key={item.id} noticeItem={item} onNoticeItemClick={this.onNoticeItemClick}/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
{currentTab === 'discussion' &&
|
||||
<ul className="notice-list list-unstyled" id="notice-popover">
|
||||
{discussionNoticeList.map(item => {
|
||||
return (
|
||||
<NoticeItem key={item.id} noticeItem={item} onNoticeItemClick={this.onNoticeItemClick}/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
</NotificationPopover>
|
||||
}
|
||||
{this.state.isShowNotificationDialog &&
|
||||
<UserNotificationsDialog onNotificationDialogToggle={this.onNotificationDialogToggle} />
|
||||
<UserNotificationsDialog
|
||||
onNotificationDialogToggle={this.onNotificationDialogToggle}
|
||||
generalNoticeListUnseen={generalNoticeListUnseen}
|
||||
discussionNoticeListUnseen={discussionNoticeListUnseen}
|
||||
updateTotalUnseenCount={this.updateTotalUnseenCount}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
15
frontend/src/components/common/seahub-modal-close.js
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import '../../css/seahub-modal-header.css';
|
||||
|
||||
const SeahubModalCloseIcon = (props) => {
|
||||
return (
|
||||
<button type="button" className={`close seahub-modal-btn ${props.className ? props.className : ''}`} data-dismiss="modal" aria-label={gettext('Close')} onClick={props.toggle}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeahubModalCloseIcon;
|
21
frontend/src/components/common/seahub-modal-header.js
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { ModalHeader } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import '../../css/seahub-modal-header.css';
|
||||
|
||||
const SeahubModalHeader = ({ children, ...props }) => {
|
||||
const customCloseBtn = (
|
||||
<button type="button" className="close seahub-modal-btn" data-dismiss="modal" aria-label={gettext('Close')} onClick={props.toggle}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<ModalHeader {...props} close={customCloseBtn}>
|
||||
{children}
|
||||
</ModalHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeahubModalHeader;
|
@ -1,5 +1,4 @@
|
||||
import SeahubSelect from './seahub-select';
|
||||
import { NoGroupMessage } from './no-group-message';
|
||||
import { MenuSelectStyle, UserSelectStyle, NoOptionsStyle } from './seahub-select-style';
|
||||
|
||||
export { SeahubSelect, NoGroupMessage, MenuSelectStyle, UserSelectStyle, NoOptionsStyle };
|
||||
export { SeahubSelect, MenuSelectStyle, UserSelectStyle, NoOptionsStyle };
|
||||
|
@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { NoOptionsStyle } from './seahub-select-style';
|
||||
|
||||
const NoGroupMessage = (props) => {
|
||||
return (
|
||||
<div {...props.innerProps} style={NoOptionsStyle}>{gettext('Group not found')}</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoGroupMessage.propTypes = {
|
||||
innerProps: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
export { NoGroupMessage };
|
@ -1,3 +1,44 @@
|
||||
// Seahub select is based on seafile-ui.css, so use the following content to override the default react-select style
|
||||
const DEFAULT_CONTROL_STYLE = {
|
||||
border: '1px solid rgba(0, 40, 100, 0.12) !important',
|
||||
};
|
||||
|
||||
const FOCUS_CONTROL_STYLE = {
|
||||
fontSize: '14px',
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#1991eb',
|
||||
outline: '0',
|
||||
boxShadow: '0 0 0 2px rgba(70, 127, 207, 0.25)',
|
||||
};
|
||||
|
||||
const noneCallback = () => ({
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
const controlCallback = (provided, state) => {
|
||||
const { isDisabled, isFocused } = state;
|
||||
if (isFocused && !isDisabled) {
|
||||
return {
|
||||
...provided,
|
||||
...FOCUS_CONTROL_STYLE,
|
||||
'&:hover': {
|
||||
...provided,
|
||||
...FOCUS_CONTROL_STYLE,
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...provided,
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
cursor: 'pointer',
|
||||
...DEFAULT_CONTROL_STYLE,
|
||||
'&:hover': {
|
||||
...DEFAULT_CONTROL_STYLE,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const MenuSelectStyle = {
|
||||
option: (provided, state) => {
|
||||
const { isDisabled, isSelected, isFocused } = state;
|
||||
@ -10,14 +51,9 @@ const MenuSelectStyle = {
|
||||
},
|
||||
});
|
||||
},
|
||||
control: (provided) => ({
|
||||
...provided,
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
lineHeight: '1.5',
|
||||
}),
|
||||
control: controlCallback,
|
||||
menuPortal: base => ({ ...base, zIndex: 9999 }),
|
||||
indicatorSeparator: () => {},
|
||||
indicatorSeparator: noneCallback,
|
||||
};
|
||||
|
||||
const UserSelectStyle = {
|
||||
@ -29,21 +65,10 @@ const UserSelectStyle = {
|
||||
backgroundColor: isFocused ? '#f5f5f5' : '#fff',
|
||||
});
|
||||
},
|
||||
control: (provided) => ({
|
||||
...provided,
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
lineHeight: '1.5',
|
||||
}),
|
||||
indicatorSeparator: () => ({
|
||||
display: 'none',
|
||||
}),
|
||||
dropdownIndicator: () => ({
|
||||
display: 'none',
|
||||
}),
|
||||
clearIndicator: () => ({
|
||||
display: 'none',
|
||||
}),
|
||||
control: controlCallback,
|
||||
indicatorSeparator: noneCallback,
|
||||
dropdownIndicator: noneCallback,
|
||||
clearIndicator: noneCallback,
|
||||
// multi select style
|
||||
multiValue: (provided) => {
|
||||
return {
|
||||
|
@ -1,15 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select, { components, createFilter } from 'react-select';
|
||||
import Select, { components } from 'react-select';
|
||||
import { MenuSelectStyle } from './seahub-select-style';
|
||||
|
||||
const DropdownIndicator = props => {
|
||||
return (
|
||||
components.DropdownIndicator && (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<span className="sf3-font sf3-font-down" style={{ fontSize: '12px', marginLeft: '-2px' }} aria-hidden="true"></span>
|
||||
</components.DropdownIndicator>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const ClearIndicator = ({ innerProps, ...props }) => {
|
||||
const onMouseDown = e => {
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
innerProps.onMouseDown(e);
|
||||
};
|
||||
props.innerProps = { ...innerProps, onMouseDown };
|
||||
return <components.ClearIndicator {...props} />;
|
||||
return (
|
||||
<components.ClearIndicator {...props} >
|
||||
<span className="sf3-font sf3-font-x-01" style={{ fontSize: '12px', marginLeft: '-2px' }} aria-hidden="true"></span>
|
||||
</components.ClearIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
ClearIndicator.propTypes = {
|
||||
@ -40,47 +54,17 @@ Option.propTypes = {
|
||||
}),
|
||||
};
|
||||
|
||||
export default class SeahubSelect extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
isMulti: PropTypes.bool,
|
||||
options: PropTypes.array.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string]),
|
||||
isSearchable: PropTypes.bool,
|
||||
isClearable: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
classNamePrefix: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
form: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
menuPortalTarget: PropTypes.string,
|
||||
menuPosition: PropTypes.string,
|
||||
noOptionsMessage: PropTypes.func,
|
||||
innerRef: PropTypes.object,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
options: [],
|
||||
value: {},
|
||||
isDisabled: false,
|
||||
isSearchable: false,
|
||||
isClearable: false,
|
||||
placeholder: '',
|
||||
isMulti: false,
|
||||
menuPortalTarget: '.modal',
|
||||
noOptionsMessage: () => {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
class SeahubSelect extends React.Component {
|
||||
|
||||
getMenuPortalTarget = () => {
|
||||
return document.querySelector(this.props.menuPortalTarget);
|
||||
const { menuPortalTarget = '.modal' } = this.props;
|
||||
return document.querySelector(menuPortalTarget);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, onChange, value, isSearchable, placeholder, isMulti, menuPosition, isClearable, noOptionsMessage,
|
||||
classNamePrefix, innerRef, isDisabled, form, className } = this.props;
|
||||
const { options = [], onChange, value = {}, isSearchable = true, placeholder = '',
|
||||
isMulti = false, menuPosition, isClearable = true, noOptionsMessage = (() => { return null; }),
|
||||
classNamePrefix, innerRef, isDisabled = false, form, className } = this.props;
|
||||
|
||||
return (
|
||||
<Select
|
||||
@ -93,11 +77,7 @@ export default class SeahubSelect extends React.Component {
|
||||
className={className}
|
||||
classNamePrefix={classNamePrefix}
|
||||
styles={MenuSelectStyle}
|
||||
components={{ Option, MenuList, ClearIndicator }}
|
||||
filterOption={createFilter({
|
||||
matchFrom: 'any',
|
||||
stringify: option => `${option.data.labelValue}`,
|
||||
})}
|
||||
components={{ Option, DropdownIndicator, MenuList, ClearIndicator }}
|
||||
placeholder={placeholder}
|
||||
isSearchable={isSearchable}
|
||||
isClearable={isClearable}
|
||||
@ -111,3 +91,23 @@ export default class SeahubSelect extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SeahubSelect.propTypes = {
|
||||
isMulti: PropTypes.bool,
|
||||
options: PropTypes.array.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string]),
|
||||
isSearchable: PropTypes.bool,
|
||||
isClearable: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
classNamePrefix: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
form: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
menuPortalTarget: PropTypes.string,
|
||||
menuPosition: PropTypes.string,
|
||||
noOptionsMessage: PropTypes.func,
|
||||
innerRef: PropTypes.object,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SeahubSelect;
|
||||
|
@ -228,7 +228,7 @@ class ContextMenu extends React.Component {
|
||||
onMouseMove={(e) => {e.stopPropagation();}}
|
||||
>
|
||||
<DropdownToggle
|
||||
tag='div'
|
||||
tag='span'
|
||||
className="dropdown-item font-weight-normal rounded-0 d-flex align-items-center"
|
||||
onMouseEnter={this.toggleSubMenuShown.bind(this, menuItem)}
|
||||
>
|
||||
|
@ -1,11 +1,15 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar';
|
||||
import DirOperationToolbar from '../../components/toolbar/dir-operation-toolbar';
|
||||
import MetadataViewName from '../../metadata/components/metadata-view-name';
|
||||
import TagViewName from '../../tag/components/tag-view-name';
|
||||
import { siteRoot, gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { debounce, Utils } from '../../utils/utils';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { EVENT_BUS_TYPE } from '../../metadata/constants';
|
||||
import { ALL_TAGS_ID } from '../../tag/constants';
|
||||
|
||||
const propTypes = {
|
||||
currentRepoInfo: PropTypes.object.isRequired,
|
||||
@ -29,7 +33,6 @@ const propTypes = {
|
||||
direntList: PropTypes.array.isRequired,
|
||||
repoTags: PropTypes.array.isRequired,
|
||||
filePermission: PropTypes.string,
|
||||
onFileTagChanged: PropTypes.func.isRequired,
|
||||
onItemMove: PropTypes.func.isRequired,
|
||||
loadDirentList: PropTypes.func.isRequired,
|
||||
};
|
||||
@ -114,40 +117,78 @@ class DirPath extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handleRefresh = debounce(() => {
|
||||
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.RELOAD_DATA);
|
||||
}, 200);
|
||||
|
||||
turnViewPathToLink = (pathList) => {
|
||||
if (!Array.isArray(pathList) || pathList.length === 0) return null;
|
||||
const [, , viewId, children] = pathList;
|
||||
return (
|
||||
<>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item path-item-read-only">{gettext('Views')}</span>
|
||||
<span className="path-split">/</span>
|
||||
<span
|
||||
className="path-item path-item-read-only"
|
||||
role={children ? 'button' : null}
|
||||
onClick={children ? this.handleRefresh : () => {}}
|
||||
>
|
||||
<MetadataViewName id={viewId} />
|
||||
</span>
|
||||
{children && (
|
||||
<>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item path-item-read-only">{children}</span>
|
||||
</>
|
||||
)}
|
||||
<div className="path-item-refresh" id="sf-metadata-view-refresh" onClick={this.handleRefresh}>
|
||||
<i className="sf3-font sf3-font-refresh"></i>
|
||||
<UncontrolledTooltip target="sf-metadata-view-refresh" placement="bottom">
|
||||
{gettext('Refresh the view')}
|
||||
</UncontrolledTooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
turnTagPathToLink = (pathList) => {
|
||||
if (!Array.isArray(pathList) || pathList.length === 0) return null;
|
||||
const [, , tagId, children] = pathList;
|
||||
const canSelectAllTags = tagId === ALL_TAGS_ID && !!children;
|
||||
return (
|
||||
<>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item path-item-read-only">{gettext('Tags')}</span>
|
||||
<span className="path-split">/</span>
|
||||
<TagViewName id={tagId} canSelectAllTags={canSelectAllTags} />
|
||||
{children && (
|
||||
<>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item path-item-read-only">{children}</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
turnPathToLink = (path) => {
|
||||
path = path[path.length - 1] === '/' ? path.slice(0, path.length - 1) : path;
|
||||
let pathList = path.split('/');
|
||||
let nodePath = '';
|
||||
if (pathList.length === 2 && !pathList[0] && pathList[1] === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES) {
|
||||
return null;
|
||||
const pathList = path.split('/');
|
||||
if (pathList.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) {
|
||||
return this.turnViewPathToLink(pathList);
|
||||
}
|
||||
if (pathList.includes(PRIVATE_FILE_TYPE.TAGS_PROPERTIES)) {
|
||||
return this.turnTagPathToLink(pathList);
|
||||
}
|
||||
let nodePath = '';
|
||||
let pathElem = pathList.map((item, index) => {
|
||||
if (item === '') {
|
||||
return null;
|
||||
}
|
||||
if (index === pathList.length - 2 && item === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES) {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{gettext('Views')}</span>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (index === pathList.length - 1 && pathList[pathList.length - 2] === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES) {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item"><MetadataViewName id={item} /></span>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (item === '') return null;
|
||||
if (index === (pathList.length - 1)) {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<DirOperationToolBar
|
||||
<DirOperationToolbar
|
||||
path={this.props.currentPath}
|
||||
repoID={this.props.repoID}
|
||||
repoName={this.props.repoName}
|
||||
@ -164,7 +205,7 @@ class DirPath extends React.Component {
|
||||
loadDirentList={this.props.loadDirentList}
|
||||
>
|
||||
<span className="path-file-name">{item}</span>
|
||||
</DirOperationToolBar>
|
||||
</DirOperationToolbar>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
@ -189,16 +230,9 @@ class DirPath extends React.Component {
|
||||
return pathElem;
|
||||
};
|
||||
|
||||
isViewMetadata = () => {
|
||||
const { currentPath } = this.props;
|
||||
const path = currentPath[currentPath.length - 1] === '/' ? currentPath.slice(0, currentPath.length - 1) : currentPath;
|
||||
const pathList = path.split('/');
|
||||
return pathList[pathList.length - 2] === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES;
|
||||
};
|
||||
|
||||
render() {
|
||||
let { currentPath, repoName } = this.props;
|
||||
let pathElem = this.turnPathToLink(currentPath);
|
||||
const { currentPath, repoName } = this.props;
|
||||
const pathElem = this.turnPathToLink(currentPath);
|
||||
return (
|
||||
<div className="path-container dir-view-path">
|
||||
<span className="cur-view-path-btn mr-1" onClick={this.props.toggleTreePanel}>
|
||||
@ -213,19 +247,19 @@ class DirPath extends React.Component {
|
||||
);
|
||||
})}
|
||||
{this.props.pathPrefix && this.props.pathPrefix.length === 0 && (
|
||||
<Fragment>
|
||||
<>
|
||||
<Link to={siteRoot + 'libraries/'} className="path-item normal" onClick={(e) => this.onTabNavClick(e, 'libraries')}>{gettext('Files')}</Link>
|
||||
<span className="path-split">/</span>
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
{!this.props.pathPrefix && (
|
||||
<Fragment>
|
||||
<>
|
||||
<Link to={siteRoot + 'libraries/'} className="path-item normal" onClick={(e) => this.onTabNavClick(e, 'libraries')}>{gettext('Files')}</Link>
|
||||
<span className="path-split">/</span>
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
{(currentPath === '/' || currentPath === '') ?
|
||||
<DirOperationToolBar
|
||||
<DirOperationToolbar
|
||||
path={this.props.currentPath}
|
||||
repoID={this.props.repoID}
|
||||
repoName={this.props.repoName}
|
||||
@ -242,7 +276,7 @@ class DirPath extends React.Component {
|
||||
loadDirentList={this.props.loadDirentList}
|
||||
>
|
||||
<span className="path-repo-name">{repoName}</span>
|
||||
</DirOperationToolBar> :
|
||||
</DirOperationToolbar> :
|
||||
<span className="path-item" data-path="/" onClick={this.onPathClick} role="button">{repoName}</span>
|
||||
}
|
||||
{pathElem}
|
||||
|
@ -1,23 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||
import { gettext, enableFileTags } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import TextTranslation from '../../utils/text-translation';
|
||||
import SeahubPopover from '../common/seahub-popover';
|
||||
import ListTagPopover from '../popover/list-tag-popover';
|
||||
import ViewModes from '../../components/view-modes';
|
||||
import ReposSortMenu from '../../components/repos-sort-menu';
|
||||
import SortMenu from '../../components/sort-menu';
|
||||
import MetadataViewToolBar from '../../metadata/components/view-toolbar';
|
||||
import TagsTableSearcher from '../../tag/views/all-tags/tags-table/tags-searcher';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { DIRENT_DETAIL_MODE } from '../dir-view-mode/constants';
|
||||
import { ALL_TAGS_ID } from '../../tag/constants';
|
||||
import AllTagsSortSetter from '../../tag/views/all-tags/tags-table/sort-setter';
|
||||
import TagFilesSortSetter from '../../tag/views/tag-files/sort-setter';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
userPerm: PropTypes.string,
|
||||
currentPath: PropTypes.string.isRequired,
|
||||
updateUsedRepoTags: PropTypes.func.isRequired,
|
||||
onDeleteRepoTag: PropTypes.func.isRequired,
|
||||
currentMode: PropTypes.string.isRequired,
|
||||
switchViewMode: PropTypes.func.isRequired,
|
||||
isCustomPermission: PropTypes.bool,
|
||||
@ -25,163 +20,56 @@ const propTypes = {
|
||||
sortOrder: PropTypes.string,
|
||||
sortItems: PropTypes.func,
|
||||
viewId: PropTypes.string,
|
||||
onToggleDetail: PropTypes.func,
|
||||
onCloseDetail: PropTypes.func,
|
||||
};
|
||||
|
||||
class DirTool extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isRepoTagDialogOpen: false,
|
||||
isDropdownMenuOpen: false,
|
||||
};
|
||||
|
||||
this.sortOptions = [
|
||||
{ value: 'name-asc', text: gettext('By name ascending') },
|
||||
{ value: 'name-desc', text: gettext('By name descending') },
|
||||
{ value: 'time-asc', text: gettext('By time ascending') },
|
||||
{ value: 'time-desc', text: gettext('By time descending') }
|
||||
];
|
||||
}
|
||||
|
||||
toggleDropdownMenu = () => {
|
||||
this.setState({
|
||||
isDropdownMenuOpen: !this.state.isDropdownMenuOpen
|
||||
});
|
||||
};
|
||||
|
||||
hidePopover = (e) => {
|
||||
if (e) {
|
||||
let dom = e.target;
|
||||
while (dom) {
|
||||
if (typeof dom.className === 'string' && dom.className.includes('tag-color-popover')) return;
|
||||
dom = dom.parentNode;
|
||||
}
|
||||
}
|
||||
this.setState({ isRepoTagDialogOpen: false });
|
||||
};
|
||||
|
||||
toggleCancel = () => {
|
||||
this.setState({ isRepoTagDialogOpen: false });
|
||||
};
|
||||
|
||||
getMenu = () => {
|
||||
const list = [];
|
||||
const { userPerm, currentPath } = this.props;
|
||||
if (userPerm !== 'rw' || Utils.isMarkdownFile(currentPath)) {
|
||||
return list;
|
||||
}
|
||||
const { TAGS } = TextTranslation;
|
||||
if (enableFileTags) {
|
||||
list.push(TAGS);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
onMenuItemClick = (item) => {
|
||||
const { key } = item;
|
||||
switch (key) {
|
||||
case 'Tags':
|
||||
this.setState({ isRepoTagDialogOpen: !this.state.isRepoTagDialogOpen });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
onMenuItemKeyDown = (e, item) => {
|
||||
if (e.key == 'Enter' || e.key == 'Space') {
|
||||
this.onMenuItemClick(item);
|
||||
}
|
||||
};
|
||||
|
||||
onSelectSortOption = (item) => {
|
||||
const [sortBy, sortOrder] = item.value.split('-');
|
||||
this.props.sortItems(sortBy, sortOrder);
|
||||
};
|
||||
|
||||
showDirentDetail = () => {
|
||||
this.props.switchViewMode(DIRENT_DETAIL_MODE);
|
||||
};
|
||||
|
||||
render() {
|
||||
const menuItems = this.getMenu();
|
||||
const { isDropdownMenuOpen } = this.state;
|
||||
const { repoID, currentMode, currentPath, sortBy, sortOrder, viewId, isCustomPermission } = this.props;
|
||||
const { currentMode, currentPath, sortBy, sortOrder, viewId, isCustomPermission, onToggleDetail, onCloseDetail } = this.props;
|
||||
const propertiesText = TextTranslation.PROPERTIES.value;
|
||||
const isFileExtended = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/');
|
||||
|
||||
const sortOptions = this.sortOptions.map(item => {
|
||||
return {
|
||||
...item,
|
||||
isSelected: item.value === `${sortBy}-${sortOrder}`
|
||||
};
|
||||
});
|
||||
const isTagView = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/');
|
||||
const isAllTagsView = currentPath.split('/').pop() === ALL_TAGS_ID;
|
||||
|
||||
if (isFileExtended) {
|
||||
return (
|
||||
<div className="dir-tool">
|
||||
<MetadataViewToolBar viewId={viewId} isCustomPermission={isCustomPermission} showDetail={this.showDirentDetail} />
|
||||
<MetadataViewToolBar
|
||||
viewId={viewId}
|
||||
isCustomPermission={isCustomPermission}
|
||||
onToggleDetail={onToggleDetail}
|
||||
onCloseDetail={onCloseDetail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isTagView) {
|
||||
return (
|
||||
<div className="dir-tool">
|
||||
{isAllTagsView && <TagsTableSearcher />}
|
||||
{isAllTagsView ? <AllTagsSortSetter /> : <TagFilesSortSetter />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="dir-tool d-flex">
|
||||
<ViewModes currentViewMode={currentMode} switchViewMode={this.props.switchViewMode} />
|
||||
<ReposSortMenu sortOptions={sortOptions} onSelectSortOption={this.onSelectSortOption}/>
|
||||
{(!isCustomPermission) &&
|
||||
<div className="cur-view-path-btn" onClick={this.showDirentDetail}>
|
||||
<span className="sf3-font sf3-font-info" aria-label={propertiesText} title={propertiesText}></span>
|
||||
</div>
|
||||
}
|
||||
{menuItems.length > 0 &&
|
||||
<Dropdown isOpen={isDropdownMenuOpen} toggle={this.toggleDropdownMenu}>
|
||||
<DropdownToggle
|
||||
tag="i"
|
||||
id="cur-folder-more-op-toggle"
|
||||
className='cur-view-path-btn sf3-font-more sf3-font'
|
||||
data-toggle="dropdown"
|
||||
title={gettext('More operations')}
|
||||
aria-label={gettext('More operations')}
|
||||
aria-expanded={isDropdownMenuOpen}
|
||||
>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu right={true}>
|
||||
{menuItems.map((menuItem, index) => {
|
||||
if (menuItem === 'Divider') {
|
||||
return <DropdownItem key={index} divider />;
|
||||
} else {
|
||||
return (
|
||||
<DropdownItem
|
||||
key={index}
|
||||
onClick={this.onMenuItemClick.bind(this, menuItem)}
|
||||
onKeyDown={this.onMenuItemKeyDown.bind(this, menuItem)}
|
||||
>{menuItem.value}
|
||||
</DropdownItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
}
|
||||
</div>
|
||||
{this.state.isRepoTagDialogOpen &&
|
||||
<SeahubPopover
|
||||
popoverClassName="list-tag-popover"
|
||||
target="cur-folder-more-op-toggle"
|
||||
hideSeahubPopover={this.hidePopover}
|
||||
hideSeahubPopoverWithEsc={this.hidePopover}
|
||||
canHideSeahubPopover={true}
|
||||
boundariesElement={document.body}
|
||||
placement={'bottom-end'}
|
||||
>
|
||||
<ListTagPopover
|
||||
repoID={repoID}
|
||||
onListTagCancel={this.toggleCancel}
|
||||
/>
|
||||
</SeahubPopover>
|
||||
<div className="dir-tool d-flex">
|
||||
<ViewModes currentViewMode={currentMode} switchViewMode={this.props.switchViewMode} />
|
||||
<SortMenu sortBy={sortBy} sortOrder={sortOrder} onSelectSortOption={this.onSelectSortOption} />
|
||||
{(!isCustomPermission) &&
|
||||
<div className="cur-view-path-btn" onClick={onToggleDetail}>
|
||||
<span className="sf3-font sf3-font-info" aria-label={propertiesText} title={propertiesText}></span>
|
||||
</div>
|
||||
}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Popover } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCodes } from '../../constants';
|
||||
import { KeyCodes } from '../constants';
|
||||
import { getEventClassName } from '../utils/dom';
|
||||
|
||||
const propTypes = {
|
||||
target: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
@ -9,17 +10,17 @@ const propTypes = {
|
||||
innerClassName: PropTypes.string,
|
||||
popoverClassName: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||
hideSeahubPopover: PropTypes.func.isRequired,
|
||||
hideSeahubPopoverWithEsc: PropTypes.func,
|
||||
hidePopover: PropTypes.func.isRequired,
|
||||
hidePopoverWithEsc: PropTypes.func,
|
||||
hideArrow: PropTypes.bool,
|
||||
canHideSeahubPopover: PropTypes.bool,
|
||||
canHidePopover: PropTypes.bool,
|
||||
placement: PropTypes.string,
|
||||
modifiers: PropTypes.object
|
||||
};
|
||||
|
||||
class SeahubPopover extends React.Component {
|
||||
class CustomizePopover extends React.Component {
|
||||
|
||||
SeahubPopoverRef = null;
|
||||
popoverRef = null;
|
||||
isSelectOpen = false;
|
||||
|
||||
componentDidMount() {
|
||||
@ -32,28 +33,23 @@ class SeahubPopover extends React.Component {
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
getEventClassName = (e) => {
|
||||
// svg mouseEvent event.target.className is an object
|
||||
if (!e || !e.target) return '';
|
||||
return e.target.getAttribute('class') || '';
|
||||
};
|
||||
|
||||
onKeyDown = (e) => {
|
||||
const { canHideSeahubPopover, hideSeahubPopoverWithEsc } = this.props;
|
||||
if (e.keyCode === KeyCodes.Escape && typeof hideSeahubPopoverWithEsc === 'function' && !this.isSelectOpen) {
|
||||
const { canHidePopover = true, hidePopoverWithEsc } = this.props;
|
||||
if (e.keyCode === KeyCodes.Escape && typeof hidePopoverWithEsc === 'function' && !this.isSelectOpen) {
|
||||
e.preventDefault();
|
||||
hideSeahubPopoverWithEsc();
|
||||
hidePopoverWithEsc();
|
||||
} else if (e.keyCode === KeyCodes.Enter) {
|
||||
// Resolve the default behavior of the enter key when entering formulas is blocked
|
||||
if (canHideSeahubPopover) return;
|
||||
if (canHidePopover) return;
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (e) => {
|
||||
if (!this.props.canHideSeahubPopover) return;
|
||||
if (this.SeahubPopoverRef && e && this.getEventClassName(e).indexOf('popover') === -1 && !this.SeahubPopoverRef.contains(e.target)) {
|
||||
this.props.hideSeahubPopover(e);
|
||||
const { canHidePopover = true } = this.props;
|
||||
if (!canHidePopover) return;
|
||||
if (this.popoverRef && e && getEventClassName(e).indexOf('popover') === -1 && !this.popoverRef.contains(e.target)) {
|
||||
this.props.hidePopover(e);
|
||||
}
|
||||
};
|
||||
|
||||
@ -63,8 +59,8 @@ class SeahubPopover extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
target, boundariesElement, innerClassName, popoverClassName, hideArrow, modifiers,
|
||||
placement,
|
||||
target, boundariesElement, innerClassName, popoverClassName, hideArrow = true, modifiers,
|
||||
placement = 'bottom-start',
|
||||
} = this.props;
|
||||
let additionalProps = {};
|
||||
if (boundariesElement) {
|
||||
@ -82,7 +78,7 @@ class SeahubPopover extends React.Component {
|
||||
modifiers={modifiers}
|
||||
{...additionalProps}
|
||||
>
|
||||
<div ref={ref => this.SeahubPopoverRef = ref} onClick={this.onPopoverInsideClick}>
|
||||
<div ref={ref => this.popoverRef = ref} onClick={this.onPopoverInsideClick}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Popover>
|
||||
@ -90,12 +86,6 @@ class SeahubPopover extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
SeahubPopover.defaultProps = {
|
||||
placement: 'bottom-start',
|
||||
hideArrow: true,
|
||||
canHideSeahubPopover: true
|
||||
};
|
||||
CustomizePopover.propTypes = propTypes;
|
||||
|
||||
SeahubPopover.propTypes = propTypes;
|
||||
|
||||
export default SeahubPopover;
|
||||
export default CustomizePopover;
|
100
frontend/src/components/customize-select/index.css
Normal file
@ -0,0 +1,100 @@
|
||||
.seafile-customize-select {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0 10px;
|
||||
border-radius: 3px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 900px;
|
||||
user-select: none;
|
||||
text-align: left;
|
||||
line-height: 1.5;
|
||||
background-image: none;
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.seafile-customize-select:focus,
|
||||
.seafile-customize-select.focus {
|
||||
border-color: #1991eb !important;
|
||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
||||
}
|
||||
|
||||
.seafile-customize-select.disabled:focus,
|
||||
.seafile-customize-select.focus.disabled,
|
||||
.seafile-customize-select.disabled:hover {
|
||||
border-color: rgba(0, 40, 100, 0.12) !important;
|
||||
box-shadow: unset;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.seafile-customize-select:hover {
|
||||
cursor: pointer;
|
||||
border-color: rgb(179, 179, 179);
|
||||
}
|
||||
|
||||
.seafile-customize-select .sf3-font-down {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.seafile-customize-select .selected-option {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.seafile-customize-select .selected-option .custom-select-dropdown-icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-collaborator .seafile-option-group .seafile-option-group-content,
|
||||
.seafile-customize-select.selector-group .seafile-option-group .seafile-option-group-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-collaborator .seafile-option-group .seafile-option-group-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-collaborator .option {
|
||||
padding: 5px 0 5px 10px !important;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-group .option {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-group .select-group-option {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-group .selected-option .selected-group {
|
||||
padding: 0 2px;
|
||||
background: #eceff4;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.seafile-customize-select .selected-option-show {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seafile-customize-select .select-placeholder {
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
173
frontend/src/components/customize-select/index.js
Normal file
@ -0,0 +1,173 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ModalPortal from '../modal-portal';
|
||||
import SelectOptionGroup from './select-option-group';
|
||||
import { getEventClassName } from '../../utils/dom';
|
||||
|
||||
import './index.css';
|
||||
|
||||
class CustomizeSelect extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowSelectOptions: false
|
||||
};
|
||||
}
|
||||
|
||||
onSelectToggle = (event) => {
|
||||
event.preventDefault();
|
||||
/*
|
||||
if select is showing, click events do not need to be monitored by other click events,
|
||||
so it can be closed when other select is clicked.
|
||||
*/
|
||||
if (this.state.isShowSelectOptions) event.stopPropagation();
|
||||
const eventClassName = getEventClassName(event);
|
||||
if (this.props.readOnly || eventClassName.indexOf('option-search-control') > -1 || eventClassName === 'seafile-option-group-search') return;
|
||||
// Prevent closing by pressing the space bar in the search input
|
||||
if (event.target.value === '') return;
|
||||
this.setState({
|
||||
isShowSelectOptions: !this.state.isShowSelectOptions
|
||||
});
|
||||
};
|
||||
|
||||
onClick = (event) => {
|
||||
if (this.props.isShowSelected && event.target.className.includes('icon-fork-number')) {
|
||||
return;
|
||||
}
|
||||
if (!this.selector.contains(event.target)) {
|
||||
this.closeSelect();
|
||||
}
|
||||
};
|
||||
|
||||
closeSelect = () => {
|
||||
this.setState({ isShowSelectOptions: false });
|
||||
};
|
||||
|
||||
getSelectedOptionTop = () => {
|
||||
if (!this.selector) return 38;
|
||||
const { height } = this.selector.getBoundingClientRect();
|
||||
return height;
|
||||
};
|
||||
|
||||
getFilterOptions = (searchValue) => {
|
||||
const { options, searchable } = this.props;
|
||||
if (!searchable) return options || [];
|
||||
const validSearchVal = searchValue.trim().toLowerCase();
|
||||
if (!validSearchVal) return options || [];
|
||||
return options.filter(option => {
|
||||
const { value, name } = option;
|
||||
if (typeof name === 'string') {
|
||||
return name.toLowerCase().indexOf(validSearchVal) > -1;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if (value.column) {
|
||||
return value.column.name.toLowerCase().indexOf(validSearchVal) > -1;
|
||||
}
|
||||
if (value.name) {
|
||||
return value.name.toLowerCase().indexOf(validSearchVal) > -1;
|
||||
}
|
||||
return value.columnOption && value.columnOption.name.toLowerCase().indexOf(validSearchVal) > -1;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
renderDropDownIcon = () => {
|
||||
const { readOnly, component } = this.props;
|
||||
if (readOnly) return;
|
||||
const { DropDownIcon } = component || {};
|
||||
if (DropDownIcon) {
|
||||
return (
|
||||
<div className="custom-select-dropdown-icon">{DropDownIcon}</div>
|
||||
);
|
||||
}
|
||||
return (<i className="sf3-font sf3-font-down" aria-hidden="true"></i>);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, value, options, placeholder, searchable, searchPlaceholder, noOptionsPlaceholder,
|
||||
readOnly, isInModal, addOptionAble, component } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(node) => this.selector = node}
|
||||
className={classnames('seafile-customize-select custom-select',
|
||||
{ 'focus': this.state.isShowSelectOptions },
|
||||
{ 'disabled': readOnly },
|
||||
className
|
||||
)}
|
||||
onClick={this.onSelectToggle}>
|
||||
<div className="selected-option">
|
||||
{value && value.label ?
|
||||
<span className="selected-option-show">{value.label}</span>
|
||||
:
|
||||
<span className="select-placeholder">{placeholder}</span>
|
||||
}
|
||||
{this.renderDropDownIcon()}
|
||||
</div>
|
||||
{this.state.isShowSelectOptions && !isInModal && (
|
||||
<SelectOptionGroup
|
||||
value={value}
|
||||
addOptionAble={addOptionAble}
|
||||
component={component}
|
||||
isShowSelected={this.props.isShowSelected}
|
||||
top={this.getSelectedOptionTop()}
|
||||
options={options}
|
||||
onSelectOption={this.props.onSelectOption}
|
||||
searchable={searchable}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
noOptionsPlaceholder={noOptionsPlaceholder}
|
||||
onClickOutside={this.onClick}
|
||||
closeSelect={this.closeSelect}
|
||||
getFilterOptions={this.getFilterOptions}
|
||||
supportMultipleSelect={this.props.supportMultipleSelect}
|
||||
/>
|
||||
)}
|
||||
{this.state.isShowSelectOptions && isInModal && (
|
||||
<ModalPortal>
|
||||
<SelectOptionGroup
|
||||
className={className}
|
||||
value={value}
|
||||
addOptionAble={addOptionAble}
|
||||
component={component}
|
||||
isShowSelected={this.props.isShowSelected}
|
||||
position={this.selector.getBoundingClientRect()}
|
||||
isInModal={isInModal}
|
||||
top={this.getSelectedOptionTop()}
|
||||
options={options}
|
||||
onSelectOption={this.props.onSelectOption}
|
||||
searchable={searchable}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
noOptionsPlaceholder={noOptionsPlaceholder}
|
||||
onClickOutside={this.onClick}
|
||||
closeSelect={this.closeSelect}
|
||||
getFilterOptions={this.getFilterOptions}
|
||||
supportMultipleSelect={this.props.supportMultipleSelect}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomizeSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.object,
|
||||
options: PropTypes.array,
|
||||
placeholder: PropTypes.string,
|
||||
onSelectOption: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
searchable: PropTypes.bool,
|
||||
addOptionAble: PropTypes.bool,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
noOptionsPlaceholder: PropTypes.string,
|
||||
component: PropTypes.object,
|
||||
supportMultipleSelect: PropTypes.bool,
|
||||
isShowSelected: PropTypes.bool,
|
||||
isInModal: PropTypes.bool, // if select component in a modal (option group need ModalPortal to show)
|
||||
};
|
||||
|
||||
export default CustomizeSelect;
|
@ -0,0 +1,103 @@
|
||||
.seafile-option-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
min-height: 60px;
|
||||
max-height: 300px;
|
||||
min-width: 100%;
|
||||
max-width: 15rem;
|
||||
padding: 0.5rem 0;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border-radius: 3px;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.seafile-option-group .seafile-option-group-search {
|
||||
width: 100%;
|
||||
padding: 0 10px 6px 10px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.seafile-option-group-search .form-control {
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.seafile-option-group .none-search-result {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.seafile-option-group .seafile-option-group-content {
|
||||
max-height: 252px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.seafile-select-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
padding: 0.25rem 10px;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
text-align: inherit;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seafile-select-option.seafile-select-option-active {
|
||||
background-color: #20a0ff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seafile-select-option.seafile-select-option-active .select-option-name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.seafile-select-option:hover .header-icon .seafile-multicolor-icon,
|
||||
.seafile-select-option.seafile-select-option-active .header-icon .seafile-multicolor-icon {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.seafile-select-option:not(.seafile-select-option-active):hover .header-icon .seafile-multicolor-icon {
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.seafile-select-option .select-option-name .single-select-option {
|
||||
margin: 0 0 0 12px;
|
||||
}
|
||||
|
||||
.seafile-select-option .select-option-name .multiple-select-option {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.seafile-option-group-selector-single-select .select-option-name,
|
||||
.seafile-option-group-selector-multiple-select .multiple-option-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.seafile-option-group-selector-multiple-select .multiple-check-icon {
|
||||
display: inline-flex;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.seafile-option-group-selector-multiple-select .multiple-check-icon .seafile-multicolor-icon-check-mark {
|
||||
font-size: 12px;
|
||||
color: #798d99;
|
||||
}
|
||||
|
||||
.seafile-option-group-selector-single-select .seafile-select-option:hover,
|
||||
.seafile-option-group-selector-single-select .seafile-select-option.seafile-select-option-active,
|
||||
.seafile-option-group-selector-multiple-select .seafile-select-option:hover,
|
||||
.seafile-option-group-selector-multiple-select .seafile-select-option.seafile-select-option-active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
@ -0,0 +1,233 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ClickOutside from '../../click-outside';
|
||||
import SearchInput from '../../search-input';
|
||||
import Option from './option';
|
||||
import { KeyCodes } from '../../../constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const OPTION_HEIGHT = 32;
|
||||
|
||||
class SelectOptionGroup extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchVal: '',
|
||||
activeIndex: -1,
|
||||
disableHover: false,
|
||||
};
|
||||
this.filterOptions = null;
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.onHotKey);
|
||||
setTimeout(() => {
|
||||
this.resetMenuStyle();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.filterOptions = null;
|
||||
this.timer && clearTimeout(this.timer);
|
||||
window.removeEventListener('keydown', this.onHotKey);
|
||||
}
|
||||
|
||||
resetMenuStyle = () => {
|
||||
const { isInModal, position } = this.props;
|
||||
const { top, height } = this.optionGroupRef.getBoundingClientRect();
|
||||
if (isInModal) {
|
||||
if (position.y + position.height + height > window.innerHeight) {
|
||||
this.optionGroupRef.style.top = (position.y - height) + 'px';
|
||||
}
|
||||
this.optionGroupRef.style.opacity = 1;
|
||||
}
|
||||
else {
|
||||
if (height + top > window.innerHeight) {
|
||||
const borderWidth = 2;
|
||||
this.optionGroupRef.style.top = -1 * (height + borderWidth) + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onHotKey = (event) => {
|
||||
const keyCode = event.keyCode;
|
||||
if (keyCode === KeyCodes.UpArrow) {
|
||||
this.onPressUp();
|
||||
} else if (keyCode === KeyCodes.DownArrow) {
|
||||
this.onPressDown();
|
||||
} else if (keyCode === KeyCodes.Enter) {
|
||||
let option = this.filterOptions && this.filterOptions[this.state.activeIndex];
|
||||
if (option) {
|
||||
this.props.onSelectOption(option.value);
|
||||
if (!this.props.supportMultipleSelect) {
|
||||
this.props.closeSelect();
|
||||
}
|
||||
}
|
||||
} else if (keyCode === KeyCodes.Tab || keyCode === KeyCodes.Escape) {
|
||||
this.props.closeSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onPressUp = () => {
|
||||
if (this.state.activeIndex > 0) {
|
||||
this.setState({ activeIndex: this.state.activeIndex - 1 }, () => {
|
||||
this.scrollContent();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onPressDown = () => {
|
||||
if (this.filterOptions && this.state.activeIndex < this.filterOptions.length - 1) {
|
||||
this.setState({ activeIndex: this.state.activeIndex + 1 }, () => {
|
||||
this.scrollContent();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (e) => {
|
||||
const { isInModal } = this.props;
|
||||
// prevent event propagation when click option or search input
|
||||
if (isInModal) {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
scrollContent = () => {
|
||||
const { offsetHeight, scrollTop } = this.optionGroupContentRef;
|
||||
this.setState({ disableHover: true });
|
||||
this.timer = setTimeout(() => {
|
||||
this.setState({ disableHover: false });
|
||||
}, 500);
|
||||
if (this.state.activeIndex * OPTION_HEIGHT === 0) {
|
||||
this.optionGroupContentRef.scrollTop = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.activeIndex * OPTION_HEIGHT < scrollTop) {
|
||||
this.optionGroupContentRef.scrollTop = scrollTop - OPTION_HEIGHT;
|
||||
}
|
||||
else if (this.state.activeIndex * OPTION_HEIGHT > offsetHeight + scrollTop) {
|
||||
this.optionGroupContentRef.scrollTop = scrollTop + OPTION_HEIGHT;
|
||||
}
|
||||
};
|
||||
|
||||
changeIndex = (index) => {
|
||||
this.setState({ activeIndex: index });
|
||||
};
|
||||
|
||||
onChangeSearch = (searchVal) => {
|
||||
let value = searchVal || '';
|
||||
if (value !== this.state.searchVal) {
|
||||
this.setState({ searchVal: value, activeIndex: -1, });
|
||||
}
|
||||
};
|
||||
|
||||
renderOptGroup = (searchVal) => {
|
||||
let { noOptionsPlaceholder, onSelectOption } = this.props;
|
||||
this.filterOptions = this.props.getFilterOptions(searchVal);
|
||||
if (this.filterOptions.length === 0) {
|
||||
return (
|
||||
<div className="none-search-result">{noOptionsPlaceholder}</div>
|
||||
);
|
||||
}
|
||||
return this.filterOptions.map((opt, i) => {
|
||||
let key = opt.value.column ? opt.value.column.key : i;
|
||||
let isActive = this.state.activeIndex === i;
|
||||
return (
|
||||
<Option
|
||||
key={key}
|
||||
index={i}
|
||||
isActive={isActive}
|
||||
value={opt.value}
|
||||
onSelectOption={onSelectOption}
|
||||
changeIndex={this.changeIndex}
|
||||
supportMultipleSelect={this.props.supportMultipleSelect}
|
||||
disableHover={this.state.disableHover}
|
||||
>
|
||||
{opt.label}
|
||||
</Option>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { searchable, searchPlaceholder, top, left, minWidth, value, isShowSelected, isInModal, position,
|
||||
className, addOptionAble, component } = this.props;
|
||||
const { AddOption } = component || {};
|
||||
let { searchVal } = this.state;
|
||||
let style = { top: top || 0, left: left || 0 };
|
||||
if (minWidth) {
|
||||
style = { top: top || 0, left: left || 0, minWidth };
|
||||
}
|
||||
if (isInModal) {
|
||||
style = {
|
||||
position: 'fixed',
|
||||
left: position.x,
|
||||
top: position.y + position.height,
|
||||
minWidth: position.width,
|
||||
opacity: 0,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.onClickOutside}>
|
||||
<div
|
||||
className={classnames('seafile-option-group', className ? 'seafile-option-group-' + className : '', {
|
||||
'pt-0': isShowSelected,
|
||||
'create-new-seafile-option-group': addOptionAble,
|
||||
})}
|
||||
ref={(ref) => this.optionGroupRef = ref}
|
||||
style={style}
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
{isShowSelected &&
|
||||
<div className="editor-list-delete mb-2" onClick={(e) => e.stopPropagation()}>{value.label || ''}</div>
|
||||
}
|
||||
{searchable && (
|
||||
<div className="seafile-option-group-search">
|
||||
<SearchInput
|
||||
className="option-search-control"
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={this.onChangeSearch}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="seafile-option-group-content" ref={(ref) => this.optionGroupContentRef = ref}>
|
||||
{this.renderOptGroup(searchVal)}
|
||||
</div>
|
||||
{addOptionAble && AddOption}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectOptionGroup.propTypes = {
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
minWidth: PropTypes.number,
|
||||
options: PropTypes.array,
|
||||
onSelectOption: PropTypes.func,
|
||||
searchable: PropTypes.bool,
|
||||
addOptionAble: PropTypes.bool,
|
||||
component: PropTypes.object,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
noOptionsPlaceholder: PropTypes.string,
|
||||
onClickOutside: PropTypes.func.isRequired,
|
||||
closeSelect: PropTypes.func.isRequired,
|
||||
getFilterOptions: PropTypes.func.isRequired,
|
||||
supportMultipleSelect: PropTypes.bool,
|
||||
value: PropTypes.object,
|
||||
isShowSelected: PropTypes.bool,
|
||||
stopClickEvent: PropTypes.bool,
|
||||
isInModal: PropTypes.bool,
|
||||
position: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SelectOptionGroup;
|
@ -0,0 +1,50 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class Option extends Component {
|
||||
|
||||
onSelectOption = (value, event) => {
|
||||
if (this.props.supportMultipleSelect) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.props.onSelectOption(value, event);
|
||||
};
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (!this.props.disableHover) {
|
||||
this.props.changeIndex(this.props.index);
|
||||
}
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
if (!this.props.disableHover) {
|
||||
this.props.changeIndex(-1);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={classnames('seafile-select-option', { 'seafile-select-option-active': this.props.isActive })}
|
||||
onClick={this.onSelectOption.bind(this, this.props.value)}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Option.propTypes = {
|
||||
index: PropTypes.number,
|
||||
isActive: PropTypes.bool,
|
||||
changeIndex: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
onSelectOption: PropTypes.func,
|
||||
supportMultipleSelect: PropTypes.bool,
|
||||
disableHover: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Option;
|
@ -89,7 +89,7 @@ Picker.propTypes = {
|
||||
showHourAndMinute: PropTypes.bool.isRequired,
|
||||
disabledDate: PropTypes.func.isRequired,
|
||||
value: PropTypes.object,
|
||||
disabled: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.func,
|
||||
inputWidth: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody } from 'reactstrap';
|
||||
import { gettext, lang, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle, seafileVersion, additionalAboutDialogLinks, aboutDialogCustomHtml } from '../../utils/constants';
|
||||
import '../../css/seahub-modal-header.css';
|
||||
|
||||
const propTypes = {
|
||||
onCloseAboutDialog: PropTypes.func.isRequired,
|
||||
@ -27,7 +28,11 @@ class AboutDialog extends React.Component {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggleDialog}>
|
||||
<ModalBody>
|
||||
<button type="button" className="close" onClick={toggleDialog}><span aria-hidden="true">×</span></button>
|
||||
<button type="button" className="close seahub-modal-btn p-0" aria-label={gettext('Close')} onClick={toggleDialog}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
<div className="about-content" dangerouslySetInnerHTML={{ __html: aboutDialogCustomHtml }}></div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
@ -36,7 +41,11 @@ class AboutDialog extends React.Component {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggleDialog}>
|
||||
<ModalBody>
|
||||
<button type="button" className="close" onClick={toggleDialog}><span aria-hidden="true">×</span></button>
|
||||
<button type="button" className="close seahub-modal-btn p-0" aria-label={gettext('Close')} onClick={toggleDialog}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
<div className="about-content">
|
||||
<p><img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" /></p>
|
||||
<p>{gettext('Server Version: ')}{seafileVersion}<br />© {(new Date()).getFullYear()} {gettext('Seafile')}</p>
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Form, FormGroup, Label, Input, Modal, ModalHeader, ModalBody, ModalFooter, Alert } from 'reactstrap';
|
||||
import { Button, Form, FormGroup, Label, Input, Modal, ModalBody, ModalFooter, Alert } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { SeahubSelect } from '../common/select';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import toaster from '../toast';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
sharedToken: PropTypes.string.isRequired,
|
||||
@ -23,6 +25,12 @@ class AddAbuseReportDialog extends React.Component {
|
||||
reporter: this.props.contactEmail,
|
||||
errMessage: '',
|
||||
};
|
||||
this.typeOptions = [
|
||||
{ value: 'copyright', label: gettext('Copyright Infringement') },
|
||||
{ value: 'virus', label: gettext('Virus') },
|
||||
{ value: 'abuse_content', label: gettext('Abuse Content') },
|
||||
{ value: 'other', label: gettext('Other') },
|
||||
];
|
||||
}
|
||||
|
||||
onAbuseReport = () => {
|
||||
@ -44,8 +52,8 @@ class AddAbuseReportDialog extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
onAbuseTypeChange = (event) => {
|
||||
let type = event.target.value;
|
||||
onAbuseTypeChange = (option) => {
|
||||
let type = option.value;
|
||||
if (type === this.state.abuseType) {
|
||||
return;
|
||||
}
|
||||
@ -65,25 +73,34 @@ class AddAbuseReportDialog extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={this.props.isAddAbuseReportDialogOpen} toggle={this.props.toggleAddAbuseReportDialog}>
|
||||
<ModalHeader toggle={this.props.toggleAddAbuseReportDialog}>{gettext('Report Abuse')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.props.toggleAddAbuseReportDialog}>{gettext('Report Abuse')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="abuse-type-select">{gettext('Abuse Type')}</Label>
|
||||
<Input type="select" id="abuse-type-select" onChange={(event) => this.onAbuseTypeChange(event)}>
|
||||
<option value='copyright'>{gettext('Copyright Infringement')}</option>
|
||||
<option value='virus'>{gettext('Virus')}</option>
|
||||
<option value='abuse_content'>{gettext('Abuse Content')}</option>
|
||||
<option value='other'>{gettext('Other')}</option>
|
||||
</Input>
|
||||
<Label>{gettext('Abuse Type')}</Label>
|
||||
<SeahubSelect
|
||||
options={this.typeOptions}
|
||||
value={this.typeOptions.find(option => option.value === this.state.abuseType) || this.typeOptions[0]}
|
||||
onChange={this.onAbuseTypeChange}
|
||||
isClearable={false}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Contact Information')}</Label>
|
||||
<Input type="text" value={this.state.reporter} onChange={(event) => this.setReporter(event)}/>
|
||||
<Input
|
||||
name="abuse-report-contact-information"
|
||||
type="text"
|
||||
value={this.state.reporter}
|
||||
onChange={(event) => this.setReporter(event)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Description')}</Label>
|
||||
<Input type="textarea" onChange={(event) => this.setDescription(event)}/>
|
||||
<Input
|
||||
name="abuse-report-description"
|
||||
type="textarea"
|
||||
onChange={(event) => this.setDescription(event)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
{this.state.errMessage && <Alert color="danger">{this.state.errMessage}</Alert>}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Label } from 'reactstrap';
|
||||
import { Button, Modal, ModalBody, ModalFooter, Input, Label } from 'reactstrap';
|
||||
import SeahubModalHeader from '../common/seahub-modal-header';
|
||||
import { gettext, isPro } from '../../utils/constants';
|
||||
import wikiAPI from '../../utils/wiki-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import { SeahubSelect, NoOptionsStyle } from '../common/select';
|
||||
import { SeahubSelect } from '../common/select';
|
||||
|
||||
const propTypes = {
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
@ -82,10 +83,16 @@ class AddWikiDialog extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} autoFocus={false} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Add Wiki')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('Add Wiki')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<Label>{gettext('Name')}</Label>
|
||||
<Input onKeyDown={this.handleKeyDown} autoFocus={true} value={this.state.name} onChange={this.inputNewName}/>
|
||||
<Input
|
||||
onKeyDown={this.handleKeyDown}
|
||||
autoFocus={true}
|
||||
value={this.state.name}
|
||||
onChange={this.inputNewName}
|
||||
name="wiki-name"
|
||||
/>
|
||||
{isPro &&
|
||||
<>
|
||||
<Label className='mt-4'>{gettext('Wiki owner')} ({gettext('Optional')})</Label>
|
||||
@ -96,9 +103,6 @@ class AddWikiDialog extends React.Component {
|
||||
placeholder={gettext('Select a department')}
|
||||
maxMenuHeight={200}
|
||||
value={this.state.selectedOption}
|
||||
components={{ NoOptionsMessage: (
|
||||
<div style={NoOptionsStyle}>{gettext('No department')}</div>
|
||||
) }}
|
||||
noOptionsMessage={() => {return gettext('No options available');}}
|
||||
/>
|
||||
</>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { Button, Modal, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
wikiPageName: PropTypes.string,
|
||||
@ -54,13 +55,14 @@ class AddWikiPageDialog extends React.Component {
|
||||
const { handleClose } = this.props;
|
||||
return (
|
||||
<Modal isOpen={true} toggle={handleClose} onOpened={this.onDialogLoad}>
|
||||
<ModalHeader toggle={handleClose}>{gettext('New page')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={handleClose}>{gettext('New page')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="pageName">{gettext('Name')}</Label>
|
||||
<Input
|
||||
id="pageName"
|
||||
name="wiki-page-name"
|
||||
onKeyDown={this.handleKeyDown}
|
||||
innerRef={this.inputRef}
|
||||
value={this.state.wikiPageName}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, ModalFooter, ModalBody } from 'reactstrap';
|
||||
import { Button, Modal, ModalFooter, ModalBody } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
groupName: PropTypes.string.isRequired,
|
||||
@ -29,9 +30,9 @@ class ChangeGroupDialog extends React.Component {
|
||||
const msg = gettext('Are you sure to change group {placeholder} to department ?').replace('{placeholder}', groupName);
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>
|
||||
{gettext('Change group to department')}
|
||||
</ModalHeader>
|
||||
</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p dangerouslySetInnerHTML={{ __html: msg }}></p>
|
||||
</ModalBody>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Alert } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Alert } from 'reactstrap';
|
||||
import { gettext, repoPasswordMinLength } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import toaster from '../toast';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
@ -100,9 +101,9 @@ class ChangeRepoPasswordDialog extends React.Component {
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} style={{ height: 'auto' }} toggle={toggleDialog}>
|
||||
<ModalHeader toggle={toggleDialog}>
|
||||
<SeahubModalHeader toggle={toggleDialog}>
|
||||
<span dangerouslySetInnerHTML={{ __html: title }} className="d-flex mw-100"></span>
|
||||
</ModalHeader>
|
||||
</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<form id="repo-change-passwd-form" action="" method="post">
|
||||
<label htmlFor="passwd">{gettext('Old Password')}</label>
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import { MenuSelectStyle } from '../common/select/seahub-select-style';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
@ -60,7 +61,7 @@ class CleanTrash extends React.Component {
|
||||
const { formErrorMsg } = this.state;
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>{gettext('Clean')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>{gettext('Clean')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<React.Fragment>
|
||||
<p>{gettext('Clear files in trash and history:')}</p>
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
||||
import { Modal, ModalBody } from 'reactstrap';
|
||||
import dayjs from 'dayjs';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import Loading from '../loading';
|
||||
import Icon from '../icon';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
import '../../css/commit-details.css';
|
||||
|
||||
@ -45,10 +47,13 @@ class CommitDetails extends React.Component {
|
||||
render() {
|
||||
const { toggleDialog, commitTime } = this.props;
|
||||
return (
|
||||
<Modal isOpen={true} centered={true} toggle={toggleDialog}>
|
||||
<ModalHeader toggle={toggleDialog}>{gettext('Modification Details')}</ModalHeader>
|
||||
<Modal isOpen={true} toggle={toggleDialog}>
|
||||
<SeahubModalHeader toggle={toggleDialog}>{gettext('Modification Details')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p className="small">{dayjs(commitTime).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
<p className="repo-commit-time mb-6 d-flex align-items-center">
|
||||
<Icon symbol="time" className="mr-1" />
|
||||
{dayjs(commitTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</p>
|
||||
<Content data={this.state} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
@ -87,11 +92,11 @@ class Content extends React.Component {
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<h6>{item.title}</h6>
|
||||
<ul>
|
||||
<h6 className="mt-4">{item.title}</h6>
|
||||
<ul className="list-unstyled">
|
||||
{
|
||||
data[item.type].map((item, index) => {
|
||||
return <li key={index} dangerouslySetInnerHTML={{ __html: item }} className="commit-detail-item text-truncate"></li>;
|
||||
return <li key={index} dangerouslySetInnerHTML={{ __html: item }} className="text-truncate"></li>;
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
@ -26,7 +27,7 @@ class CommonOperationConfirmationDialog extends Component {
|
||||
let { title, message, confirmBtnText } = this.props;
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{title}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.toggle}>{title}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p dangerouslySetInnerHTML={{ __html: message }}></p>
|
||||
</ModalBody>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
formActionURL: PropTypes.string.isRequired,
|
||||
@ -24,7 +25,7 @@ class ConfirmDeleteAccount extends Component {
|
||||
const { formActionURL, csrfToken, toggle } = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<ModalHeader toggle={toggle}>{gettext('Delete Account')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={toggle}>{gettext('Delete Account')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Really want to delete your account?')}</p>
|
||||
<form ref={this.form} className="d-none" method="post" action={formActionURL}>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
formActionURL: PropTypes.string.isRequired,
|
||||
@ -24,7 +25,7 @@ class ConfirmDisconnectDingtalk extends Component {
|
||||
const { formActionURL, csrfToken, toggle } = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<ModalHeader toggle={toggle}>{gettext('Disconnect')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={toggle}>{gettext('Disconnect')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to disconnect?')}</p>
|
||||
<form ref={this.form} className="d-none" method="post" action={formActionURL}>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
formActionURL: PropTypes.string.isRequired,
|
||||
@ -24,7 +25,7 @@ class ConfirmDisconnectWechat extends Component {
|
||||
const { formActionURL, csrfToken, toggle } = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<ModalHeader toggle={toggle}>{gettext('Disconnect')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={toggle}>{gettext('Disconnect')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to disconnect?')}</p>
|
||||
<form ref={this.form} className="d-none" method="post" action={formActionURL}>
|
||||
|
46
frontend/src/components/dialog/confirm-disconnect-weixin.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
formActionURL: PropTypes.string.isRequired,
|
||||
csrfToken: PropTypes.string.isRequired,
|
||||
toggle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class ConfirmDisconnectWeixin extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.form = React.createRef();
|
||||
}
|
||||
|
||||
disconnect = () => {
|
||||
this.form.current.submit();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { formActionURL, csrfToken, toggle } = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<SeahubModalHeader toggle={toggle}>{gettext('Disconnect')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to disconnect?')}</p>
|
||||
<form ref={this.form} className="d-none" method="post" action={formActionURL}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
</form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.disconnect}>{gettext('Disconnect')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDisconnectWeixin.propTypes = propTypes;
|
||||
|
||||
export default ConfirmDisconnectWeixin;
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
restoreRepo: PropTypes.func.isRequired,
|
||||
@ -28,7 +29,7 @@ class ConfirmRestoreRepo extends Component {
|
||||
const { toggle } = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<ModalHeader toggle={toggle}>{gettext('Restore Library')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={toggle}>{gettext('Restore Library')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to restore this library?')}</p>
|
||||
</ModalBody>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button, FormGroup, Label, Input } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button, FormGroup, Label, Input } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
executeOperation: PropTypes.func.isRequired,
|
||||
@ -35,7 +36,7 @@ class ConfirmUnlinkDevice extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Unlink device')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('Unlink device')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to unlink this device?')}</p>
|
||||
<FormGroup check>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Label } from 'reactstrap';
|
||||
import { Button, Modal, ModalBody, ModalFooter, Input, Label } from 'reactstrap';
|
||||
import { gettext, isPro } from '../../utils/constants';
|
||||
import wikiAPI from '../../utils/wiki-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import { SeahubSelect, NoOptionsStyle } from '../common/select';
|
||||
import { SeahubSelect } from '../common/select';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
@ -78,10 +79,16 @@ class ConvertWikiDialog extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} autoFocus={false} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Convert Wiki')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('Convert Wiki')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<Label>{gettext('Name')}</Label>
|
||||
<Input onKeyDown={this.handleKeyDown} autoFocus={true} value={this.state.name} onChange={this.inputNewName}/>
|
||||
<Input
|
||||
name="wiki-name"
|
||||
onKeyDown={this.handleKeyDown}
|
||||
autoFocus={true}
|
||||
value={this.state.name}
|
||||
onChange={this.inputNewName}
|
||||
/>
|
||||
{isPro &&
|
||||
<>
|
||||
<Label className='mt-4'>{gettext('Wiki owner')} ({gettext('Optional')})</Label>
|
||||
@ -92,9 +99,6 @@ class ConvertWikiDialog extends React.Component {
|
||||
placeholder={gettext('Select a department')}
|
||||
maxMenuHeight={200}
|
||||
value={this.state.selectedOption}
|
||||
components={{ NoOptionsMessage: (
|
||||
<div style={NoOptionsStyle}>{gettext('No department')}</div>
|
||||
) }}
|
||||
noOptionsMessage={() => {return gettext('No options available');}}
|
||||
/>
|
||||
</>
|
||||
|
@ -1,9 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap';
|
||||
import FileChooser from '../file-chooser';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Modal, ModalHeader } from 'reactstrap';
|
||||
import Searcher from '../file-chooser/searcher';
|
||||
import SelectDirentBody from './select-dirent-body';
|
||||
import { MODE_TYPE_MAP } from '../../constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { gettext, isPro } from '../../utils/constants';
|
||||
import { RepoInfo } from '../../models';
|
||||
import toaster from '../toast';
|
||||
|
||||
const propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
@ -15,21 +20,70 @@ const propTypes = {
|
||||
onItemsCopy: PropTypes.func,
|
||||
onCancelCopy: PropTypes.func.isRequired,
|
||||
repoEncrypted: PropTypes.bool.isRequired,
|
||||
onAddFolder: PropTypes.func,
|
||||
};
|
||||
|
||||
// need dirent file Path;
|
||||
class CopyDirent extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
repo: { repo_id: this.props.repoID },
|
||||
mode: MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY,
|
||||
currentRepo: { repo_id: this.props.repoID },
|
||||
selectedRepo: { repo_id: this.props.repoID },
|
||||
repoList: [],
|
||||
selectedPath: this.props.path,
|
||||
selectedSearchedRepo: null,
|
||||
selectedSearchedItem: { repoID: '', filePath: '' },
|
||||
searchStatus: '',
|
||||
searchResults: [],
|
||||
showSearchBar: false,
|
||||
errMessage: '',
|
||||
mode: 'only_current_library',
|
||||
initToShowChildren: false,
|
||||
};
|
||||
this.lastMode = MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
try {
|
||||
const res = await seafileAPI.getRepoInfo(this.props.repoID);
|
||||
const repo = new RepoInfo(res.data);
|
||||
this.setState({ currentRepo: repo });
|
||||
await this.fetchRepoList();
|
||||
} catch (error) {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRepoList = async () => {
|
||||
try {
|
||||
const res = await seafileAPI.listRepos();
|
||||
const repos = res.data.repos;
|
||||
const repoList = [];
|
||||
const uniqueRepoIds = new Set();
|
||||
for (const repo of repos) {
|
||||
if (repo.permission === 'rw' && repo.repo_id !== this.props.repoID && !uniqueRepoIds.has(repo.repo_id)) {
|
||||
uniqueRepoIds.add(repo.repo_id);
|
||||
repoList.push(repo);
|
||||
}
|
||||
}
|
||||
const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc');
|
||||
const selectedRepo = sortedRepoList.find((repo) => repo.repo_id === this.props.repoID);
|
||||
this.setState({
|
||||
repoList: sortedRepoList,
|
||||
repo: selectedRepo,
|
||||
});
|
||||
} catch (error) {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
}
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
if (this.props.isMultipleOperation) {
|
||||
this.copyItems();
|
||||
@ -39,10 +93,10 @@ class CopyDirent extends React.Component {
|
||||
};
|
||||
|
||||
copyItems = () => {
|
||||
let { repo, selectedPath } = this.state;
|
||||
let { selectedRepo, selectedPath } = this.state;
|
||||
let message = gettext('Invalid destination path');
|
||||
|
||||
if (!repo || selectedPath === '') {
|
||||
if (!selectedRepo || selectedPath === '') {
|
||||
this.setState({ errMessage: message });
|
||||
return;
|
||||
}
|
||||
@ -78,16 +132,16 @@ class CopyDirent extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onItemsCopy(repo, selectedPath);
|
||||
this.props.onItemsCopy(selectedRepo, selectedPath, true);
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
copyItem = () => {
|
||||
let { repo, repoID, selectedPath } = this.state;
|
||||
let { repoID, selectedRepo, selectedPath } = this.state;
|
||||
let direntPath = Utils.joinPath(this.props.path, this.props.dirent.name);
|
||||
let message = gettext('Invalid destination path');
|
||||
|
||||
if (!repo || (repo.repo_id === repoID && selectedPath === '')) {
|
||||
if (!selectedRepo || (selectedRepo.repo_id === repoID && selectedPath === '')) {
|
||||
this.setState({ errMessage: message });
|
||||
return;
|
||||
}
|
||||
@ -107,7 +161,7 @@ class CopyDirent extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onItemCopy(repo, this.props.dirent, selectedPath, this.props.path);
|
||||
this.props.onItemCopy(selectedRepo, this.props.dirent, selectedPath, this.props.path, true);
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
@ -115,9 +169,122 @@ class CopyDirent extends React.Component {
|
||||
this.props.onCancelCopy();
|
||||
};
|
||||
|
||||
selectRepo = (repo) => {
|
||||
this.setState({ selectedRepo: repo });
|
||||
};
|
||||
|
||||
selectSearchedRepo = (repo) => {
|
||||
this.setState({ selectedSearchedRepo: repo });
|
||||
};
|
||||
|
||||
setSelectedPath = (selectedPath) => {
|
||||
this.setState({ selectedPath });
|
||||
};
|
||||
|
||||
setErrMessage = (message) => {
|
||||
this.setState({ errMessage: message });
|
||||
};
|
||||
|
||||
updateMode = (mode) => {
|
||||
if (mode === this.state.mode) return;
|
||||
|
||||
if (mode !== MODE_TYPE_MAP.SEARCH_RESULTS) {
|
||||
this.lastMode = mode;
|
||||
}
|
||||
|
||||
const isShowChildren = mode === MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY || mode === MODE_TYPE_MAP.SEARCH_RESULTS;
|
||||
this.setState({
|
||||
mode,
|
||||
initToShowChildren: isShowChildren,
|
||||
});
|
||||
|
||||
if (this.state.mode === MODE_TYPE_MAP.SEARCH_RESULTS) {
|
||||
this.setState({
|
||||
selectedSearchedRepo: null,
|
||||
searchResults: [],
|
||||
showSearchBar: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.selectedSearchedRepo && mode !== MODE_TYPE_MAP.SEARCH_RESULTS) {
|
||||
this.setState({
|
||||
selectedSearchedRepo: null,
|
||||
searchResults: [],
|
||||
showSearchBar: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ selectedSearchedItem: { repoID: '', filePath: '' } });
|
||||
};
|
||||
|
||||
onUpdateSearchStatus = (status) => {
|
||||
this.setState({ searchStatus: status });
|
||||
};
|
||||
|
||||
onUpdateSearchResults = (results) => {
|
||||
if (results.length > 0) {
|
||||
const firstResult = results[0];
|
||||
this.setState({
|
||||
selectedRepo: new RepoInfo(firstResult),
|
||||
selectedPath: firstResult.path
|
||||
});
|
||||
}
|
||||
this.setState({ searchResults: results });
|
||||
};
|
||||
|
||||
onOpenSearchBar = () => {
|
||||
this.setState({ showSearchBar: true });
|
||||
};
|
||||
|
||||
onCloseSearchBar = () => {
|
||||
const mode = this.lastMode;
|
||||
this.setState({
|
||||
mode,
|
||||
searchStatus: '',
|
||||
searchResults: [],
|
||||
selectedSearchedRepo: null,
|
||||
showSearchBar: false,
|
||||
selectedPath: this.props.path,
|
||||
initToShowChildren: mode === MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY,
|
||||
});
|
||||
};
|
||||
|
||||
onSearchedItemClick = (item) => {
|
||||
item['type'] = item.is_dir ? 'dir' : 'file';
|
||||
let repo = new RepoInfo(item);
|
||||
this.onDirentItemClick(repo, item.path, item);
|
||||
};
|
||||
|
||||
onSearchedItemDoubleClick = (item) => {
|
||||
if (item.type !== 'dir') return;
|
||||
|
||||
seafileAPI.getRepoInfo(item.repo_id).then(res => {
|
||||
const repoInfo = new RepoInfo(res.data);
|
||||
const path = item.path.substring(0, item.path.length - 1);
|
||||
const mode = item.repo_id === this.props.repoID ? MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY : MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES;
|
||||
this.lastMode = mode;
|
||||
this.setState({
|
||||
mode,
|
||||
selectedRepo: repoInfo,
|
||||
selectedSearchedRepo: repoInfo,
|
||||
selectedPath: path,
|
||||
selectedSearchedItem: { repoID: item.repo_id, filePath: path },
|
||||
showSearchBar: mode === MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES,
|
||||
initToShowChildren: true,
|
||||
});
|
||||
}).catch(err => {
|
||||
const errMessage = Utils.getErrorMsg(err);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
selectSearchedItem = (item) => {
|
||||
this.setState({ selectedSearchedItem: item });
|
||||
};
|
||||
|
||||
onDirentItemClick = (repo, selectedPath) => {
|
||||
this.setState({
|
||||
repo: repo,
|
||||
selectedRepo: repo,
|
||||
selectedPath: selectedPath,
|
||||
errMessage: ''
|
||||
});
|
||||
@ -131,10 +298,6 @@ class CopyDirent extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
onSelectedMode = (mode) => {
|
||||
this.setState({ mode: mode });
|
||||
};
|
||||
|
||||
renderTitle = () => {
|
||||
const { dirent, isMultipleOperation } = this.props;
|
||||
let title = gettext('Copy {placeholder} to');
|
||||
@ -147,48 +310,69 @@ class CopyDirent extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dirent, selectedDirentList, isMultipleOperation, repoID, path } = this.props;
|
||||
const { mode, errMessage } = this.state;
|
||||
const { dirent, selectedDirentList, isMultipleOperation, path } = this.props;
|
||||
const { mode, currentRepo, selectedRepo, selectedPath, showSearchBar, searchStatus, searchResults, selectedSearchedRepo } = this.state;
|
||||
|
||||
const copiedDirent = dirent || selectedDirentList[0];
|
||||
const { permission } = copiedDirent;
|
||||
const { isCustomPermission } = Utils.getUserPermission(permission);
|
||||
|
||||
const LibraryOption = ({ mode, label }) => (
|
||||
<div className={`repo-list-item ${this.state.mode === mode ? 'active' : ''}`} onClick={() => this.onSelectedMode(mode)}>
|
||||
<span className='library'>{label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal className='custom-modal' isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>
|
||||
{isMultipleOperation ? this.renderTitle() : <div dangerouslySetInnerHTML={{ __html: this.renderTitle() }} className="d-flex mw-100"></div>}
|
||||
<Modal className="custom-modal" isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}
|
||||
close={
|
||||
<div className="header-buttons">
|
||||
<button type="button" className="close seahub-modal-btn" data-dismiss="modal" aria-label={gettext('Close')} onClick={this.toggle}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
{(isPro && !showSearchBar) &&
|
||||
<button type="button" className="close seahub-modal-btn" data-dismiss="modal" aria-label={gettext('Search')} onClick={this.onOpenSearchBar}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{isMultipleOperation ? this.renderTitle() : <div dangerouslySetInnerHTML={{ __html: this.renderTitle() }} className="d-flex"></div>}
|
||||
{(isPro && showSearchBar) &&
|
||||
<Searcher
|
||||
onUpdateMode={this.updateMode}
|
||||
onUpdateSearchStatus={this.onUpdateSearchStatus}
|
||||
onUpdateSearchResults={this.onUpdateSearchResults}
|
||||
onClose={this.onCloseSearchBar}
|
||||
/>
|
||||
}
|
||||
</ModalHeader>
|
||||
<Row>
|
||||
<Col className='repo-list-col border-right'>
|
||||
<LibraryOption mode='only_current_library' label={gettext('Current Library')} />
|
||||
{!isCustomPermission && <LibraryOption mode='only_other_libraries' label={gettext('Other Libraries')} />}
|
||||
<LibraryOption mode='recently_used' label={gettext('Recently Used')} />
|
||||
</Col>
|
||||
<Col className='file-list-col'>
|
||||
<ModalBody>
|
||||
<FileChooser
|
||||
repoID={repoID}
|
||||
currentPath={path}
|
||||
onDirentItemClick={this.onDirentItemClick}
|
||||
onRepoItemClick={this.onRepoItemClick}
|
||||
mode={mode}
|
||||
hideLibraryName={false}
|
||||
/>
|
||||
{errMessage && <Alert color="danger" className="mt-2">{errMessage}</Alert>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
</Col>
|
||||
</Row>
|
||||
<SelectDirentBody
|
||||
mode={mode}
|
||||
currentRepo={currentRepo}
|
||||
selectedRepo={selectedRepo}
|
||||
currentPath={path}
|
||||
repoList={this.state.repoList}
|
||||
selectedPath={selectedPath}
|
||||
isSupportOtherLibraries={!isCustomPermission}
|
||||
onCancel={this.toggle}
|
||||
selectRepo={this.selectRepo}
|
||||
setSelectedPath={this.setSelectedPath}
|
||||
setErrMessage={this.setErrMessage}
|
||||
handleSubmit={this.handleSubmit}
|
||||
onUpdateMode={this.updateMode}
|
||||
searchStatus={searchStatus}
|
||||
searchResults={searchResults}
|
||||
selectedSearchedItem={this.state.selectedSearchedItem}
|
||||
onSelectedSearchedItem={this.selectSearchedItem}
|
||||
onSearchedItemClick={this.onSearchedItemClick}
|
||||
onSearchedItemDoubleClick={this.onSearchedItemDoubleClick}
|
||||
selectedSearchedRepo={selectedSearchedRepo}
|
||||
onSelectSearchedRepo={this.selectSearchedRepo}
|
||||
onAddFolder={this.props.onAddFolder}
|
||||
initToShowChildren={this.state.initToShowChildren}
|
||||
fetchRepoInfo={this.fetchRepoInfo}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
||||
import { Modal, ModalBody } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
import '../../css/copy-move-dirent-progress-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
@ -24,7 +26,7 @@ class CopyMoveDirentProgressDialog extends React.Component {
|
||||
};
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog} className="copy-move-dirent-progress-dialog">
|
||||
<ModalHeader toggle={this.props.toggleDialog}>{title}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>{title}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<div className="progress">
|
||||
<div
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { Button, Modal, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { gettext, maxFileName } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
onCreateRepo: PropTypes.func.isRequired,
|
||||
@ -74,13 +75,14 @@ class CreateDepartmentRepoDialog extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle} autoFocus={false}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('New Department Library')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('New Department Library')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="repo-name">{gettext('Name')}</Label>
|
||||
<Input
|
||||
id="repo-name"
|
||||
name="repo-name"
|
||||
onKeyDown={this.handleKeyDown}
|
||||
value={this.state.repoName}
|
||||
onChange={this.handleChange}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { Button, Modal, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Utils, validateName } from '../../utils/utils';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
fileType: PropTypes.string,
|
||||
@ -88,13 +89,14 @@ class CreateFile extends React.Component {
|
||||
const { toggleDialog } = this.props;
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggleDialog} onOpened={this.onAfterModelOpened}>
|
||||
<ModalHeader toggle={toggleDialog}>{gettext('New File')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={toggleDialog}>{gettext('New File')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="fileName">{gettext('Name')}</Label>
|
||||
<Input
|
||||
id="fileName"
|
||||
name="file-name"
|
||||
onKeyDown={this.handleKeyDown}
|
||||
innerRef={this.newInput}
|
||||
value={this.state.childName}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { Button, Modal, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Utils, validateName } from '../../utils/utils';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
fileType: PropTypes.string,
|
||||
@ -76,13 +77,14 @@ class CreateForder extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle} autoFocus={false}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('New Folder')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('New Folder')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="folderName">{gettext('Name')}</Label>
|
||||
<Input
|
||||
id="folderName"
|
||||
name="folder-name"
|
||||
value={this.state.childName}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleChange}
|
||||
|
@ -2,8 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Input, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Label, Input, Button } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
class CreateGroupDialog extends React.Component {
|
||||
|
||||
@ -65,12 +66,13 @@ class CreateGroupDialog extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog} autoFocus={false}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>{gettext('New Group')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>{gettext('New Group')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<label htmlFor="groupName">{gettext('Name')}</label>
|
||||
<Label for="groupName">{gettext('Name')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="groupName"
|
||||
name="group-name"
|
||||
value={this.state.groupName}
|
||||
onChange={this.handleGroupChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { Button, Modal, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
|
||||
import { gettext, enableEncryptedLibrary, repoPasswordMinLength, storages, libraryTemplates } from '../../utils/constants';
|
||||
import { SeahubSelect } from '../common/select';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
libraryType: PropTypes.string.isRequired,
|
||||
@ -116,8 +117,7 @@ class CreateRepoDialog extends React.Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
onPermissionChange = (e) => {
|
||||
let permission = e.target.value;
|
||||
onPermissionChange = (permission) => {
|
||||
this.setState({ permission: permission });
|
||||
};
|
||||
|
||||
@ -181,13 +181,14 @@ class CreateRepoDialog extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle} autoFocus={false}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('New Library')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('New Library')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="repoName">{gettext('Name')}</Label>
|
||||
<Input
|
||||
id="repoName"
|
||||
name="repo-name"
|
||||
onKeyDown={this.handleKeyDown}
|
||||
value={this.state.repoName}
|
||||
onChange={this.handleRepoNameChange}
|
||||
@ -221,11 +222,19 @@ class CreateRepoDialog extends React.Component {
|
||||
|
||||
{this.props.libraryType === 'group' && (
|
||||
<FormGroup>
|
||||
<Label for="exampleSelect">{gettext('Permission')}</Label>
|
||||
<Input type="select" name="select" id="exampleSelect" onChange={this.onPermissionChange} value={this.state.permission}>
|
||||
<option value='rw'>{gettext('Read-Write')}</option>
|
||||
<option value='r'>{gettext('Read-Only')}</option>
|
||||
</Input>
|
||||
<Label>{gettext('Permission')}</Label>
|
||||
<SeahubSelect
|
||||
options={[
|
||||
{ value: 'rw', label: gettext('Read-Write') },
|
||||
{ value: 'r', label: gettext('Read-Only') }
|
||||
]}
|
||||
onChange={selectedOption => this.onPermissionChange(selectedOption.value)}
|
||||
value={{
|
||||
value: this.state.permission,
|
||||
label: this.state.permission === 'rw' ? gettext('Read-Write') : gettext('Read-Only')
|
||||
}}
|
||||
isClearable={false}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{enableEncryptedLibrary &&
|
||||
|
@ -1,119 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, ModalHeader, ModalBody, ModalFooter, Input } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { TAG_COLORS } from '../../constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
onRepoTagCreated: PropTypes.func,
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class CreateTagDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tagName: '',
|
||||
tagColor: TAG_COLORS[0],
|
||||
newTag: {},
|
||||
errorMsg: '',
|
||||
};
|
||||
}
|
||||
|
||||
inputNewName = (e) => {
|
||||
this.setState({
|
||||
tagName: e.target.value,
|
||||
});
|
||||
if (this.state.errorMsg) {
|
||||
this.setState({ errorMsg: '' });
|
||||
}
|
||||
};
|
||||
|
||||
selectTagcolor = (e) => {
|
||||
this.setState({
|
||||
tagColor: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
createTag = () => {
|
||||
let name = this.state.tagName;
|
||||
let color = this.state.tagColor;
|
||||
let repoID = this.props.repoID;
|
||||
seafileAPI.createRepoTag(repoID, name, color).then((res) => {
|
||||
let repoTagID = res.data.repo_tag.repo_tag_id;
|
||||
if (this.props.onRepoTagCreated) this.props.onRepoTagCreated(repoTagID);
|
||||
this.props.toggleCancel();
|
||||
}).catch((error) => {
|
||||
let errMessage;
|
||||
if (error.response.status === 500) {
|
||||
errMessage = gettext('Internal Server Error');
|
||||
} else if (error.response.status === 400) {
|
||||
errMessage = gettext('Tag "{name}" already exists.');
|
||||
errMessage = errMessage.replace('{name}', Utils.HTMLescape(name));
|
||||
}
|
||||
this.setState({ errorMsg: errMessage });
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.createTag();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let canSave = this.state.tagName.trim() ? true : false;
|
||||
return (
|
||||
<Fragment>
|
||||
<ModalHeader toggle={this.props.onClose}>
|
||||
<span className="tag-dialog-back sf3-font sf3-font-arrow rotate-180 d-inline-block" onClick={this.props.toggleCancel} aria-label={gettext('Back')}></span>
|
||||
{gettext('New Tag')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div role="form" className="tag-create">
|
||||
<div className="form-group">
|
||||
<label className="form-label">{gettext('Name')}</label>
|
||||
<Input onKeyDown={this.handleKeyDown} autoFocus={true} value={this.state.tagName} onChange={this.inputNewName}/>
|
||||
<div className="mt-2"><span className="error">{this.state.errorMsg}</span></div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">{gettext('Select a color')}</label>
|
||||
<div className="d-flex justify-content-between">
|
||||
{TAG_COLORS.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="tag-color-option" onChange={this.selectTagcolor}>
|
||||
<label className="colorinput">
|
||||
{index === 0 ?
|
||||
<input name="color" type="radio" value={item} className="colorinput-input" defaultChecked onClick={this.selectTagcolor}></input> :
|
||||
<input name="color" type="radio" value={item} className="colorinput-input" onClick={this.selectTagcolor}></input>}
|
||||
<span className="colorinput-color rounded-circle d-flex align-items-center justify-content-center" style={{ backgroundColor: item }}>
|
||||
<i className="sf2-icon-tick color-selected"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.props.toggleCancel}>{gettext('Cancel')}</Button>
|
||||
{canSave ?
|
||||
<Button color="primary" onClick={this.createTag}>{gettext('Save')}</Button> :
|
||||
<Button color="primary" disabled>{gettext('Save')}</Button>
|
||||
}
|
||||
</ModalFooter>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CreateTagDialog.propTypes = propTypes;
|
||||
|
||||
export default CreateTagDialog;
|
@ -5,19 +5,7 @@ import { gettext } from '../../../utils/constants';
|
||||
import Loading from '../../loading';
|
||||
import OpIcon from '../../op-icon';
|
||||
|
||||
const propTypes = {
|
||||
mode: PropTypes.string,
|
||||
permission: PropTypes.object,
|
||||
onChangeMode: PropTypes.func.isRequired,
|
||||
onUpdateCustomPermission: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class CustomPermissionEditor extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
mode: 'add'
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@ -50,7 +38,6 @@ class CustomPermissionEditor extends React.Component {
|
||||
} else {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onChangePermissionName = (evt) => {
|
||||
@ -108,12 +95,9 @@ class CustomPermissionEditor extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
const { mode } = this.props;
|
||||
const { mode = 'add' } = this.props;
|
||||
const title = mode === 'add' ? gettext('Add permission') : gettext('Edit permission');
|
||||
|
||||
const { isLoading, permission_name, permission_desc, permission, errMessage } = this.state;
|
||||
|
||||
return (
|
||||
<div className="custom-permission">
|
||||
<div className="permission-header">
|
||||
@ -212,6 +196,11 @@ class CustomPermissionEditor extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
CustomPermissionEditor.propTypes = propTypes;
|
||||
CustomPermissionEditor.propTypes = {
|
||||
mode: PropTypes.string,
|
||||
permission: PropTypes.object,
|
||||
onChangeMode: PropTypes.func.isRequired,
|
||||
onUpdateCustomPermission: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CustomPermissionEditor;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
currentNode: PropTypes.object.isRequired,
|
||||
@ -24,7 +25,7 @@ class Delete extends React.Component {
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{title}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.toggle}>{title}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to delete')}{' '}<b>{name}</b> ?</p>
|
||||
</ModalBody>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
@ -53,7 +54,7 @@ class DeleteFolderDialog extends Component {
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggleDialog}>
|
||||
<ModalHeader toggle={toggleDialog}>{gettext('Delete Folder')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={toggleDialog}>{gettext('Delete Folder')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p dangerouslySetInnerHTML={{ __html: message }}></p>
|
||||
{alert_message && <p className="error">{alert_message}</p>}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
repo: PropTypes.object.isRequired,
|
||||
@ -30,12 +31,14 @@ class DeleteRepoDialog extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.getRepoFolderShareInfo(this.props.repo.repo_id).then((res) => {
|
||||
this.setState({
|
||||
sharedToUserCount: res.data['shared_user_emails'].length,
|
||||
sharedToGroupCount: res.data['shared_group_ids'].length,
|
||||
if (this.props.isGetShare) {
|
||||
seafileAPI.getRepoFolderShareInfo(this.props.repo.repo_id).then((res) => {
|
||||
this.setState({
|
||||
sharedToUserCount: res.data['shared_user_emails'].length,
|
||||
sharedToGroupCount: res.data['shared_group_ids'].length,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDeleteRepo = () => {
|
||||
@ -63,7 +66,7 @@ class DeleteRepoDialog extends Component {
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggleDialog}>
|
||||
<ModalHeader toggle={toggleDialog}>{gettext('Delete Library')}</ModalHeader>
|
||||
<SeahubModalHeader toggle={toggleDialog}>{gettext('Delete Library')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<p dangerouslySetInnerHTML={{ __html: message }}></p>
|
||||
{ alert_message != '' && <p className="error" dangerouslySetInnerHTML={{ __html: alert_message }}></p>}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
@ -14,7 +15,7 @@ const propTypes = {
|
||||
function DeleteWikiDialog({ handleSubmit, toggleCancel, title, content, footer }) {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggleCancel}>
|
||||
<ModalHeader toggle={toggleCancel}>{title}</ModalHeader>
|
||||
<SeahubModalHeader toggle={toggleCancel}>{title}</SeahubModalHeader>
|
||||
<ModalBody>{content}</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={toggleCancel}>{gettext('Cancel')}</Button>
|
||||
|
249
frontend/src/components/dialog/department-detail-dialog.js
Normal file
@ -0,0 +1,249 @@
|
||||
import React, { Fragment, } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody } from 'reactstrap';
|
||||
import { gettext, isOrgContext, username } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api.js';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import EmptyTip from '../empty-tip';
|
||||
import Loading from '../loading';
|
||||
import Department from '../../models/department';
|
||||
import SeahubModalHeader from '../common/seahub-modal-header';
|
||||
import DepartmentGroup from './department-detail-widget/department-group';
|
||||
import DepartmentGroupMembers from './department-detail-widget/department-group-members';
|
||||
import DepartmentGroupMemberSelected from './department-detail-widget/department-group-member-selected';
|
||||
import '../../css/manage-members-dialog.css';
|
||||
import '../../css/group-departments.css';
|
||||
|
||||
const propTypes = {
|
||||
groupID: PropTypes.any,
|
||||
toggleManageMembersDialog: PropTypes.func,
|
||||
toggleDepartmentDetailDialog: PropTypes.func,
|
||||
isOwner: PropTypes.bool,
|
||||
addUserShares: PropTypes.func,
|
||||
usedFor: PropTypes.oneOf(['add_group_member', 'add_user_share']),
|
||||
userList: PropTypes.array,
|
||||
};
|
||||
|
||||
class DepartmentDetailDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
departments: [],
|
||||
departmentMembers: [],
|
||||
newMembersTempObj: {},
|
||||
currentDepartment: {},
|
||||
departmentsLoading: true,
|
||||
membersLoading: true,
|
||||
selectedMemberMap: {},
|
||||
departmentsTree: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getSelectedMembers();
|
||||
this.getDepartmentsList();
|
||||
}
|
||||
|
||||
getSelectedMembers = () => {
|
||||
const { usedFor, userList, groupID } = this.props;
|
||||
if (usedFor === 'add_user_share') {
|
||||
let selectedMemberMap = {};
|
||||
selectedMemberMap[username] = true;
|
||||
userList.forEach(member => {
|
||||
selectedMemberMap[member.email] = true;
|
||||
});
|
||||
this.setState({ selectedMemberMap });
|
||||
}
|
||||
else if (usedFor === 'add_group_member') {
|
||||
seafileAPI.listGroupMembers(groupID).then((res) => {
|
||||
const groupMembers = res.data;
|
||||
let selectedMemberMap = {};
|
||||
selectedMemberMap[username] = true;
|
||||
groupMembers.forEach(member => {
|
||||
selectedMemberMap[member.email] = true;
|
||||
});
|
||||
this.setState({ selectedMemberMap });
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onError = (error) => {
|
||||
let errMsg = Utils.getErrorMsg(error, true);
|
||||
if (!error.response || error.response.status !== 403) {
|
||||
toaster.danger(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
initDepartments(departments) {
|
||||
const parentIdMap = {};
|
||||
for (let i = 0; i < departments.length; i++) {
|
||||
let item = departments[i];
|
||||
parentIdMap[item.parent_group_id] = true;
|
||||
}
|
||||
return departments.map(depart => {
|
||||
depart.hasChild = !!parentIdMap[depart.id];
|
||||
depart.isExpanded = false;
|
||||
return depart;
|
||||
});
|
||||
}
|
||||
|
||||
getDepartmentsList = () => {
|
||||
seafileAPI.listAddressBookDepartments().then((res) => {
|
||||
let departments = res.data.departments.map(item => {
|
||||
return new Department(item);
|
||||
});
|
||||
let currentDepartment = departments.length > 0 ? departments[0] : {};
|
||||
let departmentsTree = this.initDepartments(departments);
|
||||
this.setState({
|
||||
departments: departments,
|
||||
currentDepartment: currentDepartment,
|
||||
departmentsLoading: false,
|
||||
departmentsTree: departmentsTree
|
||||
});
|
||||
this.getMembers(currentDepartment.id);
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
getMembers = (department_id) => {
|
||||
this.setState({ membersLoading: true });
|
||||
seafileAPI.listAddressBookDepartmentMembers(department_id).then((res) => {
|
||||
this.setState({
|
||||
departmentMembers: res.data.members,
|
||||
membersLoading: false,
|
||||
});
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggleDepartmentDetailDialog();
|
||||
};
|
||||
|
||||
onMemberChecked = (member) => {
|
||||
if (this.state.departmentMembers.indexOf(member) !== -1) {
|
||||
let newMembersTempObj = this.state.newMembersTempObj;
|
||||
if (member.email in newMembersTempObj) {
|
||||
delete newMembersTempObj[member.email];
|
||||
} else {
|
||||
newMembersTempObj[member.email] = member;
|
||||
}
|
||||
this.setState({ newMembersTempObj: newMembersTempObj });
|
||||
}
|
||||
};
|
||||
|
||||
addGroupMember = () => {
|
||||
let emails = Object.keys(this.state.newMembersTempObj);
|
||||
seafileAPI.addGroupMembers(this.props.groupID, emails).then((res) => {
|
||||
this.toggle();
|
||||
this.props.toggleManageMembersDialog();
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
addUserShares = () => {
|
||||
this.props.addUserShares(this.state.newMembersTempObj);
|
||||
};
|
||||
|
||||
removeSelectedMember = (email) => {
|
||||
let newMembersTempObj = this.state.newMembersTempObj;
|
||||
delete newMembersTempObj[email];
|
||||
this.setState({ newMembersTempObj: newMembersTempObj });
|
||||
};
|
||||
|
||||
setCurrent = (department) => {
|
||||
this.setState({ currentDepartment: department });
|
||||
};
|
||||
|
||||
selectAll = (members) => {
|
||||
let { newMembersTempObj, selectedMemberMap } = this.state;
|
||||
for (let member of members) {
|
||||
if (Object.keys(selectedMemberMap).indexOf(member.email) !== -1) {
|
||||
continue;
|
||||
}
|
||||
newMembersTempObj[member.email] = member;
|
||||
}
|
||||
this.setState({ newMembersTempObj: newMembersTempObj });
|
||||
};
|
||||
|
||||
renderHeader = () => {
|
||||
const title = this.props.usedFor === 'add_group_member' ? gettext('Select group members') : gettext('Select shared users');
|
||||
return <SeahubModalHeader toggle={this.toggle}>{title}</SeahubModalHeader>;
|
||||
};
|
||||
|
||||
render() {
|
||||
let { departmentsLoading, departments } = this.state;
|
||||
if (departmentsLoading) {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
{this.renderHeader()}
|
||||
<ModalBody>
|
||||
<div className="d-flex flex-fill align-items-center"><Loading /></div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyTips = (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
{this.renderHeader()}
|
||||
<ModalBody>
|
||||
<EmptyTip>
|
||||
<h2>{gettext('No departments')}</h2>
|
||||
</EmptyTip>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const details = (
|
||||
<Modal isOpen={true} toggle={this.toggle} className="department-dialog" style={{ maxWidth: '900px' }}>
|
||||
{this.renderHeader()}
|
||||
<ModalBody className="department-dialog-content">
|
||||
<DepartmentGroup
|
||||
departments={this.state.departments}
|
||||
getMembers={this.getMembers}
|
||||
setCurrent={this.setCurrent}
|
||||
currentDepartment={this.state.currentDepartment}
|
||||
loading={this.state.departmentsLoading}
|
||||
departmentsTree={this.state.departmentsTree}
|
||||
/>
|
||||
<DepartmentGroupMembers
|
||||
members={this.state.departmentMembers}
|
||||
memberSelected={this.state.newMembersTempObj}
|
||||
onUserChecked={this.onMemberChecked}
|
||||
currentDepartment={this.state.currentDepartment}
|
||||
selectAll={this.selectAll}
|
||||
loading={this.state.membersLoading}
|
||||
selectedMemberMap={this.state.selectedMemberMap}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
usedFor={this.props.usedFor}
|
||||
/>
|
||||
<DepartmentGroupMemberSelected
|
||||
members={this.state.newMembersTempObj}
|
||||
removeSelectedMember={this.removeSelectedMember}
|
||||
addGroupMember={this.addGroupMember}
|
||||
toggle={this.toggle}
|
||||
addUserShares={this.addUserShares}
|
||||
usedFor={this.props.usedFor}
|
||||
/>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Fragment>
|
||||
{(departments.length > 0 || isOrgContext) ? details : emptyTips}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DepartmentDetailDialog.propTypes = propTypes;
|
||||
|
||||
export default DepartmentDetailDialog;
|
@ -0,0 +1,107 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, ModalFooter } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
const ItemPropTypes = {
|
||||
member: PropTypes.object,
|
||||
removeSelectedMember: PropTypes.func
|
||||
};
|
||||
|
||||
class Item extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
highlight: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ highlight: true });
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ highlight: false });
|
||||
};
|
||||
|
||||
removeSelectedMember = (email) => {
|
||||
this.props.removeSelectedMember(email);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { member } = this.props;
|
||||
return (
|
||||
<tr
|
||||
className={this.state.highlight ? 'tr-highlight group-item' : 'group-item'}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<td width="17%"><img className="avatar" src={member.avatar_url} alt=""/></td>
|
||||
<td width="78%">{member.name}</td>
|
||||
<td width="10%">
|
||||
<i
|
||||
className="sf3-font sf3-font-close cursor-pointer"
|
||||
name={member.email}
|
||||
onClick={this.removeSelectedMember.bind(this, member.email)}>
|
||||
</i>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = ItemPropTypes;
|
||||
|
||||
|
||||
const DepartmentGroupMemberSelectedPropTypes = {
|
||||
members: PropTypes.object.isRequired,
|
||||
removeSelectedMember: PropTypes.func.isRequired,
|
||||
addGroupMember: PropTypes.func.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
usedFor: PropTypes.string,
|
||||
addUserShares: PropTypes.func,
|
||||
};
|
||||
|
||||
class DepartmentGroupMemberSelected extends Component {
|
||||
|
||||
render() {
|
||||
const { members, usedFor } = this.props;
|
||||
return (
|
||||
<div className="department-dialog-member-selected pt-4">
|
||||
<div style={{ height: 'calc(100% - 70px)' }}>
|
||||
<div className='department-dialog-member-head px-4'>
|
||||
<div className='department-name'>{gettext('Selected')}</div>
|
||||
</div>
|
||||
{Object.keys(members).length > 0 &&
|
||||
<table className="department-dialog-member-table">
|
||||
<tbody>
|
||||
{Object.keys(members).map((email, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={index}
|
||||
member={members[email]}
|
||||
removeSelectedMember={this.props.removeSelectedMember}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
|
||||
{usedFor === 'add_group_member' &&
|
||||
<Button color="primary" onClick={this.props.addGroupMember}>{gettext('Add')}</Button>
|
||||
}
|
||||
{usedFor === 'add_user_share' &&
|
||||
<Button color="primary" onClick={this.props.addUserShares}>{gettext('Add')}</Button>
|
||||
}
|
||||
</ModalFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DepartmentGroupMemberSelected.propTypes = DepartmentGroupMemberSelectedPropTypes;
|
||||
|
||||
export default DepartmentGroupMemberSelected;
|
@ -0,0 +1,170 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tooltip } from 'reactstrap';
|
||||
import { gettext, mediaUrl } from '../../../utils/constants';
|
||||
import EmptyTip from '../../empty-tip';
|
||||
import Loading from '../../loading';
|
||||
|
||||
const ItemPropTypes = {
|
||||
member: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
tip: PropTypes.string,
|
||||
memberSelected: PropTypes.object,
|
||||
isMemberSelected: PropTypes.bool,
|
||||
onUserChecked: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class Item extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
highlight: false,
|
||||
tooltipOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ highlight: true });
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ highlight: false });
|
||||
};
|
||||
|
||||
onChange = (e) => {
|
||||
const { member } = this.props;
|
||||
this.props.onUserChecked(member);
|
||||
};
|
||||
|
||||
toggleTooltip = () => {
|
||||
this.setState({ tooltipOpen: !this.state.tooltipOpen });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { member, memberSelected, isMemberSelected, index, tip } = this.props;
|
||||
if (isMemberSelected) {
|
||||
return (
|
||||
<tr
|
||||
className={this.state.highlight ? 'tr-highlight group-item' : 'group-item'}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<td width="13%">
|
||||
<input type="checkbox" className="vam" checked='checked' disabled/>
|
||||
</td>
|
||||
<td width="12%"><img className="avatar" src={member.avatar_url} alt=""/></td>
|
||||
<td width="60%">{member.name}</td>
|
||||
<td width="15%" className={this.state.highlight ? 'visible' : 'invisible' } id={`no-select-${index}`}>
|
||||
<i className="sf3-font-help sf3-font"></i>
|
||||
<Tooltip placement='bottom' isOpen={this.state.tooltipOpen} toggle={this.toggleTooltip} target={`no-select-${index}`} delay={{ show: 0, hide: 0 }} fade={false}>
|
||||
{tip}
|
||||
</Tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<tr
|
||||
className={this.state.highlight ? 'tr-highlight group-item' : 'group-item'}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<td width="13%">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="vam"
|
||||
onChange={this.onChange}
|
||||
checked={(member.email in memberSelected) ? 'checked' : ''}
|
||||
/>
|
||||
</td>
|
||||
<td width="11%"><img className="avatar" src={member.avatar_url} alt=""/></td>
|
||||
<td width="76%">{member.name}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = ItemPropTypes;
|
||||
|
||||
|
||||
const DepartmentGroupMembersPropTypes = {
|
||||
members: PropTypes.array.isRequired,
|
||||
memberSelected: PropTypes.object.isRequired,
|
||||
onUserChecked: PropTypes.func.isRequired,
|
||||
currentDepartment: PropTypes.object.isRequired,
|
||||
selectedMemberMap: PropTypes.object,
|
||||
selectAll: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
usedFor: PropTypes.oneOf(['add_group_member', 'add_user_share']),
|
||||
};
|
||||
|
||||
class DepartmentGroupMembers extends Component {
|
||||
|
||||
selectAll = () => {
|
||||
const { members } = this.props;
|
||||
this.props.selectAll(members);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { members, memberSelected, loading, selectedMemberMap, currentDepartment, usedFor } = this.props;
|
||||
let headerTitle = (currentDepartment.name || '') + ' ' + gettext('members');
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="department-dialog-member pt-4">
|
||||
<div className="w-100">
|
||||
<div className='department-dialog-member-head px-4 mt-4'>
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const enableSelectAll = Object.keys(memberSelected).length < members.length;
|
||||
const tip = usedFor === 'add_group_member' ? gettext('User is already in this group') : gettext('It is already shared to user');
|
||||
return (
|
||||
<div className="department-dialog-member pt-4">
|
||||
<div className="w-100">
|
||||
<div className='department-dialog-member-head px-4'>
|
||||
<div className='department-name'>
|
||||
{headerTitle}
|
||||
</div>
|
||||
{enableSelectAll ?
|
||||
<div className='select-all' onClick={this.selectAll}>{gettext('Select All')}</div>
|
||||
:
|
||||
<div className='select-all-disable'>{gettext('Select All')}</div>
|
||||
}
|
||||
</div>
|
||||
{members.length > 0 ?
|
||||
<Fragment>
|
||||
<table className="department-dialog-member-table">
|
||||
<tbody>
|
||||
{members.map((member, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={index}
|
||||
index={index}
|
||||
member={member}
|
||||
tip={tip}
|
||||
memberSelected={memberSelected}
|
||||
onUserChecked={this.props.onUserChecked}
|
||||
isMemberSelected={selectedMemberMap[member.email]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</Fragment>
|
||||
:
|
||||
<EmptyTip tipSrc={`${mediaUrl}img/no-users-tip.png`}>
|
||||
<h2>{gettext('No members')}</h2>
|
||||
</EmptyTip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DepartmentGroupMembers.propTypes = DepartmentGroupMembersPropTypes;
|
||||
|
||||
export default DepartmentGroupMembers;
|
@ -0,0 +1,143 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Loading from '../../loading';
|
||||
import { isOrgContext } from '../../../utils/constants';
|
||||
|
||||
const ItemPropTypes = {
|
||||
department: PropTypes.object,
|
||||
departments: PropTypes.array,
|
||||
getMembers: PropTypes.func.isRequired,
|
||||
setCurrent: PropTypes.func.isRequired,
|
||||
toggleExpanded: PropTypes.func.isRequired,
|
||||
currentDepartment: PropTypes.object,
|
||||
allMembersClick: PropTypes.bool,
|
||||
};
|
||||
|
||||
class Item extends Component {
|
||||
|
||||
getMembers = (e) => {
|
||||
e.stopPropagation();
|
||||
const { department } = this.props;
|
||||
this.props.getMembers(department.id);
|
||||
this.props.setCurrent(department);
|
||||
};
|
||||
|
||||
toggleExpanded = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.toggleExpanded(this.props.department.id, !this.props.department.isExpanded);
|
||||
};
|
||||
|
||||
renderSubDepartments = () => {
|
||||
const { departments } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{departments.map((department, index) => {
|
||||
if (department.parent_group_id !== this.props.department.id) return null;
|
||||
return (
|
||||
<Item
|
||||
key={department.id}
|
||||
department={department}
|
||||
departments={departments}
|
||||
getMembers={this.props.getMembers}
|
||||
setCurrent={this.props.setCurrent}
|
||||
toggleExpanded={this.props.toggleExpanded}
|
||||
currentDepartment={this.props.currentDepartment}
|
||||
allMembersClick={this.props.allMembersClick}
|
||||
padding={this.props.padding + 10}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { department, currentDepartment, allMembersClick } = this.props;
|
||||
const isCurrent = !allMembersClick && currentDepartment.id === department.id;
|
||||
const { hasChild, isExpanded } = department;
|
||||
return (
|
||||
<>
|
||||
<div className={isCurrent ? 'tr-highlight group-item' : 'group-item'} onClick={this.getMembers} style={{ paddingLeft: `${this.props.padding}px` }}>
|
||||
{hasChild &&
|
||||
<span
|
||||
className={`sf3-font sf3-font-down ${isExpanded ? '' : 'rotate-270'} d-inline-block`}
|
||||
onClick={this.toggleExpanded}
|
||||
style={{ color: '#666' }}
|
||||
>
|
||||
</span>
|
||||
}
|
||||
<span style={hasChild ? { paddingLeft: '8px' } : { paddingLeft: '20px' }}>{department.name}</span>
|
||||
</div>
|
||||
{(isExpanded && hasChild) && this.renderSubDepartments()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = ItemPropTypes;
|
||||
|
||||
|
||||
const DepartmentGroupPropTypes = {
|
||||
departments: PropTypes.array.isRequired,
|
||||
getMembers: PropTypes.func.isRequired,
|
||||
setCurrent: PropTypes.func.isRequired,
|
||||
currentDepartment: PropTypes.object.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
departmentsTree: PropTypes.array,
|
||||
};
|
||||
|
||||
class DepartmentGroup extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
allMembersClick: !!isOrgContext
|
||||
};
|
||||
}
|
||||
|
||||
toggleExpanded = (id, state) => {
|
||||
let departments = this.props.departmentsTree.slice(0);
|
||||
let index = departments.findIndex(item => item.id === id);
|
||||
departments[index].isExpanded = state;
|
||||
this.setState({ departments });
|
||||
};
|
||||
|
||||
getMembers = (department_id) => {
|
||||
this.props.getMembers(department_id);
|
||||
this.setState({ allMembersClick: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading } = this.props;
|
||||
let departments = this.props.departmentsTree;
|
||||
if (loading) {
|
||||
return (<Loading/>);
|
||||
}
|
||||
return (
|
||||
<div className="department-dialog-group">
|
||||
<div>
|
||||
{departments.length > 0 && departments.map((department, index) => {
|
||||
if (department.parent_group_id !== -1) return null;
|
||||
return (
|
||||
<Item
|
||||
key={department.id}
|
||||
department={department}
|
||||
departments={departments}
|
||||
getMembers={this.getMembers}
|
||||
setCurrent={this.props.setCurrent}
|
||||
toggleExpanded={this.toggleExpanded}
|
||||
currentDepartment={this.props.currentDepartment}
|
||||
allMembersClick={this.state.allMembersClick}
|
||||
padding={10}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DepartmentGroup.propTypes = DepartmentGroupPropTypes;
|
||||
|
||||
export default DepartmentGroup;
|
@ -1,21 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
class DismissGroupDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
dismissGroup = () => {
|
||||
let that = this;
|
||||
seafileAPI.deleteGroup(this.props.groupID).then((res) => {
|
||||
that.props.onGroupChanged();
|
||||
const { groupID } = this.props;
|
||||
seafileAPI.deleteGroup(groupID).then((res) => {
|
||||
this.props.onGroupDeleted();
|
||||
toaster.success(gettext('Group deleted'));
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
@ -24,13 +22,13 @@ class DismissGroupDialog extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={this.props.showDismissGroupDialog} toggle={this.props.toggleDismissGroupDialog}>
|
||||
<ModalHeader>{gettext('Delete Group')}</ModalHeader>
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog}>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>{gettext('Delete Group')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<span>{gettext('Really want to delete this group?')}</span>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.props.toggleDismissGroupDialog}>{gettext('Cancel')}</Button>
|
||||
<Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.dismissGroup}>{gettext('Delete')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
@ -39,11 +37,9 @@ class DismissGroupDialog extends React.Component {
|
||||
}
|
||||
|
||||
const DismissGroupDialogPropTypes = {
|
||||
showDismissGroupDialog: PropTypes.bool.isRequired,
|
||||
toggleDismissGroupDialog: PropTypes.func.isRequired,
|
||||
loadGroup: PropTypes.func.isRequired,
|
||||
groupID: PropTypes.string,
|
||||
onGroupChanged: PropTypes.func.isRequired,
|
||||
groupID: PropTypes.number.isRequired,
|
||||
toggleDialog: PropTypes.func.isRequired,
|
||||
onGroupDeleted: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DismissGroupDialog.propTypes = DismissGroupDialogPropTypes;
|
||||
|
@ -1,218 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import CreateTagDialog from './create-tag-dialog';
|
||||
import toaster from '../toast';
|
||||
require('../../css/repo-tag.css');
|
||||
|
||||
const TagItemPropTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
repoTag: PropTypes.object.isRequired,
|
||||
filePath: PropTypes.string.isRequired,
|
||||
fileTagList: PropTypes.array.isRequired,
|
||||
onFileTagChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class TagItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isTagHighlighted: false
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({
|
||||
isTagHighlighted: true
|
||||
});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({
|
||||
isTagHighlighted: false
|
||||
});
|
||||
};
|
||||
|
||||
getRepoTagIdList = () => {
|
||||
let repoTagIdList = [];
|
||||
let fileTagList = this.props.fileTagList || [];
|
||||
repoTagIdList = fileTagList.map((fileTag) => fileTag.repo_tag_id);
|
||||
return repoTagIdList;
|
||||
};
|
||||
|
||||
onEditFileTag = () => {
|
||||
let { repoID, repoTag, filePath } = this.props;
|
||||
let repoTagIdList = this.getRepoTagIdList();
|
||||
if (repoTagIdList.indexOf(repoTag.id) === -1) {
|
||||
let id = repoTag.id;
|
||||
seafileAPI.addFileTag(repoID, filePath, id).then(() => {
|
||||
repoTagIdList = this.getRepoTagIdList();
|
||||
this.props.onFileTagChanged();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
} else {
|
||||
let fileTag = null;
|
||||
let fileTagList = this.props.fileTagList;
|
||||
for (let i = 0; i < fileTagList.length; i++) {
|
||||
if (fileTagList[i].repo_tag_id === repoTag.id) {
|
||||
fileTag = fileTagList[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
seafileAPI.deleteFileTag(repoID, fileTag.id).then(() => {
|
||||
repoTagIdList = this.getRepoTagIdList();
|
||||
this.props.onFileTagChanged();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isTagHighlighted } = this.state;
|
||||
const { repoTag } = this.props;
|
||||
const repoTagIdList = this.getRepoTagIdList();
|
||||
const isTagSelected = repoTagIdList.indexOf(repoTag.id) != -1;
|
||||
return (
|
||||
<li
|
||||
className={`tag-list-item cursor-pointer px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
|
||||
onClick={this.onEditFileTag}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="tag-color w-4 h-4 rounded-circle" style={{ backgroundColor: repoTag.color }}></span>
|
||||
<span className="tag-name mx-2">{repoTag.name}</span>
|
||||
</div>
|
||||
{isTagSelected && <i className="sf2-icon-tick tag-selected-icon"></i>}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TagItem.propTypes = TagItemPropTypes;
|
||||
|
||||
const TagListPropTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
repoTags: PropTypes.array.isRequired,
|
||||
filePath: PropTypes.string.isRequired,
|
||||
fileTagList: PropTypes.array.isRequired,
|
||||
onFileTagChanged: PropTypes.func.isRequired,
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
createNewTag: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class TagList extends React.Component {
|
||||
|
||||
render() {
|
||||
const { repoTags } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<ModalHeader toggle={this.props.toggleCancel}>{gettext('Select Tags')}</ModalHeader>
|
||||
<ModalBody className="px-0">
|
||||
<ul className="tag-list tag-list-container">
|
||||
{repoTags.map((repoTag) => {
|
||||
return (
|
||||
<TagItem
|
||||
key={repoTag.id}
|
||||
repoTag={repoTag}
|
||||
repoID={this.props.repoID}
|
||||
filePath={this.props.filePath}
|
||||
fileTagList={this.props.fileTagList}
|
||||
onFileTagChanged={this.props.onFileTagChanged}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<a
|
||||
href="#"
|
||||
className="add-tag-link px-4 py-2 d-flex align-items-center"
|
||||
onClick={this.props.createNewTag}
|
||||
>
|
||||
<span className="sf2-icon-plus mr-2"></span>
|
||||
{gettext('Create a new tag')}
|
||||
</a>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={this.props.toggleCancel}>{gettext('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TagList.propTypes = TagListPropTypes;
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
repoTags: PropTypes.array.isRequired,
|
||||
filePath: PropTypes.string.isRequired,
|
||||
fileTagList: PropTypes.array.isRequired,
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
onFileTagChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class EditFileTagDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isCreateRepoTagShow: false,
|
||||
isListRepoTagShow: true,
|
||||
};
|
||||
}
|
||||
|
||||
createNewTag = () => {
|
||||
this.setState({
|
||||
isCreateRepoTagShow: !this.state.isCreateRepoTagShow,
|
||||
isListRepoTagShow: !this.state.isListRepoTagShow,
|
||||
});
|
||||
};
|
||||
|
||||
onRepoTagCreated = (repoTagID) => {
|
||||
let { repoID, filePath } = this.props;
|
||||
seafileAPI.addFileTag(repoID, filePath, repoTagID).then(() => {
|
||||
this.props.onFileTagChanged();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.props.toggleCancel} autoFocus={false}>
|
||||
{this.state.isListRepoTagShow &&
|
||||
<TagList
|
||||
repoID={this.props.repoID}
|
||||
repoTags={this.props.repoTags}
|
||||
filePath={this.props.filePath}
|
||||
fileTagList={this.props.fileTagList}
|
||||
onFileTagChanged={this.props.onFileTagChanged}
|
||||
toggleCancel={this.props.toggleCancel}
|
||||
createNewTag={this.createNewTag}
|
||||
/>
|
||||
}
|
||||
{this.state.isCreateRepoTagShow &&
|
||||
<CreateTagDialog
|
||||
repoID={this.props.repoID}
|
||||
onClose={this.props.toggleCancel}
|
||||
toggleCancel={this.createNewTag}
|
||||
onRepoTagCreated={this.onRepoTagCreated}
|
||||
/>
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditFileTagDialog.propTypes = propTypes;
|
||||
|
||||
export default EditFileTagDialog;
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
@ -57,8 +58,8 @@ class UpdateRepoCommitLabels extends React.Component {
|
||||
render() {
|
||||
const { formErrorMsg } = this.state;
|
||||
return (
|
||||
<Modal isOpen={true} centered={true} toggle={this.props.toggleDialog}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>{gettext('Edit labels')}</ModalHeader>
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog}>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>{gettext('Edit labels')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<React.Fragment>
|
||||
<CreatableSelect
|
||||
|
@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dayjs from 'dayjs';
|
||||
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
||||
import { Modal, ModalBody } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import { fileAccessLogAPI } from '../../utils/file-access-log-api';
|
||||
import toaster from '../toast';
|
||||
import Loading from '../loading';
|
||||
import EmptyTip from '../empty-tip';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
import '../../css/file-access-log.css';
|
||||
|
||||
@ -88,9 +89,9 @@ class FileAccessLog extends React.Component {
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog} className="file-access-log-container">
|
||||
<ModalHeader toggle={this.props.toggleDialog}>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>
|
||||
<span dangerouslySetInnerHTML={{ __html: title }} className="d-flex mw-100"></span>
|
||||
</ModalHeader>
|
||||
</SeahubModalHeader>
|
||||
<ModalBody className="file-access-log-content-container" onScroll={this.handleScroll}>
|
||||
{isLoading ? <Loading /> : (
|
||||
<>
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, Alert } from 'reactstrap';
|
||||
import { Button, Form, FormGroup, Label, Input, InputGroup, Alert } from 'reactstrap';
|
||||
import { gettext, shareLinkForceUsePassword, shareLinkPasswordMinLength, shareLinkPasswordStrengthLevel, canSendShareLinkEmail, uploadLinkExpireDaysMin, uploadLinkExpireDaysMax, uploadLinkExpireDaysDefault } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@ -300,14 +300,12 @@ class GenerateUploadLink extends React.Component {
|
||||
<Input type="text" readOnly={true} value={sharedUploadInfo.password} /> :
|
||||
<Input type="text" readOnly={true} value={'***************'} />
|
||||
}
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
aria-label={this.state.storedPasswordVisible ? gettext('Hide') : gettext('Show')}
|
||||
onClick={this.toggleStoredPasswordVisible}
|
||||
className={`link-operation-icon eye-icon sf3-font sf3-font-eye${this.state.storedPasswordVisible ? '' : '-slash'}`}
|
||||
>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button
|
||||
aria-label={this.state.storedPasswordVisible ? gettext('Hide') : gettext('Show')}
|
||||
onClick={this.toggleStoredPasswordVisible}
|
||||
className={`link-operation-icon eye-icon sf3-font sf3-font-eye${this.state.storedPasswordVisible ? '' : '-slash'}`}
|
||||
>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</dd>
|
||||
</>
|
||||
@ -338,15 +336,13 @@ class GenerateUploadLink extends React.Component {
|
||||
) : (
|
||||
<InputGroup className="share-link-details-item">
|
||||
<Input type="text" readOnly={true} value={dayjs(sharedUploadInfo.expire_date).format('YYYY-MM-DD HH:mm:ss')} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
aria-label={gettext('Edit')}
|
||||
title={gettext('Edit')}
|
||||
className="link-operation-icon sf3-font sf3-font-rename"
|
||||
onClick={this.editExpirationToggle}
|
||||
>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button
|
||||
aria-label={gettext('Edit')}
|
||||
title={gettext('Edit')}
|
||||
className="link-operation-icon sf3-font sf3-font-rename"
|
||||
onClick={this.editExpirationToggle}
|
||||
>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
)}
|
||||
</dd>
|
||||
@ -389,14 +385,12 @@ class GenerateUploadLink extends React.Component {
|
||||
<span className="tip">{passwordLengthTip}</span>
|
||||
<InputGroup style={{ width: inputWidth }}>
|
||||
<Input id="passwd" type={this.state.passwordVisible ? 'text' : 'password'} value={this.state.password || ''} onChange={this.inputPassword} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`link-operation-icon sf3-font sf3-font-eye${this.state.passwordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="link-operation-icon sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`link-operation-icon sf3-font sf3-font-eye${this.state.passwordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="link-operation-icon sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
|
111
frontend/src/components/dialog/group-invite-members-dialog.js
Normal file
@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalBody, Label } from 'reactstrap';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import toaster from '../toast';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
|
||||
import '../../css/group-invite-members-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
groupID: PropTypes.number.isRequired,
|
||||
toggleInviteMembersDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class GroupInviteMembersDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
inviteList: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listInviteLinks();
|
||||
}
|
||||
|
||||
listInviteLinks = () => {
|
||||
seafileAPI.getGroupInviteLinks(this.props.groupID).then((res) => {
|
||||
this.setState({ inviteList: res.data.group_invite_link_list });
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
addInviteLink = () => {
|
||||
seafileAPI.addGroupInviteLinks(this.props.groupID).then(() => {
|
||||
this.listInviteLinks();
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
deleteLink = (token) => {
|
||||
seafileAPI.deleteGroupInviteLinks(this.props.groupID, token).then(() => {
|
||||
this.listInviteLinks();
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
onError = (error) => {
|
||||
let errMsg = Utils.getErrorMsg(error, true);
|
||||
if (!error.response || error.response.status !== 403) {
|
||||
toaster.danger(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
copyLink = () => {
|
||||
const inviteLinkItem = this.state.inviteList[0];
|
||||
copy(inviteLinkItem.link);
|
||||
const message = gettext('Invitation link has been copied to clipboard');
|
||||
toaster.success((message), {
|
||||
duration: 2
|
||||
});
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggleInviteMembersDialog();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { inviteList } = this.state;
|
||||
const link = inviteList[0];
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle} className="group-invite-members">
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('Invite members')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
{link ?
|
||||
<>
|
||||
<Label>{gettext('Group invitation link')}</Label>
|
||||
<div className="invite-link-item">
|
||||
<div className="form-item text-truncate">{link.link}</div>
|
||||
<div className="invite-link-copy">
|
||||
<Button color="primary" onClick={this.copyLink} className="invite-link-copy-btn text-truncate">{gettext('Copy')}</Button>
|
||||
</div>
|
||||
<Button color="primary" outline onClick={this.deleteLink.bind(this, link.token)} className="delete-link-btn ml-2">
|
||||
<i className="sf3-font-delete1 sf3-font"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<div className="no-link-tip mb-4">
|
||||
{gettext('No group invitation link yet. Group invitation link let registered users to join the group by clicking a link.')}
|
||||
</div>
|
||||
<Button color="primary" onClick={this.addInviteLink} className="my-4">{gettext('Generate')}</Button>
|
||||
</>
|
||||
}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupInviteMembersDialog.propTypes = propTypes;
|
||||
|
||||
export default GroupInviteMembersDialog;
|
@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
||||
import { Modal, ModalBody } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import toaster from '../toast';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
import Loading from '../loading';
|
||||
|
||||
const propTypes = {
|
||||
groupID: PropTypes.string.isRequired,
|
||||
groupID: PropTypes.number.isRequired,
|
||||
toggleDialog: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@ -82,7 +83,7 @@ class GroupMembers extends React.Component {
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>{`${gettext('Group members')} (${memberNumber})`}</ModalHeader>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>{`${gettext('Group members')} (${memberNumber})`}</SeahubModalHeader>
|
||||
<ModalBody className="px-0 group-members-container" onScroll={this.handleScroll}>
|
||||
{isLoading ? <Loading /> : (
|
||||
<>
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody } from 'reactstrap';
|
||||
import { gettext, mediaUrl, siteName, canAddRepo } from '../../utils/constants';
|
||||
import '../../css/seahub-modal-header.css';
|
||||
|
||||
const propTypes = {
|
||||
toggleDialog: PropTypes.func.isRequired
|
||||
@ -17,7 +18,11 @@ class GuideForNewDialog extends React.Component {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalBody>
|
||||
<button type="button" className="close text-gray" onClick={this.toggle}><span aria-hidden="true">×</span></button>
|
||||
<button type="button" className="close seahub-modal-btn p-0" aria-label={gettext('Close')} onClick={this.toggle}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
<div className="p-2 text-center">
|
||||
<img src={`${mediaUrl}img/welcome.png`} width="408" alt="" />
|
||||
<h3 id="dialogTitle" className="mt-6 mb-4">{gettext('Welcome to {site_name_placeholder}').replace('{site_name_placeholder}', siteName)}</h3>
|
||||
|
@ -1,76 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import Lightbox from '@seafile/react-image-lightbox';
|
||||
import '@seafile/react-image-lightbox/style.css';
|
||||
|
||||
const propTypes = {
|
||||
imageItems: PropTypes.array.isRequired,
|
||||
imageIndex: PropTypes.number.isRequired,
|
||||
closeImagePopup: PropTypes.func.isRequired,
|
||||
moveToPrevImage: PropTypes.func.isRequired,
|
||||
moveToNextImage: PropTypes.func.isRequired,
|
||||
onDeleteImage: PropTypes.func,
|
||||
onRotateImage: PropTypes.func,
|
||||
enableRotate: PropTypes.bool,
|
||||
};
|
||||
|
||||
class ImageDialog extends React.Component {
|
||||
|
||||
downloadImage = (url) => {
|
||||
location.href = url;
|
||||
};
|
||||
|
||||
onViewOriginal = () => {
|
||||
window.open(this.props.imageItems[this.props.imageIndex].url, '_blank');
|
||||
};
|
||||
|
||||
render() {
|
||||
const { imageItems, imageIndex, closeImagePopup, moveToPrevImage, moveToNextImage, onDeleteImage, onRotateImage } = this.props;
|
||||
const imageItemsLength = imageItems.length;
|
||||
if (imageItemsLength === 0) return null;
|
||||
const name = imageItems[imageIndex].name;
|
||||
const mainImg = imageItems[imageIndex];
|
||||
const nextImg = imageItems[(imageIndex + 1) % imageItemsLength];
|
||||
const prevImg = imageItems[(imageIndex + imageItemsLength - 1) % imageItemsLength];
|
||||
// The backend server does not support rotating HEIC images
|
||||
let enableRotate = this.props.enableRotate;
|
||||
const suffix = mainImg.src.slice(mainImg.src.lastIndexOf('.') + 1, mainImg.src.lastIndexOf('?')).toLowerCase();
|
||||
if (suffix === 'heic') {
|
||||
enableRotate = false;
|
||||
}
|
||||
return (
|
||||
<Lightbox
|
||||
wrapperClassName='custom-image-previewer'
|
||||
imageTitle={`${name} (${imageIndex + 1}/${imageItemsLength})`}
|
||||
mainSrc={mainImg.thumbnail || mainImg.src}
|
||||
nextSrc={nextImg.thumbnail || nextImg.src}
|
||||
prevSrc={prevImg.thumbnail || prevImg.src}
|
||||
onCloseRequest={closeImagePopup}
|
||||
onMovePrevRequest={moveToPrevImage}
|
||||
onMoveNextRequest={moveToNextImage}
|
||||
imagePadding={70}
|
||||
imageLoadErrorMessage={gettext('The image could not be loaded.')}
|
||||
prevLabel={gettext('Previous (Left arrow key)')}
|
||||
nextLabel={gettext('Next (Right arrow key)')}
|
||||
closeLabel={gettext('Close (Esc)')}
|
||||
zoomInLabel={gettext('Zoom in')}
|
||||
zoomOutLabel={gettext('Zoom out')}
|
||||
enableRotate={enableRotate}
|
||||
onClickDownload={() => this.downloadImage(imageItems[imageIndex].downloadURL)}
|
||||
onClickDelete={onDeleteImage ? () => onDeleteImage(name) : null}
|
||||
onViewOriginal={this.onViewOriginal}
|
||||
viewOriginalImageLabel={gettext('View original image')}
|
||||
onRotateImage={(onRotateImage && enableRotate) ? (angle) => onRotateImage(imageIndex, angle) : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImageDialog.propTypes = propTypes;
|
||||
|
||||
ImageDialog.defaultProps = {
|
||||
enableRotate: true,
|
||||
};
|
||||
|
||||
export default ImageDialog;
|