mirror of
https://github.com/flarum/core.git
synced 2025-09-05 05:52:01 +02:00
Compare commits
891 Commits
next-front
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
BIN
.deploy.enc
Normal file
BIN
.deploy.enc
Normal file
Binary file not shown.
@@ -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
|
||||||
|
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,6 +1,8 @@
|
|||||||
.gitattributes export-ignore
|
.gitattributes export-ignore
|
||||||
.gitignore export-ignore
|
.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
|
.editorconfig export-ignore
|
||||||
.styleci.yml export-ignore
|
.styleci.yml export-ignore
|
||||||
@@ -8,4 +10,4 @@
|
|||||||
phpunit.xml export-ignore
|
phpunit.xml export-ignore
|
||||||
tests export-ignore
|
tests export-ignore
|
||||||
|
|
||||||
js/*/dist/*.js -diff
|
js/dist/* -diff
|
||||||
|
3
.github/CONTRIBUTING.md
vendored
3
.github/CONTRIBUTING.md
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Contributing to Flarum
|
|
||||||
|
|
||||||
Howdy! We're really excited that you are interested in contributing to Flarum. Before submitting your contribution, please take a moment and read through the [Contributing Guidelines](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md).
|
|
26
.github/ISSUE_TEMPLATE.md
vendored
26
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,26 +0,0 @@
|
|||||||
> Issues on Github are meant for bug reporting. Please post feature requests on the [discussion forum](https://discuss.flarum.org/t/features).
|
|
||||||
---
|
|
||||||
> Try to complete the below form as far as you are able and are willing to share. Add a screenshot of the issue if you can.
|
|
||||||
|
|
||||||
## Bug report
|
|
||||||
- Version of Flarum: x.y.z
|
|
||||||
- Website URL where the bug is visible: http://example.com
|
|
||||||
- The webserver you are running: apache, nginx or something else
|
|
||||||
- PHP version: x.y.z
|
|
||||||
- Hosted environment: shared or vps
|
|
||||||
- Hosting provider: http://some-amazing-provider.com
|
|
||||||
|
|
||||||
## Flarum info
|
|
||||||
|
|
||||||
```
|
|
||||||
Output of "php flarum info", run this in terminal in your Flarum directory.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional comments
|
|
||||||
Some additional information you'd like to share, eg what have you tried so far.
|
|
||||||
|
|
||||||
## Log files
|
|
||||||
|
|
||||||
```
|
|
||||||
Put any relevant logs here.
|
|
||||||
```
|
|
42
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: "🐛 Bug Report"
|
||||||
|
about: "If something isn't working as expected"
|
||||||
|
|
||||||
|
---
|
||||||
|
<!--
|
||||||
|
IMPORTANT: If you discover a security vulnerability within Flarum, please send an email to [security@flarum.org](mailto:security@flarum.org) instead. We will address these with the utmost urgency and it will prevent vulnerabilities, which may be abused, from popping up on our issue tracker.
|
||||||
|
-->
|
||||||
|
## 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.
|
8
.github/ISSUE_TEMPLATE/security-vulnerability.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/security-vulnerability.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: "🔒 Security Vulnerability"
|
||||||
|
about: "When you discover a security issue"
|
||||||
|
---
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Flarum, please send an email to [security@flarum.org](mailto:security@flarum.org) instead.
|
||||||
|
**DO NOT open an issue on this repository.**
|
||||||
|
We will address these with the utmost urgency and it will prevent vulnerabilities, which may be abused, from popping up on our issue tracker.
|
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 `php vendor/bin/phpunit`).
|
||||||
|
|
||||||
|
**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).
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,9 +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/*
|
.idea/*
|
||||||
node_modules
|
|
||||||
bower_components
|
|
70
.travis.yml
70
.travis.yml
@@ -1,35 +1,55 @@
|
|||||||
language: php
|
language: php
|
||||||
|
|
||||||
php:
|
cache:
|
||||||
- 5.6
|
directories:
|
||||||
- 7.0
|
- $HOME/.composer/cache
|
||||||
- 7.1
|
- $HOME/.npm
|
||||||
- hhvm
|
|
||||||
|
|
||||||
matrix:
|
install:
|
||||||
allow_failures:
|
- composer install
|
||||||
- php: hhvm
|
- mysql -e 'CREATE DATABASE flarum_test;'
|
||||||
fast_finish: true
|
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi;
|
- echo 'error_reporting = E_ALL' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
|
||||||
- composer self-update
|
|
||||||
- composer install
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- vendor/bin/phpunit --coverage-clover=coverage.xml
|
- composer test:setup
|
||||||
|
- composer test
|
||||||
|
|
||||||
notifications:
|
jobs:
|
||||||
email:
|
include:
|
||||||
on_failure: change
|
- php: 7.1
|
||||||
webhooks:
|
env: DB=mysql
|
||||||
urls:
|
|
||||||
- https://webhooks.gitter.im/e/7b9e9827a03b44a16588
|
|
||||||
on_success: always
|
|
||||||
on_failure: always
|
|
||||||
on_start: false
|
|
||||||
|
|
||||||
after_success:
|
- php: 7.2
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
env: DB=mysql
|
||||||
|
|
||||||
sudo: false
|
- php: 7.3
|
||||||
|
env: DB=mysql
|
||||||
|
|
||||||
|
- php: 7.1
|
||||||
|
addons:
|
||||||
|
mariadb: '10.2'
|
||||||
|
env: DB=mariadb
|
||||||
|
|
||||||
|
- php: 7.2
|
||||||
|
addons:
|
||||||
|
mariadb: '10.2'
|
||||||
|
env: DB=mariadb
|
||||||
|
|
||||||
|
- php: 7.3
|
||||||
|
addons:
|
||||||
|
mariadb: '10.2'
|
||||||
|
env: DB=mariadb
|
||||||
|
|
||||||
|
- php: 7.2
|
||||||
|
env: DB=mysql PREFIX=forum_
|
||||||
|
|
||||||
|
- stage: build
|
||||||
|
language: generic
|
||||||
|
if: branch = master AND type = push
|
||||||
|
install: skip
|
||||||
|
script: bash .travis/build.sh
|
||||||
|
-k $encrypted_678139e2bc67_key
|
||||||
|
-i $encrypted_678139e2bc67_iv
|
||||||
|
after_success: skip
|
||||||
|
33
.travis/build.sh
Executable file
33
.travis/build.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
main() {
|
||||||
|
while getopts ":k:i:" opt; do
|
||||||
|
case $opt in
|
||||||
|
k) encrypted_key="$OPTARG"
|
||||||
|
;;
|
||||||
|
i) encrypted_iv="$OPTARG"
|
||||||
|
;;
|
||||||
|
\?) echo "Invalid option -$OPTARG" >&2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
git checkout -f $TRAVIS_BRANCH
|
||||||
|
git config user.name "flarum-bot"
|
||||||
|
git config user.email "bot@flarum.org"
|
||||||
|
|
||||||
|
cd js
|
||||||
|
npm i -g npm@6.1.0
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
git add dist/* -f
|
||||||
|
git commit -m "Bundled output for commit $TRAVIS_COMMIT [skip ci]"
|
||||||
|
|
||||||
|
eval `ssh-agent -s`
|
||||||
|
openssl aes-256-cbc -K $encrypted_key -iv $encrypted_iv -in ../.deploy.enc -d | ssh-add -
|
||||||
|
|
||||||
|
git push git@github.com:$TRAVIS_REPO_SLUG.git $TRAVIS_BRANCH
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
60
CHANGELOG.md
Normal file
60
CHANGELOG.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.0-beta.9](https://github.com/flarum/core/compare/v0.1.0-beta.8.2...v0.1.0-beta.9)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 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))
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014-2017 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 Flarum's core code. If you want to set up a forum, 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
|
||||||
|
|
||||||
Flarum is open-source and we would love your help building it! Please read the [Contributing Guide](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md) to learn how you can help.
|
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).
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"name": "flarum/core",
|
"name": "flarum/core",
|
||||||
"description": "Delightfully simple forum software.",
|
"description": "Delightfully simple forum software.",
|
||||||
"keywords": ["forum", "discussion"],
|
"keywords": ["forum", "discussion"],
|
||||||
"homepage": "http://flarum.org",
|
"homepage": "https://flarum.org/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
@@ -17,47 +17,54 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/flarum/core/issues",
|
"issues": "https://github.com/flarum/core/issues",
|
||||||
"source": "https://github.com/flarum/core",
|
"source": "https://github.com/flarum/core",
|
||||||
"docs": "http://flarum.org/docs"
|
"docs": "https://flarum.org/docs/"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.6.0",
|
"php": ">=7.1",
|
||||||
|
"axy/sourcemap": "^0.1.4",
|
||||||
|
"components/font-awesome": "5.9.*",
|
||||||
"dflydev/fig-cookies": "^1.0.2",
|
"dflydev/fig-cookies": "^1.0.2",
|
||||||
"doctrine/dbal": "^2.5",
|
"doctrine/dbal": "^2.7",
|
||||||
"components/font-awesome": "^4.6",
|
|
||||||
"franzl/whoops-middleware": "^0.4.0",
|
"franzl/whoops-middleware": "^0.4.0",
|
||||||
"illuminate/bus": "5.1.*",
|
"illuminate/bus": "5.7.*",
|
||||||
"illuminate/cache": "5.1.*",
|
"illuminate/cache": "5.7.*",
|
||||||
"illuminate/config": "5.1.*",
|
"illuminate/config": "5.7.*",
|
||||||
"illuminate/container": "5.1.*",
|
"illuminate/container": "5.7.*",
|
||||||
"illuminate/contracts": "5.1.*",
|
"illuminate/contracts": "5.7.*",
|
||||||
"illuminate/database": "^5.1.31",
|
"illuminate/database": "5.7.*",
|
||||||
"illuminate/events": "5.1.*",
|
"illuminate/events": "5.7.*",
|
||||||
"illuminate/filesystem": "5.1.*",
|
"illuminate/filesystem": "5.7.*",
|
||||||
"illuminate/hashing": "5.1.*",
|
"illuminate/hashing": "5.7.*",
|
||||||
"illuminate/mail": "5.1.*",
|
"illuminate/mail": "5.7.*",
|
||||||
"illuminate/support": "5.1.*",
|
"illuminate/session": "5.7.*",
|
||||||
"illuminate/validation": "5.1.*",
|
"illuminate/support": "5.7.*",
|
||||||
"illuminate/view": "5.1.*",
|
"illuminate/validation": "5.7.*",
|
||||||
|
"illuminate/view": "5.7.*",
|
||||||
"intervention/image": "^2.3.0",
|
"intervention/image": "^2.3.0",
|
||||||
"league/flysystem": "^1.0.11",
|
"league/flysystem": "^1.0.11",
|
||||||
"league/oauth2-client": "~1.0",
|
|
||||||
"matthiasmullie/minify": "^1.3",
|
"matthiasmullie/minify": "^1.3",
|
||||||
|
"middlewares/base-path": "^1.1",
|
||||||
|
"middlewares/base-path-router": "^0.2.1",
|
||||||
|
"middlewares/request-handler": "^1.2",
|
||||||
"monolog/monolog": "^1.16.0",
|
"monolog/monolog": "^1.16.0",
|
||||||
"nikic/fast-route": "^0.6",
|
"nikic/fast-route": "^0.6",
|
||||||
"oyejorge/less.php": "~1.5",
|
"oyejorge/less.php": "^1.7",
|
||||||
"psr/http-message": "^1.0",
|
"psr/http-message": "^1.0",
|
||||||
"symfony/console": "^2.7",
|
"psr/http-server-handler": "^1.0",
|
||||||
"symfony/http-foundation": "^2.7",
|
"psr/http-server-middleware": "^1.0",
|
||||||
"symfony/translation": "^2.7",
|
"s9e/text-formatter": "^1.2.0",
|
||||||
"symfony/yaml": "^2.7",
|
"symfony/config": "^3.3",
|
||||||
"s9e/text-formatter": "^0.8.1",
|
"symfony/console": "^4.2",
|
||||||
|
"symfony/translation": "^3.3",
|
||||||
|
"symfony/yaml": "^3.3",
|
||||||
"tobscure/json-api": "^0.3.0",
|
"tobscure/json-api": "^0.3.0",
|
||||||
"zendframework/zend-diactoros": "^1.1",
|
"zendframework/zend-diactoros": "^1.8.4",
|
||||||
"zendframework/zend-stratigility": "^1.3"
|
"zendframework/zend-httphandlerrunner": "^1.0",
|
||||||
|
"zendframework/zend-stratigility": "^3.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mockery/mockery": "^0.9.4",
|
"mockery/mockery": "^0.9.4",
|
||||||
"phpunit/phpunit": "^4.8"
|
"phpunit/phpunit": "^6.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -69,12 +76,24 @@
|
|||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Tests\\": "tests/"
|
"Flarum\\Tests\\": "tests/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "0.1.x-dev"
|
"dev-master": "0.1.x-dev"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": [
|
||||||
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head lang="en">
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>403 Forbidden</h1>
|
|
||||||
<p>You do not have permissions to access this page.</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head lang="en">
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>404 Not Found</h1>
|
|
||||||
<p>Looks like this page could not be found.</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head lang="en">
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>500 Internal Server Error</h1>
|
|
||||||
<p>Something went wrong on our server.</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head lang="en">
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>503 Service Unavailable</h1>
|
|
||||||
<p>This forum is down for maintenance.</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -7,6 +7,5 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class Component {
|
export * from './src/common';
|
||||||
|
export * from './src/admin';
|
||||||
}
|
|
1
js/admin/.gitignore
vendored
1
js/admin/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
dist
|
|
@@ -1,53 +0,0 @@
|
|||||||
import * as m from 'mithril';
|
|
||||||
import Application from './lib/Application';
|
|
||||||
import routes from './routes';
|
|
||||||
import Nav from './components/Nav';
|
|
||||||
|
|
||||||
export default class AdminApplication extends Application {
|
|
||||||
/**
|
|
||||||
* A map of extension names to their settings callbacks.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
extensionSettings = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a list of permissions required to have the given permission.
|
|
||||||
*
|
|
||||||
* @param {String} permission
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
getRequiredPermissions(permission) {
|
|
||||||
const required = [];
|
|
||||||
|
|
||||||
if (permission === 'startDiscussion' || permission.indexOf('discussion.') === 0) {
|
|
||||||
required.push('viewDiscussions');
|
|
||||||
}
|
|
||||||
if (permission === 'discussion.delete') {
|
|
||||||
required.push('discussion.hide');
|
|
||||||
}
|
|
||||||
if (permission === 'discussion.deletePosts') {
|
|
||||||
required.push('discussion.editPosts');
|
|
||||||
}
|
|
||||||
|
|
||||||
return required;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
mount() {
|
|
||||||
m.route.prefix('#');
|
|
||||||
|
|
||||||
super.mount();
|
|
||||||
|
|
||||||
m.mount(document.getElementById('nav'), <Nav/>);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
registerDefaultRoutes(router) {
|
|
||||||
routes(router);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,40 +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 flarum from 'flarum';
|
|
||||||
import Modal from 'flarum/lib/components/Modal';
|
|
||||||
|
|
||||||
export default class AddExtensionModal extends Modal {
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
className() {
|
|
||||||
return super.className() + ' AddExtensionModal Modal--small';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
title() {
|
|
||||||
return flarum.translator.trans('admin.add_extension.title');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
content() {
|
|
||||||
return (
|
|
||||||
<div className="Modal-body">
|
|
||||||
<p>{flarum.translator.trans('admin.add_extension.temporary_text')}</p>
|
|
||||||
<p>{flarum.translator.trans('admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p>
|
|
||||||
<p>{flarum.translator.trans('admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import LinkButton from 'flarum/lib/components/LinkButton';
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class AdminLinkButton extends LinkButton {
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
className() {
|
|
||||||
return super.className() + ' AdminLinkButton';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
content() {
|
|
||||||
const content = super.content();
|
|
||||||
|
|
||||||
content.push(<div className="AdminLinkButton-description">{this.attrs.description}</div>);
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,52 +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/lib/Component';
|
|
||||||
import ItemList from 'flarum/lib/utils/ItemList';
|
|
||||||
import AdminLinkButton from 'flarum/components/AdminLinkButton';
|
|
||||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
|
||||||
|
|
||||||
function addLink(items, route, icon) {
|
|
||||||
items.add(route, <AdminLinkButton
|
|
||||||
href={flarum.router.to(route)}
|
|
||||||
icon={icon}
|
|
||||||
children={flarum.translator.trans(`admin.nav.${route}_button`)}
|
|
||||||
description={flarum.translator.trans(`admin.nav.${route}_text`)}/>);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class Nav extends Component {
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
view() {
|
|
||||||
return <SelectDropdown className="Nav" buttonClassName="Button" children={this.items().toArray()}/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an item list of links to show in the admin navigation.
|
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
items() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
addLink(items, 'dashboard', 'bar-chart'));
|
|
||||||
addLink(items, 'basics', 'pencil'));
|
|
||||||
addLink(items, 'mail', 'envelope'));
|
|
||||||
addLink(items, 'permissions', 'key'));
|
|
||||||
addLink(items, 'appearance', 'paint-brush'));
|
|
||||||
addLink(items, 'extensions', 'puzzle-piece'));
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
import Page from 'flarum/components/Page';
|
|
||||||
|
|
||||||
export default class DashboardPage extends Page {
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<div className="DashboardPage">
|
|
||||||
<div className="container">
|
|
||||||
<h2>{app.translator.trans('core.admin.dashboard.welcome_text')}</h2>
|
|
||||||
<p>{app.translator.trans('core.admin.dashboard.version_text', {version: <strong>{app.forum.attribute('version')}</strong>})}</p>
|
|
||||||
<p>{app.translator.trans('core.admin.dashboard.beta_warning_text', {strong: <strong/>})}</p>
|
|
||||||
<ul>
|
|
||||||
<li>{app.translator.trans('core.admin.dashboard.contributing_text', {a: <a href="http://flarum.org/docs/contributing" target="_blank"/>})}</li>
|
|
||||||
<li>{app.translator.trans('core.admin.dashboard.troubleshooting_text', {a: <a href="http://flarum.org/docs/troubleshooting" target="_blank"/>})}</li>
|
|
||||||
<li>{app.translator.trans('core.admin.dashboard.support_text', {a: <a href="http://discuss.flarum.org/t/support" target="_blank"/>})}</li>
|
|
||||||
<li>{app.translator.trans('core.admin.dashboard.features_text', {a: <a href="http://discuss.flarum.org/t/features" target="_blank"/>})}</li>
|
|
||||||
<li>{app.translator.trans('core.admin.dashboard.extension_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,102 +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 {
|
|
||||||
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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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="http://fortawesome.github.io/Font-Awesome/icons/" tabindex="-1"/>})}
|
|
||||||
</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: 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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
this.group.save({
|
|
||||||
nameSingular: this.nameSingular(),
|
|
||||||
namePlural: this.namePlural(),
|
|
||||||
color: this.color(),
|
|
||||||
icon: this.icon()
|
|
||||||
}, {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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
import SessionDropdown from './SessionDropdown';
|
|
||||||
|
|
||||||
export default class Header extends Component {
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
view() {
|
|
||||||
return this.items().toVnodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an item list for the header contents.
|
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
*/
|
|
||||||
items() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('session', <SessionDropdown/>);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
import AdminApplication from './AdminApplication';
|
|
||||||
|
|
||||||
export const app = new AdminApplication();
|
|
||||||
|
|
||||||
export const extensions = [];
|
|
||||||
|
|
||||||
// Export public API
|
|
||||||
// export { default as Extend } from './Extend';
|
|
||||||
// export { IndexPage, DicsussionList } from './components';
|
|
@@ -1 +0,0 @@
|
|||||||
/Users/toby/Projects/Flarum/app/packages/core/js/lib
|
|
@@ -1,15 +0,0 @@
|
|||||||
import DashboardPage from './components/DashboardPage';
|
|
||||||
import BasicsPage from './components/BasicsPage';
|
|
||||||
import PermissionsPage from './components/PermissionsPage';
|
|
||||||
import AppearancePage from './components/AppearancePage';
|
|
||||||
import ExtensionsPage from './components/ExtensionsPage';
|
|
||||||
import MailPage from './components/MailPage';
|
|
||||||
|
|
||||||
export default function(router) {
|
|
||||||
router.add('dashboard', '/', DashboardPage);
|
|
||||||
router.add('basics', '/basics', BasicsPage);
|
|
||||||
router.add('permissions', '/permissions', PermissionsPage);
|
|
||||||
router.add('appearance', '/appearance', AppearancePage);
|
|
||||||
router.add('extensions', '/extensions', ExtensionsPage);
|
|
||||||
router.add('mail', '/mail', MailPage);
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
import flarum from 'flarum';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a request to save the given settings to the database.
|
|
||||||
*
|
|
||||||
* @param {Object} settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export default function saveSettings(settings) {
|
|
||||||
const oldSettings = JSON.parse(JSON.stringify(flarum.data.settings));
|
|
||||||
|
|
||||||
Object.assign(flarum.data.settings, settings);
|
|
||||||
|
|
||||||
return flarum.ajax.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: flarum.forum.apiUrl + '/settings',
|
|
||||||
data: settings
|
|
||||||
}).catch(error => {
|
|
||||||
flarum.data.settings = oldSettings;
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist/",
|
|
||||||
"sourceMap": true,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"jsx": "react",
|
|
||||||
"jsxFactory": "m",
|
|
||||||
"declaration": true,
|
|
||||||
"lib": ["dom", "es2015"],
|
|
||||||
"types": [
|
|
||||||
"mithril",
|
|
||||||
"classnames"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"./src/**/*"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
@@ -1,30 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const { CheckerPlugin } = require('awesome-typescript-loader');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: path.resolve(__dirname, 'src/index.tsx'),
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.tsx', '.js', '.jsx']
|
|
||||||
},
|
|
||||||
devtool: 'source-map',
|
|
||||||
output: {
|
|
||||||
filename: 'bundle.js',
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
library: 'flarum',
|
|
||||||
libraryTarget: 'var'
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
loaders: [
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
loader: 'awesome-typescript-loader'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new CheckerPlugin()
|
|
||||||
],
|
|
||||||
externals: {
|
|
||||||
mithril: 'm'
|
|
||||||
}
|
|
||||||
};
|
|
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';
|
1
js/forum/.gitignore
vendored
1
js/forum/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
dist
|
|
@@ -1,86 +0,0 @@
|
|||||||
import Application from './lib/Application';
|
|
||||||
import routes from './routes';
|
|
||||||
import Search from './components/Search';
|
|
||||||
|
|
||||||
export default class ForumApplication extends Application {
|
|
||||||
/**
|
|
||||||
* The page's search component instance.
|
|
||||||
*
|
|
||||||
* @type {SearchBox}
|
|
||||||
*/
|
|
||||||
search = new Search();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of notification types to their components.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
notificationComponents = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of post types to their components.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
postComponents = {};
|
|
||||||
|
|
||||||
//app.postComponents.comment = CommentPost;
|
|
||||||
//app.postComponents.discussionRenamed = DiscussionRenamedPost;
|
|
||||||
|
|
||||||
// app.notificationComponents.discussionRenamed = DiscussionRenamedNotification;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
registerDefaultRoutes(router) {
|
|
||||||
routes(router);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: work out where to put these helper functions
|
|
||||||
// /**
|
|
||||||
// * Check whether or not the user is currently composing a reply to a
|
|
||||||
// * discussion.
|
|
||||||
// *
|
|
||||||
// * @param {Discussion} discussion
|
|
||||||
// * @return {Boolean}
|
|
||||||
// */
|
|
||||||
// composingReplyTo(discussion) {
|
|
||||||
// return this.composer.component instanceof ReplyComposer &&
|
|
||||||
// this.composer.component.props.discussion === discussion &&
|
|
||||||
// this.composer.position !== Composer.PositionEnum.HIDDEN;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Check whether or not the user is currently viewing a discussion.
|
|
||||||
// *
|
|
||||||
// * @param {Discussion} discussion
|
|
||||||
// * @return {Boolean}
|
|
||||||
// */
|
|
||||||
// viewingDiscussion(discussion) {
|
|
||||||
// return this.current instanceof DiscussionPage &&
|
|
||||||
// this.current.discussion === discussion;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Callback for when an external authenticator (social login) action has
|
|
||||||
// * completed.
|
|
||||||
// *
|
|
||||||
// * If the payload indicates that the user has been logged in, then the page
|
|
||||||
// * will be reloaded. Otherwise, a SignUpModal will be opened, prefilled
|
|
||||||
// * with the provided details.
|
|
||||||
// *
|
|
||||||
// * @param {Object} payload A dictionary of props to pass into the sign up
|
|
||||||
// * modal. A truthy `authenticated` prop indicates that the user has logged
|
|
||||||
// * in, and thus the page is reloaded.
|
|
||||||
// * @public
|
|
||||||
// */
|
|
||||||
// authenticationComplete(payload) {
|
|
||||||
// if (payload.authenticated) {
|
|
||||||
// window.location.reload();
|
|
||||||
// } else {
|
|
||||||
// const modal = new SignUpModal(payload);
|
|
||||||
// this.modal.show(modal);
|
|
||||||
// modal.$('[name=password]').focus();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
@@ -1,165 +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';
|
|
||||||
import extractText from 'flarum/utils/extractText';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `EditUserModal` component displays a modal dialog with a login form.
|
|
||||||
*/
|
|
||||||
export default class EditUserModal extends Modal {
|
|
||||||
init() {
|
|
||||||
super.init();
|
|
||||||
|
|
||||||
const user = this.props.user;
|
|
||||||
|
|
||||||
this.username = m.prop(user.username() || '');
|
|
||||||
this.email = m.prop(user.email() || '');
|
|
||||||
this.isActivated = m.prop(user.isActivated() || false);
|
|
||||||
this.setPassword = m.prop(false);
|
|
||||||
this.password = m.prop(user.password() || '');
|
|
||||||
this.groups = {};
|
|
||||||
|
|
||||||
app.store.all('groups')
|
|
||||||
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
|
||||||
.forEach(group => this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1));
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
return 'EditUserModal Modal--small';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return app.translator.trans('core.forum.edit_user.title');
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return (
|
|
||||||
<div className="Modal-body">
|
|
||||||
<div className="Form">
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
|
|
||||||
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
|
|
||||||
bidi={this.username} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{app.session.user !== this.props.user ? [
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
|
|
||||||
<div>
|
|
||||||
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
|
|
||||||
bidi={this.email} />
|
|
||||||
</div>
|
|
||||||
{!this.isActivated() ? (
|
|
||||||
<div>
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--block',
|
|
||||||
children: app.translator.trans('core.forum.edit_user.activate_button'),
|
|
||||||
loading: this.loading,
|
|
||||||
onclick: this.activate.bind(this)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : ''}
|
|
||||||
</div>,
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
|
|
||||||
<div>
|
|
||||||
<label className="checkbox">
|
|
||||||
<input type="checkbox" checked={this.setPassword()} onchange={e => {
|
|
||||||
this.setPassword(e.target.checked);
|
|
||||||
m.redraw(true);
|
|
||||||
if (e.target.checked) this.$('[name=password]').select();
|
|
||||||
m.redraw.strategy('none');
|
|
||||||
}}/>
|
|
||||||
{app.translator.trans('core.forum.edit_user.set_password_label')}
|
|
||||||
</label>
|
|
||||||
{this.setPassword() ? (
|
|
||||||
<input className="FormControl" type="password" name="password" placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
|
|
||||||
bidi={this.password} />
|
|
||||||
) : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
] : ''}
|
|
||||||
|
|
||||||
<div className="Form-group EditUserModal-groups">
|
|
||||||
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
|
|
||||||
<div>
|
|
||||||
{Object.keys(this.groups)
|
|
||||||
.map(id => app.store.getById('groups', id))
|
|
||||||
.map(group => (
|
|
||||||
<label className="checkbox">
|
|
||||||
<input type="checkbox"
|
|
||||||
bidi={this.groups[group.id()]}
|
|
||||||
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID} />
|
|
||||||
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
type: 'submit',
|
|
||||||
loading: this.loading,
|
|
||||||
children: app.translator.trans('core.forum.edit_user.submit_button')
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
activate() {
|
|
||||||
this.loading = true;
|
|
||||||
const data = {
|
|
||||||
username: this.username(),
|
|
||||||
isActivated: true,
|
|
||||||
};
|
|
||||||
this.props.user.save(data, {errorHandler: this.onerror.bind(this)})
|
|
||||||
.then(() => {
|
|
||||||
this.isActivated(true);
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const groups = Object.keys(this.groups)
|
|
||||||
.filter(id => this.groups[id]())
|
|
||||||
.map(id => app.store.getById('groups', id));
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
username: this.username(),
|
|
||||||
relationships: {groups}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (app.session.user !== this.props.user) {
|
|
||||||
data.email = this.email();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.setPassword()) {
|
|
||||||
data.password = this.password();
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
this.props.user.save(this.data(), {errorHandler: this.onerror.bind(this)})
|
|
||||||
.then(this.hide.bind(this))
|
|
||||||
.catch(() => {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,157 +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';
|
|
||||||
import LogInButtons from 'flarum/components/LogInButtons';
|
|
||||||
import Switch from 'flarum/components/Switch';
|
|
||||||
import extractText from 'flarum/utils/extractText';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `LogInModal` component displays a modal dialog with a login form.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `identification`
|
|
||||||
* - `password`
|
|
||||||
*/
|
|
||||||
export default class LogInModal extends Modal {
|
|
||||||
init() {
|
|
||||||
super.init();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the identification input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.identification = m.prop(this.props.identification || '');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the password input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.password = m.prop(this.props.password || '');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the remember me input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.remember = m.prop(this.props.remember && true);
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
return 'LogInModal Modal--small';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return app.translator.trans('core.forum.log_in.title');
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return [
|
|
||||||
<div className="Modal-body">
|
|
||||||
<LogInButtons/>
|
|
||||||
|
|
||||||
<div className="Form Form--centered">
|
|
||||||
<div className="Form-group">
|
|
||||||
<input className="FormControl" name="identification" type="text" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
|
|
||||||
bidi={this.identification}
|
|
||||||
disabled={this.loading} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
|
|
||||||
bidi={this.password}
|
|
||||||
disabled={this.loading} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
{Switch.component({
|
|
||||||
children: app.translator.trans('core.forum.log_in.remember_me_label'),
|
|
||||||
disabled: this.loading,
|
|
||||||
onchange: this.remember,
|
|
||||||
state: this.remember()
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component({
|
|
||||||
className: 'Button Button--primary Button--block',
|
|
||||||
type: 'submit',
|
|
||||||
loading: this.loading,
|
|
||||||
children: app.translator.trans('core.forum.log_in.submit_button')
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
<div className="Modal-footer">
|
|
||||||
<p className="LogInModal-forgotPassword">
|
|
||||||
<a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{app.forum.attribute('allowSignUp') ? (
|
|
||||||
<p className="LogInModal-signUp">
|
|
||||||
{app.translator.trans('core.forum.log_in.sign_up_text', {a: <a onclick={this.signUp.bind(this)}/>})}
|
|
||||||
</p>
|
|
||||||
) : ''}
|
|
||||||
</div>
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the forgot password modal, prefilling it with an email if the user has
|
|
||||||
* entered one.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
forgotPassword() {
|
|
||||||
const email = this.identification();
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
signUp() {
|
|
||||||
const props = {password: this.password()};
|
|
||||||
const identification = this.identification();
|
|
||||||
props[identification.indexOf('@') !== -1 ? 'email' : 'username'] = identification;
|
|
||||||
|
|
||||||
app.modal.show(new SignUpModal(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
onready() {
|
|
||||||
this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').select();
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
const identification = this.identification();
|
|
||||||
const password = this.password();
|
|
||||||
const remember = this.remember();
|
|
||||||
|
|
||||||
app.session.login({identification, password, remember}, {errorHandler: this.onerror.bind(this)})
|
|
||||||
.then(
|
|
||||||
() => window.location.reload(),
|
|
||||||
this.loaded.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onerror(error) {
|
|
||||||
if (error.status === 401) {
|
|
||||||
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onerror(error);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,145 +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 {
|
|
||||||
init() {
|
|
||||||
/**
|
|
||||||
* 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.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
|
|
||||||
onclick: this.markAllAsRead.bind(this)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</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.translator.trans('core.forum.notifications.empty_text')}</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.newNotificationsCount()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
m.redraw();
|
|
||||||
|
|
||||||
app.store.find('notifications')
|
|
||||||
.then(notifications => {
|
|
||||||
app.session.user.pushAttributes({newNotificationsCount: 0});
|
|
||||||
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
.then(() => {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark all of the notifications as read.
|
|
||||||
*/
|
|
||||||
markAllAsRead() {
|
|
||||||
if (!app.cache.notifications) return;
|
|
||||||
|
|
||||||
app.session.user.pushAttributes({unreadNotificationsCount: 0});
|
|
||||||
|
|
||||||
app.cache.notifications.forEach(notification => notification.pushAttributes({isRead: true}));
|
|
||||||
|
|
||||||
app.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/notifications/read',
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,104 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
|
||||||
import classList from 'flarum/utils/classList';
|
|
||||||
import extractText from 'flarum/utils/extractText';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `UserBio` component displays a user's bio, optionally letting the user
|
|
||||||
* edit it.
|
|
||||||
*/
|
|
||||||
export default class UserBio extends Component {
|
|
||||||
init() {
|
|
||||||
/**
|
|
||||||
* 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={extractText(app.translator.trans('core.forum.user.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.translator.trans('core.forum.user.bio_placeholder')}</p>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content = <div className="UserBio-content" onclick={this.edit.bind(this)}>{subContent}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'UserBio ' + classList({
|
|
||||||
editable: this.props.editable,
|
|
||||||
editing: this.editing
|
|
||||||
})}>
|
|
||||||
{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()
|
|
||||||
.blur(save)
|
|
||||||
.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})
|
|
||||||
.catch(() => {})
|
|
||||||
.then(() => {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editing = false;
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
import ForumApplication from './ForumApplication';
|
|
||||||
|
|
||||||
export const app = new ForumApplication();
|
|
||||||
|
|
||||||
export const extensions = [];
|
|
||||||
|
|
||||||
// Export public API
|
|
||||||
// export { default as Extend } from './Extend';
|
|
||||||
// export { IndexPage, DicsussionList } from './components';
|
|
@@ -1,91 +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 icon from 'flarum/helpers/icon';
|
|
||||||
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');
|
|
||||||
let defaultAction = 'index';
|
|
||||||
|
|
||||||
for (const i in app.routes) {
|
|
||||||
if (app.routes[i].path === defaultRoute) defaultAction = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.routes[defaultAction].path = '/';
|
|
||||||
app.history.push(defaultAction, icon('bars'), '/');
|
|
||||||
|
|
||||||
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();
|
|
||||||
if (app.session.user) {
|
|
||||||
app.store.find('users', app.session.user.id());
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.booted = true;
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
/Users/toby/Projects/Flarum/app/packages/core/js/lib
|
|
@@ -1,58 +0,0 @@
|
|||||||
import IndexPage from './components/IndexPage';
|
|
||||||
import DiscussionPage from './components/DiscussionPage';
|
|
||||||
import PostsUserPage from './components/PostsUserPage';
|
|
||||||
import DiscussionsUserPage from './components/DiscussionsUserPage';
|
|
||||||
import SettingsPage from './components/SettingsPage';
|
|
||||||
import NotificationsPage from './components/NotificationsPage';
|
|
||||||
|
|
||||||
export default function(router) {
|
|
||||||
router.add('index', '/all', IndexPage);
|
|
||||||
router.add('index.filter', '/:filter', IndexPage);
|
|
||||||
|
|
||||||
router.add('discussion', '/d/:id', DiscussionPage);
|
|
||||||
router.add('discussion.near', '/d/:id/:near', DiscussionPage);
|
|
||||||
|
|
||||||
router.add('user', '/u/:username', PostsUserPage);
|
|
||||||
router.add('user.posts', '/u/:username', PostsUserPage);
|
|
||||||
router.add('user.discussions', '/u/:username/discussions', DiscussionsUserPage);
|
|
||||||
|
|
||||||
router.add('settings', '/settings', SettingsPage);
|
|
||||||
router.add('notifications', '/notifications', NotificationsPage);
|
|
||||||
|
|
||||||
// TODO: work out where to put these shortcut functions
|
|
||||||
// /**
|
|
||||||
// * Generate a URL to a discussion.
|
|
||||||
// *
|
|
||||||
// * @param {Discussion} discussion
|
|
||||||
// * @param {Integer} [near]
|
|
||||||
// * @return {String}
|
|
||||||
// */
|
|
||||||
// app.route.discussion = (discussion, near) => {
|
|
||||||
// return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
|
|
||||||
// id: discussion.id() + '-' + discussion.slug(),
|
|
||||||
// near: near && near !== 1 ? near : undefined
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Generate a URL to a post.
|
|
||||||
// *
|
|
||||||
// * @param {Post} post
|
|
||||||
// * @return {String}
|
|
||||||
// */
|
|
||||||
// app.route.post = post => {
|
|
||||||
// return app.route.discussion(post.discussion(), post.number());
|
|
||||||
// };
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Generate a URL to a user.
|
|
||||||
// *
|
|
||||||
// * @param {User} user
|
|
||||||
// * @return {String}
|
|
||||||
// */
|
|
||||||
// app.route.user = user => {
|
|
||||||
// return app.route('user', {
|
|
||||||
// username: user.username()
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist/",
|
|
||||||
"sourceMap": true,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"jsx": "react",
|
|
||||||
"jsxFactory": "m",
|
|
||||||
"declaration": true,
|
|
||||||
"lib": ["dom", "es2015"],
|
|
||||||
"types": [
|
|
||||||
"mithril",
|
|
||||||
"classnames"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"./src/**/*"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
@@ -1,30 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const { CheckerPlugin } = require('awesome-typescript-loader');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: path.resolve(__dirname, 'src/index.tsx'),
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.tsx', '.js', '.jsx']
|
|
||||||
},
|
|
||||||
devtool: 'source-map',
|
|
||||||
output: {
|
|
||||||
filename: 'bundle.js',
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
library: 'flarum',
|
|
||||||
libraryTarget: 'var'
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
loaders: [
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
loader: 'awesome-typescript-loader'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new CheckerPlugin()
|
|
||||||
],
|
|
||||||
externals: {
|
|
||||||
mithril: 'm'
|
|
||||||
}
|
|
||||||
};
|
|
148
js/lib/Ajax.tsx
148
js/lib/Ajax.tsx
@@ -1,148 +0,0 @@
|
|||||||
import { app } from 'flarum';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
import Alert from 'flarum/components/Alert';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import RequestErrorModal from 'flarum/components/RequestErrorModal';
|
|
||||||
import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal';
|
|
||||||
import Translator from 'flarum/Translator';
|
|
||||||
import extract from 'flarum/utils/extract';
|
|
||||||
import patchMithril from 'flarum/utils/patchMithril';
|
|
||||||
import RequestError from 'flarum/utils/RequestError';
|
|
||||||
import { extend } from 'flarum/extend';
|
|
||||||
|
|
||||||
export default class Ajax {
|
|
||||||
/**
|
|
||||||
* 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(originalOptions) {
|
|
||||||
const options = Object.assign({}, originalOptions);
|
|
||||||
|
|
||||||
// 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.background = options.background || true;
|
|
||||||
|
|
||||||
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken));
|
|
||||||
|
|
||||||
// If the method is something like PATCH or DELETE, which not all servers
|
|
||||||
// and clients support, then we'll send it as a POST request with the
|
|
||||||
// intended method specified in the X-HTTP-Method-Override header.
|
|
||||||
if (options.method !== 'GET' && options.method !== 'POST') {
|
|
||||||
const method = options.method;
|
|
||||||
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-HTTP-Method-Override', method));
|
|
||||||
options.method = 'POST';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 => responseText);
|
|
||||||
|
|
||||||
options.errorHandler = options.errorHandler || (error => {
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 => {
|
|
||||||
let responseText;
|
|
||||||
|
|
||||||
if (original) {
|
|
||||||
responseText = original(xhr.responseText);
|
|
||||||
} else {
|
|
||||||
responseText = xhr.responseText || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = xhr.status;
|
|
||||||
|
|
||||||
if (status < 200 || status > 299) {
|
|
||||||
throw new RequestError(status, responseText, options, xhr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xhr.getResponseHeader) {
|
|
||||||
const csrfToken = xhr.getResponseHeader('X-CSRF-Token');
|
|
||||||
if (csrfToken) app.session.csrfToken = csrfToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(responseText);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RequestError(500, responseText, options, xhr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.requestError) this.alerts.dismiss(this.requestError.alert);
|
|
||||||
|
|
||||||
// Now make the request. If it's a failure, inspect the error that was
|
|
||||||
// returned and show an alert containing its contents.
|
|
||||||
const deferred = m.deferred();
|
|
||||||
|
|
||||||
m.request(options).then(response => deferred.resolve(response), error => {
|
|
||||||
this.requestError = error;
|
|
||||||
|
|
||||||
let children;
|
|
||||||
|
|
||||||
switch (error.status) {
|
|
||||||
case 422:
|
|
||||||
children = error.response.errors
|
|
||||||
.map(error => [error.detail, <br/>])
|
|
||||||
.reduce((a, b) => a.concat(b), [])
|
|
||||||
.slice(0, -1);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 401:
|
|
||||||
case 403:
|
|
||||||
children = app.translator.trans('core.lib.error.permission_denied_message');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 404:
|
|
||||||
case 410:
|
|
||||||
children = app.translator.trans('core.lib.error.not_found_message');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 429:
|
|
||||||
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
children = app.translator.trans('core.lib.error.generic_message');
|
|
||||||
}
|
|
||||||
|
|
||||||
error.alert = new Alert({
|
|
||||||
type: 'error',
|
|
||||||
children,
|
|
||||||
controls: app.forum.attribute('debug') ? [
|
|
||||||
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>Debug</Button>
|
|
||||||
] : undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
options.errorHandler(error);
|
|
||||||
} catch (error) {
|
|
||||||
this.alerts.show(error.alert);
|
|
||||||
}
|
|
||||||
|
|
||||||
deferred.reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {RequestError} error
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
showDebug(error) {
|
|
||||||
this.alerts.dismiss(this.requestErrorAlert);
|
|
||||||
|
|
||||||
this.modal.show(new RequestErrorModal({error}));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,168 +0,0 @@
|
|||||||
import * as m from 'mithril';
|
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
|
||||||
import Alert from 'flarum/components/Alert';
|
|
||||||
import Button from 'flarum/components/Button';
|
|
||||||
import RequestErrorModal from 'flarum/components/RequestErrorModal';
|
|
||||||
import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal';
|
|
||||||
import Translator from 'flarum/Translator';
|
|
||||||
import extract from 'flarum/utils/extract';
|
|
||||||
import patchMithril from 'flarum/utils/patchMithril';
|
|
||||||
import RequestError from 'flarum/utils/RequestError';
|
|
||||||
import { extend } from 'flarum/extend';
|
|
||||||
import routes from 'flarum/routes';
|
|
||||||
import Header from 'flarum/components/Header';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 Application {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* [data description]
|
|
||||||
*
|
|
||||||
* @type {[type]}
|
|
||||||
*/
|
|
||||||
this.data = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The forum model for this application.
|
|
||||||
*
|
|
||||||
* @type {Forum}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.forum = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [router description]
|
|
||||||
*
|
|
||||||
* @type {Router}
|
|
||||||
*/
|
|
||||||
this.router = new Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The app's session.
|
|
||||||
*
|
|
||||||
* @type {Session}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.user = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The app's translator.
|
|
||||||
*
|
|
||||||
* @type {Translator}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.translator = new Translator();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The app's data store.
|
|
||||||
*
|
|
||||||
* @type {Store}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.store = new Store();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A local cache that can be used to store data at the application level, so
|
|
||||||
* that it persists between different routes.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.cache = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [title description]
|
|
||||||
*
|
|
||||||
* @type {DocumentTitle}
|
|
||||||
*/
|
|
||||||
this.title = new DocumentTitle();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [ajax description]
|
|
||||||
*
|
|
||||||
* @type {Ajax}
|
|
||||||
*/
|
|
||||||
this.ajax = new Ajax();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
registerDefaultRoutes(router) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [loadModules description]
|
|
||||||
*
|
|
||||||
* @param {[type]} modules [description]
|
|
||||||
* @return {[type]}
|
|
||||||
*/
|
|
||||||
loadModules(modules) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boot the application by running all of the registered initializers.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
boot(data) {
|
|
||||||
this.data = data;
|
|
||||||
|
|
||||||
this.translator.addMessages(data.locale);
|
|
||||||
|
|
||||||
this.store.push(data.resources);
|
|
||||||
|
|
||||||
this.forum = new Forum(data.forum);
|
|
||||||
|
|
||||||
this.user = this.store.find('users', data.userId);
|
|
||||||
|
|
||||||
this.mount();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mount Flarum's interface onto the page.
|
|
||||||
*
|
|
||||||
* @return {[type]}
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
mount() {
|
|
||||||
this.modal = m.mount(document.getElementById('modal'), <ModalManager/>);
|
|
||||||
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>);
|
|
||||||
|
|
||||||
m.mount(document.getElementById('header'), <Header/>);
|
|
||||||
m.route(document.getElementById('content'), '/', this.router.build());
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the API response document that has been preloaded into the application.
|
|
||||||
*
|
|
||||||
* @return {Object|null}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
preloadedDocument() {
|
|
||||||
if (this.data.document) {
|
|
||||||
const results = this.store.sync(this.data.document);
|
|
||||||
|
|
||||||
this.data.document = null;
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,3 +0,0 @@
|
|||||||
export default class DocumentTitle {
|
|
||||||
|
|
||||||
}
|
|
@@ -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,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,30 +0,0 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
|
||||||
|
|
||||||
export default class RequestErrorModal extends Modal {
|
|
||||||
className() {
|
|
||||||
return 'RequestErrorModal Modal--large';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return this.props.error.xhr
|
|
||||||
? this.props.error.xhr.status+' '+this.props.error.xhr.statusText
|
|
||||||
: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
let responseText;
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseText = JSON.stringify(JSON.parse(this.props.error.responseText), null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
responseText = this.props.error.responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="Modal-body">
|
|
||||||
<pre>
|
|
||||||
{this.props.error.options.method} {this.props.error.options.url}<br/><br/>
|
|
||||||
{responseText}
|
|
||||||
</pre>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
import Model from 'flarum/Model';
|
|
||||||
import computed from 'flarum/utils/computed';
|
|
||||||
|
|
||||||
export default class Notification extends Model {}
|
|
||||||
|
|
||||||
Object.assign(Notification.prototype, {
|
|
||||||
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,28 +0,0 @@
|
|||||||
import Model from 'flarum/Model';
|
|
||||||
import computed from 'flarum/utils/computed';
|
|
||||||
import { getPlainContent } from 'flarum/utils/string';
|
|
||||||
|
|
||||||
export default class Post extends Model {}
|
|
||||||
|
|
||||||
Object.assign(Post.prototype, {
|
|
||||||
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')
|
|
||||||
});
|
|
4963
js/package-lock.json
generated
Normal file
4963
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.11",
|
||||||
|
"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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
js/src/admin/components/AdminLinkButton.js
Normal file
24
js/src/admin/components/AdminLinkButton.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
|
|
||||||
|
export default class AdminLinkButton extends LinkButton {
|
||||||
|
getButtonContent() {
|
||||||
|
const content = super.getButtonContent();
|
||||||
|
|
||||||
|
content.push(
|
||||||
|
<div className="AdminLinkButton-description">
|
||||||
|
{this.props.description}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
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;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,11 @@
|
|||||||
import Page from 'flarum/components/Page';
|
import Page from './Page';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Switch from 'flarum/components/Switch';
|
import Switch from '../../common/components/Switch';
|
||||||
import EditCustomCssModal from 'flarum/components/EditCustomCssModal';
|
import EditCustomCssModal from './EditCustomCssModal';
|
||||||
import EditCustomHeaderModal from 'flarum/components/EditCustomHeaderModal';
|
import EditCustomHeaderModal from './EditCustomHeaderModal';
|
||||||
import UploadImageButton from 'flarum/components/UploadImageButton';
|
import EditCustomFooterModal from './EditCustomFooterModal';
|
||||||
import saveSettings from 'flarum/utils/saveSettings';
|
import UploadImageButton from './UploadImageButton';
|
||||||
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
export default class AppearancePage extends Page {
|
export default class AppearancePage extends Page {
|
||||||
init() {
|
init() {
|
||||||
@@ -28,8 +29,8 @@ export default class AppearancePage extends Page {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="AppearancePage-colors-input">
|
<div className="AppearancePage-colors-input">
|
||||||
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
||||||
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
|
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Switch.component({
|
{Switch.component({
|
||||||
@@ -81,6 +82,18 @@ export default class AppearancePage extends Page {
|
|||||||
})}
|
})}
|
||||||
</fieldset>
|
</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>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
|
||||||
<div className="helpText">
|
<div className="helpText">
|
@@ -1,11 +1,11 @@
|
|||||||
import Page from 'flarum/components/Page';
|
import Page from './Page';
|
||||||
import FieldSet from 'flarum/components/FieldSet';
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
import Select from 'flarum/components/Select';
|
import Select from '../../common/components/Select';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Alert from 'flarum/components/Alert';
|
import Alert from '../../common/components/Alert';
|
||||||
import saveSettings from 'flarum/utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import Switch from 'flarum/components/Switch';
|
import Switch from '../../common/components/Switch';
|
||||||
|
|
||||||
export default class BasicsPage extends Page {
|
export default class BasicsPage extends Page {
|
||||||
init() {
|
init() {
|
||||||
@@ -25,7 +25,7 @@ export default class BasicsPage extends Page {
|
|||||||
this.values = {};
|
this.values = {};
|
||||||
|
|
||||||
const settings = app.data.settings;
|
const settings = app.data.settings;
|
||||||
this.fields.forEach(key => this.values[key] = m.prop(settings[key] || false));
|
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
|
||||||
|
|
||||||
this.localeOptions = {};
|
this.localeOptions = {};
|
||||||
const locales = app.data.locales;
|
const locales = app.data.locales;
|
||||||
@@ -66,19 +66,16 @@ export default class BasicsPage extends Page {
|
|||||||
options: this.localeOptions,
|
options: this.localeOptions,
|
||||||
value: this.values.default_locale(),
|
value: this.values.default_locale(),
|
||||||
onchange: this.values.default_locale
|
onchange: this.values.default_locale
|
||||||
|
}),
|
||||||
|
Switch.component({
|
||||||
|
state: this.values.show_language_selector(),
|
||||||
|
onchange: this.values.show_language_selector,
|
||||||
|
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
{Switch.component({
|
|
||||||
state: this.values.show_language_selector(),
|
|
||||||
onchange: this.values.show_language_selector,
|
|
||||||
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
|
||||||
})}
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: app.translator.trans('core.admin.basics.home_page_heading'),
|
label: app.translator.trans('core.admin.basics.home_page_heading'),
|
||||||
className: 'BasicsPage-homePage',
|
className: 'BasicsPage-homePage',
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import SettingsModal from 'flarum/components/SettingsModal';
|
import SettingsModal from './SettingsModal';
|
||||||
|
|
||||||
export default class EditCustomCssModal extends SettingsModal {
|
export default class EditCustomCssModal extends SettingsModal {
|
||||||
className() {
|
className() {
|
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();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import SettingsModal from 'flarum/components/SettingsModal';
|
import SettingsModal from './SettingsModal';
|
||||||
|
|
||||||
export default class EditCustomHeaderModal extends SettingsModal {
|
export default class EditCustomHeaderModal extends SettingsModal {
|
||||||
className() {
|
className() {
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,13 @@
|
|||||||
import Page from 'flarum/components/Page';
|
import Page from './Page';
|
||||||
import LinkButton from 'flarum/components/LinkButton';
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Dropdown from 'flarum/components/Dropdown';
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
import Separator from 'flarum/components/Separator';
|
import Separator from '../../common/components/Separator';
|
||||||
import AddExtensionModal from 'flarum/components/AddExtensionModal';
|
import AddExtensionModal from './AddExtensionModal';
|
||||||
import LoadingModal from 'flarum/components/LoadingModal';
|
import LoadingModal from './LoadingModal';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import icon from 'flarum/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
import listItems from 'flarum/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
|
||||||
export default class ExtensionsPage extends Page {
|
export default class ExtensionsPage extends Page {
|
||||||
view() {
|
view() {
|
||||||
@@ -17,7 +17,7 @@ export default class ExtensionsPage extends Page {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
{Button.component({
|
{Button.component({
|
||||||
children: app.translator.trans('core.admin.extensions.add_button'),
|
children: app.translator.trans('core.admin.extensions.add_button'),
|
||||||
icon: 'plus',
|
icon: 'fas fa-plus',
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
onclick: () => app.modal.show(new AddExtensionModal())
|
onclick: () => app.modal.show(new AddExtensionModal())
|
||||||
})}
|
})}
|
||||||
@@ -42,15 +42,18 @@ export default class ExtensionsPage extends Page {
|
|||||||
className="ExtensionListItem-controls"
|
className="ExtensionListItem-controls"
|
||||||
buttonClassName="Button Button--icon Button--flat"
|
buttonClassName="Button Button--icon Button--flat"
|
||||||
menuClassName="Dropdown-menu--right"
|
menuClassName="Dropdown-menu--right"
|
||||||
icon="ellipsis-h">
|
icon="fas fa-ellipsis-h">
|
||||||
{controls}
|
{controls}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
) : ''}
|
) : ''}
|
||||||
<label className="ExtensionListItem-title">
|
<div className="ExtensionListItem-main">
|
||||||
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '}
|
<label className="ExtensionListItem-title">
|
||||||
{extension.extra['flarum-extension'].title}
|
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '}
|
||||||
</label>
|
{extension.extra['flarum-extension'].title}
|
||||||
<div className="ExtensionListItem-version">{extension.version}</div>
|
</label>
|
||||||
|
<div className="ExtensionListItem-version">{extension.version}</div>
|
||||||
|
<div className="ExtensionListItem-description">{extension.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>;
|
</li>;
|
||||||
})}
|
})}
|
||||||
@@ -67,7 +70,7 @@ export default class ExtensionsPage extends Page {
|
|||||||
|
|
||||||
if (app.extensionSettings[name]) {
|
if (app.extensionSettings[name]) {
|
||||||
items.add('settings', Button.component({
|
items.add('settings', Button.component({
|
||||||
icon: 'cog',
|
icon: 'fas fa-cog',
|
||||||
children: app.translator.trans('core.admin.extensions.settings_button'),
|
children: app.translator.trans('core.admin.extensions.settings_button'),
|
||||||
onclick: app.extensionSettings[name]
|
onclick: app.extensionSettings[name]
|
||||||
}));
|
}));
|
||||||
@@ -75,7 +78,7 @@ export default class ExtensionsPage extends Page {
|
|||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
items.add('uninstall', Button.component({
|
items.add('uninstall', Button.component({
|
||||||
icon: 'trash-o',
|
icon: 'far fa-trash-alt',
|
||||||
children: app.translator.trans('core.admin.extensions.uninstall_button'),
|
children: app.translator.trans('core.admin.extensions.uninstall_button'),
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
app.request({
|
app.request({
|
@@ -1,6 +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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `HeaderPrimary` component displays primary header controls. On the
|
* The `HeaderPrimary` component displays primary header controls. On the
|
37
js/src/admin/components/HeaderSecondary.js
Normal file
37
js/src/admin/components/HeaderSecondary.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import Component from '../../common/Component';
|
||||||
|
import SessionDropdown from './SessionDropdown';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `HeaderSecondary` component displays secondary header controls.
|
||||||
|
*/
|
||||||
|
export default class HeaderSecondary extends Component {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<ul className="Header-controls">
|
||||||
|
{listItems(this.items().toArray())}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @return {ItemList}
|
||||||
|
*/
|
||||||
|
items() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('session', SessionDropdown.component());
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
@@ -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() {
|
@@ -1,37 +1,56 @@
|
|||||||
import Page from 'flarum/components/Page';
|
import Page from './Page';
|
||||||
import FieldSet from 'flarum/components/FieldSet';
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Alert from 'flarum/components/Alert';
|
import Alert from '../../common/components/Alert';
|
||||||
import saveSettings from 'flarum/utils/saveSettings';
|
import Select from '../../common/components/Select';
|
||||||
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
export default class MailPage extends Page {
|
export default class MailPage extends Page {
|
||||||
init() {
|
init() {
|
||||||
super.init();
|
super.init();
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = true;
|
||||||
|
this.saving = false;
|
||||||
|
|
||||||
this.fields = [
|
this.driverFields = {};
|
||||||
'mail_driver',
|
this.fields = ['mail_driver', 'mail_from'];
|
||||||
'mail_host',
|
|
||||||
'mail_from',
|
|
||||||
'mail_port',
|
|
||||||
'mail_username',
|
|
||||||
'mail_password',
|
|
||||||
'mail_encryption'
|
|
||||||
];
|
|
||||||
this.values = {};
|
this.values = {};
|
||||||
|
|
||||||
const settings = app.data.settings;
|
const settings = app.data.settings;
|
||||||
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
|
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
|
||||||
|
|
||||||
this.localeOptions = {};
|
app.request({
|
||||||
const locales = app.locales;
|
method: 'GET',
|
||||||
for (const i in locales) {
|
url: app.forum.attribute('apiUrl') + '/mail-drivers'
|
||||||
this.localeOptions[i] = `${locales[i]} (${i})`;
|
}).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() {
|
view() {
|
||||||
|
if (this.loading) {
|
||||||
|
return (
|
||||||
|
<div className="MailPage">
|
||||||
|
<div className="container">
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="MailPage">
|
<div className="MailPage">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@@ -41,36 +60,6 @@ export default class MailPage extends Page {
|
|||||||
{app.translator.trans('core.admin.email.text')}
|
{app.translator.trans('core.admin.email.text')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{FieldSet.component({
|
|
||||||
label: app.translator.trans('core.admin.email.server_heading'),
|
|
||||||
className: 'MailPage-MailSettings',
|
|
||||||
children: [
|
|
||||||
<div className="MailPage-MailSettings-input">
|
|
||||||
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
|
|
||||||
<input className="FormControl" value={this.values.mail_driver() || ''} oninput={m.withAttr('value', this.values.mail_driver)} />
|
|
||||||
<label>{app.translator.trans('core.admin.email.host_label')}</label>
|
|
||||||
<input className="FormControl" value={this.values.mail_host() || ''} oninput={m.withAttr('value', this.values.mail_host)} />
|
|
||||||
<label>{app.translator.trans('core.admin.email.port_label')}</label>
|
|
||||||
<input className="FormControl" value={this.values.mail_port() || ''} oninput={m.withAttr('value', this.values.mail_port)} />
|
|
||||||
<label>{app.translator.trans('core.admin.email.encryption_label')}</label>
|
|
||||||
<input className="FormControl" value={this.values.mail_encryption() || ''} oninput={m.withAttr('value', this.values.mail_encryption)} />
|
|
||||||
</div>
|
|
||||||
]
|
|
||||||
})}
|
|
||||||
|
|
||||||
{FieldSet.component({
|
|
||||||
label: app.translator.trans('core.admin.email.account_heading'),
|
|
||||||
className: 'MailPage-MailSettings',
|
|
||||||
children: [
|
|
||||||
<div className="MailPage-MailSettings-input">
|
|
||||||
<label>{app.translator.trans('core.admin.email.username_label')}</label>
|
|
||||||
<input className="FormControl" value={this.values.mail_username() || ''} oninput={m.withAttr('value', this.values.mail_username)} />
|
|
||||||
<label>{app.translator.trans('core.admin.email.password_label')}</label>
|
|
||||||
<input className="FormControl" value={this.values.mail_password() || ''} oninput={m.withAttr('value', this.values.mail_password)} />
|
|
||||||
</div>
|
|
||||||
]
|
|
||||||
})}
|
|
||||||
|
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: app.translator.trans('core.admin.email.addresses_heading'),
|
label: app.translator.trans('core.admin.email.addresses_heading'),
|
||||||
className: 'MailPage-MailSettings',
|
className: 'MailPage-MailSettings',
|
||||||
@@ -82,11 +71,35 @@ export default class MailPage extends Page {
|
|||||||
]
|
]
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{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({
|
{Button.component({
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
children: app.translator.trans('core.admin.email.submit_button'),
|
children: app.translator.trans('core.admin.email.submit_button'),
|
||||||
loading: this.loading,
|
loading: this.saving,
|
||||||
disabled: !this.changed()
|
disabled: !this.changed()
|
||||||
})}
|
})}
|
||||||
</form>
|
</form>
|
||||||
@@ -102,9 +115,9 @@ export default class MailPage extends Page {
|
|||||||
onsubmit(e) {
|
onsubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (this.loading) return;
|
if (this.saving) return;
|
||||||
|
|
||||||
this.loading = true;
|
this.saving = true;
|
||||||
app.alerts.dismiss(this.successAlert);
|
app.alerts.dismiss(this.successAlert);
|
||||||
|
|
||||||
const settings = {};
|
const settings = {};
|
||||||
@@ -117,7 +130,7 @@ export default class MailPage extends Page {
|
|||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.loading = false;
|
this.saving = false;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
});
|
});
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import Component from 'flarum/Component';
|
import Component from '../../common/Component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Page` component
|
* The `Page` component
|
@@ -1,9 +1,9 @@
|
|||||||
import Dropdown from 'flarum/components/Dropdown';
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Separator from 'flarum/components/Separator';
|
import Separator from '../../common/components/Separator';
|
||||||
import Group from 'flarum/models/Group';
|
import Group from '../../common/models/Group';
|
||||||
import Badge from 'flarum/components/Badge';
|
import Badge from '../../common/components/Badge';
|
||||||
import GroupBadge from 'flarum/components/GroupBadge';
|
import GroupBadge from '../../common/components/GroupBadge';
|
||||||
|
|
||||||
function badgeForId(id) {
|
function badgeForId(id) {
|
||||||
const group = app.store.getById('groups', id);
|
const group = app.store.getById('groups', id);
|
||||||
@@ -52,9 +52,9 @@ export default class PermissionDropdown extends Dropdown {
|
|||||||
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
||||||
|
|
||||||
if (everyone) {
|
if (everyone) {
|
||||||
this.props.label = Badge.component({icon: 'globe'});
|
this.props.label = Badge.component({icon: 'fas fa-globe'});
|
||||||
} else if (members) {
|
} else if (members) {
|
||||||
this.props.label = Badge.component({icon: 'user'});
|
this.props.label = Badge.component({icon: 'fas fa-user'});
|
||||||
} else {
|
} else {
|
||||||
this.props.label = [
|
this.props.label = [
|
||||||
badgeForId(Group.ADMINISTRATOR_ID),
|
badgeForId(Group.ADMINISTRATOR_ID),
|
||||||
@@ -66,8 +66,8 @@ export default class PermissionDropdown extends Dropdown {
|
|||||||
if (this.props.allowGuest) {
|
if (this.props.allowGuest) {
|
||||||
this.props.children.push(
|
this.props.children.push(
|
||||||
Button.component({
|
Button.component({
|
||||||
children: [Badge.component({icon: 'globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
|
children: [Badge.component({icon: 'fas fa-globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
|
||||||
icon: everyone ? 'check' : true,
|
icon: everyone ? 'fas fa-check' : true,
|
||||||
onclick: () => this.save([Group.GUEST_ID]),
|
onclick: () => this.save([Group.GUEST_ID]),
|
||||||
disabled: this.isGroupDisabled(Group.GUEST_ID)
|
disabled: this.isGroupDisabled(Group.GUEST_ID)
|
||||||
})
|
})
|
||||||
@@ -76,8 +76,8 @@ export default class PermissionDropdown extends Dropdown {
|
|||||||
|
|
||||||
this.props.children.push(
|
this.props.children.push(
|
||||||
Button.component({
|
Button.component({
|
||||||
children: [Badge.component({icon: 'user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
|
children: [Badge.component({icon: 'fas fa-user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
|
||||||
icon: members ? 'check' : true,
|
icon: members ? 'fas fa-check' : true,
|
||||||
onclick: () => this.save([Group.MEMBER_ID]),
|
onclick: () => this.save([Group.MEMBER_ID]),
|
||||||
disabled: this.isGroupDisabled(Group.MEMBER_ID)
|
disabled: this.isGroupDisabled(Group.MEMBER_ID)
|
||||||
}),
|
}),
|
||||||
@@ -86,7 +86,7 @@ export default class PermissionDropdown extends Dropdown {
|
|||||||
|
|
||||||
Button.component({
|
Button.component({
|
||||||
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
|
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
|
||||||
icon: !everyone && !members ? 'check' : true,
|
icon: !everyone && !members ? 'fas fa-check' : true,
|
||||||
disabled: !everyone && !members,
|
disabled: !everyone && !members,
|
||||||
onclick: e => {
|
onclick: e => {
|
||||||
if (e.shiftKey) e.stopPropagation();
|
if (e.shiftKey) e.stopPropagation();
|
||||||
@@ -101,7 +101,7 @@ export default class PermissionDropdown extends Dropdown {
|
|||||||
.filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
.filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||||
.map(group => Button.component({
|
.map(group => Button.component({
|
||||||
children: [badgeForId(group.id()), ' ', group.namePlural()],
|
children: [badgeForId(group.id()), ' ', group.namePlural()],
|
||||||
icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true,
|
icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
if (e.shiftKey) e.stopPropagation();
|
if (e.shiftKey) e.stopPropagation();
|
||||||
this.toggle(group.id());
|
this.toggle(group.id());
|
@@ -1,9 +1,9 @@
|
|||||||
import Component from 'flarum/Component';
|
import Component from '../../common/Component';
|
||||||
import PermissionDropdown from 'flarum/components/PermissionDropdown';
|
import PermissionDropdown from './PermissionDropdown';
|
||||||
import SettingDropdown from 'flarum/components/SettingDropdown';
|
import SettingDropdown from './SettingDropdown';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import icon from 'flarum/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
|
|
||||||
export default class PermissionGrid extends Component {
|
export default class PermissionGrid extends Component {
|
||||||
init() {
|
init() {
|
||||||
@@ -29,7 +29,7 @@ export default class PermissionGrid extends Component {
|
|||||||
{scopes.map(scope => (
|
{scopes.map(scope => (
|
||||||
<th>
|
<th>
|
||||||
{scope.label}{' '}
|
{scope.label}{' '}
|
||||||
{scope.onremove ? Button.component({icon: 'times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
|
{scope.onremove ? Button.component({icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
<th>{this.scopeControlItems().toArray()}</th>
|
<th>{this.scopeControlItems().toArray()}</th>
|
||||||
@@ -85,21 +85,21 @@ export default class PermissionGrid extends Component {
|
|||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add('viewDiscussions', {
|
items.add('viewDiscussions', {
|
||||||
icon: 'eye',
|
icon: 'fas fa-eye',
|
||||||
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
|
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
|
||||||
permission: 'viewDiscussions',
|
permission: 'viewDiscussions',
|
||||||
allowGuest: true
|
allowGuest: true
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
items.add('viewUserList', {
|
items.add('viewUserList', {
|
||||||
icon: 'users',
|
icon: 'fas fa-users',
|
||||||
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
|
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
|
||||||
permission: 'viewUserList',
|
permission: 'viewUserList',
|
||||||
allowGuest: true
|
allowGuest: true
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
items.add('signUp', {
|
items.add('signUp', {
|
||||||
icon: 'user-plus',
|
icon: 'fas fa-user-plus',
|
||||||
label: app.translator.trans('core.admin.permissions.sign_up_label'),
|
label: app.translator.trans('core.admin.permissions.sign_up_label'),
|
||||||
setting: () => SettingDropdown.component({
|
setting: () => SettingDropdown.component({
|
||||||
key: 'allow_sign_up',
|
key: 'allow_sign_up',
|
||||||
@@ -110,6 +110,12 @@ export default class PermissionGrid extends Component {
|
|||||||
})
|
})
|
||||||
}, 90);
|
}, 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;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,13 +123,13 @@ export default class PermissionGrid extends Component {
|
|||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add('start', {
|
items.add('start', {
|
||||||
icon: 'edit',
|
icon: 'fas fa-edit',
|
||||||
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
|
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
|
||||||
permission: 'startDiscussion'
|
permission: 'startDiscussion'
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
items.add('allowRenaming', {
|
items.add('allowRenaming', {
|
||||||
icon: 'i-cursor',
|
icon: 'fas fa-i-cursor',
|
||||||
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
|
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
|
||||||
setting: () => {
|
setting: () => {
|
||||||
const minutes = parseInt(app.data.settings.allow_renaming, 10);
|
const minutes = parseInt(app.data.settings.allow_renaming, 10);
|
||||||
@@ -149,13 +155,13 @@ export default class PermissionGrid extends Component {
|
|||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add('reply', {
|
items.add('reply', {
|
||||||
icon: 'reply',
|
icon: 'fas fa-reply',
|
||||||
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
|
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
|
||||||
permission: 'discussion.reply'
|
permission: 'discussion.reply'
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
items.add('allowPostEditing', {
|
items.add('allowPostEditing', {
|
||||||
icon: 'pencil',
|
icon: 'fas fa-pencil-alt',
|
||||||
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
|
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
|
||||||
setting: () => {
|
setting: () => {
|
||||||
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
|
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
|
||||||
@@ -181,37 +187,43 @@ export default class PermissionGrid extends Component {
|
|||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add('viewIpsPosts', {
|
items.add('viewIpsPosts', {
|
||||||
icon: 'bullseye',
|
icon: 'fas fa-bullseye',
|
||||||
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
|
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
|
||||||
permission: 'discussion.viewIpsPosts'
|
permission: 'discussion.viewIpsPosts'
|
||||||
}, 110);
|
}, 110);
|
||||||
|
|
||||||
items.add('renameDiscussions', {
|
items.add('renameDiscussions', {
|
||||||
icon: 'i-cursor',
|
icon: 'fas fa-i-cursor',
|
||||||
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
|
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
|
||||||
permission: 'discussion.rename'
|
permission: 'discussion.rename'
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
items.add('hideDiscussions', {
|
items.add('hideDiscussions', {
|
||||||
icon: 'trash-o',
|
icon: 'far fa-trash-alt',
|
||||||
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
|
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
|
||||||
permission: 'discussion.hide'
|
permission: 'discussion.hide'
|
||||||
}, 90);
|
}, 90);
|
||||||
|
|
||||||
items.add('deleteDiscussions', {
|
items.add('deleteDiscussions', {
|
||||||
icon: 'times',
|
icon: 'fas fa-times',
|
||||||
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
|
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
|
||||||
permission: 'discussion.delete'
|
permission: 'discussion.delete'
|
||||||
}, 80);
|
}, 80);
|
||||||
|
|
||||||
items.add('editPosts', {
|
items.add('editPosts', {
|
||||||
icon: 'pencil',
|
icon: 'fas fa-pencil-alt',
|
||||||
label: app.translator.trans('core.admin.permissions.edit_and_delete_posts_label'),
|
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
|
||||||
permission: 'discussion.editPosts'
|
permission: 'discussion.editPosts'
|
||||||
}, 70);
|
}, 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', {
|
items.add('deletePosts', {
|
||||||
icon: 'times',
|
icon: 'fas fa-times',
|
||||||
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
|
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
|
||||||
permission: 'discussion.deletePosts'
|
permission: 'discussion.deletePosts'
|
||||||
}, 60);
|
}, 60);
|
@@ -1,9 +1,9 @@
|
|||||||
import Page from 'flarum/components/Page';
|
import Page from './Page';
|
||||||
import GroupBadge from 'flarum/components/GroupBadge';
|
import GroupBadge from '../../common/components/GroupBadge';
|
||||||
import EditGroupModal from 'flarum/components/EditGroupModal';
|
import EditGroupModal from './EditGroupModal';
|
||||||
import Group from 'flarum/models/Group';
|
import Group from '../../common/models/Group';
|
||||||
import icon from 'flarum/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
import PermissionGrid from 'flarum/components/PermissionGrid';
|
import PermissionGrid from './PermissionGrid';
|
||||||
|
|
||||||
export default class PermissionsPage extends Page {
|
export default class PermissionsPage extends Page {
|
||||||
view() {
|
view() {
|
||||||
@@ -24,7 +24,7 @@ export default class PermissionsPage extends Page {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
|
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
|
||||||
{icon('plus', {className: 'Group-icon'})}
|
{icon('fas fa-plus', {className: 'Group-icon'})}
|
||||||
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
|
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
@@ -1,8 +1,8 @@
|
|||||||
import avatar from 'flarum/helpers/avatar';
|
import avatar from '../../common/helpers/avatar';
|
||||||
import username from 'flarum/helpers/username';
|
import username from '../../common/helpers/username';
|
||||||
import Dropdown from 'flarum/components/Dropdown';
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `SessionDropdown` component shows a button with the current user's
|
* The `SessionDropdown` component shows a button with the current user's
|
||||||
@@ -42,7 +42,7 @@ export default class SessionDropdown extends Dropdown {
|
|||||||
|
|
||||||
items.add('logOut',
|
items.add('logOut',
|
||||||
Button.component({
|
Button.component({
|
||||||
icon: 'sign-out',
|
icon: 'fas fa-sign-out-alt',
|
||||||
children: app.translator.trans('core.admin.header.log_out_button'),
|
children: app.translator.trans('core.admin.header.log_out_button'),
|
||||||
onclick: app.session.logout.bind(app.session)
|
onclick: app.session.logout.bind(app.session)
|
||||||
}),
|
}),
|
@@ -1,6 +1,6 @@
|
|||||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import saveSettings from 'flarum/utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
export default class SettingDropdown extends SelectDropdown {
|
export default class SettingDropdown extends SelectDropdown {
|
||||||
static initProps(props) {
|
static initProps(props) {
|
||||||
@@ -8,7 +8,7 @@ export default class SettingDropdown extends SelectDropdown {
|
|||||||
|
|
||||||
props.className = 'SettingDropdown';
|
props.className = 'SettingDropdown';
|
||||||
props.buttonClassName = 'Button Button--text';
|
props.buttonClassName = 'Button Button--text';
|
||||||
props.caretIcon = 'caret-down';
|
props.caretIcon = 'fas fa-caret-down';
|
||||||
props.defaultLabel = 'Custom';
|
props.defaultLabel = 'Custom';
|
||||||
|
|
||||||
props.children = props.options.map(({value, label}) => {
|
props.children = props.options.map(({value, label}) => {
|
||||||
@@ -16,7 +16,7 @@ export default class SettingDropdown extends SelectDropdown {
|
|||||||
|
|
||||||
return Button.component({
|
return Button.component({
|
||||||
children: label,
|
children: label,
|
||||||
icon: active ? 'check' : true,
|
icon: active ? 'fas fa-check' : true,
|
||||||
onclick: saveSettings.bind(this, {[props.key]: value}),
|
onclick: saveSettings.bind(this, {[props.key]: value}),
|
||||||
active
|
active
|
||||||
});
|
});
|
@@ -1,6 +1,6 @@
|
|||||||
import Modal from 'flarum/components/Modal';
|
import Modal from '../../common/components/Modal';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import saveSettings from 'flarum/utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
export default class SettingsModal extends Modal {
|
export default class SettingsModal extends Modal {
|
||||||
init() {
|
init() {
|
58
js/src/admin/components/StatusWidget.js
Normal file
58
js/src/admin/components/StatusWidget.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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 DashboardWidget from './DashboardWidget';
|
||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import LoadingModal from './LoadingModal';
|
||||||
|
|
||||||
|
export default class StatusWidget extends DashboardWidget {
|
||||||
|
className() {
|
||||||
|
return 'StatusWidget';
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return (
|
||||||
|
<ul>{listItems(this.items().toArray())}</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('tools', (
|
||||||
|
<Dropdown
|
||||||
|
label={app.translator.trans('core.admin.dashboard.tools_button')}
|
||||||
|
icon="fas fa-cog"
|
||||||
|
buttonClassName="Button"
|
||||||
|
menuClassName="Dropdown-menu--right">
|
||||||
|
<Button onclick={this.handleClearCache.bind(this)}>
|
||||||
|
{app.translator.trans('core.admin.dashboard.clear_cache_button')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
));
|
||||||
|
|
||||||
|
items.add('version-flarum', [<strong>Flarum</strong>, <br/>, app.forum.attribute('version')]);
|
||||||
|
items.add('version-php', [<strong>PHP</strong>, <br/>, app.data.phpVersion]);
|
||||||
|
items.add('version-mysql', [<strong>MySQL</strong>, <br/>, app.data.mysqlVersion]);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClearCache(e) {
|
||||||
|
app.modal.show(new LoadingModal());
|
||||||
|
|
||||||
|
app.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: app.forum.attribute('apiUrl') + '/cache'
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import Button from 'flarum/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
|
|
||||||
export default class UploadImageButton extends Button {
|
export default class UploadImageButton extends Button {
|
||||||
init() {
|
init() {
|
38
js/src/admin/components/Widget.js
Normal file
38
js/src/admin/components/Widget.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 DashboardWidget extends Component {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className={"DashboardWidget "+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 [];
|
||||||
|
}
|
||||||
|
}
|
18
js/src/admin/index.js
Normal file
18
js/src/admin/index.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import AdminApplication from './AdminApplication';
|
||||||
|
|
||||||
|
const app = new AdminApplication();
|
||||||
|
|
||||||
|
// Backwards compatibility
|
||||||
|
window.app = app;
|
||||||
|
|
||||||
|
export { app };
|
||||||
|
|
||||||
|
// Export public API
|
||||||
|
|
||||||
|
|
||||||
|
// Export compat API
|
||||||
|
import compat from './compat';
|
||||||
|
|
||||||
|
compat.app = app;
|
||||||
|
|
||||||
|
export { compat };
|
22
js/src/admin/routes.js
Normal file
22
js/src/admin/routes.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import DashboardPage from './components/DashboardPage';
|
||||||
|
import BasicsPage from './components/BasicsPage';
|
||||||
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
|
import AppearancePage from './components/AppearancePage';
|
||||||
|
import ExtensionsPage from './components/ExtensionsPage';
|
||||||
|
import MailPage from './components/MailPage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `routes` initializer defines the forum 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()},
|
||||||
|
'mail': {path: '/mail', component: MailPage.component()}
|
||||||
|
};
|
||||||
|
}
|
14
js/src/admin/utils/saveSettings.js
Normal file
14
js/src/admin/utils/saveSettings.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default function saveSettings(settings) {
|
||||||
|
const oldSettings = JSON.parse(JSON.stringify(app.data.settings));
|
||||||
|
|
||||||
|
Object.assign(app.data.settings, settings);
|
||||||
|
|
||||||
|
return app.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: app.forum.attribute('apiUrl') + '/settings',
|
||||||
|
data: settings
|
||||||
|
}).catch(error => {
|
||||||
|
app.data.settings = oldSettings;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
363
js/src/common/Application.js
Normal file
363
js/src/common/Application.js
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
import ItemList from './utils/ItemList';
|
||||||
|
import Alert from './components/Alert';
|
||||||
|
import ModalManager from './components/ModalManager';
|
||||||
|
import AlertManager from './components/AlertManager';
|
||||||
|
import Translator from './Translator';
|
||||||
|
import Store from './Store';
|
||||||
|
import Session from './Session';
|
||||||
|
import extract from './utils/extract';
|
||||||
|
import Drawer from './utils/Drawer';
|
||||||
|
import mapRoutes from './utils/mapRoutes';
|
||||||
|
import RequestError from './utils/RequestError';
|
||||||
|
import ScrollListener from './utils/ScrollListener';
|
||||||
|
import { extend } from './extend';
|
||||||
|
|
||||||
|
import Forum from './models/Forum';
|
||||||
|
import User from './models/User';
|
||||||
|
import Discussion from './models/Discussion';
|
||||||
|
import Post from './models/Post';
|
||||||
|
import Group from './models/Group';
|
||||||
|
import Notification from './models/Notification';
|
||||||
|
import { flattenDeep } from 'lodash-es';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Application {
|
||||||
|
/**
|
||||||
|
* The forum model for this application.
|
||||||
|
*
|
||||||
|
* @type {Forum}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
routes = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ordered list of initializers to bootstrap the application.
|
||||||
|
*
|
||||||
|
* @type {ItemList}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
initializers = new ItemList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The app's session.
|
||||||
|
*
|
||||||
|
* @type {Session}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
session = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The app's translator.
|
||||||
|
*
|
||||||
|
* @type {Translator}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
translator = new Translator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The app's data store.
|
||||||
|
*
|
||||||
|
* @type {Store}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
store = new Store({
|
||||||
|
forums: Forum,
|
||||||
|
users: User,
|
||||||
|
discussions: Discussion,
|
||||||
|
posts: Post,
|
||||||
|
groups: Group,
|
||||||
|
notifications: Notification
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local cache that can be used to store data at the application level, so
|
||||||
|
* that is persists between different routes.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
cache = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the app has been booted.
|
||||||
|
*
|
||||||
|
* @type {Boolean}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
requestError = null;
|
||||||
|
|
||||||
|
data;
|
||||||
|
|
||||||
|
title = '';
|
||||||
|
titleCount = 0;
|
||||||
|
|
||||||
|
load(payload) {
|
||||||
|
this.data = payload;
|
||||||
|
this.translator.locale = payload.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
boot() {
|
||||||
|
this.initializers.toArray().forEach(initializer => initializer(this));
|
||||||
|
|
||||||
|
this.store.pushPayload({data: this.data.resources});
|
||||||
|
|
||||||
|
this.forum = this.store.getById('forums', 1);
|
||||||
|
|
||||||
|
this.session = new Session(
|
||||||
|
this.store.getById('users', this.data.session.userId),
|
||||||
|
this.data.session.csrfToken
|
||||||
|
);
|
||||||
|
|
||||||
|
this.mount();
|
||||||
|
}
|
||||||
|
|
||||||
|
bootExtensions(extensions) {
|
||||||
|
Object.keys(extensions).forEach(name => {
|
||||||
|
const extension = extensions[name];
|
||||||
|
|
||||||
|
const extenders = flattenDeep(extension.extend);
|
||||||
|
|
||||||
|
for (const extender of extenders) {
|
||||||
|
extender.extend(this, { name, exports: extension });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(basePath = '') {
|
||||||
|
this.modal = m.mount(document.getElementById('modal'), <ModalManager/>);
|
||||||
|
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>);
|
||||||
|
|
||||||
|
this.drawer = new Drawer();
|
||||||
|
|
||||||
|
m.route(
|
||||||
|
document.getElementById('content'),
|
||||||
|
basePath + '/',
|
||||||
|
mapRoutes(this.routes, basePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the API response document that has been preloaded into the application.
|
||||||
|
*
|
||||||
|
* @return {Object|null}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
preloadedApiDocument() {
|
||||||
|
if (this.data.apiDocument) {
|
||||||
|
const results = this.store.pushPayload(this.data.apiDocument);
|
||||||
|
|
||||||
|
this.data.apiDocument = null;
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the <title> of the page.
|
||||||
|
*
|
||||||
|
* @param {String} 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(originalOptions) {
|
||||||
|
const options = Object.assign({}, originalOptions);
|
||||||
|
|
||||||
|
// 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.background = options.background || true;
|
||||||
|
|
||||||
|
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken));
|
||||||
|
|
||||||
|
// If the method is something like PATCH or DELETE, which not all servers
|
||||||
|
// and clients support, then we'll send it as a POST request with the
|
||||||
|
// intended method specified in the X-HTTP-Method-Override header.
|
||||||
|
if (options.method !== 'GET' && options.method !== 'POST') {
|
||||||
|
const method = options.method;
|
||||||
|
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-HTTP-Method-Override', method));
|
||||||
|
options.method = 'POST';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 => responseText);
|
||||||
|
|
||||||
|
options.errorHandler = options.errorHandler || (error => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 => {
|
||||||
|
let responseText;
|
||||||
|
|
||||||
|
if (original) {
|
||||||
|
responseText = original(xhr.responseText);
|
||||||
|
} else {
|
||||||
|
responseText = xhr.responseText || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = xhr.status;
|
||||||
|
|
||||||
|
if (status < 200 || status > 299) {
|
||||||
|
throw new RequestError(status, responseText, options, xhr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.getResponseHeader) {
|
||||||
|
const csrfToken = xhr.getResponseHeader('X-CSRF-Token');
|
||||||
|
if (csrfToken) app.session.csrfToken = csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(responseText);
|
||||||
|
} catch (e) {
|
||||||
|
throw new RequestError(500, responseText, options, xhr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.requestError) this.alerts.dismiss(this.requestError.alert);
|
||||||
|
|
||||||
|
// Now make the request. If it's a failure, inspect the error that was
|
||||||
|
// returned and show an alert containing its contents.
|
||||||
|
const deferred = m.deferred();
|
||||||
|
|
||||||
|
m.request(options).then(response => deferred.resolve(response), error => {
|
||||||
|
this.requestError = error;
|
||||||
|
|
||||||
|
let children;
|
||||||
|
|
||||||
|
switch (error.status) {
|
||||||
|
case 422:
|
||||||
|
children = error.response.errors
|
||||||
|
.map(error => [error.detail, <br/>])
|
||||||
|
.reduce((a, b) => a.concat(b), [])
|
||||||
|
.slice(0, -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 401:
|
||||||
|
case 403:
|
||||||
|
children = app.translator.trans('core.lib.error.permission_denied_message');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 404:
|
||||||
|
case 410:
|
||||||
|
children = app.translator.trans('core.lib.error.not_found_message');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 429:
|
||||||
|
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
children = app.translator.trans('core.lib.error.generic_message');
|
||||||
|
}
|
||||||
|
|
||||||
|
error.alert = new Alert({
|
||||||
|
type: 'error',
|
||||||
|
children
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
options.errorHandler(error);
|
||||||
|
} catch (error) {
|
||||||
|
this.alerts.show(error.alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
deferred.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 : '');
|
||||||
|
}
|
||||||
|
}
|
225
js/src/common/Component.js
Normal file
225
js/src/common/Component.js
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Component` class defines a user interface 'building block'. A component
|
||||||
|
* can generate a virtual DOM to be rendered on each redraw.
|
||||||
|
*
|
||||||
|
* An instance's virtual DOM can be retrieved directly using the {@link
|
||||||
|
* Component#render} method.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* this.myComponentInstance = new MyComponent({foo: 'bar'});
|
||||||
|
* return m('div', this.myComponentInstance.render());
|
||||||
|
*
|
||||||
|
* Alternatively, components can be nested, letting Mithril take care of
|
||||||
|
* instance persistence. For this, the static {@link Component.component} method
|
||||||
|
* can be used.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* return m('div', MyComponent.component({foo: 'bar'));
|
||||||
|
*
|
||||||
|
* @see https://lhorie.github.io/mithril/mithril.component.html
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
export default class Component {
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array|Object} children
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
constructor(props = {}, children = null) {
|
||||||
|
if (children) props.children = children;
|
||||||
|
|
||||||
|
this.constructor.initProps(props);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties passed into the component.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
this.props = props;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root DOM element for the component.
|
||||||
|
*
|
||||||
|
* @type DOMElement
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.element = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to retain the component's subtree on redraw.
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.retain = false;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the component is constructed.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the component is destroyed, i.e. after a redraw where it is no
|
||||||
|
* longer a part of the view.
|
||||||
|
*
|
||||||
|
* @see https://lhorie.github.io/mithril/mithril.component.html#unloading-components
|
||||||
|
* @param {Object} e
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
onunload() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the renderable virtual DOM that represents the component's view.
|
||||||
|
*
|
||||||
|
* This should NOT be overridden by subclasses. Subclasses wishing to define
|
||||||
|
* their virtual DOM should override Component#view instead.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* this.myComponentInstance = new MyComponent({foo: 'bar'});
|
||||||
|
* return m('div', this.myComponentInstance.render());
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
* @final
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const vdom = this.retain ? {subtree: 'retain'} : this.view();
|
||||||
|
|
||||||
|
// Override the root element's config attribute with our own function, which
|
||||||
|
// will set the component instance's element property to the root DOM
|
||||||
|
// element, and then run the component class' config method.
|
||||||
|
vdom.attrs = vdom.attrs || {};
|
||||||
|
|
||||||
|
const originalConfig = vdom.attrs.config;
|
||||||
|
|
||||||
|
vdom.attrs.config = (...args) => {
|
||||||
|
this.element = args[0];
|
||||||
|
this.config.apply(this, args.slice(1));
|
||||||
|
if (originalConfig) originalConfig.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
return vdom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a jQuery object for this component's element. If you pass in a
|
||||||
|
* selector string, this method will return a jQuery object, using the current
|
||||||
|
* element as its buffer.
|
||||||
|
*
|
||||||
|
* For example, calling `component.$('li')` will return a jQuery object
|
||||||
|
* containing all of the `li` elements inside the DOM element of this
|
||||||
|
* component.
|
||||||
|
*
|
||||||
|
* @param {String} [selector] a jQuery-compatible selector string
|
||||||
|
* @returns {jQuery} the jQuery object for the DOM node
|
||||||
|
* @final
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
$(selector) {
|
||||||
|
const $element = $(this.element);
|
||||||
|
|
||||||
|
return selector ? $element.find(selector) : $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the component's root element is redrawn. This hook can be used
|
||||||
|
* to perform any actions on the DOM, both on the initial draw and any
|
||||||
|
* subsequent redraws. See Mithril's documentation for more information.
|
||||||
|
*
|
||||||
|
* @see https://lhorie.github.io/mithril/mithril.html#the-config-attribute
|
||||||
|
* @param {Boolean} isInitialized
|
||||||
|
* @param {Object} context
|
||||||
|
* @param {Object} vdom
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
config() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the virtual DOM that represents the component's view.
|
||||||
|
*
|
||||||
|
* @return {Object} The virtual DOM
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
view() {
|
||||||
|
throw new Error('Component#view must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a Mithril component object for this component, preloaded with props.
|
||||||
|
*
|
||||||
|
* @see https://lhorie.github.io/mithril/mithril.component.html
|
||||||
|
* @param {Object} [props] Properties to set on the component
|
||||||
|
* @param children
|
||||||
|
* @return {Object} The Mithril component object
|
||||||
|
* @property {function} controller
|
||||||
|
* @property {function} view
|
||||||
|
* @property {Object} component The class of this component
|
||||||
|
* @property {Object} props The props that were passed to the component
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static component(props = {}, children = null) {
|
||||||
|
const componentProps = Object.assign({}, props);
|
||||||
|
|
||||||
|
if (children) componentProps.children = children;
|
||||||
|
|
||||||
|
this.initProps(componentProps);
|
||||||
|
|
||||||
|
// Set up a function for Mithril to get the component's view. It will accept
|
||||||
|
// the component's controller (which happens to be the component itself, in
|
||||||
|
// our case), update its props with the ones supplied, and then render the view.
|
||||||
|
const view = (component) => {
|
||||||
|
component.props = componentProps;
|
||||||
|
return component.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mithril uses this property on the view function to cache component
|
||||||
|
// controllers between redraws, thus persisting component state.
|
||||||
|
view.$original = this.prototype.view;
|
||||||
|
|
||||||
|
// Our output object consists of a controller constructor + a view function
|
||||||
|
// which Mithril will use to instantiate and render the component. We also
|
||||||
|
// attach a reference to the props that were passed through and the
|
||||||
|
// component's class for reference.
|
||||||
|
const output = {
|
||||||
|
controller: this.bind(undefined, componentProps),
|
||||||
|
view: view,
|
||||||
|
props: componentProps,
|
||||||
|
component: this
|
||||||
|
};
|
||||||
|
|
||||||
|
// If a `key` prop was set, then we'll assume that we want that to actually
|
||||||
|
// show up as an attribute on the component object so that Mithril's key
|
||||||
|
// algorithm can be applied.
|
||||||
|
if (componentProps.key) {
|
||||||
|
output.attrs = {key: componentProps.key};
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component's props.
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static initProps(props) {
|
||||||
|
}
|
||||||
|
}
|
49
js/src/common/Session.js
Normal file
49
js/src/common/Session.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* The `Session` class defines the current user session. It stores a reference
|
||||||
|
* to the current authenticated user, and provides methods to log in/out.
|
||||||
|
*/
|
||||||
|
export default class Session {
|
||||||
|
constructor(user, csrfToken) {
|
||||||
|
/**
|
||||||
|
* The current authenticated user.
|
||||||
|
*
|
||||||
|
* @type {User|null}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CSRF token.
|
||||||
|
*
|
||||||
|
* @type {String|null}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.csrfToken = csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to log in a user.
|
||||||
|
*
|
||||||
|
* @param {String} identification The username/email.
|
||||||
|
* @param {String} password
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @return {Promise}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
login(data, options = {}) {
|
||||||
|
return app.request(Object.assign({
|
||||||
|
method: 'POST',
|
||||||
|
url: app.forum.attribute('baseUrl') + '/login',
|
||||||
|
data
|
||||||
|
}, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the user out.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
logout() {
|
||||||
|
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
|
||||||
|
}
|
||||||
|
}
|
@@ -75,7 +75,7 @@ export default class Store {
|
|||||||
* Make a request to the API to find record(s) of a specific type.
|
* Make a request to the API to find record(s) of a specific type.
|
||||||
*
|
*
|
||||||
* @param {String} type The resource type.
|
* @param {String} type The resource type.
|
||||||
* @param {Integer|Integer[]|Object} [id] The ID(s) of the model(s) to retreive.
|
* @param {Integer|Integer[]|Object} [id] The ID(s) of the model(s) to retrieve.
|
||||||
* Alternatively, if an object is passed, it will be handled as the
|
* Alternatively, if an object is passed, it will be handled as the
|
||||||
* `query` parameter.
|
* `query` parameter.
|
||||||
* @param {Object} [query]
|
* @param {Object} [query]
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user