mirror of
https://github.com/flarum/core.git
synced 2025-09-03 21:22:47 +02:00
Compare commits
973 Commits
v0.1.0-bet
...
next-front
Author | SHA1 | Date | |
---|---|---|---|
|
fd3f484eaf | ||
|
8b3971f202 | ||
|
5b68b80e73 | ||
|
ef4c9d4f8a | ||
|
4585002118 | ||
|
5451aac693 | ||
|
01b56eecdb | ||
|
bcdcb8c20e | ||
|
d6c99eccdb | ||
|
01cb8ab79d | ||
|
877aed215b | ||
|
57570d960e | ||
|
04c4806f6f | ||
|
4c0339c30e | ||
|
e64dc4ea45 | ||
|
305076814f | ||
|
e31edd29d2 | ||
|
23b423c6ce | ||
|
1af1f472f9 | ||
|
7c86f7a34c | ||
|
14e49269d6 | ||
|
7837fff107 | ||
|
3dfa6bc8cb | ||
|
e47fe288fa | ||
|
03e30d7d4f | ||
|
9836ff6c54 | ||
|
bb1c655c90 | ||
|
bf20fe595a | ||
|
b5db57156b | ||
|
a7d3bdf244 | ||
|
c82f0bde61 | ||
|
986102c1d3 | ||
|
2140619c0b | ||
|
2f714a01ed | ||
|
231d018de5 | ||
|
5d62231004 | ||
|
123c3a93f5 | ||
|
c1eec2b261 | ||
|
60d3d6ef99 | ||
|
7862bd32dd | ||
|
92b555a246 | ||
|
687ec6a199 | ||
|
f788a0a972 | ||
|
e7bec9fe29 | ||
|
57da4e24cb | ||
|
7d1a22bcb5 | ||
|
8cc117d89d | ||
|
31ef02dc2c | ||
|
95c9ff9243 | ||
|
9718f54683 | ||
|
bb1e3278de | ||
|
bbcc33b5b5 | ||
|
30076547e5 | ||
|
7b710d5898 | ||
|
d02b5c9db7 | ||
|
cd70819fd5 | ||
|
20b4619e75 | ||
|
fdec2fd094 | ||
|
d7e4ae09b3 | ||
|
fcfc1b2a37 | ||
|
01eba18164 | ||
|
0dcf7d6aa9 | ||
|
015967a76c | ||
|
3cd59e12f5 | ||
|
26d07699e9 | ||
|
b7d6ba4893 | ||
|
d3753d94ae | ||
|
9349ed13fc | ||
|
91ace15f6d | ||
|
7c1b0bfcf2 | ||
|
542bae6277 | ||
|
275c14ee7f | ||
|
bccc970231 | ||
|
da6f79b34a | ||
|
a3cbec25db | ||
|
2225fdec72 | ||
|
6a532ec14e | ||
|
9416d16ebb | ||
|
d6857b0fe5 | ||
|
2c7e7f5b39 | ||
|
b5b18dd436 | ||
|
4778ae5f74 | ||
|
0936a630ef | ||
|
ec8ae6e03b | ||
|
9ffdeff608 | ||
|
8540932638 | ||
|
974f45e4e8 | ||
|
32ac48c6a9 | ||
|
af5b86806a | ||
|
aeef45b3cd | ||
|
8aa70de765 | ||
|
076a71c621 | ||
|
06c32b668d | ||
|
7af4b8d45f | ||
|
cbba325a87 | ||
|
b7d7e8b18a | ||
|
1031826a3d | ||
|
3612ca7aca | ||
|
c2ee84a115 | ||
|
060745ecb7 | ||
|
dd209b1747 | ||
|
aeb0a411b9 | ||
|
1ebb8bf39a | ||
|
fcdf36b3d0 | ||
|
ab912ba1ad | ||
|
4b8eb5d6e4 | ||
|
0e20949eb0 | ||
|
b2c691a03d | ||
|
dde0de046a | ||
|
7a9795fbc3 | ||
|
f30fac6a94 | ||
|
1fb8092987 | ||
|
ea6b943dbd | ||
|
b9918e6c40 | ||
|
b3e1a023c2 | ||
|
46bb66dd94 | ||
|
96926a180a | ||
|
e58ff71f93 | ||
|
2d5090ef12 | ||
|
f3bdc163fa | ||
|
0df6eee10f | ||
|
971b4c121c | ||
|
9bb7ca5d80 | ||
|
258a4b352d | ||
|
24580ced7a | ||
|
8e90d9f9e2 | ||
|
af36ef3fa9 | ||
|
eef63745e6 | ||
|
c702e911b3 | ||
|
73d2ee825b | ||
|
9f99610542 | ||
|
1192867c4f | ||
|
b048498b84 | ||
|
81f7a39a31 | ||
|
ea12bbaf48 | ||
|
c8122a7879 | ||
|
1a5d7a337d | ||
|
c29ea98d48 | ||
|
3702ffa998 | ||
|
58f9c22375 | ||
|
939a1e9ca8 | ||
|
736f22a31a | ||
|
34f3d93ce5 | ||
|
cba278611a | ||
|
f20b35080b | ||
|
df247925d4 | ||
|
44726633ce | ||
|
0d8c8c3be3 | ||
|
592dd6a927 | ||
|
882d22191f | ||
|
0d99f75a6d | ||
|
d5797dae79 | ||
|
82be1cea5d | ||
|
e1b3642453 | ||
|
7031ef7ef7 | ||
|
371e2ef759 | ||
|
203358a796 | ||
|
ff68c104a6 | ||
|
7b3ac18c14 | ||
|
1ced0456ca | ||
|
c0407ab016 | ||
|
2c5aa138cd | ||
|
53fd7b66b4 | ||
|
a1a22aa4ce | ||
|
cde5d20c4c | ||
|
05c9ce335e | ||
|
08aaba6426 | ||
|
2819fd63aa | ||
|
cc23430a9e | ||
|
1a2174d614 | ||
|
85bd82eab1 | ||
|
d06a834238 | ||
|
32aa3f0cba | ||
|
998bb5708e | ||
|
5f7291db39 | ||
|
f5988bae23 | ||
|
0b3cc0c18f | ||
|
6db27dff4f | ||
|
58d7be95c1 | ||
|
feffe53a86 | ||
|
01a6dccb83 | ||
|
786c2fcfa5 | ||
|
27556fea38 | ||
|
8600d81a5e | ||
|
1ce6afaaeb | ||
|
4bd05ee561 | ||
|
8328c446b0 | ||
|
31997b8fdf | ||
|
2d5a7ce064 | ||
|
a380424de4 | ||
|
c3dfa3560a | ||
|
40a78d302e | ||
|
7c0a72047a | ||
|
15adfc528f | ||
|
be08c32c96 | ||
|
a9199ad9d9 | ||
|
fd44db407c | ||
|
240aa9e83b | ||
|
1177880483 | ||
|
607b3a66ae | ||
|
96eda5cfeb | ||
|
0b0c1055d6 | ||
|
2b9ec71a81 | ||
|
6aa017659f | ||
|
f0f668fb93 | ||
|
b322cf669a | ||
|
8e99059f62 | ||
|
1b7a0ecb33 | ||
|
9bfb797fdc | ||
|
2525e3e7ad | ||
|
a14562b100 | ||
|
d3606b7f7e | ||
|
ac096926af | ||
|
909f52522b | ||
|
38c15c5a08 | ||
|
1f5764e5e3 | ||
|
28f72d5648 | ||
|
9f69b7b846 | ||
|
c05f8b732d | ||
|
58c9a6164a | ||
|
60e50713e7 | ||
|
513c586be3 | ||
|
9f8c2ed458 | ||
|
0cc75be55e | ||
|
fa7871cc16 | ||
|
a884a3592b | ||
|
9637d27b56 | ||
|
3866e518fa | ||
|
9d2d302f2d | ||
|
99dbea4524 | ||
|
77837ef7d1 | ||
|
3f9fe7d33e | ||
|
dd0dc44dd8 | ||
|
18ee8578e8 | ||
|
7256122a43 | ||
|
696f562b0e | ||
|
aca497e7be | ||
|
bc34b858a2 | ||
|
251862222c | ||
|
006ea02227 | ||
|
3c6429aba8 | ||
|
619561cf56 | ||
|
805f86b249 | ||
|
eba782d48f | ||
|
6d809cb023 | ||
|
77a5b59a10 | ||
|
7192c4391b | ||
|
3d812c287f | ||
|
7bd3fa82b1 | ||
|
6b108d99cb | ||
|
777579e146 | ||
|
d8b043dacb | ||
|
645a908dff | ||
|
bf79383204 | ||
|
db53103396 | ||
|
b5a9d0183e | ||
|
8065dc1806 | ||
|
694f5ad2e8 | ||
|
821bce38be | ||
|
45045a2ac1 | ||
|
3000ec695d | ||
|
323339190c | ||
|
5f60297eb1 | ||
|
af5f47bb90 | ||
|
02b110e545 | ||
|
f177c0d8a0 | ||
|
a12b5591c3 | ||
|
5293117c80 | ||
|
181c19eac7 | ||
|
f403feb3b1 | ||
|
b5fc7b9bf5 | ||
|
cd16adfa69 | ||
|
e11401b551 | ||
|
c9112624c0 | ||
|
603537c3d1 | ||
|
0f975da403 | ||
|
66a39bbbf5 | ||
|
6dd190114d | ||
|
01c0cf443b | ||
|
ffaafb92d4 | ||
|
8673a0bc2d | ||
|
b068536dbd | ||
|
4dc9e7741c | ||
|
6d5582e4ac | ||
|
e30f8f261b | ||
|
729103c519 | ||
|
ee8f4f04de | ||
|
8e35afe204 | ||
|
37d7f315d3 | ||
|
b799039c29 | ||
|
b74ca9979f | ||
|
895281acb2 | ||
|
42c9086c32 | ||
|
1fbce0db33 | ||
|
bd50a23966 | ||
|
210bbc800a | ||
|
f97ebfcbc0 | ||
|
9e79470603 | ||
|
328a244f92 | ||
|
d6c6e78193 | ||
|
bc11ec68dd | ||
|
9ae189bb9f | ||
|
179fcfb3ca | ||
|
51da153592 | ||
|
25d18d79fb | ||
|
eb76767e70 | ||
|
98c4883cfd | ||
|
dbbbc689bb | ||
|
16b229649a | ||
|
d1c25a4bad | ||
|
4b2f0c2d1a | ||
|
48be5ac2eb | ||
|
0b3a4264a3 | ||
|
76ea6f3695 | ||
|
7120ba2050 | ||
|
ff77912dc6 | ||
|
53b32eda12 | ||
|
6d69e90662 | ||
|
589e903c71 | ||
|
4fe7acfddf | ||
|
685d5f1517 | ||
|
a5c8ef0566 | ||
|
cb428f1e4a | ||
|
3d11309b35 | ||
|
b13adfec84 | ||
|
b2b5789c25 | ||
|
673a78a203 | ||
|
31caced04c | ||
|
5d88ad2431 | ||
|
96a40fd6ea | ||
|
77086c9be6 | ||
|
3c629f091d | ||
|
820752f61c | ||
|
67f3a4a5bf | ||
|
cd4d669127 | ||
|
238f2fca73 | ||
|
7e33690660 | ||
|
eef895c16f | ||
|
2be964f8e2 | ||
|
2f05a2d80b | ||
|
e6a001335d | ||
|
4c03f13fef | ||
|
588dd7b213 | ||
|
1ca1639139 | ||
|
476c1a5691 | ||
|
3b19fe3a33 | ||
|
65f2d84d55 | ||
|
cf63e063ba | ||
|
cd6e6addf7 | ||
|
1395ce6c30 | ||
|
05732be929 | ||
|
5097d7f9a4 | ||
|
0b3bc9f2ba | ||
|
8087d9ea47 | ||
|
d1c436c4d5 | ||
|
e37c7a9b06 | ||
|
dc757fae5f | ||
|
3b236dd66e | ||
|
e2e5ac8c0c | ||
|
beb2f91fef | ||
|
2391471937 | ||
|
f631b98df6 | ||
|
01cb5c4478 | ||
|
fc517ca94d | ||
|
393fa67d2d | ||
|
cb6ac9e9e2 | ||
|
7d2f24bb47 | ||
|
5a7b57df96 | ||
|
a75a76e95b | ||
|
639f5c0114 | ||
|
15c0a8c2db | ||
|
1b5b91c85b | ||
|
5d5f47aab2 | ||
|
24713733fc | ||
|
56b39f9fba | ||
|
cdbc4b9717 | ||
|
594a2ba8cc | ||
|
445517ee84 | ||
|
b4cf197cc6 | ||
|
102db3c913 | ||
|
0ccfad3931 | ||
|
a6cf10f854 | ||
|
83c22d73a4 | ||
|
952b4693da | ||
|
c7b6426fd4 | ||
|
acdb1ff749 | ||
|
50e56ac0a1 | ||
|
82fc4dd483 | ||
|
5390187a4f | ||
|
e4412178b1 | ||
|
2b5dab73f9 | ||
|
db7a03fbe5 | ||
|
ad95a44e7d | ||
|
59613910b1 | ||
|
13fe162db3 | ||
|
51955504aa | ||
|
05fe4446bf | ||
|
71d2e71908 | ||
|
93f3f22623 | ||
|
ff69dade15 | ||
|
17851c4dfe | ||
|
46dfdf2deb | ||
|
d944a9e618 | ||
|
2143a96c19 | ||
|
d7fe3ca35b | ||
|
48e29ed168 | ||
|
0ad4c0ac61 | ||
|
458f4f811c | ||
|
e90dfe04fd | ||
|
191589e2b1 | ||
|
96c4e6b147 | ||
|
d15a9dc0f0 | ||
|
08312568ba | ||
|
31be2f8f86 | ||
|
89598646c1 | ||
|
b3035c18b6 | ||
|
235c265c06 | ||
|
f1a1a7a806 | ||
|
dfef3c1ff1 | ||
|
fb09cef540 | ||
|
24ed2c0d8f | ||
|
173f88da92 | ||
|
9ecb5f437a | ||
|
97979b2189 | ||
|
efff4c1801 | ||
|
2018e424ec | ||
|
36ad4a8554 | ||
|
3581fe8d1e | ||
|
90ce0fa521 | ||
|
63b5cd0812 | ||
|
2a3240b9d1 | ||
|
e0790de2e5 | ||
|
c99c83435b | ||
|
c8f2d94558 | ||
|
c842fa0184 | ||
|
ad2bbdd115 | ||
|
db06b8c71a | ||
|
3cec7e8b46 | ||
|
60d78cedef | ||
|
2980c94247 | ||
|
9b5ec9d7ba | ||
|
f17f0b5278 | ||
|
be924c4fa0 | ||
|
285e397d05 | ||
|
2e27d5938a | ||
|
be013c6db0 | ||
|
dfc0cf53b0 | ||
|
09ad4a180b | ||
|
194f304752 | ||
|
aaab2cc86e | ||
|
ba7fba9015 | ||
|
4ec108f28a | ||
|
e5a7013c2c | ||
|
df2a199b48 | ||
|
b123e435ff | ||
|
17da649d0a | ||
|
1e33ca4111 | ||
|
8506d095db | ||
|
94a62293eb | ||
|
02bcb0f898 | ||
|
98ea4d1e71 | ||
|
5120d9577e | ||
|
23eaee6b16 | ||
|
15398fcc6d | ||
|
bd1d05ee2c | ||
|
4a6137fdb1 | ||
|
537ab6e41f | ||
|
ace4bcf7d8 | ||
|
159810c335 | ||
|
b7120fb176 | ||
|
1f5219f2a2 | ||
|
e8a6fe2f7b | ||
|
1a2cc6a603 | ||
|
417b7f7972 | ||
|
9e3771cac3 | ||
|
819728d8dd | ||
|
e3c7f5379b | ||
|
41ccade385 | ||
|
6d42bcb5ce | ||
|
096aae7919 | ||
|
5bbcba6332 | ||
|
b671c3ccfa | ||
|
9d89d8a127 | ||
|
6dfe455fd6 | ||
|
1f2eaea960 | ||
|
b2ec380d4c | ||
|
08dbc246dd | ||
|
3767ee4bf6 | ||
|
248de34242 | ||
|
8d671f4de4 | ||
|
6de7038f83 | ||
|
07a20a10fd | ||
|
c8027d344a | ||
|
f7709aff95 | ||
|
46818ccd94 | ||
|
f6f9e45085 | ||
|
ff0ce09620 | ||
|
e86cc39f5b | ||
|
a719d4109f | ||
|
1aaf588341 | ||
|
0fcc8dca46 | ||
|
5a4e3b09cf | ||
|
bf87518161 | ||
|
08dae7b530 | ||
|
aa516fb5c3 | ||
|
1cac48f90a | ||
|
5e476fae16 | ||
|
341ffaced5 | ||
|
595d715b1d | ||
|
8c8de8eb22 | ||
|
5431a90dbd | ||
|
7a8c7518bd | ||
|
08f0425c43 | ||
|
ffb76715f6 | ||
|
9cb45c98d8 | ||
|
e0db5823ee | ||
|
46f7f6b3fe | ||
|
fbcd2cf88c | ||
|
e55b7a14e5 | ||
|
32601d2c98 | ||
|
d9d52dab3c | ||
|
d743e56bc1 | ||
|
0cf000122f | ||
|
973ca16eee | ||
|
262dc70fe1 | ||
|
3efd5fbcb0 | ||
|
c97b01a445 | ||
|
b0b3af0305 | ||
|
387109002e | ||
|
1d9e7b0262 | ||
|
094ad74abc | ||
|
67e9e23df1 | ||
|
1cfae4ad14 | ||
|
9896378b59 | ||
|
287ce2fddd | ||
|
cea1cbc2d6 | ||
|
b9148364fa | ||
|
2ba890c239 | ||
|
55e80f135d | ||
|
81a1c0955b | ||
|
05386b1259 | ||
|
d96e57eabb | ||
|
173de809b8 | ||
|
c432ed7d5c | ||
|
172fffd1ed | ||
|
4bfbf68bca | ||
|
cd411a0c6b | ||
|
7f05d9dce3 | ||
|
b3a5822ddb | ||
|
a1e1635019 | ||
|
1cc5e1cb26 | ||
|
a80d72d165 | ||
|
153a82e937 | ||
|
262a934747 | ||
|
a61929730e | ||
|
ce02387ee4 | ||
|
2c4fae60bc | ||
|
7eab206f91 | ||
|
599958354c | ||
|
2088fceb8b | ||
|
5b25a77e82 | ||
|
59c534a882 | ||
|
c79bda6279 | ||
|
6374f92676 | ||
|
1f4e03d1fa | ||
|
acf67ca416 | ||
|
bd750ca154 | ||
|
61b09ac982 | ||
|
6d895e6d77 | ||
|
e199997231 | ||
|
095e8164e8 | ||
|
0bdf873e65 | ||
|
439b867dde | ||
|
4734dbf46d | ||
|
783a14610a | ||
|
63d00e8b34 | ||
|
351d2d1366 | ||
|
e7b417121a | ||
|
9e3ecd528e | ||
|
3518fb2299 | ||
|
a6eff9383b | ||
|
d806c4491d | ||
|
c9a878d49c | ||
|
30856a8e2b | ||
|
3d3be6983a | ||
|
96b85f1330 | ||
|
25932cf7c4 | ||
|
d497782f65 | ||
|
98ccfdcee5 | ||
|
b4439dc6b3 | ||
|
72a2749943 | ||
|
f13ded1255 | ||
|
c719cc6d8a | ||
|
7dcb99621d | ||
|
4e047bae6a | ||
|
90def3f0db | ||
|
17619843b5 | ||
|
d46316e979 | ||
|
b44ffd9f8d | ||
|
953f81176b | ||
|
119d1721e0 | ||
|
7d4a04760c | ||
|
73c44adb96 | ||
|
eb571c5595 | ||
|
95e3ff8fa8 | ||
|
e1315d27a4 | ||
|
7127bea15e | ||
|
a3a5d0a351 | ||
|
aa7b4dd754 | ||
|
409a63d77a | ||
|
78f6249b24 | ||
|
2edda9baaa | ||
|
49fddbd450 | ||
|
1b3d674c39 | ||
|
400aa4fef9 | ||
|
a4ef9e7cf4 | ||
|
f230c72ebb | ||
|
f0883471ef | ||
|
5e2f659f54 | ||
|
bb250baddf | ||
|
68498cedae | ||
|
f3612261ec | ||
|
0a65d2bb0d | ||
|
59fa623f11 | ||
|
e95cb09caa | ||
|
9836fa64ed | ||
|
415b68f84f | ||
|
c0364cbc9d | ||
|
1cd6908dbb | ||
|
323ced8b00 | ||
|
ea98e4bda9 | ||
|
a471a44ca6 | ||
|
2903a7068c | ||
|
fc7db914db | ||
|
a7c2a7a2d3 | ||
|
2a5c0c1c7a | ||
|
14af6c0e8b | ||
|
c2f802878a | ||
|
b148c9d7da | ||
|
b23e821013 | ||
|
9aeaccf9a4 | ||
|
12830265d9 | ||
|
6d7b826133 | ||
|
9b0aa574f0 | ||
|
845daf1ab6 | ||
|
22ffb76cb5 | ||
|
067552efe5 | ||
|
659cfb72ad | ||
|
49d59089e4 | ||
|
a9eb62880e | ||
|
26a821e3e2 | ||
|
7490709af8 | ||
|
96c42ed337 | ||
|
5cd2d6a79f | ||
|
a3a64749c5 | ||
|
1242fa79af | ||
|
ddfedcb4dd | ||
|
43c44efe3d | ||
|
7e763ec22b | ||
|
e0b6aacc9e | ||
|
d8eed9d171 | ||
|
46ba8a3b8d | ||
|
c08b62af80 | ||
|
a23180f279 | ||
|
cc68c6f503 | ||
|
d0a188bc42 | ||
|
dfb9f23eee | ||
|
044d730480 | ||
|
e145873d59 | ||
|
7b49f3c24c | ||
|
4b3e1b16d9 | ||
|
cde8dd0dc4 | ||
|
bdf626b552 | ||
|
68a7886cec | ||
|
8fc43cac9e | ||
|
a3c11587b7 | ||
|
c7c2d9a755 | ||
|
b928cb523a | ||
|
60bdbe6e52 | ||
|
9772e398f6 | ||
|
b83c81c06e | ||
|
e3569d39cc | ||
|
33dd5fff36 | ||
|
2ae7392dea | ||
|
6df48b04c2 | ||
|
6f7cce5adf | ||
|
4c2ff6e82d | ||
|
b53e612007 | ||
|
208d90293d | ||
|
1c3fda4a71 | ||
|
663de42917 | ||
|
cde7dd3ce1 | ||
|
4580ebe100 | ||
|
60483b2c62 | ||
|
cf42765513 | ||
|
8e5b13903e | ||
|
7387dfb7da | ||
|
0b888ea342 | ||
|
6f1c46819e | ||
|
18def302d6 | ||
|
bddbf24055 | ||
|
0ce014b3bb | ||
|
c3cf5fe074 | ||
|
72a3582287 | ||
|
dd67291ce0 | ||
|
1a3e085a9c | ||
|
78cd35d93c | ||
|
4725ac4131 | ||
|
8c7cdb184f | ||
|
2223e1a13c | ||
|
296b822636 | ||
|
232f3b6bc6 | ||
|
0c065520e4 | ||
|
56e10ce6ba | ||
|
03f862fe8c | ||
|
b4cb5a11da | ||
|
ef2cc9b0cd | ||
|
2a17590412 | ||
|
e251cf34c4 | ||
|
0142b01cc5 | ||
|
8a5eb9cd42 | ||
|
89338290a4 | ||
|
58eaf79a98 | ||
|
f255d318ef | ||
|
9e91ada4a8 | ||
|
06de5c430b | ||
|
a590150698 | ||
|
0a66229169 | ||
|
4e5b3099f8 | ||
|
aa203de6e9 | ||
|
e0aa99fabb | ||
|
6463d912a9 | ||
|
b39a991940 | ||
|
0db4708ef9 | ||
|
5382d0ce1a | ||
|
295f29e53e | ||
|
ce094be83e | ||
|
f5b5d9ca5c | ||
|
040ce52724 | ||
|
56f9016ff7 | ||
|
1f7afb3e4a | ||
|
b179ca1c48 | ||
|
c3374197d1 | ||
|
9529ce9ba2 | ||
|
bac3fe84da | ||
|
a00226c05a | ||
|
7706714ad9 | ||
|
538a3e5e98 | ||
|
f1c40eeccc | ||
|
3efbffdcec | ||
|
02e40f7c47 | ||
|
26143272bd | ||
|
eabd8842ed | ||
|
4851596c78 | ||
|
de216af08d | ||
|
418b1b9bac | ||
|
68369ac5bb | ||
|
7404debb21 | ||
|
88372640aa | ||
|
fdb598187f | ||
|
753808c3f1 | ||
|
dbef2a4c1f | ||
|
35360b690c | ||
|
d2c4569112 | ||
|
b9bda2d443 | ||
|
b126055611 | ||
|
9b3b87e4db | ||
|
91fb24f7a3 | ||
|
393f2de146 | ||
|
6f47f4a86f | ||
|
4c6e03a692 | ||
|
c2ad1181b1 | ||
|
d5d7185794 | ||
|
a0267d9515 | ||
|
69a50565bb | ||
|
273461040c | ||
|
858feb5ac0 | ||
|
ee9862004d | ||
|
0b0f1bc142 | ||
|
153655f1f1 | ||
|
3020710959 | ||
|
db067c7d87 | ||
|
7a0299d246 | ||
|
5598e885b7 | ||
|
264725d872 | ||
|
c7ed189cf3 | ||
|
ab6e3351b4 | ||
|
8e19312534 | ||
|
ed602c6032 | ||
|
d6ed04ffce | ||
|
bd02e4307a | ||
|
3eafed0ae3 | ||
|
f591851cb2 | ||
|
f55d95c9b7 | ||
|
d610ea663f | ||
|
8ab0686666 | ||
|
8937050aed | ||
|
efca923d30 | ||
|
80665450fc | ||
|
4041c18014 | ||
|
514eec7466 | ||
|
8f387bbd52 | ||
|
c4dc1a5ee2 | ||
|
ca09e834b1 | ||
|
4752142c11 | ||
|
6582c5fcf0 | ||
|
6fff3cc0dc | ||
|
0b406a06a1 | ||
|
1fc369c59e | ||
|
f4a4ed8b49 | ||
|
dbd33f687c | ||
|
e038c5c9d9 | ||
|
974d301bed | ||
|
7fb582e8d7 | ||
|
633f84bbe5 | ||
|
84e670082b | ||
|
8b06a6c282 | ||
|
a2b43f6f78 | ||
|
66510d6887 | ||
|
9767bce1e3 | ||
|
ad060126ae | ||
|
ffcba1f173 | ||
|
c019ed6fb0 | ||
|
b0da51309e | ||
|
92437edd1b | ||
|
b695f4d063 | ||
|
8414a59908 | ||
|
64207a53c6 | ||
|
a9e001a4ce | ||
|
fc8dfd8893 | ||
|
701ad0a977 | ||
|
cd5f5515e2 | ||
|
3221e80014 | ||
|
d8c2cbc265 | ||
|
f6ad891850 | ||
|
e524c59f97 | ||
|
cac670e699 | ||
|
d9062ced96 | ||
|
90a3bff638 | ||
|
ddafefc354 | ||
|
fa265152c7 | ||
|
5c98a08e0f | ||
|
6beb4fe898 | ||
|
53f7112248 | ||
|
a2def83045 | ||
|
cbcad27679 | ||
|
9bf485359a | ||
|
60323e0cf9 | ||
|
8cccaaaf6b | ||
|
b7d8afe6a4 | ||
|
ff8ec59310 | ||
|
8eda6c7d36 | ||
|
d5b58b3146 | ||
|
f00d2b1363 | ||
|
190aa925ac | ||
|
60b19efe0a | ||
|
b2fa6b1a2e | ||
|
e7d7df3b0c | ||
|
3b5a01e603 | ||
|
b05f83d25a | ||
|
902d01712b | ||
|
502a3787d5 | ||
|
b8ac49ffcc | ||
|
4b4cea4d87 | ||
|
c0e7ff5ea1 | ||
|
e54944d6c3 | ||
|
d39bca192e | ||
|
efff485d6c | ||
|
6a5427b600 | ||
|
e8621636c5 | ||
|
1aaff46f8e | ||
|
8c4e095f23 | ||
|
05c44ad2df | ||
|
84012ca2fd | ||
|
6393432d92 | ||
|
f6e21b75e1 | ||
|
6ee9412f35 | ||
|
478ca90c31 | ||
|
1f8f79d272 | ||
|
85fc0a3129 | ||
|
db8b9ed0c0 | ||
|
a3d59977b3 | ||
|
211d2d25cd | ||
|
0a992ee9f2 | ||
|
42f1abacaf | ||
|
b26c67dd3c | ||
|
fc7fc41383 | ||
|
a5d3aa9b36 | ||
|
b18909f1af | ||
|
695df18be0 | ||
|
ece23de750 | ||
|
4705600d47 | ||
|
8423de754c | ||
|
b597e6f8f6 | ||
|
276334ec52 | ||
|
9277fca0ec | ||
|
9ca67635fb | ||
|
7a6c48c30b | ||
|
f0186d7674 | ||
|
9bf6862c6d | ||
|
44f460cb11 | ||
|
7cce5b02ba | ||
|
722058f2fb | ||
|
70815b024a | ||
|
7269385786 | ||
|
2f8a449b74 | ||
|
b3aa0298d5 | ||
|
e192402a42 | ||
|
c81ceafb54 | ||
|
93b6f11484 | ||
|
0413daab74 | ||
|
f0c240f863 | ||
|
21dd516eaa | ||
|
3c9d851889 | ||
|
942db77416 | ||
|
04db806995 | ||
|
f3bc7d1c23 | ||
|
bd47653377 | ||
|
07ed4d10c0 | ||
|
25141c0f2f | ||
|
e35bb9e400 | ||
|
753a846e7a | ||
|
d3e57d77b4 | ||
|
6e0bffe395 | ||
|
eec4e97d65 | ||
|
bf83b36882 | ||
|
6aafe54ee7 | ||
|
c91f8de1f5 | ||
|
5783dbe77b | ||
|
ab496eb8f8 | ||
|
6f13a246db | ||
|
4c34d0867d | ||
|
f2a3a0cb10 | ||
|
5b7527144c | ||
|
6c169499b5 | ||
|
5e22458014 | ||
|
c72bdc8238 | ||
|
2438bbfd41 | ||
|
5af5f1fc77 | ||
|
e7f4e5060c | ||
|
bcc16a3329 | ||
|
283abb88c2 | ||
|
af2307868a | ||
|
f9d724738c | ||
|
42e722d824 | ||
|
f5517fbd88 | ||
|
6a0e3fcf2d | ||
|
0ae2d18f28 | ||
|
0474f410a4 | ||
|
9f28b4e8dc | ||
|
f44e9f5140 | ||
|
3e14ef0714 | ||
|
c999226449 | ||
|
ba097dc147 | ||
|
1d1cc9e443 | ||
|
f5d2d2ff79 | ||
|
a04acca92e | ||
|
4033319ed0 | ||
|
a4fe6f3ce3 | ||
|
ae06b45bc1 | ||
|
be33761950 | ||
|
015aaaa899 | ||
|
67f6b8599d | ||
|
12d5e48b95 | ||
|
a41e3e66ce | ||
|
874c023f8a | ||
|
bb3c57f9a4 | ||
|
98a79e957d | ||
|
cf68c95fb8 | ||
|
d5074c5286 | ||
|
41019597d0 | ||
|
b689c9de3b | ||
|
baed659668 |
@@ -12,21 +12,8 @@ insert_final_newline = true
|
|||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.js]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.{css,less}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.html]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.{diff,md}]
|
[*.{diff,md}]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.php]
|
[*.php]
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
**/bower_components/**/*
|
|
||||||
**/node_modules/**/*
|
|
||||||
vendor/**/*
|
|
||||||
**/Gulpfile.js
|
|
||||||
**/dist/**/*
|
|
170
.eslintrc
170
.eslintrc
@@ -1,170 +0,0 @@
|
|||||||
{
|
|
||||||
"parser": "babel-eslint", // https://github.com/babel/babel-eslint
|
|
||||||
"env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments
|
|
||||||
"browser": true // browser global variables
|
|
||||||
},
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"arrowFunctions": true,
|
|
||||||
"blockBindings": true,
|
|
||||||
"classes": true,
|
|
||||||
"defaultParams": true,
|
|
||||||
"destructuring": true,
|
|
||||||
"forOf": true,
|
|
||||||
"generators": false,
|
|
||||||
"modules": true,
|
|
||||||
"objectLiteralComputedProperties": true,
|
|
||||||
"objectLiteralDuplicateProperties": false,
|
|
||||||
"objectLiteralShorthandMethods": true,
|
|
||||||
"objectLiteralShorthandProperties": true,
|
|
||||||
"spread": true,
|
|
||||||
"superInFunctions": true,
|
|
||||||
"templateStrings": true,
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"m": true,
|
|
||||||
"app": true,
|
|
||||||
"$": true,
|
|
||||||
"moment": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
/**
|
|
||||||
* Strict mode
|
|
||||||
*/
|
|
||||||
// babel inserts "use strict"; for us
|
|
||||||
"strict": [2, "never"], // http://eslint.org/docs/rules/strict
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ES6
|
|
||||||
*/
|
|
||||||
"no-var": 2, // http://eslint.org/docs/rules/no-var
|
|
||||||
"prefer-const": 2, // http://eslint.org/docs/rules/prefer-const
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Variables
|
|
||||||
*/
|
|
||||||
"no-shadow": 2, // http://eslint.org/docs/rules/no-shadow
|
|
||||||
"no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
|
|
||||||
"no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars
|
|
||||||
"vars": "local",
|
|
||||||
"args": "after-used"
|
|
||||||
}],
|
|
||||||
"no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible errors
|
|
||||||
*/
|
|
||||||
"comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle
|
|
||||||
"no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign
|
|
||||||
"no-console": 1, // http://eslint.org/docs/rules/no-console
|
|
||||||
"no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
|
|
||||||
"no-alert": 1, // http://eslint.org/docs/rules/no-alert
|
|
||||||
"no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
|
|
||||||
"no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
|
|
||||||
"no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
|
|
||||||
"no-empty": 2, // http://eslint.org/docs/rules/no-empty
|
|
||||||
"no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
|
|
||||||
"no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
|
|
||||||
"no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
|
|
||||||
"no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
|
|
||||||
"no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
|
|
||||||
"no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
|
|
||||||
"no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
|
|
||||||
"no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
|
|
||||||
"no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys
|
|
||||||
"no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
|
|
||||||
"no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
|
|
||||||
"use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
|
|
||||||
"block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Best practices
|
|
||||||
*/
|
|
||||||
"consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
|
|
||||||
"curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
|
|
||||||
"default-case": 2, // http://eslint.org/docs/rules/default-case
|
|
||||||
"dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation
|
|
||||||
"allowKeywords": true
|
|
||||||
}],
|
|
||||||
"eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
|
|
||||||
"no-caller": 2, // http://eslint.org/docs/rules/no-caller
|
|
||||||
"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return
|
|
||||||
"no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
|
|
||||||
"no-eval": 2, // http://eslint.org/docs/rules/no-eval
|
|
||||||
"no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
|
|
||||||
"no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
|
|
||||||
"no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
|
|
||||||
"no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
|
|
||||||
"no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
|
|
||||||
"no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
|
|
||||||
"no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
|
|
||||||
"no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
|
|
||||||
"no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
|
|
||||||
"no-new": 2, // http://eslint.org/docs/rules/no-new
|
|
||||||
"no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
|
|
||||||
"no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
|
|
||||||
"no-octal": 2, // http://eslint.org/docs/rules/no-octal
|
|
||||||
"no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
|
|
||||||
"no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign
|
|
||||||
"no-proto": 2, // http://eslint.org/docs/rules/no-proto
|
|
||||||
"no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
|
|
||||||
"no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
|
|
||||||
"no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
|
|
||||||
"no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
|
|
||||||
"no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
|
|
||||||
"no-with": 2, // http://eslint.org/docs/rules/no-with
|
|
||||||
"radix": 2, // http://eslint.org/docs/rules/radix
|
|
||||||
"vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top
|
|
||||||
"wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
|
|
||||||
"yoda": 2, // http://eslint.org/docs/rules/yoda
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Style
|
|
||||||
*/
|
|
||||||
"indent": [2, 2], // http://eslint.org/docs/rules/indent
|
|
||||||
"brace-style": [2, // http://eslint.org/docs/rules/brace-style
|
|
||||||
"1tbs", {
|
|
||||||
"allowSingleLine": true
|
|
||||||
}],
|
|
||||||
"quotes": [
|
|
||||||
2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
|
|
||||||
],
|
|
||||||
"camelcase": [2, { // http://eslint.org/docs/rules/camelcase
|
|
||||||
"properties": "never"
|
|
||||||
}],
|
|
||||||
"comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing
|
|
||||||
"before": false,
|
|
||||||
"after": true
|
|
||||||
}],
|
|
||||||
"comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
|
|
||||||
"eol-last": 2, // http://eslint.org/docs/rules/eol-last
|
|
||||||
"key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing
|
|
||||||
"beforeColon": false,
|
|
||||||
"afterColon": true
|
|
||||||
}],
|
|
||||||
"new-cap": [2, { // http://eslint.org/docs/rules/new-cap
|
|
||||||
"newIsCap": true
|
|
||||||
}],
|
|
||||||
"no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines
|
|
||||||
"max": 2
|
|
||||||
}],
|
|
||||||
"no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
|
|
||||||
"no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
|
|
||||||
"no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
|
|
||||||
"no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func
|
|
||||||
"no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
|
|
||||||
"one-var": [2, "never"], // http://eslint.org/docs/rules/one-var
|
|
||||||
"padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
|
|
||||||
"semi": [2, "always"], // http://eslint.org/docs/rules/semi
|
|
||||||
"semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing
|
|
||||||
"before": false,
|
|
||||||
"after": true
|
|
||||||
}],
|
|
||||||
"space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords
|
|
||||||
"space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
|
|
||||||
"space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren
|
|
||||||
"space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops
|
|
||||||
"space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case
|
|
||||||
"spaced-line-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment
|
|
||||||
}
|
|
||||||
}
|
|
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -2,3 +2,10 @@
|
|||||||
.gitignore export-ignore
|
.gitignore export-ignore
|
||||||
.gitmodules export-ignore
|
.gitmodules export-ignore
|
||||||
.travis.yml export-ignore
|
.travis.yml export-ignore
|
||||||
|
.editorconfig export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
|
|
||||||
|
phpunit.xml export-ignore
|
||||||
|
tests export-ignore
|
||||||
|
|
||||||
|
js/*/dist/*.js -diff
|
||||||
|
3
.github/CONTRIBUTING.md
vendored
Normal file
3
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Contributing to Flarum
|
||||||
|
|
||||||
|
Howdy! We're really excited that you are interested in contributing to Flarum. Before submitting your contribution, please take a moment and read through the [Contributing Guidelines](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md).
|
26
.github/ISSUE_TEMPLATE.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
> Issues on Github are meant for bug reporting. Please post feature requests on the [discussion forum](https://discuss.flarum.org/t/features).
|
||||||
|
---
|
||||||
|
> Try to complete the below form as far as you are able and are willing to share. Add a screenshot of the issue if you can.
|
||||||
|
|
||||||
|
## Bug report
|
||||||
|
- Version of Flarum: x.y.z
|
||||||
|
- Website URL where the bug is visible: http://example.com
|
||||||
|
- The webserver you are running: apache, nginx or something else
|
||||||
|
- PHP version: x.y.z
|
||||||
|
- Hosted environment: shared or vps
|
||||||
|
- Hosting provider: http://some-amazing-provider.com
|
||||||
|
|
||||||
|
## Flarum info
|
||||||
|
|
||||||
|
```
|
||||||
|
Output of "php flarum info", run this in terminal in your Flarum directory.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional comments
|
||||||
|
Some additional information you'd like to share, eg what have you tried so far.
|
||||||
|
|
||||||
|
## Log files
|
||||||
|
|
||||||
|
```
|
||||||
|
Put any relevant logs here.
|
||||||
|
```
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ composer.phar
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
tests/_output/*
|
tests/_output/*
|
||||||
.vagrant
|
.vagrant
|
||||||
|
.idea/*
|
||||||
|
node_modules
|
||||||
|
bower_components
|
26
.php_cs
26
.php_cs
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
$header = <<<EOF
|
|
||||||
This file is part of Flarum.
|
|
||||||
|
|
||||||
(c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
|
|
||||||
For the full copyright and license information, please view the LICENSE
|
|
||||||
file that was distributed with this source code.
|
|
||||||
EOF;
|
|
||||||
|
|
||||||
Symfony\CS\Fixer\Contrib\HeaderCommentFixer::setHeader($header);
|
|
||||||
|
|
||||||
$finder = Symfony\CS\Finder\DefaultFinder::create()
|
|
||||||
->exclude('stubs')
|
|
||||||
->in(__DIR__);
|
|
||||||
|
|
||||||
return Symfony\CS\Config\Config::create()
|
|
||||||
->setUsingCache(true)
|
|
||||||
->level(Symfony\CS\FixerInterface::PSR2_LEVEL)
|
|
||||||
->fixers([
|
|
||||||
'short_array_syntax',
|
|
||||||
'header_comment',
|
|
||||||
'-psr0'
|
|
||||||
])
|
|
||||||
->finder($finder);
|
|
18
.styleci.yml
Normal file
18
.styleci.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
preset: recommended
|
||||||
|
|
||||||
|
enabled:
|
||||||
|
- logical_not_operators_with_successor_space
|
||||||
|
|
||||||
|
disabled:
|
||||||
|
- align_double_arrow
|
||||||
|
- blank_line_after_opening_tag
|
||||||
|
- multiline_array_trailing_comma
|
||||||
|
- new_with_braces
|
||||||
|
- phpdoc_align
|
||||||
|
- phpdoc_order
|
||||||
|
- phpdoc_separation
|
||||||
|
- phpdoc_types
|
||||||
|
|
||||||
|
finder:
|
||||||
|
exclude:
|
||||||
|
- "stubs"
|
15
.travis.yml
15
.travis.yml
@@ -1,8 +1,10 @@
|
|||||||
language: php
|
language: php
|
||||||
|
|
||||||
php:
|
php:
|
||||||
- 5.5
|
|
||||||
- 5.6
|
- 5.6
|
||||||
|
- 7.0
|
||||||
|
- 7.1
|
||||||
|
- hhvm
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
@@ -10,12 +12,12 @@ matrix:
|
|||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- curl -s http://getcomposer.org/installer | php
|
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi;
|
||||||
- php composer.phar install
|
- composer self-update
|
||||||
|
- composer install
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- vendor/bin/phpcs --standard=PSR2 -np src
|
- vendor/bin/phpunit --coverage-clover=coverage.xml
|
||||||
- vendor/bin/phpspec run
|
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
email:
|
email:
|
||||||
@@ -27,4 +29,7 @@ notifications:
|
|||||||
on_failure: always
|
on_failure: always
|
||||||
on_start: false
|
on_start: false
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
sudo: false
|
sudo: false
|
||||||
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014-2015 Toby Zerner
|
Copyright (c) 2014-2017 Toby Zerner
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# Flarum Core
|
# Flarum Core
|
||||||
|
|
||||||
This repository contains the core code of Flarum. If you want to install Flarum, visit the [main Flarum repository](http://github.com/flarum/flarum).
|
This repository contains Flarum's core code. If you want to set up a forum, visit the [main Flarum repository](http://github.com/flarum/flarum).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Interested in contributing to Flarum? Please read the [Contributing docs](http://flarum.org/docs/contributing) to learn how you can help.
|
Flarum is open-source and we would love your help building it! Please read the [Contributing Guide](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md) to learn how you can help.
|
||||||
|
@@ -1,20 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "flarum/core",
|
"name": "flarum/core",
|
||||||
"description": "",
|
"description": "Delightfully simple forum software.",
|
||||||
|
"keywords": ["forum", "discussion"],
|
||||||
|
"homepage": "http://flarum.org",
|
||||||
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Toby Zerner",
|
"name": "Toby Zerner",
|
||||||
"email": "toby@flarum.org"
|
"email": "toby.zerner@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Franz Liedke",
|
||||||
|
"email": "franz@develophp.org"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/flarum/core/issues",
|
||||||
|
"source": "https://github.com/flarum/core",
|
||||||
|
"docs": "http://flarum.org/docs"
|
||||||
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.4.0",
|
"php": ">=5.6.0",
|
||||||
|
"dflydev/fig-cookies": "^1.0.2",
|
||||||
|
"doctrine/dbal": "^2.5",
|
||||||
|
"components/font-awesome": "^4.6",
|
||||||
|
"franzl/whoops-middleware": "^0.4.0",
|
||||||
"illuminate/bus": "5.1.*",
|
"illuminate/bus": "5.1.*",
|
||||||
"illuminate/cache": "5.1.*",
|
"illuminate/cache": "5.1.*",
|
||||||
"illuminate/config": "5.1.*",
|
"illuminate/config": "5.1.*",
|
||||||
"illuminate/container": "5.1.*",
|
"illuminate/container": "5.1.*",
|
||||||
"illuminate/contracts": "5.1.*",
|
"illuminate/contracts": "5.1.*",
|
||||||
"illuminate/database": "5.1.*",
|
"illuminate/database": "^5.1.31",
|
||||||
"illuminate/events": "5.1.*",
|
"illuminate/events": "5.1.*",
|
||||||
"illuminate/filesystem": "5.1.*",
|
"illuminate/filesystem": "5.1.*",
|
||||||
"illuminate/hashing": "5.1.*",
|
"illuminate/hashing": "5.1.*",
|
||||||
@@ -22,21 +38,26 @@
|
|||||||
"illuminate/support": "5.1.*",
|
"illuminate/support": "5.1.*",
|
||||||
"illuminate/validation": "5.1.*",
|
"illuminate/validation": "5.1.*",
|
||||||
"illuminate/view": "5.1.*",
|
"illuminate/view": "5.1.*",
|
||||||
"league/flysystem": "^1.0.11",
|
|
||||||
"tobscure/json-api": "^0.1.1",
|
|
||||||
"oyejorge/less.php": "~1.5",
|
|
||||||
"intervention/image": "^2.3.0",
|
"intervention/image": "^2.3.0",
|
||||||
"s9e/text-formatter": "^0.1.0",
|
"league/flysystem": "^1.0.11",
|
||||||
"psr/http-message": "^1.0",
|
"league/oauth2-client": "~1.0",
|
||||||
"zendframework/zend-diactoros": "^1.1",
|
"matthiasmullie/minify": "^1.3",
|
||||||
|
"monolog/monolog": "^1.16.0",
|
||||||
"nikic/fast-route": "^0.6",
|
"nikic/fast-route": "^0.6",
|
||||||
"dflydev/fig-cookies": "^1.0",
|
"oyejorge/less.php": "~1.5",
|
||||||
|
"psr/http-message": "^1.0",
|
||||||
"symfony/console": "^2.7",
|
"symfony/console": "^2.7",
|
||||||
"symfony/yaml": "^2.7"
|
"symfony/http-foundation": "^2.7",
|
||||||
|
"symfony/translation": "^2.7",
|
||||||
|
"symfony/yaml": "^2.7",
|
||||||
|
"s9e/text-formatter": "^0.8.1",
|
||||||
|
"tobscure/json-api": "^0.3.0",
|
||||||
|
"zendframework/zend-diactoros": "^1.1",
|
||||||
|
"zendframework/zend-stratigility": "^1.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"squizlabs/php_codesniffer": "2.*",
|
"mockery/mockery": "^0.9.4",
|
||||||
"phpspec/phpspec": "^2.2"
|
"phpunit/phpunit": "^4.8"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -45,5 +66,15 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"src/helpers.php"
|
"src/helpers.php"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "0.1.x-dev"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2392
composer.lock
generated
2392
composer.lock
generated
File diff suppressed because it is too large
Load Diff
13
error/403.html
Normal file
13
error/403.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>403 Forbidden</h1>
|
||||||
|
<p>You do not have permissions to access this page.</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
13
error/404.html
Normal file
13
error/404.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>404 Not Found</h1>
|
||||||
|
<p>Looks like this page could not be found.</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
13
error/500.html
Normal file
13
error/500.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>500 Internal Server Error</h1>
|
||||||
|
<p>Something went wrong on our server.</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
13
error/503.html
Normal file
13
error/503.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>503 Service Unavailable</h1>
|
||||||
|
<p>This forum is down for maintenance.</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
1
js/.gitignore
vendored
1
js/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
bower_components
|
|
4
js/admin/.gitignore
vendored
4
js/admin/.gitignore
vendored
@@ -1,3 +1 @@
|
|||||||
node_modules
|
dist
|
||||||
mithril.js
|
|
||||||
dist
|
|
@@ -1,33 +0,0 @@
|
|||||||
var gulp = require('flarum-gulp');
|
|
||||||
|
|
||||||
var nodeDir = 'node_modules';
|
|
||||||
var bowerDir = '../bower_components';
|
|
||||||
|
|
||||||
gulp({
|
|
||||||
files: [
|
|
||||||
nodeDir + '/babel-core/external-helpers.js',
|
|
||||||
|
|
||||||
bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
|
|
||||||
|
|
||||||
bowerDir + '/mithril/mithril.js',
|
|
||||||
bowerDir + '/jquery/dist/jquery.js',
|
|
||||||
bowerDir + '/moment/moment.js',
|
|
||||||
|
|
||||||
bowerDir + '/bootstrap/js/affix.js',
|
|
||||||
bowerDir + '/bootstrap/js/dropdown.js',
|
|
||||||
bowerDir + '/bootstrap/js/modal.js',
|
|
||||||
bowerDir + '/bootstrap/js/tooltip.js',
|
|
||||||
bowerDir + '/bootstrap/js/transition.js',
|
|
||||||
|
|
||||||
bowerDir + '/spin.js/spin.js',
|
|
||||||
bowerDir + '/spin.js/jquery.spin.js'
|
|
||||||
],
|
|
||||||
modules: {
|
|
||||||
'flarum': [
|
|
||||||
'src/**/*.js',
|
|
||||||
'../lib/**/*.js'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
externalHelpers: true,
|
|
||||||
outputFile: 'dist/app.js'
|
|
||||||
});
|
|
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"devDependencies": {
|
|
||||||
"gulp": "^3.8.11",
|
|
||||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
|
|
||||||
"babel-core": "^5.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
53
js/admin/src/AdminApplication.tsx
Normal file
53
js/admin/src/AdminApplication.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as m from 'mithril';
|
||||||
|
import Application from './lib/Application';
|
||||||
|
import routes from './routes';
|
||||||
|
import Nav from './components/Nav';
|
||||||
|
|
||||||
|
export default class AdminApplication extends Application {
|
||||||
|
/**
|
||||||
|
* A map of extension names to their settings callbacks.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
extensionSettings = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a list of permissions required to have the given permission.
|
||||||
|
*
|
||||||
|
* @param {String} permission
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
getRequiredPermissions(permission) {
|
||||||
|
const required = [];
|
||||||
|
|
||||||
|
if (permission === 'startDiscussion' || permission.indexOf('discussion.') === 0) {
|
||||||
|
required.push('viewDiscussions');
|
||||||
|
}
|
||||||
|
if (permission === 'discussion.delete') {
|
||||||
|
required.push('discussion.hide');
|
||||||
|
}
|
||||||
|
if (permission === 'discussion.deletePosts') {
|
||||||
|
required.push('discussion.editPosts');
|
||||||
|
}
|
||||||
|
|
||||||
|
return required;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
mount() {
|
||||||
|
m.route.prefix('#');
|
||||||
|
|
||||||
|
super.mount();
|
||||||
|
|
||||||
|
m.mount(document.getElementById('nav'), <Nav/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
registerDefaultRoutes(router) {
|
||||||
|
routes(router);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,17 +0,0 @@
|
|||||||
import App from 'flarum/App';
|
|
||||||
import store from 'flarum/initializers/store';
|
|
||||||
import preload from 'flarum/initializers/preload';
|
|
||||||
import routes from 'flarum/initializers/routes';
|
|
||||||
import boot from 'flarum/initializers/boot';
|
|
||||||
|
|
||||||
const app = new App();
|
|
||||||
|
|
||||||
app.initializers.add('store', store);
|
|
||||||
app.initializers.add('routes', routes);
|
|
||||||
|
|
||||||
app.initializers.add('preload', preload, -100);
|
|
||||||
app.initializers.add('boot', boot, -100);
|
|
||||||
|
|
||||||
app.extensionSettings = {};
|
|
||||||
|
|
||||||
export default app;
|
|
@@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Modal from 'flarum/components/Modal';
|
|
||||||
|
|
||||||
export default class AddExtensionModal extends Modal {
|
|
||||||
className() {
|
|
||||||
return 'AddExtensionModal Modal--small';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return 'Add Extension';
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return (
|
|
||||||
<div className="Modal-body">
|
|
||||||
<p>One day in the not-too-distant future, this dialog will allow you to add an extension to your forum with ease. We're building an ecosystem as we speak!</p>
|
|
||||||
<p>In the meantime, if you manage to get your hands on a new extension, simply drop it in your forum's <code>extensions</code> directory.</p>
|
|
||||||
<p>If you're a developer, you can <a href="http://flarum.org/docs/extend">read the docs</a> and have a go at building your own.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
40
js/admin/src/components/AddExtensionModal.tsx
Normal file
40
js/admin/src/components/AddExtensionModal.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import flarum from 'flarum';
|
||||||
|
import Modal from 'flarum/lib/components/Modal';
|
||||||
|
|
||||||
|
export default class AddExtensionModal extends Modal {
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
className() {
|
||||||
|
return super.className() + ' AddExtensionModal Modal--small';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
title() {
|
||||||
|
return flarum.translator.trans('admin.add_extension.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
content() {
|
||||||
|
return (
|
||||||
|
<div className="Modal-body">
|
||||||
|
<p>{flarum.translator.trans('admin.add_extension.temporary_text')}</p>
|
||||||
|
<p>{flarum.translator.trans('admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p>
|
||||||
|
<p>{flarum.translator.trans('admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,24 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import LinkButton from 'flarum/components/LinkButton';
|
|
||||||
|
|
||||||
export default class AdminLinkButton extends LinkButton {
|
|
||||||
getButtonContent() {
|
|
||||||
const content = super.getButtonContent();
|
|
||||||
|
|
||||||
content.push(
|
|
||||||
<div className="AdminLinkButton-description">
|
|
||||||
{this.props.description}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
|
33
js/admin/src/components/AdminLinkButton.tsx
Normal file
33
js/admin/src/components/AdminLinkButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import LinkButton from 'flarum/lib/components/LinkButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class AdminLinkButton extends LinkButton {
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
className() {
|
||||||
|
return super.className() + ' AdminLinkButton';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
content() {
|
||||||
|
const content = super.content();
|
||||||
|
|
||||||
|
content.push(<div className="AdminLinkButton-description">{this.attrs.description}</div>);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Component from 'flarum/Component';
|
|
||||||
import AdminLinkButton from 'flarum/components/AdminLinkButton';
|
|
||||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
|
||||||
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
|
|
||||||
export default class AdminNav extends Component {
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<SelectDropdown
|
|
||||||
className="AdminNav App-titleControl"
|
|
||||||
buttonClassName="Button"
|
|
||||||
children={this.items().toArray()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an item list of links to show in the admin navigation.
|
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
*/
|
|
||||||
items() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('dashboard', AdminLinkButton.component({
|
|
||||||
href: app.route('dashboard'),
|
|
||||||
icon: 'bar-chart',
|
|
||||||
children: 'Dashboard',
|
|
||||||
description: 'Your forum at a glance.'
|
|
||||||
}));
|
|
||||||
|
|
||||||
items.add('basics', AdminLinkButton.component({
|
|
||||||
href: app.route('basics'),
|
|
||||||
icon: 'pencil',
|
|
||||||
children: 'Basics',
|
|
||||||
description: 'Set your forum title, language, and other basic settings.'
|
|
||||||
}));
|
|
||||||
|
|
||||||
items.add('permissions', AdminLinkButton.component({
|
|
||||||
href: app.route('permissions'),
|
|
||||||
icon: 'key',
|
|
||||||
children: 'Permissions',
|
|
||||||
description: 'Configure who can see and do what.'
|
|
||||||
}));
|
|
||||||
|
|
||||||
items.add('appearance', AdminLinkButton.component({
|
|
||||||
href: app.route('appearance'),
|
|
||||||
icon: 'paint-brush',
|
|
||||||
children: 'Appearance',
|
|
||||||
description: 'Customize your forum\'s colors, logos, and other variables.'
|
|
||||||
}));
|
|
||||||
|
|
||||||
items.add('extensions', AdminLinkButton.component({
|
|
||||||
href: app.route('extensions'),
|
|
||||||
icon: 'puzzle-piece',
|
|
||||||
children: 'Extensions',
|
|
||||||
description: 'Add extra functionality to your forum and make it your own.'
|
|
||||||
}));
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
52
js/admin/src/components/AdminNav.tsx
Normal file
52
js/admin/src/components/AdminNav.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Component from 'flarum/lib/Component';
|
||||||
|
import ItemList from 'flarum/lib/utils/ItemList';
|
||||||
|
import AdminLinkButton from 'flarum/components/AdminLinkButton';
|
||||||
|
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||||
|
|
||||||
|
function addLink(items, route, icon) {
|
||||||
|
items.add(route, <AdminLinkButton
|
||||||
|
href={flarum.router.to(route)}
|
||||||
|
icon={icon}
|
||||||
|
children={flarum.translator.trans(`admin.nav.${route}_button`)}
|
||||||
|
description={flarum.translator.trans(`admin.nav.${route}_text`)}/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class Nav extends Component {
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
view() {
|
||||||
|
return <SelectDropdown className="Nav" buttonClassName="Button" children={this.items().toArray()}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list of links to show in the admin navigation.
|
||||||
|
*
|
||||||
|
* @return {ItemList}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
items() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
addLink(items, 'dashboard', 'bar-chart'));
|
||||||
|
addLink(items, 'basics', 'pencil'));
|
||||||
|
addLink(items, 'mail', 'envelope'));
|
||||||
|
addLink(items, 'permissions', 'key'));
|
||||||
|
addLink(items, 'appearance', 'paint-brush'));
|
||||||
|
addLink(items, 'extensions', 'puzzle-piece'));
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,88 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import Switch from 'flarum/components/Switch';
|
|
||||||
import EditCustomCssModal from 'flarum/components/EditCustomCssModal';
|
|
||||||
import saveConfig from 'flarum/utils/saveConfig';
|
|
||||||
|
|
||||||
export default class AppearancePage extends Component {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
this.primaryColor = m.prop(app.config.theme_primary_color);
|
|
||||||
this.secondaryColor = m.prop(app.config.theme_secondary_color);
|
|
||||||
this.darkMode = m.prop(app.config.theme_dark_mode === '1');
|
|
||||||
this.coloredHeader = m.prop(app.config.theme_colored_header === '1');
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<div className="AppearancePage">
|
|
||||||
<div className="container">
|
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
|
||||||
<fieldset className="AppearancePage-colors">
|
|
||||||
<legend>Colors</legend>
|
|
||||||
<div className="helpText">
|
|
||||||
Choose two colors to theme your forum with. The first will be used as a highlight color, while the second will be used to style background elements.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="AppearancePage-colors-input">
|
|
||||||
<input className="FormControl" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
|
||||||
<input className="FormControl" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{Switch.component({
|
|
||||||
state: this.darkMode(),
|
|
||||||
children: 'Dark Mode',
|
|
||||||
onchange: this.darkMode
|
|
||||||
})}
|
|
||||||
|
|
||||||
{Switch.component({
|
|
||||||
state: this.coloredHeader(),
|
|
||||||
children: 'Colored Header',
|
|
||||||
onchange: this.coloredHeader
|
|
||||||
})}
|
|
||||||
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
children: 'Save Changes',
|
|
||||||
loading: this.loading
|
|
||||||
})}
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<legend>Custom Styles</legend>
|
|
||||||
<div className="helpText">
|
|
||||||
Customize your forum's appearance by adding your own LESS/CSS code to be applied on top of Flarum's default styles.
|
|
||||||
</div>
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button',
|
|
||||||
children: 'Edit Custom CSS',
|
|
||||||
onclick: () => app.modal.show(new EditCustomCssModal())
|
|
||||||
})}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
|
|
||||||
|
|
||||||
if (!hex.test(this.primaryColor()) || !hex.test(this.secondaryColor())) {
|
|
||||||
alert('Please enter a hexadecimal color code.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
saveConfig({
|
|
||||||
theme_primary_color: this.primaryColor(),
|
|
||||||
theme_secondary_color: this.secondaryColor(),
|
|
||||||
theme_dark_mode: this.darkMode(),
|
|
||||||
theme_colored_header: this.coloredHeader()
|
|
||||||
}).then(() => window.location.reload());
|
|
||||||
}
|
|
||||||
}
|
|
119
js/admin/src/components/AppearancePage.tsx
Normal file
119
js/admin/src/components/AppearancePage.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import Page from 'flarum/components/Page';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import Switch from 'flarum/components/Switch';
|
||||||
|
import EditCustomCssModal from 'flarum/components/EditCustomCssModal';
|
||||||
|
import EditCustomHeaderModal from 'flarum/components/EditCustomHeaderModal';
|
||||||
|
import UploadImageButton from 'flarum/components/UploadImageButton';
|
||||||
|
import saveSettings from 'flarum/utils/saveSettings';
|
||||||
|
|
||||||
|
export default class AppearancePage extends Page {
|
||||||
|
init() {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
this.primaryColor = m.prop(app.data.settings.theme_primary_color);
|
||||||
|
this.secondaryColor = m.prop(app.data.settings.theme_secondary_color);
|
||||||
|
this.darkMode = m.prop(app.data.settings.theme_dark_mode === '1');
|
||||||
|
this.coloredHeader = m.prop(app.data.settings.theme_colored_header === '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="AppearancePage">
|
||||||
|
<div className="container">
|
||||||
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
|
<fieldset className="AppearancePage-colors">
|
||||||
|
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.colors_text')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="AppearancePage-colors-input">
|
||||||
|
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
||||||
|
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Switch.component({
|
||||||
|
state: this.darkMode(),
|
||||||
|
children: app.translator.trans('core.admin.appearance.dark_mode_label'),
|
||||||
|
onchange: this.darkMode
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Switch.component({
|
||||||
|
state: this.coloredHeader(),
|
||||||
|
children: app.translator.trans('core.admin.appearance.colored_header_label'),
|
||||||
|
onchange: this.coloredHeader
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Button.component({
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
type: 'submit',
|
||||||
|
children: app.translator.trans('core.admin.appearance.submit_button'),
|
||||||
|
loading: this.loading
|
||||||
|
})}
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.logo_text')}
|
||||||
|
</div>
|
||||||
|
<UploadImageButton name="logo"/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.favicon_text')}
|
||||||
|
</div>
|
||||||
|
<UploadImageButton name="favicon"/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.custom_header_text')}
|
||||||
|
</div>
|
||||||
|
{Button.component({
|
||||||
|
className: 'Button',
|
||||||
|
children: app.translator.trans('core.admin.appearance.edit_header_button'),
|
||||||
|
onclick: () => app.modal.show(new EditCustomHeaderModal())
|
||||||
|
})}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.custom_styles_text')}
|
||||||
|
</div>
|
||||||
|
{Button.component({
|
||||||
|
className: 'Button',
|
||||||
|
children: app.translator.trans('core.admin.appearance.edit_css_button'),
|
||||||
|
onclick: () => app.modal.show(new EditCustomCssModal())
|
||||||
|
})}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onsubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
|
||||||
|
|
||||||
|
if (!hex.test(this.primaryColor()) || !hex.test(this.secondaryColor())) {
|
||||||
|
alert(app.translator.trans('core.admin.appearance.enter_hex_message'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
saveSettings({
|
||||||
|
theme_primary_color: this.primaryColor(),
|
||||||
|
theme_secondary_color: this.secondaryColor(),
|
||||||
|
theme_dark_mode: this.darkMode(),
|
||||||
|
theme_colored_header: this.coloredHeader()
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,15 @@
|
|||||||
import Component from 'flarum/Component';
|
import Page from 'flarum/components/Page';
|
||||||
import FieldSet from 'flarum/components/FieldSet';
|
import FieldSet from 'flarum/components/FieldSet';
|
||||||
import Select from 'flarum/components/Select';
|
import Select from 'flarum/components/Select';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
import Alert from 'flarum/components/Alert';
|
import Alert from 'flarum/components/Alert';
|
||||||
import saveConfig from 'flarum/utils/saveConfig';
|
import saveSettings from 'flarum/utils/saveSettings';
|
||||||
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
|
import Switch from 'flarum/components/Switch';
|
||||||
|
|
||||||
export default class BasicsPage extends Component {
|
export default class BasicsPage extends Page {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
@@ -15,20 +17,23 @@ export default class BasicsPage extends Component {
|
|||||||
'forum_title',
|
'forum_title',
|
||||||
'forum_description',
|
'forum_description',
|
||||||
'default_locale',
|
'default_locale',
|
||||||
|
'show_language_selector',
|
||||||
'default_route',
|
'default_route',
|
||||||
'welcome_title',
|
'welcome_title',
|
||||||
'welcome_message'
|
'welcome_message'
|
||||||
];
|
];
|
||||||
this.values = {};
|
this.values = {};
|
||||||
|
|
||||||
const config = app.config;
|
const settings = app.data.settings;
|
||||||
this.fields.forEach(key => this.values[key] = m.prop(config[key]));
|
this.fields.forEach(key => this.values[key] = m.prop(settings[key] || false));
|
||||||
|
|
||||||
this.localeOptions = {};
|
this.localeOptions = {};
|
||||||
const locales = app.locales;
|
const locales = app.data.locales;
|
||||||
for (const i in locales) {
|
for (const i in locales) {
|
||||||
this.localeOptions[i] = `${locales[i]} (${i})`;
|
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof this.values.show_language_selector() !== "number") this.values.show_language_selector(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
@@ -37,17 +42,17 @@ export default class BasicsPage extends Component {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: 'Forum Title',
|
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
||||||
children: [
|
children: [
|
||||||
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
|
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
|
||||||
]
|
]
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: 'Forum Description',
|
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
||||||
children: [
|
children: [
|
||||||
<div className="helpText">
|
<div className="helpText">
|
||||||
Enter a short sentence or two that describes your community. This will appear in the meta tag and show up in search engines.
|
{app.translator.trans('core.admin.basics.forum_description_text')}
|
||||||
</div>,
|
</div>,
|
||||||
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
|
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
|
||||||
]
|
]
|
||||||
@@ -55,44 +60,47 @@ export default class BasicsPage extends Component {
|
|||||||
|
|
||||||
{Object.keys(this.localeOptions).length > 1
|
{Object.keys(this.localeOptions).length > 1
|
||||||
? FieldSet.component({
|
? FieldSet.component({
|
||||||
label: 'Default Language',
|
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
||||||
children: [
|
children: [
|
||||||
Select.component({
|
Select.component({
|
||||||
options: this.localeOptions,
|
options: this.localeOptions,
|
||||||
|
value: this.values.default_locale(),
|
||||||
onchange: this.values.default_locale
|
onchange: this.values.default_locale
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
|
{Switch.component({
|
||||||
|
state: this.values.show_language_selector(),
|
||||||
|
onchange: this.values.show_language_selector,
|
||||||
|
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
||||||
|
})}
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: 'Home Page',
|
label: app.translator.trans('core.admin.basics.home_page_heading'),
|
||||||
className: 'BasicsPage-homePage',
|
className: 'BasicsPage-homePage',
|
||||||
children: [
|
children: [
|
||||||
<div className="helpText">
|
<div className="helpText">
|
||||||
Choose the page which users will first see when they visit your forum. If entering a custom value, use the path relative to the forum root.
|
{app.translator.trans('core.admin.basics.home_page_text')}
|
||||||
</div>,
|
</div>,
|
||||||
<label className="checkbox">
|
this.homePageItems().toArray().map(({path, label}) =>
|
||||||
<input type="radio" name="homePage" value="/all" checked={this.values.default_route() === '/all'} onclick={m.withAttr('value', this.values.default_route)}/>
|
<label className="checkbox">
|
||||||
All Discussions
|
<input type="radio" name="homePage" value={path} checked={this.values.default_route() === path} onclick={m.withAttr('value', this.values.default_route)}/>
|
||||||
</label>,
|
{label}
|
||||||
<label className="checkbox">
|
</label>
|
||||||
<input type="radio" name="homePage" value="custom" checked={this.values.default_route() !== '/all'} onclick={() => {
|
)
|
||||||
this.values.default_route('');
|
|
||||||
m.redraw(true);
|
|
||||||
this.$('.BasicsPage-homePage input').select();
|
|
||||||
}}/>
|
|
||||||
Custom <input className="FormControl" value={this.values.default_route()} oninput={m.withAttr('value', this.values.default_route)} style={this.values.default_route() !== '/all' ? 'margin-top: 5px' : 'display:none'}/>
|
|
||||||
</label>
|
|
||||||
]
|
]
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: 'Welcome Banner',
|
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
||||||
className: 'BasicsPage-welcomeBanner',
|
className: 'BasicsPage-welcomeBanner',
|
||||||
children: [
|
children: [
|
||||||
<div className="helpText">
|
<div className="helpText">
|
||||||
Configure the text that displays in the banner on the All Discussions page. Use this to welcome guests to your forum.
|
{app.translator.trans('core.admin.basics.welcome_banner_text')}
|
||||||
</div>,
|
</div>,
|
||||||
<div className="BasicsPage-welcomeBanner-input">
|
<div className="BasicsPage-welcomeBanner-input">
|
||||||
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
|
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
|
||||||
@@ -104,7 +112,7 @@ export default class BasicsPage extends Component {
|
|||||||
{Button.component({
|
{Button.component({
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
children: 'Save Changes',
|
children: app.translator.trans('core.admin.basics.submit_button'),
|
||||||
loading: this.loading,
|
loading: this.loading,
|
||||||
disabled: !this.changed()
|
disabled: !this.changed()
|
||||||
})}
|
})}
|
||||||
@@ -115,9 +123,25 @@ export default class BasicsPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changed() {
|
changed() {
|
||||||
const config = app.config;
|
return this.fields.some(key => this.values[key]() !== app.data.settings[key]);
|
||||||
|
}
|
||||||
|
|
||||||
return this.fields.some(key => this.values[key]() !== config[key]);
|
/**
|
||||||
|
* Build a list of options for the default homepage. Each option must be an
|
||||||
|
* object with `path` and `label` properties.
|
||||||
|
*
|
||||||
|
* @return {ItemList}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
homePageItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('allDiscussions', {
|
||||||
|
path: '/all',
|
||||||
|
label: app.translator.trans('core.admin.basics.all_discussions_label')
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
onsubmit(e) {
|
onsubmit(e) {
|
||||||
@@ -128,15 +152,16 @@ export default class BasicsPage extends Component {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
app.alerts.dismiss(this.successAlert);
|
app.alerts.dismiss(this.successAlert);
|
||||||
|
|
||||||
const config = {};
|
const settings = {};
|
||||||
|
|
||||||
this.fields.forEach(key => config[key] = this.values[key]());
|
this.fields.forEach(key => settings[key] = this.values[key]());
|
||||||
|
|
||||||
saveConfig(config)
|
saveSettings(settings)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
app.alerts.show(this.successAlert = new Alert({type: 'success', children: 'Your changes were saved.'}));
|
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.catch(() => {})
|
||||||
|
.then(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
});
|
});
|
@@ -1,21 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
|
|
||||||
export default class DashboardPage extends Component {
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<div className="DashboardPage">
|
|
||||||
<div className="container">
|
|
||||||
<h2>Welcome to Flarum Beta</h2>
|
|
||||||
<p>Thanks for trying out Flarum! You are running version <strong>{app.forum.attribute('version')}</strong>. This is beta software, and should not be used in production.</p>
|
|
||||||
<ul>
|
|
||||||
<li>Having problems? Read the <a href="http://flarum.org/docs/troubleshooting" target="_blank">Troubleshooting docs</a>.</li>
|
|
||||||
<li>Found a bug? Please <a href="https://github.com/flarum/core/issues" target="_blank">report it on GitHub</a>.</li>
|
|
||||||
<li>Got some feedback? Let us know what you think on the <a href="http://discuss.flarum.org" target="_blank">Support Forum</a>.</li>
|
|
||||||
<li>Want to contribute? Read the <a href="http://flarum.org/docs/contributing" target="_blank">Contributing docs</a>.</li>
|
|
||||||
<li>Interested in developing extensions? Read the <a href="http://flarum.org/docs/extend" target="_blank">Extension docs</a>.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
22
js/admin/src/components/DashboardPage.tsx
Normal file
22
js/admin/src/components/DashboardPage.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import Page from 'flarum/components/Page';
|
||||||
|
|
||||||
|
export default class DashboardPage extends Page {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="DashboardPage">
|
||||||
|
<div className="container">
|
||||||
|
<h2>{app.translator.trans('core.admin.dashboard.welcome_text')}</h2>
|
||||||
|
<p>{app.translator.trans('core.admin.dashboard.version_text', {version: <strong>{app.forum.attribute('version')}</strong>})}</p>
|
||||||
|
<p>{app.translator.trans('core.admin.dashboard.beta_warning_text', {strong: <strong/>})}</p>
|
||||||
|
<ul>
|
||||||
|
<li>{app.translator.trans('core.admin.dashboard.contributing_text', {a: <a href="http://flarum.org/docs/contributing" target="_blank"/>})}</li>
|
||||||
|
<li>{app.translator.trans('core.admin.dashboard.troubleshooting_text', {a: <a href="http://flarum.org/docs/troubleshooting" target="_blank"/>})}</li>
|
||||||
|
<li>{app.translator.trans('core.admin.dashboard.support_text', {a: <a href="http://discuss.flarum.org/t/support" target="_blank"/>})}</li>
|
||||||
|
<li>{app.translator.trans('core.admin.dashboard.features_text', {a: <a href="http://discuss.flarum.org/t/features" target="_blank"/>})}</li>
|
||||||
|
<li>{app.translator.trans('core.admin.dashboard.extension_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,51 +0,0 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import saveConfig from 'flarum/utils/saveConfig';
|
|
||||||
|
|
||||||
export default class EditCustomCssModal extends Modal {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
this.customLess = m.prop(app.config.custom_less || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
return 'EditCustomCssModal Modal--large';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return 'Edit Custom CSS';
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return (
|
|
||||||
<div className="Modal-body">
|
|
||||||
<p>Customize your forum's appearance by adding your own LESS/CSS code to be applied on top of Flarum's default styles. <a href="">Read the documentation</a> for more information.</p>
|
|
||||||
|
|
||||||
<div className="Form">
|
|
||||||
<div className="Form-group">
|
|
||||||
<textarea className="FormControl" rows="30" value={this.customLess()} onchange={m.withAttr('value', this.customLess)}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
children: 'Save Changes',
|
|
||||||
loading: this.loading
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
saveConfig({
|
|
||||||
custom_less: this.customLess()
|
|
||||||
}).then(() => window.location.reload());
|
|
||||||
}
|
|
||||||
}
|
|
24
js/admin/src/components/EditCustomCssModal.tsx
Normal file
24
js/admin/src/components/EditCustomCssModal.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import SettingsModal from 'flarum/components/SettingsModal';
|
||||||
|
|
||||||
|
export default class EditCustomCssModal extends SettingsModal {
|
||||||
|
className() {
|
||||||
|
return 'EditCustomCssModal Modal--large';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return app.translator.trans('core.admin.edit_css.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
form() {
|
||||||
|
return [
|
||||||
|
<p>{app.translator.trans('core.admin.edit_css.customize_text', {a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank"/>})}</p>,
|
||||||
|
<div className="Form-group">
|
||||||
|
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')}/>
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
onsaved() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
24
js/admin/src/components/EditCustomHeaderModal.tsx
Normal file
24
js/admin/src/components/EditCustomHeaderModal.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import SettingsModal from 'flarum/components/SettingsModal';
|
||||||
|
|
||||||
|
export default class EditCustomHeaderModal extends SettingsModal {
|
||||||
|
className() {
|
||||||
|
return 'EditCustomHeaderModal Modal--large';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return app.translator.trans('core.admin.edit_header.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
form() {
|
||||||
|
return [
|
||||||
|
<p>{app.translator.trans('core.admin.edit_header.customize_text')}</p>,
|
||||||
|
<div className="Form-group">
|
||||||
|
<textarea className="FormControl" rows="30" bidi={this.setting('custom_header')}/>
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
onsaved() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
@@ -8,9 +8,7 @@ import Group from 'flarum/models/Group';
|
|||||||
* to create or edit a group.
|
* to create or edit a group.
|
||||||
*/
|
*/
|
||||||
export default class EditGroupModal extends Modal {
|
export default class EditGroupModal extends Modal {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
|
||||||
|
|
||||||
this.group = this.props.group || app.store.createRecord('groups');
|
this.group = this.props.group || app.store.createRecord('groups');
|
||||||
|
|
||||||
this.nameSingular = m.prop(this.group.nameSingular() || '');
|
this.nameSingular = m.prop(this.group.nameSingular() || '');
|
||||||
@@ -30,7 +28,7 @@ export default class EditGroupModal extends Modal {
|
|||||||
style: {backgroundColor: this.color()}
|
style: {backgroundColor: this.color()}
|
||||||
}) : '',
|
}) : '',
|
||||||
' ',
|
' ',
|
||||||
this.namePlural() || 'Create Group'
|
this.namePlural() || app.translator.trans('core.admin.edit_group.title')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,22 +37,22 @@ export default class EditGroupModal extends Modal {
|
|||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
<div className="Form">
|
<div className="Form">
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<label>Name</label>
|
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
|
||||||
<div className="EditGroupModal-name-input">
|
<div className="EditGroupModal-name-input">
|
||||||
<input className="FormControl" placeholder="Singular (e.g. Mod)" value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
|
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
|
||||||
<input className="FormControl" placeholder="Plural (e.g. Mods)" value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
|
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<label>Color</label>
|
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
|
||||||
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
|
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<label>Icon</label>
|
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
|
||||||
<div className="helpText">
|
<div className="helpText">
|
||||||
Enter the name of any <a href="http://fortawesome.github.io/Font-Awesome/icons/" tabindex="-1">FontAwesome</a> icon class, <em>without</em> the <code>fa-</code> prefix.
|
{app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="http://fortawesome.github.io/Font-Awesome/icons/" tabindex="-1"/>})}
|
||||||
</div>
|
</div>
|
||||||
<input className="FormControl" placeholder="bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
|
<input className="FormControl" placeholder="bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,11 +62,11 @@ export default class EditGroupModal extends Modal {
|
|||||||
type: 'submit',
|
type: 'submit',
|
||||||
className: 'Button Button--primary EditGroupModal-save',
|
className: 'Button Button--primary EditGroupModal-save',
|
||||||
loading: this.loading,
|
loading: this.loading,
|
||||||
children: 'Save Changes'
|
children: app.translator.trans('core.admin.edit_group.submit_button')
|
||||||
})}
|
})}
|
||||||
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
||||||
<button type="button" className="Button EditGroupModal-delete" onclick={this.delete.bind(this)}>
|
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
|
||||||
Delete Group
|
{app.translator.trans('core.admin.edit_group.delete_button')}
|
||||||
</button>
|
</button>
|
||||||
) : ''}
|
) : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -87,17 +85,16 @@ export default class EditGroupModal extends Modal {
|
|||||||
namePlural: this.namePlural(),
|
namePlural: this.namePlural(),
|
||||||
color: this.color(),
|
color: this.color(),
|
||||||
icon: this.icon()
|
icon: this.icon()
|
||||||
}).then(
|
}, {errorHandler: this.onerror.bind(this)})
|
||||||
() => this.hide(),
|
.then(this.hide.bind(this))
|
||||||
() => {
|
.catch(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
deleteGroup() {
|
||||||
if (confirm('Are you sure you want to delete this group? The group members will NOT be deleted.')) {
|
if (confirm(app.translator.trans('core.admin.edit_group.delete_confirmation'))) {
|
||||||
this.group.delete().then(() => m.redraw());
|
this.group.delete().then(() => m.redraw());
|
||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
@@ -1,115 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import LinkButton from 'flarum/components/LinkButton';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import Dropdown from 'flarum/components/Dropdown';
|
|
||||||
import Separator from 'flarum/components/Separator';
|
|
||||||
import AddExtensionModal from 'flarum/components/AddExtensionModal';
|
|
||||||
import LoadingModal from 'flarum/components/LoadingModal';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
import icon from 'flarum/helpers/icon';
|
|
||||||
|
|
||||||
export default class ExtensionsPage extends Component {
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<div className="ExtensionsPage">
|
|
||||||
<div className="ExtensionsPage-header">
|
|
||||||
<div className="container">
|
|
||||||
{Button.component({
|
|
||||||
children: 'Add Extension',
|
|
||||||
icon: 'plus',
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
onclick: () => app.modal.show(new AddExtensionModal())
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ExtensionsPage-list">
|
|
||||||
<div className="container">
|
|
||||||
<ul className="ExtensionList">
|
|
||||||
{app.extensions
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
.map(extension => (
|
|
||||||
<li className={'ExtensionListItem ' + (!this.isEnabled(extension.name) ? 'disabled' : '')}>
|
|
||||||
{Dropdown.component({
|
|
||||||
icon: 'ellipsis-v',
|
|
||||||
children: this.controlItems(extension).toArray(),
|
|
||||||
className: 'ExtensionListItem-controls',
|
|
||||||
buttonClassName: 'Button Button--icon Button--flat',
|
|
||||||
menuClassName: 'Dropdown-menu--right'
|
|
||||||
})}
|
|
||||||
<div className="ExtensionListItem-content">
|
|
||||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
|
||||||
{extension.icon ? icon(extension.icon.name) : ''}
|
|
||||||
</span>
|
|
||||||
<h4 className="ExtensionListItem-title">
|
|
||||||
{extension.title}{' '}
|
|
||||||
<small className="ExtensionListItem-version">{extension.version}</small>
|
|
||||||
</h4>
|
|
||||||
<div className="ExtensionListItem-description">{extension.description}</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
controlItems(extension) {
|
|
||||||
const items = new ItemList();
|
|
||||||
const enabled = this.isEnabled(extension.name);
|
|
||||||
|
|
||||||
if (app.extensionSettings[extension.name]) {
|
|
||||||
items.add('settings', Button.component({
|
|
||||||
icon: 'cog',
|
|
||||||
children: 'Settings',
|
|
||||||
onclick: app.extensionSettings[extension.name]
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
items.add('toggle', Button.component({
|
|
||||||
icon: 'power-off',
|
|
||||||
children: enabled ? 'Disable' : 'Enable',
|
|
||||||
onclick: () => {
|
|
||||||
app.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + extension.name,
|
|
||||||
method: 'PATCH',
|
|
||||||
data: {enabled: !enabled}
|
|
||||||
}).then(() => window.location.reload());
|
|
||||||
|
|
||||||
app.modal.show(new LoadingModal());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
items.add('uninstall', Button.component({
|
|
||||||
icon: 'trash-o',
|
|
||||||
children: 'Uninstall',
|
|
||||||
onclick: () => {
|
|
||||||
app.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + extension.name,
|
|
||||||
method: 'DELETE',
|
|
||||||
}).then(() => window.location.reload());
|
|
||||||
|
|
||||||
app.modal.show(new LoadingModal());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// items.add('separator2', Separator.component());
|
|
||||||
|
|
||||||
// items.add('support', LinkButton.component({
|
|
||||||
// icon: 'support',
|
|
||||||
// children: 'Support'
|
|
||||||
// }));
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled(name) {
|
|
||||||
const enabled = JSON.parse(app.config.extensions_enabled);
|
|
||||||
|
|
||||||
return enabled.indexOf(name) !== -1;
|
|
||||||
}
|
|
||||||
}
|
|
114
js/admin/src/components/ExtensionsPage.tsx
Normal file
114
js/admin/src/components/ExtensionsPage.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import Page from 'flarum/components/Page';
|
||||||
|
import LinkButton from 'flarum/components/LinkButton';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import Dropdown from 'flarum/components/Dropdown';
|
||||||
|
import Separator from 'flarum/components/Separator';
|
||||||
|
import AddExtensionModal from 'flarum/components/AddExtensionModal';
|
||||||
|
import LoadingModal from 'flarum/components/LoadingModal';
|
||||||
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
|
import icon from 'flarum/helpers/icon';
|
||||||
|
import listItems from 'flarum/helpers/listItems';
|
||||||
|
|
||||||
|
export default class ExtensionsPage extends Page {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="ExtensionsPage">
|
||||||
|
<div className="ExtensionsPage-header">
|
||||||
|
<div className="container">
|
||||||
|
{Button.component({
|
||||||
|
children: app.translator.trans('core.admin.extensions.add_button'),
|
||||||
|
icon: 'plus',
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
onclick: () => app.modal.show(new AddExtensionModal())
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ExtensionsPage-list">
|
||||||
|
<div className="container">
|
||||||
|
<ul className="ExtensionList">
|
||||||
|
{Object.keys(app.data.extensions)
|
||||||
|
.map(id => {
|
||||||
|
const extension = app.data.extensions[id];
|
||||||
|
const controls = this.controlItems(extension.id).toArray();
|
||||||
|
|
||||||
|
return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
|
||||||
|
<div className="ExtensionListItem-content">
|
||||||
|
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
||||||
|
{extension.icon ? icon(extension.icon.name) : ''}
|
||||||
|
</span>
|
||||||
|
{controls.length ? (
|
||||||
|
<Dropdown
|
||||||
|
className="ExtensionListItem-controls"
|
||||||
|
buttonClassName="Button Button--icon Button--flat"
|
||||||
|
menuClassName="Dropdown-menu--right"
|
||||||
|
icon="ellipsis-h">
|
||||||
|
{controls}
|
||||||
|
</Dropdown>
|
||||||
|
) : ''}
|
||||||
|
<label className="ExtensionListItem-title">
|
||||||
|
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '}
|
||||||
|
{extension.extra['flarum-extension'].title}
|
||||||
|
</label>
|
||||||
|
<div className="ExtensionListItem-version">{extension.version}</div>
|
||||||
|
</div>
|
||||||
|
</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
controlItems(name) {
|
||||||
|
const items = new ItemList();
|
||||||
|
const enabled = this.isEnabled(name);
|
||||||
|
|
||||||
|
if (app.extensionSettings[name]) {
|
||||||
|
items.add('settings', Button.component({
|
||||||
|
icon: 'cog',
|
||||||
|
children: app.translator.trans('core.admin.extensions.settings_button'),
|
||||||
|
onclick: app.extensionSettings[name]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
items.add('uninstall', Button.component({
|
||||||
|
icon: 'trash-o',
|
||||||
|
children: app.translator.trans('core.admin.extensions.uninstall_button'),
|
||||||
|
onclick: () => {
|
||||||
|
app.request({
|
||||||
|
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
|
||||||
|
method: 'DELETE'
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
|
||||||
|
app.modal.show(new LoadingModal());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(name) {
|
||||||
|
const enabled = JSON.parse(app.data.settings.extensions_enabled);
|
||||||
|
|
||||||
|
return enabled.indexOf(name) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(id) {
|
||||||
|
const enabled = this.isEnabled(id);
|
||||||
|
|
||||||
|
app.request({
|
||||||
|
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
||||||
|
method: 'PATCH',
|
||||||
|
data: {enabled: !enabled}
|
||||||
|
}).then(() => {
|
||||||
|
if (!enabled) localStorage.setItem('enabledExtension', id);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.modal.show(new LoadingModal());
|
||||||
|
}
|
||||||
|
}
|
25
js/admin/src/components/Header.tsx
Normal file
25
js/admin/src/components/Header.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Component from 'flarum/Component';
|
||||||
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
|
import SessionDropdown from './SessionDropdown';
|
||||||
|
|
||||||
|
export default class Header extends Component {
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
view() {
|
||||||
|
return this.items().toVnodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the header contents.
|
||||||
|
*
|
||||||
|
* @return {ItemList}
|
||||||
|
*/
|
||||||
|
items() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('session', <SessionDropdown/>);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,30 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import SessionDropdown from 'flarum/components/SessionDropdown';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
import listItems from 'flarum/helpers/listItems';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `HeaderSecondary` component displays secondary header controls.
|
|
||||||
*/
|
|
||||||
export default class HeaderSecondary extends Component {
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<ul className="Header-controls">
|
|
||||||
{listItems(this.items().toArray())}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an item list for the controls.
|
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
*/
|
|
||||||
items() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('session', SessionDropdown.component());
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -10,7 +10,7 @@ export default class LoadingModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return 'Please Wait...';
|
return app.translator.trans('core.admin.loading.title');
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
124
js/admin/src/components/MailPage.tsx
Normal file
124
js/admin/src/components/MailPage.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import Page from 'flarum/components/Page';
|
||||||
|
import FieldSet from 'flarum/components/FieldSet';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import Alert from 'flarum/components/Alert';
|
||||||
|
import saveSettings from 'flarum/utils/saveSettings';
|
||||||
|
|
||||||
|
export default class MailPage extends Page {
|
||||||
|
init() {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
this.fields = [
|
||||||
|
'mail_driver',
|
||||||
|
'mail_host',
|
||||||
|
'mail_from',
|
||||||
|
'mail_port',
|
||||||
|
'mail_username',
|
||||||
|
'mail_password',
|
||||||
|
'mail_encryption'
|
||||||
|
];
|
||||||
|
this.values = {};
|
||||||
|
|
||||||
|
const settings = app.data.settings;
|
||||||
|
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
|
||||||
|
|
||||||
|
this.localeOptions = {};
|
||||||
|
const locales = app.locales;
|
||||||
|
for (const i in locales) {
|
||||||
|
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="MailPage">
|
||||||
|
<div className="container">
|
||||||
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
|
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.email.text')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.email.server_heading'),
|
||||||
|
className: 'MailPage-MailSettings',
|
||||||
|
children: [
|
||||||
|
<div className="MailPage-MailSettings-input">
|
||||||
|
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_driver() || ''} oninput={m.withAttr('value', this.values.mail_driver)} />
|
||||||
|
<label>{app.translator.trans('core.admin.email.host_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_host() || ''} oninput={m.withAttr('value', this.values.mail_host)} />
|
||||||
|
<label>{app.translator.trans('core.admin.email.port_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_port() || ''} oninput={m.withAttr('value', this.values.mail_port)} />
|
||||||
|
<label>{app.translator.trans('core.admin.email.encryption_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_encryption() || ''} oninput={m.withAttr('value', this.values.mail_encryption)} />
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.email.account_heading'),
|
||||||
|
className: 'MailPage-MailSettings',
|
||||||
|
children: [
|
||||||
|
<div className="MailPage-MailSettings-input">
|
||||||
|
<label>{app.translator.trans('core.admin.email.username_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_username() || ''} oninput={m.withAttr('value', this.values.mail_username)} />
|
||||||
|
<label>{app.translator.trans('core.admin.email.password_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_password() || ''} oninput={m.withAttr('value', this.values.mail_password)} />
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.email.addresses_heading'),
|
||||||
|
className: 'MailPage-MailSettings',
|
||||||
|
children: [
|
||||||
|
<div className="MailPage-MailSettings-input">
|
||||||
|
<label>{app.translator.trans('core.admin.email.from_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Button.component({
|
||||||
|
type: 'submit',
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
children: app.translator.trans('core.admin.email.submit_button'),
|
||||||
|
loading: this.loading,
|
||||||
|
disabled: !this.changed()
|
||||||
|
})}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changed() {
|
||||||
|
return this.fields.some(key => this.values[key]() !== app.data.settings[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
onsubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.loading) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
app.alerts.dismiss(this.successAlert);
|
||||||
|
|
||||||
|
const settings = {};
|
||||||
|
|
||||||
|
this.fields.forEach(key => settings[key] = this.values[key]());
|
||||||
|
|
||||||
|
saveSettings(settings)
|
||||||
|
.then(() => {
|
||||||
|
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
32
js/admin/src/components/Page.tsx
Normal file
32
js/admin/src/components/Page.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import Component from 'flarum/Component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Page` component
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
export default class Page extends Component {
|
||||||
|
init() {
|
||||||
|
app.previous = app.current;
|
||||||
|
app.current = this;
|
||||||
|
|
||||||
|
app.modal.close();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class name to apply to the body while the route is active.
|
||||||
|
*
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
this.bodyClass = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
if (this.bodyClass) {
|
||||||
|
$('#app').addClass(this.bodyClass);
|
||||||
|
|
||||||
|
context.onunload = () => $('#app').removeClass(this.bodyClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,115 +0,0 @@
|
|||||||
import Dropdown from 'flarum/components/Dropdown';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import Separator from 'flarum/components/Separator';
|
|
||||||
import Group from 'flarum/models/Group';
|
|
||||||
import GroupBadge from 'flarum/components/GroupBadge';
|
|
||||||
|
|
||||||
function badgeForId(id) {
|
|
||||||
const group = app.store.getById('groups', id);
|
|
||||||
|
|
||||||
return group ? GroupBadge.component({group, label: null}) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PermissionDropdown extends Dropdown {
|
|
||||||
static initProps(props) {
|
|
||||||
super.initProps(props);
|
|
||||||
|
|
||||||
props.className = 'PermissionDropdown';
|
|
||||||
props.buttonClassName = 'Button Button--text';
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
this.props.children = [];
|
|
||||||
|
|
||||||
const groupIds = app.permissions[this.props.permission] || [];
|
|
||||||
const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
|
|
||||||
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
|
|
||||||
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
|
||||||
|
|
||||||
if (everyone) {
|
|
||||||
this.props.label = 'Everyone';
|
|
||||||
} else if (members) {
|
|
||||||
this.props.label = 'Members';
|
|
||||||
} else {
|
|
||||||
this.props.label = [
|
|
||||||
badgeForId(Group.ADMINISTRATOR_ID),
|
|
||||||
groupIds.map(badgeForId)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.allowGuest) {
|
|
||||||
this.props.children.push(
|
|
||||||
Button.component({
|
|
||||||
children: 'Everyone',
|
|
||||||
icon: everyone ? 'check' : true,
|
|
||||||
onclick: () => this.save([Group.GUEST_ID])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.children.push(
|
|
||||||
Button.component({
|
|
||||||
children: 'Members',
|
|
||||||
icon: members ? 'check' : true,
|
|
||||||
onclick: () => this.save([Group.MEMBER_ID])
|
|
||||||
}),
|
|
||||||
|
|
||||||
Separator.component(),
|
|
||||||
|
|
||||||
Button.component({
|
|
||||||
children: [GroupBadge.component({group: adminGroup, label: null}), ' ', adminGroup.namePlural()],
|
|
||||||
icon: !everyone && !members ? 'check' : true,
|
|
||||||
disabled: !everyone && !members,
|
|
||||||
onclick: e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.save([]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
[].push.apply(
|
|
||||||
this.props.children,
|
|
||||||
app.store.all('groups')
|
|
||||||
.filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
|
||||||
.map(group => Button.component({
|
|
||||||
children: [GroupBadge.component({group, label: null}), ' ', group.namePlural()],
|
|
||||||
icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true,
|
|
||||||
onclick: (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.toggle(group.id());
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
return super.view();
|
|
||||||
}
|
|
||||||
|
|
||||||
save(groupIds) {
|
|
||||||
const permission = this.props.permission;
|
|
||||||
|
|
||||||
app.permissions[permission] = groupIds;
|
|
||||||
|
|
||||||
app.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: app.forum.attribute('apiUrl') + '/permission',
|
|
||||||
data: {permission, groupIds}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle(groupId) {
|
|
||||||
const permission = this.props.permission;
|
|
||||||
|
|
||||||
let groupIds = app.permissions[permission] || [];
|
|
||||||
|
|
||||||
const index = groupIds.indexOf(groupId);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
groupIds.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
groupIds.push(groupId);
|
|
||||||
groupIds = groupIds.filter(id => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save(groupIds);
|
|
||||||
}
|
|
||||||
}
|
|
149
js/admin/src/components/PermissionDropdown.tsx
Normal file
149
js/admin/src/components/PermissionDropdown.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import Dropdown from 'flarum/components/Dropdown';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import Separator from 'flarum/components/Separator';
|
||||||
|
import Group from 'flarum/models/Group';
|
||||||
|
import Badge from 'flarum/components/Badge';
|
||||||
|
import GroupBadge from 'flarum/components/GroupBadge';
|
||||||
|
|
||||||
|
function badgeForId(id) {
|
||||||
|
const group = app.store.getById('groups', id);
|
||||||
|
|
||||||
|
return group ? GroupBadge.component({group, label: null}) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByRequiredPermissions(groupIds, permission) {
|
||||||
|
app.getRequiredPermissions(permission)
|
||||||
|
.forEach(required => {
|
||||||
|
const restrictToGroupIds = app.data.permissions[required] || [];
|
||||||
|
|
||||||
|
if (restrictToGroupIds.indexOf(Group.GUEST_ID) !== -1) {
|
||||||
|
// do nothing
|
||||||
|
} else if (restrictToGroupIds.indexOf(Group.MEMBER_ID) !== -1) {
|
||||||
|
groupIds = groupIds.filter(id => id !== Group.GUEST_ID);
|
||||||
|
} else if (groupIds.indexOf(Group.MEMBER_ID) !== -1) {
|
||||||
|
groupIds = restrictToGroupIds;
|
||||||
|
} else {
|
||||||
|
groupIds = restrictToGroupIds.filter(id => groupIds.indexOf(id) !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIds = filterByRequiredPermissions(groupIds, required);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PermissionDropdown extends Dropdown {
|
||||||
|
static initProps(props) {
|
||||||
|
super.initProps(props);
|
||||||
|
|
||||||
|
props.className = 'PermissionDropdown';
|
||||||
|
props.buttonClassName = 'Button Button--text';
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
this.props.children = [];
|
||||||
|
|
||||||
|
let groupIds = app.data.permissions[this.props.permission] || [];
|
||||||
|
|
||||||
|
groupIds = filterByRequiredPermissions(groupIds, this.props.permission);
|
||||||
|
|
||||||
|
const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
|
||||||
|
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
|
||||||
|
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
||||||
|
|
||||||
|
if (everyone) {
|
||||||
|
this.props.label = Badge.component({icon: 'globe'});
|
||||||
|
} else if (members) {
|
||||||
|
this.props.label = Badge.component({icon: 'user'});
|
||||||
|
} else {
|
||||||
|
this.props.label = [
|
||||||
|
badgeForId(Group.ADMINISTRATOR_ID),
|
||||||
|
groupIds.map(badgeForId)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showing) {
|
||||||
|
if (this.props.allowGuest) {
|
||||||
|
this.props.children.push(
|
||||||
|
Button.component({
|
||||||
|
children: [Badge.component({icon: 'globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
|
||||||
|
icon: everyone ? 'check' : true,
|
||||||
|
onclick: () => this.save([Group.GUEST_ID]),
|
||||||
|
disabled: this.isGroupDisabled(Group.GUEST_ID)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.children.push(
|
||||||
|
Button.component({
|
||||||
|
children: [Badge.component({icon: 'user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
|
||||||
|
icon: members ? 'check' : true,
|
||||||
|
onclick: () => this.save([Group.MEMBER_ID]),
|
||||||
|
disabled: this.isGroupDisabled(Group.MEMBER_ID)
|
||||||
|
}),
|
||||||
|
|
||||||
|
Separator.component(),
|
||||||
|
|
||||||
|
Button.component({
|
||||||
|
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
|
||||||
|
icon: !everyone && !members ? 'check' : true,
|
||||||
|
disabled: !everyone && !members,
|
||||||
|
onclick: e => {
|
||||||
|
if (e.shiftKey) e.stopPropagation();
|
||||||
|
this.save([]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
[].push.apply(
|
||||||
|
this.props.children,
|
||||||
|
app.store.all('groups')
|
||||||
|
.filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||||
|
.map(group => Button.component({
|
||||||
|
children: [badgeForId(group.id()), ' ', group.namePlural()],
|
||||||
|
icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true,
|
||||||
|
onclick: (e) => {
|
||||||
|
if (e.shiftKey) e.stopPropagation();
|
||||||
|
this.toggle(group.id());
|
||||||
|
},
|
||||||
|
disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.view();
|
||||||
|
}
|
||||||
|
|
||||||
|
save(groupIds) {
|
||||||
|
const permission = this.props.permission;
|
||||||
|
|
||||||
|
app.data.permissions[permission] = groupIds;
|
||||||
|
|
||||||
|
app.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: app.forum.attribute('apiUrl') + '/permission',
|
||||||
|
data: {permission, groupIds}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(groupId) {
|
||||||
|
const permission = this.props.permission;
|
||||||
|
|
||||||
|
let groupIds = app.data.permissions[permission] || [];
|
||||||
|
|
||||||
|
const index = groupIds.indexOf(groupId);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
groupIds.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
groupIds.push(groupId);
|
||||||
|
groupIds = groupIds.filter(id => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save(groupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
isGroupDisabled(id) {
|
||||||
|
return filterByRequiredPermissions([id], this.props.permission).indexOf(id) === -1;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,212 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import PermissionDropdown from 'flarum/components/PermissionDropdown';
|
|
||||||
import ConfigDropdown from 'flarum/components/ConfigDropdown';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
|
|
||||||
export default class PermissionGrid extends Component {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
this.permissions = this.permissionItems().toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
const scopes = this.scopeItems().toArray();
|
|
||||||
|
|
||||||
const permissionCells = permission => {
|
|
||||||
return scopes.map(scope => (
|
|
||||||
<td>
|
|
||||||
{scope.render(permission)}
|
|
||||||
</td>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table className="PermissionGrid">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
{scopes.map(scope => (
|
|
||||||
<th>
|
|
||||||
{scope.label}{' '}
|
|
||||||
{scope.onremove ? Button.component({icon: 'times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
<th>{this.scopeControlItems().toArray()}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{this.permissions.map(section => (
|
|
||||||
<tbody>
|
|
||||||
<tr className="PermissionGrid-section">
|
|
||||||
<th>{section.label}</th>
|
|
||||||
{permissionCells(section)}
|
|
||||||
<td/>
|
|
||||||
</tr>
|
|
||||||
{section.children.map(child => (
|
|
||||||
<tr className="PermissionGrid-child">
|
|
||||||
<th>{child.label}</th>
|
|
||||||
{permissionCells(child)}
|
|
||||||
<td/>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
))}
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
permissionItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('view', {
|
|
||||||
label: 'View the forum',
|
|
||||||
children: this.viewItems().toArray()
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('start', {
|
|
||||||
label: 'Start discussions',
|
|
||||||
children: this.startItems().toArray()
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('reply', {
|
|
||||||
label: 'Reply to discussions',
|
|
||||||
children: this.replyItems().toArray()
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('moderate', {
|
|
||||||
label: 'Moderate',
|
|
||||||
children: this.moderateItems().toArray()
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('view', {
|
|
||||||
label: 'View discussions',
|
|
||||||
permission: 'forum.view',
|
|
||||||
allowGuest: true
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('signUp', {
|
|
||||||
label: 'Sign up',
|
|
||||||
setting: () => ConfigDropdown.component({
|
|
||||||
key: 'allow_sign_up',
|
|
||||||
options: [
|
|
||||||
{value: '1', label: 'Open'},
|
|
||||||
{value: '0', label: 'Closed'}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
startItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('start', {
|
|
||||||
label: 'Start discussions',
|
|
||||||
permission: 'forum.startDiscussion'
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('allowRenaming', {
|
|
||||||
label: 'Allow renaming',
|
|
||||||
setting: () => {
|
|
||||||
const minutes = parseInt(app.config.allow_renaming, 10);
|
|
||||||
|
|
||||||
return ConfigDropdown.component({
|
|
||||||
defaultLabel: minutes ? `For ${minutes} minutes` : 'Indefinitely',
|
|
||||||
key: 'allow_renaming',
|
|
||||||
options: [
|
|
||||||
{value: '-1', label: 'Indefinitely'},
|
|
||||||
{value: '10', label: 'For 10 minutes'},
|
|
||||||
{value: 'reply', label: 'Until next reply'}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
replyItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('reply', {
|
|
||||||
label: 'Reply to discussions',
|
|
||||||
permission: 'discussion.reply'
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('allowPostEditing', {
|
|
||||||
label: 'Allow post editing',
|
|
||||||
setting: () => {
|
|
||||||
const minutes = parseInt(app.config.allow_post_editing, 10);
|
|
||||||
|
|
||||||
return ConfigDropdown.component({
|
|
||||||
defaultLabel: minutes ? `For ${minutes} minutes` : 'Indefinitely',
|
|
||||||
key: 'allow_post_editing',
|
|
||||||
options: [
|
|
||||||
{value: '-1', label: 'Indefinitely'},
|
|
||||||
{value: '10', label: 'For 10 minutes'},
|
|
||||||
{value: 'reply', label: 'Until next reply'}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
moderateItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('editPosts', {
|
|
||||||
label: 'Edit posts',
|
|
||||||
permission: 'discussion.editPosts'
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('deletePosts', {
|
|
||||||
label: 'Delete posts',
|
|
||||||
permission: 'discussion.deletePosts'
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('renameDiscussions', {
|
|
||||||
label: 'Rename discussions',
|
|
||||||
permission: 'discussion.rename'
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('deleteDiscussions', {
|
|
||||||
label: 'Delete discussions',
|
|
||||||
permission: 'discussion.delete'
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
scopeItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('global', {
|
|
||||||
label: 'Global',
|
|
||||||
render: item => {
|
|
||||||
if (item.setting) {
|
|
||||||
return item.setting();
|
|
||||||
} else if (item.permission) {
|
|
||||||
return PermissionDropdown.component(Object.assign({}, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
scopeControlItems() {
|
|
||||||
return new ItemList();
|
|
||||||
}
|
|
||||||
}
|
|
247
js/admin/src/components/PermissionGrid.tsx
Normal file
247
js/admin/src/components/PermissionGrid.tsx
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import Component from 'flarum/Component';
|
||||||
|
import PermissionDropdown from 'flarum/components/PermissionDropdown';
|
||||||
|
import SettingDropdown from 'flarum/components/SettingDropdown';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
|
import icon from 'flarum/helpers/icon';
|
||||||
|
|
||||||
|
export default class PermissionGrid extends Component {
|
||||||
|
init() {
|
||||||
|
this.permissions = this.permissionItems().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
const scopes = this.scopeItems().toArray();
|
||||||
|
|
||||||
|
const permissionCells = permission => {
|
||||||
|
return scopes.map(scope => (
|
||||||
|
<td>
|
||||||
|
{scope.render(permission)}
|
||||||
|
</td>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="PermissionGrid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
{scopes.map(scope => (
|
||||||
|
<th>
|
||||||
|
{scope.label}{' '}
|
||||||
|
{scope.onremove ? Button.component({icon: 'times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th>{this.scopeControlItems().toArray()}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{this.permissions.map(section => (
|
||||||
|
<tbody>
|
||||||
|
<tr className="PermissionGrid-section">
|
||||||
|
<th>{section.label}</th>
|
||||||
|
{permissionCells(section)}
|
||||||
|
<td/>
|
||||||
|
</tr>
|
||||||
|
{section.children.map(child => (
|
||||||
|
<tr className="PermissionGrid-child">
|
||||||
|
<th>{icon(child.icon)}{child.label}</th>
|
||||||
|
{permissionCells(child)}
|
||||||
|
<td/>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
))}
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('view', {
|
||||||
|
label: app.translator.trans('core.admin.permissions.read_heading'),
|
||||||
|
children: this.viewItems().toArray()
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('start', {
|
||||||
|
label: app.translator.trans('core.admin.permissions.create_heading'),
|
||||||
|
children: this.startItems().toArray()
|
||||||
|
}, 90);
|
||||||
|
|
||||||
|
items.add('reply', {
|
||||||
|
label: app.translator.trans('core.admin.permissions.participate_heading'),
|
||||||
|
children: this.replyItems().toArray()
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
items.add('moderate', {
|
||||||
|
label: app.translator.trans('core.admin.permissions.moderate_heading'),
|
||||||
|
children: this.moderateItems().toArray()
|
||||||
|
}, 70);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('viewDiscussions', {
|
||||||
|
icon: 'eye',
|
||||||
|
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
|
||||||
|
permission: 'viewDiscussions',
|
||||||
|
allowGuest: true
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('viewUserList', {
|
||||||
|
icon: 'users',
|
||||||
|
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
|
||||||
|
permission: 'viewUserList',
|
||||||
|
allowGuest: true
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('signUp', {
|
||||||
|
icon: 'user-plus',
|
||||||
|
label: app.translator.trans('core.admin.permissions.sign_up_label'),
|
||||||
|
setting: () => SettingDropdown.component({
|
||||||
|
key: 'allow_sign_up',
|
||||||
|
options: [
|
||||||
|
{value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button')},
|
||||||
|
{value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button')}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}, 90);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
startItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('start', {
|
||||||
|
icon: 'edit',
|
||||||
|
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
|
||||||
|
permission: 'startDiscussion'
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('allowRenaming', {
|
||||||
|
icon: 'i-cursor',
|
||||||
|
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
|
||||||
|
setting: () => {
|
||||||
|
const minutes = parseInt(app.data.settings.allow_renaming, 10);
|
||||||
|
|
||||||
|
return SettingDropdown.component({
|
||||||
|
defaultLabel: minutes
|
||||||
|
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
|
||||||
|
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||||
|
key: 'allow_renaming',
|
||||||
|
options: [
|
||||||
|
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
|
||||||
|
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
|
||||||
|
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 90);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
replyItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('reply', {
|
||||||
|
icon: 'reply',
|
||||||
|
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
|
||||||
|
permission: 'discussion.reply'
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('allowPostEditing', {
|
||||||
|
icon: 'pencil',
|
||||||
|
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
|
||||||
|
setting: () => {
|
||||||
|
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
|
||||||
|
|
||||||
|
return SettingDropdown.component({
|
||||||
|
defaultLabel: minutes
|
||||||
|
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
|
||||||
|
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||||
|
key: 'allow_post_editing',
|
||||||
|
options: [
|
||||||
|
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
|
||||||
|
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
|
||||||
|
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 90);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
moderateItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('viewIpsPosts', {
|
||||||
|
icon: 'bullseye',
|
||||||
|
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
|
||||||
|
permission: 'discussion.viewIpsPosts'
|
||||||
|
}, 110);
|
||||||
|
|
||||||
|
items.add('renameDiscussions', {
|
||||||
|
icon: 'i-cursor',
|
||||||
|
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
|
||||||
|
permission: 'discussion.rename'
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('hideDiscussions', {
|
||||||
|
icon: 'trash-o',
|
||||||
|
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
|
||||||
|
permission: 'discussion.hide'
|
||||||
|
}, 90);
|
||||||
|
|
||||||
|
items.add('deleteDiscussions', {
|
||||||
|
icon: 'times',
|
||||||
|
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
|
||||||
|
permission: 'discussion.delete'
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
items.add('editPosts', {
|
||||||
|
icon: 'pencil',
|
||||||
|
label: app.translator.trans('core.admin.permissions.edit_and_delete_posts_label'),
|
||||||
|
permission: 'discussion.editPosts'
|
||||||
|
}, 70);
|
||||||
|
|
||||||
|
items.add('deletePosts', {
|
||||||
|
icon: 'times',
|
||||||
|
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
|
||||||
|
permission: 'discussion.deletePosts'
|
||||||
|
}, 60);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('global', {
|
||||||
|
label: app.translator.trans('core.admin.permissions.global_heading'),
|
||||||
|
render: item => {
|
||||||
|
if (item.setting) {
|
||||||
|
return item.setting();
|
||||||
|
} else if (item.permission) {
|
||||||
|
return PermissionDropdown.component({
|
||||||
|
permission: item.permission,
|
||||||
|
allowGuest: item.allowGuest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeControlItems() {
|
||||||
|
return new ItemList();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +1,11 @@
|
|||||||
import Component from 'flarum/Component';
|
import Page from 'flarum/components/Page';
|
||||||
import GroupBadge from 'flarum/components/GroupBadge';
|
import GroupBadge from 'flarum/components/GroupBadge';
|
||||||
import EditGroupModal from 'flarum/components/EditGroupModal';
|
import EditGroupModal from 'flarum/components/EditGroupModal';
|
||||||
import Group from 'flarum/models/Group';
|
import Group from 'flarum/models/Group';
|
||||||
import icon from 'flarum/helpers/icon';
|
import icon from 'flarum/helpers/icon';
|
||||||
import PermissionGrid from 'flarum/components/PermissionGrid';
|
import PermissionGrid from 'flarum/components/PermissionGrid';
|
||||||
|
|
||||||
export default class PermissionsPage extends Component {
|
export default class PermissionsPage extends Page {
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="PermissionsPage">
|
<div className="PermissionsPage">
|
||||||
@@ -25,7 +25,7 @@ export default class PermissionsPage extends Component {
|
|||||||
))}
|
))}
|
||||||
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
|
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
|
||||||
{icon('plus', {className: 'Group-icon'})}
|
{icon('plus', {className: 'Group-icon'})}
|
||||||
<span className="Group-name">New Group</span>
|
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -43,7 +43,7 @@ export default class SessionDropdown extends Dropdown {
|
|||||||
items.add('logOut',
|
items.add('logOut',
|
||||||
Button.component({
|
Button.component({
|
||||||
icon: 'sign-out',
|
icon: 'sign-out',
|
||||||
children: app.trans('core.log_out'),
|
children: app.translator.trans('core.admin.header.log_out_button'),
|
||||||
onclick: app.session.logout.bind(app.session)
|
onclick: app.session.logout.bind(app.session)
|
||||||
}),
|
}),
|
||||||
-100
|
-100
|
@@ -1,23 +1,23 @@
|
|||||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
import saveConfig from 'flarum/utils/saveConfig';
|
import saveSettings from 'flarum/utils/saveSettings';
|
||||||
|
|
||||||
export default class ConfigDropdown extends SelectDropdown {
|
export default class SettingDropdown extends SelectDropdown {
|
||||||
static initProps(props) {
|
static initProps(props) {
|
||||||
super.initProps(props);
|
super.initProps(props);
|
||||||
|
|
||||||
props.className = 'ConfigDropdown';
|
props.className = 'SettingDropdown';
|
||||||
props.buttonClassName = 'Button Button--text';
|
props.buttonClassName = 'Button Button--text';
|
||||||
props.caretIcon = 'caret-down';
|
props.caretIcon = 'caret-down';
|
||||||
props.defaultLabel = 'Custom';
|
props.defaultLabel = 'Custom';
|
||||||
|
|
||||||
props.children = props.options.map(({value, label}) => {
|
props.children = props.options.map(({value, label}) => {
|
||||||
const active = app.config[props.key] === value;
|
const active = app.data.settings[props.key] === value;
|
||||||
|
|
||||||
return Button.component({
|
return Button.component({
|
||||||
children: label,
|
children: label,
|
||||||
icon: active ? 'check' : true,
|
icon: active ? 'check' : true,
|
||||||
onclick: saveConfig.bind(this, {[props.key]: value}),
|
onclick: saveSettings.bind(this, {[props.key]: value}),
|
||||||
active
|
active
|
||||||
});
|
});
|
||||||
});
|
});
|
79
js/admin/src/components/SettingsModal.tsx
Normal file
79
js/admin/src/components/SettingsModal.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import Modal from 'flarum/components/Modal';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import saveSettings from 'flarum/utils/saveSettings';
|
||||||
|
|
||||||
|
export default class SettingsModal extends Modal {
|
||||||
|
init() {
|
||||||
|
this.settings = {};
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
form() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return (
|
||||||
|
<div className="Modal-body">
|
||||||
|
<div className="Form">
|
||||||
|
{this.form()}
|
||||||
|
|
||||||
|
<div className="Form-group">
|
||||||
|
{this.submitButton()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitButton() {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="Button Button--primary"
|
||||||
|
loading={this.loading}
|
||||||
|
disabled={!this.changed()}>
|
||||||
|
{app.translator.trans('core.admin.settings.submit_button')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setting(key, fallback = '') {
|
||||||
|
this.settings[key] = this.settings[key] || m.prop(app.data.settings[key] || fallback);
|
||||||
|
|
||||||
|
return this.settings[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
dirty() {
|
||||||
|
const dirty = {};
|
||||||
|
|
||||||
|
Object.keys(this.settings).forEach(key => {
|
||||||
|
const value = this.settings[key]();
|
||||||
|
|
||||||
|
if (value !== app.data.settings[key]) {
|
||||||
|
dirty[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
changed() {
|
||||||
|
return Object.keys(this.dirty()).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
onsubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
saveSettings(this.dirty()).then(
|
||||||
|
this.onsaved.bind(this),
|
||||||
|
this.loaded.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onsaved() {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
97
js/admin/src/components/UploadImageButton.tsx
Normal file
97
js/admin/src/components/UploadImageButton.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
|
||||||
|
export default class UploadImageButton extends Button {
|
||||||
|
init() {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
this.props.loading = this.loading;
|
||||||
|
this.props.className = (this.props.className || '') + ' Button';
|
||||||
|
|
||||||
|
if (app.data.settings[this.props.name + '_path']) {
|
||||||
|
this.props.onclick = this.remove.bind(this);
|
||||||
|
this.props.children = app.translator.trans('core.admin.upload_image.remove_button');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p><img src={app.forum.attribute(this.props.name+'Url')} alt=""/></p>
|
||||||
|
<p>{super.view()}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.props.onclick = this.upload.bind(this);
|
||||||
|
this.props.children = app.translator.trans('core.admin.upload_image.upload_button');
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.view();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt the user to upload an image.
|
||||||
|
*/
|
||||||
|
upload() {
|
||||||
|
if (this.loading) return;
|
||||||
|
|
||||||
|
const $input = $('<input type="file">');
|
||||||
|
|
||||||
|
$input.appendTo('body').hide().click().on('change', e => {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append(this.props.name, $(e.target)[0].files[0]);
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
app.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: this.resourceUrl(),
|
||||||
|
serialize: raw => raw,
|
||||||
|
data
|
||||||
|
}).then(
|
||||||
|
this.success.bind(this),
|
||||||
|
this.failure.bind(this)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the logo.
|
||||||
|
*/
|
||||||
|
remove() {
|
||||||
|
this.loading = true;
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
app.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: this.resourceUrl()
|
||||||
|
}).then(
|
||||||
|
this.success.bind(this),
|
||||||
|
this.failure.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceUrl() {
|
||||||
|
return app.forum.attribute('apiUrl') + '/' + this.props.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After a successful upload/removal, reload the page.
|
||||||
|
*
|
||||||
|
* @param {Object} response
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
success(response) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If upload/removal fails, stop loading.
|
||||||
|
*
|
||||||
|
* @param {Object} response
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
failure(response) {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
}
|
9
js/admin/src/index.ts
Normal file
9
js/admin/src/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import AdminApplication from './AdminApplication';
|
||||||
|
|
||||||
|
export const app = new AdminApplication();
|
||||||
|
|
||||||
|
export const extensions = [];
|
||||||
|
|
||||||
|
// Export public API
|
||||||
|
// export { default as Extend } from './Extend';
|
||||||
|
// export { IndexPage, DicsussionList } from './components';
|
@@ -1,57 +0,0 @@
|
|||||||
/*global FastClick*/
|
|
||||||
|
|
||||||
import ScrollListener from 'flarum/utils/ScrollListener';
|
|
||||||
import Drawer from 'flarum/utils/Drawer';
|
|
||||||
import mapRoutes from 'flarum/utils/mapRoutes';
|
|
||||||
|
|
||||||
import Navigation from 'flarum/components/Navigation';
|
|
||||||
import HeaderPrimary from 'flarum/components/HeaderPrimary';
|
|
||||||
import HeaderSecondary from 'flarum/components/HeaderSecondary';
|
|
||||||
import AdminNav from 'flarum/components/AdminNav';
|
|
||||||
import ModalManager from 'flarum/components/ModalManager';
|
|
||||||
import AlertManager from 'flarum/components/AlertManager';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `boot` initializer boots up the admin app. It initializes some app
|
|
||||||
* globals, mounts components to the page, and begins routing.
|
|
||||||
*
|
|
||||||
* @param {ForumApp} app
|
|
||||||
*/
|
|
||||||
export default function boot(app) {
|
|
||||||
m.startComputation();
|
|
||||||
|
|
||||||
m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
|
|
||||||
m.mount(document.getElementById('header-navigation'), Navigation.component());
|
|
||||||
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
|
|
||||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
|
|
||||||
m.mount(document.getElementById('admin-navigation'), AdminNav.component());
|
|
||||||
|
|
||||||
app.drawer = new Drawer();
|
|
||||||
app.modal = m.mount(document.getElementById('modal'), ModalManager.component());
|
|
||||||
app.alerts = m.mount(document.getElementById('alerts'), AlertManager.component());
|
|
||||||
app.history = {
|
|
||||||
canGoBack: () => true,
|
|
||||||
backUrl: () => app.forum.attribute('baseUrl'),
|
|
||||||
back: function() {
|
|
||||||
window.location = this.backUrl();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
m.route.mode = 'hash';
|
|
||||||
m.route(document.getElementById('content'), '/', mapRoutes(app.routes));
|
|
||||||
|
|
||||||
m.endComputation();
|
|
||||||
|
|
||||||
// Add a class to the body which indicates that the page has been scrolled
|
|
||||||
// down.
|
|
||||||
new ScrollListener(top => {
|
|
||||||
const $app = $('#app');
|
|
||||||
const offset = $app.offset().top;
|
|
||||||
|
|
||||||
$app
|
|
||||||
.toggleClass('affix', top >= offset)
|
|
||||||
.toggleClass('scrolled', top > offset);
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
app.booted = true;
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
import DashboardPage from 'flarum/components/DashboardPage';
|
|
||||||
import BasicsPage from 'flarum/components/BasicsPage';
|
|
||||||
import PermissionsPage from 'flarum/components/PermissionsPage';
|
|
||||||
import AppearancePage from 'flarum/components/AppearancePage';
|
|
||||||
import ExtensionsPage from 'flarum/components/ExtensionsPage';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `routes` initializer defines the admin app's routes.
|
|
||||||
*
|
|
||||||
* @param {App} app
|
|
||||||
*/
|
|
||||||
export default function(app) {
|
|
||||||
app.routes = {
|
|
||||||
'dashboard': {path: '/', component: DashboardPage.component()},
|
|
||||||
'basics': {path: '/basics', component: BasicsPage.component()},
|
|
||||||
'permissions': {path: '/permissions', component: PermissionsPage.component()},
|
|
||||||
'appearance': {path: '/appearance', component: AppearancePage.component()},
|
|
||||||
'extensions': {path: '/extensions', component: ExtensionsPage.component()}
|
|
||||||
};
|
|
||||||
}
|
|
1
js/admin/src/lib
Symbolic link
1
js/admin/src/lib
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/Users/toby/Projects/Flarum/app/packages/core/js/lib
|
15
js/admin/src/routes.ts
Normal file
15
js/admin/src/routes.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import DashboardPage from './components/DashboardPage';
|
||||||
|
import BasicsPage from './components/BasicsPage';
|
||||||
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
|
import AppearancePage from './components/AppearancePage';
|
||||||
|
import ExtensionsPage from './components/ExtensionsPage';
|
||||||
|
import MailPage from './components/MailPage';
|
||||||
|
|
||||||
|
export default function(router) {
|
||||||
|
router.add('dashboard', '/', DashboardPage);
|
||||||
|
router.add('basics', '/basics', BasicsPage);
|
||||||
|
router.add('permissions', '/permissions', PermissionsPage);
|
||||||
|
router.add('appearance', '/appearance', AppearancePage);
|
||||||
|
router.add('extensions', '/extensions', ExtensionsPage);
|
||||||
|
router.add('mail', '/mail', MailPage);
|
||||||
|
}
|
@@ -1,14 +0,0 @@
|
|||||||
export default function saveConfig(config) {
|
|
||||||
const oldConfig = JSON.parse(JSON.stringify(app.config));
|
|
||||||
|
|
||||||
Object.assign(app.config, config);
|
|
||||||
|
|
||||||
return app.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: app.forum.attribute('apiUrl') + '/config',
|
|
||||||
data: {config}
|
|
||||||
}).catch(error => {
|
|
||||||
app.config = oldConfig;
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
22
js/admin/src/utils/saveSettings.tsx
Normal file
22
js/admin/src/utils/saveSettings.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import flarum from 'flarum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request to save the given settings to the database.
|
||||||
|
*
|
||||||
|
* @param {Object} settings
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export default function saveSettings(settings) {
|
||||||
|
const oldSettings = JSON.parse(JSON.stringify(flarum.data.settings));
|
||||||
|
|
||||||
|
Object.assign(flarum.data.settings, settings);
|
||||||
|
|
||||||
|
return flarum.ajax.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: flarum.forum.apiUrl + '/settings',
|
||||||
|
data: settings
|
||||||
|
}).catch(error => {
|
||||||
|
flarum.data.settings = oldSettings;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
23
js/admin/tsconfig.json
Normal file
23
js/admin/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/",
|
||||||
|
"sourceMap": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "m",
|
||||||
|
"declaration": true,
|
||||||
|
"lib": ["dom", "es2015"],
|
||||||
|
"types": [
|
||||||
|
"mithril",
|
||||||
|
"classnames"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
30
js/admin/webpack.config.js
Normal file
30
js/admin/webpack.config.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const { CheckerPlugin } = require('awesome-typescript-loader');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: path.resolve(__dirname, 'src/index.tsx'),
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.tsx', '.js', '.jsx']
|
||||||
|
},
|
||||||
|
devtool: 'source-map',
|
||||||
|
output: {
|
||||||
|
filename: 'bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
library: 'flarum',
|
||||||
|
libraryTarget: 'var'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
loader: 'awesome-typescript-loader'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CheckerPlugin()
|
||||||
|
],
|
||||||
|
externals: {
|
||||||
|
mithril: 'm'
|
||||||
|
}
|
||||||
|
};
|
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "flarum",
|
|
||||||
"dependencies": {
|
|
||||||
"jquery": "~2.1.3",
|
|
||||||
"jquery.hotkeys": "jeresig/jquery.hotkeys#0.2.0",
|
|
||||||
"bootstrap": "~3.3.2",
|
|
||||||
"spin.js": "~2.0.1",
|
|
||||||
"moment": "~2.8.4",
|
|
||||||
"color-thief": "v2.0",
|
|
||||||
"mithril": "lhorie/mithril.js#next",
|
|
||||||
"es6-micro-loader": "caridy/es6-micro-loader#v0.2.1",
|
|
||||||
"fastclick": "~1.0.6",
|
|
||||||
"autolink": "*"
|
|
||||||
}
|
|
||||||
}
|
|
4
js/forum/.gitignore
vendored
4
js/forum/.gitignore
vendored
@@ -1,3 +1 @@
|
|||||||
node_modules
|
dist
|
||||||
mithril.js
|
|
||||||
dist
|
|
@@ -1,37 +0,0 @@
|
|||||||
var gulp = require('flarum-gulp');
|
|
||||||
|
|
||||||
var nodeDir = 'node_modules';
|
|
||||||
var bowerDir = '../bower_components';
|
|
||||||
|
|
||||||
gulp({
|
|
||||||
files: [
|
|
||||||
nodeDir + '/babel-core/external-helpers.js',
|
|
||||||
|
|
||||||
bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
|
|
||||||
|
|
||||||
bowerDir + '/mithril/mithril.js',
|
|
||||||
bowerDir + '/jquery/dist/jquery.js',
|
|
||||||
bowerDir + '/jquery.hotkeys/jquery.hotkeys.js',
|
|
||||||
bowerDir + '/color-thief/js/color-thief.js',
|
|
||||||
bowerDir + '/moment/moment.js',
|
|
||||||
bowerDir + '/autolink/autolink.js',
|
|
||||||
|
|
||||||
bowerDir + '/bootstrap/js/affix.js',
|
|
||||||
bowerDir + '/bootstrap/js/dropdown.js',
|
|
||||||
bowerDir + '/bootstrap/js/modal.js',
|
|
||||||
bowerDir + '/bootstrap/js/tooltip.js',
|
|
||||||
bowerDir + '/bootstrap/js/transition.js',
|
|
||||||
|
|
||||||
bowerDir + '/spin.js/spin.js',
|
|
||||||
bowerDir + '/spin.js/jquery.spin.js',
|
|
||||||
bowerDir + '/fastclick/lib/fastclick.js'
|
|
||||||
],
|
|
||||||
modules: {
|
|
||||||
'flarum': [
|
|
||||||
'src/**/*.js',
|
|
||||||
'../lib/**/*.js'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
externalHelpers: true,
|
|
||||||
outputFile: 'dist/app.js'
|
|
||||||
});
|
|
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"devDependencies": {
|
|
||||||
"gulp": "^3.8.11",
|
|
||||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
|
|
||||||
"babel-core": "^5.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,79 +0,0 @@
|
|||||||
import History from 'flarum/utils/History';
|
|
||||||
import App from 'flarum/App';
|
|
||||||
import Search from 'flarum/components/Search';
|
|
||||||
import Composer from 'flarum/components/Composer';
|
|
||||||
import ReplyComposer from 'flarum/components/ReplyComposer';
|
|
||||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
|
||||||
|
|
||||||
export default class ForumApp extends App {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The app's history stack, which keeps track of which routes the user visits
|
|
||||||
* so that they can easily navigate back to the previous route.
|
|
||||||
*
|
|
||||||
* @type {History}
|
|
||||||
*/
|
|
||||||
this.history = new History();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object which controls the state of the page's side pane.
|
|
||||||
*
|
|
||||||
* @type {Pane}
|
|
||||||
*/
|
|
||||||
this.pane = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The page's search component instance.
|
|
||||||
*
|
|
||||||
* @type {SearchBox}
|
|
||||||
*/
|
|
||||||
this.search = new Search();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object which controls the state of the page's drawer.
|
|
||||||
*
|
|
||||||
* @type {Drawer}
|
|
||||||
*/
|
|
||||||
this.drawer = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of post types to their components.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
this.postComponents = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of notification types to their components.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
this.notificationComponents = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether or not the user is currently composing a reply to a
|
|
||||||
* discussion.
|
|
||||||
*
|
|
||||||
* @param {Discussion} discussion
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
composingReplyTo(discussion) {
|
|
||||||
return this.composer.component instanceof ReplyComposer &&
|
|
||||||
this.composer.component.props.discussion === discussion &&
|
|
||||||
this.composer.position !== Composer.PositionEnum.HIDDEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether or not the user is currently viewing a discussion.
|
|
||||||
*
|
|
||||||
* @param {Discussion} discussion
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
viewingDiscussion(discussion) {
|
|
||||||
return this.current instanceof DiscussionPage &&
|
|
||||||
this.current.discussion === discussion;
|
|
||||||
}
|
|
||||||
}
|
|
86
js/forum/src/ForumApplication.tsx
Normal file
86
js/forum/src/ForumApplication.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import Application from './lib/Application';
|
||||||
|
import routes from './routes';
|
||||||
|
import Search from './components/Search';
|
||||||
|
|
||||||
|
export default class ForumApplication extends Application {
|
||||||
|
/**
|
||||||
|
* The page's search component instance.
|
||||||
|
*
|
||||||
|
* @type {SearchBox}
|
||||||
|
*/
|
||||||
|
search = new Search();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of notification types to their components.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
notificationComponents = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of post types to their components.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
postComponents = {};
|
||||||
|
|
||||||
|
//app.postComponents.comment = CommentPost;
|
||||||
|
//app.postComponents.discussionRenamed = DiscussionRenamedPost;
|
||||||
|
|
||||||
|
// app.notificationComponents.discussionRenamed = DiscussionRenamedNotification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
registerDefaultRoutes(router) {
|
||||||
|
routes(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: work out where to put these helper functions
|
||||||
|
// /**
|
||||||
|
// * Check whether or not the user is currently composing a reply to a
|
||||||
|
// * discussion.
|
||||||
|
// *
|
||||||
|
// * @param {Discussion} discussion
|
||||||
|
// * @return {Boolean}
|
||||||
|
// */
|
||||||
|
// composingReplyTo(discussion) {
|
||||||
|
// return this.composer.component instanceof ReplyComposer &&
|
||||||
|
// this.composer.component.props.discussion === discussion &&
|
||||||
|
// this.composer.position !== Composer.PositionEnum.HIDDEN;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Check whether or not the user is currently viewing a discussion.
|
||||||
|
// *
|
||||||
|
// * @param {Discussion} discussion
|
||||||
|
// * @return {Boolean}
|
||||||
|
// */
|
||||||
|
// viewingDiscussion(discussion) {
|
||||||
|
// return this.current instanceof DiscussionPage &&
|
||||||
|
// this.current.discussion === discussion;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Callback for when an external authenticator (social login) action has
|
||||||
|
// * completed.
|
||||||
|
// *
|
||||||
|
// * If the payload indicates that the user has been logged in, then the page
|
||||||
|
// * will be reloaded. Otherwise, a SignUpModal will be opened, prefilled
|
||||||
|
// * with the provided details.
|
||||||
|
// *
|
||||||
|
// * @param {Object} payload A dictionary of props to pass into the sign up
|
||||||
|
// * modal. A truthy `authenticated` prop indicates that the user has logged
|
||||||
|
// * in, and thus the page is reloaded.
|
||||||
|
// * @public
|
||||||
|
// */
|
||||||
|
// authenticationComplete(payload) {
|
||||||
|
// if (payload.authenticated) {
|
||||||
|
// window.location.reload();
|
||||||
|
// } else {
|
||||||
|
// const modal = new SignUpModal(payload);
|
||||||
|
// this.modal.show(modal);
|
||||||
|
// modal.$('[name=password]').focus();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
@@ -1,19 +0,0 @@
|
|||||||
import ForumApp from 'flarum/ForumApp';
|
|
||||||
import store from 'flarum/initializers/store';
|
|
||||||
import preload from 'flarum/initializers/preload';
|
|
||||||
import routes from 'flarum/initializers/routes';
|
|
||||||
import components from 'flarum/initializers/components';
|
|
||||||
import humanTime from 'flarum/initializers/humanTime';
|
|
||||||
import boot from 'flarum/initializers/boot';
|
|
||||||
|
|
||||||
const app = new ForumApp();
|
|
||||||
|
|
||||||
app.initializers.add('store', store);
|
|
||||||
app.initializers.add('routes', routes);
|
|
||||||
app.initializers.add('components', components);
|
|
||||||
app.initializers.add('humanTime', humanTime);
|
|
||||||
|
|
||||||
app.initializers.add('preload', preload, -100);
|
|
||||||
app.initializers.add('boot', boot, -100);
|
|
||||||
|
|
||||||
export default app;
|
|
@@ -1,66 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import humanTime from 'flarum/helpers/humanTime';
|
|
||||||
import avatar from 'flarum/helpers/avatar';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Activity` component represents a piece of activity of a user's activity
|
|
||||||
* feed. Subclasses should implement the `description` and `content` methods.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `activity`
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export default class Activity extends Component {
|
|
||||||
view() {
|
|
||||||
const activity = this.props.activity;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="Activity">
|
|
||||||
{avatar(this.user(), {className: 'Activity-avatar'})}
|
|
||||||
|
|
||||||
<div className="Activity-header">
|
|
||||||
<strong className="Activity-description">{this.description()}</strong>
|
|
||||||
{humanTime(this.time())}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.content()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user whose avatar should be displayed.
|
|
||||||
*
|
|
||||||
* @return {User}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
user() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the time of the activity.
|
|
||||||
*
|
|
||||||
* @return {Date}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
time() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the description of the activity.
|
|
||||||
*
|
|
||||||
* @return {VirtualElement}
|
|
||||||
*/
|
|
||||||
description() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content to show below the activity description.
|
|
||||||
*
|
|
||||||
* @return {VirtualElement}
|
|
||||||
*/
|
|
||||||
content() {
|
|
||||||
}
|
|
||||||
}
|
|
@@ -16,9 +16,7 @@ import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
|||||||
* - `user`
|
* - `user`
|
||||||
*/
|
*/
|
||||||
export default class AvatarEditor extends Component {
|
export default class AvatarEditor extends Component {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not an avatar upload is in progress.
|
* Whether or not an avatar upload is in progress.
|
||||||
*
|
*
|
||||||
@@ -39,10 +37,11 @@ export default class AvatarEditor extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '')}>
|
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '')}>
|
||||||
{avatar(user)}
|
{avatar(user)}
|
||||||
<a className="Dropdown-toggle"
|
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" }
|
||||||
|
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
onclick={this.quickUpload.bind(this)}>
|
onclick={this.quickUpload.bind(this)}>
|
||||||
{this.loading ? LoadingIndicator.component() : icon('pencil')}
|
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('pencil') : icon('plus-circle'))}
|
||||||
</a>
|
</a>
|
||||||
<ul className="Dropdown-menu Menu">
|
<ul className="Dropdown-menu Menu">
|
||||||
{listItems(this.controlItems().toArray())}
|
{listItems(this.controlItems().toArray())}
|
||||||
@@ -62,7 +61,7 @@ export default class AvatarEditor extends Component {
|
|||||||
items.add('upload',
|
items.add('upload',
|
||||||
Button.component({
|
Button.component({
|
||||||
icon: 'upload',
|
icon: 'upload',
|
||||||
children: app.trans('core.upload'),
|
children: app.translator.trans('core.forum.user.avatar_upload_button'),
|
||||||
onclick: this.upload.bind(this)
|
onclick: this.upload.bind(this)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -70,7 +69,7 @@ export default class AvatarEditor extends Component {
|
|||||||
items.add('remove',
|
items.add('remove',
|
||||||
Button.component({
|
Button.component({
|
||||||
icon: 'times',
|
icon: 'times',
|
||||||
children: app.trans('core.remove'),
|
children: app.translator.trans('core.forum.user.avatar_remove_button'),
|
||||||
onclick: this.remove.bind(this)
|
onclick: this.remove.bind(this)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -163,7 +162,8 @@ export default class AvatarEditor extends Component {
|
|||||||
* @param {Object} response
|
* @param {Object} response
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
failure() {
|
failure(response) {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -6,8 +6,8 @@ import Button from 'flarum/components/Button';
|
|||||||
* to change their email address.
|
* to change their email address.
|
||||||
*/
|
*/
|
||||||
export default class ChangeEmailModal extends Modal {
|
export default class ChangeEmailModal extends Modal {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the email has been changed successfully.
|
* Whether or not the email has been changed successfully.
|
||||||
@@ -22,6 +22,13 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
* @type {function}
|
* @type {function}
|
||||||
*/
|
*/
|
||||||
this.email = m.prop(app.session.user.email());
|
this.email = m.prop(app.session.user.email());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the password input.
|
||||||
|
*
|
||||||
|
* @type {function}
|
||||||
|
*/
|
||||||
|
this.password = m.prop('');
|
||||||
}
|
}
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
@@ -29,21 +36,19 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return app.trans('core.change_email');
|
return app.translator.trans('core.forum.change_email.title');
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
if (this.success) {
|
if (this.success) {
|
||||||
const emailProviderName = this.email().split('@')[1];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
<div className="Form Form--centered">
|
<div className="Form Form--centered">
|
||||||
<p className="helpText">{m.trust(app.trans('core.confirmation_email_sent', {email: <strong>{this.email()}</strong>}))}</p>
|
<p className="helpText">{app.translator.trans('core.forum.change_email.confirmation_message', {email: <strong>{this.email()}</strong>})}</p>
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
|
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
|
||||||
{app.trans('core.go_to', {location: emailProviderName})}
|
{app.translator.trans('core.forum.change_email.dismiss_button')}
|
||||||
</a>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,8 +61,13 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<input type="email" name="email" className="FormControl"
|
<input type="email" name="email" className="FormControl"
|
||||||
placeholder={app.session.user.email()}
|
placeholder={app.session.user.email()}
|
||||||
value={this.email()}
|
bidi={this.email}
|
||||||
onchange={m.withAttr('value', this.email)}
|
disabled={this.loading}/>
|
||||||
|
</div>
|
||||||
|
<div className="Form-group">
|
||||||
|
<input type="password" name="password" className="FormControl"
|
||||||
|
placeholder={app.translator.trans('core.forum.change_email.confirm_password_placeholder')}
|
||||||
|
bidi={this.password}
|
||||||
disabled={this.loading}/>
|
disabled={this.loading}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
@@ -65,7 +75,7 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
className: 'Button Button--primary Button--block',
|
className: 'Button Button--primary Button--block',
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
loading: this.loading,
|
loading: this.loading,
|
||||||
children: app.trans('core.save_changes')
|
children: app.translator.trans('core.forum.change_email.submit_button')
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,17 +93,24 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldEmail = app.session.user.email();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
app.session.user.save({email: this.email()}).then(
|
app.session.user.save({email: this.email()}, {
|
||||||
() => {
|
errorHandler: this.onerror.bind(this),
|
||||||
this.loading = false;
|
meta: {password: this.password()}
|
||||||
this.success = true;
|
})
|
||||||
m.redraw();
|
.then(() => this.success = true)
|
||||||
},
|
.catch(() => {})
|
||||||
() => {
|
.then(this.loaded.bind(this));
|
||||||
this.loading = false;
|
}
|
||||||
}
|
|
||||||
);
|
onerror(error) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
error.alert.props.children = app.translator.trans('core.forum.change_email.incorrect_password_message');
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onerror(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -11,20 +11,20 @@ export default class ChangePasswordModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return app.trans('core.change_password');
|
return app.translator.trans('core.forum.change_password.title');
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
return (
|
return (
|
||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
<div className="Form Form--centered">
|
<div className="Form Form--centered">
|
||||||
<p className="helpText">{app.trans('core.change_password_help')}</p>
|
<p className="helpText">{app.translator.trans('core.forum.change_password.text')}</p>
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
{Button.component({
|
{Button.component({
|
||||||
className: 'Button Button--primary Button--block',
|
className: 'Button Button--primary Button--block',
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
loading: this.loading,
|
loading: this.loading,
|
||||||
children: app.trans('core.send_password_reset_email')
|
children: app.translator.trans('core.forum.change_password.send_button')
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,8 +42,8 @@ export default class ChangePasswordModal extends Modal {
|
|||||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||||
data: {email: app.session.user.email()}
|
data: {email: app.session.user.email()}
|
||||||
}).then(
|
}).then(
|
||||||
() => this.hide(),
|
this.hide.bind(this),
|
||||||
() => this.loading = false
|
this.loaded.bind(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
/*global s9e*/
|
/*global s9e, hljs*/
|
||||||
|
|
||||||
import Post from 'flarum/components/Post';
|
import Post from 'flarum/components/Post';
|
||||||
import classList from 'flarum/utils/classList';
|
import classList from 'flarum/utils/classList';
|
||||||
@@ -6,7 +6,6 @@ import PostUser from 'flarum/components/PostUser';
|
|||||||
import PostMeta from 'flarum/components/PostMeta';
|
import PostMeta from 'flarum/components/PostMeta';
|
||||||
import PostEdited from 'flarum/components/PostEdited';
|
import PostEdited from 'flarum/components/PostEdited';
|
||||||
import EditPostComposer from 'flarum/components/EditPostComposer';
|
import EditPostComposer from 'flarum/components/EditPostComposer';
|
||||||
import Composer from 'flarum/components/Composer';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
import listItems from 'flarum/helpers/listItems';
|
import listItems from 'flarum/helpers/listItems';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
@@ -21,8 +20,8 @@ import Button from 'flarum/components/Button';
|
|||||||
* - `post`
|
* - `post`
|
||||||
*/
|
*/
|
||||||
export default class CommentPost extends Post {
|
export default class CommentPost extends Post {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the post has been hidden, then this flag determines whether or not its
|
* If the post has been hidden, then this flag determines whether or not its
|
||||||
@@ -42,36 +41,54 @@ export default class CommentPost extends Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
return [
|
// Note: we avoid using JSX for the <ul> below because it results in some
|
||||||
<header className="Post-header"><ul>{listItems(this.headerItems().toArray())}</ul></header>,
|
// weirdness in Mithril.js 0.1.x (see flarum/core#975). This workaround can
|
||||||
|
// be reverted when we upgrade to Mithril 1.0.
|
||||||
|
return super.content().concat([
|
||||||
|
<header className="Post-header">{m('ul', listItems(this.headerItems().toArray()))}</header>,
|
||||||
<div className="Post-body">
|
<div className="Post-body">
|
||||||
{this.isEditing()
|
{this.isEditing()
|
||||||
? <div className="Post-preview" config={this.configPreview.bind(this)}/>
|
? <div className="Post-preview" config={this.configPreview.bind(this)}/>
|
||||||
: m.trust(this.props.post.contentHtml())}
|
: m.trust(this.props.post.contentHtml())}
|
||||||
</div>,
|
</div>
|
||||||
<footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>,
|
]);
|
||||||
<aside className="Post-actions"><ul>{listItems(this.actionItems().toArray())}</ul></aside>
|
}
|
||||||
];
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
super.config(...arguments);
|
||||||
|
|
||||||
|
const contentHtml = this.isEditing() ? '' : this.props.post.contentHtml();
|
||||||
|
|
||||||
|
// If the post content has changed since the last render, we'll run through
|
||||||
|
// all of the <script> tags in the content and evaluate them. This is
|
||||||
|
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
|
||||||
|
if (context.contentHtml !== contentHtml) {
|
||||||
|
this.$('.Post-body script').each(function() {
|
||||||
|
eval.call(window, $(this).text());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
context.contentHtml = contentHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
isEditing() {
|
isEditing() {
|
||||||
return app.composer.component instanceof EditPostComposer &&
|
return app.composer.component instanceof EditPostComposer &&
|
||||||
app.composer.component.props.post === this.props.post &&
|
app.composer.component.props.post === this.props.post;
|
||||||
app.composer.position !== Composer.PositionEnum.MINIMIZED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs() {
|
attrs() {
|
||||||
const post = this.props.post;
|
const post = this.props.post;
|
||||||
|
const attrs = super.attrs();
|
||||||
|
|
||||||
return {
|
attrs.className += ' '+classList({
|
||||||
className: classList({
|
'CommentPost': true,
|
||||||
'CommentPost': true,
|
'Post--hidden': post.isHidden(),
|
||||||
'hidden': post.isHidden(),
|
'Post--edited': post.isEdited(),
|
||||||
'edited': post.isEdited(),
|
'revealContent': this.revealContent,
|
||||||
'revealContent': this.revealContent,
|
'editing': this.isEditing()
|
||||||
'editing': this.isEditing()
|
});
|
||||||
})
|
|
||||||
};
|
return attrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
configPreview(element, isInitialized, context) {
|
configPreview(element, isInitialized, context) {
|
||||||
@@ -133,22 +150,4 @@ export default class CommentPost extends Post {
|
|||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an item list for the post's footer.
|
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
*/
|
|
||||||
footerItems() {
|
|
||||||
return new ItemList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an item list for the post's actions.
|
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
*/
|
|
||||||
actionItems() {
|
|
||||||
return new ItemList();
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -11,9 +11,7 @@ import computed from 'flarum/utils/computed';
|
|||||||
* `show`, `hide`, `close`, `minimize`, `fullScreen`, and `exitFullScreen`.
|
* `show`, `hide`, `close`, `minimize`, `fullScreen`, and `exitFullScreen`.
|
||||||
*/
|
*/
|
||||||
class Composer extends Component {
|
class Composer extends Component {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The composer's current position.
|
* The composer's current position.
|
||||||
*
|
*
|
||||||
@@ -21,13 +19,6 @@ class Composer extends Component {
|
|||||||
*/
|
*/
|
||||||
this.position = Composer.PositionEnum.HIDDEN;
|
this.position = Composer.PositionEnum.HIDDEN;
|
||||||
|
|
||||||
/**
|
|
||||||
* The composer's previous position.
|
|
||||||
*
|
|
||||||
* @type {Composer.PositionEnum}
|
|
||||||
*/
|
|
||||||
this.oldPosition = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The composer's intended height, which can be modified by the user
|
* The composer's intended height, which can be modified by the user
|
||||||
* (by dragging the composer handle).
|
* (by dragging the composer handle).
|
||||||
@@ -36,6 +27,13 @@ class Composer extends Component {
|
|||||||
*/
|
*/
|
||||||
this.height = null;
|
this.height = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the composer currently has focus.
|
||||||
|
*
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
|
this.active = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed the composer's current height, based on the intended height, and
|
* Computed the composer's current height, based on the intended height, and
|
||||||
* the composer's current state. This will be applied to the composer's
|
* the composer's current state. This will be applied to the composer's
|
||||||
@@ -61,20 +59,19 @@ class Composer extends Component {
|
|||||||
|
|
||||||
view() {
|
view() {
|
||||||
const classes = {
|
const classes = {
|
||||||
|
'normal': this.position === Composer.PositionEnum.NORMAL,
|
||||||
'minimized': this.position === Composer.PositionEnum.MINIMIZED,
|
'minimized': this.position === Composer.PositionEnum.MINIMIZED,
|
||||||
'fullScreen': this.position === Composer.PositionEnum.FULLSCREEN
|
'fullScreen': this.position === Composer.PositionEnum.FULLSCREEN,
|
||||||
|
'active': this.active
|
||||||
};
|
};
|
||||||
classes.visible = this.position === Composer.PositionEnum.NORMAL || classes.minimized || classes.fullScreen;
|
classes.visible = classes.normal || classes.minimized || classes.fullScreen;
|
||||||
|
|
||||||
// If the composer is minimized, tell the composer's content component that
|
// If the composer is minimized, tell the composer's content component that
|
||||||
// it shouldn't let the user interact with it. Set up a handler so that if
|
// it shouldn't let the user interact with it. Set up a handler so that if
|
||||||
// the content IS clicked, the composer will be shown.
|
// the content IS clicked, the composer will be shown.
|
||||||
if (this.component) this.component.props.disabled = classes.minimized;
|
if (this.component) this.component.props.disabled = classes.minimized;
|
||||||
|
|
||||||
const showIfMinimized = () => {
|
const showIfMinimized = this.position === Composer.PositionEnum.MINIMIZED ? this.show.bind(this) : undefined;
|
||||||
if (this.position === Composer.PositionEnum.MINIMIZED) this.show();
|
|
||||||
m.redraw.strategy('none');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'Composer ' + classList(classes)}>
|
<div className={'Composer ' + classList(classes)}>
|
||||||
@@ -88,7 +85,11 @@ class Composer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config(isInitialized, context) {
|
config(isInitialized, context) {
|
||||||
this.updateHeight();
|
let defaultHeight;
|
||||||
|
|
||||||
|
if (!isInitialized) {
|
||||||
|
defaultHeight = this.$().height();
|
||||||
|
}
|
||||||
|
|
||||||
if (isInitialized) return;
|
if (isInitialized) return;
|
||||||
|
|
||||||
@@ -99,12 +100,15 @@ class Composer extends Component {
|
|||||||
// Initialize the composer's intended height based on what the user has set
|
// Initialize the composer's intended height based on what the user has set
|
||||||
// it at previously, or otherwise the composer's default height. After that,
|
// it at previously, or otherwise the composer's default height. After that,
|
||||||
// we'll hide the composer.
|
// we'll hide the composer.
|
||||||
this.height = localStorage.getItem('composerHeight') || this.$().height();
|
this.height = localStorage.getItem('composerHeight') || defaultHeight;
|
||||||
this.$().hide();
|
this.$().hide().css('bottom', -this.height);
|
||||||
|
|
||||||
// Whenever any of the inputs inside the composer are have focus, we want to
|
// Whenever any of the inputs inside the composer are have focus, we want to
|
||||||
// add a class to the composer to draw attention to it.
|
// add a class to the composer to draw attention to it.
|
||||||
this.$().on('focus blur', ':input', e => this.$().toggleClass('active', e.type === 'focusin'));
|
this.$().on('focus blur', ':input', e => {
|
||||||
|
this.active = e.type === 'focusin';
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
|
||||||
// When the escape key is pressed on any inputs, close the composer.
|
// When the escape key is pressed on any inputs, close the composer.
|
||||||
this.$().on('keydown', ':input', 'esc', () => this.close());
|
this.$().on('keydown', ':input', 'esc', () => this.close());
|
||||||
@@ -113,7 +117,7 @@ class Composer extends Component {
|
|||||||
// component a chance to scream at the user to make sure they don't
|
// component a chance to scream at the user to make sure they don't
|
||||||
// unintentionally lose any contnet.
|
// unintentionally lose any contnet.
|
||||||
window.onbeforeunload = () => {
|
window.onbeforeunload = () => {
|
||||||
return (this.component && this.component.preventExit()) || null;
|
return (this.component && this.component.preventExit()) || undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
@@ -146,7 +150,7 @@ class Composer extends Component {
|
|||||||
const composer = this;
|
const composer = this;
|
||||||
|
|
||||||
$(element).css('cursor', 'row-resize')
|
$(element).css('cursor', 'row-resize')
|
||||||
.bind('dragstart mousedown', e => e.preventDefault())
|
.on('dragstart mousedown', e => e.preventDefault())
|
||||||
.mousedown(function(e) {
|
.mousedown(function(e) {
|
||||||
composer.mouseStart = e.clientY;
|
composer.mouseStart = e.clientY;
|
||||||
composer.heightStart = composer.$().height();
|
composer.heightStart = composer.$().height();
|
||||||
@@ -198,19 +202,17 @@ class Composer extends Component {
|
|||||||
* of any flexible elements inside the composer's body.
|
* of any flexible elements inside the composer's body.
|
||||||
*/
|
*/
|
||||||
updateHeight() {
|
updateHeight() {
|
||||||
// TODO: update this in a way that is independent of the TextEditor being
|
|
||||||
// present.
|
|
||||||
const height = this.computedHeight();
|
const height = this.computedHeight();
|
||||||
const $flexible = this.$('.TextEditor-flexible');
|
const $flexible = this.$('.Composer-flexible');
|
||||||
|
|
||||||
this.$().height(height);
|
this.$().height(height);
|
||||||
|
|
||||||
if ($flexible.length) {
|
if ($flexible.length) {
|
||||||
const headerHeight = $flexible.offset().top - this.$().offset().top;
|
const headerHeight = $flexible.offset().top - this.$().offset().top;
|
||||||
const paddingBottom = parseInt($flexible.css('padding-bottom'), 10);
|
const paddingBottom = parseInt($flexible.css('padding-bottom'), 10);
|
||||||
const footerHeight = this.$('.TextEditor-controls').outerHeight(true);
|
const footerHeight = this.$('.Composer-footer').outerHeight(true);
|
||||||
|
|
||||||
$flexible.height(height - headerHeight - paddingBottom - footerHeight);
|
$flexible.height(this.$().outerHeight() - headerHeight - paddingBottom - footerHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,99 +223,27 @@ class Composer extends Component {
|
|||||||
*/
|
*/
|
||||||
updateBodyPadding() {
|
updateBodyPadding() {
|
||||||
const visible = this.position !== Composer.PositionEnum.HIDDEN &&
|
const visible = this.position !== Composer.PositionEnum.HIDDEN &&
|
||||||
this.position !== Composer.PositionEnum.MINIMIZED;
|
this.position !== Composer.PositionEnum.MINIMIZED &&
|
||||||
|
this.$().css('position') !== 'absolute';
|
||||||
|
|
||||||
const paddingBottom = visible
|
const paddingBottom = visible
|
||||||
? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10)
|
? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
$('#content').css({paddingBottom});
|
$('#content').css({paddingBottom});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update (and animate) the DOM to reflect the composer's current state.
|
* Determine whether or not the Composer is covering the screen.
|
||||||
|
*
|
||||||
|
* This will be true if the Composer is in full-screen mode on desktop, or
|
||||||
|
* if the Composer is positioned absolutely as on mobile devices.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
update() {
|
isFullScreen() {
|
||||||
// Before we redraw the composer to its new state, we need to save the
|
return this.position === Composer.PositionEnum.FULLSCREEN || this.$().css('position') === 'absolute';
|
||||||
// current height of the composer, as well as the page's scroll position, so
|
|
||||||
// that we can smoothly transition from the old to the new state.
|
|
||||||
const $composer = this.$().stop(true);
|
|
||||||
const oldHeight = $composer.is(':visible') ? $composer.outerHeight() : 0;
|
|
||||||
const scrollTop = $(window).scrollTop();
|
|
||||||
|
|
||||||
m.redraw(true);
|
|
||||||
|
|
||||||
// Now that we've redrawn and the composer's DOM has been updated, we want
|
|
||||||
// to update the composer's height. Once we've done that, we'll capture the
|
|
||||||
// real value to use as the end point for our animation later on.
|
|
||||||
$composer.show();
|
|
||||||
this.updateHeight();
|
|
||||||
|
|
||||||
const newHeight = $composer.outerHeight();
|
|
||||||
|
|
||||||
switch (this.position) {
|
|
||||||
case Composer.PositionEnum.NORMAL:
|
|
||||||
// If the composer is being opened, we will make it visible and animate
|
|
||||||
// it growing/sliding up from the bottom of the viewport. Or if the user
|
|
||||||
// has just exited fullscreen mode, we will simply tell the content to
|
|
||||||
// take focus.
|
|
||||||
if (this.oldPosition !== Composer.PositionEnum.FULLSCREEN) {
|
|
||||||
$composer.show()
|
|
||||||
.css({height: oldHeight})
|
|
||||||
.animate({bottom: 0, height: newHeight}, 'fast', this.component.focus.bind(this.component));
|
|
||||||
|
|
||||||
if ($composer.css('position') === 'absolute') {
|
|
||||||
$composer.css('top', $(window).scrollTop());
|
|
||||||
|
|
||||||
this.$backdrop = $('<div/>')
|
|
||||||
.addClass('composer-backdrop')
|
|
||||||
.appendTo('body');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.component.focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Composer.PositionEnum.MINIMIZED:
|
|
||||||
// If the composer has been minimized, we will animate it shrinking down
|
|
||||||
// to its new smaller size.
|
|
||||||
$composer.css({top: 'auto', height: oldHeight})
|
|
||||||
.animate({height: newHeight}, 'fast');
|
|
||||||
|
|
||||||
if (this.$backdrop) this.$backdrop.remove();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Composer.PositionEnum.HIDDEN:
|
|
||||||
// If the composer has been hidden, then we will animate it sliding down
|
|
||||||
// beyond the edge of the viewport. Once the animation is complete, we
|
|
||||||
// un-draw the composer's component.
|
|
||||||
$composer.css({top: 'auto', height: oldHeight})
|
|
||||||
.animate({bottom: -newHeight}, 'fast', () => {
|
|
||||||
$composer.hide();
|
|
||||||
this.clear();
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.$backdrop) this.$backdrop.remove();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Composer.PositionEnum.FULLSCREEN:
|
|
||||||
this.component.focus();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provided the composer isn't in fullscreen mode, we'll want to update the
|
|
||||||
// body's padding to make sure all of the page's content can still be seen.
|
|
||||||
// Plus, we'll scroll back to where we were before the composer was opened,
|
|
||||||
// as its opening may have changed the content of the page.
|
|
||||||
if (this.position !== Composer.PositionEnum.FULLSCREEN) {
|
|
||||||
this.updateBodyPadding();
|
|
||||||
$('html, body').scrollTop(scrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.oldPosition = this.position;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,20 +293,77 @@ class Composer extends Component {
|
|||||||
this.component = null;
|
this.component = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animate the Composer into the given position.
|
||||||
|
*
|
||||||
|
* @param {Composer.PositionEnum} position
|
||||||
|
*/
|
||||||
|
animateToPosition(position) {
|
||||||
|
// Before we redraw the composer to its new state, we need to save the
|
||||||
|
// current height of the composer, as well as the page's scroll position, so
|
||||||
|
// that we can smoothly transition from the old to the new state.
|
||||||
|
const oldPosition = this.position;
|
||||||
|
const $composer = this.$().stop(true);
|
||||||
|
const oldHeight = $composer.outerHeight();
|
||||||
|
const scrollTop = $(window).scrollTop();
|
||||||
|
|
||||||
|
this.position = position;
|
||||||
|
|
||||||
|
m.redraw(true);
|
||||||
|
|
||||||
|
// Now that we've redrawn and the composer's DOM has been updated, we want
|
||||||
|
// to update the composer's height. Once we've done that, we'll capture the
|
||||||
|
// real value to use as the end point for our animation later on.
|
||||||
|
$composer.show();
|
||||||
|
this.updateHeight();
|
||||||
|
|
||||||
|
const newHeight = $composer.outerHeight();
|
||||||
|
|
||||||
|
if (oldPosition === Composer.PositionEnum.HIDDEN) {
|
||||||
|
$composer.css({bottom: -newHeight, height: newHeight});
|
||||||
|
} else {
|
||||||
|
$composer.css({height: oldHeight});
|
||||||
|
}
|
||||||
|
|
||||||
|
$composer.animate({bottom: 0, height: newHeight}, 'fast', () => this.component.focus());
|
||||||
|
|
||||||
|
this.updateBodyPadding();
|
||||||
|
$(window).scrollTop(scrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the Composer backdrop.
|
||||||
|
*/
|
||||||
|
showBackdrop() {
|
||||||
|
this.$backdrop = $('<div/>')
|
||||||
|
.addClass('composer-backdrop')
|
||||||
|
.appendTo('body');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the Composer backdrop.
|
||||||
|
*/
|
||||||
|
hideBackdrop() {
|
||||||
|
if (this.$backdrop) this.$backdrop.remove();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the composer.
|
* Show the composer.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
show() {
|
show() {
|
||||||
// If the composer is hidden or minimized, we'll need to update its
|
if (this.position === Composer.PositionEnum.NORMAL || this.position === Composer.PositionEnum.FULLSCREEN) {
|
||||||
// position. Otherwise, if the composer is already showing (whether it's
|
return;
|
||||||
// fullscreen or not), we can leave it as is.
|
|
||||||
if ([Composer.PositionEnum.MINIMIZED, Composer.PositionEnum.HIDDEN].indexOf(this.position) !== -1) {
|
|
||||||
this.position = Composer.PositionEnum.NORMAL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update();
|
this.animateToPosition(Composer.PositionEnum.NORMAL);
|
||||||
|
|
||||||
|
if (this.isFullScreen()) {
|
||||||
|
this.$().css('top', $(window).scrollTop());
|
||||||
|
this.showBackdrop();
|
||||||
|
this.component.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,8 +372,20 @@ class Composer extends Component {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
this.position = Composer.PositionEnum.HIDDEN;
|
const $composer = this.$();
|
||||||
this.update();
|
|
||||||
|
// Animate the composer sliding down off the bottom edge of the viewport.
|
||||||
|
// Only when the animation is completed, update the Composer state flag and
|
||||||
|
// other elements on the page.
|
||||||
|
$composer.stop(true).animate({bottom: -$composer.height()}, 'fast', () => {
|
||||||
|
this.position = Composer.PositionEnum.HIDDEN;
|
||||||
|
this.clear();
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
$composer.hide();
|
||||||
|
this.hideBackdrop();
|
||||||
|
this.updateBodyPadding();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -407,10 +406,12 @@ class Composer extends Component {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
minimize() {
|
minimize() {
|
||||||
if (this.position !== Composer.PositionEnum.HIDDEN) {
|
if (this.position === Composer.PositionEnum.HIDDEN) return;
|
||||||
this.position = Composer.PositionEnum.MINIMIZED;
|
|
||||||
this.update();
|
this.animateToPosition(Composer.PositionEnum.MINIMIZED);
|
||||||
}
|
|
||||||
|
this.$().css('top', 'auto');
|
||||||
|
this.hideBackdrop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -422,7 +423,9 @@ class Composer extends Component {
|
|||||||
fullScreen() {
|
fullScreen() {
|
||||||
if (this.position !== Composer.PositionEnum.HIDDEN) {
|
if (this.position !== Composer.PositionEnum.HIDDEN) {
|
||||||
this.position = Composer.PositionEnum.FULLSCREEN;
|
this.position = Composer.PositionEnum.FULLSCREEN;
|
||||||
this.update();
|
m.redraw();
|
||||||
|
this.updateHeight();
|
||||||
|
this.component.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +437,9 @@ class Composer extends Component {
|
|||||||
exitFullScreen() {
|
exitFullScreen() {
|
||||||
if (this.position === Composer.PositionEnum.FULLSCREEN) {
|
if (this.position === Composer.PositionEnum.FULLSCREEN) {
|
||||||
this.position = Composer.PositionEnum.NORMAL;
|
this.position = Composer.PositionEnum.NORMAL;
|
||||||
this.update();
|
m.redraw();
|
||||||
|
this.updateHeight();
|
||||||
|
this.component.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,28 +454,28 @@ class Composer extends Component {
|
|||||||
if (this.position === Composer.PositionEnum.FULLSCREEN) {
|
if (this.position === Composer.PositionEnum.FULLSCREEN) {
|
||||||
items.add('exitFullScreen', ComposerButton.component({
|
items.add('exitFullScreen', ComposerButton.component({
|
||||||
icon: 'compress',
|
icon: 'compress',
|
||||||
title: app.trans('core.exit_full_screen'),
|
title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),
|
||||||
onclick: this.exitFullScreen.bind(this)
|
onclick: this.exitFullScreen.bind(this)
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
if (this.position !== Composer.PositionEnum.MINIMIZED) {
|
if (this.position !== Composer.PositionEnum.MINIMIZED) {
|
||||||
items.add('minimize', ComposerButton.component({
|
items.add('minimize', ComposerButton.component({
|
||||||
icon: 'minus minimize',
|
icon: 'minus minimize',
|
||||||
title: app.trans('core.minimize'),
|
title: app.translator.trans('core.forum.composer.minimize_tooltip'),
|
||||||
onclick: this.minimize.bind(this),
|
onclick: this.minimize.bind(this),
|
||||||
itemClassName: 'App-backControl'
|
itemClassName: 'App-backControl'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
items.add('fullScreen', ComposerButton.component({
|
items.add('fullScreen', ComposerButton.component({
|
||||||
icon: 'expand',
|
icon: 'expand',
|
||||||
title: app.trans('core.full_screen'),
|
title: app.translator.trans('core.forum.composer.full_screen_tooltip'),
|
||||||
onclick: this.fullScreen.bind(this)
|
onclick: this.fullScreen.bind(this)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add('close', ComposerButton.component({
|
items.add('close', ComposerButton.component({
|
||||||
icon: 'times',
|
icon: 'times',
|
||||||
title: app.trans('core.close'),
|
title: app.translator.trans('core.forum.composer.close_tooltip'),
|
||||||
onclick: this.close.bind(this)
|
onclick: this.close.bind(this)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
@@ -22,9 +22,7 @@ import ItemList from 'flarum/utils/ItemList';
|
|||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
export default class ComposerBody extends Component {
|
export default class ComposerBody extends Component {
|
||||||
constructor(props) {
|
init() {
|
||||||
super(props);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the component is loading.
|
* Whether or not the component is loading.
|
||||||
*
|
*
|
||||||
@@ -58,7 +56,7 @@ export default class ComposerBody extends Component {
|
|||||||
this.editor.props.disabled = this.loading;
|
this.editor.props.disabled = this.loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ComposerBody">
|
<div className={'ComposerBody ' + (this.props.className || '')}>
|
||||||
{avatar(this.props.user, {className: 'ComposerBody-avatar'})}
|
{avatar(this.props.user, {className: 'ComposerBody-avatar'})}
|
||||||
<div className="ComposerBody-content">
|
<div className="ComposerBody-content">
|
||||||
<ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>
|
<ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>
|
||||||
@@ -104,4 +102,12 @@ export default class ComposerBody extends Component {
|
|||||||
*/
|
*/
|
||||||
onsubmit() {
|
onsubmit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop loading.
|
||||||
|
*/
|
||||||
|
loaded() {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
import ComposerBody from 'flarum/components/ComposerBody';
|
import ComposerBody from 'flarum/components/ComposerBody';
|
||||||
|
import extractText from 'flarum/utils/extractText';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `DiscussionComposer` component displays the composer content for starting
|
* The `DiscussionComposer` component displays the composer content for starting
|
||||||
@@ -12,8 +13,8 @@ import ComposerBody from 'flarum/components/ComposerBody';
|
|||||||
* - `titlePlaceholder`
|
* - `titlePlaceholder`
|
||||||
*/
|
*/
|
||||||
export default class DiscussionComposer extends ComposerBody {
|
export default class DiscussionComposer extends ComposerBody {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the title input.
|
* The value of the title input.
|
||||||
@@ -26,16 +27,19 @@ export default class DiscussionComposer extends ComposerBody {
|
|||||||
static initProps(props) {
|
static initProps(props) {
|
||||||
super.initProps(props);
|
super.initProps(props);
|
||||||
|
|
||||||
props.placeholder = props.placeholder || app.trans('core.write_a_post');
|
props.placeholder = props.placeholder || extractText(app.translator.trans('core.forum.composer_discussion.body_placeholder'));
|
||||||
props.submitLabel = props.submitLabel || app.trans('core.post_discussion');
|
props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_discussion.submit_button');
|
||||||
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_discussion');
|
props.confirmExit = props.confirmExit || extractText(app.translator.trans('core.forum.composer_discussion.discard_confirmation'));
|
||||||
props.titlePlaceholder = props.titlePlaceholder || app.trans('core.discussion_title');
|
props.titlePlaceholder = props.titlePlaceholder || extractText(app.translator.trans('core.forum.composer_discussion.title_placeholder'));
|
||||||
|
props.className = 'ComposerBody--discussion';
|
||||||
}
|
}
|
||||||
|
|
||||||
headerItems() {
|
headerItems() {
|
||||||
const items = super.headerItems();
|
const items = super.headerItems();
|
||||||
|
|
||||||
items.add('title', (
|
items.add('title', <h3>{app.translator.trans('core.forum.composer_discussion.title')}</h3>, 100);
|
||||||
|
|
||||||
|
items.add('discussionTitle', (
|
||||||
<h3>
|
<h3>
|
||||||
<input className="FormControl"
|
<input className="FormControl"
|
||||||
value={this.title()}
|
value={this.title()}
|
||||||
@@ -64,23 +68,6 @@ export default class DiscussionComposer extends ComposerBody {
|
|||||||
m.redraw.strategy('none');
|
m.redraw.strategy('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
config(isInitialized, context) {
|
|
||||||
super.config(isInitialized, context);
|
|
||||||
|
|
||||||
// If the user presses the backspace key in the text editor, and the cursor
|
|
||||||
// is already at the start, then we'll move the focus back into the title
|
|
||||||
// input.
|
|
||||||
this.editor.$('textarea').keydown((e) => {
|
|
||||||
if (e.which === 8 && e.target.selectionStart === 0 && e.target.selectionEnd === 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const $title = this.$(':input:enabled:visible:first')[0];
|
|
||||||
$title.focus();
|
|
||||||
$title.selectionStart = $title.selectionEnd = $title.value.length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
preventExit() {
|
preventExit() {
|
||||||
return (this.title() || this.content()) && this.props.confirmExit;
|
return (this.title() || this.content()) && this.props.confirmExit;
|
||||||
}
|
}
|
||||||
@@ -108,11 +95,7 @@ export default class DiscussionComposer extends ComposerBody {
|
|||||||
app.cache.discussionList.addDiscussion(discussion);
|
app.cache.discussionList.addDiscussion(discussion);
|
||||||
m.route(app.route.discussion(discussion));
|
m.route(app.route.discussion(discussion));
|
||||||
},
|
},
|
||||||
response => {
|
this.loaded.bind(this)
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
app.alertErrors(response.errors);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,6 +2,7 @@ import Component from 'flarum/Component';
|
|||||||
import DiscussionListItem from 'flarum/components/DiscussionListItem';
|
import DiscussionListItem from 'flarum/components/DiscussionListItem';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||||
|
import Placeholder from 'flarum/components/Placeholder';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `DiscussionList` component displays a list of discussions.
|
* The `DiscussionList` component displays a list of discussions.
|
||||||
@@ -12,9 +13,7 @@ import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
|||||||
* to send along in the API request to get discussion results.
|
* to send along in the API request to get discussion results.
|
||||||
*/
|
*/
|
||||||
export default class DiscussionList extends Component {
|
export default class DiscussionList extends Component {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not discussion results are loading.
|
* Whether or not discussion results are loading.
|
||||||
*
|
*
|
||||||
@@ -47,12 +46,21 @@ export default class DiscussionList extends Component {
|
|||||||
loading = LoadingIndicator.component();
|
loading = LoadingIndicator.component();
|
||||||
} else if (this.moreResults) {
|
} else if (this.moreResults) {
|
||||||
loading = Button.component({
|
loading = Button.component({
|
||||||
children: app.trans('core.load_more'),
|
children: app.translator.trans('core.forum.discussion_list.load_more_button'),
|
||||||
className: 'Button',
|
className: 'Button',
|
||||||
onclick: this.loadMore.bind(this)
|
onclick: this.loadMore.bind(this)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.discussions.length === 0 && !this.loading) {
|
||||||
|
const text = app.translator.trans('core.forum.discussion_list.empty_text');
|
||||||
|
return (
|
||||||
|
<div className="DiscussionList">
|
||||||
|
{Placeholder.component({text})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="DiscussionList">
|
<div className="DiscussionList">
|
||||||
<ul className="DiscussionList-discussions">
|
<ul className="DiscussionList-discussions">
|
||||||
@@ -107,7 +115,7 @@ export default class DiscussionList extends Component {
|
|||||||
map.latest = '-lastTime';
|
map.latest = '-lastTime';
|
||||||
map.top = '-commentsCount';
|
map.top = '-commentsCount';
|
||||||
map.newest = '-startTime';
|
map.newest = '-startTime';
|
||||||
map.oldest = '+startTime';
|
map.oldest = 'startTime';
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@@ -179,12 +187,7 @@ export default class DiscussionList extends Component {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.moreResults = !!results.payload.links.next;
|
this.moreResults = !!results.payload.links.next;
|
||||||
|
|
||||||
// Since this may be called during the component's constructor, i.e. in the
|
m.lazyRedraw();
|
||||||
// middle of a redraw, forcing another redraw would not bode well. Instead
|
|
||||||
// we start/end a computation so Mithril will only redraw if it isn't
|
|
||||||
// already doing so.
|
|
||||||
m.startComputation();
|
|
||||||
m.endComputation();
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
@@ -13,6 +13,7 @@ import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
|
|||||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||||
import slidable from 'flarum/utils/slidable';
|
import slidable from 'flarum/utils/slidable';
|
||||||
import extractText from 'flarum/utils/extractText';
|
import extractText from 'flarum/utils/extractText';
|
||||||
|
import classList from 'flarum/utils/classList';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `DiscussionListItem` component shows a single discussion in the
|
* The `DiscussionListItem` component shows a single discussion in the
|
||||||
@@ -24,9 +25,7 @@ import extractText from 'flarum/utils/extractText';
|
|||||||
* - `params`
|
* - `params`
|
||||||
*/
|
*/
|
||||||
export default class DiscussionListItem extends Component {
|
export default class DiscussionListItem extends Component {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up a subtree retainer so that the discussion will not be redrawn
|
* Set up a subtree retainer so that the discussion will not be redrawn
|
||||||
* unless new data comes in.
|
* unless new data comes in.
|
||||||
@@ -43,6 +42,16 @@ export default class DiscussionListItem extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attrs() {
|
||||||
|
return {
|
||||||
|
className: classList([
|
||||||
|
'DiscussionListItem',
|
||||||
|
this.active() ? 'active' : '',
|
||||||
|
this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : ''
|
||||||
|
])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
const retain = this.subtree.retain();
|
const retain = this.subtree.retain();
|
||||||
|
|
||||||
@@ -51,13 +60,15 @@ export default class DiscussionListItem extends Component {
|
|||||||
const discussion = this.props.discussion;
|
const discussion = this.props.discussion;
|
||||||
const startUser = discussion.startUser();
|
const startUser = discussion.startUser();
|
||||||
const isUnread = discussion.isUnread();
|
const isUnread = discussion.isUnread();
|
||||||
|
const isRead = discussion.isRead();
|
||||||
const showUnread = !this.showRepliesCount() && isUnread;
|
const showUnread = !this.showRepliesCount() && isUnread;
|
||||||
const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
||||||
const relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
|
const relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
|
||||||
const controls = DiscussionControls.controls(discussion, this).toArray();
|
const controls = DiscussionControls.controls(discussion, this).toArray();
|
||||||
|
const attrs = this.attrs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'DiscussionListItem ' + (this.active() ? 'active' : '')}>
|
<div {...attrs}>
|
||||||
|
|
||||||
{controls.length ? Dropdown.component({
|
{controls.length ? Dropdown.component({
|
||||||
icon: 'ellipsis-v',
|
icon: 'ellipsis-v',
|
||||||
@@ -71,10 +82,10 @@ export default class DiscussionListItem extends Component {
|
|||||||
{icon('check')}
|
{icon('check')}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '')}>
|
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
|
||||||
<a href={startUser ? app.route.user(startUser) : '#'}
|
<a href={startUser ? app.route.user(startUser) : '#'}
|
||||||
className="DiscussionListItem-author"
|
className="DiscussionListItem-author"
|
||||||
title={extractText(app.trans('core.discussion_started', {user: startUser, ago: humanTime(discussion.startTime())}))}
|
title={extractText(app.translator.trans('core.forum.discussion_list.started_text', {user: startUser, ago: humanTime(discussion.startTime())}))}
|
||||||
config={function(element) {
|
config={function(element) {
|
||||||
$(element).tooltip({placement: 'right'});
|
$(element).tooltip({placement: 'right'});
|
||||||
m.route.apply(this, arguments);
|
m.route.apply(this, arguments);
|
||||||
@@ -95,7 +106,7 @@ export default class DiscussionListItem extends Component {
|
|||||||
|
|
||||||
<span className="DiscussionListItem-count"
|
<span className="DiscussionListItem-count"
|
||||||
onclick={this.markAsRead.bind(this)}
|
onclick={this.markAsRead.bind(this)}
|
||||||
title={showUnread ? app.trans('core.mark_as_read') : ''}>
|
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}>
|
||||||
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
|
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
|
||||||
</span>
|
</span>
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
import Component from 'flarum/Component';
|
import Page from 'flarum/components/Page';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
import DiscussionHero from 'flarum/components/DiscussionHero';
|
import DiscussionHero from 'flarum/components/DiscussionHero';
|
||||||
import PostStream from 'flarum/components/PostStream';
|
import PostStream from 'flarum/components/PostStream';
|
||||||
@@ -6,17 +6,15 @@ import PostStreamScrubber from 'flarum/components/PostStreamScrubber';
|
|||||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||||
import SplitDropdown from 'flarum/components/SplitDropdown';
|
import SplitDropdown from 'flarum/components/SplitDropdown';
|
||||||
import listItems from 'flarum/helpers/listItems';
|
import listItems from 'flarum/helpers/listItems';
|
||||||
import mixin from 'flarum/utils/mixin';
|
|
||||||
import evented from 'flarum/utils/evented';
|
|
||||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `DiscussionPage` component displays a whole discussion page, including
|
* The `DiscussionPage` component displays a whole discussion page, including
|
||||||
* the discussion list pane, the hero, the posts, and the sidebar.
|
* the discussion list pane, the hero, the posts, and the sidebar.
|
||||||
*/
|
*/
|
||||||
export default class DiscussionPage extends mixin(Component, evented) {
|
export default class DiscussionPage extends Page {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The discussion that is being viewed.
|
* The discussion that is being viewed.
|
||||||
@@ -43,18 +41,14 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
|||||||
app.pane.enable();
|
app.pane.enable();
|
||||||
app.pane.hide();
|
app.pane.hide();
|
||||||
|
|
||||||
if (app.current instanceof DiscussionPage) {
|
if (app.previous instanceof DiscussionPage) {
|
||||||
m.redraw.strategy('diff');
|
m.redraw.strategy('diff');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push onto the history stack, but use a generalised key so that navigating
|
|
||||||
// to a few different discussions won't override the behaviour of the back
|
|
||||||
// button.
|
|
||||||
app.history.push('discussion');
|
app.history.push('discussion');
|
||||||
app.current = this;
|
|
||||||
app.drawer.hide();
|
this.bodyClass = 'App--discussion';
|
||||||
app.modal.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload(e) {
|
onunload(e) {
|
||||||
@@ -67,9 +61,9 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
|||||||
if (idParam && idParam.split('-')[0] === this.discussion.id()) {
|
if (idParam && idParam.split('-')[0] === this.discussion.id()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const near = Number(m.route.param('near')) || 1;
|
const near = m.route.param('near') || '1';
|
||||||
|
|
||||||
if (near !== Number(this.near)) {
|
if (near !== String(this.near)) {
|
||||||
this.stream.goToNumber(near);
|
this.stream.goToNumber(near);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,13 +115,6 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
config(isInitialized, context) {
|
|
||||||
if (isInitialized) return;
|
|
||||||
|
|
||||||
$('#app').addClass('App--discussion');
|
|
||||||
context.onunload = () => $('#app').removeClass('App--discussion');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear and reload the discussion.
|
* Clear and reload the discussion.
|
||||||
*/
|
*/
|
||||||
@@ -141,20 +128,15 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
|||||||
// component for the first time on page load, then any calls to m.redraw
|
// component for the first time on page load, then any calls to m.redraw
|
||||||
// will be ineffective and thus any configs (scroll code) will be run
|
// will be ineffective and thus any configs (scroll code) will be run
|
||||||
// before stuff is drawn to the page.
|
// before stuff is drawn to the page.
|
||||||
setTimeout(this.init.bind(this, preloadedDiscussion));
|
setTimeout(this.show.bind(this, preloadedDiscussion), 0);
|
||||||
} else {
|
} else {
|
||||||
const params = this.requestParams();
|
const params = this.requestParams();
|
||||||
|
|
||||||
app.store.find('discussions', m.route.param('id').split('-')[0], params)
|
app.store.find('discussions', m.route.param('id').split('-')[0], params)
|
||||||
.then(this.init.bind(this));
|
.then(this.show.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since this may be called during the component's constructor, i.e. in the
|
m.lazyRedraw();
|
||||||
// middle of a redraw, forcing another redraw would not bode well. Instead
|
|
||||||
// we start/end a computation so Mithril will only redraw if it isn't
|
|
||||||
// already doing so.
|
|
||||||
m.startComputation();
|
|
||||||
m.endComputation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,9 +156,10 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
|||||||
*
|
*
|
||||||
* @param {Discussion} discussion
|
* @param {Discussion} discussion
|
||||||
*/
|
*/
|
||||||
init(discussion) {
|
show(discussion) {
|
||||||
this.discussion = discussion;
|
this.discussion = discussion;
|
||||||
|
|
||||||
|
app.history.push('discussion', discussion.title());
|
||||||
app.setTitle(discussion.title());
|
app.setTitle(discussion.title());
|
||||||
app.setTitleCount(0);
|
app.setTitleCount(0);
|
||||||
|
|
||||||
@@ -201,9 +184,7 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
|||||||
// the specific post that was routed to.
|
// the specific post that was routed to.
|
||||||
this.stream = new PostStream({discussion, includedPosts});
|
this.stream = new PostStream({discussion, includedPosts});
|
||||||
this.stream.on('positionChanged', this.positionChanged.bind(this));
|
this.stream.on('positionChanged', this.positionChanged.bind(this));
|
||||||
this.stream.goToNumber(m.route.param('near') || includedPosts[0].number(), true);
|
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
|
||||||
|
|
||||||
this.trigger('loaded', discussion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,7 +274,7 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
|||||||
m.route(url, true);
|
m.route(url, true);
|
||||||
window.history.replaceState(null, document.title, url);
|
window.history.replaceState(null, document.title, url);
|
||||||
|
|
||||||
app.history.push('discussion');
|
app.history.push('discussion', discussion.title());
|
||||||
|
|
||||||
// If the user hasn't read past here before, then we'll update their read
|
// If the user hasn't read past here before, then we'll update their read
|
||||||
// state and redraw.
|
// state and redraw.
|
@@ -20,6 +20,6 @@ export default class DiscussionRenamedNotification extends Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
return app.trans('core.discussion_renamed_notification', {user: this.props.notification.sender()});
|
return app.translator.trans('core.forum.notifications.discussion_renamed_text', {user: this.props.notification.sender()});
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
import EventPost from 'flarum/components/EventPost';
|
import EventPost from 'flarum/components/EventPost';
|
||||||
|
import extractText from 'flarum/utils/extractText';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `DiscussionRenamedPost` component displays a discussion event post
|
* The `DiscussionRenamedPost` component displays a discussion event post
|
||||||
@@ -13,8 +14,11 @@ export default class DiscussionRenamedPost extends EventPost {
|
|||||||
return 'pencil';
|
return 'pencil';
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptionKey() {
|
description(data) {
|
||||||
return 'core.discussion_renamed_post';
|
const renamed = app.translator.trans('core.forum.post_stream.discussion_renamed_text', data);
|
||||||
|
const oldName = app.translator.trans('core.forum.post_stream.discussion_renamed_old_tooltip', data);
|
||||||
|
|
||||||
|
return <span title={extractText(oldName)}>{renamed}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptionData() {
|
descriptionData() {
|
||||||
@@ -23,8 +27,8 @@ export default class DiscussionRenamedPost extends EventPost {
|
|||||||
const newTitle = post.content()[1];
|
const newTitle = post.content()[1];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
old: <strong className="DiscussionRenamedPost-old">{oldTitle}</strong>,
|
'old': oldTitle,
|
||||||
new: <strong className="DiscussionRenamedPost-new">{newTitle}</strong>
|
'new': <strong className="DiscussionRenamedPost-new">{newTitle}</strong>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -13,6 +13,8 @@ export default class DiscussionsSearchSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search(query) {
|
search(query) {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
|
||||||
this.results[query] = [];
|
this.results[query] = [];
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
@@ -25,14 +27,16 @@ export default class DiscussionsSearchSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
view(query) {
|
view(query) {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
|
||||||
const results = this.results[query] || [];
|
const results = this.results[query] || [];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<li className="Dropdown-header">{app.trans('core.discussions')}</li>,
|
<li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
|
||||||
<li>
|
<li>
|
||||||
{LinkButton.component({
|
{LinkButton.component({
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
children: app.trans('core.search_all_discussions', {query}),
|
children: app.translator.trans('core.forum.search.all_discussions_button', {query}),
|
||||||
href: app.route('index', {q: query})
|
href: app.route('index', {q: query})
|
||||||
})}
|
})}
|
||||||
</li>,
|
</li>,
|
@@ -6,8 +6,8 @@ import DiscussionList from 'flarum/components/DiscussionList';
|
|||||||
* page.
|
* page.
|
||||||
*/
|
*/
|
||||||
export default class DiscussionsUserPage extends UserPage {
|
export default class DiscussionsUserPage extends UserPage {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
this.loadUser(m.route.param('username'));
|
this.loadUser(m.route.param('username'));
|
||||||
}
|
}
|
@@ -1,6 +1,13 @@
|
|||||||
import ComposerBody from 'flarum/components/ComposerBody';
|
import ComposerBody from 'flarum/components/ComposerBody';
|
||||||
import icon from 'flarum/helpers/icon';
|
import icon from 'flarum/helpers/icon';
|
||||||
|
|
||||||
|
function minimizeComposerIfFullScreen(e) {
|
||||||
|
if (app.composer.isFullScreen()) {
|
||||||
|
app.composer.minimize();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `EditPostComposer` component displays the composer content for editing a
|
* The `EditPostComposer` component displays the composer content for editing a
|
||||||
* post. It sets the initial content to the content of the post that is being
|
* post. It sets the initial content to the content of the post that is being
|
||||||
@@ -12,10 +19,12 @@ import icon from 'flarum/helpers/icon';
|
|||||||
* - `post`
|
* - `post`
|
||||||
*/
|
*/
|
||||||
export default class EditPostComposer extends ComposerBody {
|
export default class EditPostComposer extends ComposerBody {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
|
this.editor.props.preview = e => {
|
||||||
|
minimizeComposerIfFullScreen(e);
|
||||||
|
|
||||||
this.editor.props.preview = () => {
|
|
||||||
m.route(app.route.post(this.props.post));
|
m.route(app.route.post(this.props.post));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -23,8 +32,8 @@ export default class EditPostComposer extends ComposerBody {
|
|||||||
static initProps(props) {
|
static initProps(props) {
|
||||||
super.initProps(props);
|
super.initProps(props);
|
||||||
|
|
||||||
props.submitLabel = props.submitLabel || app.trans('core.save_changes');
|
props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_edit.submit_button');
|
||||||
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_edit');
|
props.confirmExit = props.confirmExit || app.translator.trans('core.forum.composer_edit.discard_confirmation');
|
||||||
props.originalContent = props.originalContent || props.post.content();
|
props.originalContent = props.originalContent || props.post.content();
|
||||||
props.user = props.user || props.post.user();
|
props.user = props.user || props.post.user();
|
||||||
|
|
||||||
@@ -35,11 +44,17 @@ export default class EditPostComposer extends ComposerBody {
|
|||||||
const items = super.headerItems();
|
const items = super.headerItems();
|
||||||
const post = this.props.post;
|
const post = this.props.post;
|
||||||
|
|
||||||
|
const routeAndMinimize = function(element, isInitialized) {
|
||||||
|
if (isInitialized) return;
|
||||||
|
$(element).on('click', minimizeComposerIfFullScreen);
|
||||||
|
m.route.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
items.add('title', (
|
items.add('title', (
|
||||||
<h3>
|
<h3>
|
||||||
{icon('pencil')}{' '}
|
{icon('pencil')} {' '}
|
||||||
<a href={app.route.discussion(post.discussion(), post.number())} config={m.route}>
|
<a href={app.route.discussion(post.discussion(), post.number())} config={routeAndMinimize}>
|
||||||
{app.trans('core.editing_post', {number: post.number(), discussion: post.discussion().title()})}
|
{app.translator.trans('core.forum.composer_edit.post_link', {number: post.number(), discussion: post.discussion().title()})}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
));
|
));
|
||||||
@@ -64,11 +79,8 @@ export default class EditPostComposer extends ComposerBody {
|
|||||||
const data = this.data();
|
const data = this.data();
|
||||||
|
|
||||||
this.props.post.save(data).then(
|
this.props.post.save(data).then(
|
||||||
() => {
|
() => app.composer.hide(),
|
||||||
app.composer.hide();
|
this.loaded.bind(this)
|
||||||
m.redraw();
|
|
||||||
},
|
|
||||||
() => this.loading = false
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,131 +0,0 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import GroupBadge from 'flarum/components/GroupBadge';
|
|
||||||
import Group from 'flarum/models/Group';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `EditUserModal` component displays a modal dialog with a login form.
|
|
||||||
*/
|
|
||||||
export default class EditUserModal extends Modal {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
const user = this.props.user;
|
|
||||||
|
|
||||||
this.username = m.prop(user.username() || '');
|
|
||||||
this.email = m.prop(user.email() || '');
|
|
||||||
this.setPassword = m.prop(false);
|
|
||||||
this.password = m.prop(user.password() || '');
|
|
||||||
this.groups = {};
|
|
||||||
|
|
||||||
app.store.all('groups')
|
|
||||||
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
|
||||||
.forEach(group => this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1));
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
return 'EditUserModal Modal--small';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return 'Edit User';
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return (
|
|
||||||
<div className="Modal-body">
|
|
||||||
<div className="Form">
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>Username</label>
|
|
||||||
<input className="FormControl" placeholder={app.trans('core.username')}
|
|
||||||
value={this.username()}
|
|
||||||
onchange={m.withAttr('value', this.username)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>Email</label>
|
|
||||||
<div>
|
|
||||||
<input className="FormControl" placeholder={app.trans('core.email')}
|
|
||||||
value={this.email()}
|
|
||||||
onchange={m.withAttr('value', this.email)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>Password</label>
|
|
||||||
<div>
|
|
||||||
<label className="checkbox">
|
|
||||||
<input type="checkbox" checked={this.setPassword()} onchange={e => {
|
|
||||||
this.setPassword(e.target.checked);
|
|
||||||
m.redraw(true);
|
|
||||||
if (e.target.checked) this.$('[name=password]').select();
|
|
||||||
m.redraw.strategy('none');
|
|
||||||
}}/>
|
|
||||||
Set new password
|
|
||||||
</label>
|
|
||||||
{this.setPassword() ? (
|
|
||||||
<input className="FormControl" type="password" name="password" placeholder={app.trans('core.password')}
|
|
||||||
value={this.password()}
|
|
||||||
onchange={m.withAttr('value', this.password)} />
|
|
||||||
) : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group EditUserModal-groups">
|
|
||||||
<label>Groups</label>
|
|
||||||
<div>
|
|
||||||
{Object.keys(this.groups)
|
|
||||||
.map(id => app.store.getById('groups', id))
|
|
||||||
.map(group => (
|
|
||||||
<label className="checkbox">
|
|
||||||
<input type="checkbox"
|
|
||||||
checked={this.groups[group.id()]()}
|
|
||||||
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID}
|
|
||||||
onchange={m.withAttr('checked', this.groups[group.id()])}/>
|
|
||||||
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
type: 'submit',
|
|
||||||
loading: this.loading,
|
|
||||||
children: app.trans('core.save_changes')
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
const groups = Object.keys(this.groups)
|
|
||||||
.filter(id => this.groups[id]())
|
|
||||||
.map(id => app.store.getById('groups', id));
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
username: this.username(),
|
|
||||||
email: this.email(),
|
|
||||||
relationships: {groups}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.setPassword()) {
|
|
||||||
data.password = this.password();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.user.save(data).then(
|
|
||||||
() => this.hide(),
|
|
||||||
response => {
|
|
||||||
this.loading = false;
|
|
||||||
this.handleErrors(response);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
165
js/forum/src/components/EditUserModal.tsx
Normal file
165
js/forum/src/components/EditUserModal.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import Modal from 'flarum/components/Modal';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import GroupBadge from 'flarum/components/GroupBadge';
|
||||||
|
import Group from 'flarum/models/Group';
|
||||||
|
import extractText from 'flarum/utils/extractText';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `EditUserModal` component displays a modal dialog with a login form.
|
||||||
|
*/
|
||||||
|
export default class EditUserModal extends Modal {
|
||||||
|
init() {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
const user = this.props.user;
|
||||||
|
|
||||||
|
this.username = m.prop(user.username() || '');
|
||||||
|
this.email = m.prop(user.email() || '');
|
||||||
|
this.isActivated = m.prop(user.isActivated() || false);
|
||||||
|
this.setPassword = m.prop(false);
|
||||||
|
this.password = m.prop(user.password() || '');
|
||||||
|
this.groups = {};
|
||||||
|
|
||||||
|
app.store.all('groups')
|
||||||
|
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||||
|
.forEach(group => this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
className() {
|
||||||
|
return 'EditUserModal Modal--small';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return app.translator.trans('core.forum.edit_user.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return (
|
||||||
|
<div className="Modal-body">
|
||||||
|
<div className="Form">
|
||||||
|
<div className="Form-group">
|
||||||
|
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
|
||||||
|
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
|
||||||
|
bidi={this.username} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{app.session.user !== this.props.user ? [
|
||||||
|
<div className="Form-group">
|
||||||
|
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
|
||||||
|
<div>
|
||||||
|
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
|
||||||
|
bidi={this.email} />
|
||||||
|
</div>
|
||||||
|
{!this.isActivated() ? (
|
||||||
|
<div>
|
||||||
|
{Button.component({
|
||||||
|
className: 'Button Button--block',
|
||||||
|
children: app.translator.trans('core.forum.edit_user.activate_button'),
|
||||||
|
loading: this.loading,
|
||||||
|
onclick: this.activate.bind(this)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : ''}
|
||||||
|
</div>,
|
||||||
|
|
||||||
|
<div className="Form-group">
|
||||||
|
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
|
||||||
|
<div>
|
||||||
|
<label className="checkbox">
|
||||||
|
<input type="checkbox" checked={this.setPassword()} onchange={e => {
|
||||||
|
this.setPassword(e.target.checked);
|
||||||
|
m.redraw(true);
|
||||||
|
if (e.target.checked) this.$('[name=password]').select();
|
||||||
|
m.redraw.strategy('none');
|
||||||
|
}}/>
|
||||||
|
{app.translator.trans('core.forum.edit_user.set_password_label')}
|
||||||
|
</label>
|
||||||
|
{this.setPassword() ? (
|
||||||
|
<input className="FormControl" type="password" name="password" placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
|
||||||
|
bidi={this.password} />
|
||||||
|
) : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
] : ''}
|
||||||
|
|
||||||
|
<div className="Form-group EditUserModal-groups">
|
||||||
|
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
|
||||||
|
<div>
|
||||||
|
{Object.keys(this.groups)
|
||||||
|
.map(id => app.store.getById('groups', id))
|
||||||
|
.map(group => (
|
||||||
|
<label className="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
bidi={this.groups[group.id()]}
|
||||||
|
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID} />
|
||||||
|
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="Form-group">
|
||||||
|
{Button.component({
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
type: 'submit',
|
||||||
|
loading: this.loading,
|
||||||
|
children: app.translator.trans('core.forum.edit_user.submit_button')
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
this.loading = true;
|
||||||
|
const data = {
|
||||||
|
username: this.username(),
|
||||||
|
isActivated: true,
|
||||||
|
};
|
||||||
|
this.props.user.save(data, {errorHandler: this.onerror.bind(this)})
|
||||||
|
.then(() => {
|
||||||
|
this.isActivated(true);
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const groups = Object.keys(this.groups)
|
||||||
|
.filter(id => this.groups[id]())
|
||||||
|
.map(id => app.store.getById('groups', id));
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
username: this.username(),
|
||||||
|
relationships: {groups}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (app.session.user !== this.props.user) {
|
||||||
|
data.email = this.email();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.setPassword()) {
|
||||||
|
data.password = this.password();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
onsubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.props.user.save(this.data(), {errorHandler: this.onerror.bind(this)})
|
||||||
|
.then(this.hide.bind(this))
|
||||||
|
.catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -16,9 +16,11 @@ import icon from 'flarum/helpers/icon';
|
|||||||
*/
|
*/
|
||||||
export default class EventPost extends Post {
|
export default class EventPost extends Post {
|
||||||
attrs() {
|
attrs() {
|
||||||
return {
|
const attrs = super.attrs();
|
||||||
className: 'EventPost ' + ucfirst(this.props.post.contentType()) + 'Post'
|
|
||||||
};
|
attrs.className += ' EventPost ' + ucfirst(this.props.post.contentType()) + 'Post';
|
||||||
|
|
||||||
|
return attrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
@@ -31,12 +33,12 @@ export default class EventPost extends Post {
|
|||||||
: username
|
: username
|
||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return super.content().concat([
|
||||||
icon(this.icon(), {className: 'EventPost-icon'}),
|
icon(this.icon(), {className: 'EventPost-icon'}),
|
||||||
<div class="EventPost-info">
|
<div class="EventPost-info">
|
||||||
{app.trans(this.descriptionKey(), data)}
|
{this.description(data)}
|
||||||
</div>
|
</div>
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +50,16 @@ export default class EventPost extends Post {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the description text for the event.
|
||||||
|
*
|
||||||
|
* @param {Object} data
|
||||||
|
* @return {String|Object} The description to render in the DOM
|
||||||
|
*/
|
||||||
|
description(data) {
|
||||||
|
return app.translator.transChoice(this.descriptionKey(), data.count, data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the translation key for the description of the event.
|
* Get the translation key for the description of the event.
|
||||||
*
|
*
|
@@ -1,6 +1,7 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
import Modal from 'flarum/components/Modal';
|
||||||
import Alert from 'flarum/components/Alert';
|
import Alert from 'flarum/components/Alert';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
|
import extractText from 'flarum/utils/extractText';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `ForgotPasswordModal` component displays a modal which allows the user to
|
* The `ForgotPasswordModal` component displays a modal which allows the user to
|
||||||
@@ -11,8 +12,8 @@ import Button from 'flarum/components/Button';
|
|||||||
* - `email`
|
* - `email`
|
||||||
*/
|
*/
|
||||||
export default class ForgotPasswordModal extends Modal {
|
export default class ForgotPasswordModal extends Modal {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the email input.
|
* The value of the email input.
|
||||||
@@ -34,21 +35,19 @@ export default class ForgotPasswordModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return app.trans('core.forgot_password');
|
return app.translator.trans('core.forum.forgot_password.title');
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
if (this.success) {
|
if (this.success) {
|
||||||
const emailProviderName = this.email().split('@')[1];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
<div className="Form Form--centered">
|
<div className="Form Form--centered">
|
||||||
<p className="helpText">{app.trans('core.password_reset_email_sent')}</p>
|
<p className="helpText">{app.translator.trans('core.forum.forgot_password.email_sent_message')}</p>
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
|
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
|
||||||
{app.trans('core.go_to', {location: emailProviderName})}
|
{app.translator.trans('core.forum.forgot_password.dismiss_button')}
|
||||||
</a>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,9 +57,9 @@ export default class ForgotPasswordModal extends Modal {
|
|||||||
return (
|
return (
|
||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
<div className="Form Form--centered">
|
<div className="Form Form--centered">
|
||||||
<p className="helpText">{app.trans('core.forgot_password_help')}</p>
|
<p className="helpText">{app.translator.trans('core.forum.forgot_password.text')}</p>
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
|
<input className="FormControl" name="email" type="email" placeholder={extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'))}
|
||||||
value={this.email()}
|
value={this.email()}
|
||||||
onchange={m.withAttr('value', this.email)}
|
onchange={m.withAttr('value', this.email)}
|
||||||
disabled={this.loading} />
|
disabled={this.loading} />
|
||||||
@@ -70,7 +69,7 @@ export default class ForgotPasswordModal extends Modal {
|
|||||||
className: 'Button Button--primary Button--block',
|
className: 'Button Button--primary Button--block',
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
loading: this.loading,
|
loading: this.loading,
|
||||||
children: app.trans('core.recover_password')
|
children: app.translator.trans('core.forum.forgot_password.submit_button')
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,23 +86,21 @@ export default class ForgotPasswordModal extends Modal {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||||
data: {email: this.email()},
|
data: {email: this.email()},
|
||||||
handlers: {
|
errorHandler: this.onerror.bind(this)
|
||||||
404: () => {
|
})
|
||||||
this.alert = new Alert({type: 'warning', message: 'That email wasn\'t found in our database.'});
|
.then(() => {
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).then(
|
|
||||||
() => {
|
|
||||||
this.loading = false;
|
|
||||||
this.success = true;
|
this.success = true;
|
||||||
this.alert = null;
|
this.alert = null;
|
||||||
m.redraw();
|
})
|
||||||
},
|
.catch(() => {})
|
||||||
response => {
|
.then(this.loaded.bind(this));
|
||||||
this.loading = false;
|
}
|
||||||
this.handleErrors(response);
|
|
||||||
}
|
onerror(error) {
|
||||||
);
|
if (error.status === 404) {
|
||||||
|
error.alert.props.children = app.translator.trans('core.forum.forgot_password.not_found_message');
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onerror(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,28 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
import listItems from 'flarum/helpers/listItems';
|
|
||||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `HeaderPrimary` component displays primary header controls. On the
|
|
||||||
* default skin, these are shown just to the right of the forum title.
|
|
||||||
*/
|
|
||||||
export default class HeaderPrimary extends Component {
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<ul className="Header-controls">
|
|
||||||
{listItems(this.items().toArray())}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an item list for the controls.
|
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
*/
|
|
||||||
items() {
|
|
||||||
return new ItemList();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -15,6 +15,13 @@ export default class HeaderPrimary extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
// Since this component is 'above' the content of the page (that is, it is a
|
||||||
|
// part of the global UI that persists between routes), we will flag the DOM
|
||||||
|
// to be retained across route changes.
|
||||||
|
context.retain = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an item list for the controls.
|
* Build an item list for the controls.
|
||||||
*
|
*
|
@@ -22,6 +22,13 @@ export default class HeaderSecondary extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
// Since this component is 'above' the content of the page (that is, it is a
|
||||||
|
// part of the global UI that persists between routes), we will flag the DOM
|
||||||
|
// to be retained across route changes.
|
||||||
|
context.retain = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an item list for the controls.
|
* Build an item list for the controls.
|
||||||
*
|
*
|
||||||
@@ -30,16 +37,16 @@ export default class HeaderSecondary extends Component {
|
|||||||
items() {
|
items() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add('search', app.search.render());
|
items.add('search', app.search.render(), 30);
|
||||||
|
|
||||||
if (Object.keys(app.locales).length > 1) {
|
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 1) {
|
||||||
const locales = [];
|
const locales = [];
|
||||||
|
|
||||||
for (const locale in app.locales) {
|
for (const locale in app.data.locales) {
|
||||||
locales.push(Button.component({
|
locales.push(Button.component({
|
||||||
active: app.locale === locale,
|
active: app.data.locale === locale,
|
||||||
children: app.locales[locale],
|
children: app.data.locales[locale],
|
||||||
icon: app.locale === locale ? 'check' : true,
|
icon: app.data.locale === locale ? 'check' : true,
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
if (app.session.user) {
|
if (app.session.user) {
|
||||||
app.session.user.savePreferences({locale}).then(() => window.location.reload());
|
app.session.user.savePreferences({locale}).then(() => window.location.reload());
|
||||||
@@ -54,29 +61,29 @@ export default class HeaderSecondary extends Component {
|
|||||||
items.add('locale', SelectDropdown.component({
|
items.add('locale', SelectDropdown.component({
|
||||||
children: locales,
|
children: locales,
|
||||||
buttonClassName: 'Button Button--link'
|
buttonClassName: 'Button Button--link'
|
||||||
}));
|
}), 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.session.user) {
|
if (app.session.user) {
|
||||||
items.add('notifications', NotificationsDropdown.component());
|
items.add('notifications', NotificationsDropdown.component(), 10);
|
||||||
items.add('session', SessionDropdown.component());
|
items.add('session', SessionDropdown.component(), 0);
|
||||||
} else {
|
} else {
|
||||||
if (app.forum.attribute('allowSignUp')) {
|
if (app.forum.attribute('allowSignUp')) {
|
||||||
items.add('signUp',
|
items.add('signUp',
|
||||||
Button.component({
|
Button.component({
|
||||||
children: app.trans('core.sign_up'),
|
children: app.translator.trans('core.forum.header.sign_up_link'),
|
||||||
className: 'Button Button--link',
|
className: 'Button Button--link',
|
||||||
onclick: () => app.modal.show(new SignUpModal())
|
onclick: () => app.modal.show(new SignUpModal())
|
||||||
})
|
}), 10
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add('logIn',
|
items.add('logIn',
|
||||||
Button.component({
|
Button.component({
|
||||||
children: app.trans('core.log_in'),
|
children: app.translator.trans('core.forum.header.log_in_link'),
|
||||||
className: 'Button Button--link',
|
className: 'Button Button--link',
|
||||||
onclick: () => app.modal.show(new LogInModal())
|
onclick: () => app.modal.show(new LogInModal())
|
||||||
})
|
}), 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@@ -1,7 +1,8 @@
|
|||||||
import Component from 'flarum/Component';
|
import { extend } from 'flarum/extend';
|
||||||
|
import Page from 'flarum/components/Page';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
import affixSidebar from 'flarum/utils/affixSidebar';
|
|
||||||
import listItems from 'flarum/helpers/listItems';
|
import listItems from 'flarum/helpers/listItems';
|
||||||
|
import icon from 'flarum/helpers/icon';
|
||||||
import DiscussionList from 'flarum/components/DiscussionList';
|
import DiscussionList from 'flarum/components/DiscussionList';
|
||||||
import WelcomeHero from 'flarum/components/WelcomeHero';
|
import WelcomeHero from 'flarum/components/WelcomeHero';
|
||||||
import DiscussionComposer from 'flarum/components/DiscussionComposer';
|
import DiscussionComposer from 'flarum/components/DiscussionComposer';
|
||||||
@@ -16,22 +17,22 @@ import SelectDropdown from 'flarum/components/SelectDropdown';
|
|||||||
* The `IndexPage` component displays the index page, including the welcome
|
* The `IndexPage` component displays the index page, including the welcome
|
||||||
* hero, the sidebar, and the discussion list.
|
* hero, the sidebar, and the discussion list.
|
||||||
*/
|
*/
|
||||||
export default class IndexPage extends Component {
|
export default class IndexPage extends Page {
|
||||||
constructor(...args) {
|
init() {
|
||||||
super(...args);
|
super.init();
|
||||||
|
|
||||||
// If the user is returning from a discussion page, then take note of which
|
// If the user is returning from a discussion page, then take note of which
|
||||||
// discussion they have just visited. After the view is rendered, we will
|
// discussion they have just visited. After the view is rendered, we will
|
||||||
// scroll down so that this discussion is in view.
|
// scroll down so that this discussion is in view.
|
||||||
if (app.current instanceof DiscussionPage) {
|
if (app.previous instanceof DiscussionPage) {
|
||||||
this.lastDiscussion = app.current.discussion;
|
this.lastDiscussion = app.previous.discussion;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user is coming from the discussion list, then they have either
|
// If the user is coming from the discussion list, then they have either
|
||||||
// just switched one of the parameters (filter, sort, search) or they
|
// just switched one of the parameters (filter, sort, search) or they
|
||||||
// probably want to refresh the results. We will clear the discussion list
|
// probably want to refresh the results. We will clear the discussion list
|
||||||
// cache so that results are reloaded.
|
// cache so that results are reloaded.
|
||||||
if (app.current instanceof IndexPage) {
|
if (app.previous instanceof IndexPage) {
|
||||||
app.cache.discussionList = null;
|
app.cache.discussionList = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +55,9 @@ export default class IndexPage extends Component {
|
|||||||
app.cache.discussionList = new DiscussionList({params});
|
app.cache.discussionList = new DiscussionList({params});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.history.push('index');
|
app.history.push('index', icon('bars'));
|
||||||
app.current = this;
|
|
||||||
app.drawer.hide();
|
this.bodyClass = 'App--index';
|
||||||
app.modal.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
@@ -71,7 +71,7 @@ export default class IndexPage extends Component {
|
|||||||
<div className="IndexPage">
|
<div className="IndexPage">
|
||||||
{this.hero()}
|
{this.hero()}
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<nav className="IndexPage-nav sideNav" config={affixSidebar}>
|
<nav className="IndexPage-nav sideNav">
|
||||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="IndexPage-results sideNavOffset">
|
<div className="IndexPage-results sideNavOffset">
|
||||||
@@ -87,27 +87,30 @@ export default class IndexPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config(isInitialized, context) {
|
config(isInitialized, context) {
|
||||||
|
super.config(...arguments);
|
||||||
|
|
||||||
if (isInitialized) return;
|
if (isInitialized) return;
|
||||||
|
|
||||||
$('#app').addClass('App--index');
|
extend(context, 'onunload', () => $('#app').css('min-height', ''));
|
||||||
context.onunload = () => {
|
|
||||||
$('#app').removeClass('App--index')
|
|
||||||
.css('min-height', '');
|
|
||||||
};
|
|
||||||
|
|
||||||
app.setTitle('');
|
app.setTitle('');
|
||||||
app.setTitleCount(0);
|
app.setTitleCount(0);
|
||||||
|
|
||||||
// Work out the difference between the height of this hero and that of the
|
// Work out the difference between the height of this hero and that of the
|
||||||
// previous hero. Maintain the same scroll position relative to the bottom
|
// previous hero. Maintain the same scroll position relative to the bottom
|
||||||
// of the hero so that the 'fixed' sidebar doesn't jump around.
|
// of the hero so that the sidebar doesn't jump around.
|
||||||
const heroHeight = this.$('.Hero').outerHeight();
|
const oldHeroHeight = app.cache.heroHeight;
|
||||||
|
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight();
|
||||||
const scrollTop = app.cache.scrollTop;
|
const scrollTop = app.cache.scrollTop;
|
||||||
|
|
||||||
$('#app').css('min-height', $(window).height() + heroHeight);
|
$('#app').css('min-height', $(window).height() + heroHeight);
|
||||||
$(window).scrollTop(scrollTop - (app.cache.heroHeight - heroHeight));
|
|
||||||
|
|
||||||
app.cache.heroHeight = heroHeight;
|
// Scroll to the remembered position. We do this after a short delay so that
|
||||||
|
// it happens after the browser has done its own "back button" scrolling,
|
||||||
|
// which isn't right. https://github.com/flarum/core/issues/835
|
||||||
|
const scroll = () => $(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
|
||||||
|
scroll();
|
||||||
|
setTimeout(scroll, 1);
|
||||||
|
|
||||||
// If we've just returned from a discussion page, then the constructor will
|
// If we've just returned from a discussion page, then the constructor will
|
||||||
// have set the `lastDiscussion` property. If this is the case, we want to
|
// have set the `lastDiscussion` property. If this is the case, we want to
|
||||||
@@ -146,11 +149,11 @@ export default class IndexPage extends Component {
|
|||||||
*/
|
*/
|
||||||
sidebarItems() {
|
sidebarItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
const canStartDiscussion = app.forum.canStartDiscussion() || !app.session.user;
|
const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
|
||||||
|
|
||||||
items.add('newDiscussion',
|
items.add('newDiscussion',
|
||||||
Button.component({
|
Button.component({
|
||||||
children: canStartDiscussion ? app.trans('core.start_a_discussion') : 'Can\'t Start Discussion',
|
children: app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'),
|
||||||
icon: 'edit',
|
icon: 'edit',
|
||||||
className: 'Button Button--primary IndexPage-newDiscussion',
|
className: 'Button Button--primary IndexPage-newDiscussion',
|
||||||
itemClassName: 'App-primaryControl',
|
itemClassName: 'App-primaryControl',
|
||||||
@@ -183,7 +186,7 @@ export default class IndexPage extends Component {
|
|||||||
items.add('allDiscussions',
|
items.add('allDiscussions',
|
||||||
LinkButton.component({
|
LinkButton.component({
|
||||||
href: app.route('index', params),
|
href: app.route('index', params),
|
||||||
children: app.trans('core.all_discussions'),
|
children: app.translator.trans('core.forum.index.all_discussions_link'),
|
||||||
icon: 'comments-o'
|
icon: 'comments-o'
|
||||||
}),
|
}),
|
||||||
100
|
100
|
||||||
@@ -201,16 +204,17 @@ export default class IndexPage extends Component {
|
|||||||
*/
|
*/
|
||||||
viewItems() {
|
viewItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
const sortMap = app.cache.discussionList.sortMap();
|
||||||
|
|
||||||
const sortOptions = {};
|
const sortOptions = {};
|
||||||
for (const i in app.cache.discussionList.sortMap()) {
|
for (const i in sortMap) {
|
||||||
sortOptions[i] = app.trans('core.sort_' + i);
|
sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button');
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add('sort',
|
items.add('sort',
|
||||||
Select.component({
|
Select.component({
|
||||||
options: sortOptions,
|
options: sortOptions,
|
||||||
value: this.params().sort,
|
value: this.params().sort || Object.keys(sortMap)[0],
|
||||||
onchange: this.changeSort.bind(this)
|
onchange: this.changeSort.bind(this)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -229,17 +233,23 @@ export default class IndexPage extends Component {
|
|||||||
|
|
||||||
items.add('refresh',
|
items.add('refresh',
|
||||||
Button.component({
|
Button.component({
|
||||||
title: app.trans('core.refresh'),
|
title: app.translator.trans('core.forum.index.refresh_tooltip'),
|
||||||
icon: 'refresh',
|
icon: 'refresh',
|
||||||
className: 'Button Button--icon',
|
className: 'Button Button--icon',
|
||||||
onclick: () => app.cache.discussionList.refresh()
|
onclick: () => {
|
||||||
|
app.cache.discussionList.refresh();
|
||||||
|
if (app.session.user) {
|
||||||
|
app.store.find('users', app.session.user.id());
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (app.session.user) {
|
if (app.session.user) {
|
||||||
items.add('markAllAsRead',
|
items.add('markAllAsRead',
|
||||||
Button.component({
|
Button.component({
|
||||||
title: app.trans('core.mark_all_as_read'),
|
title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'),
|
||||||
icon: 'check',
|
icon: 'check',
|
||||||
className: 'Button Button--icon',
|
className: 'Button Button--icon',
|
||||||
onclick: this.markAllAsRead.bind(this)
|
onclick: this.markAllAsRead.bind(this)
|
||||||
@@ -360,6 +370,10 @@ export default class IndexPage extends Component {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
markAllAsRead() {
|
markAllAsRead() {
|
||||||
app.session.user.save({readTime: new Date()});
|
const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
app.session.user.save({readTime: new Date()});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
30
js/forum/src/components/LogInButton.tsx
Normal file
30
js/forum/src/components/LogInButton.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `LogInButton` component displays a social login button which will open
|
||||||
|
* a popup window containing the specified path.
|
||||||
|
*
|
||||||
|
* ### Props
|
||||||
|
*
|
||||||
|
* - `path`
|
||||||
|
*/
|
||||||
|
export default class LogInButton extends Button {
|
||||||
|
static initProps(props) {
|
||||||
|
props.className = (props.className || '') + ' LogInButton';
|
||||||
|
|
||||||
|
props.onclick = function() {
|
||||||
|
const width = 600;
|
||||||
|
const height = 400;
|
||||||
|
const $window = $(window);
|
||||||
|
|
||||||
|
window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup',
|
||||||
|
`width=${width},` +
|
||||||
|
`height=${height},` +
|
||||||
|
`top=${$window.height() / 2 - height / 2},` +
|
||||||
|
`left=${$window.width() / 2 - width / 2},` +
|
||||||
|
'status=no,scrollbars=no,resizable=no');
|
||||||
|
};
|
||||||
|
|
||||||
|
super.initProps(props);
|
||||||
|
}
|
||||||
|
}
|
25
js/forum/src/components/LogInButtons.tsx
Normal file
25
js/forum/src/components/LogInButtons.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Component from 'flarum/Component';
|
||||||
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `LogInButtons` component displays a collection of social login buttons.
|
||||||
|
*/
|
||||||
|
export default class LogInButtons extends Component {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="LogInButtons">
|
||||||
|
{this.items().toArray()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a list of LogInButton components.
|
||||||
|
*
|
||||||
|
* @return {ItemList}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
items() {
|
||||||
|
return new ItemList();
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user