mirror of
https://github.com/flarum/core.git
synced 2025-08-16 13:24:11 +02:00
Compare commits
1838 Commits
v0.1.0-bet
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
fb0a875c6d | ||
|
74b6b9935b | ||
|
3b5691ee28 | ||
|
18593e0d7d | ||
|
40e1b61fe6 | ||
|
95dcb45d65 | ||
|
bd989df769 | ||
|
538136153c | ||
|
c330662241 | ||
|
588cbaee2d | ||
|
a9557c399a | ||
|
14e7bc73ee | ||
|
edc579fa6f | ||
|
119831e51c | ||
|
2aee020c14 | ||
|
f20696210e | ||
|
ea84fc4836 | ||
|
5ff04d0c68 | ||
|
e2ec52c28c | ||
|
6196081bdf | ||
|
6d8e6583c8 | ||
|
c2b0060852 | ||
|
24964b94bf | ||
|
2b624c935d | ||
|
2e647cdda8 | ||
|
ba175144f4 | ||
|
e9af36ab47 | ||
|
8b3913339a | ||
|
3cced4156f | ||
|
e88a9394ed | ||
|
ba73c59601 | ||
|
0191babb05 | ||
|
ed51f9ff0a | ||
|
0a2bdbaa09 | ||
|
26229db1fd | ||
|
1aef3162be | ||
|
dcf88df0c7 | ||
|
3eb28dfb16 | ||
|
1d43371fa9 | ||
|
4df455cf04 | ||
|
2c43ccf66c | ||
|
1d010efbca | ||
|
2135d5908e | ||
|
9640dd6419 | ||
|
98464a8a33 | ||
|
2b6535525b | ||
|
b60617b849 | ||
|
0836d99e83 | ||
|
279c7df9b9 | ||
|
04bcf1eef6 | ||
|
70e98f810c | ||
|
3851d805f7 | ||
|
085468382a | ||
|
7dbdd8c024 | ||
|
ad25307e68 | ||
|
6c454b8279 | ||
|
9f15e9ba86 | ||
|
41009dba74 | ||
|
a045f8bef9 | ||
|
689d767f82 | ||
|
77fff9fde8 | ||
|
c6c1ae32e6 | ||
|
bdac88b573 | ||
|
31ee65be93 | ||
|
29df6b60be | ||
|
1e6f175379 | ||
|
065ff3456f | ||
|
37e0a5579b | ||
|
cd9aa0096e | ||
|
d06493c61e | ||
|
9f71e2c3cb | ||
|
81a8736ba9 | ||
|
57ce25301d | ||
|
cfbaa84fbc | ||
|
3417f5a77e | ||
|
1035636d0f | ||
|
d00fc2c49d | ||
|
f3b889a665 | ||
|
c5122bf5d5 | ||
|
5ed55195e1 | ||
|
8604ea3020 | ||
|
2648e960a7 | ||
|
f0dff95d62 | ||
|
894db01ad8 | ||
|
bd04023359 | ||
|
f357434a72 | ||
|
c2586586c4 | ||
|
06cd062a1b | ||
|
1502fc98d8 | ||
|
ed97989ca2 | ||
|
7f1048352d | ||
|
d2700961ba | ||
|
b2dbb0439c | ||
|
085c924a07 | ||
|
f31f02d4cc | ||
|
797f6eea50 | ||
|
9fb3a31b51 | ||
|
f8061bbca1 | ||
|
de67927ef2 | ||
|
8c841c3266 | ||
|
2f656146a7 | ||
|
d66d2aa26e | ||
|
f4c0d4ba87 | ||
|
646bd40bca | ||
|
307b912019 | ||
|
cbc896eba7 | ||
|
cc4e4a068b | ||
|
a720f6f651 | ||
|
54d7c0d3b6 | ||
|
b5876d9f31 | ||
|
25ef4c10bd | ||
|
985b87da6c | ||
|
a6aa28566c | ||
|
e3340ba3e1 | ||
|
590b311570 | ||
|
935a968257 | ||
|
fe558eb0ba | ||
|
fda9cba4ce | ||
|
89f6cfd949 | ||
|
803582c437 | ||
|
8e86d38804 | ||
|
fd66722945 | ||
|
ce42b5e035 | ||
|
bfd3a667dd | ||
|
b669490d33 | ||
|
ba956f51ac | ||
|
c126b95451 | ||
|
7f7484e790 | ||
|
5d64056e89 | ||
|
e927254e99 | ||
|
8061bfd74a | ||
|
4c309d2ad7 | ||
|
54876cfbd6 | ||
|
9e2b796a7c | ||
|
7f5bd1e96b | ||
|
5e1680c458 | ||
|
6e26b988bd | ||
|
2e8d4e4b6b | ||
|
14bede2847 | ||
|
54660ebd63 | ||
|
1a62b7e07a | ||
|
4b04c0e0ce | ||
|
4d45ce389b | ||
|
d2674fb309 | ||
|
5eb69e1f59 | ||
|
f42142979d | ||
|
5f79d3b499 | ||
|
8e4d97260f | ||
|
ee3640e160 | ||
|
bd584802e5 | ||
|
f4dd045326 | ||
|
24522943f6 | ||
|
56fde28e43 | ||
|
1c1d661bdd | ||
|
d3be186fb6 | ||
|
8f8cc558be | ||
|
5ea9e1cf5e | ||
|
99a6066f96 | ||
|
8b7db726dc | ||
|
7a44086bf3 | ||
|
12fdfc9b54 | ||
|
ecc3b5e227 | ||
|
bf2c5a5564 | ||
|
d3a5c91845 | ||
|
e17bb0b433 | ||
|
c4ba41f850 | ||
|
0c4de6f163 | ||
|
cd313952c7 | ||
|
ef57b443c1 | ||
|
5154d7e5a6 | ||
|
2bd40b50c7 | ||
|
c50d58d0f4 | ||
|
8c65316961 | ||
|
0a818cfdf3 | ||
|
57204c6ed0 | ||
|
a21052c903 | ||
|
441ebacfd7 | ||
|
46acfb6c23 | ||
|
9910e884fc | ||
|
d292aaabf8 | ||
|
d822a6f84c | ||
|
26c3bcdb74 | ||
|
33deea4791 | ||
|
20227a2201 | ||
|
0493682dba | ||
|
49dda87e86 | ||
|
d959d08561 | ||
|
e8ab49abc1 | ||
|
296677b5fc | ||
|
f3931b537c | ||
|
d0ba4e5268 | ||
|
654ab4cc29 | ||
|
e0becd0c7b | ||
|
ed43ad9c3f | ||
|
4611abe5db | ||
|
df0bd52283 | ||
|
d387a9ff02 | ||
|
5556df54f9 | ||
|
cf746079ed | ||
|
4d10536d35 | ||
|
ba16ebe61f | ||
|
6484dc4982 | ||
|
1a9f1f7a3d | ||
|
4d1411e2a8 | ||
|
968152b740 | ||
|
af185fd3d1 | ||
|
ed9591c16f | ||
|
8ad326941f | ||
|
8e4f02d994 | ||
|
8ae85bc49f | ||
|
7ff9a90204 | ||
|
f4fb1ab272 | ||
|
484c6d2edb | ||
|
8b68ff6232 | ||
|
0a59b7164e | ||
|
0879829dc4 | ||
|
78ba3bd854 | ||
|
44c91099cd | ||
|
4585f03ee3 | ||
|
bc9e8f68f1 | ||
|
f5a21584c2 | ||
|
e0a508a765 | ||
|
89e018a4f0 | ||
|
de6001f4cf | ||
|
790d5beee5 | ||
|
abf224bb0a | ||
|
c7d2e165d7 | ||
|
0ab9facc4b | ||
|
9b68bbe44e | ||
|
862404f052 | ||
|
b9a93f3440 | ||
|
c67fb2d4b6 | ||
|
1b2d4f1e1d | ||
|
54fdc40d87 | ||
|
390148456c | ||
|
167059027e | ||
|
208bad393f | ||
|
8a93f8b6b6 | ||
|
9db04a4e19 | ||
|
ac5e26a254 | ||
|
9794a08f39 | ||
|
ababb8ebef | ||
|
cb3baf9955 | ||
|
dbe8cba14e | ||
|
9fe671c9bb | ||
|
0e5f334a0b | ||
|
e4514d8413 | ||
|
1080d25561 | ||
|
ba594de13a | ||
|
209d13affd | ||
|
671fdec8d0 | ||
|
9eca9192ca | ||
|
3468bdf511 | ||
|
54503d2c29 | ||
|
565131e2a7 | ||
|
f0da3cf304 | ||
|
6acc91577d | ||
|
3e0cd3a21f | ||
|
5c9fa4c62d | ||
|
4b00f7996b | ||
|
b58380e224 | ||
|
b0e996e7ff | ||
|
b41d9fb0e7 | ||
|
ed02eed88f | ||
|
c761802900 | ||
|
16eb1fa63b | ||
|
0ceb8d64df | ||
|
9712eccb03 | ||
|
9684fbc4da | ||
|
67f9375d47 | ||
|
0d16fac001 | ||
|
a8f5ca8d97 | ||
|
bbe62f400f | ||
|
fc5977f86f | ||
|
796b57753d | ||
|
88e43cc694 | ||
|
6370f7ecff | ||
|
d9d7027ed0 | ||
|
1106a2e3a3 | ||
|
b7f666525c | ||
|
40dc6ac604 | ||
|
15e1a154e5 | ||
|
57d5846b64 | ||
|
fb6b51b1cf | ||
|
57f73c9638 | ||
|
7705a2b7d7 | ||
|
f591585d02 | ||
|
45afc33eb0 | ||
|
213fd62be3 | ||
|
66607a5674 | ||
|
546b4f01ac | ||
|
96e282458b | ||
|
24ff8899a0 | ||
|
1b32c7cc51 | ||
|
6c2a4a5ff7 | ||
|
2d31a6f72e | ||
|
9115b9e28f | ||
|
3bff2e0f5c | ||
|
edaca3160e | ||
|
9e63f32105 | ||
|
f3a5a89e12 | ||
|
b074f47298 | ||
|
2ef66ac716 | ||
|
6654894da1 | ||
|
41544c8529 | ||
|
53d1b87daf | ||
|
c11e4720d9 | ||
|
c73d03a427 | ||
|
d0d6c52839 | ||
|
9585d448d2 | ||
|
04d46b9925 | ||
|
c3b2d8e7d8 | ||
|
62a40036d0 | ||
|
2764ad87cc | ||
|
2c1be86857 | ||
|
b26eb8e609 | ||
|
1f0bf33cfb | ||
|
6fadc0b653 | ||
|
0255393108 | ||
|
b474843cc2 | ||
|
7e95b80341 | ||
|
18b90d16e3 | ||
|
64a7e8ac3a | ||
|
e8b8cd0078 | ||
|
3c8262ccde | ||
|
68c6638fb5 | ||
|
105dd093fe | ||
|
920802e5ae | ||
|
882c4aa105 | ||
|
b826f9ce36 | ||
|
c13dfa2228 | ||
|
bf3934d16f | ||
|
a08068b112 | ||
|
60149fbe15 | ||
|
13c593cbaa | ||
|
f7a320bcca | ||
|
b980c6fb7d | ||
|
0f9118fe2b | ||
|
95f0edcd80 | ||
|
222e3c3fe2 | ||
|
903c1e329d | ||
|
295a007cd5 | ||
|
66404e1f61 | ||
|
64e43ec9a4 | ||
|
bf8bc0222f | ||
|
6d14d0c39b | ||
|
17fdc0ebe0 | ||
|
9de786d1e6 | ||
|
b92ae61294 | ||
|
e99f7fcdac | ||
|
009ddcdb63 | ||
|
d021dc2399 | ||
|
1fff5dbbbc | ||
|
4de5accfc1 | ||
|
bb0fc165af | ||
|
fb185f70cd | ||
|
3b630cb03e | ||
|
f283f0c7bd | ||
|
4b915c688c | ||
|
83e99ed5a5 | ||
|
a09894a906 | ||
|
1c7d2c3d27 | ||
|
7db6cfac3f | ||
|
c446c5cc61 | ||
|
bd10ebff24 | ||
|
104d3982fe | ||
|
a1948e7bb8 | ||
|
4775535421 | ||
|
2392e06c0e | ||
|
e3e10a8fc3 | ||
|
b4dbab5df1 | ||
|
f062f69f00 | ||
|
0e3b0fc5a0 | ||
|
21b3737dc2 | ||
|
4ed1d0aaee | ||
|
86b26ce2fb | ||
|
eafc637475 | ||
|
a03f243ca5 | ||
|
5f5e1c512c | ||
|
a4d540f74b | ||
|
c23af9550e | ||
|
8fd3e8908c | ||
|
cc95faa07d | ||
|
f1add1798b | ||
|
81f6ce220e | ||
|
9fa7258325 | ||
|
4841661ee2 | ||
|
7b34636636 | ||
|
8474dfd6e2 | ||
|
737d33826e | ||
|
3006f58274 | ||
|
d3a5e2451a | ||
|
f03c954dcc | ||
|
3b70b9e76e | ||
|
b823a9df47 | ||
|
8621500501 | ||
|
f48101dc04 | ||
|
3c827d2fce | ||
|
1f0e9b6280 | ||
|
bf61753361 | ||
|
70cb2f6f2e | ||
|
1736fe3f58 | ||
|
ae81c4f0f1 | ||
|
8c679c715c | ||
|
4236f3d49e | ||
|
e3afb38427 | ||
|
26a5b107bd | ||
|
aa70441632 | ||
|
0fb9aa3940 | ||
|
b6a60721e2 | ||
|
fe868af224 | ||
|
0984979403 | ||
|
5dfb9b474c | ||
|
fcb97b256f | ||
|
4429cf4eba | ||
|
c54f739484 | ||
|
6c0d73afa0 | ||
|
1cd8ec6873 | ||
|
4ed1c7a1bb | ||
|
c67f673819 | ||
|
7917ce130e | ||
|
2b174b17fc | ||
|
550d35e86f | ||
|
64686ef7a9 | ||
|
5ce702a5d0 | ||
|
fdf5fdbaf3 | ||
|
184ffcd991 | ||
|
ba946237e4 | ||
|
8ec0578ddf | ||
|
c34fcecf03 | ||
|
9e487b4e41 | ||
|
986d811a16 | ||
|
c32af6559e | ||
|
07298e165d | ||
|
c15bbc9c5e | ||
|
6c8c525e57 | ||
|
93dfb6dec9 | ||
|
750d9d05a6 | ||
|
0ce289be4c | ||
|
7fcf556f13 | ||
|
68afdd21ae | ||
|
54aa9ee3cf | ||
|
5635bd26f9 | ||
|
622bf26510 | ||
|
2367a45c18 | ||
|
4770a5c906 | ||
|
c61badd754 | ||
|
14393ec53e | ||
|
96045ca390 | ||
|
eb228dd7b9 | ||
|
092e5b9d23 | ||
|
7e3980744e | ||
|
85c965afbc | ||
|
43fc2c0952 | ||
|
5a9b47cdf7 | ||
|
5374f8a352 | ||
|
5f5af894ab | ||
|
d7c283a48f | ||
|
c5a3715701 | ||
|
710c63ba6a | ||
|
5142c639c1 | ||
|
eb3232dfc9 | ||
|
72d46bc461 | ||
|
9792576464 | ||
|
5c0c2d1c40 | ||
|
ce39bc9070 | ||
|
37ffd04b3f | ||
|
d8d2de438f | ||
|
70058652b5 | ||
|
d9d8162684 | ||
|
2ee10bb49f | ||
|
64abbde8b2 | ||
|
ca93c8c609 | ||
|
8248ba2f7a | ||
|
dd65801d57 | ||
|
07c08ca798 | ||
|
ae75f21b6b | ||
|
d47c406d9c | ||
|
29cef23404 | ||
|
a7ffed6778 | ||
|
9074f7e592 | ||
|
99e5013ac3 | ||
|
1e9d9b8322 | ||
|
568006fe73 | ||
|
4756bf1daf | ||
|
8ecb67d49d | ||
|
e241518506 | ||
|
cbd0643540 | ||
|
7716944616 | ||
|
e135b7830e | ||
|
950ab30c29 | ||
|
582054c61c | ||
|
280d51e678 | ||
|
e9ed935ed1 | ||
|
809b161d71 | ||
|
6bc434c918 | ||
|
4c8908c005 | ||
|
2d4dc02ca1 | ||
|
869ec54bd0 | ||
|
59b1ca9b7c | ||
|
7439069fe2 | ||
|
973fbcf17b | ||
|
034000ea0b | ||
|
fb5740926a | ||
|
32ad926cbc | ||
|
626d16de6f | ||
|
0222692c53 | ||
|
b4b72fe62f | ||
|
5b821b21b1 | ||
|
7a6e208554 | ||
|
3a0e982df1 | ||
|
6057151c29 | ||
|
fcb9a049e9 | ||
|
190bcb5e9a | ||
|
e72ac76997 | ||
|
62e7a7188c | ||
|
ca16a23383 | ||
|
195f77ff10 | ||
|
5f83285442 | ||
|
c16ddf24f2 | ||
|
394fc4232e | ||
|
fd36d18729 | ||
|
2e16b0ce2e | ||
|
af47558ec2 | ||
|
641079b3fe | ||
|
df0f4e8462 | ||
|
da0adf83ae | ||
|
0452838802 | ||
|
40e4c0acdd | ||
|
ef9ed7f4fa | ||
|
24fd2f32c7 | ||
|
076288db21 | ||
|
df7e24cba6 | ||
|
5438aea759 | ||
|
e46ce861dc | ||
|
01d8bd5344 | ||
|
7f5080d9d5 | ||
|
640b93af36 | ||
|
cfc207f255 | ||
|
d4a80eae5d | ||
|
254d5d0c5b | ||
|
1709d4ef2c | ||
|
baeaa73597 | ||
|
677a7dd2d3 | ||
|
c562302161 | ||
|
d42f33971a | ||
|
aa4c4b07bd | ||
|
420bb2efc8 | ||
|
ed57d6e51c | ||
|
b4f6c4be1f | ||
|
ff7f7681c7 | ||
|
7d0813bce4 | ||
|
4f259425b0 | ||
|
22fadb7f9c | ||
|
d0115de771 | ||
|
9d790c18d6 | ||
|
6f3eb3f335 | ||
|
2d667d885d | ||
|
0fb81958cb | ||
|
87bba2186e | ||
|
93b9513df2 | ||
|
fb6b2d05b1 | ||
|
fe73cf3237 | ||
|
c7efbba0da | ||
|
be266a73cd | ||
|
e1a282e0e1 | ||
|
5a04635e7a | ||
|
7d59b4da7e | ||
|
f89c111f13 | ||
|
6a6b9ac6b1 | ||
|
cbe328cdc5 | ||
|
7b802a76ba | ||
|
90792abf10 | ||
|
5139ce647e | ||
|
8779e40ec5 | ||
|
1e372f3881 | ||
|
b2e873ba7b | ||
|
d6414cfb44 | ||
|
85ceda0b0b | ||
|
dc7c31e1c2 | ||
|
177ac74596 | ||
|
c76d9e1298 | ||
|
dee54a008f | ||
|
551ca23267 | ||
|
3d845d5730 | ||
|
9b03f8c71a | ||
|
665f241348 | ||
|
ce90d2bbdd | ||
|
6e5b0f5289 | ||
|
fba31995b1 | ||
|
604c789ee8 | ||
|
034b82f4d4 | ||
|
9be13cb1cd | ||
|
823f0263ae | ||
|
fb9a89f67f | ||
|
7f63923aa0 | ||
|
73f8922553 | ||
|
2c15597ec9 | ||
|
7e43de25a6 | ||
|
d42205a8ff | ||
|
00bc8fc0bc | ||
|
c17af492a9 | ||
|
0e73785498 | ||
|
0f5ddc1c43 | ||
|
eaf98ccfc5 | ||
|
9c7cc0548e | ||
|
cf80cf86e5 | ||
|
a23dc0dfcd | ||
|
54678e8d5c | ||
|
4d2d7465ee | ||
|
b78129b36b | ||
|
afe06ea750 | ||
|
9449fb4f1f | ||
|
aac194616a | ||
|
d9b357c18e | ||
|
48ac132959 | ||
|
b3f8379a15 | ||
|
1800f4290a | ||
|
2234a81ee7 | ||
|
805768a9e0 | ||
|
e3c2ddad2e | ||
|
520e1550d1 | ||
|
79b00cb94f | ||
|
0bcc6e74a8 | ||
|
542e8715ea | ||
|
3f683dd6ee | ||
|
d234badbb2 | ||
|
2cd77e231f | ||
|
07eda60561 | ||
|
2bc7c4134a | ||
|
050496a20e | ||
|
3b87778fbb | ||
|
569e6c9a92 | ||
|
c498e68530 | ||
|
305841ddd4 | ||
|
066baed5b9 | ||
|
211e7681cc | ||
|
22f2df3670 | ||
|
3bf74eaf10 | ||
|
d301d260c1 | ||
|
a1c3da9f8f | ||
|
26b02adc9d | ||
|
3ec32f8430 | ||
|
f137eb358f | ||
|
b91a3573db | ||
|
b3d45fd6f8 | ||
|
e6b8ff856e | ||
|
4a2aa7e892 | ||
|
b3cbc5d1bd | ||
|
3680d88fb7 | ||
|
5d0ebde6b8 | ||
|
0278d52cbe | ||
|
c293fdaec0 | ||
|
4654c3eb50 | ||
|
68d1edb8fd | ||
|
30358e98c0 | ||
|
e226f81515 | ||
|
09938f8633 | ||
|
4c55d278b6 | ||
|
808e7a226a | ||
|
3e3e1cbde5 | ||
|
a9501ceae0 | ||
|
2a721926d3 | ||
|
81cb67e87c | ||
|
fd859e33be | ||
|
7539c25048 | ||
|
0058067b1b | ||
|
3c41011548 | ||
|
49c643609c | ||
|
4df0101f56 | ||
|
b8632d693a | ||
|
684985c25c | ||
|
a2927b725f | ||
|
efa3b62fb8 | ||
|
406be427ad | ||
|
665ac178e9 | ||
|
c4a501f82a | ||
|
beec59232f | ||
|
371f33e99e | ||
|
264664ac79 | ||
|
17f29f83c9 | ||
|
c9c8fa0fde | ||
|
45f28b6f72 | ||
|
3ef7843540 | ||
|
7d41c4e510 | ||
|
8574b57fc5 | ||
|
dcb3821777 | ||
|
043aa0f2d9 | ||
|
f51e29ff4c | ||
|
46f80e8d72 | ||
|
29d6b3306f | ||
|
4e2c32b108 | ||
|
92c8c616e1 | ||
|
f42273e679 | ||
|
44376da57d | ||
|
2b6ee50c58 | ||
|
155582831d | ||
|
baa11acfa8 | ||
|
4aad293284 | ||
|
c60d6e9dee | ||
|
0cf351edb9 | ||
|
447ca18558 | ||
|
a70e6e639c | ||
|
fdbf0c86a1 | ||
|
6ea60248e3 | ||
|
8d2d987680 | ||
|
bb49e24ffe | ||
|
5672819549 | ||
|
b4e093ab8a | ||
|
5645bcbf9c | ||
|
9d30be1617 | ||
|
d5ef9486d0 | ||
|
0c5c322cb4 | ||
|
3f45d18383 | ||
|
aba22b9119 | ||
|
26dfc8ae21 | ||
|
db7cd71f19 | ||
|
2967b5d106 | ||
|
e8d915850d | ||
|
a061eda019 | ||
|
cc22d1d6f8 | ||
|
9b1c338b68 | ||
|
571938a677 | ||
|
1a2df2d581 | ||
|
0f554585ac | ||
|
0b478379fc | ||
|
ba96f311a9 | ||
|
34588a74e2 | ||
|
58ffa27bfb | ||
|
f0cea11e79 | ||
|
160493e725 | ||
|
5561e28286 | ||
|
c9cfcee12a | ||
|
479fafbf5c | ||
|
aff1b9a5e4 | ||
|
a53d95a3d9 | ||
|
ff10ed0ea9 | ||
|
7721288ac6 | ||
|
c3a6f7daef | ||
|
a4704b1e2e | ||
|
0ab1f2cfe7 | ||
|
322a84f516 | ||
|
80ec3b5e17 | ||
|
636e965873 | ||
|
419adb748b | ||
|
25154dabff | ||
|
2eae968a70 | ||
|
7651907f56 | ||
|
557a65aadd | ||
|
ad4bd3d001 | ||
|
4b1a299b3c | ||
|
fa14be591c | ||
|
072f4f89cb | ||
|
ed3e833181 | ||
|
7f92838225 | ||
|
2159107214 | ||
|
d357364712 | ||
|
26449a64fe | ||
|
ae2e07e94c | ||
|
801d619a36 | ||
|
0befe041c7 | ||
|
1c87c33d4d | ||
|
3480a65989 | ||
|
2979e8bc28 | ||
|
8c470954eb | ||
|
6913e8f0f8 | ||
|
30a04e7bf9 | ||
|
0af97c427c | ||
|
1c1cefa017 | ||
|
c6747b6910 | ||
|
1ce70eeb6e | ||
|
714775cfed | ||
|
bdc1a100cd | ||
|
f3e29ab801 | ||
|
26e53fc51b | ||
|
848293a7d5 | ||
|
5af65dede1 | ||
|
e774baf32f | ||
|
2ac04aac8e | ||
|
c6aeeeb3c1 | ||
|
f247d8c2a6 | ||
|
0380536cb4 | ||
|
6dc96b38af | ||
|
9342723f64 | ||
|
8d049126d0 | ||
|
63be95fb8a | ||
|
1d47047d45 | ||
|
4e30ad5891 | ||
|
f4ad227576 | ||
|
5b6d043f80 | ||
|
c41e58531a | ||
|
b760d113d2 | ||
|
936f67e953 | ||
|
3f7e7520b0 | ||
|
7ccb263926 | ||
|
fe56f57e8f | ||
|
fa9d89d690 | ||
|
292fe06001 | ||
|
f2ce4e11e2 | ||
|
47eb853bf9 | ||
|
d807171c44 | ||
|
57a91c966d | ||
|
0525f467c7 | ||
|
b791790d2f | ||
|
ea353a2f2c | ||
|
ac7d28ca58 | ||
|
8a6344cfcf | ||
|
a0621e85bf | ||
|
56231d61be | ||
|
c8a1a5fcfa | ||
|
8d5132fd5a | ||
|
6efe2ee91a | ||
|
bfd98e3371 | ||
|
e9da1ba2f5 | ||
|
b2fe76c819 | ||
|
2b8c66354d | ||
|
4cf481355f | ||
|
2ec183778c | ||
|
1b94ef90ea | ||
|
183a22b5c5 | ||
|
654fca9c2c | ||
|
7be01119f5 | ||
|
28d4cff156 | ||
|
2aba61668c | ||
|
fccadcc6ab | ||
|
f65e4dcba3 | ||
|
c6ce172caa | ||
|
d2f187716e | ||
|
93aa3d77b5 | ||
|
a0c95e6705 | ||
|
77c25ab725 | ||
|
3dcfe32b27 | ||
|
617a76dda8 | ||
|
1a239ee93a | ||
|
f8d065bd78 | ||
|
0a654d1f31 | ||
|
b806dc3db2 | ||
|
92de751154 | ||
|
5b46ec801d | ||
|
1ef9217f4d | ||
|
79fee3686f | ||
|
8edc8223e5 | ||
|
6280fb2498 | ||
|
ba769e0c7e | ||
|
ea2fc1ff8a | ||
|
b7c1cc5cef | ||
|
e8a4e5e0ef | ||
|
295193eb3c | ||
|
a065c8e6f5 | ||
|
9392e1bec3 | ||
|
479e44dd04 | ||
|
c01268d9ae | ||
|
f4fc245df4 | ||
|
ac0b4cb2d2 | ||
|
55b945f129 | ||
|
4d9e2335c7 | ||
|
642332ffe2 | ||
|
7b2663e0bc | ||
|
e2d61d1aeb | ||
|
7796580210 | ||
|
0aa74c987c | ||
|
55a09a2f57 | ||
|
a28dbccf1a | ||
|
3c80612d80 | ||
|
dedcbae359 | ||
|
9cc67fe312 | ||
|
5f7060fb4a | ||
|
1a102766a9 | ||
|
abda11c6c5 | ||
|
b43fdec2e9 | ||
|
3321b4e829 | ||
|
a8826dcd88 | ||
|
15b573bd93 | ||
|
41df32f66e | ||
|
094345de85 | ||
|
54597ee5eb | ||
|
3be98b9f8e | ||
|
257ee936f4 | ||
|
1a928ca0ab | ||
|
0b1043c9d2 | ||
|
72c232d5a3 | ||
|
914b94b62d | ||
|
1b7cb3bec2 | ||
|
55b763a570 | ||
|
d7306dedb7 | ||
|
3eede757bb | ||
|
a8f8ca7f87 | ||
|
44e9007790 | ||
|
eeed7c20e1 | ||
|
40ebc13292 | ||
|
2754a8c867 | ||
|
123c8bb73d | ||
|
f3b4d35587 | ||
|
42ecee42a1 | ||
|
5a43f915cb | ||
|
6a10b4484f | ||
|
06aa37d2fd | ||
|
cb92deee98 | ||
|
bedf710768 | ||
|
3b1f8771c4 | ||
|
2dbcfe02d8 | ||
|
a7f3ca4b22 | ||
|
3f9dc81874 | ||
|
1c01145a14 | ||
|
2037371886 | ||
|
87bf84ef6e | ||
|
096e552c74 | ||
|
8ccfb1aac6 | ||
|
ea4d889b76 | ||
|
19d15d4302 | ||
|
43d8a9d0e8 | ||
|
02377663ce | ||
|
4f688fc9a2 | ||
|
fddd134fa0 | ||
|
1215a1ef9b | ||
|
4aad7c1040 | ||
|
e46b3d54d1 | ||
|
051bb5acb8 | ||
|
69b517ea79 | ||
|
b4c7f8ca89 | ||
|
3ece3ca976 | ||
|
9c77475985 | ||
|
b72407440d | ||
|
f824dcfb53 | ||
|
78f3681fc1 | ||
|
5b0d0d9f0f | ||
|
95dc7e71f4 | ||
|
a39ed6edec | ||
|
66f35d2530 | ||
|
e6e4531771 | ||
|
e71deed8d5 | ||
|
8a16c1ecc8 | ||
|
b38ade986d | ||
|
4a13cd8088 | ||
|
920a4071b6 | ||
|
c22219ec20 | ||
|
11bf3e34b7 | ||
|
4fb38d6458 | ||
|
66abd7ecfd | ||
|
3481798875 | ||
|
9abc63aaac | ||
|
6cd6a7d260 | ||
|
6c9ff72efb | ||
|
33e3d757c3 | ||
|
551e76f296 | ||
|
564ea8ff73 | ||
|
fda8c597f4 | ||
|
5d1564e0fc | ||
|
1f1b63363e | ||
|
bdf455c0c6 | ||
|
927e4ca3ed | ||
|
0be13d50bd | ||
|
6268c3010f | ||
|
acf43606a8 | ||
|
5ae2e9d232 | ||
|
d897839097 | ||
|
c6985ae31c | ||
|
9b24fbd5e5 | ||
|
5127514d35 | ||
|
eb72307a54 | ||
|
f917d1438c | ||
|
ef89b1f6b1 | ||
|
377d439c47 | ||
|
37cf95f94d | ||
|
c31c1ea062 | ||
|
084f74946d | ||
|
1e8399c014 | ||
|
ad153c8484 | ||
|
cbe4464178 | ||
|
7df9594a04 | ||
|
c037658675 | ||
|
287085dc25 | ||
|
a2e0daed70 | ||
|
57f828b3f7 | ||
|
7ec92813e3 | ||
|
ce8a5b3e0f | ||
|
5faf0fcde5 | ||
|
65c0b436c0 | ||
|
8d76168bd4 | ||
|
d16f4dbefa | ||
|
e3e4786391 | ||
|
c1c7d4c73a | ||
|
8da8c9ac7d | ||
|
fb68aa88db | ||
|
afc597c189 | ||
|
4f3e67714e | ||
|
54be3ad3c8 | ||
|
0b00d56416 | ||
|
89d4a1e849 | ||
|
43ee7b59a4 | ||
|
d052f6b639 | ||
|
4b47adabcf | ||
|
93140b8fa4 | ||
|
ade2166310 | ||
|
a9969119d2 | ||
|
94a8eaec64 | ||
|
8ea13dc826 | ||
|
99d42372c3 | ||
|
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 | ||
|
e54944d6c3 | ||
|
d39bca192e | ||
|
efff485d6c |
@@ -15,5 +15,5 @@ indent_size = 2
|
|||||||
[*.{diff,md}]
|
[*.{diff,md}]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.php]
|
[*.{php,xml}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
**/bower_components/**/*
|
|
||||||
**/node_modules/**/*
|
|
||||||
vendor/**/*
|
|
||||||
**/Gulpfile.js
|
|
||||||
**/dist/**/*
|
|
175
.eslintrc
175
.eslintrc
@@ -1,175 +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
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react/jsx-uses-vars": 1,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
10
.gitattributes
vendored
10
.gitattributes
vendored
@@ -1,5 +1,13 @@
|
|||||||
.gitattributes export-ignore
|
.gitattributes export-ignore
|
||||||
.gitignore export-ignore
|
.gitignore export-ignore
|
||||||
stubs/extension/.gitignore -export-ignore
|
|
||||||
.gitmodules export-ignore
|
.gitmodules export-ignore
|
||||||
|
.github export-ignore
|
||||||
|
.travis 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/* -diff
|
||||||
|
39
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
39
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: "🐛 Bug Report"
|
||||||
|
about: "If something isn't working as expected"
|
||||||
|
|
||||||
|
---
|
||||||
|
## Bug Report
|
||||||
|
|
||||||
|
**Current Behavior**
|
||||||
|
A clear and concise description of the behavior.
|
||||||
|
|
||||||
|
**Steps to Reproduce**
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected Behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
- Flarum version: x.y.z
|
||||||
|
- Website URL: http://example.com
|
||||||
|
- Webserver: [e.g. apache, nginx]
|
||||||
|
- Hosting environment: [e.g. shared, vps]
|
||||||
|
- PHP version: x.y.z
|
||||||
|
- Browser: [e.g. chrome 67, safari 11]
|
||||||
|
|
||||||
|
```
|
||||||
|
Output of "php flarum info", run this in terminal in your Flarum directory.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Possible Solution**
|
||||||
|
<!--- Only if you have suggestions or a fix for the bug -->
|
||||||
|
|
||||||
|
**Additional Context**
|
||||||
|
Add any other context about the problem here.
|
26
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: "🚀 Feature Request"
|
||||||
|
about: "I have a suggestion (and may want to implement it!)"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
IMPORTANT: Feature requests on this GitHub issue tracker are only accepted in case they have been approved by a core developer or contain extensive argumentation and directions for implementation. For all other feature requests, ideas and feedback please post in the Flarum Community: https://discuss.flarum.org/t/feedback.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Feature Request
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. eg. I have an issue when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A detailed description of your proposed solution. Include:
|
||||||
|
- How the feature would work/behave
|
||||||
|
- Any potential drawbacks
|
||||||
|
- Maybe a screenshot, design, or example code
|
||||||
|
|
||||||
|
**Justify why this feature belongs in Flarum's core, rather than in a third-party extension**
|
||||||
|
Consider who this change will be useful to – most Flarum forums, or just a few?
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
11
.github/ISSUE_TEMPLATE/support-question.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/support-question.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: "🙋 Support Question"
|
||||||
|
about: "If you have a question, please check out our forum or Discord!"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We primarily use GitHub as an issue tracker; for usage and support questions, please check out these resources below. Thanks!
|
||||||
|
|
||||||
|
* Flarum Community: https://discuss.flarum.org/
|
||||||
|
* Discord Chat: https://flarum.org/discord/
|
||||||
|
* Twitter: https://twitter.com/Flarum
|
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!--
|
||||||
|
IMPORTANT: We applaud pull requests, they excite us every single time. As we have an obligation to maintain a healthy code standard and quality, we take sufficient time for reviews. Please do create a separate pull request per change/issue/feature; we will ask you to split bundled pull requests.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Fixes #0000**
|
||||||
|
|
||||||
|
**Changes proposed in this pull request:**
|
||||||
|
<!-- fill this out, mention the pages and/or components which have been impacted -->
|
||||||
|
|
||||||
|
**Reviewers should focus on:**
|
||||||
|
<!-- fill this out, ask for feedback on specific changes you are unsure about -->
|
||||||
|
|
||||||
|
**Screenshot**
|
||||||
|
<!-- include an image of the most relevant user-facing change, if any -->
|
||||||
|
|
||||||
|
**Confirmed**
|
||||||
|
|
||||||
|
- [ ] Frontend changes: tested on a local Flarum installation.
|
||||||
|
- [ ] Backend changes: tests are green (run `composer test`).
|
||||||
|
|
||||||
|
**Required changes:**
|
||||||
|
|
||||||
|
- [ ] Related documentation PR: (Remove if irrelevant)
|
||||||
|
- [ ] Related core extension PRs: (Remove if irrelevant)
|
13
.github/SECURITY.md
vendored
Normal file
13
.github/SECURITY.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
During the beta phase, we will only patch security vulnerabilities in the latest beta release.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Flarum, please send an email to security@flarum.org so we can address it promptly.
|
||||||
|
|
||||||
|
We will get back to you as time allows.
|
||||||
|
Discussions may commence internally, so you may not hear back immediately.
|
||||||
|
When reporting a vulnerability, please provide your GitHub username (if available), so that we can invite you to collaborate on a [security advisory on GitHub](https://help.github.com/en/articles/about-maintainer-security-advisories).
|
16
.github/workflows/build.yml
vendored
Normal file
16
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: JavaScript
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- uses: flarum/action-build@master
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
65
.github/workflows/test.yml
vendored
Normal file
65
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php: [7.1, 7.2, 7.3]
|
||||||
|
service: ['mysql:5.7', mariadb]
|
||||||
|
prefix: ['', flarum_]
|
||||||
|
|
||||||
|
include:
|
||||||
|
- service: 'mysql:5.7'
|
||||||
|
db: MySQL
|
||||||
|
- service: mariadb
|
||||||
|
db: MariaDB
|
||||||
|
- prefix: flarum_
|
||||||
|
prefixStr: (prefix)
|
||||||
|
|
||||||
|
exclude:
|
||||||
|
- php: 7.1
|
||||||
|
service: 'mysql:5.7'
|
||||||
|
prefix: flarum_
|
||||||
|
- php: 7.1
|
||||||
|
service: mariadb
|
||||||
|
prefix: flarum_
|
||||||
|
- php: 7.2
|
||||||
|
service: 'mysql:5.7'
|
||||||
|
prefix: flarum_
|
||||||
|
- php: 7.2
|
||||||
|
service: mariadb
|
||||||
|
prefix: flarum_
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: ${{ matrix.service }}
|
||||||
|
ports:
|
||||||
|
- 13306:3306
|
||||||
|
|
||||||
|
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
|
- name: Select PHP version
|
||||||
|
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
|
||||||
|
|
||||||
|
- name: Create MySQL Database
|
||||||
|
run: mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||||
|
|
||||||
|
- name: Install Composer dependencies
|
||||||
|
run: composer install
|
||||||
|
|
||||||
|
- name: Setup Composer tests
|
||||||
|
run: composer test:setup
|
||||||
|
env:
|
||||||
|
DB_PORT: 13306
|
||||||
|
DB_PASSWORD: root
|
||||||
|
DB_PREFIX: ${{ matrix.prefix }}
|
||||||
|
|
||||||
|
- name: Run Composer tests
|
||||||
|
run: composer test
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,9 @@
|
|||||||
/vendor
|
/vendor
|
||||||
|
composer.lock
|
||||||
composer.phar
|
composer.phar
|
||||||
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
tests/_output/*
|
/tests/integration/tmp
|
||||||
.vagrant
|
.vagrant
|
||||||
|
.idea/*
|
||||||
|
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"
|
30
.travis.yml
30
.travis.yml
@@ -1,30 +0,0 @@
|
|||||||
language: php
|
|
||||||
|
|
||||||
php:
|
|
||||||
- 5.5
|
|
||||||
- 5.6
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- php: hhvm
|
|
||||||
fast_finish: true
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- curl -s http://getcomposer.org/installer | php
|
|
||||||
- php composer.phar install
|
|
||||||
|
|
||||||
script:
|
|
||||||
- php composer.phar style
|
|
||||||
- php composer.phar test
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email:
|
|
||||||
on_failure: change
|
|
||||||
webhooks:
|
|
||||||
urls:
|
|
||||||
- https://webhooks.gitter.im/e/7b9e9827a03b44a16588
|
|
||||||
on_success: always
|
|
||||||
on_failure: always
|
|
||||||
on_start: false
|
|
||||||
|
|
||||||
sudo: false
|
|
112
CHANGELOG.md
112
CHANGELOG.md
@@ -1,44 +1,86 @@
|
|||||||
# Change Log
|
# Changelog
|
||||||
All notable changes to Flarum and its bundled extensions will be documented in this file.
|
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
||||||
|
|
||||||
## [Unreleased][unreleased]
|
## [0.1.0-beta.10](https://github.com/flarum/core/compare/v0.1.0-beta.9...v0.1.0-beta.10)
|
||||||
*nothing yet*
|
|
||||||
|
|
||||||
## [0.1.0-beta.2] - 2015-09-15
|
|
||||||
### Added
|
### Added
|
||||||
- Check prerequisites (PHP version, extensions, etc.) before installation (#364)
|
- Initial queue support: Infrastructure for offloading long-running tasks (e.g. email sending) to background workers (#1773)
|
||||||
- Enforce maximum title and post length through validation (#53, #338)
|
- Notifications can now be marked as read without visiting a discussion (#151)
|
||||||
- Ctrl+Enter submits posts (#276)
|
- SEO: The discussion list now has a `rel="canonical"` meta tag, preventing duplicate content (#1134, #1814)
|
||||||
- Syntax highlighting for code blocks (#248)
|
- The "Edit User" permission can now be edited in the UI (#1845)
|
||||||
- All links open in new window, receive rel=nofollow attribute (#247)
|
- New status message and redirect after user deletion (#1750, #1777)
|
||||||
- Default build script for extensions (#438)
|
- Errors in Flarum's boot process are now presented with more detailed information (#1607)
|
||||||
- Input validation in installer
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Ask for admin password confirmation in installer (#405)
|
- Better, more detailed and extensible error handling (#1641, #1843)
|
||||||
- Increased some text contrasts for accessibility (#390)
|
- Error pages in debug mode now return the same HTTP status codes as in production (#1648)
|
||||||
|
- Tweak HTTP status codes for authentication / authorization errors (#1854)
|
||||||
|
- Already-used links from account activation emails now show a better error message (#1337)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Discussion list did not work with non-empty database prefix (#269, #380)
|
- Security vulnerabilities in dependencies
|
||||||
- Non-admins could not reset their password (#229)
|
- Performance: High CPU usage when scrolling in a discussion (#1222)
|
||||||
- Requests ending with a slash resulted in a 404 (#334)
|
- Special characters crashed the search (#1498)
|
||||||
- In rare cases, posts did not load correctly (#295)
|
- Missing declarations for language and text direction in HTML output (#1772)
|
||||||
- Avatars did not show up when installed in a subfolder (#291)
|
- Private messages were counted in user post counts (#1695)
|
||||||
- Installer crashed when views directory was not writable (#376)
|
- Extensions could not change the forum's default page (#1819)
|
||||||
- Table prefix could not be set in web installer (#269)
|
- API requests authenticated using access tokens needed to provide a CSRF token (#1828)
|
||||||
- Enabling an extension disabled all other extensions (#402)
|
- Accessibility: Screenreaders did not read the "Back to discussion list" link (#1835)
|
||||||
- Invalid custom CSS could crash the application (#400)
|
|
||||||
- First posts could not be restored or deleted
|
|
||||||
- Several design bugs
|
|
||||||
- Set cookies to be HTTP-only
|
|
||||||
- Tags: Sometimes, tags could not be dragged for reordering in the admin panel (#341)
|
|
||||||
- Suspend: Use correct column name in when migrating database
|
|
||||||
- Lock: Check for correct permission when displaying lock control
|
|
||||||
- Likes: Allow liking permissions to be configured
|
|
||||||
|
|
||||||
## 0.1.0-beta - 2015-08-27
|
## [0.1.0-beta.9](https://github.com/flarum/core/compare/v0.1.0-beta.8.2...v0.1.0-beta.9)
|
||||||
First Version
|
|
||||||
|
|
||||||
[unreleased]: https://github.com/flarum/core/compare/v0.1.0-beta.2...HEAD
|
### Added
|
||||||
[0.1.0-beta.2]: https://github.com/flarum/core/compare/v0.1.0-beta...v0.1.0-beta.2
|
- New `hasPermission()` helper method for `Group` objects ([9684fbc](https://github.com/flarum/core/commit/9684fbc4da07d32aa322d9228302a23418412cb9))
|
||||||
|
- Expose supported mail drivers in IoC container ([208bad3](https://github.com/flarum/core/commit/208bad393f37bfdb76007afcddfa4b7451563e9d))
|
||||||
|
- More test for some API endpoints ([1670590](https://github.com/flarum/core/commit/167059027e5a066d618599c90164ef1b5a509148))
|
||||||
|
- The `Formatter\Rendering` event now receives the HTTP request instance as well ([0ab9fac](https://github.com/flarum/core/commit/0ab9facc4bd59a260575e6fc650793c663e5866a))
|
||||||
|
- More and better validation in installer UIs
|
||||||
|
- Check and enforce minimum MariaDB ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
|
||||||
|
- Revert publication of assets when installation fails ([ed9591c](https://github.com/flarum/core/commit/ed9591c16fb2ea7a4be3387b805d855a53e0a7d5))
|
||||||
|
- Benefit from Laravel's database reconnection logic in long-running tasks ([e0becd0](https://github.com/flarum/core/commit/e0becd0c7bda939048923c1f86648793feee78d5))
|
||||||
|
- The "vendor path" (where Composer dependencies can be found) can now be configured ([5e1680c](https://github.com/flarum/core/commit/5e1680c458cd3ba274faeb92de3ac2053789131e))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Performance: Actually cache translations on disk ([0d16fac](https://github.com/flarum/core/commit/0d16fac001bb735ee66e82871183516aeac269b7))
|
||||||
|
- Allow per-site extenders to override extension extenders ([ba594de](https://github.com/flarum/core/commit/ba594de13a033480834d53d73f747b05fe9796f8))
|
||||||
|
- Do not resolve objects from the IoC container (in service providers and extenders) until they are actually used
|
||||||
|
- Replace event subscribers (that resolve objects from the IoC container) with listeners (that resolve lazily)
|
||||||
|
- Use custom service provider for Mail component ([ac5e26a](https://github.com/flarum/core/commit/ac5e26a254d89e21bd4c115b6cbd40338e2e4b4b))
|
||||||
|
- Update to Laravel 5.7, revert custom logic for building database index names
|
||||||
|
- Refactored installer, extracted Installation class and pipeline for reuse in CLI and web installers ([790d5be](https://github.com/flarum/core/commit/790d5beee5e283178716bc8f9901c758d9e5b6a0))
|
||||||
|
- Use whitelist for enabling pre-installed extensions during installation ([4585f03](https://github.com/flarum/core/commit/4585f03ee356c92942fbc2ae8c683c651b473954))
|
||||||
|
- Update minimum MySQL version ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Signing up via OAuth providers was broken ([67f9375](https://github.com/flarum/core/commit/67f9375d4745add194ae3249d526197c32fd5461))
|
||||||
|
- Group badges were overlapping ([16eb1fa](https://github.com/flarum/core/commit/16eb1fa63b6d7b80ec30c24c0e406a2b7ab09934))
|
||||||
|
- API: Endpoint for uninstalling extensions returned an error ([c761802](https://github.com/flarum/core/commit/c76180290056ddbab67baf5ede814fcedf1dcf14))
|
||||||
|
- Documentation links in installer were outdated ([b58380e](https://github.com/flarum/core/commit/b58380e224ee54abdade3d0a4cc107ef5c91c9a9))
|
||||||
|
- Event posts where counted when aggregating user posts ([671fdec](https://github.com/flarum/core/commit/671fdec8d0a092ccceb5d4d5f657d0f4287fc4c7))
|
||||||
|
- Admins could not reset user passwords ([c67fb2d](https://github.com/flarum/core/commit/c67fb2d4b6a128c71d65dc6703310c0b62f91be2))
|
||||||
|
- Several down migrations were invalid
|
||||||
|
- Validation errors on reset password page resulted in HTTP 404 ([4611abe](https://github.com/flarum/core/commit/4611abe5db8b94ca3dc7bf9c447fca7c67358ee3))
|
||||||
|
- `is:unread` gambit generated an invalid query ([e17bb0b](https://github.com/flarum/core/commit/e17bb0b4331f2c92459292195c6b7db8cde1f9f3))
|
||||||
|
- Entire forum was breaking when the `custom_less` setting was missing from the database ([bf2c5a5](https://github.com/flarum/core/commit/bf2c5a5564dff3f5ef13efe7a8d69f2617570ce6))
|
||||||
|
- Dropdown icon was not showing in user card when on user page ([12fdfc9](https://github.com/flarum/core/commit/12fdfc9b544a27f6fe59c82ad6bddd3420cc0181))
|
||||||
|
- Requests were missing the `original*` attributes, which broke installations in subfolders ([56fde28](https://github.com/flarum/core/commit/56fde28e436f52fee0c03c538f0a6049bc584b53))
|
||||||
|
- Special characters such as `%` and `_` could return incorrect results ([ee3640e](https://github.com/flarum/core/commit/ee3640e1605ff67fef4b3d5cd0596f14a6ae73c9))
|
||||||
|
- FontAwesome component package changed paths in version 5.9.0 ([5eb69e1](https://github.com/flarum/core/commit/5eb69e1f59fa73fdfd5badbf41a05a6a040e7426))
|
||||||
|
- Some server environments had problems accessing the system-wide tmp path for storing JS file maps ([54660eb](https://github.com/flarum/core/commit/54660ebd6311f9ea142f1b573263d0d907400786))
|
||||||
|
- Content length of posts.content was not migrated to mediumText in 2017 ([590b311](https://github.com/flarum/core/commit/590b3115708bf94a9c7f169d98c6126380c7056e))
|
||||||
|
- An error occurred when going to the previous route if there was no previous route found ([985b87da](https://github.com/flarum/core/commit/985b87da6c9942c568a1a192e2fdcfde72e030ee))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `php flarum install --defaults` - this was meant to be used in our old development VM ([44c9109](https://github.com/flarum/core/commit/44c91099cd77138bb5fc29f14fb1e81a9781272d))
|
||||||
|
- Obsolete `id` attributes in JSON-API responses ([ecc3b5e](https://github.com/flarum/core/commit/ecc3b5e2271f8d9b38d52cd54476d86995dbe32e) and [7a44086](https://github.com/flarum/core/commit/7a44086bf3a0e3ba907dceb13d07ac695eca05ea))
|
||||||
|
|
||||||
|
## [0.1.0-beta.8.1](https://github.com/flarum/core/compare/v0.1.0-beta.8...v0.1.0-beta.8.1)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix live output in `migrate:reset` command ([f591585](https://github.com/flarum/core/commit/f591585d02f8c4ff0211c5bf4413dd6baa724c05))
|
||||||
|
- Fix search with database prefix ([7705a2b](https://github.com/flarum/core/commit/7705a2b7d751943ef9d0c7379ec34f8530b99310))
|
||||||
|
- Fix invalid join time of admin user created by installer ([57f73c9](https://github.com/flarum/core/commit/57f73c9638eeb825f9e336ed3c443afccfd8995e))
|
||||||
|
- Ensure InnoDB engine is used for all tables ([fb6b51b](https://github.com/flarum/core/commit/fb6b51b1cfef0af399607fe038603c8240800b2b), [6370f7e](https://github.com/flarum/core/commit/6370f7ecffa9ea7d5fb64d9551400edbc63318db))
|
||||||
|
- Fix dropping foreign keys in `down` migrations ([57d5846](https://github.com/flarum/core/commit/57d5846b647881009d9e60f9ffca20b1bb77776e))
|
||||||
|
- Fix discussion list scroll position not being maintained when hero is not visible ([40dc6ac](https://github.com/flarum/core/commit/40dc6ac604c2a0973356b38217aa8d09352daae5))
|
||||||
|
- Fix empty meta description tag ([88e43cc](https://github.com/flarum/core/commit/88e43cc6940ee30d6529e9ce659471ec4fb1c474))
|
||||||
|
- Remove empty attributes on `<html>` tag ([796b577](https://github.com/flarum/core/commit/796b57753d34d4ea741dbebcbc550b17808f6c94))
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
# Contributing to Flarum
|
|
||||||
|
|
||||||
Thanks for your interest in contributing to Flarum! Please read the [Contributing docs](http://flarum.org/docs/contributing) to learn how you can help.
|
|
||||||
|
|
||||||
## Contributor License Agreement
|
|
||||||
|
|
||||||
By contributing your code to Flarum you grant Toby Zerner a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, transferable license under all of Your relevant intellectual property rights (including copyright, patent, and any other rights), to use, copy, prepare derivative works of, distribute and publicly perform and display the Contributions on any licensing terms, including without limitation: (a) open source licenses like the MIT license; and (b) binary, proprietary, or commercial licenses. Except for the licenses granted herein, You reserve all right, title, and interest in and to the Contribution.
|
|
||||||
|
|
||||||
You confirm that you are able to grant us these rights. You represent that You are legally entitled to grant the above license. If Your employer has rights to intellectual property that You create, You represent that You have received permission to make the Contributions on behalf of that employer, or that Your employer has waived such rights for the Contributions.
|
|
||||||
|
|
||||||
You represent that the Contributions are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to the Contributions. You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license.
|
|
||||||
|
|
||||||
Toby Zerner acknowledges that, except as explicitly described in this Agreement, any Contribution which you provide is on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
|
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014-2015 Toby Zerner
|
Copyright (c) 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
|
||||||
|
34
README.md
34
README.md
@@ -1,7 +1,35 @@
|
|||||||
# Flarum Core
|
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
|
||||||
|
|
||||||
This repository contains the core code of Flarum. If you want to install Flarum, visit the [main Flarum repository](http://github.com/flarum/flarum).
|
<p align="center">
|
||||||
|
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Flarum
|
||||||
|
|
||||||
|
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
|
||||||
|
|
||||||
|
* **Fast and simple.** No clutter, no bloat, no complex dependencies. Flarum is built with PHP so it’s quick and easy to deploy. The interface is powered by Mithril, a performant JavaScript framework with a tiny footprint.
|
||||||
|
|
||||||
|
* **Beautiful and responsive.** This is forum software for humans. Flarum is carefully designed to be consistent and intuitive across platforms, out-of-the-box.
|
||||||
|
|
||||||
|
* **Powerful and extensible.** Customize, extend, and integrate Flarum to suit your community. Flarum’s architecture is amazingly flexible, with a powerful Extension API.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This repository contains Flarum's core code. If you want to set up a forum, visit the [Flarum skeleton repository](https://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.
|
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://flarum.org/docs/contributing.html)** to learn how you can help.
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Flarum, please send an e-mail to [security@flarum.org](mailto:security@flarum.org). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
|
||||||
|
|
||||||
|
116
composer.json
116
composer.json
@@ -1,43 +1,75 @@
|
|||||||
{
|
{
|
||||||
"name": "flarum/core",
|
"name": "flarum/core",
|
||||||
"description": "",
|
"description": "Delightfully simple forum software.",
|
||||||
|
"keywords": ["forum", "discussion"],
|
||||||
|
"homepage": "https://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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Daniel Klabbers",
|
||||||
|
"email": "daniel@klabbers.email"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/flarum/core/issues",
|
||||||
|
"source": "https://github.com/flarum/core",
|
||||||
|
"docs": "https://flarum.org/docs/"
|
||||||
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.4.0",
|
"php": ">=7.1",
|
||||||
"illuminate/bus": "5.1.*",
|
"axy/sourcemap": "^0.1.4",
|
||||||
"illuminate/cache": "5.1.*",
|
"components/font-awesome": "5.9.*",
|
||||||
"illuminate/config": "5.1.*",
|
"dflydev/fig-cookies": "^1.0.2",
|
||||||
"illuminate/container": "5.1.*",
|
"doctrine/dbal": "^2.7",
|
||||||
"illuminate/contracts": "5.1.*",
|
"franzl/whoops-middleware": "^0.4.0",
|
||||||
"illuminate/database": "5.1.*",
|
"illuminate/bus": "5.7.*",
|
||||||
"illuminate/events": "5.1.*",
|
"illuminate/cache": "5.7.*",
|
||||||
"illuminate/filesystem": "5.1.*",
|
"illuminate/config": "5.7.*",
|
||||||
"illuminate/hashing": "5.1.*",
|
"illuminate/container": "5.7.*",
|
||||||
"illuminate/mail": "5.1.*",
|
"illuminate/contracts": "5.7.*",
|
||||||
"illuminate/support": "5.1.*",
|
"illuminate/database": "5.7.*",
|
||||||
"illuminate/validation": "5.1.*",
|
"illuminate/events": "5.7.*",
|
||||||
"illuminate/view": "5.1.*",
|
"illuminate/filesystem": "5.7.*",
|
||||||
"league/flysystem": "^1.0.11",
|
"illuminate/hashing": "5.7.*",
|
||||||
"tobscure/json-api": "^0.1.1",
|
"illuminate/mail": "5.7.*",
|
||||||
"oyejorge/less.php": "~1.5",
|
"illuminate/queue": "5.7.*",
|
||||||
|
"illuminate/session": "5.7.*",
|
||||||
|
"illuminate/support": "5.7.*",
|
||||||
|
"illuminate/validation": "5.7.*",
|
||||||
|
"illuminate/view": "5.7.*",
|
||||||
"intervention/image": "^2.3.0",
|
"intervention/image": "^2.3.0",
|
||||||
"s9e/text-formatter": "^0.3.2",
|
"league/flysystem": "^1.0.11",
|
||||||
"psr/http-message": "^1.0",
|
"matthiasmullie/minify": "^1.3",
|
||||||
"zendframework/zend-diactoros": "^1.1",
|
"middlewares/base-path": "^1.1",
|
||||||
|
"middlewares/base-path-router": "^0.2.1",
|
||||||
|
"middlewares/request-handler": "^1.2",
|
||||||
|
"monolog/monolog": "^1.16.0",
|
||||||
"nikic/fast-route": "^0.6",
|
"nikic/fast-route": "^0.6",
|
||||||
"dflydev/fig-cookies": "^1.0",
|
"oyejorge/less.php": "^1.7",
|
||||||
"symfony/console": "^2.7",
|
"psr/http-message": "^1.0",
|
||||||
"symfony/yaml": "^2.7",
|
"psr/http-server-handler": "^1.0",
|
||||||
"doctrine/dbal": "^2.5"
|
"psr/http-server-middleware": "^1.0",
|
||||||
|
"s9e/text-formatter": "^1.2.0",
|
||||||
|
"symfony/config": "^3.3",
|
||||||
|
"symfony/console": "^4.2",
|
||||||
|
"symfony/translation": "^3.3",
|
||||||
|
"symfony/yaml": "^3.3",
|
||||||
|
"tobscure/json-api": "^0.3.0",
|
||||||
|
"zendframework/zend-diactoros": "^1.8.4",
|
||||||
|
"zendframework/zend-httphandlerrunner": "^1.0",
|
||||||
|
"zendframework/zend-stratigility": "^3.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"squizlabs/php_codesniffer": "2.*",
|
"mockery/mockery": "^1.0",
|
||||||
"phpspec/phpspec": "^2.2"
|
"phpunit/phpunit": "^7.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -47,8 +79,32 @@
|
|||||||
"src/helpers.php"
|
"src/helpers.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Flarum\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "0.1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "phpspec run",
|
"test": [
|
||||||
"style": "phpcs --standard=PSR2 -np src"
|
"@test:unit",
|
||||||
|
"@test:integration"
|
||||||
|
],
|
||||||
|
"test:unit": "phpunit -c tests/phpunit.unit.xml",
|
||||||
|
"test:integration": "phpunit -c tests/phpunit.integration.xml",
|
||||||
|
"test:setup": "@php tests/integration/setup.php"
|
||||||
|
},
|
||||||
|
"scripts-descriptions": {
|
||||||
|
"test": "Runs all tests.",
|
||||||
|
"test:unit": "Runs all unit tests.",
|
||||||
|
"test:integration": "Runs all integration tests.",
|
||||||
|
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2874
composer.lock
generated
2874
composer.lock
generated
File diff suppressed because it is too large
Load Diff
1
js/.gitignore
vendored
1
js/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
bower_components
|
|
11
js/admin.js
Normal file
11
js/admin.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './src/common';
|
||||||
|
export * from './src/admin';
|
3
js/admin/.gitignore
vendored
3
js/admin/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
node_modules
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,144 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import FieldSet from 'flarum/components/FieldSet';
|
|
||||||
import Select from 'flarum/components/Select';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import Alert from 'flarum/components/Alert';
|
|
||||||
import saveConfig from 'flarum/utils/saveConfig';
|
|
||||||
|
|
||||||
export default class BasicsPage extends Component {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
this.fields = [
|
|
||||||
'forum_title',
|
|
||||||
'forum_description',
|
|
||||||
'default_locale',
|
|
||||||
'default_route',
|
|
||||||
'welcome_title',
|
|
||||||
'welcome_message'
|
|
||||||
];
|
|
||||||
this.values = {};
|
|
||||||
|
|
||||||
const config = app.config;
|
|
||||||
this.fields.forEach(key => this.values[key] = m.prop(config[key]));
|
|
||||||
|
|
||||||
this.localeOptions = {};
|
|
||||||
const locales = app.locales;
|
|
||||||
for (const i in locales) {
|
|
||||||
this.localeOptions[i] = `${locales[i]} (${i})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<div className="BasicsPage">
|
|
||||||
<div className="container">
|
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
|
||||||
{FieldSet.component({
|
|
||||||
label: 'Forum Title',
|
|
||||||
children: [
|
|
||||||
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
|
|
||||||
]
|
|
||||||
})}
|
|
||||||
|
|
||||||
{FieldSet.component({
|
|
||||||
label: 'Forum Description',
|
|
||||||
children: [
|
|
||||||
<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.
|
|
||||||
</div>,
|
|
||||||
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
|
|
||||||
]
|
|
||||||
})}
|
|
||||||
|
|
||||||
{Object.keys(this.localeOptions).length > 1
|
|
||||||
? FieldSet.component({
|
|
||||||
label: 'Default Language',
|
|
||||||
children: [
|
|
||||||
Select.component({
|
|
||||||
options: this.localeOptions,
|
|
||||||
onchange: this.values.default_locale
|
|
||||||
})
|
|
||||||
]
|
|
||||||
})
|
|
||||||
: ''}
|
|
||||||
|
|
||||||
{FieldSet.component({
|
|
||||||
label: 'Home Page',
|
|
||||||
className: 'BasicsPage-homePage',
|
|
||||||
children: [
|
|
||||||
<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.
|
|
||||||
</div>,
|
|
||||||
<label className="checkbox">
|
|
||||||
<input type="radio" name="homePage" value="/all" checked={this.values.default_route() === '/all'} onclick={m.withAttr('value', this.values.default_route)}/>
|
|
||||||
All Discussions
|
|
||||||
</label>,
|
|
||||||
<label className="checkbox">
|
|
||||||
<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({
|
|
||||||
label: 'Welcome Banner',
|
|
||||||
className: 'BasicsPage-welcomeBanner',
|
|
||||||
children: [
|
|
||||||
<div className="helpText">
|
|
||||||
Configure the text that displays in the banner on the All Discussions page. Use this to welcome guests to your forum.
|
|
||||||
</div>,
|
|
||||||
<div className="BasicsPage-welcomeBanner-input">
|
|
||||||
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
|
|
||||||
<textarea className="FormControl" value={this.values.welcome_message()} oninput={m.withAttr('value', this.values.welcome_message)}/>
|
|
||||||
</div>
|
|
||||||
]
|
|
||||||
})}
|
|
||||||
|
|
||||||
{Button.component({
|
|
||||||
type: 'submit',
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
children: 'Save Changes',
|
|
||||||
loading: this.loading,
|
|
||||||
disabled: !this.changed()
|
|
||||||
})}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
changed() {
|
|
||||||
const config = app.config;
|
|
||||||
|
|
||||||
return this.fields.some(key => this.values[key]() !== config[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (this.loading) return;
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
app.alerts.dismiss(this.successAlert);
|
|
||||||
|
|
||||||
const config = {};
|
|
||||||
|
|
||||||
this.fields.forEach(key => config[key] = this.values[key]());
|
|
||||||
|
|
||||||
saveConfig(config)
|
|
||||||
.then(() => {
|
|
||||||
app.alerts.show(this.successAlert = new Alert({type: 'success', children: 'Your changes were saved.'}));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import saveConfig from 'flarum/utils/saveConfig';
|
|
||||||
|
|
||||||
export default class ConfigDropdown extends SelectDropdown {
|
|
||||||
static initProps(props) {
|
|
||||||
super.initProps(props);
|
|
||||||
|
|
||||||
props.className = 'ConfigDropdown';
|
|
||||||
props.buttonClassName = 'Button Button--text';
|
|
||||||
props.caretIcon = 'caret-down';
|
|
||||||
props.defaultLabel = 'Custom';
|
|
||||||
|
|
||||||
props.children = props.options.map(({value, label}) => {
|
|
||||||
const active = app.config[props.key] === value;
|
|
||||||
|
|
||||||
return Button.component({
|
|
||||||
children: label,
|
|
||||||
icon: active ? 'check' : true,
|
|
||||||
onclick: saveConfig.bind(this, {[props.key]: value}),
|
|
||||||
active
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,105 +0,0 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import Badge from 'flarum/components/Badge';
|
|
||||||
import Group from 'flarum/models/Group';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `EditGroupModal` component shows a modal dialog which allows the user
|
|
||||||
* to create or edit a group.
|
|
||||||
*/
|
|
||||||
export default class EditGroupModal extends Modal {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
this.group = this.props.group || app.store.createRecord('groups');
|
|
||||||
|
|
||||||
this.nameSingular = m.prop(this.group.nameSingular() || '');
|
|
||||||
this.namePlural = m.prop(this.group.namePlural() || '');
|
|
||||||
this.icon = m.prop(this.group.icon() || '');
|
|
||||||
this.color = m.prop(this.group.color() || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
return 'EditGroupModal Modal--small';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return [
|
|
||||||
this.color() || this.icon() ? Badge.component({
|
|
||||||
icon: this.icon(),
|
|
||||||
style: {backgroundColor: this.color()}
|
|
||||||
}) : '',
|
|
||||||
' ',
|
|
||||||
this.namePlural() || 'Create Group'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return (
|
|
||||||
<div className="Modal-body">
|
|
||||||
<div className="Form">
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>Name</label>
|
|
||||||
<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="Plural (e.g. Mods)" value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>Color</label>
|
|
||||||
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>Icon</label>
|
|
||||||
<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.
|
|
||||||
</div>
|
|
||||||
<input className="FormControl" placeholder="bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component({
|
|
||||||
type: 'submit',
|
|
||||||
className: 'Button Button--primary EditGroupModal-save',
|
|
||||||
loading: this.loading,
|
|
||||||
children: 'Save Changes'
|
|
||||||
})}
|
|
||||||
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
|
||||||
<button type="button" className="Button EditGroupModal-delete" onclick={this.delete.bind(this)}>
|
|
||||||
Delete Group
|
|
||||||
</button>
|
|
||||||
) : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
this.group.save({
|
|
||||||
nameSingular: this.nameSingular(),
|
|
||||||
namePlural: this.namePlural(),
|
|
||||||
color: this.color(),
|
|
||||||
icon: this.icon()
|
|
||||||
}).then(
|
|
||||||
() => this.hide(),
|
|
||||||
() => {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete() {
|
|
||||||
if (confirm('Are you sure you want to delete this group? The group members will NOT be deleted.')) {
|
|
||||||
this.group.delete().then(() => m.redraw());
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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,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;
|
|
||||||
});
|
|
||||||
}
|
|
@@ -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": "*"
|
|
||||||
}
|
|
||||||
}
|
|
40
js/dist/admin.js
vendored
Normal file
40
js/dist/admin.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
js/dist/admin.js.map
vendored
Normal file
1
js/dist/admin.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
67
js/dist/forum.js
vendored
Normal file
67
js/dist/forum.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
js/dist/forum.js.map
vendored
Normal file
1
js/dist/forum.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
11
js/forum.js
Normal file
11
js/forum.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './src/common';
|
||||||
|
export * from './src/forum';
|
3
js/forum/.gitignore
vendored
3
js/forum/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
node_modules
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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,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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,140 +0,0 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
|
||||||
import ForgotPasswordModal from 'flarum/components/ForgotPasswordModal';
|
|
||||||
import SignUpModal from 'flarum/components/SignUpModal';
|
|
||||||
import Alert from 'flarum/components/Alert';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `LogInModal` component displays a modal dialog with a login form.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `email`
|
|
||||||
* - `password`
|
|
||||||
*/
|
|
||||||
export default class LogInModal extends Modal {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the email input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.email = m.prop(this.props.email || '');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the password input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.password = m.prop(this.props.password || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
return 'LogInModal Modal--small';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return app.trans('core.log_in');
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return [
|
|
||||||
<div className="Modal-body">
|
|
||||||
<div className="Form Form--centered">
|
|
||||||
<div className="Form-group">
|
|
||||||
<input className="FormControl" name="email" placeholder={app.trans('core.username_or_email')}
|
|
||||||
value={this.email()}
|
|
||||||
onchange={m.withAttr('value', this.email)}
|
|
||||||
disabled={this.loading} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
|
|
||||||
value={this.password()}
|
|
||||||
onchange={m.withAttr('value', this.password)}
|
|
||||||
disabled={this.loading} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--primary Button--block',
|
|
||||||
type: 'submit',
|
|
||||||
loading: this.loading,
|
|
||||||
children: app.trans('core.log_in')
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
<div className="Modal-footer">
|
|
||||||
<p className="LogInModal-forgotPassword">
|
|
||||||
<a onclick={this.forgotPassword.bind(this)}>{app.trans('core.forgot_password_link')}</a>
|
|
||||||
</p>
|
|
||||||
{app.forum.attribute('allowSignUp') ? (
|
|
||||||
<p className="LogInModal-signUp">
|
|
||||||
{app.trans('core.before_sign_up_link')}{' '}
|
|
||||||
<a onclick={this.signUp.bind(this)}>{app.trans('core.sign_up')}</a>
|
|
||||||
</p>
|
|
||||||
) : ''}
|
|
||||||
</div>
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the forgot password modal, prefilling it with an email if the user has
|
|
||||||
* entered one.
|
|
||||||
*/
|
|
||||||
forgotPassword() {
|
|
||||||
const email = this.email();
|
|
||||||
const props = email.indexOf('@') !== -1 ? {email} : undefined;
|
|
||||||
|
|
||||||
app.modal.show(new ForgotPasswordModal(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the sign up modal, prefilling it with an email/username/password if
|
|
||||||
* the user has entered one.
|
|
||||||
*/
|
|
||||||
signUp() {
|
|
||||||
const props = {password: this.password()};
|
|
||||||
const email = this.email();
|
|
||||||
props[email.indexOf('@') !== -1 ? 'email' : 'username'] = email;
|
|
||||||
|
|
||||||
app.modal.show(new SignUpModal(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
onready() {
|
|
||||||
this.$('[name=' + (this.email() ? 'password' : 'email') + ']').select();
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
const email = this.email();
|
|
||||||
const password = this.password();
|
|
||||||
|
|
||||||
app.session.login(email, password).then(
|
|
||||||
null,
|
|
||||||
response => {
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
if (response && response.code === 'confirm_email') {
|
|
||||||
this.alert = Alert.component({
|
|
||||||
children: app.trans('core.email_confirmation_required', {email: response.email})
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.alert = Alert.component({
|
|
||||||
type: 'error',
|
|
||||||
children: app.trans('core.invalid_login')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
m.redraw();
|
|
||||||
this.onready();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,141 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import listItems from 'flarum/helpers/listItems';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
|
||||||
import Discussion from 'flarum/models/Discussion';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `NotificationList` component displays a list of the logged-in user's
|
|
||||||
* notifications, grouped by discussion.
|
|
||||||
*/
|
|
||||||
export default class NotificationList extends Component {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the notifications are loading.
|
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
const groups = [];
|
|
||||||
|
|
||||||
if (app.cache.notifications) {
|
|
||||||
const discussions = {};
|
|
||||||
|
|
||||||
// Build an array of discussions which the notifications are related to,
|
|
||||||
// and add the notifications as children.
|
|
||||||
app.cache.notifications.forEach(notification => {
|
|
||||||
const subject = notification.subject();
|
|
||||||
|
|
||||||
if (typeof subject === 'undefined') return;
|
|
||||||
|
|
||||||
// Get the discussion that this notification is related to. If it's not
|
|
||||||
// directly related to a discussion, it may be related to a post or
|
|
||||||
// other entity which is related to a discussion.
|
|
||||||
let discussion = false;
|
|
||||||
if (subject instanceof Discussion) discussion = subject;
|
|
||||||
else if (subject && subject.discussion) discussion = subject.discussion();
|
|
||||||
|
|
||||||
// If the notification is not related to a discussion directly or
|
|
||||||
// indirectly, then we will assign it to a neutral group.
|
|
||||||
const key = discussion ? discussion.id() : 0;
|
|
||||||
discussions[key] = discussions[key] || {discussion: discussion, notifications: []};
|
|
||||||
discussions[key].notifications.push(notification);
|
|
||||||
|
|
||||||
if (groups.indexOf(discussions[key]) === -1) {
|
|
||||||
groups.push(discussions[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="NotificationList">
|
|
||||||
<div className="NotificationList-header">
|
|
||||||
<div className="App-primaryControl">
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--icon Button--link',
|
|
||||||
icon: 'check',
|
|
||||||
title: app.trans('core.mark_all_as_read'),
|
|
||||||
onclick: this.markAllAsRead.bind(this)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 className="App-titleControl App-titleControl--text">{app.trans('core.notifications')}</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="NotificationList-content">
|
|
||||||
{groups.length
|
|
||||||
? groups.map(group => {
|
|
||||||
const badges = group.discussion && group.discussion.badges().toArray();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="NotificationGroup">
|
|
||||||
{group.discussion
|
|
||||||
? (
|
|
||||||
<a className="NotificationGroup-header"
|
|
||||||
href={app.route.discussion(group.discussion)}
|
|
||||||
config={m.route}>
|
|
||||||
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
|
|
||||||
{group.discussion.title()}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<div className="NotificationGroup-header">
|
|
||||||
{app.forum.attribute('title')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul className="NotificationGroup-content">
|
|
||||||
{group.notifications.map(notification => {
|
|
||||||
const NotificationComponent = app.notificationComponents[notification.contentType()];
|
|
||||||
return NotificationComponent ? <li>{NotificationComponent.component({notification})}</li> : '';
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: !this.loading
|
|
||||||
? <div className="NotificationList-empty">{app.trans('core.no_notifications')}</div>
|
|
||||||
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load notifications into the application's cache if they haven't already
|
|
||||||
* been loaded.
|
|
||||||
*/
|
|
||||||
load() {
|
|
||||||
if (app.cache.notifications && !app.session.user.unreadNotificationsCount()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
m.redraw();
|
|
||||||
|
|
||||||
app.store.find('notifications').then(notifications => {
|
|
||||||
app.session.user.pushAttributes({unreadNotificationsCount: 0});
|
|
||||||
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark all of the notifications as read.
|
|
||||||
*/
|
|
||||||
markAllAsRead() {
|
|
||||||
if (!app.cache.notifications) return;
|
|
||||||
|
|
||||||
app.cache.notifications.forEach(notification => {
|
|
||||||
if (!notification.isRead()) {
|
|
||||||
notification.save({isRead: true});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,81 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
|
|
||||||
import Dropdown from 'flarum/components/Dropdown';
|
|
||||||
import PostControls from 'flarum/utils/PostControls';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Post` component displays a single post. The basic post template just
|
|
||||||
* includes a controls dropdown; subclasses must implement `content` and `attrs`
|
|
||||||
* methods.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `post`
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export default class Post extends Component {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up a subtree retainer so that the post will not be redrawn
|
|
||||||
* unless new data comes in.
|
|
||||||
*
|
|
||||||
* @type {SubtreeRetainer}
|
|
||||||
*/
|
|
||||||
this.subtree = new SubtreeRetainer(
|
|
||||||
() => this.props.post.freshness,
|
|
||||||
() => {
|
|
||||||
const user = this.props.post.user();
|
|
||||||
return user && user.freshness;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
const attrs = this.attrs();
|
|
||||||
|
|
||||||
attrs.className = 'Post ' + (attrs.className || '');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article {...attrs}>
|
|
||||||
{this.subtree.retain() || (() => {
|
|
||||||
const controls = PostControls.controls(this.props.post, this).toArray();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{controls.length ? Dropdown.component({
|
|
||||||
children: controls,
|
|
||||||
className: 'Post-controls',
|
|
||||||
buttonClassName: 'Button Button--icon Button--flat',
|
|
||||||
menuClassName: 'Dropdown-menu--right',
|
|
||||||
icon: 'ellipsis-v'
|
|
||||||
}) : ''}
|
|
||||||
|
|
||||||
{this.content()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get attributes for the post element.
|
|
||||||
*
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
attrs() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the post's content.
|
|
||||||
*
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
content() {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,30 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import icon from 'flarum/helpers/icon';
|
|
||||||
import humanTime from 'flarum/utils/humanTime';
|
|
||||||
import extractText from 'flarum/utils/extractText';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `PostEdited` component displays information about when and by whom a post
|
|
||||||
* was edited.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `post`
|
|
||||||
*/
|
|
||||||
export default class PostEdited extends Component {
|
|
||||||
view() {
|
|
||||||
const post = this.props.post;
|
|
||||||
const editUser = post.editUser();
|
|
||||||
const title = extractText(app.trans('core.post_edited', {user: editUser, ago: humanTime(post.editTime())}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="PostEdited" title={title}>{icon('pencil')}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
config(isInitialized) {
|
|
||||||
if (isInitialized) return;
|
|
||||||
|
|
||||||
this.$().tooltip();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,198 +0,0 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
|
||||||
import LogInModal from 'flarum/components/LogInModal';
|
|
||||||
import avatar from 'flarum/helpers/avatar';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `SignUpModal` component displays a modal dialog with a singup form.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `username`
|
|
||||||
* - `email`
|
|
||||||
* - `password`
|
|
||||||
*/
|
|
||||||
export default class SignUpModal extends Modal {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the username input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.username = m.prop(this.props.username || '');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the email input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.email = m.prop(this.props.email || '');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the password input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.password = m.prop(this.props.password || '');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The user that has been signed up and that should be welcomed.
|
|
||||||
*
|
|
||||||
* @type {null|User}
|
|
||||||
*/
|
|
||||||
this.welcomeUser = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
return 'Modal--small SignUpModal' + (this.welcomeUser ? ' SignUpModal--success' : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return app.trans('core.sign_up');
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return [
|
|
||||||
<div className="Modal-body">
|
|
||||||
{this.body()}
|
|
||||||
</div>,
|
|
||||||
<div className="Modal-footer">
|
|
||||||
{this.footer()}
|
|
||||||
</div>
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
body() {
|
|
||||||
const body = [(
|
|
||||||
<div className="Form Form--centered">
|
|
||||||
<div className="Form-group">
|
|
||||||
<input className="FormControl" name="username" placeholder={app.trans('core.username')}
|
|
||||||
value={this.username()}
|
|
||||||
onchange={m.withAttr('value', this.username)}
|
|
||||||
disabled={this.loading} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
|
|
||||||
value={this.email()}
|
|
||||||
onchange={m.withAttr('value', this.email)}
|
|
||||||
disabled={this.loading} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
|
|
||||||
value={this.password()}
|
|
||||||
onchange={m.withAttr('value', this.password)}
|
|
||||||
disabled={this.loading} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--primary Button--block',
|
|
||||||
type: 'submit',
|
|
||||||
loading: this.loading,
|
|
||||||
children: app.trans('core.sign_up')
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)];
|
|
||||||
|
|
||||||
if (this.welcomeUser) {
|
|
||||||
const user = this.welcomeUser;
|
|
||||||
const emailProviderName = user.email().split('@')[1];
|
|
||||||
|
|
||||||
const fadeIn = (element, isInitialized) => {
|
|
||||||
if (isInitialized) return;
|
|
||||||
$(element).hide().fadeIn();
|
|
||||||
};
|
|
||||||
|
|
||||||
body.push(
|
|
||||||
<div className="SignUpModal-welcome" style={{background: user.color()}} config={fadeIn}>
|
|
||||||
<div className="darkenBackground">
|
|
||||||
<div className="container">
|
|
||||||
{avatar(user)}
|
|
||||||
<h3>{app.trans('core.welcome_user', {user})}</h3>
|
|
||||||
|
|
||||||
{!user.isActivated() ? [
|
|
||||||
<p>{app.trans('core.confirmation_email_sent', {email: <strong>{user.email()}</strong>})}</p>,
|
|
||||||
<p>
|
|
||||||
<a href={`http://${emailProviderName}`} className="Button Button--primary" target="_blank">
|
|
||||||
{app.trans('core.go_to', {location: emailProviderName})}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
] : (
|
|
||||||
<p>
|
|
||||||
<button className="Button Button--primary" onclick={this.hide.bind(this)}>
|
|
||||||
{app.trans('core.dismiss')}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer() {
|
|
||||||
return [
|
|
||||||
<p className="SignUpModal-logIn">
|
|
||||||
{app.trans('core.before_log_in_link')}{' '}
|
|
||||||
<a onclick={this.logIn.bind(this)}>{app.trans('core.log_in')}</a>
|
|
||||||
</p>
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the log in modal, prefilling it with an email/username/password if
|
|
||||||
* the user has entered one.
|
|
||||||
*/
|
|
||||||
logIn() {
|
|
||||||
const props = {
|
|
||||||
email: this.email() || this.username(),
|
|
||||||
password: this.password()
|
|
||||||
};
|
|
||||||
|
|
||||||
app.modal.show(new LogInModal(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
onready() {
|
|
||||||
if (this.props.username) {
|
|
||||||
this.$('[name=email]').select();
|
|
||||||
} else {
|
|
||||||
super.onready();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
const data = this.submitData();
|
|
||||||
|
|
||||||
app.store.createRecord('users').save(data).then(
|
|
||||||
user => {
|
|
||||||
this.welcomeUser = user;
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
},
|
|
||||||
response => {
|
|
||||||
this.loading = false;
|
|
||||||
this.handleErrors(response.errors);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
submitData() {
|
|
||||||
return {
|
|
||||||
username: this.username(),
|
|
||||||
email: this.email(),
|
|
||||||
password: this.password()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,29 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import humanTime from 'flarum/helpers/humanTime';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays information about a the first or last post in a discussion.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `discussion`
|
|
||||||
* - `lastPost`
|
|
||||||
*/
|
|
||||||
export default class TerminalPost extends Component {
|
|
||||||
view() {
|
|
||||||
const discussion = this.props.discussion;
|
|
||||||
const lastPost = this.props.lastPost && discussion.repliesCount();
|
|
||||||
|
|
||||||
const user = discussion[lastPost ? 'lastUser' : 'startUser']();
|
|
||||||
const time = discussion[lastPost ? 'lastTime' : 'startTime']();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{app.trans('core.discussion_' + (lastPost ? 'replied' : 'started'), {
|
|
||||||
user,
|
|
||||||
ago: humanTime(time)
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,104 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
|
||||||
import classList from 'flarum/utils/classList';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `UserBio` component displays a user's bio, optionally letting the user
|
|
||||||
* edit it.
|
|
||||||
*/
|
|
||||||
export default class UserBio extends Component {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the bio is currently being edited.
|
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
|
||||||
this.editing = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the bio is currently being saved.
|
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
const user = this.props.user;
|
|
||||||
let content;
|
|
||||||
|
|
||||||
if (this.editing) {
|
|
||||||
content = <textarea className="FormControl" placeholder={app.trans('core.bio_placeholder')} rows="3" value={user.bio()}/>;
|
|
||||||
} else {
|
|
||||||
let subContent;
|
|
||||||
|
|
||||||
if (this.loading) {
|
|
||||||
subContent = <p className="UserBio-placeholder">{LoadingIndicator.component({size: 'tiny'})}</p>;
|
|
||||||
} else {
|
|
||||||
const bioHtml = user.bioHtml();
|
|
||||||
|
|
||||||
if (bioHtml) {
|
|
||||||
subContent = m.trust(bioHtml);
|
|
||||||
} else if (this.props.editable) {
|
|
||||||
subContent = <p className="UserBio-placeholder">{app.trans('core.bio_placeholder')}</p>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content = <div className="UserBio-content">{subContent}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'UserBio ' + classList({
|
|
||||||
editable: this.props.editable,
|
|
||||||
editing: this.editing
|
|
||||||
})}
|
|
||||||
onclick={this.edit.bind(this)}>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit the bio.
|
|
||||||
*/
|
|
||||||
edit() {
|
|
||||||
if (!this.props.editable) return;
|
|
||||||
|
|
||||||
this.editing = true;
|
|
||||||
m.redraw();
|
|
||||||
|
|
||||||
const bio = this;
|
|
||||||
const save = function(e) {
|
|
||||||
if (e.shiftKey) return;
|
|
||||||
e.preventDefault();
|
|
||||||
bio.save($(this).val());
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$('textarea').focus()
|
|
||||||
.bind('blur', save)
|
|
||||||
.bind('keydown', 'return', save);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the bio.
|
|
||||||
*
|
|
||||||
* @param {String} value
|
|
||||||
*/
|
|
||||||
save(value) {
|
|
||||||
const user = this.props.user;
|
|
||||||
|
|
||||||
if (user.bio() !== value) {
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
user.save({bio: value}).then(() => {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editing = false;
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,36 +0,0 @@
|
|||||||
import highlight from 'flarum/helpers/highlight';
|
|
||||||
import avatar from 'flarum/helpers/avatar';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `UsersSearchSource` finds and displays user search results in the search
|
|
||||||
* dropdown.
|
|
||||||
*
|
|
||||||
* @implements SearchSource
|
|
||||||
*/
|
|
||||||
export default class UsersSearchResults {
|
|
||||||
search(query) {
|
|
||||||
return app.store.find('users', {
|
|
||||||
filter: {q: query},
|
|
||||||
page: {limit: 5}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
view(query) {
|
|
||||||
const results = app.store.all('users')
|
|
||||||
.filter(user => user.username().toLowerCase().substr(0, query.length) === query);
|
|
||||||
|
|
||||||
if (!results.length) return '';
|
|
||||||
|
|
||||||
return [
|
|
||||||
<li className="Dropdown-header">{app.trans('core.users')}</li>,
|
|
||||||
results.map(user => (
|
|
||||||
<li className="UserSearchResult" data-index={'users' + user.id()}>
|
|
||||||
<a href={app.route.user(user)} config={m.route}>
|
|
||||||
{avatar(user)}
|
|
||||||
{highlight(user.username(), query)}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,82 +0,0 @@
|
|||||||
/*global FastClick*/
|
|
||||||
|
|
||||||
import ScrollListener from 'flarum/utils/ScrollListener';
|
|
||||||
import Pane from 'flarum/utils/Pane';
|
|
||||||
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 Composer from 'flarum/components/Composer';
|
|
||||||
import ModalManager from 'flarum/components/ModalManager';
|
|
||||||
import AlertManager from 'flarum/components/AlertManager';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `boot` initializer boots up the forum app. It initializes some app
|
|
||||||
* globals, mounts components to the page, and begins routing.
|
|
||||||
*
|
|
||||||
* @param {ForumApp} app
|
|
||||||
*/
|
|
||||||
export default function boot(app) {
|
|
||||||
// Get the configured default route and update that route's path to be '/'.
|
|
||||||
// Push the homepage as the first route, so that the user will always be
|
|
||||||
// able to click on the 'back' button to go home, regardless of which page
|
|
||||||
// they started on.
|
|
||||||
const defaultRoute = app.forum.attribute('defaultRoute');
|
|
||||||
|
|
||||||
for (const i in app.routes) {
|
|
||||||
if (app.routes[i].path === defaultRoute) {
|
|
||||||
app.routes[i].path = '/';
|
|
||||||
app.history.push(i, '/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
app.pane = new Pane(document.getElementById('app'));
|
|
||||||
app.drawer = new Drawer();
|
|
||||||
app.composer = m.mount(document.getElementById('composer'), Composer.component());
|
|
||||||
app.modal = m.mount(document.getElementById('modal'), ModalManager.component());
|
|
||||||
app.alerts = m.mount(document.getElementById('alerts'), AlertManager.component());
|
|
||||||
|
|
||||||
const basePath = app.forum.attribute('basePath');
|
|
||||||
m.route.mode = 'pathname';
|
|
||||||
m.route(
|
|
||||||
document.getElementById('content'),
|
|
||||||
basePath + '/',
|
|
||||||
mapRoutes(app.routes, basePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
m.endComputation();
|
|
||||||
|
|
||||||
// Route the home link back home when clicked. We do not want it to register
|
|
||||||
// if the user is opening it in a new tab, however.
|
|
||||||
$('#home-link').click(e => {
|
|
||||||
if (e.ctrlKey || e.metaKey || e.which === 2) return;
|
|
||||||
e.preventDefault();
|
|
||||||
app.history.home();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
// Initialize FastClick, which makes links and buttons much more responsive on
|
|
||||||
// touch devices.
|
|
||||||
$(() => FastClick.attach(document.body));
|
|
||||||
|
|
||||||
app.booted = true;
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
import CommentPost from 'flarum/components/CommentPost';
|
|
||||||
import DiscussionRenamedPost from 'flarum/components/DiscussionRenamedPost';
|
|
||||||
import PostedActivity from 'flarum/components/PostedActivity';
|
|
||||||
import JoinedActivity from 'flarum/components/JoinedActivity';
|
|
||||||
import DiscussionRenamedNotification from 'flarum/components/DiscussionRenamedNotification';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `components` initializer registers components to display the default post
|
|
||||||
* types, activity types, and notifications type with the application.
|
|
||||||
*
|
|
||||||
* @param {ForumApp} app
|
|
||||||
*/
|
|
||||||
export default function components(app) {
|
|
||||||
app.postComponents.comment = CommentPost;
|
|
||||||
app.postComponents.discussionRenamed = DiscussionRenamedPost;
|
|
||||||
|
|
||||||
app.notificationComponents.discussionRenamed = DiscussionRenamedNotification;
|
|
||||||
}
|
|
@@ -1,138 +0,0 @@
|
|||||||
import EditPostComposer from 'flarum/components/EditPostComposer';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import Separator from 'flarum/components/Separator';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `PostControls` utility constructs a list of buttons for a post which
|
|
||||||
* perform actions on it.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
/**
|
|
||||||
* Get a list of controls for a post.
|
|
||||||
*
|
|
||||||
* @param {Post} post
|
|
||||||
* @param {*} context The parent component under which the controls menu will
|
|
||||||
* be displayed.
|
|
||||||
* @return {ItemList}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
controls(post, context) {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
['user', 'moderation', 'destructive'].forEach(section => {
|
|
||||||
const controls = this[section + 'Controls'](post, context).toArray();
|
|
||||||
if (controls.length) {
|
|
||||||
controls.forEach(item => items.add(item.itemName, item));
|
|
||||||
items.add(section + 'Separator', Separator.component());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get controls for a post pertaining to the current user (e.g. report).
|
|
||||||
*
|
|
||||||
* @param {Post} post
|
|
||||||
* @param {*} context The parent component under which the controls menu will
|
|
||||||
* be displayed.
|
|
||||||
* @return {ItemList}
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
userControls() {
|
|
||||||
return new ItemList();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get controls for a post pertaining to moderation (e.g. edit).
|
|
||||||
*
|
|
||||||
* @param {Post} post
|
|
||||||
* @param {*} context The parent component under which the controls menu will
|
|
||||||
* be displayed.
|
|
||||||
* @return {ItemList}
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
moderationControls(post) {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
if (post.contentType() === 'comment' && post.canEdit()) {
|
|
||||||
if (post.isHidden()) {
|
|
||||||
items.add('restore', Button.component({
|
|
||||||
icon: 'reply',
|
|
||||||
children: app.trans('core.restore'),
|
|
||||||
onclick: this.restoreAction.bind(post)
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
items.add('edit', Button.component({
|
|
||||||
icon: 'pencil',
|
|
||||||
children: app.trans('core.edit'),
|
|
||||||
onclick: this.editAction.bind(post)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get controls for a post that are destructive (e.g. delete).
|
|
||||||
*
|
|
||||||
* @param {Post} post
|
|
||||||
* @param {*} context The parent component under which the controls menu will
|
|
||||||
* be displayed.
|
|
||||||
* @return {ItemList}
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
destructiveControls(post) {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
if (post.contentType() === 'comment' && !post.isHidden() && post.canEdit()) {
|
|
||||||
items.add('hide', Button.component({
|
|
||||||
icon: 'times',
|
|
||||||
children: app.trans('core.delete'),
|
|
||||||
onclick: this.hideAction.bind(post)
|
|
||||||
}));
|
|
||||||
} else if (post.number() !== 1 && (post.contentType() !== 'comment' || post.isHidden()) && post.canDelete()) {
|
|
||||||
items.add('delete', Button.component({
|
|
||||||
icon: 'times',
|
|
||||||
children: app.trans('core.delete_forever'),
|
|
||||||
onclick: this.deleteAction.bind(post)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the composer to edit a post.
|
|
||||||
*/
|
|
||||||
editAction() {
|
|
||||||
app.composer.load(new EditPostComposer({ post: this }));
|
|
||||||
app.composer.show();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide a post.
|
|
||||||
*/
|
|
||||||
hideAction() {
|
|
||||||
this.save({ isHidden: true });
|
|
||||||
this.pushAttributes({ hideTime: new Date(), hideUser: app.session.user });
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore a post.
|
|
||||||
*/
|
|
||||||
restoreAction() {
|
|
||||||
this.save({ isHidden: false });
|
|
||||||
this.pushAttributes({ hideTime: null, hideUser: null });
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a post.
|
|
||||||
*/
|
|
||||||
deleteAction() {
|
|
||||||
this.delete();
|
|
||||||
this.discussion().removePost(this.id());
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Setup the sidebar DOM element to be affixed to the top of the viewport
|
|
||||||
* using Bootstrap's affix plugin.
|
|
||||||
*
|
|
||||||
* @param {DOMElement} element
|
|
||||||
* @param {Boolean} isInitialized
|
|
||||||
*/
|
|
||||||
export default function affixSidebar(element, isInitialized) {
|
|
||||||
if (isInitialized) return;
|
|
||||||
|
|
||||||
const $sidebar = $(element);
|
|
||||||
const $header = $('#header');
|
|
||||||
const $footer = $('#footer');
|
|
||||||
|
|
||||||
// Don't affix the sidebar if it is taller than the viewport (otherwise
|
|
||||||
// there would be no way to scroll through its content).
|
|
||||||
if ($sidebar.outerHeight(true) > $(window).height() - $header.outerHeight(true)) return;
|
|
||||||
|
|
||||||
$sidebar.find('> ul').affix({
|
|
||||||
offset: {
|
|
||||||
top: () => $sidebar.offset().top - $header.outerHeight(true) - parseInt($sidebar.css('margin-top'), 10),
|
|
||||||
bottom: () => this.bottom = $footer.outerHeight(true)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
276
js/lib/App.js
276
js/lib/App.js
@@ -1,276 +0,0 @@
|
|||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
import Alert from 'flarum/components/Alert';
|
|
||||||
import Translator from 'flarum/Translator';
|
|
||||||
import extract from 'flarum/utils/extract';
|
|
||||||
import patchMithril from 'flarum/utils/patchMithril';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `App` class provides a container for an application, as well as various
|
|
||||||
* utilities for the rest of the app to use.
|
|
||||||
*/
|
|
||||||
export default class App {
|
|
||||||
constructor() {
|
|
||||||
patchMithril(window);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The forum model for this application.
|
|
||||||
*
|
|
||||||
* @type {Forum}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.forum = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of routes, keyed by a unique route name. Each route is an object
|
|
||||||
* containing the following properties:
|
|
||||||
*
|
|
||||||
* - `path` The path that the route is accessed at.
|
|
||||||
* - `component` The Mithril component to render when this route is active.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* app.routes.discussion = {path: '/d/:id', component: DiscussionPage.component()};
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.routes = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing data to preload into the application.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
* @property {Object} preload.data An array of resource objects to preload
|
|
||||||
* into the data store.
|
|
||||||
* @property {Object} preload.document An API response document to be used
|
|
||||||
* by the route that is first activated.
|
|
||||||
* @property {Object} preload.session A response from the /api/token
|
|
||||||
* endpoint containing the session's authentication token and user ID.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.preload = {
|
|
||||||
data: null,
|
|
||||||
document: null,
|
|
||||||
session: null
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ordered list of initializers to bootstrap the application.
|
|
||||||
*
|
|
||||||
* @type {ItemList}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.initializers = new ItemList();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The app's session.
|
|
||||||
*
|
|
||||||
* @type {Session}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.session = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The app's translator.
|
|
||||||
*
|
|
||||||
* @type {Translator}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.translator = new Translator();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The app's data store.
|
|
||||||
*
|
|
||||||
* @type {Store}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.store = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A local cache that can be used to store data at the application level, so
|
|
||||||
* that is persists between different routes.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.cache = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the app has been booted.
|
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.booted = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An Alert that was shown as a result of an AJAX request error. If present,
|
|
||||||
* it will be dismissed on the next successful request.
|
|
||||||
*
|
|
||||||
* @type {null|Alert}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this.requestError = null;
|
|
||||||
|
|
||||||
this.title = '';
|
|
||||||
this.titleCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boot the application by running all of the registered initializers.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
boot() {
|
|
||||||
this.initializers.toArray().forEach(initializer => initializer(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the API response document that has been preloaded into the application.
|
|
||||||
*
|
|
||||||
* @return {Object|null}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
preloadedDocument() {
|
|
||||||
if (app.preload.document) {
|
|
||||||
const results = app.store.pushPayload(app.preload.document);
|
|
||||||
app.preload.document = null;
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the <title> of the page.
|
|
||||||
*
|
|
||||||
* @param {String} title
|
|
||||||
* @param {Boolean} [separator] Whether or not to separate the given title and
|
|
||||||
* the forum's title.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
setTitle(title) {
|
|
||||||
this.title = title;
|
|
||||||
this.updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a number to display in the <title> of the page.
|
|
||||||
*
|
|
||||||
* @param {Integer} count
|
|
||||||
*/
|
|
||||||
setTitleCount(count) {
|
|
||||||
this.titleCount = count;
|
|
||||||
this.updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTitle() {
|
|
||||||
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') +
|
|
||||||
(this.title ? this.title + ' - ' : '') +
|
|
||||||
this.forum.attribute('title');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an AJAX request, handling any low-level errors that may occur.
|
|
||||||
*
|
|
||||||
* @see https://lhorie.github.io/mithril/mithril.request.html
|
|
||||||
* @param {Object} options
|
|
||||||
* @return {Promise}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
request(options) {
|
|
||||||
// Set some default options if they haven't been overridden. We want to
|
|
||||||
// authenticate all requests with the session token. We also want all
|
|
||||||
// requests to run asynchronously in the background, so that they don't
|
|
||||||
// prevent redraws from occurring.
|
|
||||||
options.config = options.config || this.session.authorize.bind(this.session);
|
|
||||||
options.background = options.background || true;
|
|
||||||
|
|
||||||
// When we deserialize JSON data, if for some reason the server has provided
|
|
||||||
// a dud response, we don't want the application to crash. We'll show an
|
|
||||||
// error message to the user instead.
|
|
||||||
options.deserialize = options.deserialize || (responseText => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(responseText);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Oops! Something went wrong on the server. Please reload the page and try again.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// When extracting the data from the response, we can check the server
|
|
||||||
// response code and show an error message to the user if something's gone
|
|
||||||
// awry.
|
|
||||||
const original = options.extract;
|
|
||||||
options.extract = xhr => {
|
|
||||||
const status = xhr.status;
|
|
||||||
|
|
||||||
if (status >= 500 && status <= 599) {
|
|
||||||
throw new Error('Oops! Something went wrong on the server. Please reload the page and try again.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (original) {
|
|
||||||
return original(xhr.responseText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return xhr.responseText.length > 0 ? xhr.responseText : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.alerts.dismiss(this.requestError);
|
|
||||||
|
|
||||||
// Now make the request. If it's a failure, inspect the error that was
|
|
||||||
// returned and show an alert containing its contents.
|
|
||||||
return m.request(options).then(null, response => {
|
|
||||||
if (response instanceof Error) {
|
|
||||||
this.alerts.show(this.requestError = new Alert({
|
|
||||||
type: 'error',
|
|
||||||
children: response.message
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw response;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show alert error messages for each error returned in an API response.
|
|
||||||
*
|
|
||||||
* @param {Array} errors
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
alertErrors(errors) {
|
|
||||||
errors.forEach(error => {
|
|
||||||
this.alerts.show(new Alert({
|
|
||||||
type: 'error',
|
|
||||||
children: error.detail
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a URL to the route with the given name.
|
|
||||||
*
|
|
||||||
* @param {String} name
|
|
||||||
* @param {Object} params
|
|
||||||
* @return {String}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
route(name, params = {}) {
|
|
||||||
const url = this.routes[name].path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
|
|
||||||
const queryString = m.route.buildQueryString(params);
|
|
||||||
const prefix = m.route.mode === 'pathname' ? app.forum.attribute('basePath') : '';
|
|
||||||
|
|
||||||
return prefix + url + (queryString ? '?' + queryString : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shortcut to translate the given key.
|
|
||||||
*
|
|
||||||
* @param {String} key
|
|
||||||
* @param {Object} input
|
|
||||||
* @return {String}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
trans(key, input) {
|
|
||||||
return this.translator.trans(key, input);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,86 +0,0 @@
|
|||||||
import User from 'flarum/models/User';
|
|
||||||
import username from 'flarum/helpers/username';
|
|
||||||
import extractText from 'flarum/utils/extractText';
|
|
||||||
import extract from 'flarum/utils/extract';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Translator` class translates strings using the loaded localization.
|
|
||||||
*/
|
|
||||||
export default class Translator {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* A map of translation keys to their translated values.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.translations = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the key of a translation that should be used for the given count.
|
|
||||||
* The default implementation is for English plurals. It should be overridden
|
|
||||||
* by a locale's JavaScript file if necessary.
|
|
||||||
*
|
|
||||||
* @param {Integer} count
|
|
||||||
* @return {String}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
plural(count) {
|
|
||||||
return count === 1 ? 'one' : 'other';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a string.
|
|
||||||
*
|
|
||||||
* @param {String} key
|
|
||||||
* @param {Object} input
|
|
||||||
* @param {VirtualElement} fallback
|
|
||||||
* @return {VirtualElement}
|
|
||||||
*/
|
|
||||||
trans(key, input = {}, fallback) {
|
|
||||||
const parts = key.split('.');
|
|
||||||
let translation = this.translations;
|
|
||||||
|
|
||||||
// Drill down into the translation tree to find the translation for this
|
|
||||||
// key.
|
|
||||||
parts.forEach(part => {
|
|
||||||
translation = translation && translation[part];
|
|
||||||
});
|
|
||||||
|
|
||||||
// If this translation has multiple options and a 'count' has been provided
|
|
||||||
// in the input, we'll work out which option to choose using the `plural`
|
|
||||||
// method.
|
|
||||||
if (translation && typeof translation === 'object' && typeof input.count !== 'undefined') {
|
|
||||||
translation = translation[this.plural(extractText(input.count))];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've been given a user model as one of the input parameters, then
|
|
||||||
// we'll extract the username and use that for the translation. In the
|
|
||||||
// future there should be a hook here to inspect the user and change the
|
|
||||||
// translation key. This will allow a gender property to determine which
|
|
||||||
// translation key is used.
|
|
||||||
if ('user' in input) {
|
|
||||||
const user = extract(input, 'user');
|
|
||||||
|
|
||||||
if (!input.username) input.username = username(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've found the appropriate translation string, then we'll sub in the
|
|
||||||
// input.
|
|
||||||
if (typeof translation === 'string') {
|
|
||||||
translation = translation.split(new RegExp('({[^}]+})', 'gi'));
|
|
||||||
|
|
||||||
translation.forEach((part, i) => {
|
|
||||||
const match = part.match(/^{(.+)}$/i);
|
|
||||||
if (match) {
|
|
||||||
translation[i] = input[match[1]];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return translation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback || [key];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,27 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `LoadingIndicator` component displays a loading spinner with spin.js. It
|
|
||||||
* may have the following special props:
|
|
||||||
*
|
|
||||||
* - `size` The spin.js size preset to use. Defaults to 'small'.
|
|
||||||
*
|
|
||||||
* All other props will be assigned as attributes on the element.
|
|
||||||
*/
|
|
||||||
export default class LoadingIndicator extends Component {
|
|
||||||
view() {
|
|
||||||
const attrs = Object.assign({}, this.props);
|
|
||||||
|
|
||||||
attrs.className = 'LoadingIndicator ' + (attrs.className || '');
|
|
||||||
delete attrs.size;
|
|
||||||
|
|
||||||
return <div {...attrs}>{m.trust(' ')}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
config() {
|
|
||||||
const size = this.props.size || 'small';
|
|
||||||
|
|
||||||
$.fn.spin.presets[size].zIndex = 'auto';
|
|
||||||
this.$().spin(size);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* The `icon` helper displays a FontAwesome icon. The fa-fw class is applied.
|
|
||||||
*
|
|
||||||
* @param {String} name The name of the icon class, without the `fa-` prefix.
|
|
||||||
* @param {Object} attrs Any other attributes to apply.
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
export default function icon(name, attrs = {}) {
|
|
||||||
attrs.className = 'icon fa fa-fw fa-' + name + ' ' + (attrs.className || '');
|
|
||||||
|
|
||||||
return <i {...attrs}/>;
|
|
||||||
}
|
|
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* The `punctuate` helper formats a list of strings (e.g. names) to read
|
|
||||||
* fluently in the application's locale.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* punctuate(['Toby', 'Franz', 'Dominion'])
|
|
||||||
* // Toby, Franz, and Dominion
|
|
||||||
*
|
|
||||||
* @param {Array} items
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
export default function punctuate(items) {
|
|
||||||
const punctuated = [];
|
|
||||||
|
|
||||||
// FIXME: update to use translation
|
|
||||||
items.forEach((item, i) => {
|
|
||||||
punctuated.push(item);
|
|
||||||
|
|
||||||
// If this item is not the last one, then we will follow it with some
|
|
||||||
// punctuation. If the list is more than 2 items long, we'll add a comma.
|
|
||||||
// And if this is the second-to-last item, we'll add 'and'.
|
|
||||||
if (i < items.length - 1) {
|
|
||||||
punctuated.push((items.length > 2 ? ', ' : '') + (i === items.length - 2 ? ' and ' : ''));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return punctuated;
|
|
||||||
};
|
|
@@ -1,24 +0,0 @@
|
|||||||
import Session from 'flarum/Session';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `preload` initializer creates the application session and preloads it
|
|
||||||
* with data that has been set on the application's `preload` property. It also
|
|
||||||
* preloads any data on the application's `preload` property into the store.
|
|
||||||
* Finally, it sets the application's `forum` instance to the one that was
|
|
||||||
* preloaded.
|
|
||||||
*
|
|
||||||
* `app.preload.session` should be the same as the response from the /api/token
|
|
||||||
* endpoint: it should contain `token` and `userId` keys.
|
|
||||||
*
|
|
||||||
* @param {App} app
|
|
||||||
*/
|
|
||||||
export default function preload(app) {
|
|
||||||
app.store.pushPayload({data: app.preload.data});
|
|
||||||
|
|
||||||
app.forum = app.store.getById('forums', 1);
|
|
||||||
|
|
||||||
app.session = new Session(
|
|
||||||
app.preload.session.token,
|
|
||||||
app.store.getById('users', app.preload.session.userId)
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,26 +0,0 @@
|
|||||||
import Store from 'flarum/Store';
|
|
||||||
import Forum from 'flarum/models/Forum';
|
|
||||||
import User from 'flarum/models/User';
|
|
||||||
import Discussion from 'flarum/models/Discussion';
|
|
||||||
import Post from 'flarum/models/Post';
|
|
||||||
import Group from 'flarum/models/Group';
|
|
||||||
import Activity from 'flarum/models/Activity';
|
|
||||||
import Notification from 'flarum/models/Notification';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `store` initializer creates the application's data store and registers
|
|
||||||
* the default resource types to their models.
|
|
||||||
*
|
|
||||||
* @param {App} app
|
|
||||||
*/
|
|
||||||
export default function store(app) {
|
|
||||||
app.store = new Store({
|
|
||||||
forums: Forum,
|
|
||||||
users: User,
|
|
||||||
discussions: Discussion,
|
|
||||||
posts: Post,
|
|
||||||
groups: Group,
|
|
||||||
activity: Activity,
|
|
||||||
notifications: Notification
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,90 +0,0 @@
|
|||||||
import Model from 'flarum/Model';
|
|
||||||
import mixin from 'flarum/utils/mixin';
|
|
||||||
import computed from 'flarum/utils/computed';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
import { slug } from 'flarum/utils/string';
|
|
||||||
|
|
||||||
export default class Discussion extends mixin(Model, {
|
|
||||||
title: Model.attribute('title'),
|
|
||||||
slug: computed('title', slug),
|
|
||||||
|
|
||||||
startTime: Model.attribute('startTime', Model.transformDate),
|
|
||||||
startUser: Model.hasOne('startUser'),
|
|
||||||
startPost: Model.hasOne('startPost'),
|
|
||||||
|
|
||||||
lastTime: Model.attribute('lastTime', Model.transformDate),
|
|
||||||
lastUser: Model.hasOne('lastUser'),
|
|
||||||
lastPost: Model.hasOne('lastPost'),
|
|
||||||
lastPostNumber: Model.attribute('lastPostNumber'),
|
|
||||||
|
|
||||||
commentsCount: Model.attribute('commentsCount'),
|
|
||||||
repliesCount: computed('commentsCount', commentsCount => Math.max(0, commentsCount - 1)),
|
|
||||||
posts: Model.hasMany('posts'),
|
|
||||||
relevantPosts: Model.hasMany('relevantPosts'),
|
|
||||||
|
|
||||||
readTime: Model.attribute('readTime', Model.transformDate),
|
|
||||||
readNumber: Model.attribute('readNumber'),
|
|
||||||
isUnread: computed('unreadCount', unreadCount => !!unreadCount),
|
|
||||||
isRead: computed('unreadCount', unreadCount => app.session.user && !unreadCount),
|
|
||||||
|
|
||||||
canReply: Model.attribute('canReply'),
|
|
||||||
canRename: Model.attribute('canRename'),
|
|
||||||
canDelete: Model.attribute('canDelete')
|
|
||||||
}) {
|
|
||||||
/**
|
|
||||||
* Remove a post from the discussion's posts relationship.
|
|
||||||
*
|
|
||||||
* @param {Integer} id The ID of the post to remove.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
removePost(id) {
|
|
||||||
const relationships = this.data.relationships;
|
|
||||||
const posts = relationships && relationships.posts;
|
|
||||||
|
|
||||||
if (posts) {
|
|
||||||
posts.data.some((data, i) => {
|
|
||||||
if (id === data.id) {
|
|
||||||
posts.data.splice(i, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the estimated number of unread posts in this discussion for the current
|
|
||||||
* user.
|
|
||||||
*
|
|
||||||
* @return {Integer}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
unreadCount() {
|
|
||||||
const user = app.session.user;
|
|
||||||
|
|
||||||
if (user && user.readTime() < this.lastTime()) {
|
|
||||||
return Math.max(0, this.lastPostNumber() - (this.readNumber() || 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the Badge components that apply to this discussion.
|
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
badges() {
|
|
||||||
return new ItemList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of all of the post IDs in this discussion.
|
|
||||||
*
|
|
||||||
* @return {Array}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
postIds() {
|
|
||||||
return this.data.relationships.posts.data.map(link => link.id);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
import Model from 'flarum/Model';
|
|
||||||
import mixin from 'flarum/utils/mixin';
|
|
||||||
|
|
||||||
export default class Forum extends mixin(Model, {
|
|
||||||
canStartDiscussion: Model.attribute('canStartDiscussion')
|
|
||||||
}) {
|
|
||||||
apiEndpoint() {
|
|
||||||
return '/forum';
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
import Model from 'flarum/Model';
|
|
||||||
import mixin from 'flarum/utils/mixin';
|
|
||||||
import computed from 'flarum/utils/computed';
|
|
||||||
|
|
||||||
export default class Notification extends mixin(Model, {
|
|
||||||
contentType: Model.attribute('contentType'),
|
|
||||||
subjectId: Model.attribute('subjectId'),
|
|
||||||
content: Model.attribute('content'),
|
|
||||||
time: Model.attribute('time', Model.date),
|
|
||||||
|
|
||||||
isRead: Model.attribute('isRead'),
|
|
||||||
unreadCount: Model.attribute('unreadCount'),
|
|
||||||
additionalUnreadCount: computed('unreadCount', unreadCount => Math.max(0, unreadCount - 1)),
|
|
||||||
|
|
||||||
user: Model.hasOne('user'),
|
|
||||||
sender: Model.hasOne('sender'),
|
|
||||||
subject: Model.hasOne('subject')
|
|
||||||
}) {}
|
|
@@ -1,27 +0,0 @@
|
|||||||
import Model from 'flarum/Model';
|
|
||||||
import mixin from 'flarum/utils/mixin';
|
|
||||||
import computed from 'flarum/utils/computed';
|
|
||||||
import { getPlainContent } from 'flarum/utils/string';
|
|
||||||
|
|
||||||
export default class Post extends mixin(Model, {
|
|
||||||
number: Model.attribute('number'),
|
|
||||||
discussion: Model.hasOne('discussion'),
|
|
||||||
|
|
||||||
time: Model.attribute('time', Model.transformDate),
|
|
||||||
user: Model.hasOne('user'),
|
|
||||||
contentType: Model.attribute('contentType'),
|
|
||||||
content: Model.attribute('content'),
|
|
||||||
contentHtml: Model.attribute('contentHtml'),
|
|
||||||
contentPlain: computed('contentHtml', getPlainContent),
|
|
||||||
|
|
||||||
editTime: Model.attribute('editTime', Model.transformDate),
|
|
||||||
editUser: Model.hasOne('editUser'),
|
|
||||||
isEdited: computed('editTime', editTime => !!editTime),
|
|
||||||
|
|
||||||
hideTime: Model.attribute('hideTime', Model.transformDate),
|
|
||||||
hideUser: Model.hasOne('hideUser'),
|
|
||||||
isHidden: computed('hideTime', hideTime => !!hideTime),
|
|
||||||
|
|
||||||
canEdit: Model.attribute('canEdit'),
|
|
||||||
canDelete: Model.attribute('canDelete')
|
|
||||||
}) {}
|
|
@@ -1,71 +0,0 @@
|
|||||||
class Item {
|
|
||||||
constructor(content, priority) {
|
|
||||||
this.content = content;
|
|
||||||
this.priority = priority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `ItemList` class collects items and then arranges them into an array
|
|
||||||
* by priority.
|
|
||||||
*/
|
|
||||||
export default class ItemList {
|
|
||||||
/**
|
|
||||||
* Add an item to the list.
|
|
||||||
*
|
|
||||||
* @param {String} key A unique key for the item.
|
|
||||||
* @param {*} content The item's content.
|
|
||||||
* @param {Integer} [priority] The priority of the item. Items with a higher
|
|
||||||
* priority will be positioned before items with a lower priority.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
add(key, content, priority = 0) {
|
|
||||||
this[key] = new Item(content, priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge another list's items into this one.
|
|
||||||
*
|
|
||||||
* @param {ItemList} items
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
merge(items) {
|
|
||||||
for (const i in items) {
|
|
||||||
if (items.hasOwnProperty(i) && items[i] instanceof Item) {
|
|
||||||
this[i] = items[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the list into an array of item content arranged by priority. Each
|
|
||||||
* item's content will be assigned an `itemName` property equal to the item's
|
|
||||||
* unique key.
|
|
||||||
*
|
|
||||||
* @return {Array}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
toArray() {
|
|
||||||
const items = [];
|
|
||||||
|
|
||||||
for (const i in this) {
|
|
||||||
if (this.hasOwnProperty(i) && this[i] instanceof Item) {
|
|
||||||
this[i].content = Object(this[i].content);
|
|
||||||
|
|
||||||
this[i].content.itemName = i;
|
|
||||||
items.push(this[i]);
|
|
||||||
this[i].key = items.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.sort((a, b) => {
|
|
||||||
if (a.priority === b.priority) {
|
|
||||||
return a.key - b.key;
|
|
||||||
} else if (a.priority > b.priority) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}).map(item => item.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
4943
js/package-lock.json
generated
Normal file
4943
js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
js/package.json
Normal file
26
js/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "@flarum/core",
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^3.4.1",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"color-thief-browser": "^2.0.2",
|
||||||
|
"expose-loader": "^0.7.5",
|
||||||
|
"flarum-webpack-config": "0.1.0-beta.10",
|
||||||
|
"jquery": "^3.4.1",
|
||||||
|
"jquery.hotkeys": "^0.1.0",
|
||||||
|
"lodash-es": "^4.17.14",
|
||||||
|
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
|
||||||
|
"mithril": "^0.2.8",
|
||||||
|
"moment": "^2.22.2",
|
||||||
|
"punycode": "^2.1.1",
|
||||||
|
"spin.js": "^3.1.0",
|
||||||
|
"webpack": "^4.26.0",
|
||||||
|
"webpack-cli": "^3.1.2",
|
||||||
|
"webpack-merge": "^4.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack --mode development --watch",
|
||||||
|
"build": "webpack --mode production"
|
||||||
|
}
|
||||||
|
}
|
63
js/src/admin/AdminApplication.js
Normal file
63
js/src/admin/AdminApplication.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import HeaderPrimary from './components/HeaderPrimary';
|
||||||
|
import HeaderSecondary from './components/HeaderSecondary';
|
||||||
|
import routes from './routes';
|
||||||
|
import Application from '../common/Application';
|
||||||
|
import Navigation from '../common/components/Navigation';
|
||||||
|
import AdminNav from './components/AdminNav';
|
||||||
|
|
||||||
|
export default class AdminApplication extends Application {
|
||||||
|
extensionSettings = {};
|
||||||
|
|
||||||
|
history = {
|
||||||
|
canGoBack: () => true,
|
||||||
|
getPrevious: () => {},
|
||||||
|
backUrl: () => this.forum.attribute('baseUrl'),
|
||||||
|
back: function() {
|
||||||
|
window.location = this.backUrl();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
routes(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
mount() {
|
||||||
|
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());
|
||||||
|
|
||||||
|
m.route.mode = 'hash';
|
||||||
|
super.mount();
|
||||||
|
|
||||||
|
// If an extension has just been enabled, then we will run its settings
|
||||||
|
// callback.
|
||||||
|
const enabled = localStorage.getItem('enabledExtension');
|
||||||
|
if (enabled && this.extensionSettings[enabled]) {
|
||||||
|
this.extensionSettings[enabled]();
|
||||||
|
localStorage.removeItem('enabledExtension');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.hidePosts');
|
||||||
|
}
|
||||||
|
|
||||||
|
return required;
|
||||||
|
};
|
||||||
|
}
|
63
js/src/admin/compat.js
Normal file
63
js/src/admin/compat.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import compat from '../common/compat';
|
||||||
|
|
||||||
|
import saveSettings from './utils/saveSettings';
|
||||||
|
import SettingDropdown from './components/SettingDropdown';
|
||||||
|
import EditCustomFooterModal from './components/EditCustomFooterModal';
|
||||||
|
import SessionDropdown from './components/SessionDropdown';
|
||||||
|
import HeaderPrimary from './components/HeaderPrimary';
|
||||||
|
import AppearancePage from './components/AppearancePage';
|
||||||
|
import Page from './components/Page';
|
||||||
|
import StatusWidget from './components/StatusWidget';
|
||||||
|
import HeaderSecondary from './components/HeaderSecondary';
|
||||||
|
import SettingsModal from './components/SettingsModal';
|
||||||
|
import DashboardWidget from './components/DashboardWidget';
|
||||||
|
import AddExtensionModal from './components/AddExtensionModal';
|
||||||
|
import ExtensionsPage from './components/ExtensionsPage';
|
||||||
|
import AdminLinkButton from './components/AdminLinkButton';
|
||||||
|
import PermissionGrid from './components/PermissionGrid';
|
||||||
|
import Widget from './components/Widget';
|
||||||
|
import MailPage from './components/MailPage';
|
||||||
|
import UploadImageButton from './components/UploadImageButton';
|
||||||
|
import LoadingModal from './components/LoadingModal';
|
||||||
|
import DashboardPage from './components/DashboardPage';
|
||||||
|
import BasicsPage from './components/BasicsPage';
|
||||||
|
import EditCustomHeaderModal from './components/EditCustomHeaderModal';
|
||||||
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
|
import PermissionDropdown from './components/PermissionDropdown';
|
||||||
|
import AdminNav from './components/AdminNav';
|
||||||
|
import EditCustomCssModal from './components/EditCustomCssModal';
|
||||||
|
import EditGroupModal from './components/EditGroupModal';
|
||||||
|
import routes from './routes';
|
||||||
|
import AdminApplication from './AdminApplication';
|
||||||
|
|
||||||
|
export default Object.assign(compat, {
|
||||||
|
'utils/saveSettings': saveSettings,
|
||||||
|
'components/SettingDropdown': SettingDropdown,
|
||||||
|
'components/EditCustomFooterModal': EditCustomFooterModal,
|
||||||
|
'components/SessionDropdown': SessionDropdown,
|
||||||
|
'components/HeaderPrimary': HeaderPrimary,
|
||||||
|
'components/AppearancePage': AppearancePage,
|
||||||
|
'components/Page': Page,
|
||||||
|
'components/StatusWidget': StatusWidget,
|
||||||
|
'components/HeaderSecondary': HeaderSecondary,
|
||||||
|
'components/SettingsModal': SettingsModal,
|
||||||
|
'components/DashboardWidget': DashboardWidget,
|
||||||
|
'components/AddExtensionModal': AddExtensionModal,
|
||||||
|
'components/ExtensionsPage': ExtensionsPage,
|
||||||
|
'components/AdminLinkButton': AdminLinkButton,
|
||||||
|
'components/PermissionGrid': PermissionGrid,
|
||||||
|
'components/Widget': Widget,
|
||||||
|
'components/MailPage': MailPage,
|
||||||
|
'components/UploadImageButton': UploadImageButton,
|
||||||
|
'components/LoadingModal': LoadingModal,
|
||||||
|
'components/DashboardPage': DashboardPage,
|
||||||
|
'components/BasicsPage': BasicsPage,
|
||||||
|
'components/EditCustomHeaderModal': EditCustomHeaderModal,
|
||||||
|
'components/PermissionsPage': PermissionsPage,
|
||||||
|
'components/PermissionDropdown': PermissionDropdown,
|
||||||
|
'components/AdminNav': AdminNav,
|
||||||
|
'components/EditCustomCssModal': EditCustomCssModal,
|
||||||
|
'components/EditGroupModal': EditGroupModal,
|
||||||
|
'routes': routes,
|
||||||
|
'AdminApplication': AdminApplication
|
||||||
|
});
|
30
js/src/admin/components/AddExtensionModal.js
Normal file
30
js/src/admin/components/AddExtensionModal.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 '../../common/components/Modal';
|
||||||
|
|
||||||
|
export default class AddExtensionModal extends Modal {
|
||||||
|
className() {
|
||||||
|
return 'AddExtensionModal Modal--small';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return app.translator.trans('core.admin.add_extension.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return (
|
||||||
|
<div className="Modal-body">
|
||||||
|
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
|
||||||
|
<p>{app.translator.trans('core.admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p>
|
||||||
|
<p>{app.translator.trans('core.admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import LinkButton from 'flarum/components/LinkButton';
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
|
|
||||||
export default class AdminLinkButton extends LinkButton {
|
export default class AdminLinkButton extends LinkButton {
|
||||||
getButtonContent() {
|
getButtonContent() {
|
78
js/src/admin/components/AdminNav.js
Normal file
78
js/src/admin/components/AdminNav.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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 '../../common/Component';
|
||||||
|
import AdminLinkButton from './AdminLinkButton';
|
||||||
|
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
|
||||||
|
export default class AdminNav extends Component {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<SelectDropdown
|
||||||
|
className="AdminNav App-titleControl"
|
||||||
|
buttonClassName="Button">
|
||||||
|
{this.items().toArray()}
|
||||||
|
</SelectDropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: 'far fa-chart-bar',
|
||||||
|
children: app.translator.trans('core.admin.nav.dashboard_button'),
|
||||||
|
description: app.translator.trans('core.admin.nav.dashboard_text')
|
||||||
|
}));
|
||||||
|
|
||||||
|
items.add('basics', AdminLinkButton.component({
|
||||||
|
href: app.route('basics'),
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
children: app.translator.trans('core.admin.nav.basics_button'),
|
||||||
|
description: app.translator.trans('core.admin.nav.basics_text')
|
||||||
|
}));
|
||||||
|
|
||||||
|
items.add('mail', AdminLinkButton.component({
|
||||||
|
href: app.route('mail'),
|
||||||
|
icon: 'fas fa-envelope',
|
||||||
|
children: app.translator.trans('core.admin.nav.email_button'),
|
||||||
|
description: app.translator.trans('core.admin.nav.email_text')
|
||||||
|
}));
|
||||||
|
|
||||||
|
items.add('permissions', AdminLinkButton.component({
|
||||||
|
href: app.route('permissions'),
|
||||||
|
icon: 'fas fa-key',
|
||||||
|
children: app.translator.trans('core.admin.nav.permissions_button'),
|
||||||
|
description: app.translator.trans('core.admin.nav.permissions_text')
|
||||||
|
}));
|
||||||
|
|
||||||
|
items.add('appearance', AdminLinkButton.component({
|
||||||
|
href: app.route('appearance'),
|
||||||
|
icon: 'fas fa-paint-brush',
|
||||||
|
children: app.translator.trans('core.admin.nav.appearance_button'),
|
||||||
|
description: app.translator.trans('core.admin.nav.appearance_text')
|
||||||
|
}));
|
||||||
|
|
||||||
|
items.add('extensions', AdminLinkButton.component({
|
||||||
|
href: app.route('extensions'),
|
||||||
|
icon: 'fas fa-puzzle-piece',
|
||||||
|
children: app.translator.trans('core.admin.nav.extensions_button'),
|
||||||
|
description: app.translator.trans('core.admin.nav.extensions_text')
|
||||||
|
}));
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
132
js/src/admin/components/AppearancePage.js
Normal file
132
js/src/admin/components/AppearancePage.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import Page from './Page';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import Switch from '../../common/components/Switch';
|
||||||
|
import EditCustomCssModal from './EditCustomCssModal';
|
||||||
|
import EditCustomHeaderModal from './EditCustomHeaderModal';
|
||||||
|
import EditCustomFooterModal from './EditCustomFooterModal';
|
||||||
|
import UploadImageButton from './UploadImageButton';
|
||||||
|
import saveSettings from '../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="text" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
||||||
|
<input className="FormControl" type="text" 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_footer_heading')}</legend>
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.custom_footer_text')}
|
||||||
|
</div>
|
||||||
|
{Button.component({
|
||||||
|
className: 'Button',
|
||||||
|
children: app.translator.trans('core.admin.appearance.edit_footer_button'),
|
||||||
|
onclick: () => app.modal.show(new EditCustomFooterModal())
|
||||||
|
})}
|
||||||
|
</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());
|
||||||
|
}
|
||||||
|
}
|
166
js/src/admin/components/BasicsPage.js
Normal file
166
js/src/admin/components/BasicsPage.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import Page from './Page';
|
||||||
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
|
import Select from '../../common/components/Select';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import Alert from '../../common/components/Alert';
|
||||||
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import Switch from '../../common/components/Switch';
|
||||||
|
|
||||||
|
export default class BasicsPage extends Page {
|
||||||
|
init() {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
this.fields = [
|
||||||
|
'forum_title',
|
||||||
|
'forum_description',
|
||||||
|
'default_locale',
|
||||||
|
'show_language_selector',
|
||||||
|
'default_route',
|
||||||
|
'welcome_title',
|
||||||
|
'welcome_message'
|
||||||
|
];
|
||||||
|
this.values = {};
|
||||||
|
|
||||||
|
const settings = app.data.settings;
|
||||||
|
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
|
||||||
|
|
||||||
|
this.localeOptions = {};
|
||||||
|
const locales = app.data.locales;
|
||||||
|
for (const i in locales) {
|
||||||
|
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.values.show_language_selector() !== "number") this.values.show_language_selector(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="BasicsPage">
|
||||||
|
<div className="container">
|
||||||
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
||||||
|
children: [
|
||||||
|
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
||||||
|
children: [
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.basics.forum_description_text')}
|
||||||
|
</div>,
|
||||||
|
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Object.keys(this.localeOptions).length > 1
|
||||||
|
? FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
||||||
|
children: [
|
||||||
|
Select.component({
|
||||||
|
options: this.localeOptions,
|
||||||
|
value: 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'),
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.basics.home_page_heading'),
|
||||||
|
className: 'BasicsPage-homePage',
|
||||||
|
children: [
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.basics.home_page_text')}
|
||||||
|
</div>,
|
||||||
|
this.homePageItems().toArray().map(({path, label}) =>
|
||||||
|
<label className="checkbox">
|
||||||
|
<input type="radio" name="homePage" value={path} checked={this.values.default_route() === path} onclick={m.withAttr('value', this.values.default_route)}/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
||||||
|
className: 'BasicsPage-welcomeBanner',
|
||||||
|
children: [
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.basics.welcome_banner_text')}
|
||||||
|
</div>,
|
||||||
|
<div className="BasicsPage-welcomeBanner-input">
|
||||||
|
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
|
||||||
|
<textarea className="FormControl" value={this.values.welcome_message()} oninput={m.withAttr('value', this.values.welcome_message)}/>
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Button.component({
|
||||||
|
type: 'submit',
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
children: app.translator.trans('core.admin.basics.submit_button'),
|
||||||
|
loading: this.loading,
|
||||||
|
disabled: !this.changed()
|
||||||
|
})}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changed() {
|
||||||
|
return this.fields.some(key => this.values[key]() !== app.data.settings[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) {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
18
js/src/admin/components/DashboardPage.js
Normal file
18
js/src/admin/components/DashboardPage.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Page from './Page';
|
||||||
|
import StatusWidget from './StatusWidget';
|
||||||
|
|
||||||
|
export default class DashboardPage extends Page {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="DashboardPage">
|
||||||
|
<div className="container">
|
||||||
|
{this.availableWidgets()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
availableWidgets() {
|
||||||
|
return [<StatusWidget/>];
|
||||||
|
}
|
||||||
|
}
|
38
js/src/admin/components/DashboardWidget.js
Normal file
38
js/src/admin/components/DashboardWidget.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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 '../../common/Component';
|
||||||
|
|
||||||
|
export default class Widget extends Component {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className={"Widget "+this.className()}>
|
||||||
|
{this.content()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the class name to apply to the widget.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
*/
|
||||||
|
className() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content of the widget.
|
||||||
|
*
|
||||||
|
* @return {VirtualElement}
|
||||||
|
*/
|
||||||
|
content() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
24
js/src/admin/components/EditCustomCssModal.js
Normal file
24
js/src/admin/components/EditCustomCssModal.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import SettingsModal from './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/src/admin/components/EditCustomFooterModal.js
Normal file
24
js/src/admin/components/EditCustomFooterModal.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import SettingsModal from './SettingsModal';
|
||||||
|
|
||||||
|
export default class EditCustomFooterModal extends SettingsModal {
|
||||||
|
className() {
|
||||||
|
return 'EditCustomFooterModal Modal--large';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return app.translator.trans('core.admin.edit_footer.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
form() {
|
||||||
|
return [
|
||||||
|
<p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>,
|
||||||
|
<div className="Form-group">
|
||||||
|
<textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')}/>
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
onsaved() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
24
js/src/admin/components/EditCustomHeaderModal.js
Normal file
24
js/src/admin/components/EditCustomHeaderModal.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import SettingsModal from './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();
|
||||||
|
}
|
||||||
|
}
|
115
js/src/admin/components/EditGroupModal.js
Normal file
115
js/src/admin/components/EditGroupModal.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import Modal from '../../common/components/Modal';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import Badge from '../../common/components/Badge';
|
||||||
|
import Group from '../../common/models/Group';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `EditGroupModal` component shows a modal dialog which allows the user
|
||||||
|
* to create or edit a group.
|
||||||
|
*/
|
||||||
|
export default class EditGroupModal extends Modal {
|
||||||
|
init() {
|
||||||
|
this.group = this.props.group || app.store.createRecord('groups');
|
||||||
|
|
||||||
|
this.nameSingular = m.prop(this.group.nameSingular() || '');
|
||||||
|
this.namePlural = m.prop(this.group.namePlural() || '');
|
||||||
|
this.icon = m.prop(this.group.icon() || '');
|
||||||
|
this.color = m.prop(this.group.color() || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
className() {
|
||||||
|
return 'EditGroupModal Modal--small';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return [
|
||||||
|
this.color() || this.icon() ? Badge.component({
|
||||||
|
icon: this.icon(),
|
||||||
|
style: {backgroundColor: this.color()}
|
||||||
|
}) : '',
|
||||||
|
' ',
|
||||||
|
this.namePlural() || app.translator.trans('core.admin.edit_group.title')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return (
|
||||||
|
<div className="Modal-body">
|
||||||
|
<div className="Form">
|
||||||
|
{this.fields().toArray()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fields() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('name', <div className="Form-group">
|
||||||
|
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
|
||||||
|
<div className="EditGroupModal-name-input">
|
||||||
|
<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={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
|
||||||
|
</div>
|
||||||
|
</div>, 30);
|
||||||
|
|
||||||
|
items.add('color', <div className="Form-group">
|
||||||
|
<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)}/>
|
||||||
|
</div>, 20);
|
||||||
|
|
||||||
|
items.add('icon', <div className="Form-group">
|
||||||
|
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1"/>})}
|
||||||
|
</div>
|
||||||
|
<input className="FormControl" placeholder="fas fa-bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
|
||||||
|
</div>, 10);
|
||||||
|
|
||||||
|
items.add('submit', <div className="Form-group">
|
||||||
|
{Button.component({
|
||||||
|
type: 'submit',
|
||||||
|
className: 'Button Button--primary EditGroupModal-save',
|
||||||
|
loading: this.loading,
|
||||||
|
children: app.translator.trans('core.admin.edit_group.submit_button')
|
||||||
|
})}
|
||||||
|
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
||||||
|
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
|
||||||
|
{app.translator.trans('core.admin.edit_group.delete_button')}
|
||||||
|
</button>
|
||||||
|
) : ''}
|
||||||
|
</div>, -10);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitData() {
|
||||||
|
return {
|
||||||
|
nameSingular: this.nameSingular(),
|
||||||
|
namePlural: this.namePlural(),
|
||||||
|
color: this.color(),
|
||||||
|
icon: this.icon()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onsubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.group.save(this.submitData(), {errorHandler: this.onerror.bind(this)})
|
||||||
|
.then(this.hide.bind(this))
|
||||||
|
.catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteGroup() {
|
||||||
|
if (confirm(app.translator.trans('core.admin.edit_group.delete_confirmation'))) {
|
||||||
|
this.group.delete().then(() => m.redraw());
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
117
js/src/admin/components/ExtensionsPage.js
Normal file
117
js/src/admin/components/ExtensionsPage.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import Page from './Page';
|
||||||
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
|
import Separator from '../../common/components/Separator';
|
||||||
|
import AddExtensionModal from './AddExtensionModal';
|
||||||
|
import LoadingModal from './LoadingModal';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import icon from '../../common/helpers/icon';
|
||||||
|
import listItems from '../../common/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: 'fas fa-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="fas fa-ellipsis-h">
|
||||||
|
{controls}
|
||||||
|
</Dropdown>
|
||||||
|
) : ''}
|
||||||
|
<div className="ExtensionListItem-main">
|
||||||
|
<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 className="ExtensionListItem-description">{extension.description}</div>
|
||||||
|
</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: 'fas fa-cog',
|
||||||
|
children: app.translator.trans('core.admin.extensions.settings_button'),
|
||||||
|
onclick: app.extensionSettings[name]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
items.add('uninstall', Button.component({
|
||||||
|
icon: 'far fa-trash-alt',
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
@@ -1,8 +1,6 @@
|
|||||||
import Component from 'flarum/Component';
|
import Component from '../../common/Component';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import listItems from 'flarum/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `HeaderPrimary` component displays primary header controls. On the
|
* The `HeaderPrimary` component displays primary header controls. On the
|
||||||
@@ -17,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.
|
||||||
*
|
*
|
@@ -1,7 +1,7 @@
|
|||||||
import Component from 'flarum/Component';
|
import Component from '../../common/Component';
|
||||||
import SessionDropdown from 'flarum/components/SessionDropdown';
|
import SessionDropdown from './SessionDropdown';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import listItems from 'flarum/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `HeaderSecondary` component displays secondary header controls.
|
* The `HeaderSecondary` component displays secondary header controls.
|
||||||
@@ -15,6 +15,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.
|
||||||
*
|
*
|
@@ -1,4 +1,4 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
import Modal from '../../common/components/Modal';
|
||||||
|
|
||||||
export default class LoadingModal extends Modal {
|
export default class LoadingModal extends Modal {
|
||||||
isDismissible() {
|
isDismissible() {
|
||||||
@@ -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() {
|
137
js/src/admin/components/MailPage.js
Normal file
137
js/src/admin/components/MailPage.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import Page from './Page';
|
||||||
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import Alert from '../../common/components/Alert';
|
||||||
|
import Select from '../../common/components/Select';
|
||||||
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
|
export default class MailPage extends Page {
|
||||||
|
init() {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.saving = false;
|
||||||
|
|
||||||
|
this.driverFields = {};
|
||||||
|
this.fields = ['mail_driver', 'mail_from'];
|
||||||
|
this.values = {};
|
||||||
|
|
||||||
|
const settings = app.data.settings;
|
||||||
|
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
|
||||||
|
|
||||||
|
app.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: app.forum.attribute('apiUrl') + '/mail-drivers'
|
||||||
|
}).then(response => {
|
||||||
|
this.driverFields = response['data'].reduce(
|
||||||
|
(hash, driver) => ({...hash, [driver['id']]: driver['attributes']['fields']}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.keys(this.driverFields).flatMap(key => this.driverFields[key]).forEach(
|
||||||
|
key => {
|
||||||
|
this.fields.push(key);
|
||||||
|
this.values[key] = m.prop(settings[key]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
if (this.loading) {
|
||||||
|
return (
|
||||||
|
<div className="MailPage">
|
||||||
|
<div className="container">
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.email.driver_heading'),
|
||||||
|
className: 'MailPage-MailSettings',
|
||||||
|
children: [
|
||||||
|
<div className="MailPage-MailSettings-input">
|
||||||
|
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
|
||||||
|
<Select value={this.values.mail_driver()} options={Object.keys(this.driverFields).reduce((memo, val) => ({...memo, [val]: val}), {})} onchange={this.values.mail_driver} />
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Object.keys(this.driverFields[this.values.mail_driver()]).length > 0 && FieldSet.component({
|
||||||
|
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
|
||||||
|
className: 'MailPage-MailSettings',
|
||||||
|
children: [
|
||||||
|
<div className="MailPage-MailSettings-input">
|
||||||
|
{this.driverFields[this.values.mail_driver()].flatMap(field => [
|
||||||
|
<label>{app.translator.trans(`core.admin.email.${field}_label`)}</label>,
|
||||||
|
<input className="FormControl" value={this.values[field]() || ''} oninput={m.withAttr('value', this.values[field])} />
|
||||||
|
])}
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Button.component({
|
||||||
|
type: 'submit',
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
children: app.translator.trans('core.admin.email.submit_button'),
|
||||||
|
loading: this.saving,
|
||||||
|
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.saving) return;
|
||||||
|
|
||||||
|
this.saving = 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.saving = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
32
js/src/admin/components/Page.js
Normal file
32
js/src/admin/components/Page.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import Component from '../../common/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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
js/src/admin/components/PermissionDropdown.js
Normal file
149
js/src/admin/components/PermissionDropdown.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import Separator from '../../common/components/Separator';
|
||||||
|
import Group from '../../common/models/Group';
|
||||||
|
import Badge from '../../common/components/Badge';
|
||||||
|
import GroupBadge from '../../common/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: 'fas fa-globe'});
|
||||||
|
} else if (members) {
|
||||||
|
this.props.label = Badge.component({icon: 'fas fa-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: 'fas fa-globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
|
||||||
|
icon: everyone ? 'fas fa-check' : true,
|
||||||
|
onclick: () => this.save([Group.GUEST_ID]),
|
||||||
|
disabled: this.isGroupDisabled(Group.GUEST_ID)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.children.push(
|
||||||
|
Button.component({
|
||||||
|
children: [Badge.component({icon: 'fas fa-user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
|
||||||
|
icon: members ? 'fas fa-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 ? 'fas fa-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 ? 'fas fa-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;
|
||||||
|
}
|
||||||
|
}
|
265
js/src/admin/components/PermissionGrid.js
Normal file
265
js/src/admin/components/PermissionGrid.js
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import Component from '../../common/Component';
|
||||||
|
import PermissionDropdown from './PermissionDropdown';
|
||||||
|
import SettingDropdown from './SettingDropdown';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import icon from '../../common/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: 'fas fa-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: 'fas fa-eye',
|
||||||
|
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
|
||||||
|
permission: 'viewDiscussions',
|
||||||
|
allowGuest: true
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('viewUserList', {
|
||||||
|
icon: 'fas fa-users',
|
||||||
|
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
|
||||||
|
permission: 'viewUserList',
|
||||||
|
allowGuest: true
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('signUp', {
|
||||||
|
icon: 'fas fa-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);
|
||||||
|
|
||||||
|
items.add('viewLastSeenAt', {
|
||||||
|
icon: 'far fa-clock',
|
||||||
|
label: app.translator.trans('core.admin.permissions.view_last_seen_at_label'),
|
||||||
|
permission: 'user.viewLastSeenAt',
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
startItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('start', {
|
||||||
|
icon: 'fas fa-edit',
|
||||||
|
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
|
||||||
|
permission: 'startDiscussion'
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('allowRenaming', {
|
||||||
|
icon: 'fas fa-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: 'fas fa-reply',
|
||||||
|
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
|
||||||
|
permission: 'discussion.reply'
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('allowPostEditing', {
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
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: 'fas fa-bullseye',
|
||||||
|
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
|
||||||
|
permission: 'discussion.viewIpsPosts'
|
||||||
|
}, 110);
|
||||||
|
|
||||||
|
items.add('renameDiscussions', {
|
||||||
|
icon: 'fas fa-i-cursor',
|
||||||
|
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
|
||||||
|
permission: 'discussion.rename'
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
items.add('hideDiscussions', {
|
||||||
|
icon: 'far fa-trash-alt',
|
||||||
|
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
|
||||||
|
permission: 'discussion.hide'
|
||||||
|
}, 90);
|
||||||
|
|
||||||
|
items.add('deleteDiscussions', {
|
||||||
|
icon: 'fas fa-times',
|
||||||
|
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
|
||||||
|
permission: 'discussion.delete'
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
items.add('editPosts', {
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
|
||||||
|
permission: 'discussion.editPosts'
|
||||||
|
}, 70);
|
||||||
|
|
||||||
|
items.add('hidePosts', {
|
||||||
|
icon: 'far fa-trash-alt',
|
||||||
|
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
|
||||||
|
permission: 'discussion.hidePosts'
|
||||||
|
}, 60);
|
||||||
|
|
||||||
|
items.add('deletePosts', {
|
||||||
|
icon: 'fas fa-times',
|
||||||
|
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
|
||||||
|
permission: 'discussion.deletePosts'
|
||||||
|
}, 60);
|
||||||
|
|
||||||
|
items.add('userEdit', {
|
||||||
|
icon: 'fas fa-user-cog',
|
||||||
|
label: app.translator.trans('core.admin.permissions.edit_users_label'),
|
||||||
|
permission: 'user.edit'
|
||||||
|
}, 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();
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user