mirror of
https://github.com/flarum/core.git
synced 2025-08-16 05:14:20 +02:00
Compare commits
201 Commits
post_strea
...
1236-user-
Author | SHA1 | Date | |
---|---|---|---|
|
4eaf9ae23f | ||
|
4d80700e97 | ||
|
8ad95a4dfb | ||
|
03faaaedef | ||
|
5c3a0e3e6e | ||
|
588f7498e1 | ||
|
8d1240559b | ||
|
7ad8eb7544 | ||
|
ebd2c69c8d | ||
|
637ae1624c | ||
|
0c9bcba3a6 | ||
|
5b3acfc0d9 | ||
|
7adfb5bd7e | ||
|
6b916065e9 | ||
|
f99f48b155 | ||
|
d2c345c834 | ||
|
966a093911 | ||
|
0560238945 | ||
|
63801484fa | ||
|
f8d92edc9a | ||
|
e1dbfa7d68 | ||
|
ef7623e4ff | ||
|
d7fd076220 | ||
|
30a2421749 | ||
|
1bf6e79b32 | ||
|
8aaa39bd4e | ||
|
b1e11830d1 | ||
|
3375f283eb | ||
|
df4c193ab7 | ||
|
5a8326f442 | ||
|
d8dd870efe | ||
|
60572b93fb | ||
|
810bfdc28f | ||
|
9c4a24b258 | ||
|
18462c079f | ||
|
10d6e653cb | ||
|
4e1f753f59 | ||
|
3c2dd23765 | ||
|
aebc23cba2 | ||
|
f6d03771cb | ||
|
54be9573ac | ||
|
bf46ea3840 | ||
|
bab49650e6 | ||
|
a789c6b4e9 | ||
|
3b3459ad3d | ||
|
223f4d93d4 | ||
|
5d1fe9b815 | ||
|
521834f5da | ||
|
622e2a6644 | ||
|
c5e38a5b1f | ||
|
9d2595d531 | ||
|
3526083320 | ||
|
82562294b7 | ||
|
51ae92f841 | ||
|
6448babaa5 | ||
|
f7feea496d | ||
|
75e624d7ca | ||
|
5a01b63c99 | ||
|
cae4a6eb45 | ||
|
36d6d79011 | ||
|
22c599b283 | ||
|
456f5095da | ||
|
0b3ce2e7d0 | ||
|
607eeb530d | ||
|
5f4efe3c66 | ||
|
fac61b5bce | ||
|
ea64de5952 | ||
|
ff6c407e53 | ||
|
c4ba13f608 | ||
|
cef46ec357 | ||
|
5788c7373e | ||
|
8fdd8a1089 | ||
|
dc31a0a076 | ||
|
29eb233dd1 | ||
|
f6e48fa054 | ||
|
1b5e8f221a | ||
|
f903487ef3 | ||
|
1da4b72eac | ||
|
55bdad55fc | ||
|
b603c7b336 | ||
|
a530c52fb4 | ||
|
f7dc716042 | ||
|
a72f87d7ee | ||
|
5dbe97630c | ||
|
23709a77a2 | ||
|
29a2c247a1 | ||
|
a7f97c14ec | ||
|
8fcd62955b | ||
|
618e91805f | ||
|
431ab9f3e8 | ||
|
4f06133d75 | ||
|
bf79f2474c | ||
|
1b74e43cb9 | ||
|
3643e2010b | ||
|
58299edc20 | ||
|
18774e0b10 | ||
|
86d890d043 | ||
|
ab0ba707e7 | ||
|
04b2cf4462 | ||
|
28e3ec4014 | ||
|
a6decb2350 | ||
|
1e55361539 | ||
|
e80f5429d0 | ||
|
108a23c1eb | ||
|
1dd329982a | ||
|
e0c2ef5e64 | ||
|
d654517c91 | ||
|
0232d949e9 | ||
|
6363753d0f | ||
|
0918b04fe2 | ||
|
929d7b87c1 | ||
|
544f687cf4 | ||
|
a7ed625d16 | ||
|
a67eca0c9e | ||
|
855dd2445a | ||
|
1a3d955b4f | ||
|
8db91e3395 | ||
|
d725012a84 | ||
|
5a03cd865a | ||
|
0a32a96207 | ||
|
1587d48e59 | ||
|
b750554011 | ||
|
db7e28d316 | ||
|
14e89546ca | ||
|
92642519d4 | ||
|
f779f4d092 | ||
|
7b73036441 | ||
|
8b628be507 | ||
|
51f4bcdcb0 | ||
|
47a528305b | ||
|
6121229c6f | ||
|
df7f1291a7 | ||
|
52e73b2481 | ||
|
d08f851c0b | ||
|
22b32bd601 | ||
|
6797770c75 | ||
|
4cab48c0fd | ||
|
f7222d7e20 | ||
|
53c728b184 | ||
|
1d525d0a78 | ||
|
301e571772 | ||
|
e7c12ce928 | ||
|
5d5ebc088e | ||
|
6e62240153 | ||
|
17d1942c5c | ||
|
e786e297ef | ||
|
2829618814 | ||
|
5875b31fd5 | ||
|
ae59bf549f | ||
|
d45bf04341 | ||
|
7f9588af62 | ||
|
17dfb58590 | ||
|
c5e3e26d07 | ||
|
5d768db6d2 | ||
|
6e089c12d4 | ||
|
5ddb843eb2 | ||
|
bbeacc0299 | ||
|
82480457ce | ||
|
685459c0bc | ||
|
347edcf2cd | ||
|
731a038f29 | ||
|
af5113eb7b | ||
|
ddfb2c1ec1 | ||
|
6cf3c1088d | ||
|
2f174edfd0 | ||
|
2c231aa475 | ||
|
1e5c7e54ee | ||
|
408043a203 | ||
|
9b449386d6 | ||
|
f1d9753aee | ||
|
54f733ca80 | ||
|
a737b98e7f | ||
|
80546b9ed7 | ||
|
9758dfac47 | ||
|
970c0f5604 | ||
|
42a7f2f586 | ||
|
3611fa1bb9 | ||
|
c881f9f633 | ||
|
0a22a66189 | ||
|
64b53fb0ac | ||
|
627724839d | ||
|
fa3915fa53 | ||
|
1948b7e6f4 | ||
|
4465e7648b | ||
|
aef56c055a | ||
|
dd3a4173a1 | ||
|
7c204c82ab | ||
|
12fff33763 | ||
|
603367a41a | ||
|
6bdebfbf3c | ||
|
58ab6052ad | ||
|
3737ce8146 | ||
|
ca5404db76 | ||
|
d6fc3a91a6 | ||
|
31134ca16d | ||
|
6cfc9182f4 | ||
|
caa63107ad | ||
|
0acab8f1c7 | ||
|
c19b2e99bd | ||
|
e7a5e1e5e2 | ||
|
209c4c6143 |
31
.github/workflows/lint.yml
vendored
31
.github/workflows/lint.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- 'js/src/**'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'js/src/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
prettier:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
name: JS / Prettier
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: "12"
|
|
||||||
|
|
||||||
- name: Install JS dependencies
|
|
||||||
run: npm ci
|
|
||||||
working-directory: ./js
|
|
||||||
|
|
||||||
- name: Check JS code for formatting
|
|
||||||
run: node_modules/.bin/prettier --check src
|
|
||||||
working-directory: ./js
|
|
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php: [7.2, 7.3, 7.4]
|
php: [7.1, 7.2, 7.3]
|
||||||
service: ['mysql:5.7', mariadb]
|
service: ['mysql:5.7', mariadb]
|
||||||
prefix: ['', flarum_]
|
prefix: ['', flarum_]
|
||||||
|
|
||||||
@@ -21,16 +21,16 @@ jobs:
|
|||||||
prefixStr: (prefix)
|
prefixStr: (prefix)
|
||||||
|
|
||||||
exclude:
|
exclude:
|
||||||
- php: 7.2
|
- php: 7.1
|
||||||
service: 'mysql:5.7'
|
service: 'mysql:5.7'
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
- php: 7.2
|
- php: 7.1
|
||||||
service: mariadb
|
service: mariadb
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
- php: 7.3
|
- php: 7.2
|
||||||
service: 'mysql:5.7'
|
service: 'mysql:5.7'
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
- php: 7.3
|
- php: 7.2
|
||||||
service: mariadb
|
service: mariadb
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
|
|
||||||
@@ -49,9 +49,7 @@ jobs:
|
|||||||
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
|
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
|
||||||
|
|
||||||
- name: Create MySQL Database
|
- name: Create MySQL Database
|
||||||
run: |
|
run: mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||||
sudo systemctl start mysql
|
|
||||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
|
||||||
|
|
||||||
- name: Install Composer dependencies
|
- name: Install Composer dependencies
|
||||||
run: composer install
|
run: composer install
|
||||||
|
79
CHANGELOG.md
79
CHANGELOG.md
@@ -1,86 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Console extender (#2057)
|
|
||||||
- CSRF extender (#2095)
|
|
||||||
- Event extender (#2097)
|
|
||||||
- Mail extender (#2012)
|
|
||||||
- Model extender (#2100)
|
|
||||||
- Posts by users that started a discussion now have the CSS class `.Post--by-start-user`
|
|
||||||
- PHPUnit 8 compatibility
|
|
||||||
- Composer 2 compatibility
|
|
||||||
- Permission groups can now be hidden (#2129)
|
|
||||||
- Confirmation popup when hiding or deleting posts (#2135)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Updated less.php dependency version to 3.0
|
|
||||||
- Updated JS dependencies
|
|
||||||
- All notifications and other emails now processed through the queue, if enabled (#978, #1928, #1931, #2096)
|
|
||||||
- Simplified uploads, removing need to store intermediate files (#2117)
|
|
||||||
- Improved date handling for dates older than 1 year (#2034)
|
|
||||||
- Linting and automatic formatting for JS (#2099)
|
|
||||||
- Translation files from Language Packs are only loaded for extensions that are enabled (#2020)
|
|
||||||
- PHP extenders' properties are now `private` instead of `protected`, intentionally making it harder to extend these classes (#1958)
|
|
||||||
- Preparation for upgrading Laravel components to 5.8 and then 6.0 (#2055, #2117)
|
|
||||||
- Allowed permission checks based on model classes in addition to instances (#1977)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Users can no longer restore discussions hidden by admins (#2037)
|
|
||||||
- Issues of the Modal not showing or auto hiding (#1504, #1813, #2080)
|
|
||||||
- Columnar layout on admin extensions page was broken in Firefox (#2029, #2111)
|
|
||||||
- Non-dismissible modals could still be dismissed using the ESC key (#1917)
|
|
||||||
- New discussions were added to the discussion list above unread sticky posts (#1751, #1868)
|
|
||||||
- New discussions not visible to users when using Pusher (#2076, #2077)
|
|
||||||
- Permission icons were aligned unevenly in admin permissions list (#2016, #2018)
|
|
||||||
- Notification bubble not inversed on mobile with colored header (#1983, #2109)
|
|
||||||
- Post stream scrubber clicks jumped back to first post (#1945)
|
|
||||||
- Loading state of Switch toggle component was hard to see (#2039, #1491)
|
|
||||||
- `Flarum\Extend\Middleware`: The methods `insertBefore()` and `insertAfter()` did not work as described (#2063, #2084)
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Support for PHP 7.1 (#2014)
|
|
||||||
- Zend compatibility bridge (#2010)
|
|
||||||
- SES mail support (#2011)
|
|
||||||
- Backward compatibility layer for `Flarum\Mail\DriverInterface`, new methods from beta.12 are now required
|
|
||||||
- `Flarum\Util\Str` helper class
|
|
||||||
- `Flarum\Event\ConfigureMiddleware` event
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
- `Flarum\Event\AbstractConfigureRoutes` event class
|
|
||||||
- `Flarum\Event\ConfigureApiRoutes` event class
|
|
||||||
- `Flarum\Event\ConfigureForumRoutes` event class
|
|
||||||
- `Flarum\Event\ConfigureLocales` event class
|
|
||||||
|
|
||||||
## [0.1.0-beta.12](https://github.com/flarum/core/compare/v0.1.0-beta.11.1...v0.1.0-beta.12)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Full support for PHP 7.4 (#1980)
|
|
||||||
- Mail settings: Configure region for the Mailgun driver (#1834, #1850)
|
|
||||||
- Mail settings: Alert admins about incomplete settings (#1763, #1921)
|
|
||||||
- New permission that allows users to post without throttling (#1255, #1938)
|
|
||||||
- Basic transliteration of discussion "slugs" / pretty URLs (#194, #1975)
|
|
||||||
- User profiles: Render basic content on server side (#1901)
|
|
||||||
- New extender for configuring middleware (#1919, #1952, #1957, #1971)
|
|
||||||
- New extender for configuring error handling (#1781, #1970)
|
|
||||||
- Automated tests for PHP extenders to guarantee their backwards compatibility
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Profile URLs for non-existing users properly return HTTP 404 (#1846, #1901)
|
|
||||||
- Confirmation email subject no longer contains the forum title (#1613)
|
|
||||||
- Improved error handling during Flarum's early boot phase (#1607)
|
|
||||||
- Updated deprecated "Zend" libraries to their new "Laminas" equivalents (#1963)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Update page did not work when installed in subdirectories (#1947)
|
|
||||||
- Avatar upload did not work in IE11 / Edge (#1125, #1570)
|
|
||||||
- Translation fallback was ignored for client-rendered pages (#1774, #1961)
|
|
||||||
- The success alert when posting replies was invisible (#1976)
|
|
||||||
|
|
||||||
## [0.1.0-beta.11.1](https://github.com/flarum/core/compare/v0.1.0-beta.11...v0.1.0-beta.11.1)
|
## [0.1.0-beta.11.1](https://github.com/flarum/core/compare/v0.1.0-beta.11...v0.1.0-beta.11.1)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Saving custom css in admin failed (#1946)
|
- Saving custom css in admin failed (#1946)
|
||||||
|
|
||||||
## [0.1.0-beta.11](https://github.com/flarum/core/compare/v0.1.0-beta.10...v0.1.0-beta.11)
|
## [0.1.0-beta.11](https://github.com/flarum/core/compare/v0.1.0-beta.10...v0.1.0-beta.11)
|
||||||
|
@@ -5,28 +5,17 @@
|
|||||||
"homepage": "https://flarum.org/",
|
"homepage": "https://flarum.org/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Toby Zerner",
|
||||||
|
"email": "toby.zerner@gmail.com"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Franz Liedke",
|
"name": "Franz Liedke",
|
||||||
"email": "franz@develophp.org"
|
"email": "franz@develophp.org"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Daniel Klabbers",
|
"name": "Daniel Klabbers",
|
||||||
"email": "daniel@klabbers.email",
|
"email": "daniel@klabbers.email"
|
||||||
"homepage": "https://luceos.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "David Sevilla Martin",
|
|
||||||
"email": "me+flarum@datitisev.me",
|
|
||||||
"homepage": "https://datitisev.me"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Clark Winkelmann",
|
|
||||||
"email": "clark.winkelmann@gmail.com",
|
|
||||||
"homepage": "https://clarkwinkelmann.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Matthew Kilgore",
|
|
||||||
"email": "matthew@kilgore.dev"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
@@ -35,50 +24,50 @@
|
|||||||
"docs": "https://flarum.org/docs/"
|
"docs": "https://flarum.org/docs/"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.2",
|
"php": ">=7.1",
|
||||||
"axy/sourcemap": "^0.1.4",
|
"axy/sourcemap": "^0.1.4",
|
||||||
"components/font-awesome": "5.9.*",
|
"components/font-awesome": "5.9.*",
|
||||||
"dflydev/fig-cookies": "^2.0.1",
|
"dflydev/fig-cookies": "^1.0.2",
|
||||||
"doctrine/dbal": "^2.7",
|
"doctrine/dbal": "^2.7",
|
||||||
"franzl/whoops-middleware": "^0.4.0",
|
"franzl/whoops-middleware": "^0.4.0",
|
||||||
"illuminate/bus": "^6.0",
|
"illuminate/bus": "5.7.*",
|
||||||
"illuminate/cache": "^6.0",
|
"illuminate/cache": "5.7.*",
|
||||||
"illuminate/config": "^6.0",
|
"illuminate/config": "5.7.*",
|
||||||
"illuminate/container": "^6.0",
|
"illuminate/container": "5.7.*",
|
||||||
"illuminate/contracts": "^6.0",
|
"illuminate/contracts": "5.7.*",
|
||||||
"illuminate/database": "^6.0",
|
"illuminate/database": "5.7.*",
|
||||||
"illuminate/events": "^6.0",
|
"illuminate/events": "5.7.*",
|
||||||
"illuminate/filesystem": "^6.0",
|
"illuminate/filesystem": "5.7.*",
|
||||||
"illuminate/hashing": "^6.0",
|
"illuminate/hashing": "5.7.*",
|
||||||
"illuminate/mail": "^6.0",
|
"illuminate/mail": "5.7.*",
|
||||||
"illuminate/queue": "^6.0",
|
"illuminate/queue": "5.7.*",
|
||||||
"illuminate/session": "^6.0",
|
"illuminate/session": "5.7.*",
|
||||||
"illuminate/support": "^6.0",
|
"illuminate/support": "5.7.*",
|
||||||
"illuminate/validation": "^6.0",
|
"illuminate/validation": "5.7.*",
|
||||||
"illuminate/view": "^6.0",
|
"illuminate/view": "5.7.*",
|
||||||
"intervention/image": "^2.5.0",
|
"intervention/image": "^2.3.0",
|
||||||
"laminas/laminas-diactoros": "^1.8.4",
|
"laminas/laminas-diactoros": "^1.8.4",
|
||||||
"laminas/laminas-httphandlerrunner": "^1.0",
|
"laminas/laminas-httphandlerrunner": "^1.0",
|
||||||
"laminas/laminas-stratigility": "^3.0",
|
"laminas/laminas-stratigility": "^3.0",
|
||||||
|
"laminas/laminas-zendframework-bridge": "^1.0",
|
||||||
"league/flysystem": "^1.0.11",
|
"league/flysystem": "^1.0.11",
|
||||||
"matthiasmullie/minify": "^1.3",
|
"matthiasmullie/minify": "^1.3",
|
||||||
"middlewares/base-path": "^1.1",
|
"middlewares/base-path": "^1.1",
|
||||||
"middlewares/base-path-router": "^0.2.1",
|
"middlewares/base-path-router": "^0.2.1",
|
||||||
"middlewares/request-handler": "^1.2",
|
"middlewares/request-handler": "^1.2",
|
||||||
"monolog/monolog": "^1.16.0",
|
"monolog/monolog": "^1.16.0",
|
||||||
"nesbot/carbon": "^2.0",
|
|
||||||
"nikic/fast-route": "^0.6",
|
"nikic/fast-route": "^0.6",
|
||||||
|
"oyejorge/less.php": "^1.7",
|
||||||
"psr/http-message": "^1.0",
|
"psr/http-message": "^1.0",
|
||||||
"psr/http-server-handler": "^1.0",
|
"psr/http-server-handler": "^1.0",
|
||||||
"psr/http-server-middleware": "^1.0",
|
"psr/http-server-middleware": "^1.0",
|
||||||
"s9e/text-formatter": "^2.3.6",
|
"s9e/text-formatter": "^1.2.0",
|
||||||
"symfony/config": "^3.3",
|
"symfony/config": "^3.3",
|
||||||
"symfony/console": "^4.2",
|
"symfony/console": "^4.2",
|
||||||
"symfony/event-dispatcher": "^4.3.2",
|
"symfony/event-dispatcher": "^4.3.2",
|
||||||
"symfony/translation": "^3.3",
|
"symfony/translation": "^3.3",
|
||||||
"symfony/yaml": "^3.3",
|
"symfony/yaml": "^3.3",
|
||||||
"tobscure/json-api": "^0.3.0",
|
"tobscure/json-api": "^0.3.0"
|
||||||
"wikimedia/less.php": "^3.0"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mockery/mockery": "^1.0",
|
"mockery/mockery": "^1.0",
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 150,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
6
js/dist/admin.js
vendored
6
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
8
js/dist/forum.js
vendored
8
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
1798
js/package-lock.json
generated
1798
js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"name": "@flarum/core",
|
"name": "@flarum/core",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.10.1",
|
|
||||||
"bootstrap": "^3.4.1",
|
"bootstrap": "^3.4.1",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"color-thief-browser": "^2.0.2",
|
"color-thief-browser": "^2.0.2",
|
||||||
"dayjs": "^1.8.28",
|
|
||||||
"expose-loader": "^0.7.5",
|
"expose-loader": "^0.7.5",
|
||||||
"flarum-webpack-config": "0.1.0-beta.10",
|
"flarum-webpack-config": "0.1.0-beta.10",
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.4.1",
|
||||||
@@ -14,25 +12,15 @@
|
|||||||
"lodash-es": "^4.17.14",
|
"lodash-es": "^4.17.14",
|
||||||
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
|
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
|
||||||
"mithril": "^0.2.8",
|
"mithril": "^0.2.8",
|
||||||
|
"moment": "^2.22.2",
|
||||||
"punycode": "^2.1.1",
|
"punycode": "^2.1.1",
|
||||||
"spin.js": "^3.1.0",
|
"spin.js": "^3.1.0",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.41.2",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.1.2",
|
||||||
"webpack-merge": "^4.1.4"
|
"webpack-merge": "^4.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"husky": "^4.2.5",
|
|
||||||
"prettier": "2.0.2"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production"
|
||||||
"format": "prettier --write src",
|
|
||||||
"format-check": "prettier --check src"
|
|
||||||
},
|
|
||||||
"husky": {
|
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "npm run format"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,9 +12,9 @@ export default class AdminApplication extends Application {
|
|||||||
canGoBack: () => true,
|
canGoBack: () => true,
|
||||||
getPrevious: () => {},
|
getPrevious: () => {},
|
||||||
backUrl: () => this.forum.attribute('baseUrl'),
|
backUrl: () => this.forum.attribute('baseUrl'),
|
||||||
back: function () {
|
back: function() {
|
||||||
window.location = this.backUrl();
|
window.location = this.backUrl();
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -27,7 +27,7 @@ export default class AdminApplication extends Application {
|
|||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
mount() {
|
mount() {
|
||||||
m.mount(document.getElementById('app-navigation'), Navigation.component({ className: 'App-backControl', drawer: true }));
|
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-navigation'), Navigation.component());
|
||||||
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
|
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
|
||||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
|
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
|
||||||
@@ -59,5 +59,5 @@ export default class AdminApplication extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return required;
|
return required;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import EditCustomFooterModal from './components/EditCustomFooterModal';
|
|||||||
import SessionDropdown from './components/SessionDropdown';
|
import SessionDropdown from './components/SessionDropdown';
|
||||||
import HeaderPrimary from './components/HeaderPrimary';
|
import HeaderPrimary from './components/HeaderPrimary';
|
||||||
import AppearancePage from './components/AppearancePage';
|
import AppearancePage from './components/AppearancePage';
|
||||||
|
import Page from './components/Page';
|
||||||
import StatusWidget from './components/StatusWidget';
|
import StatusWidget from './components/StatusWidget';
|
||||||
import HeaderSecondary from './components/HeaderSecondary';
|
import HeaderSecondary from './components/HeaderSecondary';
|
||||||
import SettingsModal from './components/SettingsModal';
|
import SettingsModal from './components/SettingsModal';
|
||||||
@@ -14,6 +15,7 @@ import AddExtensionModal from './components/AddExtensionModal';
|
|||||||
import ExtensionsPage from './components/ExtensionsPage';
|
import ExtensionsPage from './components/ExtensionsPage';
|
||||||
import AdminLinkButton from './components/AdminLinkButton';
|
import AdminLinkButton from './components/AdminLinkButton';
|
||||||
import PermissionGrid from './components/PermissionGrid';
|
import PermissionGrid from './components/PermissionGrid';
|
||||||
|
import Widget from './components/Widget';
|
||||||
import MailPage from './components/MailPage';
|
import MailPage from './components/MailPage';
|
||||||
import UploadImageButton from './components/UploadImageButton';
|
import UploadImageButton from './components/UploadImageButton';
|
||||||
import LoadingModal from './components/LoadingModal';
|
import LoadingModal from './components/LoadingModal';
|
||||||
@@ -35,6 +37,7 @@ export default Object.assign(compat, {
|
|||||||
'components/SessionDropdown': SessionDropdown,
|
'components/SessionDropdown': SessionDropdown,
|
||||||
'components/HeaderPrimary': HeaderPrimary,
|
'components/HeaderPrimary': HeaderPrimary,
|
||||||
'components/AppearancePage': AppearancePage,
|
'components/AppearancePage': AppearancePage,
|
||||||
|
'components/Page': Page,
|
||||||
'components/StatusWidget': StatusWidget,
|
'components/StatusWidget': StatusWidget,
|
||||||
'components/HeaderSecondary': HeaderSecondary,
|
'components/HeaderSecondary': HeaderSecondary,
|
||||||
'components/SettingsModal': SettingsModal,
|
'components/SettingsModal': SettingsModal,
|
||||||
@@ -43,6 +46,7 @@ export default Object.assign(compat, {
|
|||||||
'components/ExtensionsPage': ExtensionsPage,
|
'components/ExtensionsPage': ExtensionsPage,
|
||||||
'components/AdminLinkButton': AdminLinkButton,
|
'components/AdminLinkButton': AdminLinkButton,
|
||||||
'components/PermissionGrid': PermissionGrid,
|
'components/PermissionGrid': PermissionGrid,
|
||||||
|
'components/Widget': Widget,
|
||||||
'components/MailPage': MailPage,
|
'components/MailPage': MailPage,
|
||||||
'components/UploadImageButton': UploadImageButton,
|
'components/UploadImageButton': UploadImageButton,
|
||||||
'components/LoadingModal': LoadingModal,
|
'components/LoadingModal': LoadingModal,
|
||||||
@@ -54,6 +58,6 @@ export default Object.assign(compat, {
|
|||||||
'components/AdminNav': AdminNav,
|
'components/AdminNav': AdminNav,
|
||||||
'components/EditCustomCssModal': EditCustomCssModal,
|
'components/EditCustomCssModal': EditCustomCssModal,
|
||||||
'components/EditGroupModal': EditGroupModal,
|
'components/EditGroupModal': EditGroupModal,
|
||||||
routes: routes,
|
'routes': routes,
|
||||||
AdminApplication: AdminApplication,
|
'AdminApplication': AdminApplication
|
||||||
});
|
});
|
||||||
|
@@ -22,10 +22,8 @@ export default class AddExtensionModal extends Modal {
|
|||||||
return (
|
return (
|
||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
|
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
|
||||||
<p>
|
<p>{app.translator.trans('core.admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p>
|
||||||
{app.translator.trans('core.admin.add_extension.install_text', { a: <a href="https://discuss.flarum.org/t/extensions" target="_blank" /> })}
|
<p>{app.translator.trans('core.admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p>
|
||||||
</p>
|
|
||||||
<p>{app.translator.trans('core.admin.add_extension.developer_text', { a: <a href="http://flarum.org/docs/extend" target="_blank" /> })}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,11 @@ export default class AdminLinkButton extends LinkButton {
|
|||||||
getButtonContent() {
|
getButtonContent() {
|
||||||
const content = super.getButtonContent();
|
const content = super.getButtonContent();
|
||||||
|
|
||||||
content.push(<div className="AdminLinkButton-description">{this.props.description}</div>);
|
content.push(
|
||||||
|
<div className="AdminLinkButton-description">
|
||||||
|
{this.props.description}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,9 @@ import ItemList from '../../common/utils/ItemList';
|
|||||||
export default class AdminNav extends Component {
|
export default class AdminNav extends Component {
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<SelectDropdown className="AdminNav App-titleControl" buttonClassName="Button">
|
<SelectDropdown
|
||||||
|
className="AdminNav App-titleControl"
|
||||||
|
buttonClassName="Button">
|
||||||
{this.items().toArray()}
|
{this.items().toArray()}
|
||||||
</SelectDropdown>
|
</SelectDropdown>
|
||||||
);
|
);
|
||||||
@@ -29,65 +31,47 @@ export default class AdminNav extends Component {
|
|||||||
items() {
|
items() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('dashboard', AdminLinkButton.component({
|
||||||
'dashboard',
|
|
||||||
AdminLinkButton.component({
|
|
||||||
href: app.route('dashboard'),
|
href: app.route('dashboard'),
|
||||||
icon: 'far fa-chart-bar',
|
icon: 'far fa-chart-bar',
|
||||||
children: app.translator.trans('core.admin.nav.dashboard_button'),
|
children: app.translator.trans('core.admin.nav.dashboard_button'),
|
||||||
description: app.translator.trans('core.admin.nav.dashboard_text'),
|
description: app.translator.trans('core.admin.nav.dashboard_text')
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('basics', AdminLinkButton.component({
|
||||||
'basics',
|
|
||||||
AdminLinkButton.component({
|
|
||||||
href: app.route('basics'),
|
href: app.route('basics'),
|
||||||
icon: 'fas fa-pencil-alt',
|
icon: 'fas fa-pencil-alt',
|
||||||
children: app.translator.trans('core.admin.nav.basics_button'),
|
children: app.translator.trans('core.admin.nav.basics_button'),
|
||||||
description: app.translator.trans('core.admin.nav.basics_text'),
|
description: app.translator.trans('core.admin.nav.basics_text')
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('mail', AdminLinkButton.component({
|
||||||
'mail',
|
|
||||||
AdminLinkButton.component({
|
|
||||||
href: app.route('mail'),
|
href: app.route('mail'),
|
||||||
icon: 'fas fa-envelope',
|
icon: 'fas fa-envelope',
|
||||||
children: app.translator.trans('core.admin.nav.email_button'),
|
children: app.translator.trans('core.admin.nav.email_button'),
|
||||||
description: app.translator.trans('core.admin.nav.email_text'),
|
description: app.translator.trans('core.admin.nav.email_text')
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('permissions', AdminLinkButton.component({
|
||||||
'permissions',
|
|
||||||
AdminLinkButton.component({
|
|
||||||
href: app.route('permissions'),
|
href: app.route('permissions'),
|
||||||
icon: 'fas fa-key',
|
icon: 'fas fa-key',
|
||||||
children: app.translator.trans('core.admin.nav.permissions_button'),
|
children: app.translator.trans('core.admin.nav.permissions_button'),
|
||||||
description: app.translator.trans('core.admin.nav.permissions_text'),
|
description: app.translator.trans('core.admin.nav.permissions_text')
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('appearance', AdminLinkButton.component({
|
||||||
'appearance',
|
|
||||||
AdminLinkButton.component({
|
|
||||||
href: app.route('appearance'),
|
href: app.route('appearance'),
|
||||||
icon: 'fas fa-paint-brush',
|
icon: 'fas fa-paint-brush',
|
||||||
children: app.translator.trans('core.admin.nav.appearance_button'),
|
children: app.translator.trans('core.admin.nav.appearance_button'),
|
||||||
description: app.translator.trans('core.admin.nav.appearance_text'),
|
description: app.translator.trans('core.admin.nav.appearance_text')
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('extensions', AdminLinkButton.component({
|
||||||
'extensions',
|
|
||||||
AdminLinkButton.component({
|
|
||||||
href: app.route('extensions'),
|
href: app.route('extensions'),
|
||||||
icon: 'fas fa-puzzle-piece',
|
icon: 'fas fa-puzzle-piece',
|
||||||
children: app.translator.trans('core.admin.nav.extensions_button'),
|
children: app.translator.trans('core.admin.nav.extensions_button'),
|
||||||
description: app.translator.trans('core.admin.nav.extensions_text'),
|
description: app.translator.trans('core.admin.nav.extensions_text')
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import Page from '../../common/components/Page';
|
import Page from './Page';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Switch from '../../common/components/Switch';
|
import Switch from '../../common/components/Switch';
|
||||||
import EditCustomCssModal from './EditCustomCssModal';
|
import EditCustomCssModal from './EditCustomCssModal';
|
||||||
@@ -13,8 +13,8 @@ export default class AppearancePage extends Page {
|
|||||||
|
|
||||||
this.primaryColor = m.prop(app.data.settings.theme_primary_color);
|
this.primaryColor = m.prop(app.data.settings.theme_primary_color);
|
||||||
this.secondaryColor = m.prop(app.data.settings.theme_secondary_color);
|
this.secondaryColor = m.prop(app.data.settings.theme_secondary_color);
|
||||||
this.darkMode = m.prop(app.data.settings.theme_dark_mode);
|
this.darkMode = m.prop(app.data.settings.theme_dark_mode === '1');
|
||||||
this.coloredHeader = m.prop(app.data.settings.theme_colored_header);
|
this.coloredHeader = m.prop(app.data.settings.theme_colored_header === '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
@@ -24,85 +24,85 @@ export default class AppearancePage extends Page {
|
|||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
<fieldset className="AppearancePage-colors">
|
<fieldset className="AppearancePage-colors">
|
||||||
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.colors_text')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="AppearancePage-colors-input">
|
<div className="AppearancePage-colors-input">
|
||||||
<input
|
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
||||||
className="FormControl"
|
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
|
||||||
type="text"
|
|
||||||
placeholder="#aaaaaa"
|
|
||||||
value={this.primaryColor()}
|
|
||||||
onchange={m.withAttr('value', this.primaryColor)}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="FormControl"
|
|
||||||
type="text"
|
|
||||||
placeholder="#aaaaaa"
|
|
||||||
value={this.secondaryColor()}
|
|
||||||
onchange={m.withAttr('value', this.secondaryColor)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Switch.component({
|
{Switch.component({
|
||||||
state: this.darkMode(),
|
state: this.darkMode(),
|
||||||
children: app.translator.trans('core.admin.appearance.dark_mode_label'),
|
children: app.translator.trans('core.admin.appearance.dark_mode_label'),
|
||||||
onchange: this.darkMode,
|
onchange: this.darkMode
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{Switch.component({
|
{Switch.component({
|
||||||
state: this.coloredHeader(),
|
state: this.coloredHeader(),
|
||||||
children: app.translator.trans('core.admin.appearance.colored_header_label'),
|
children: app.translator.trans('core.admin.appearance.colored_header_label'),
|
||||||
onchange: this.coloredHeader,
|
onchange: this.coloredHeader
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{Button.component({
|
{Button.component({
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
children: app.translator.trans('core.admin.appearance.submit_button'),
|
children: app.translator.trans('core.admin.appearance.submit_button'),
|
||||||
loading: this.loading,
|
loading: this.loading
|
||||||
})}
|
})}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
|
<div className="helpText">
|
||||||
<UploadImageButton name="logo" />
|
{app.translator.trans('core.admin.appearance.logo_text')}
|
||||||
|
</div>
|
||||||
|
<UploadImageButton name="logo"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
|
<div className="helpText">
|
||||||
<UploadImageButton name="favicon" />
|
{app.translator.trans('core.admin.appearance.favicon_text')}
|
||||||
|
</div>
|
||||||
|
<UploadImageButton name="favicon"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.custom_header_text')}
|
||||||
|
</div>
|
||||||
{Button.component({
|
{Button.component({
|
||||||
className: 'Button',
|
className: 'Button',
|
||||||
children: app.translator.trans('core.admin.appearance.edit_header_button'),
|
children: app.translator.trans('core.admin.appearance.edit_header_button'),
|
||||||
onclick: () => app.modal.show(EditCustomHeaderModal),
|
onclick: () => app.modal.show(new EditCustomHeaderModal())
|
||||||
})}
|
})}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.custom_footer_text')}
|
||||||
|
</div>
|
||||||
{Button.component({
|
{Button.component({
|
||||||
className: 'Button',
|
className: 'Button',
|
||||||
children: app.translator.trans('core.admin.appearance.edit_footer_button'),
|
children: app.translator.trans('core.admin.appearance.edit_footer_button'),
|
||||||
onclick: () => app.modal.show(EditCustomFooterModal),
|
onclick: () => app.modal.show(new EditCustomFooterModal())
|
||||||
})}
|
})}
|
||||||
</fieldset>
|
</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">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.appearance.custom_styles_text')}
|
||||||
|
</div>
|
||||||
{Button.component({
|
{Button.component({
|
||||||
className: 'Button',
|
className: 'Button',
|
||||||
children: app.translator.trans('core.admin.appearance.edit_css_button'),
|
children: app.translator.trans('core.admin.appearance.edit_css_button'),
|
||||||
onclick: () => app.modal.show(EditCustomCssModal),
|
onclick: () => app.modal.show(new EditCustomCssModal())
|
||||||
})}
|
})}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +126,7 @@ export default class AppearancePage extends Page {
|
|||||||
theme_primary_color: this.primaryColor(),
|
theme_primary_color: this.primaryColor(),
|
||||||
theme_secondary_color: this.secondaryColor(),
|
theme_secondary_color: this.secondaryColor(),
|
||||||
theme_dark_mode: this.darkMode(),
|
theme_dark_mode: this.darkMode(),
|
||||||
theme_colored_header: this.coloredHeader(),
|
theme_colored_header: this.coloredHeader()
|
||||||
}).then(() => window.location.reload());
|
}).then(() => window.location.reload());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import Page from '../../common/components/Page';
|
import Page from './Page';
|
||||||
import FieldSet from '../../common/components/FieldSet';
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
import Select from '../../common/components/Select';
|
import Select from '../../common/components/Select';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
|
import Alert from '../../common/components/Alert';
|
||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import Switch from '../../common/components/Switch';
|
import Switch from '../../common/components/Switch';
|
||||||
@@ -19,13 +20,12 @@ export default class BasicsPage extends Page {
|
|||||||
'show_language_selector',
|
'show_language_selector',
|
||||||
'default_route',
|
'default_route',
|
||||||
'welcome_title',
|
'welcome_title',
|
||||||
'welcome_message',
|
'welcome_message'
|
||||||
'display_name_driver',
|
|
||||||
];
|
];
|
||||||
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 = {};
|
this.localeOptions = {};
|
||||||
const locales = app.data.locales;
|
const locales = app.data.locales;
|
||||||
@@ -33,15 +33,7 @@ export default class BasicsPage extends Page {
|
|||||||
this.localeOptions[i] = `${locales[i]} (${i})`;
|
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.displayNameOptions = {};
|
if (typeof this.values.show_language_selector() !== "number") this.values.show_language_selector(1);
|
||||||
const displayNameDrivers = app.data.displayNameDrivers;
|
|
||||||
displayNameDrivers.forEach(function (identifier) {
|
|
||||||
this.displayNameOptions[identifier] = identifier;
|
|
||||||
}, this);
|
|
||||||
|
|
||||||
if (!this.values.display_name_driver() && displayNameDrivers.includes('username')) this.values.display_name_driver('username');
|
|
||||||
|
|
||||||
if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
@@ -51,19 +43,19 @@ export default class BasicsPage extends Page {
|
|||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
||||||
children: [<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)} />],
|
children: [
|
||||||
|
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
|
||||||
|
]
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
||||||
children: [
|
children: [
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.forum_description_text')}</div>,
|
<div className="helpText">
|
||||||
<textarea
|
{app.translator.trans('core.admin.basics.forum_description_text')}
|
||||||
className="FormControl"
|
</div>,
|
||||||
value={this.values.forum_description()}
|
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
|
||||||
oninput={m.withAttr('value', this.values.forum_description)}
|
]
|
||||||
/>,
|
|
||||||
],
|
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{Object.keys(this.localeOptions).length > 1
|
{Object.keys(this.localeOptions).length > 1
|
||||||
@@ -73,14 +65,14 @@ export default class BasicsPage extends Page {
|
|||||||
Select.component({
|
Select.component({
|
||||||
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({
|
Switch.component({
|
||||||
state: this.values.show_language_selector(),
|
state: this.values.show_language_selector(),
|
||||||
onchange: this.values.show_language_selector,
|
onchange: this.values.show_language_selector,
|
||||||
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
||||||
}),
|
})
|
||||||
],
|
]
|
||||||
})
|
})
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
@@ -88,60 +80,38 @@ export default class BasicsPage extends Page {
|
|||||||
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',
|
||||||
children: [
|
children: [
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>,
|
<div className="helpText">
|
||||||
this.homePageItems()
|
{app.translator.trans('core.admin.basics.home_page_text')}
|
||||||
.toArray()
|
</div>,
|
||||||
.map(({ path, label }) => (
|
this.homePageItems().toArray().map(({path, label}) =>
|
||||||
<label className="checkbox">
|
<label className="checkbox">
|
||||||
<input
|
<input type="radio" name="homePage" value={path} checked={this.values.default_route() === path} onclick={m.withAttr('value', this.values.default_route)}/>
|
||||||
type="radio"
|
|
||||||
name="homePage"
|
|
||||||
value={path}
|
|
||||||
checked={this.values.default_route() === path}
|
|
||||||
onclick={m.withAttr('value', this.values.default_route)}
|
|
||||||
/>
|
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)),
|
)
|
||||||
],
|
]
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
||||||
className: 'BasicsPage-welcomeBanner',
|
className: 'BasicsPage-welcomeBanner',
|
||||||
children: [
|
children: [
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>,
|
<div className="helpText">
|
||||||
<div className="BasicsPage-welcomeBanner-input">
|
{app.translator.trans('core.admin.basics.welcome_banner_text')}
|
||||||
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)} />
|
|
||||||
<textarea
|
|
||||||
className="FormControl"
|
|
||||||
value={this.values.welcome_message()}
|
|
||||||
oninput={m.withAttr('value', this.values.welcome_message)}
|
|
||||||
/>
|
|
||||||
</div>,
|
</div>,
|
||||||
],
|
<div className="BasicsPage-welcomeBanner-input">
|
||||||
|
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
|
||||||
|
<textarea className="FormControl" value={this.values.welcome_message()} oninput={m.withAttr('value', this.values.welcome_message)}/>
|
||||||
|
</div>
|
||||||
|
]
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{Object.keys(this.displayNameOptions).length > 1
|
|
||||||
? FieldSet.component({
|
|
||||||
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
|
||||||
children: [
|
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>,
|
|
||||||
Select.component({
|
|
||||||
options: this.displayNameOptions,
|
|
||||||
value: this.values.display_name_driver(),
|
|
||||||
onchange: this.values.display_name_driver,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
: ''}
|
|
||||||
|
|
||||||
{Button.component({
|
{Button.component({
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
children: app.translator.trans('core.admin.basics.submit_button'),
|
children: app.translator.trans('core.admin.basics.submit_button'),
|
||||||
loading: this.loading,
|
loading: this.loading,
|
||||||
disabled: !this.changed(),
|
disabled: !this.changed()
|
||||||
})}
|
})}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,7 +120,7 @@ export default class BasicsPage extends Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changed() {
|
changed() {
|
||||||
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
|
return this.fields.some(key => this.values[key]() !== app.data.settings[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,7 +135,7 @@ export default class BasicsPage extends Page {
|
|||||||
|
|
||||||
items.add('allDiscussions', {
|
items.add('allDiscussions', {
|
||||||
path: '/all',
|
path: '/all',
|
||||||
label: app.translator.trans('core.admin.basics.all_discussions_label'),
|
label: app.translator.trans('core.admin.basics.all_discussions_label')
|
||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -181,14 +151,11 @@ export default class BasicsPage extends Page {
|
|||||||
|
|
||||||
const settings = {};
|
const settings = {};
|
||||||
|
|
||||||
this.fields.forEach((key) => (settings[key] = this.values[key]()));
|
this.fields.forEach(key => settings[key] = this.values[key]());
|
||||||
|
|
||||||
saveSettings(settings)
|
saveSettings(settings)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.successAlert = app.alerts.show({
|
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
|
||||||
type: 'success',
|
|
||||||
children: app.translator.trans('core.admin.basics.saved_message'),
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
import Page from '../../common/components/Page';
|
import Page from './Page';
|
||||||
import StatusWidget from './StatusWidget';
|
import StatusWidget from './StatusWidget';
|
||||||
|
|
||||||
export default class DashboardPage extends Page {
|
export default class DashboardPage extends Page {
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="DashboardPage">
|
<div className="DashboardPage">
|
||||||
<div className="container">{this.availableWidgets()}</div>
|
<div className="container">
|
||||||
|
{this.availableWidgets()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
availableWidgets() {
|
availableWidgets() {
|
||||||
return [<StatusWidget />];
|
return [<StatusWidget/>];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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 Component from '../../common/Component';
|
||||||
|
|
||||||
export default class DashboardWidget extends Component {
|
export default class Widget extends Component {
|
||||||
view() {
|
view() {
|
||||||
return <div className={'DashboardWidget Widget ' + this.className()}>{this.content()}</div>;
|
return (
|
||||||
|
<div className={"Widget "+this.className()}>
|
||||||
|
{this.content()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -11,14 +11,10 @@ export default class EditCustomCssModal extends SettingsModal {
|
|||||||
|
|
||||||
form() {
|
form() {
|
||||||
return [
|
return [
|
||||||
<p>
|
<p>{app.translator.trans('core.admin.edit_css.customize_text', {a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank"/>})}</p>,
|
||||||
{app.translator.trans('core.admin.edit_css.customize_text', {
|
|
||||||
a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank" />,
|
|
||||||
})}
|
|
||||||
</p>,
|
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')} />
|
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')}/>
|
||||||
</div>,
|
</div>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,8 +13,8 @@ export default class EditCustomFooterModal extends SettingsModal {
|
|||||||
return [
|
return [
|
||||||
<p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>,
|
<p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>,
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')} />
|
<textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')}/>
|
||||||
</div>,
|
</div>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,8 +13,8 @@ export default class EditCustomHeaderModal extends SettingsModal {
|
|||||||
return [
|
return [
|
||||||
<p>{app.translator.trans('core.admin.edit_header.customize_text')}</p>,
|
<p>{app.translator.trans('core.admin.edit_header.customize_text')}</p>,
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<textarea className="FormControl" rows="30" bidi={this.setting('custom_header')} />
|
<textarea className="FormControl" rows="30" bidi={this.setting('custom_header')}/>
|
||||||
</div>,
|
</div>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,6 @@ import Button from '../../common/components/Button';
|
|||||||
import Badge from '../../common/components/Badge';
|
import Badge from '../../common/components/Badge';
|
||||||
import Group from '../../common/models/Group';
|
import Group from '../../common/models/Group';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import Switch from '../../common/components/Switch';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `EditGroupModal` component shows a modal dialog which allows the user
|
* The `EditGroupModal` component shows a modal dialog which allows the user
|
||||||
@@ -17,7 +16,6 @@ export default class EditGroupModal extends Modal {
|
|||||||
this.namePlural = m.prop(this.group.namePlural() || '');
|
this.namePlural = m.prop(this.group.namePlural() || '');
|
||||||
this.icon = m.prop(this.group.icon() || '');
|
this.icon = m.prop(this.group.icon() || '');
|
||||||
this.color = m.prop(this.group.color() || '');
|
this.color = m.prop(this.group.color() || '');
|
||||||
this.isHidden = m.prop(this.group.isHidden() || false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
@@ -26,21 +24,21 @@ export default class EditGroupModal extends Modal {
|
|||||||
|
|
||||||
title() {
|
title() {
|
||||||
return [
|
return [
|
||||||
this.color() || this.icon()
|
this.color() || this.icon() ? Badge.component({
|
||||||
? Badge.component({
|
|
||||||
icon: this.icon(),
|
icon: this.icon(),
|
||||||
style: { backgroundColor: this.color() },
|
style: {backgroundColor: this.color()}
|
||||||
})
|
}) : '',
|
||||||
: '',
|
|
||||||
' ',
|
' ',
|
||||||
this.namePlural() || app.translator.trans('core.admin.edit_group.title'),
|
this.namePlural() || app.translator.trans('core.admin.edit_group.title')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
return (
|
return (
|
||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
<div className="Form">{this.fields().toArray()}</div>
|
<div className="Form">
|
||||||
|
{this.fields().toArray()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -48,80 +46,40 @@ export default class EditGroupModal extends Modal {
|
|||||||
fields() {
|
fields() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('name', <div className="Form-group">
|
||||||
'name',
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
|
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
|
||||||
<div className="EditGroupModal-name-input">
|
<div className="EditGroupModal-name-input">
|
||||||
<input
|
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
|
||||||
className="FormControl"
|
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
|
||||||
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>,
|
</div>, 30);
|
||||||
30
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('color', <div className="Form-group">
|
||||||
'color',
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
|
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
|
||||||
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)} />
|
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
|
||||||
</div>,
|
</div>, 20);
|
||||||
20
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('icon', <div className="Form-group">
|
||||||
'icon',
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
|
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
|
||||||
<div className="helpText">
|
<div className="helpText">
|
||||||
{app.translator.trans('core.admin.edit_group.icon_text', { a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1" /> })}
|
{app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1"/>})}
|
||||||
</div>
|
</div>
|
||||||
<input className="FormControl" placeholder="fas fa-bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)} />
|
<input className="FormControl" placeholder="fas fa-bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
|
||||||
</div>,
|
</div>, 10);
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('submit', <div className="Form-group">
|
||||||
'hidden',
|
|
||||||
<div className="Form-group">
|
|
||||||
{Switch.component({
|
|
||||||
state: !!Number(this.isHidden()),
|
|
||||||
children: app.translator.trans('core.admin.edit_group.hide_label'),
|
|
||||||
onchange: this.isHidden,
|
|
||||||
})}
|
|
||||||
</div>,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
'submit',
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component({
|
{Button.component({
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
className: 'Button Button--primary EditGroupModal-save',
|
className: 'Button Button--primary EditGroupModal-save',
|
||||||
loading: this.loading,
|
loading: this.loading,
|
||||||
children: app.translator.trans('core.admin.edit_group.submit_button'),
|
children: app.translator.trans('core.admin.edit_group.submit_button')
|
||||||
})}
|
})}
|
||||||
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
||||||
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
|
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
|
||||||
{app.translator.trans('core.admin.edit_group.delete_button')}
|
{app.translator.trans('core.admin.edit_group.delete_button')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : ''}
|
||||||
''
|
</div>, -10);
|
||||||
)}
|
|
||||||
</div>,
|
|
||||||
-10
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -131,8 +89,7 @@ export default class EditGroupModal extends Modal {
|
|||||||
nameSingular: this.nameSingular(),
|
nameSingular: this.nameSingular(),
|
||||||
namePlural: this.namePlural(),
|
namePlural: this.namePlural(),
|
||||||
color: this.color(),
|
color: this.color(),
|
||||||
icon: this.icon(),
|
icon: this.icon()
|
||||||
isHidden: this.isHidden(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,8 +98,7 @@ export default class EditGroupModal extends Modal {
|
|||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
this.group
|
this.group.save(this.submitData(), {errorHandler: this.onerror.bind(this)})
|
||||||
.save(this.submitData(), { errorHandler: this.onerror.bind(this) })
|
|
||||||
.then(this.hide.bind(this))
|
.then(this.hide.bind(this))
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import Page from '../../common/components/Page';
|
import Page from './Page';
|
||||||
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Dropdown from '../../common/components/Dropdown';
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
|
import Separator from '../../common/components/Separator';
|
||||||
import AddExtensionModal from './AddExtensionModal';
|
import AddExtensionModal from './AddExtensionModal';
|
||||||
import LoadingModal from './LoadingModal';
|
import LoadingModal from './LoadingModal';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import icon from '../../common/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
|
||||||
export default class ExtensionsPage extends Page {
|
export default class ExtensionsPage extends Page {
|
||||||
view() {
|
view() {
|
||||||
@@ -16,7 +19,7 @@ export default class ExtensionsPage extends Page {
|
|||||||
children: app.translator.trans('core.admin.extensions.add_button'),
|
children: app.translator.trans('core.admin.extensions.add_button'),
|
||||||
icon: 'fas fa-plus',
|
icon: 'fas fa-plus',
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
onclick: () => app.modal.show(AddExtensionModal),
|
onclick: () => app.modal.show(new AddExtensionModal())
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,12 +27,12 @@ export default class ExtensionsPage extends Page {
|
|||||||
<div className="ExtensionsPage-list">
|
<div className="ExtensionsPage-list">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<ul className="ExtensionList">
|
<ul className="ExtensionList">
|
||||||
{Object.keys(app.data.extensions).map((id) => {
|
{Object.keys(app.data.extensions)
|
||||||
|
.map(id => {
|
||||||
const extension = app.data.extensions[id];
|
const extension = app.data.extensions[id];
|
||||||
const controls = this.controlItems(extension.id).toArray();
|
const controls = this.controlItems(extension.id).toArray();
|
||||||
|
|
||||||
return (
|
return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
|
||||||
<li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
|
|
||||||
<div className="ExtensionListItem-content">
|
<div className="ExtensionListItem-content">
|
||||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
||||||
{extension.icon ? icon(extension.icon.name) : ''}
|
{extension.icon ? icon(extension.icon.name) : ''}
|
||||||
@@ -39,24 +42,20 @@ 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="fas fa-ellipsis-h"
|
icon="fas fa-ellipsis-h">
|
||||||
>
|
|
||||||
{controls}
|
{controls}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
) : (
|
) : ''}
|
||||||
''
|
|
||||||
)}
|
|
||||||
<div className="ExtensionListItem-main">
|
<div className="ExtensionListItem-main">
|
||||||
<label className="ExtensionListItem-title">
|
<label className="ExtensionListItem-title">
|
||||||
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)} />{' '}
|
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '}
|
||||||
{extension.extra['flarum-extension'].title}
|
{extension.extra['flarum-extension'].title}
|
||||||
</label>
|
</label>
|
||||||
<div className="ExtensionListItem-version">{extension.version}</div>
|
<div className="ExtensionListItem-version">{extension.version}</div>
|
||||||
<div className="ExtensionListItem-description">{extension.description}</div>
|
<div className="ExtensionListItem-description">{extension.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>;
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,34 +69,26 @@ export default class ExtensionsPage extends Page {
|
|||||||
const enabled = this.isEnabled(name);
|
const enabled = this.isEnabled(name);
|
||||||
|
|
||||||
if (app.extensionSettings[name]) {
|
if (app.extensionSettings[name]) {
|
||||||
items.add(
|
items.add('settings', Button.component({
|
||||||
'settings',
|
|
||||||
Button.component({
|
|
||||||
icon: 'fas fa-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]
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
items.add(
|
items.add('uninstall', Button.component({
|
||||||
'uninstall',
|
|
||||||
Button.component({
|
|
||||||
icon: 'far fa-trash-alt',
|
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
|
app.request({
|
||||||
.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
|
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
|
||||||
method: 'DELETE',
|
method: 'DELETE'
|
||||||
})
|
}).then(() => window.location.reload());
|
||||||
.then(() => window.location.reload());
|
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
app.modal.show(new LoadingModal());
|
||||||
},
|
}
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -112,17 +103,15 @@ export default class ExtensionsPage extends Page {
|
|||||||
toggle(id) {
|
toggle(id) {
|
||||||
const enabled = this.isEnabled(id);
|
const enabled = this.isEnabled(id);
|
||||||
|
|
||||||
app
|
app.request({
|
||||||
.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
data: { enabled: !enabled },
|
data: {enabled: !enabled}
|
||||||
})
|
}).then(() => {
|
||||||
.then(() => {
|
|
||||||
if (!enabled) localStorage.setItem('enabledExtension', id);
|
if (!enabled) localStorage.setItem('enabledExtension', id);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
app.modal.show(new LoadingModal());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,11 @@ import listItems from '../../common/helpers/listItems';
|
|||||||
*/
|
*/
|
||||||
export default class HeaderPrimary extends Component {
|
export default class HeaderPrimary extends Component {
|
||||||
view() {
|
view() {
|
||||||
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
|
return (
|
||||||
|
<ul className="Header-controls">
|
||||||
|
{listItems(this.items().toArray())}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
config(isInitialized, context) {
|
config(isInitialized, context) {
|
||||||
|
@@ -8,7 +8,11 @@ import listItems from '../../common/helpers/listItems';
|
|||||||
*/
|
*/
|
||||||
export default class HeaderSecondary extends Component {
|
export default class HeaderSecondary extends Component {
|
||||||
view() {
|
view() {
|
||||||
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
|
return (
|
||||||
|
<ul className="Header-controls">
|
||||||
|
{listItems(this.items().toArray())}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
config(isInitialized, context) {
|
config(isInitialized, context) {
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
import Modal from '../../common/components/Modal';
|
import Modal from '../../common/components/Modal';
|
||||||
|
|
||||||
export default class LoadingModal extends Modal {
|
export default class LoadingModal extends Modal {
|
||||||
/**
|
isDismissible() {
|
||||||
* @inheritdoc
|
return false;
|
||||||
*/
|
}
|
||||||
static isDismissible = false;
|
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
return 'LoadingModal Modal--small';
|
return 'LoadingModal Modal--small';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import Page from '../../common/components/Page';
|
import Page from './Page';
|
||||||
import FieldSet from '../../common/components/FieldSet';
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Alert from '../../common/components/Alert';
|
import Alert from '../../common/components/Alert';
|
||||||
@@ -10,31 +10,24 @@ export default class MailPage extends Page {
|
|||||||
init() {
|
init() {
|
||||||
super.init();
|
super.init();
|
||||||
|
|
||||||
this.saving = false;
|
|
||||||
this.sendingTest = false;
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.saving = false;
|
||||||
|
|
||||||
this.driverFields = {};
|
this.driverFields = {};
|
||||||
this.fields = ['mail_driver', 'mail_from'];
|
this.fields = ['mail_driver', 'mail_from'];
|
||||||
this.values = {};
|
this.values = {};
|
||||||
this.status = { sending: false, errors: {} };
|
|
||||||
|
|
||||||
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]));
|
||||||
|
|
||||||
app
|
app.request({
|
||||||
.request({
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: app.forum.attribute('apiUrl') + '/mail/settings',
|
url: app.forum.attribute('apiUrl') + '/mail-drivers'
|
||||||
})
|
}).then(response => {
|
||||||
.then((response) => {
|
this.driverFields = response['data'].reduce(
|
||||||
this.driverFields = response['data']['attributes']['fields'];
|
(hash, driver) => ({...hash, [driver['id']]: driver['attributes']['fields']}),
|
||||||
this.status.sending = response['data']['attributes']['sending'];
|
{}
|
||||||
this.status.errors = response['data']['attributes']['errors'];
|
);
|
||||||
|
|
||||||
for (const driver in this.driverFields) {
|
for (const driver in this.driverFields) {
|
||||||
for (const field in this.driverFields[driver]) {
|
for (const field in this.driverFields[driver]) {
|
||||||
@@ -49,7 +42,7 @@ export default class MailPage extends Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
if (this.loading || this.saving) {
|
if (this.loading) {
|
||||||
return (
|
return (
|
||||||
<div className="MailPage">
|
<div className="MailPage">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@@ -59,27 +52,24 @@ export default class MailPage extends Page {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = this.driverFields[this.values.mail_driver()];
|
|
||||||
const fieldKeys = Object.keys(fields);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="MailPage">
|
<div className="MailPage">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
|
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.email.text')}</div>
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.email.text')}
|
||||||
|
</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',
|
||||||
children: [
|
children: [
|
||||||
<div className="MailPage-MailSettings-input">
|
<div className="MailPage-MailSettings-input">
|
||||||
<label>
|
<label>{app.translator.trans('core.admin.email.from_label')}</label>
|
||||||
{app.translator.trans('core.admin.email.from_label')}
|
|
||||||
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
|
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
|
||||||
</label>
|
</div>
|
||||||
</div>,
|
]
|
||||||
],
|
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{FieldSet.component({
|
{FieldSet.component({
|
||||||
@@ -87,62 +77,31 @@ export default class MailPage extends Page {
|
|||||||
className: 'MailPage-MailSettings',
|
className: 'MailPage-MailSettings',
|
||||||
children: [
|
children: [
|
||||||
<div className="MailPage-MailSettings-input">
|
<div className="MailPage-MailSettings-input">
|
||||||
<label>
|
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
|
||||||
{app.translator.trans('core.admin.email.driver_label')}
|
<Select value={this.values.mail_driver()} options={Object.keys(this.driverFields).reduce((memo, val) => ({...memo, [val]: val}), {})} onchange={this.values.mail_driver} />
|
||||||
<Select
|
</div>
|
||||||
value={this.values.mail_driver()}
|
]
|
||||||
options={Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {})}
|
|
||||||
onchange={this.values.mail_driver}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>,
|
|
||||||
],
|
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{this.status.sending ||
|
{Object.keys(this.driverFields[this.values.mail_driver()]).length > 0 && FieldSet.component({
|
||||||
Alert.component({
|
|
||||||
children: app.translator.trans('core.admin.email.not_sending_message'),
|
|
||||||
dismissible: false,
|
|
||||||
})}
|
|
||||||
|
|
||||||
{fieldKeys.length > 0 &&
|
|
||||||
FieldSet.component({
|
|
||||||
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
|
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
|
||||||
className: 'MailPage-MailSettings',
|
className: 'MailPage-MailSettings',
|
||||||
children: [
|
children: [
|
||||||
<div className="MailPage-MailSettings-input">
|
<div className="MailPage-MailSettings-input">
|
||||||
{fieldKeys.map((field) => [
|
{Object.keys(this.driverFields[this.values.mail_driver()]).map(field => [
|
||||||
<label>
|
<label>{app.translator.trans(`core.admin.email.${field}_label`)}</label>,
|
||||||
{app.translator.trans(`core.admin.email.${field}_label`)}
|
this.renderField(field),
|
||||||
{this.renderField(field)}
|
|
||||||
</label>,
|
|
||||||
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
|
|
||||||
])}
|
])}
|
||||||
</div>,
|
</div>
|
||||||
],
|
]
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<FieldSet>
|
|
||||||
{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'),
|
||||||
disabled: !this.changed(),
|
loading: this.saving,
|
||||||
})}
|
disabled: !this.changed()
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
{FieldSet.component({
|
|
||||||
label: app.translator.trans('core.admin.email.send_test_mail_heading'),
|
|
||||||
className: 'MailPage-MailSettings',
|
|
||||||
children: [
|
|
||||||
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user.email() })}</div>,
|
|
||||||
Button.component({
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
children: app.translator.trans('core.admin.email.send_test_mail_button'),
|
|
||||||
disabled: this.sendingTest || this.changed(),
|
|
||||||
onclick: () => this.sendTestEmail(),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})}
|
})}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,64 +115,36 @@ export default class MailPage extends Page {
|
|||||||
const prop = this.values[name];
|
const prop = this.values[name];
|
||||||
|
|
||||||
if (typeof field === 'string') {
|
if (typeof field === 'string') {
|
||||||
return <input className="FormControl" value={prop() || ''} oninput={m.withAttr('value', prop)} />;
|
return <input className="FormControl" value={prop() || ''} oninput={m.withAttr('value', prop)}/>;
|
||||||
} else {
|
} else {
|
||||||
return <Select value={prop()} options={field} onchange={prop} />;
|
return <Select value={prop()} options={field} onchange={prop} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changed() {
|
changed() {
|
||||||
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
|
return this.fields.some(key => this.values[key]() !== app.data.settings[key]);
|
||||||
}
|
|
||||||
|
|
||||||
sendTestEmail() {
|
|
||||||
if (this.saving || this.sendingTest) return;
|
|
||||||
|
|
||||||
this.sendingTest = true;
|
|
||||||
app.alerts.dismiss(this.testEmailSuccessAlert);
|
|
||||||
|
|
||||||
app
|
|
||||||
.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: app.forum.attribute('apiUrl') + '/mail/test',
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
this.sendingTest = false;
|
|
||||||
this.testEmailSuccessAlert = app.alerts.show({
|
|
||||||
type: 'success',
|
|
||||||
children: app.translator.trans('core.admin.email.send_test_mail_success'),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.sendingTest = false;
|
|
||||||
m.redraw();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onsubmit(e) {
|
onsubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (this.saving || this.sendingTest) return;
|
if (this.saving) return;
|
||||||
|
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
app.alerts.dismiss(this.successAlert);
|
app.alerts.dismiss(this.successAlert);
|
||||||
|
|
||||||
const settings = {};
|
const settings = {};
|
||||||
|
|
||||||
this.fields.forEach((key) => (settings[key] = this.values[key]()));
|
this.fields.forEach(key => settings[key] = this.values[key]());
|
||||||
|
|
||||||
saveSettings(settings)
|
saveSettings(settings)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.successAlert = app.alerts.show({
|
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
|
||||||
type: 'success',
|
|
||||||
children: app.translator.trans('core.admin.basics.saved_message'),
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.refresh();
|
m.redraw();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
js/src/admin/components/Page.js
Normal file
32
js/src/admin/components/Page.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import Component from '../../common/Component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Page` component
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
export default class Page extends Component {
|
||||||
|
init() {
|
||||||
|
app.previous = app.current;
|
||||||
|
app.current = this;
|
||||||
|
|
||||||
|
app.modal.close();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class name to apply to the body while the route is active.
|
||||||
|
*
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
this.bodyClass = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
if (this.bodyClass) {
|
||||||
|
$('#app').addClass(this.bodyClass);
|
||||||
|
|
||||||
|
context.onunload = () => $('#app').removeClass(this.bodyClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -8,21 +8,22 @@ 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);
|
||||||
|
|
||||||
return group ? GroupBadge.component({ group, label: null }) : '';
|
return group ? GroupBadge.component({group, label: null}) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterByRequiredPermissions(groupIds, permission) {
|
function filterByRequiredPermissions(groupIds, permission) {
|
||||||
app.getRequiredPermissions(permission).forEach((required) => {
|
app.getRequiredPermissions(permission)
|
||||||
|
.forEach(required => {
|
||||||
const restrictToGroupIds = app.data.permissions[required] || [];
|
const restrictToGroupIds = app.data.permissions[required] || [];
|
||||||
|
|
||||||
if (restrictToGroupIds.indexOf(Group.GUEST_ID) !== -1) {
|
if (restrictToGroupIds.indexOf(Group.GUEST_ID) !== -1) {
|
||||||
// do nothing
|
// do nothing
|
||||||
} else if (restrictToGroupIds.indexOf(Group.MEMBER_ID) !== -1) {
|
} else if (restrictToGroupIds.indexOf(Group.MEMBER_ID) !== -1) {
|
||||||
groupIds = groupIds.filter((id) => id !== Group.GUEST_ID);
|
groupIds = groupIds.filter(id => id !== Group.GUEST_ID);
|
||||||
} else if (groupIds.indexOf(Group.MEMBER_ID) !== -1) {
|
} else if (groupIds.indexOf(Group.MEMBER_ID) !== -1) {
|
||||||
groupIds = restrictToGroupIds;
|
groupIds = restrictToGroupIds;
|
||||||
} else {
|
} else {
|
||||||
groupIds = restrictToGroupIds.filter((id) => groupIds.indexOf(id) !== -1);
|
groupIds = restrictToGroupIds.filter(id => groupIds.indexOf(id) !== -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
groupIds = filterByRequiredPermissions(groupIds, required);
|
groupIds = filterByRequiredPermissions(groupIds, required);
|
||||||
@@ -51,31 +52,34 @@ 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: 'fas fa-globe' });
|
this.props.label = Badge.component({icon: 'fas fa-globe'});
|
||||||
} else if (members) {
|
} else if (members) {
|
||||||
this.props.label = Badge.component({ icon: 'fas fa-user' });
|
this.props.label = Badge.component({icon: 'fas fa-user'});
|
||||||
} else {
|
} else {
|
||||||
this.props.label = [badgeForId(Group.ADMINISTRATOR_ID), groupIds.map(badgeForId)];
|
this.props.label = [
|
||||||
|
badgeForId(Group.ADMINISTRATOR_ID),
|
||||||
|
groupIds.map(badgeForId)
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.showing) {
|
if (this.showing) {
|
||||||
if (this.props.allowGuest) {
|
if (this.props.allowGuest) {
|
||||||
this.props.children.push(
|
this.props.children.push(
|
||||||
Button.component({
|
Button.component({
|
||||||
children: [Badge.component({ icon: 'fas fa-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 ? 'fas fa-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)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.children.push(
|
this.props.children.push(
|
||||||
Button.component({
|
Button.component({
|
||||||
children: [Badge.component({ icon: 'fas fa-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 ? 'fas fa-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)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Separator.component(),
|
Separator.component(),
|
||||||
@@ -84,29 +88,26 @@ export default class PermissionDropdown extends Dropdown {
|
|||||||
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
|
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
|
||||||
icon: !everyone && !members ? 'fas fa-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();
|
||||||
this.save([]);
|
this.save([]);
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
[].push.apply(
|
[].push.apply(
|
||||||
this.props.children,
|
this.props.children,
|
||||||
app.store
|
app.store.all('groups')
|
||||||
.all('groups')
|
.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 ? 'fas fa-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());
|
||||||
},
|
},
|
||||||
disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID),
|
disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID)
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ export default class PermissionDropdown extends Dropdown {
|
|||||||
app.request({
|
app.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: app.forum.attribute('apiUrl') + '/permission',
|
url: app.forum.attribute('apiUrl') + '/permission',
|
||||||
data: { permission, groupIds },
|
data: {permission, groupIds}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ export default class PermissionDropdown extends Dropdown {
|
|||||||
groupIds.splice(index, 1);
|
groupIds.splice(index, 1);
|
||||||
} else {
|
} else {
|
||||||
groupIds.push(groupId);
|
groupIds.push(groupId);
|
||||||
groupIds = groupIds.filter((id) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1);
|
groupIds = groupIds.filter(id => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.save(groupIds);
|
this.save(groupIds);
|
||||||
|
@@ -13,8 +13,12 @@ export default class PermissionGrid extends Component {
|
|||||||
view() {
|
view() {
|
||||||
const scopes = this.scopeItems().toArray();
|
const scopes = this.scopeItems().toArray();
|
||||||
|
|
||||||
const permissionCells = (permission) => {
|
const permissionCells = permission => {
|
||||||
return scopes.map((scope) => <td>{scope.render(permission)}</td>);
|
return scopes.map(scope => (
|
||||||
|
<td>
|
||||||
|
{scope.render(permission)}
|
||||||
|
</td>
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -22,32 +26,27 @@ export default class PermissionGrid extends Component {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
{scopes.map((scope) => (
|
{scopes.map(scope => (
|
||||||
<th>
|
<th>
|
||||||
{scope.label}{' '}
|
{scope.label}{' '}
|
||||||
{scope.onremove
|
{scope.onremove ? Button.component({icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: 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>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{this.permissions.map((section) => (
|
{this.permissions.map(section => (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className="PermissionGrid-section">
|
<tr className="PermissionGrid-section">
|
||||||
<th>{section.label}</th>
|
<th>{section.label}</th>
|
||||||
{permissionCells(section)}
|
{permissionCells(section)}
|
||||||
<td />
|
<td/>
|
||||||
</tr>
|
</tr>
|
||||||
{section.children.map((child) => (
|
{section.children.map(child => (
|
||||||
<tr className="PermissionGrid-child">
|
<tr className="PermissionGrid-child">
|
||||||
<th>
|
<th>{icon(child.icon)}{child.label}</th>
|
||||||
{icon(child.icon)}
|
|
||||||
{child.label}
|
|
||||||
</th>
|
|
||||||
{permissionCells(child)}
|
{permissionCells(child)}
|
||||||
<td />
|
<td/>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -59,41 +58,25 @@ export default class PermissionGrid extends Component {
|
|||||||
permissionItems() {
|
permissionItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('view', {
|
||||||
'view',
|
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.permissions.read_heading'),
|
label: app.translator.trans('core.admin.permissions.read_heading'),
|
||||||
children: this.viewItems().toArray(),
|
children: this.viewItems().toArray()
|
||||||
},
|
}, 100);
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('start', {
|
||||||
'start',
|
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.permissions.create_heading'),
|
label: app.translator.trans('core.admin.permissions.create_heading'),
|
||||||
children: this.startItems().toArray(),
|
children: this.startItems().toArray()
|
||||||
},
|
}, 90);
|
||||||
90
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('reply', {
|
||||||
'reply',
|
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.permissions.participate_heading'),
|
label: app.translator.trans('core.admin.permissions.participate_heading'),
|
||||||
children: this.replyItems().toArray(),
|
children: this.replyItems().toArray()
|
||||||
},
|
}, 80);
|
||||||
80
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('moderate', {
|
||||||
'moderate',
|
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.permissions.moderate_heading'),
|
label: app.translator.trans('core.admin.permissions.moderate_heading'),
|
||||||
children: this.moderateItems().toArray(),
|
children: this.moderateItems().toArray()
|
||||||
},
|
}, 70);
|
||||||
70
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -101,54 +84,31 @@ export default class PermissionGrid extends Component {
|
|||||||
viewItems() {
|
viewItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('viewDiscussions', {
|
||||||
'viewDiscussions',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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(
|
items.add('viewUserList', {
|
||||||
'viewHiddenGroups',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-users',
|
|
||||||
label: app.translator.trans('core.admin.permissions.view_hidden_groups_label'),
|
|
||||||
permission: 'viewHiddenGroups',
|
|
||||||
},
|
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
'viewUserList',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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(
|
items.add('signUp', {
|
||||||
'signUp',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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: () =>
|
setting: () => SettingDropdown.component({
|
||||||
SettingDropdown.component({
|
|
||||||
key: 'allow_sign_up',
|
key: 'allow_sign_up',
|
||||||
options: [
|
options: [
|
||||||
{ value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button') },
|
{value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button')},
|
||||||
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button') },
|
{value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button')}
|
||||||
],
|
]
|
||||||
}),
|
})
|
||||||
},
|
}, 90);
|
||||||
90
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add('viewLastSeenAt', {
|
items.add('viewLastSeenAt', {
|
||||||
icon: 'far fa-clock',
|
icon: 'far fa-clock',
|
||||||
@@ -162,19 +122,13 @@ export default class PermissionGrid extends Component {
|
|||||||
startItems() {
|
startItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('start', {
|
||||||
'start',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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(
|
items.add('allowRenaming', {
|
||||||
'allowRenaming',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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: () => {
|
||||||
@@ -182,19 +136,17 @@ export default class PermissionGrid extends Component {
|
|||||||
|
|
||||||
return SettingDropdown.component({
|
return SettingDropdown.component({
|
||||||
defaultLabel: minutes
|
defaultLabel: minutes
|
||||||
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, { count: minutes })
|
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
|
||||||
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||||
key: 'allow_renaming',
|
key: 'allow_renaming',
|
||||||
options: [
|
options: [
|
||||||
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
|
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
|
||||||
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
|
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
|
||||||
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
|
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
},
|
}, 90);
|
||||||
90
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -202,19 +154,13 @@ export default class PermissionGrid extends Component {
|
|||||||
replyItems() {
|
replyItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('reply', {
|
||||||
'reply',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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(
|
items.add('allowPostEditing', {
|
||||||
'allowPostEditing',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-pencil-alt',
|
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: () => {
|
||||||
@@ -222,19 +168,17 @@ export default class PermissionGrid extends Component {
|
|||||||
|
|
||||||
return SettingDropdown.component({
|
return SettingDropdown.component({
|
||||||
defaultLabel: minutes
|
defaultLabel: minutes
|
||||||
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, { count: minutes })
|
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
|
||||||
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||||
key: 'allow_post_editing',
|
key: 'allow_post_editing',
|
||||||
options: [
|
options: [
|
||||||
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
|
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
|
||||||
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
|
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
|
||||||
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
|
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
},
|
}, 90);
|
||||||
90
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -242,95 +186,53 @@ export default class PermissionGrid extends Component {
|
|||||||
moderateItems() {
|
moderateItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('viewIpsPosts', {
|
||||||
'viewIpsPosts',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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(
|
items.add('renameDiscussions', {
|
||||||
'renameDiscussions',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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(
|
items.add('hideDiscussions', {
|
||||||
'hideDiscussions',
|
|
||||||
{
|
|
||||||
icon: 'far fa-trash-alt',
|
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(
|
items.add('deleteDiscussions', {
|
||||||
'deleteDiscussions',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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(
|
items.add('editPosts', {
|
||||||
'postWithoutThrottle',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-swimmer',
|
|
||||||
label: app.translator.trans('core.admin.permissions.post_without_throttle_label'),
|
|
||||||
permission: 'postWithoutThrottle',
|
|
||||||
},
|
|
||||||
70
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
'editPosts',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-pencil-alt',
|
icon: 'fas fa-pencil-alt',
|
||||||
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
|
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
|
||||||
permission: 'discussion.editPosts',
|
permission: 'discussion.editPosts'
|
||||||
},
|
}, 70);
|
||||||
70
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('hidePosts', {
|
||||||
'hidePosts',
|
|
||||||
{
|
|
||||||
icon: 'far fa-trash-alt',
|
icon: 'far fa-trash-alt',
|
||||||
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
|
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
|
||||||
permission: 'discussion.hidePosts',
|
permission: 'discussion.hidePosts'
|
||||||
},
|
}, 60);
|
||||||
60
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('deletePosts', {
|
||||||
'deletePosts',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-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
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('userEdit', {
|
||||||
'userEdit',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-user-cog',
|
icon: 'fas fa-user-cog',
|
||||||
label: app.translator.trans('core.admin.permissions.edit_users_label'),
|
label: app.translator.trans('core.admin.permissions.edit_users_label'),
|
||||||
permission: 'user.edit',
|
permission: 'user.edit'
|
||||||
},
|
}, 60);
|
||||||
60
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -338,25 +240,21 @@ export default class PermissionGrid extends Component {
|
|||||||
scopeItems() {
|
scopeItems() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('global', {
|
||||||
'global',
|
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.permissions.global_heading'),
|
label: app.translator.trans('core.admin.permissions.global_heading'),
|
||||||
render: (item) => {
|
render: item => {
|
||||||
if (item.setting) {
|
if (item.setting) {
|
||||||
return item.setting();
|
return item.setting();
|
||||||
} else if (item.permission) {
|
} else if (item.permission) {
|
||||||
return PermissionDropdown.component({
|
return PermissionDropdown.component({
|
||||||
permission: item.permission,
|
permission: item.permission,
|
||||||
allowGuest: item.allowGuest,
|
allowGuest: item.allowGuest
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
},
|
}
|
||||||
},
|
}, 100);
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import Page from '../../common/components/Page';
|
import Page from './Page';
|
||||||
import GroupBadge from '../../common/components/GroupBadge';
|
import GroupBadge from '../../common/components/GroupBadge';
|
||||||
import EditGroupModal from './EditGroupModal';
|
import EditGroupModal from './EditGroupModal';
|
||||||
import Group from '../../common/models/Group';
|
import Group from '../../common/models/Group';
|
||||||
@@ -11,28 +11,29 @@ export default class PermissionsPage extends Page {
|
|||||||
<div className="PermissionsPage">
|
<div className="PermissionsPage">
|
||||||
<div className="PermissionsPage-groups">
|
<div className="PermissionsPage-groups">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{app.store
|
{app.store.all('groups')
|
||||||
.all('groups')
|
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||||
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
.map(group => (
|
||||||
.map((group) => (
|
<button className="Button Group" onclick={() => app.modal.show(new EditGroupModal({group}))}>
|
||||||
<button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}>
|
|
||||||
{GroupBadge.component({
|
{GroupBadge.component({
|
||||||
group,
|
group,
|
||||||
className: 'Group-icon',
|
className: 'Group-icon',
|
||||||
label: null,
|
label: null
|
||||||
})}
|
})}
|
||||||
<span className="Group-name">{group.namePlural()}</span>
|
<span className="Group-name">{group.namePlural()}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<button className="Button Group Group--add" onclick={() => app.modal.show(EditGroupModal)}>
|
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
|
||||||
{icon('fas fa-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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="PermissionsPage-permissions">
|
<div className="PermissionsPage-permissions">
|
||||||
<div className="container">{PermissionGrid.component()}</div>
|
<div className="container">
|
||||||
|
{PermissionGrid.component()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -26,7 +26,10 @@ export default class SessionDropdown extends Dropdown {
|
|||||||
getButtonContent() {
|
getButtonContent() {
|
||||||
const user = app.session.user;
|
const user = app.session.user;
|
||||||
|
|
||||||
return [avatar(user), ' ', <span className="Button-label">{username(user)}</span>];
|
return [
|
||||||
|
avatar(user), ' ',
|
||||||
|
<span className="Button-label">{username(user)}</span>
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,12 +40,11 @@ export default class SessionDropdown extends Dropdown {
|
|||||||
items() {
|
items() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('logOut',
|
||||||
'logOut',
|
|
||||||
Button.component({
|
Button.component({
|
||||||
icon: 'fas fa-sign-out-alt',
|
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)
|
||||||
}),
|
}),
|
||||||
-100
|
-100
|
||||||
);
|
);
|
||||||
|
@@ -11,14 +11,14 @@ export default class SettingDropdown extends SelectDropdown {
|
|||||||
props.caretIcon = 'fas fa-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}) => {
|
||||||
const active = app.data.settings[props.key] === value;
|
const active = app.data.settings[props.key] === value;
|
||||||
|
|
||||||
return Button.component({
|
return Button.component({
|
||||||
children: label,
|
children: label,
|
||||||
icon: active ? 'fas fa-check' : true,
|
icon: active ? 'fas fa-check' : true,
|
||||||
onclick: saveSettings.bind(this, { [props.key]: value }),
|
onclick: saveSettings.bind(this, {[props.key]: value}),
|
||||||
active,
|
active
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,9 @@ export default class SettingsModal extends Modal {
|
|||||||
<div className="Form">
|
<div className="Form">
|
||||||
{this.form()}
|
{this.form()}
|
||||||
|
|
||||||
<div className="Form-group">{this.submitButton()}</div>
|
<div className="Form-group">
|
||||||
|
{this.submitButton()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -26,7 +28,11 @@ export default class SettingsModal extends Modal {
|
|||||||
|
|
||||||
submitButton() {
|
submitButton() {
|
||||||
return (
|
return (
|
||||||
<Button type="submit" className="Button Button--primary" loading={this.loading} disabled={!this.changed()}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="Button Button--primary"
|
||||||
|
loading={this.loading}
|
||||||
|
disabled={!this.changed()}>
|
||||||
{app.translator.trans('core.admin.settings.submit_button')}
|
{app.translator.trans('core.admin.settings.submit_button')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -41,7 +47,7 @@ export default class SettingsModal extends Modal {
|
|||||||
dirty() {
|
dirty() {
|
||||||
const dirty = {};
|
const dirty = {};
|
||||||
|
|
||||||
Object.keys(this.settings).forEach((key) => {
|
Object.keys(this.settings).forEach(key => {
|
||||||
const value = this.settings[key]();
|
const value = this.settings[key]();
|
||||||
|
|
||||||
if (value !== app.data.settings[key]) {
|
if (value !== app.data.settings[key]) {
|
||||||
@@ -61,7 +67,10 @@ export default class SettingsModal extends Modal {
|
|||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
saveSettings(this.dirty()).then(this.onsaved.bind(this), this.loaded.bind(this));
|
saveSettings(this.dirty()).then(
|
||||||
|
this.onsaved.bind(this),
|
||||||
|
this.loaded.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onsaved() {
|
onsaved() {
|
||||||
|
@@ -20,39 +20,39 @@ export default class StatusWidget extends DashboardWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
return <ul>{listItems(this.items().toArray())}</ul>;
|
return (
|
||||||
|
<ul>{listItems(this.items().toArray())}</ul>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
items() {
|
items() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
items.add('tools', (
|
||||||
'tools',
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
label={app.translator.trans('core.admin.dashboard.tools_button')}
|
label={app.translator.trans('core.admin.dashboard.tools_button')}
|
||||||
icon="fas fa-cog"
|
icon="fas fa-cog"
|
||||||
buttonClassName="Button"
|
buttonClassName="Button"
|
||||||
menuClassName="Dropdown-menu--right"
|
menuClassName="Dropdown-menu--right">
|
||||||
>
|
<Button onclick={this.handleClearCache.bind(this)}>
|
||||||
<Button onclick={this.handleClearCache.bind(this)}>{app.translator.trans('core.admin.dashboard.clear_cache_button')}</Button>
|
{app.translator.trans('core.admin.dashboard.clear_cache_button')}
|
||||||
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
));
|
||||||
|
|
||||||
items.add('version-flarum', [<strong>Flarum</strong>, <br />, app.forum.attribute('version')]);
|
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-php', [<strong>PHP</strong>, <br/>, app.data.phpVersion]);
|
||||||
items.add('version-mysql', [<strong>MySQL</strong>, <br />, app.data.mysqlVersion]);
|
items.add('version-mysql', [<strong>MySQL</strong>, <br/>, app.data.mysqlVersion]);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClearCache(e) {
|
handleClearCache(e) {
|
||||||
app.modal.show(LoadingModal);
|
app.modal.show(new LoadingModal());
|
||||||
|
|
||||||
app
|
app.request({
|
||||||
.request({
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: app.forum.attribute('apiUrl') + '/cache',
|
url: app.forum.attribute('apiUrl') + '/cache'
|
||||||
})
|
}).then(() => window.location.reload());
|
||||||
.then(() => window.location.reload());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,9 +15,7 @@ export default class UploadImageButton extends Button {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p><img src={app.forum.attribute(this.props.name+'Url')} alt=""/></p>
|
||||||
<img src={app.forum.attribute(this.props.name + 'Url')} alt="" />
|
|
||||||
</p>
|
|
||||||
<p>{super.view()}</p>
|
<p>{super.view()}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -37,25 +35,22 @@ export default class UploadImageButton extends Button {
|
|||||||
|
|
||||||
const $input = $('<input type="file">');
|
const $input = $('<input type="file">');
|
||||||
|
|
||||||
$input
|
$input.appendTo('body').hide().click().on('change', e => {
|
||||||
.appendTo('body')
|
|
||||||
.hide()
|
|
||||||
.click()
|
|
||||||
.on('change', (e) => {
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append(this.props.name, $(e.target)[0].files[0]);
|
data.append(this.props.name, $(e.target)[0].files[0]);
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
|
||||||
app
|
app.request({
|
||||||
.request({
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: this.resourceUrl(),
|
url: this.resourceUrl(),
|
||||||
serialize: (raw) => raw,
|
serialize: raw => raw,
|
||||||
data,
|
data
|
||||||
})
|
}).then(
|
||||||
.then(this.success.bind(this), this.failure.bind(this));
|
this.success.bind(this),
|
||||||
|
this.failure.bind(this)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,12 +61,13 @@ export default class UploadImageButton extends Button {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
|
||||||
app
|
app.request({
|
||||||
.request({
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: this.resourceUrl(),
|
url: this.resourceUrl()
|
||||||
})
|
}).then(
|
||||||
.then(this.success.bind(this), this.failure.bind(this));
|
this.success.bind(this),
|
||||||
|
this.failure.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceUrl() {
|
resourceUrl() {
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
@@ -9,6 +9,7 @@ export { app };
|
|||||||
|
|
||||||
// Export public API
|
// Export public API
|
||||||
|
|
||||||
|
|
||||||
// Export compat API
|
// Export compat API
|
||||||
import compat from './compat';
|
import compat from './compat';
|
||||||
|
|
||||||
|
@@ -10,13 +10,13 @@ import MailPage from './components/MailPage';
|
|||||||
*
|
*
|
||||||
* @param {App} app
|
* @param {App} app
|
||||||
*/
|
*/
|
||||||
export default function (app) {
|
export default function(app) {
|
||||||
app.routes = {
|
app.routes = {
|
||||||
dashboard: { path: '/', component: DashboardPage.component() },
|
'dashboard': {path: '/', component: DashboardPage.component()},
|
||||||
basics: { path: '/basics', component: BasicsPage.component() },
|
'basics': {path: '/basics', component: BasicsPage.component()},
|
||||||
permissions: { path: '/permissions', component: PermissionsPage.component() },
|
'permissions': {path: '/permissions', component: PermissionsPage.component()},
|
||||||
appearance: { path: '/appearance', component: AppearancePage.component() },
|
'appearance': {path: '/appearance', component: AppearancePage.component()},
|
||||||
extensions: { path: '/extensions', component: ExtensionsPage.component() },
|
'extensions': {path: '/extensions', component: ExtensionsPage.component()},
|
||||||
mail: { path: '/mail', component: MailPage.component() },
|
'mail': {path: '/mail', component: MailPage.component()}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -3,13 +3,11 @@ export default function saveSettings(settings) {
|
|||||||
|
|
||||||
Object.assign(app.data.settings, settings);
|
Object.assign(app.data.settings, settings);
|
||||||
|
|
||||||
return app
|
return app.request({
|
||||||
.request({
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: app.forum.attribute('apiUrl') + '/settings',
|
url: app.forum.attribute('apiUrl') + '/settings',
|
||||||
data: settings,
|
data: settings
|
||||||
})
|
}).catch(error => {
|
||||||
.catch((error) => {
|
|
||||||
app.data.settings = oldSettings;
|
app.data.settings = oldSettings;
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import ItemList from './utils/ItemList';
|
import ItemList from './utils/ItemList';
|
||||||
|
import Alert from './components/Alert';
|
||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
import ModalManager from './components/ModalManager';
|
import ModalManager from './components/ModalManager';
|
||||||
import AlertManager from './components/AlertManager';
|
import AlertManager from './components/AlertManager';
|
||||||
@@ -11,7 +12,6 @@ import Drawer from './utils/Drawer';
|
|||||||
import mapRoutes from './utils/mapRoutes';
|
import mapRoutes from './utils/mapRoutes';
|
||||||
import RequestError from './utils/RequestError';
|
import RequestError from './utils/RequestError';
|
||||||
import ScrollListener from './utils/ScrollListener';
|
import ScrollListener from './utils/ScrollListener';
|
||||||
import liveHumanTimes from './utils/liveHumanTimes';
|
|
||||||
import { extend } from './extend';
|
import { extend } from './extend';
|
||||||
|
|
||||||
import Forum from './models/Forum';
|
import Forum from './models/Forum';
|
||||||
@@ -21,9 +21,6 @@ import Post from './models/Post';
|
|||||||
import Group from './models/Group';
|
import Group from './models/Group';
|
||||||
import Notification from './models/Notification';
|
import Notification from './models/Notification';
|
||||||
import { flattenDeep } from 'lodash-es';
|
import { flattenDeep } from 'lodash-es';
|
||||||
import PageState from './states/PageState';
|
|
||||||
import ModalManagerState from './states/ModalManagerState';
|
|
||||||
import AlertManagerState from './states/AlertManagerState';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `App` class provides a container for an application, as well as various
|
* The `App` class provides a container for an application, as well as various
|
||||||
@@ -89,7 +86,7 @@ export default class Application {
|
|||||||
discussions: Discussion,
|
discussions: Discussion,
|
||||||
posts: Post,
|
posts: Post,
|
||||||
groups: Group,
|
groups: Group,
|
||||||
notifications: Notification,
|
notifications: Notification
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,49 +107,13 @@ export default class Application {
|
|||||||
booted = false;
|
booted = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The key for an Alert that was shown as a result of an AJAX request error.
|
* An Alert that was shown as a result of an AJAX request error. If present,
|
||||||
* If present, it will be dismissed on the next successful request.
|
* it will be dismissed on the next successful request.
|
||||||
*
|
*
|
||||||
* @type {int}
|
* @type {null|Alert}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
requestErrorAlert = null;
|
requestError = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* The page the app is currently on.
|
|
||||||
*
|
|
||||||
* This object holds information about the type of page we are currently
|
|
||||||
* visiting, and sometimes additional arbitrary page state that may be
|
|
||||||
* relevant to lower-level components.
|
|
||||||
*
|
|
||||||
* @type {PageState}
|
|
||||||
*/
|
|
||||||
current = new PageState(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The page the app was on before the current page.
|
|
||||||
*
|
|
||||||
* Once the application navigates to another page, the object previously
|
|
||||||
* assigned to this.current will be moved to this.previous, while this.current
|
|
||||||
* is re-initialized.
|
|
||||||
*
|
|
||||||
* @type {PageState}
|
|
||||||
*/
|
|
||||||
previous = new PageState(null);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* An object that manages modal state.
|
|
||||||
*
|
|
||||||
* @type {ModalManagerState}
|
|
||||||
*/
|
|
||||||
modal = new ModalManagerState();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object that manages the state of active alerts.
|
|
||||||
*
|
|
||||||
* @type {AlertManagerState}
|
|
||||||
*/
|
|
||||||
alerts = new AlertManagerState();
|
|
||||||
|
|
||||||
data;
|
data;
|
||||||
|
|
||||||
@@ -165,19 +126,22 @@ export default class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boot() {
|
boot() {
|
||||||
this.initializers.toArray().forEach((initializer) => initializer(this));
|
this.initializers.toArray().forEach(initializer => initializer(this));
|
||||||
|
|
||||||
this.store.pushPayload({ data: this.data.resources });
|
this.store.pushPayload({data: this.data.resources});
|
||||||
|
|
||||||
this.forum = this.store.getById('forums', 1);
|
this.forum = this.store.getById('forums', 1);
|
||||||
|
|
||||||
this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
|
this.session = new Session(
|
||||||
|
this.store.getById('users', this.data.session.userId),
|
||||||
|
this.data.session.csrfToken
|
||||||
|
);
|
||||||
|
|
||||||
this.mount();
|
this.mount();
|
||||||
}
|
}
|
||||||
|
|
||||||
bootExtensions(extensions) {
|
bootExtensions(extensions) {
|
||||||
Object.keys(extensions).forEach((name) => {
|
Object.keys(extensions).forEach(name => {
|
||||||
const extension = extensions[name];
|
const extension = extensions[name];
|
||||||
|
|
||||||
const extenders = flattenDeep(extension.extend);
|
const extenders = flattenDeep(extension.extend);
|
||||||
@@ -189,27 +153,31 @@ export default class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mount(basePath = '') {
|
mount(basePath = '') {
|
||||||
m.mount(document.getElementById('modal'), <ModalManager state={this.modal} />);
|
this.modal = m.mount(document.getElementById('modal'), <ModalManager/>);
|
||||||
m.mount(document.getElementById('alerts'), <AlertManager state={this.alerts} />);
|
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>);
|
||||||
|
|
||||||
this.drawer = new Drawer();
|
this.drawer = new Drawer();
|
||||||
|
|
||||||
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
|
m.route(
|
||||||
|
document.getElementById('content'),
|
||||||
|
basePath + '/',
|
||||||
|
mapRoutes(this.routes, basePath)
|
||||||
|
);
|
||||||
|
|
||||||
// Add a class to the body which indicates that the page has been scrolled
|
// Add a class to the body which indicates that the page has been scrolled
|
||||||
// down.
|
// down.
|
||||||
new ScrollListener((top) => {
|
new ScrollListener(top => {
|
||||||
const $app = $('#app');
|
const $app = $('#app');
|
||||||
const offset = $app.offset().top;
|
const offset = $app.offset().top;
|
||||||
|
|
||||||
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
|
$app
|
||||||
|
.toggleClass('affix', top >= offset)
|
||||||
|
.toggleClass('scrolled', top > offset);
|
||||||
}).start();
|
}).start();
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
||||||
});
|
});
|
||||||
|
|
||||||
liveHumanTimes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,16 +198,6 @@ export default class Application {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the current screen mode, based on our media queries.
|
|
||||||
*
|
|
||||||
* @returns {String} - one of "phone", "tablet", "desktop" or "desktop-hd"
|
|
||||||
*/
|
|
||||||
screen() {
|
|
||||||
const styles = getComputedStyle(document.documentElement);
|
|
||||||
return styles.getPropertyValue('--flarum-screen');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the <title> of the page.
|
* Set the <title> of the page.
|
||||||
*
|
*
|
||||||
@@ -262,10 +220,9 @@ export default class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTitle() {
|
updateTitle() {
|
||||||
const count = this.titleCount ? `(${this.titleCount}) ` : '';
|
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') +
|
||||||
const pageTitleWithSeparator = this.title && m.route() !== '/' ? this.title + ' - ' : '';
|
(this.title ? this.title + ' - ' : '') +
|
||||||
const title = this.forum.attribute('title');
|
this.forum.attribute('title');
|
||||||
document.title = count + pageTitleWithSeparator + title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -299,11 +256,9 @@ export default class Application {
|
|||||||
// When we deserialize JSON data, if for some reason the server has provided
|
// 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
|
// a dud response, we don't want the application to crash. We'll show an
|
||||||
// error message to the user instead.
|
// error message to the user instead.
|
||||||
options.deserialize = options.deserialize || ((responseText) => responseText);
|
options.deserialize = options.deserialize || (responseText => responseText);
|
||||||
|
|
||||||
options.errorHandler =
|
options.errorHandler = options.errorHandler || (error => {
|
||||||
options.errorHandler ||
|
|
||||||
((error) => {
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,7 +266,7 @@ export default class Application {
|
|||||||
// response code and show an error message to the user if something's gone
|
// response code and show an error message to the user if something's gone
|
||||||
// awry.
|
// awry.
|
||||||
const original = options.extract;
|
const original = options.extract;
|
||||||
options.extract = (xhr) => {
|
options.extract = xhr => {
|
||||||
let responseText;
|
let responseText;
|
||||||
|
|
||||||
if (original) {
|
if (original) {
|
||||||
@@ -338,21 +293,21 @@ export default class Application {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.requestErrorAlert) this.alerts.dismiss(this.requestErrorAlert);
|
if (this.requestError) this.alerts.dismiss(this.requestError.alert);
|
||||||
|
|
||||||
// Now make the request. If it's a failure, inspect the error that was
|
// Now make the request. If it's a failure, inspect the error that was
|
||||||
// returned and show an alert containing its contents.
|
// returned and show an alert containing its contents.
|
||||||
const deferred = m.deferred();
|
const deferred = m.deferred();
|
||||||
|
|
||||||
m.request(options).then(
|
m.request(options).then(response => deferred.resolve(response), error => {
|
||||||
(response) => deferred.resolve(response),
|
this.requestError = error;
|
||||||
(error) => {
|
|
||||||
let children;
|
let children;
|
||||||
|
|
||||||
switch (error.status) {
|
switch (error.status) {
|
||||||
case 422:
|
case 422:
|
||||||
children = error.response.errors
|
children = error.response.errors
|
||||||
.map((error) => [error.detail, <br />])
|
.map(error => [error.detail, <br/>])
|
||||||
.reduce((a, b) => a.concat(b), [])
|
.reduce((a, b) => a.concat(b), [])
|
||||||
.slice(0, -1);
|
.slice(0, -1);
|
||||||
break;
|
break;
|
||||||
@@ -376,53 +331,35 @@ export default class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDebug = app.forum.attribute('debug');
|
const isDebug = app.forum.attribute('debug');
|
||||||
// contains a formatted errors if possible, response must be an JSON API array of errors
|
|
||||||
// the details property is decoded to transform escaped characters such as '\n'
|
|
||||||
const formattedError = error.response && Array.isArray(error.response.errors) && error.response.errors.map((e) => decodeURI(e.detail));
|
|
||||||
|
|
||||||
error.alert = {
|
error.alert = new Alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
children,
|
children,
|
||||||
controls: isDebug && [
|
controls: isDebug && [
|
||||||
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error, formattedError)}>
|
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>Debug</Button>
|
||||||
Debug
|
]
|
||||||
</Button>,
|
});
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
options.errorHandler(error);
|
options.errorHandler(error);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isDebug && error.xhr) {
|
this.alerts.show(error.alert);
|
||||||
const { method, url } = error.options;
|
|
||||||
const { status = '' } = error.xhr;
|
|
||||||
|
|
||||||
console.group(`${method} ${url} ${status}`);
|
|
||||||
|
|
||||||
console.error(...(formattedError || [error]));
|
|
||||||
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.requestErrorAlert = this.alerts.show(error.alert);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {RequestError} error
|
* @param {RequestError} error
|
||||||
* @param {string[]} [formattedError]
|
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
showDebug(error, formattedError) {
|
showDebug(error) {
|
||||||
this.alerts.dismiss(this.requestErrorAlert);
|
this.alerts.dismiss(this.requestError.alert);
|
||||||
|
|
||||||
this.modal.show(RequestErrorModal, { error, formattedError });
|
this.modal.show(new RequestErrorModal({error}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -70,7 +70,8 @@ export default class Component {
|
|||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
init() {}
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the component is destroyed, i.e. after a redraw where it is no
|
* Called when the component is destroyed, i.e. after a redraw where it is no
|
||||||
@@ -80,7 +81,8 @@ export default class Component {
|
|||||||
* @param {Object} e
|
* @param {Object} e
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
onunload() {}
|
onunload() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the renderable virtual DOM that represents the component's view.
|
* Get the renderable virtual DOM that represents the component's view.
|
||||||
@@ -97,7 +99,7 @@ export default class Component {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const vdom = this.retain ? { subtree: 'retain' } : this.view();
|
const vdom = this.retain ? {subtree: 'retain'} : this.view();
|
||||||
|
|
||||||
// Override the root element's config attribute with our own function, which
|
// Override the root element's config attribute with our own function, which
|
||||||
// will set the component instance's element property to the root DOM
|
// will set the component instance's element property to the root DOM
|
||||||
@@ -146,7 +148,8 @@ export default class Component {
|
|||||||
* @param {Object} vdom
|
* @param {Object} vdom
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
config() {}
|
config() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the virtual DOM that represents the component's view.
|
* Get the virtual DOM that represents the component's view.
|
||||||
@@ -198,14 +201,14 @@ export default class Component {
|
|||||||
controller: this.bind(undefined, componentProps),
|
controller: this.bind(undefined, componentProps),
|
||||||
view: view,
|
view: view,
|
||||||
props: componentProps,
|
props: componentProps,
|
||||||
component: this,
|
component: this
|
||||||
};
|
};
|
||||||
|
|
||||||
// If a `key` prop was set, then we'll assume that we want that to actually
|
// 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
|
// show up as an attribute on the component object so that Mithril's key
|
||||||
// algorithm can be applied.
|
// algorithm can be applied.
|
||||||
if (componentProps.key) {
|
if (componentProps.key) {
|
||||||
output.attrs = { key: componentProps.key };
|
output.attrs = {key: componentProps.key};
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
@@ -217,5 +220,6 @@ export default class Component {
|
|||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
static initProps(props) {}
|
static initProps(props) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -88,7 +88,7 @@ export default class Model {
|
|||||||
// relationship data object.
|
// relationship data object.
|
||||||
for (const innerKey in data[key]) {
|
for (const innerKey in data[key]) {
|
||||||
if (data[key][innerKey] instanceof Model) {
|
if (data[key][innerKey] instanceof Model) {
|
||||||
data[key][innerKey] = { data: Model.getIdentifier(data[key][innerKey]) };
|
data[key][innerKey] = {data: Model.getIdentifier(data[key][innerKey])};
|
||||||
}
|
}
|
||||||
this.data[key][innerKey] = data[key][innerKey];
|
this.data[key][innerKey] = data[key][innerKey];
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ export default class Model {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
pushAttributes(attributes) {
|
pushAttributes(attributes) {
|
||||||
this.pushData({ attributes });
|
this.pushData({attributes});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,7 +125,7 @@ export default class Model {
|
|||||||
const data = {
|
const data = {
|
||||||
type: this.data.type,
|
type: this.data.type,
|
||||||
id: this.data.id,
|
id: this.data.id,
|
||||||
attributes,
|
attributes
|
||||||
};
|
};
|
||||||
|
|
||||||
// If a 'relationships' key exists, extract it from the attributes hash and
|
// If a 'relationships' key exists, extract it from the attributes hash and
|
||||||
@@ -138,7 +138,9 @@ export default class Model {
|
|||||||
const model = attributes.relationships[key];
|
const model = attributes.relationships[key];
|
||||||
|
|
||||||
data.relationships[key] = {
|
data.relationships[key] = {
|
||||||
data: model instanceof Array ? model.map(Model.getIdentifier) : Model.getIdentifier(model),
|
data: model instanceof Array
|
||||||
|
? model.map(Model.getIdentifier)
|
||||||
|
: Model.getIdentifier(model)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,25 +154,18 @@ export default class Model {
|
|||||||
|
|
||||||
this.pushData(data);
|
this.pushData(data);
|
||||||
|
|
||||||
const request = { data };
|
const request = {data};
|
||||||
if (options.meta) request.meta = options.meta;
|
if (options.meta) request.meta = options.meta;
|
||||||
|
|
||||||
return app
|
return app.request(Object.assign({
|
||||||
.request(
|
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
method: this.exists ? 'PATCH' : 'POST',
|
method: this.exists ? 'PATCH' : 'POST',
|
||||||
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
||||||
data: request,
|
data: request
|
||||||
},
|
}, options)).then(
|
||||||
options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
// If everything went well, we'll make sure the store knows that this
|
// If everything went well, we'll make sure the store knows that this
|
||||||
// model exists now (if it didn't already), and we'll push the data that
|
// model exists now (if it didn't already), and we'll push the data that
|
||||||
// the API returned into the store.
|
// the API returned into the store.
|
||||||
(payload) => {
|
payload => {
|
||||||
this.store.data[payload.data.type] = this.store.data[payload.data.type] || {};
|
this.store.data[payload.data.type] = this.store.data[payload.data.type] || {};
|
||||||
this.store.data[payload.data.type][payload.data.id] = this;
|
this.store.data[payload.data.type][payload.data.id] = this;
|
||||||
return this.store.pushPayload(payload);
|
return this.store.pushPayload(payload);
|
||||||
@@ -178,7 +173,7 @@ export default class Model {
|
|||||||
|
|
||||||
// If something went wrong, though... good thing we backed up our model's
|
// If something went wrong, though... good thing we backed up our model's
|
||||||
// old data! We'll revert to that and let others handle the error.
|
// old data! We'll revert to that and let others handle the error.
|
||||||
(response) => {
|
response => {
|
||||||
this.pushData(oldData);
|
this.pushData(oldData);
|
||||||
m.lazyRedraw();
|
m.lazyRedraw();
|
||||||
throw response;
|
throw response;
|
||||||
@@ -197,18 +192,11 @@ export default class Model {
|
|||||||
delete(data, options = {}) {
|
delete(data, options = {}) {
|
||||||
if (!this.exists) return m.deferred().resolve().promise;
|
if (!this.exists) return m.deferred().resolve().promise;
|
||||||
|
|
||||||
return app
|
return app.request(Object.assign({
|
||||||
.request(
|
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
||||||
data,
|
data
|
||||||
},
|
}, options)).then(() => {
|
||||||
options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.exists = false;
|
this.exists = false;
|
||||||
this.store.remove(this);
|
this.store.remove(this);
|
||||||
});
|
});
|
||||||
@@ -237,7 +225,7 @@ export default class Model {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
static attribute(name, transform) {
|
static attribute(name, transform) {
|
||||||
return function () {
|
return function() {
|
||||||
const value = this.data.attributes && this.data.attributes[name];
|
const value = this.data.attributes && this.data.attributes[name];
|
||||||
|
|
||||||
return transform ? transform(value) : value;
|
return transform ? transform(value) : value;
|
||||||
@@ -255,7 +243,7 @@ export default class Model {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
static hasOne(name) {
|
static hasOne(name) {
|
||||||
return function () {
|
return function() {
|
||||||
if (this.data.relationships) {
|
if (this.data.relationships) {
|
||||||
const relationship = this.data.relationships[name];
|
const relationship = this.data.relationships[name];
|
||||||
|
|
||||||
@@ -279,12 +267,12 @@ export default class Model {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
static hasMany(name) {
|
static hasMany(name) {
|
||||||
return function () {
|
return function() {
|
||||||
if (this.data.relationships) {
|
if (this.data.relationships) {
|
||||||
const relationship = this.data.relationships[name];
|
const relationship = this.data.relationships[name];
|
||||||
|
|
||||||
if (relationship) {
|
if (relationship) {
|
||||||
return relationship.data.map((data) => app.store.getById(data.type, data.id));
|
return relationship.data.map(data => app.store.getById(data.type, data.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +301,7 @@ export default class Model {
|
|||||||
static getIdentifier(model) {
|
static getIdentifier(model) {
|
||||||
return {
|
return {
|
||||||
type: model.data.type,
|
type: model.data.type,
|
||||||
id: model.data.id,
|
id: model.data.id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,16 +31,11 @@ export default class Session {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
login(data, options = {}) {
|
login(data, options = {}) {
|
||||||
return app.request(
|
return app.request(Object.assign({
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: app.forum.attribute('baseUrl') + '/login',
|
url: app.forum.attribute('baseUrl') + '/login',
|
||||||
data,
|
data
|
||||||
},
|
}, options));
|
||||||
options
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -34,7 +34,9 @@ export default class Store {
|
|||||||
pushPayload(payload) {
|
pushPayload(payload) {
|
||||||
if (payload.included) payload.included.map(this.pushObject.bind(this));
|
if (payload.included) payload.included.map(this.pushObject.bind(this));
|
||||||
|
|
||||||
const result = payload.data instanceof Array ? payload.data.map(this.pushObject.bind(this)) : this.pushObject(payload.data);
|
const result = payload.data instanceof Array
|
||||||
|
? payload.data.map(this.pushObject.bind(this))
|
||||||
|
: this.pushObject(payload.data);
|
||||||
|
|
||||||
// Attach the original payload to the model that we give back. This is
|
// Attach the original payload to the model that we give back. This is
|
||||||
// useful to consumers as it allows them to access meta information
|
// useful to consumers as it allows them to access meta information
|
||||||
@@ -56,7 +58,7 @@ export default class Store {
|
|||||||
pushObject(data) {
|
pushObject(data) {
|
||||||
if (!this.models[data.type]) return null;
|
if (!this.models[data.type]) return null;
|
||||||
|
|
||||||
const type = (this.data[data.type] = this.data[data.type] || {});
|
const type = this.data[data.type] = this.data[data.type] || {};
|
||||||
|
|
||||||
if (type[data.id]) {
|
if (type[data.id]) {
|
||||||
type[data.id].pushData(data);
|
type[data.id].pushData(data);
|
||||||
@@ -93,18 +95,11 @@ export default class Store {
|
|||||||
url += '/' + id;
|
url += '/' + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app.request(Object.assign({
|
||||||
.request(
|
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
data,
|
data
|
||||||
},
|
}, options)).then(this.pushPayload.bind(this));
|
||||||
options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(this.pushPayload.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,7 +124,7 @@ export default class Store {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getBy(type, key, value) {
|
getBy(type, key, value) {
|
||||||
return this.all(type).filter((model) => model[key]() === value)[0];
|
return this.all(type).filter(model => model[key]() === value)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,7 +137,7 @@ export default class Store {
|
|||||||
all(type) {
|
all(type) {
|
||||||
const records = this.data[type];
|
const records = this.data[type];
|
||||||
|
|
||||||
return records ? Object.keys(records).map((id) => records[id]) : [];
|
return records ? Object.keys(records).map(id => records[id]) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,6 +160,6 @@ export default class Store {
|
|||||||
createRecord(type, data = {}) {
|
createRecord(type, data = {}) {
|
||||||
data.type = data.type || type;
|
data.type = data.type || type;
|
||||||
|
|
||||||
return new this.models[type](data, this);
|
return new (this.models[type])(data, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -67,7 +67,7 @@ export default class Translator {
|
|||||||
const hydrated = [];
|
const hydrated = [];
|
||||||
const open = [hydrated];
|
const open = [hydrated];
|
||||||
|
|
||||||
translation.forEach((part) => {
|
translation.forEach(part => {
|
||||||
const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));
|
const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -77,7 +77,7 @@ export default class Translator {
|
|||||||
if (match[2]) {
|
if (match[2]) {
|
||||||
open.shift();
|
open.shift();
|
||||||
} else {
|
} else {
|
||||||
let tag = input[match[3]] || { tag: match[3], children: [] };
|
let tag = input[match[3]] || {tag: match[3], children: []};
|
||||||
open[0].push(tag);
|
open[0].push(tag);
|
||||||
open.unshift(tag.children || tag);
|
open.unshift(tag.children || tag);
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ export default class Translator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return hydrated.filter((part) => part);
|
return hydrated.filter(part => part);
|
||||||
}
|
}
|
||||||
|
|
||||||
pluralize(translation, number) {
|
pluralize(translation, number) {
|
||||||
@@ -97,7 +97,7 @@ export default class Translator {
|
|||||||
standardRules = [],
|
standardRules = [],
|
||||||
explicitRules = [];
|
explicitRules = [];
|
||||||
|
|
||||||
translation.split('|').forEach((part) => {
|
translation.split('|').forEach(part => {
|
||||||
if (cPluralRegex.test(part)) {
|
if (cPluralRegex.test(part)) {
|
||||||
const matches = part.match(cPluralRegex);
|
const matches = part.match(cPluralRegex);
|
||||||
explicitRules[matches[0]] = matches[matches.length - 1];
|
explicitRules[matches[0]] = matches[matches.length - 1];
|
||||||
@@ -125,10 +125,8 @@ export default class Translator {
|
|||||||
var leftNumber = this.convertNumber(matches[4]);
|
var leftNumber = this.convertNumber(matches[4]);
|
||||||
var rightNumber = this.convertNumber(matches[5]);
|
var rightNumber = this.convertNumber(matches[5]);
|
||||||
|
|
||||||
if (
|
if (('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
|
||||||
('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
|
(']' === matches[6] ? number <= rightNumber : number < rightNumber)) {
|
||||||
(']' === matches[6] ? number <= rightNumber : number < rightNumber)
|
|
||||||
) {
|
|
||||||
return explicitRules[e];
|
return explicitRules[e];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,7 +223,7 @@ export default class Translator {
|
|||||||
case 'tr':
|
case 'tr':
|
||||||
case 'ur':
|
case 'ur':
|
||||||
case 'zu':
|
case 'zu':
|
||||||
return number == 1 ? 0 : 1;
|
return (number == 1) ? 0 : 1;
|
||||||
|
|
||||||
case 'am':
|
case 'am':
|
||||||
case 'bh':
|
case 'bh':
|
||||||
@@ -239,7 +237,7 @@ export default class Translator {
|
|||||||
case 'xbr':
|
case 'xbr':
|
||||||
case 'ti':
|
case 'ti':
|
||||||
case 'wa':
|
case 'wa':
|
||||||
return number === 0 || number == 1 ? 0 : 1;
|
return ((number === 0) || (number == 1)) ? 0 : 1;
|
||||||
|
|
||||||
case 'be':
|
case 'be':
|
||||||
case 'bs':
|
case 'bs':
|
||||||
@@ -247,41 +245,41 @@ export default class Translator {
|
|||||||
case 'ru':
|
case 'ru':
|
||||||
case 'sr':
|
case 'sr':
|
||||||
case 'uk':
|
case 'uk':
|
||||||
return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
|
return ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2);
|
||||||
|
|
||||||
case 'cs':
|
case 'cs':
|
||||||
case 'sk':
|
case 'sk':
|
||||||
return number == 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;
|
return (number == 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2);
|
||||||
|
|
||||||
case 'ga':
|
case 'ga':
|
||||||
return number == 1 ? 0 : number == 2 ? 1 : 2;
|
return (number == 1) ? 0 : ((number == 2) ? 1 : 2);
|
||||||
|
|
||||||
case 'lt':
|
case 'lt':
|
||||||
return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
|
return ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2);
|
||||||
|
|
||||||
case 'sl':
|
case 'sl':
|
||||||
return number % 100 == 1 ? 0 : number % 100 == 2 ? 1 : number % 100 == 3 || number % 100 == 4 ? 2 : 3;
|
return (number % 100 == 1) ? 0 : ((number % 100 == 2) ? 1 : (((number % 100 == 3) || (number % 100 == 4)) ? 2 : 3));
|
||||||
|
|
||||||
case 'mk':
|
case 'mk':
|
||||||
return number % 10 == 1 ? 0 : 1;
|
return (number % 10 == 1) ? 0 : 1;
|
||||||
|
|
||||||
case 'mt':
|
case 'mt':
|
||||||
return number == 1 ? 0 : number === 0 || (number % 100 > 1 && number % 100 < 11) ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3;
|
return (number == 1) ? 0 : (((number === 0) || ((number % 100 > 1) && (number % 100 < 11))) ? 1 : (((number % 100 > 10) && (number % 100 < 20)) ? 2 : 3));
|
||||||
|
|
||||||
case 'lv':
|
case 'lv':
|
||||||
return number === 0 ? 0 : number % 10 == 1 && number % 100 != 11 ? 1 : 2;
|
return (number === 0) ? 0 : (((number % 10 == 1) && (number % 100 != 11)) ? 1 : 2);
|
||||||
|
|
||||||
case 'pl':
|
case 'pl':
|
||||||
return number == 1 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2;
|
return (number == 1) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12) || (number % 100 > 14))) ? 1 : 2);
|
||||||
|
|
||||||
case 'cy':
|
case 'cy':
|
||||||
return number == 1 ? 0 : number == 2 ? 1 : number == 8 || number == 11 ? 2 : 3;
|
return (number == 1) ? 0 : ((number == 2) ? 1 : (((number == 8) || (number == 11)) ? 2 : 3));
|
||||||
|
|
||||||
case 'ro':
|
case 'ro':
|
||||||
return number == 1 ? 0 : number === 0 || (number % 100 > 0 && number % 100 < 20) ? 1 : 2;
|
return (number == 1) ? 0 : (((number === 0) || ((number % 100 > 0) && (number % 100 < 20))) ? 1 : 2);
|
||||||
|
|
||||||
case 'ar':
|
case 'ar':
|
||||||
return number === 0 ? 0 : number == 1 ? 1 : number == 2 ? 2 : number >= 3 && number <= 10 ? 3 : number >= 11 && number <= 99 ? 4 : 5;
|
return (number === 0) ? 0 : ((number == 1) ? 1 : ((number == 2) ? 2 : (((number >= 3) && (number <= 10)) ? 3 : (((number >= 11) && (number <= 99)) ? 4 : 5))));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
|
@@ -30,7 +30,6 @@ import Forum from './models/Forum';
|
|||||||
import Component from './Component';
|
import Component from './Component';
|
||||||
import Translator from './Translator';
|
import Translator from './Translator';
|
||||||
import AlertManager from './components/AlertManager';
|
import AlertManager from './components/AlertManager';
|
||||||
import Page from './components/Page';
|
|
||||||
import Switch from './components/Switch';
|
import Switch from './components/Switch';
|
||||||
import Badge from './components/Badge';
|
import Badge from './components/Badge';
|
||||||
import LoadingIndicator from './components/LoadingIndicator';
|
import LoadingIndicator from './components/LoadingIndicator';
|
||||||
@@ -63,9 +62,9 @@ import userOnline from './helpers/userOnline';
|
|||||||
import listItems from './helpers/listItems';
|
import listItems from './helpers/listItems';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extend: extend,
|
'extend': extend,
|
||||||
Session: Session,
|
'Session': Session,
|
||||||
Store: Store,
|
'Store': Store,
|
||||||
'utils/evented': evented,
|
'utils/evented': evented,
|
||||||
'utils/liveHumanTimes': liveHumanTimes,
|
'utils/liveHumanTimes': liveHumanTimes,
|
||||||
'utils/ItemList': ItemList,
|
'utils/ItemList': ItemList,
|
||||||
@@ -92,10 +91,9 @@ export default {
|
|||||||
'models/Discussion': Discussion,
|
'models/Discussion': Discussion,
|
||||||
'models/Group': Group,
|
'models/Group': Group,
|
||||||
'models/Forum': Forum,
|
'models/Forum': Forum,
|
||||||
Component: Component,
|
'Component': Component,
|
||||||
Translator: Translator,
|
'Translator': Translator,
|
||||||
'components/AlertManager': AlertManager,
|
'components/AlertManager': AlertManager,
|
||||||
'components/Page': Page,
|
|
||||||
'components/Switch': Switch,
|
'components/Switch': Switch,
|
||||||
'components/Badge': Badge,
|
'components/Badge': Badge,
|
||||||
'components/LoadingIndicator': LoadingIndicator,
|
'components/LoadingIndicator': LoadingIndicator,
|
||||||
@@ -115,8 +113,8 @@ export default {
|
|||||||
'components/Button': Button,
|
'components/Button': Button,
|
||||||
'components/Modal': Modal,
|
'components/Modal': Modal,
|
||||||
'components/GroupBadge': GroupBadge,
|
'components/GroupBadge': GroupBadge,
|
||||||
Model: Model,
|
'Model': Model,
|
||||||
Application: Application,
|
'Application': Application,
|
||||||
'helpers/fullTime': fullTime,
|
'helpers/fullTime': fullTime,
|
||||||
'helpers/avatar': avatar,
|
'helpers/avatar': avatar,
|
||||||
'helpers/icon': icon,
|
'helpers/icon': icon,
|
||||||
@@ -125,5 +123,5 @@ export default {
|
|||||||
'helpers/highlight': highlight,
|
'helpers/highlight': highlight,
|
||||||
'helpers/username': username,
|
'helpers/username': username,
|
||||||
'helpers/userOnline': userOnline,
|
'helpers/userOnline': userOnline,
|
||||||
'helpers/listItems': listItems,
|
'helpers/listItems': listItems
|
||||||
};
|
};
|
||||||
|
@@ -35,13 +35,22 @@ export default class Alert extends Component {
|
|||||||
const dismissControl = [];
|
const dismissControl = [];
|
||||||
|
|
||||||
if (dismissible || dismissible === undefined) {
|
if (dismissible || dismissible === undefined) {
|
||||||
dismissControl.push(<Button icon="fas fa-times" className="Button Button--link Button--icon Alert-dismiss" onclick={ondismiss} />);
|
dismissControl.push(
|
||||||
|
<Button
|
||||||
|
icon="fas fa-times"
|
||||||
|
className="Button Button--link Button--icon Alert-dismiss"
|
||||||
|
onclick={ondismiss}/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...attrs}>
|
<div {...attrs}>
|
||||||
<span className="Alert-body">{children}</span>
|
<span className="Alert-body">
|
||||||
<ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
|
{children}
|
||||||
|
</span>
|
||||||
|
<ul className="Alert-controls">
|
||||||
|
{listItems(controls.concat(dismissControl))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -7,17 +7,19 @@ import Alert from './Alert';
|
|||||||
*/
|
*/
|
||||||
export default class AlertManager extends Component {
|
export default class AlertManager extends Component {
|
||||||
init() {
|
init() {
|
||||||
this.state = this.props.state;
|
/**
|
||||||
|
* An array of Alert components which are currently showing.
|
||||||
|
*
|
||||||
|
* @type {Alert[]}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
this.components = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="AlertManager">
|
<div className="AlertManager">
|
||||||
{Object.entries(this.state.getActiveAlerts()).map(([key, alert]) => (
|
{this.components.map(component => <div className="AlertManager-alert">{component}</div>)}
|
||||||
<div className="AlertManager-alert">
|
|
||||||
{(alert.componentClass || Alert).component({ ...alert.attrs, ondismiss: this.state.dismiss.bind(this.state, key) })}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -28,4 +30,46 @@ export default class AlertManager extends Component {
|
|||||||
// to be retained across route changes.
|
// to be retained across route changes.
|
||||||
context.retain = true;
|
context.retain = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an Alert in the alerts area.
|
||||||
|
*
|
||||||
|
* @param {Alert} component
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
show(component) {
|
||||||
|
if (!(component instanceof Alert)) {
|
||||||
|
throw new Error('The AlertManager component can only show Alert components');
|
||||||
|
}
|
||||||
|
|
||||||
|
component.props.ondismiss = this.dismiss.bind(this, component);
|
||||||
|
|
||||||
|
this.components.push(component);
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss an alert.
|
||||||
|
*
|
||||||
|
* @param {Alert} component
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
dismiss(component) {
|
||||||
|
const index = this.components.indexOf(component);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
this.components.splice(index, 1);
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all alerts.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.components = [];
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,12 +24,16 @@ export default class Badge extends Component {
|
|||||||
attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
|
attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
|
||||||
attrs.title = extract(attrs, 'label') || '';
|
attrs.title = extract(attrs, 'label') || '';
|
||||||
|
|
||||||
return <span {...attrs}>{iconName ? icon(iconName, { className: 'Badge-icon' }) : m.trust(' ')}</span>;
|
return (
|
||||||
|
<span {...attrs}>
|
||||||
|
{iconName ? icon(iconName, {className: 'Badge-icon'}) : m.trust(' ')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
config(isInitialized) {
|
config(isInitialized) {
|
||||||
if (isInitialized) return;
|
if (isInitialized) return;
|
||||||
|
|
||||||
if (this.props.label) this.$().tooltip();
|
if (this.props.label) this.$().tooltip({container: 'body'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,9 +62,9 @@ export default class Button extends Component {
|
|||||||
const iconName = this.props.icon;
|
const iconName = this.props.icon;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
iconName && iconName !== true ? icon(iconName, { className: 'Button-icon' }) : '',
|
iconName && iconName !== true ? icon(iconName, {className: 'Button-icon'}) : '',
|
||||||
this.props.children ? <span className="Button-label">{this.props.children}</span> : '',
|
this.props.children ? <span className="Button-label">{this.props.children}</span> : '',
|
||||||
this.props.loading ? LoadingIndicator.component({ size: 'tiny', className: 'LoadingIndicator--inline' }) : '',
|
this.props.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : ''
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,23 +10,34 @@ import icon from '../helpers/icon';
|
|||||||
* - `state` Whether or not the checkbox is checked.
|
* - `state` Whether or not the checkbox is checked.
|
||||||
* - `className` The class name for the root element.
|
* - `className` The class name for the root element.
|
||||||
* - `disabled` Whether or not the checkbox is disabled.
|
* - `disabled` Whether or not the checkbox is disabled.
|
||||||
* - `loading` Whether or not the checkbox is loading.
|
|
||||||
* - `onchange` A callback to run when the checkbox is checked/unchecked.
|
* - `onchange` A callback to run when the checkbox is checked/unchecked.
|
||||||
* - `children` A text label to display next to the checkbox.
|
* - `children` A text label to display next to the checkbox.
|
||||||
*/
|
*/
|
||||||
export default class Checkbox extends Component {
|
export default class Checkbox extends Component {
|
||||||
|
init() {
|
||||||
|
/**
|
||||||
|
* Whether or not the checkbox's value is in the process of being saved.
|
||||||
|
*
|
||||||
|
* @type {Boolean}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
// Sometimes, false is stored in the DB as '0'. This is a temporary
|
|
||||||
// conversion layer until a more robust settings encoding is introduced
|
|
||||||
if (this.props.state === '0') this.props.state = false;
|
|
||||||
let className = 'Checkbox ' + (this.props.state ? 'on' : 'off') + ' ' + (this.props.className || '');
|
let className = 'Checkbox ' + (this.props.state ? 'on' : 'off') + ' ' + (this.props.className || '');
|
||||||
if (this.props.loading) className += ' loading';
|
if (this.loading) className += ' loading';
|
||||||
if (this.props.disabled) className += ' disabled';
|
if (this.props.disabled) className += ' disabled';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={className}>
|
<label className={className}>
|
||||||
<input type="checkbox" checked={this.props.state} disabled={this.props.disabled} onchange={m.withAttr('checked', this.onchange.bind(this))} />
|
<input type="checkbox"
|
||||||
<div className="Checkbox-display">{this.getDisplay()}</div>
|
checked={this.props.state}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
onchange={m.withAttr('checked', this.onchange.bind(this))}/>
|
||||||
|
<div className="Checkbox-display">
|
||||||
|
{this.getDisplay()}
|
||||||
|
</div>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
@@ -39,7 +50,9 @@ export default class Checkbox extends Component {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getDisplay() {
|
getDisplay() {
|
||||||
return this.props.loading ? LoadingIndicator.component({ size: 'tiny' }) : icon(this.props.state ? 'fas fa-check' : 'fas fa-times');
|
return this.loading
|
||||||
|
? LoadingIndicator.component({size: 'tiny'})
|
||||||
|
: icon(this.props.state ? 'fas fa-check' : 'fas fa-times');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
import Component from '../Component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `ConfirmDocumentUnload` component can be used to register a global
|
|
||||||
* event handler that prevents closing the browser window/tab based on the
|
|
||||||
* return value of a given callback prop.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `when` - a callback returning true when the browser should prompt for
|
|
||||||
* confirmation before closing the window/tab
|
|
||||||
*
|
|
||||||
* ### Children
|
|
||||||
*
|
|
||||||
* NOTE: Only the first child will be rendered. (Use this component to wrap
|
|
||||||
* another component / DOM element.)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class ConfirmDocumentUnload extends Component {
|
|
||||||
config(isInitialized, context) {
|
|
||||||
if (isInitialized) return;
|
|
||||||
|
|
||||||
const handler = () => this.props.when() || undefined;
|
|
||||||
|
|
||||||
$(window).on('beforeunload', handler);
|
|
||||||
|
|
||||||
context.onunload = () => {
|
|
||||||
$(window).off('beforeunload', handler);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
// To avoid having to render another wrapping <div> here, we assume that
|
|
||||||
// this component is only wrapped around a single element / component.
|
|
||||||
return this.props.children[0];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -64,13 +64,19 @@ export default class Dropdown extends Component {
|
|||||||
|
|
||||||
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
|
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
|
||||||
|
|
||||||
$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
|
$menu.toggleClass(
|
||||||
|
'Dropdown-menu--top',
|
||||||
|
$menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
|
||||||
|
);
|
||||||
|
|
||||||
if ($menu.offset().top < 0) {
|
if ($menu.offset().top < 0) {
|
||||||
$menu.removeClass('Dropdown-menu--top');
|
$menu.removeClass('Dropdown-menu--top');
|
||||||
}
|
}
|
||||||
|
|
||||||
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
|
$menu.toggleClass(
|
||||||
|
'Dropdown-menu--right',
|
||||||
|
isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$().on('hidden.bs.dropdown', () => {
|
this.$().on('hidden.bs.dropdown', () => {
|
||||||
@@ -92,7 +98,10 @@ export default class Dropdown extends Component {
|
|||||||
*/
|
*/
|
||||||
getButton() {
|
getButton() {
|
||||||
return (
|
return (
|
||||||
<button className={'Dropdown-toggle ' + this.props.buttonClassName} data-toggle="dropdown" onclick={this.props.onclick}>
|
<button
|
||||||
|
className={'Dropdown-toggle ' + this.props.buttonClassName}
|
||||||
|
data-toggle="dropdown"
|
||||||
|
onclick={this.props.onclick}>
|
||||||
{this.getButtonContent()}
|
{this.getButtonContent()}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -106,13 +115,17 @@ export default class Dropdown extends Component {
|
|||||||
*/
|
*/
|
||||||
getButtonContent() {
|
getButtonContent() {
|
||||||
return [
|
return [
|
||||||
this.props.icon ? icon(this.props.icon, { className: 'Button-icon' }) : '',
|
this.props.icon ? icon(this.props.icon, {className: 'Button-icon'}) : '',
|
||||||
<span className="Button-label">{this.props.label}</span>,
|
<span className="Button-label">{this.props.label}</span>,
|
||||||
this.props.caretIcon ? icon(this.props.caretIcon, { className: 'Button-caret' }) : '',
|
this.props.caretIcon ? icon(this.props.caretIcon, {className: 'Button-caret'}) : ''
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getMenu(items) {
|
getMenu(items) {
|
||||||
return <ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>{items}</ul>;
|
return (
|
||||||
|
<ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
|
||||||
|
{items}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ export default class GroupBadge extends Badge {
|
|||||||
|
|
||||||
if (props.group) {
|
if (props.group) {
|
||||||
props.icon = props.group.icon();
|
props.icon = props.group.icon();
|
||||||
props.style = { backgroundColor: props.group.color() };
|
props.style = {backgroundColor: props.group.color()};
|
||||||
props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label;
|
props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label;
|
||||||
props.type = 'group--' + props.group.id();
|
props.type = 'group--' + props.group.id();
|
||||||
|
|
||||||
|
@@ -33,6 +33,8 @@ export default class LinkButton extends Button {
|
|||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
static isActive(props) {
|
static isActive(props) {
|
||||||
return typeof props.active !== 'undefined' ? props.active : m.route() === props.href;
|
return typeof props.active !== 'undefined'
|
||||||
|
? props.active
|
||||||
|
: m.route() === props.href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,56 +9,39 @@ import Button from './Button';
|
|||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
export default class Modal extends Component {
|
export default class Modal extends Component {
|
||||||
/**
|
|
||||||
* Determine whether or not the modal should be dismissible via an 'x' button.
|
|
||||||
*/
|
|
||||||
static isDismissible = true;
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
/**
|
/**
|
||||||
* Attributes for an alert component to show below the header.
|
* An alert component to show below the header.
|
||||||
*
|
*
|
||||||
* @type {object}
|
* @type {Alert}
|
||||||
*/
|
*/
|
||||||
this.alertAttrs = null;
|
this.alert = null;
|
||||||
}
|
|
||||||
|
|
||||||
config(isInitialized, context) {
|
|
||||||
if (isInitialized) return;
|
|
||||||
|
|
||||||
this.props.onshow(() => this.onready());
|
|
||||||
|
|
||||||
context.onunload = () => {
|
|
||||||
this.props.onhide();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
if (this.alertAttrs) {
|
if (this.alert) {
|
||||||
this.alertAttrs.dismissible = false;
|
this.alert.props.dismissible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'Modal modal-dialog ' + this.className()}>
|
<div className={'Modal modal-dialog ' + this.className()}>
|
||||||
<div className="Modal-content">
|
<div className="Modal-content">
|
||||||
{this.constructor.isDismissible ? (
|
{this.isDismissible() ? (
|
||||||
<div className="Modal-close App-backControl">
|
<div className="Modal-close App-backControl">
|
||||||
{Button.component({
|
{Button.component({
|
||||||
icon: 'fas fa-times',
|
icon: 'fas fa-times',
|
||||||
onclick: this.hide.bind(this),
|
onclick: this.hide.bind(this),
|
||||||
className: 'Button Button--icon Button--link',
|
className: 'Button Button--icon Button--link'
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : ''}
|
||||||
''
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
<div className="Modal-header">
|
<div className="Modal-header">
|
||||||
<h3 className="App-titleControl App-titleControl--text">{this.title()}</h3>
|
<h3 className="App-titleControl App-titleControl--text">{this.title()}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this.alertAttrs ? <div className="Modal-alert">{Alert.component(this.alertAttrs)}</div> : ''}
|
{alert ? <div className="Modal-alert">{this.alert}</div> : ''}
|
||||||
|
|
||||||
{this.content()}
|
{this.content()}
|
||||||
</form>
|
</form>
|
||||||
@@ -67,13 +50,23 @@ export default class Modal extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether or not the modal should be dismissible via an 'x' button.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
isDismissible() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the class name to apply to the modal.
|
* Get the class name to apply to the modal.
|
||||||
*
|
*
|
||||||
* @return {String}
|
* @return {String}
|
||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
className() {}
|
className() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the title of the modal dialog.
|
* Get the title of the modal dialog.
|
||||||
@@ -81,7 +74,8 @@ export default class Modal extends Component {
|
|||||||
* @return {String}
|
* @return {String}
|
||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
title() {}
|
title() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content of the modal.
|
* Get the content of the modal.
|
||||||
@@ -89,14 +83,16 @@ export default class Modal extends Component {
|
|||||||
* @return {VirtualElement}
|
* @return {VirtualElement}
|
||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
content() {}
|
content() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the modal form's submit event.
|
* Handle the modal form's submit event.
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
*/
|
*/
|
||||||
onsubmit() {}
|
onsubmit() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focus on the first input when the modal is ready to be used.
|
* Focus on the first input when the modal is ready to be used.
|
||||||
@@ -105,13 +101,14 @@ export default class Modal extends Component {
|
|||||||
this.$('form').find('input, select, textarea').first().focus().select();
|
this.$('form').find('input, select, textarea').first().focus().select();
|
||||||
}
|
}
|
||||||
|
|
||||||
onhide() {}
|
onhide() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the modal.
|
* Hide the modal.
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
this.props.onhide();
|
app.modal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,7 +126,7 @@ export default class Modal extends Component {
|
|||||||
* @param {RequestError} error
|
* @param {RequestError} error
|
||||||
*/
|
*/
|
||||||
onerror(error) {
|
onerror(error) {
|
||||||
this.alertAttrs = error.alert;
|
this.alert = error.alert;
|
||||||
|
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import Component from '../Component';
|
import Component from '../Component';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `ModalManager` component manages a modal dialog. Only one modal dialog
|
* The `ModalManager` component manages a modal dialog. Only one modal dialog
|
||||||
@@ -7,15 +8,14 @@ import Component from '../Component';
|
|||||||
*/
|
*/
|
||||||
export default class ModalManager extends Component {
|
export default class ModalManager extends Component {
|
||||||
init() {
|
init() {
|
||||||
this.state = this.props.state;
|
this.showing = false;
|
||||||
|
this.component = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
const modal = this.state.modal;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ModalManager modal fade">
|
<div className="ModalManager modal fade">
|
||||||
{modal ? modal.componentClass.component({ ...modal.attrs, onshow: this.animateShow.bind(this), onhide: this.animateHide.bind(this) }) : ''}
|
{this.component && this.component.render()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -28,25 +28,79 @@ export default class ModalManager extends Component {
|
|||||||
// to be retained across route changes.
|
// to be retained across route changes.
|
||||||
context.retain = true;
|
context.retain = true;
|
||||||
|
|
||||||
// Ensure the modal state is notified about a closed modal, even when the
|
|
||||||
// DOM-based Bootstrap JavaScript code triggered the closing of the modal,
|
|
||||||
// e.g. via ESC key or a click on the modal backdrop.
|
|
||||||
this.$().on('hidden.bs.modal', this.state.close.bind(this.state));
|
|
||||||
}
|
|
||||||
|
|
||||||
animateShow(readyCallback) {
|
|
||||||
const dismissible = !!this.state.modal.componentClass.isDismissible;
|
|
||||||
|
|
||||||
this.$()
|
this.$()
|
||||||
.one('shown.bs.modal', readyCallback)
|
.on('hidden.bs.modal', this.clear.bind(this))
|
||||||
.modal({
|
.on('shown.bs.modal', this.onready.bind(this));
|
||||||
backdrop: dismissible || 'static',
|
|
||||||
keyboard: dismissible,
|
|
||||||
})
|
|
||||||
.modal('show');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
animateHide() {
|
/**
|
||||||
|
* Show a modal dialog.
|
||||||
|
*
|
||||||
|
* @param {Modal} component
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
show(component) {
|
||||||
|
if (!(component instanceof Modal)) {
|
||||||
|
throw new Error('The ModalManager component can only show Modal components');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(this.hideTimeout);
|
||||||
|
|
||||||
|
this.showing = true;
|
||||||
|
this.component = component;
|
||||||
|
|
||||||
|
if (app.current) app.current.retain = true;
|
||||||
|
|
||||||
|
m.redraw(true);
|
||||||
|
|
||||||
|
this.$().modal({backdrop: this.component.isDismissible() ? true : 'static'}).modal('show');
|
||||||
|
this.onready();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal dialog.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
if (!this.showing) return;
|
||||||
|
|
||||||
|
// Don't hide the modal immediately, because if the consumer happens to call
|
||||||
|
// the `show` method straight after to show another modal dialog, it will
|
||||||
|
// cause Bootstrap's modal JS to misbehave. Instead we will wait for a tiny
|
||||||
|
// bit to give the `show` method the opportunity to prevent this from going
|
||||||
|
// ahead.
|
||||||
|
this.hideTimeout = setTimeout(() => {
|
||||||
this.$().modal('hide');
|
this.$().modal('hide');
|
||||||
|
this.showing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear content from the modal area.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
if (this.component) {
|
||||||
|
this.component.onhide();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.component = null;
|
||||||
|
|
||||||
|
app.current.retain = false;
|
||||||
|
|
||||||
|
m.lazyRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the modal dialog is ready to be used, tell it!
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
onready() {
|
||||||
|
if (this.component && this.component.onready) {
|
||||||
|
this.component.onready(this.$());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,15 +19,15 @@ import LinkButton from './LinkButton';
|
|||||||
*/
|
*/
|
||||||
export default class Navigation extends Component {
|
export default class Navigation extends Component {
|
||||||
view() {
|
view() {
|
||||||
const { history, pane } = app;
|
const {history, pane} = app;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={'Navigation ButtonGroup ' + (this.props.className || '')}
|
||||||
className={'Navigation ButtonGroup ' + (this.props.className || '')}
|
|
||||||
onmouseenter={pane && pane.show.bind(pane)}
|
onmouseenter={pane && pane.show.bind(pane)}
|
||||||
onmouseleave={pane && pane.onmouseleave.bind(pane)}
|
onmouseleave={pane && pane.onmouseleave.bind(pane)}>
|
||||||
>
|
{history.canGoBack()
|
||||||
{history.canGoBack() ? [this.getBackButton(), this.getPaneButton()] : this.getDrawerButton()}
|
? [this.getBackButton(), this.getPaneButton()]
|
||||||
|
: this.getDrawerButton()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ export default class Navigation extends Component {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getBackButton() {
|
getBackButton() {
|
||||||
const { history } = app;
|
const {history} = app;
|
||||||
const previous = history.getPrevious() || {};
|
const previous = history.getPrevious() || {};
|
||||||
|
|
||||||
return LinkButton.component({
|
return LinkButton.component({
|
||||||
@@ -55,11 +55,11 @@ export default class Navigation extends Component {
|
|||||||
icon: 'fas fa-chevron-left',
|
icon: 'fas fa-chevron-left',
|
||||||
title: previous.title,
|
title: previous.title,
|
||||||
config: () => {},
|
config: () => {},
|
||||||
onclick: (e) => {
|
onclick: e => {
|
||||||
if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return;
|
if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
history.back();
|
history.back();
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,14 +70,14 @@ export default class Navigation extends Component {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getPaneButton() {
|
getPaneButton() {
|
||||||
const { pane } = app;
|
const {pane} = app;
|
||||||
|
|
||||||
if (!pane || !pane.active) return '';
|
if (!pane || !pane.active) return '';
|
||||||
|
|
||||||
return Button.component({
|
return Button.component({
|
||||||
className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''),
|
className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''),
|
||||||
onclick: pane.togglePinned.bind(pane),
|
onclick: pane.togglePinned.bind(pane),
|
||||||
icon: 'fas fa-thumbtack',
|
icon: 'fas fa-thumbtack'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,16 +90,17 @@ export default class Navigation extends Component {
|
|||||||
getDrawerButton() {
|
getDrawerButton() {
|
||||||
if (!this.props.drawer) return '';
|
if (!this.props.drawer) return '';
|
||||||
|
|
||||||
const { drawer } = app;
|
const {drawer} = app;
|
||||||
const user = app.session.user;
|
const user = app.session.user;
|
||||||
|
|
||||||
return Button.component({
|
return Button.component({
|
||||||
className: 'Button Button--icon Navigation-drawer' + (user && user.newNotificationCount() ? ' new' : ''),
|
className: 'Button Button--icon Navigation-drawer' +
|
||||||
onclick: (e) => {
|
(user && user.newNotificationCount() ? ' new' : ''),
|
||||||
|
onclick: e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
drawer.show();
|
drawer.show();
|
||||||
},
|
},
|
||||||
icon: 'fas fa-bars',
|
icon: 'fas fa-bars'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,37 +6,25 @@ export default class RequestErrorModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return this.props.error.xhr ? `${this.props.error.xhr.status} ${this.props.error.xhr.statusText}` : '';
|
return this.props.error.xhr
|
||||||
|
? this.props.error.xhr.status+' '+this.props.error.xhr.statusText
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
const { error, formattedError } = this.props;
|
|
||||||
|
|
||||||
let responseText;
|
let responseText;
|
||||||
|
|
||||||
// If the error is already formatted, just add line endings;
|
|
||||||
// else try to parse it as JSON and stringify it with indentation
|
|
||||||
if (formattedError) {
|
|
||||||
responseText = formattedError.join('\n\n');
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
const json = error.response || JSON.parse(error.responseText);
|
responseText = JSON.stringify(JSON.parse(this.props.error.responseText), null, 2);
|
||||||
|
|
||||||
responseText = JSON.stringify(json, null, 2);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
responseText = error.responseText;
|
responseText = this.props.error.responseText;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className="Modal-body">
|
||||||
<div className="Modal-body">
|
|
||||||
<pre>
|
<pre>
|
||||||
{this.props.error.options.method} {this.props.error.options.url}
|
{this.props.error.options.method} {this.props.error.options.url}<br/><br/>
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
{responseText}
|
{responseText}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,25 +8,17 @@ import icon from '../helpers/icon';
|
|||||||
* - `options` A map of option values to labels.
|
* - `options` A map of option values to labels.
|
||||||
* - `onchange` A callback to run when the selected value is changed.
|
* - `onchange` A callback to run when the selected value is changed.
|
||||||
* - `value` The value of the selected option.
|
* - `value` The value of the selected option.
|
||||||
* - `disabled` Disabled state for the input.
|
|
||||||
*/
|
*/
|
||||||
export default class Select extends Component {
|
export default class Select extends Component {
|
||||||
view() {
|
view() {
|
||||||
const { options, onchange, value, disabled } = this.props;
|
const {options, onchange, value} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="Select">
|
<span className="Select">
|
||||||
<select
|
<select className="Select-input FormControl" onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined} value={value}>
|
||||||
className="Select-input FormControl"
|
{Object.keys(options).map(key => <option value={key}>{options[key]}</option>)}
|
||||||
onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined}
|
|
||||||
value={value}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{Object.keys(options).map((key) => (
|
|
||||||
<option value={key}>{options[key]}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
{icon('fas fa-sort', { className: 'Select-caret' })}
|
{icon('fas fa-sort', {className: 'Select-caret'})}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -21,11 +21,14 @@ export default class SelectDropdown extends Dropdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getButtonContent() {
|
getButtonContent() {
|
||||||
const activeChild = this.props.children.filter((child) => child.props.active)[0];
|
const activeChild = this.props.children.filter(child => child.props.active)[0];
|
||||||
let label = (activeChild && activeChild.props.children) || this.props.defaultLabel;
|
let label = activeChild && activeChild.props.children || this.props.defaultLabel;
|
||||||
|
|
||||||
if (label instanceof Array) label = label[0];
|
if (label instanceof Array) label = label[0];
|
||||||
|
|
||||||
return [<span className="Button-label">{label}</span>, icon(this.props.caretIcon, { className: 'Button-caret' })];
|
return [
|
||||||
|
<span className="Button-label">{label}</span>,
|
||||||
|
icon(this.props.caretIcon, {className: 'Button-caret'})
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ import Component from '../Component';
|
|||||||
*/
|
*/
|
||||||
class Separator extends Component {
|
class Separator extends Component {
|
||||||
view() {
|
view() {
|
||||||
return <li className="Dropdown-separator" />;
|
return <li className="Dropdown-separator"/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -24,10 +24,12 @@ export default class SplitDropdown extends Dropdown {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
Button.component(buttonProps),
|
Button.component(buttonProps),
|
||||||
<button className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName} data-toggle="dropdown">
|
<button
|
||||||
{icon(this.props.icon, { className: 'Button-icon' })}
|
className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName}
|
||||||
{icon('fas fa-caret-down', { className: 'Button-caret' })}
|
data-toggle="dropdown">
|
||||||
</button>,
|
{icon(this.props.icon, {className: 'Button-icon'})}
|
||||||
|
{icon('fas fa-caret-down', {className: 'Button-caret'})}
|
||||||
|
</button>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,6 +12,6 @@ export default class Switch extends Checkbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDisplay() {
|
getDisplay() {
|
||||||
return this.props.loading ? super.getDisplay() : '';
|
return this.loading ? super.getDisplay() : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
export function extend(object, method, callback) {
|
export function extend(object, method, callback) {
|
||||||
const original = object[method];
|
const original = object[method];
|
||||||
|
|
||||||
object[method] = function (...args) {
|
object[method] = function(...args) {
|
||||||
const value = original ? original.apply(this, args) : undefined;
|
const value = original ? original.apply(this, args) : undefined;
|
||||||
|
|
||||||
callback.apply(this, [value].concat(args));
|
callback.apply(this, [value].concat(args));
|
||||||
@@ -57,7 +57,7 @@ export function extend(object, method, callback) {
|
|||||||
export function override(object, method, newMethod) {
|
export function override(object, method, newMethod) {
|
||||||
const original = object[method];
|
const original = object[method];
|
||||||
|
|
||||||
object[method] = function (...args) {
|
object[method] = function(...args) {
|
||||||
return newMethod.apply(this, [original.bind(this)].concat(args));
|
return newMethod.apply(this, [original.bind(this)].concat(args));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
export default class Model {
|
export default class Routes {
|
||||||
type;
|
type;
|
||||||
attributes = [];
|
attributes = [];
|
||||||
hasOnes = [];
|
hasOnes = [];
|
||||||
@@ -34,8 +34,8 @@ export default class Model {
|
|||||||
|
|
||||||
const model = app.store.models[this.type];
|
const model = app.store.models[this.type];
|
||||||
|
|
||||||
this.attributes.forEach((name) => (model.prototype[name] = model.attribute(name)));
|
this.attributes.forEach(name => model.prototype[name] = model.attribute(name));
|
||||||
this.hasOnes.forEach((name) => (model.prototype[name] = model.hasOne(name)));
|
this.hasOnes.forEach(name => model.prototype[name] = model.hasOne(name));
|
||||||
this.hasManys.forEach((name) => (model.prototype[name] = model.hasMany(name)));
|
this.hasManys.forEach(name => model.prototype[name] = model.hasMany(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -25,11 +25,11 @@ export default function avatar(user, attrs = {}) {
|
|||||||
if (hasTitle) attrs.title = attrs.title || username;
|
if (hasTitle) attrs.title = attrs.title || username;
|
||||||
|
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
return <img {...attrs} src={avatarUrl} />;
|
return <img {...attrs} src={avatarUrl}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
content = username.charAt(0).toUpperCase();
|
content = username.charAt(0).toUpperCase();
|
||||||
attrs.style = { background: user.color() };
|
attrs.style = {background: user.color()};
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span {...attrs}>{content}</span>;
|
return <span {...attrs}>{content}</span>;
|
||||||
|
@@ -6,14 +6,10 @@
|
|||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export default function fullTime(time) {
|
export default function fullTime(time) {
|
||||||
const d = dayjs(time);
|
const mo = moment(time);
|
||||||
|
|
||||||
const datetime = d.format();
|
const datetime = mo.format();
|
||||||
const full = d.format('LLLL');
|
const full = mo.format('LLLL');
|
||||||
|
|
||||||
return (
|
return <time pubdate datetime={datetime}>{full}</time>;
|
||||||
<time pubdate datetime={datetime}>
|
|
||||||
{full}
|
|
||||||
</time>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -9,15 +9,11 @@ import humanTimeUtil from '../utils/humanTime';
|
|||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export default function humanTime(time) {
|
export default function humanTime(time) {
|
||||||
const d = dayjs(time);
|
const mo = moment(time);
|
||||||
|
|
||||||
const datetime = d.format();
|
const datetime = mo.format();
|
||||||
const full = d.format('LLLL');
|
const full = mo.format('LLLL');
|
||||||
const ago = humanTimeUtil(time);
|
const ago = humanTimeUtil(time);
|
||||||
|
|
||||||
return (
|
return <time pubdate datetime={datetime} title={full} data-humantime>{ago}</time>;
|
||||||
<time pubdate datetime={datetime} title={full} data-humantime>
|
|
||||||
{ago}
|
|
||||||
</time>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
export default function icon(fontClass, attrs = {}) {
|
export default function icon(fontClass, attrs = {}) {
|
||||||
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
|
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
|
||||||
|
|
||||||
return <i {...attrs} />;
|
return <i {...attrs}/>;
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@ function withoutUnnecessarySeparators(items) {
|
|||||||
export default function listItems(items) {
|
export default function listItems(items) {
|
||||||
if (!(items instanceof Array)) items = [items];
|
if (!(items instanceof Array)) items = [items];
|
||||||
|
|
||||||
return withoutUnnecessarySeparators(items).map((item) => {
|
return withoutUnnecessarySeparators(items).map(item => {
|
||||||
const isListItem = item.component && item.component.isListItem;
|
const isListItem = item.component && item.component.isListItem;
|
||||||
const active = item.component && item.component.isActive && item.component.isActive(item.props);
|
const active = item.component && item.component.isActive && item.component.isActive(item.props);
|
||||||
const className = item.props ? item.props.itemClassName : item.itemClassName;
|
const className = item.props ? item.props.itemClassName : item.itemClassName;
|
||||||
@@ -39,12 +39,15 @@ export default function listItems(items) {
|
|||||||
item.attrs.key = item.attrs.key || item.itemName;
|
item.attrs.key = item.attrs.key || item.itemName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isListItem ? (
|
return isListItem
|
||||||
item
|
? item
|
||||||
) : (
|
: <li className={classList([
|
||||||
<li className={classList([item.itemName ? 'item-' + item.itemName : '', className, active ? 'active' : ''])} key={item.itemName}>
|
(item.itemName ? 'item-' + item.itemName : ''),
|
||||||
|
className,
|
||||||
|
(active ? 'active' : '')
|
||||||
|
])}
|
||||||
|
key={item.itemName}>
|
||||||
{item}
|
{item}
|
||||||
</li>
|
</li>;
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ export default function punctuateSeries(items) {
|
|||||||
if (items.length === 2) {
|
if (items.length === 2) {
|
||||||
return app.translator.trans('core.lib.series.two_text', {
|
return app.translator.trans('core.lib.series.two_text', {
|
||||||
first: items[0],
|
first: items[0],
|
||||||
second: items[1],
|
second: items[1]
|
||||||
});
|
});
|
||||||
} else if (items.length >= 3) {
|
} else if (items.length >= 3) {
|
||||||
// If there are three or more items, we will join all but the first and
|
// If there are three or more items, we will join all but the first and
|
||||||
@@ -27,7 +27,7 @@ export default function punctuateSeries(items) {
|
|||||||
return app.translator.trans('core.lib.series.three_text', {
|
return app.translator.trans('core.lib.series.three_text', {
|
||||||
first: items[0],
|
first: items[0],
|
||||||
second,
|
second,
|
||||||
third: items[items.length - 1],
|
third: items[items.length - 1]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import 'expose-loader?$!expose-loader?jQuery!jquery';
|
import 'expose-loader?$!expose-loader?jQuery!jquery';
|
||||||
import 'expose-loader?m!mithril';
|
import 'expose-loader?m!mithril';
|
||||||
import 'expose-loader?moment!expose-loader?dayjs!dayjs';
|
import 'expose-loader?moment!moment';
|
||||||
import 'expose-loader?m.bidi!m.attrs.bidi';
|
import 'expose-loader?m.bidi!m.attrs.bidi';
|
||||||
import 'bootstrap/js/affix';
|
import 'bootstrap/js/affix';
|
||||||
import 'bootstrap/js/dropdown';
|
import 'bootstrap/js/dropdown';
|
||||||
@@ -9,12 +9,6 @@ import 'bootstrap/js/tooltip';
|
|||||||
import 'bootstrap/js/transition';
|
import 'bootstrap/js/transition';
|
||||||
import 'jquery.hotkeys/jquery.hotkeys';
|
import 'jquery.hotkeys/jquery.hotkeys';
|
||||||
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
dayjs.extend(localizedFormat);
|
|
||||||
|
|
||||||
import patchMithril from './utils/patchMithril';
|
import patchMithril from './utils/patchMithril';
|
||||||
|
|
||||||
patchMithril(window);
|
patchMithril(window);
|
||||||
|
@@ -19,18 +19,18 @@ Object.assign(Discussion.prototype, {
|
|||||||
lastPostNumber: Model.attribute('lastPostNumber'),
|
lastPostNumber: Model.attribute('lastPostNumber'),
|
||||||
|
|
||||||
commentCount: Model.attribute('commentCount'),
|
commentCount: Model.attribute('commentCount'),
|
||||||
replyCount: computed('commentCount', (commentCount) => Math.max(0, commentCount - 1)),
|
replyCount: computed('commentCount', commentCount => Math.max(0, commentCount - 1)),
|
||||||
posts: Model.hasMany('posts'),
|
posts: Model.hasMany('posts'),
|
||||||
mostRelevantPost: Model.hasOne('mostRelevantPost'),
|
mostRelevantPost: Model.hasOne('mostRelevantPost'),
|
||||||
|
|
||||||
lastReadAt: Model.attribute('lastReadAt', Model.transformDate),
|
lastReadAt: Model.attribute('lastReadAt', Model.transformDate),
|
||||||
lastReadPostNumber: Model.attribute('lastReadPostNumber'),
|
lastReadPostNumber: Model.attribute('lastReadPostNumber'),
|
||||||
isUnread: computed('unreadCount', (unreadCount) => !!unreadCount),
|
isUnread: computed('unreadCount', unreadCount => !!unreadCount),
|
||||||
isRead: computed('unreadCount', (unreadCount) => app.session.user && !unreadCount),
|
isRead: computed('unreadCount', unreadCount => app.session.user && !unreadCount),
|
||||||
|
|
||||||
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
|
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
|
||||||
hiddenUser: Model.hasOne('hiddenUser'),
|
hiddenUser: Model.hasOne('hiddenUser'),
|
||||||
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
|
isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt),
|
||||||
|
|
||||||
canReply: Model.attribute('canReply'),
|
canReply: Model.attribute('canReply'),
|
||||||
canRename: Model.attribute('canRename'),
|
canRename: Model.attribute('canRename'),
|
||||||
@@ -68,10 +68,7 @@ Object.assign(Discussion.prototype, {
|
|||||||
const user = app.session.user;
|
const user = app.session.user;
|
||||||
|
|
||||||
if (user && user.markedAllAsReadAt() < this.lastPostedAt()) {
|
if (user && user.markedAllAsReadAt() < this.lastPostedAt()) {
|
||||||
const unreadCount = Math.max(0, this.lastPostNumber() - (this.lastReadPostNumber() || 0));
|
return Math.max(0, this.lastPostNumber() - (this.lastReadPostNumber() || 0));
|
||||||
// If posts have been deleted, it's possible that the unread count could exceed the
|
|
||||||
// actual post count. As such, we take the min of the two to ensure this isn't an issue.
|
|
||||||
return Math.min(unreadCount, this.commentCount());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -87,7 +84,7 @@ Object.assign(Discussion.prototype, {
|
|||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
if (this.isHidden()) {
|
if (this.isHidden()) {
|
||||||
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')} />);
|
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')}/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -102,6 +99,6 @@ Object.assign(Discussion.prototype, {
|
|||||||
postIds() {
|
postIds() {
|
||||||
const posts = this.data.relationships.posts;
|
const posts = this.data.relationships.posts;
|
||||||
|
|
||||||
return posts ? posts.data.map((link) => link.id) : [];
|
return posts ? posts.data.map(link => link.id) : [];
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
@@ -6,8 +6,7 @@ Object.assign(Group.prototype, {
|
|||||||
nameSingular: Model.attribute('nameSingular'),
|
nameSingular: Model.attribute('nameSingular'),
|
||||||
namePlural: Model.attribute('namePlural'),
|
namePlural: Model.attribute('namePlural'),
|
||||||
color: Model.attribute('color'),
|
color: Model.attribute('color'),
|
||||||
icon: Model.attribute('icon'),
|
icon: Model.attribute('icon')
|
||||||
isHidden: Model.attribute('isHidden'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Group.ADMINISTRATOR_ID = '1';
|
Group.ADMINISTRATOR_ID = '1';
|
||||||
|
@@ -11,5 +11,5 @@ Object.assign(Notification.prototype, {
|
|||||||
|
|
||||||
user: Model.hasOne('user'),
|
user: Model.hasOne('user'),
|
||||||
fromUser: Model.hasOne('fromUser'),
|
fromUser: Model.hasOne('fromUser'),
|
||||||
subject: Model.hasOne('subject'),
|
subject: Model.hasOne('subject')
|
||||||
});
|
});
|
||||||
|
@@ -17,13 +17,13 @@ Object.assign(Post.prototype, {
|
|||||||
|
|
||||||
editedAt: Model.attribute('editedAt', Model.transformDate),
|
editedAt: Model.attribute('editedAt', Model.transformDate),
|
||||||
editedUser: Model.hasOne('editedUser'),
|
editedUser: Model.hasOne('editedUser'),
|
||||||
isEdited: computed('editedAt', (editedAt) => !!editedAt),
|
isEdited: computed('editedAt', editedAt => !!editedAt),
|
||||||
|
|
||||||
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
|
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
|
||||||
hiddenUser: Model.hasOne('hiddenUser'),
|
hiddenUser: Model.hasOne('hiddenUser'),
|
||||||
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
|
isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt),
|
||||||
|
|
||||||
canEdit: Model.attribute('canEdit'),
|
canEdit: Model.attribute('canEdit'),
|
||||||
canHide: Model.attribute('canHide'),
|
canHide: Model.attribute('canHide'),
|
||||||
canDelete: Model.attribute('canDelete'),
|
canDelete: Model.attribute('canDelete')
|
||||||
});
|
});
|
||||||
|
@@ -32,7 +32,7 @@ Object.assign(User.prototype, {
|
|||||||
canDelete: Model.attribute('canDelete'),
|
canDelete: Model.attribute('canDelete'),
|
||||||
|
|
||||||
avatarColor: null,
|
avatarColor: null,
|
||||||
color: computed('username', 'avatarUrl', 'avatarColor', function (username, avatarUrl, avatarColor) {
|
color: computed('username', 'avatarUrl', 'avatarColor', function(username, avatarUrl, avatarColor) {
|
||||||
// If we've already calculated and cached the dominant color of the user's
|
// If we've already calculated and cached the dominant color of the user's
|
||||||
// avatar, then we can return that in RGB format. If we haven't, we'll want
|
// avatar, then we can return that in RGB format. If we haven't, we'll want
|
||||||
// to calculate it. Unless the user doesn't have an avatar, in which case
|
// to calculate it. Unless the user doesn't have an avatar, in which case
|
||||||
@@ -54,7 +54,7 @@ Object.assign(User.prototype, {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
isOnline() {
|
isOnline() {
|
||||||
return dayjs().subtract(5, 'minutes').isBefore(this.lastSeenAt());
|
return this.lastSeenAt() > moment().subtract(5, 'minutes').toDate();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,8 +67,8 @@ Object.assign(User.prototype, {
|
|||||||
const groups = this.groups();
|
const groups = this.groups();
|
||||||
|
|
||||||
if (groups) {
|
if (groups) {
|
||||||
groups.forEach((group) => {
|
groups.forEach(group => {
|
||||||
items.add('group' + group.id(), GroupBadge.component({ group }));
|
items.add('group' + group.id(), GroupBadge.component({group}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ Object.assign(User.prototype, {
|
|||||||
const image = new Image();
|
const image = new Image();
|
||||||
const user = this;
|
const user = this;
|
||||||
|
|
||||||
image.onload = function () {
|
image.onload = function() {
|
||||||
const colorThief = new ColorThief();
|
const colorThief = new ColorThief();
|
||||||
user.avatarColor = colorThief.getColor(this);
|
user.avatarColor = colorThief.getColor(this);
|
||||||
user.freshness = new Date();
|
user.freshness = new Date();
|
||||||
@@ -106,6 +106,6 @@ Object.assign(User.prototype, {
|
|||||||
|
|
||||||
Object.assign(preferences, newPreferences);
|
Object.assign(preferences, newPreferences);
|
||||||
|
|
||||||
return this.save({ preferences });
|
return this.save({preferences});
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,50 +0,0 @@
|
|||||||
import Alert from '../components/Alert';
|
|
||||||
|
|
||||||
export default class AlertManagerState {
|
|
||||||
constructor() {
|
|
||||||
this.activeAlerts = {};
|
|
||||||
this.alertId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveAlerts() {
|
|
||||||
return this.activeAlerts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an Alert in the alerts area.
|
|
||||||
*/
|
|
||||||
show(attrs, componentClass = Alert) {
|
|
||||||
// Breaking Change Compliance Warning, Remove in Beta 15.
|
|
||||||
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
|
|
||||||
if (attrs === Alert || attrs instanceof Alert) {
|
|
||||||
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
|
||||||
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
|
||||||
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
|
||||||
}
|
|
||||||
// End Change Compliance Warning, Remove in Beta 15
|
|
||||||
this.activeAlerts[++this.alertId] = { attrs, componentClass };
|
|
||||||
m.redraw();
|
|
||||||
|
|
||||||
return this.alertId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dismiss an alert.
|
|
||||||
*/
|
|
||||||
dismiss(key) {
|
|
||||||
if (!key || !(key in this.activeAlerts)) return;
|
|
||||||
|
|
||||||
delete this.activeAlerts[key];
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all alerts.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
clear() {
|
|
||||||
this.activeAlerts = {};
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,56 +0,0 @@
|
|||||||
import Modal from '../components/Modal';
|
|
||||||
|
|
||||||
export default class ModalManagerState {
|
|
||||||
constructor() {
|
|
||||||
this.modal = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a modal dialog.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
show(componentClass, attrs) {
|
|
||||||
// Breaking Change Compliance Warning, Remove in Beta 15.
|
|
||||||
if (!(componentClass.prototype instanceof Modal)) {
|
|
||||||
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
|
||||||
console.error('The ModalManager can only show Modals');
|
|
||||||
throw new Error('The ModalManager can only show Modals');
|
|
||||||
}
|
|
||||||
if (componentClass.init) {
|
|
||||||
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
|
||||||
console.error(
|
|
||||||
'The componentClass parameter must be a modal class, not a modal instance. Whichever extension triggered this modal should be updated to comply with beta 14.'
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
'The componentClass parameter must be a modal class, not a modal instance. Whichever extension triggered this modal should be updated to comply with beta 14.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// End Change Compliance Warning, Remove in Beta 15
|
|
||||||
|
|
||||||
clearTimeout(this.closeTimeout);
|
|
||||||
|
|
||||||
this.modal = { componentClass, attrs };
|
|
||||||
|
|
||||||
m.redraw(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the modal dialog.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
close() {
|
|
||||||
if (!this.modal) return;
|
|
||||||
|
|
||||||
// Don't hide the modal immediately, because if the consumer happens to call
|
|
||||||
// the `show` method straight after to show another modal dialog, it will
|
|
||||||
// cause Bootstrap's modal JS to misbehave. Instead we will wait for a tiny
|
|
||||||
// bit to give the `show` method the opportunity to prevent this from going
|
|
||||||
// ahead.
|
|
||||||
this.closeTimeout = setTimeout(() => {
|
|
||||||
this.modal = null;
|
|
||||||
m.lazyRedraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,33 +0,0 @@
|
|||||||
import subclassOf from '../../common/utils/subclassOf';
|
|
||||||
|
|
||||||
export default class PageState {
|
|
||||||
constructor(type, data = {}) {
|
|
||||||
this.type = type;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine whether the page matches the given class and data.
|
|
||||||
*
|
|
||||||
* @param {object} type The page class to check against. Subclasses are
|
|
||||||
* accepted as well.
|
|
||||||
* @param {object} data
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
matches(type, data = {}) {
|
|
||||||
// Fail early when the page is of a different type
|
|
||||||
if (!subclassOf(this.type, type)) return false;
|
|
||||||
|
|
||||||
// Now that the type is known to be correct, we loop through the provided
|
|
||||||
// data to see whether it matches the data in our state.
|
|
||||||
return Object.keys(data).every((key) => this.data[key] === data[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key) {
|
|
||||||
return this.data[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key, value) {
|
|
||||||
this.data[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,7 +7,7 @@ export default class Drawer {
|
|||||||
constructor() {
|
constructor() {
|
||||||
// Set up an event handler so that whenever the content area is tapped,
|
// Set up an event handler so that whenever the content area is tapped,
|
||||||
// the drawer will close.
|
// the drawer will close.
|
||||||
$('#content').click((e) => {
|
$('#content').click(e => {
|
||||||
if (this.isOpen()) {
|
if (this.isOpen()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.hide();
|
this.hide();
|
||||||
|
@@ -1,9 +1,5 @@
|
|||||||
class Item {
|
class Item {
|
||||||
content: any;
|
constructor(content, priority) {
|
||||||
priority: number;
|
|
||||||
key?: number;
|
|
||||||
|
|
||||||
constructor(content: any, priority?: number) {
|
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.priority = priority;
|
this.priority = priority;
|
||||||
}
|
}
|
||||||
@@ -14,17 +10,25 @@ class Item {
|
|||||||
* by priority.
|
* by priority.
|
||||||
*/
|
*/
|
||||||
export default class ItemList {
|
export default class ItemList {
|
||||||
|
constructor() {
|
||||||
/**
|
/**
|
||||||
* The items in the list
|
* The items in the list.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
items: { [key: string]: Item } = {};
|
this.items = {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the list is empty.
|
* Check whether the list is empty.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
isEmpty(): boolean {
|
isEmpty() {
|
||||||
for (const i in this.items) {
|
for (const i in this.items) {
|
||||||
if (this.items.hasOwnProperty(i)) {
|
if(this.items.hasOwnProperty(i)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,27 +38,36 @@ export default class ItemList {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether an item is present in the list.
|
* Check whether an item is present in the list.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
has(key: string): boolean {
|
has(key) {
|
||||||
return !!this.items[key];
|
return !!this.items[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content of an item.
|
* Get the content of an item.
|
||||||
|
*
|
||||||
|
* @param {String} key
|
||||||
|
* @return {*}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
get(key: string): any {
|
get(key) {
|
||||||
return this.items[key].content;
|
return this.items[key].content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an item to the list.
|
* Add an item to the list.
|
||||||
*
|
*
|
||||||
* @param key A unique key for the item.
|
* @param {String} key A unique key for the item.
|
||||||
* @param content The item's content.
|
* @param {*} content The item's content.
|
||||||
* @param [priority] The priority of the item. Items with a higher
|
* @param {Integer} [priority] The priority of the item. Items with a higher
|
||||||
* priority will be positioned before items with a lower priority.
|
* priority will be positioned before items with a lower priority.
|
||||||
|
* @return {ItemList}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
add(key: string, content: any, priority: number = 0): this {
|
add(key, content, priority = 0) {
|
||||||
this.items[key] = new Item(content, priority);
|
this.items[key] = new Item(content, priority);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@@ -62,8 +75,14 @@ export default class ItemList {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace an item in the list, only if it is already present.
|
* Replace an item in the list, only if it is already present.
|
||||||
|
*
|
||||||
|
* @param {String} key
|
||||||
|
* @param {*} [content]
|
||||||
|
* @param {Integer} [priority]
|
||||||
|
* @return {ItemList}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
replace(key: string, content: any = null, priority: number = null): this {
|
replace(key, content = null, priority = null) {
|
||||||
if (this.items[key]) {
|
if (this.items[key]) {
|
||||||
if (content !== null) {
|
if (content !== null) {
|
||||||
this.items[key].content = content;
|
this.items[key].content = content;
|
||||||
@@ -79,8 +98,12 @@ export default class ItemList {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an item from the list.
|
* Remove an item from the list.
|
||||||
|
*
|
||||||
|
* @param {String} key
|
||||||
|
* @return {ItemList}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
remove(key: string): this {
|
remove(key) {
|
||||||
delete this.items[key];
|
delete this.items[key];
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@@ -88,8 +111,12 @@ export default class ItemList {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge another list's items into this one.
|
* Merge another list's items into this one.
|
||||||
|
*
|
||||||
|
* @param {ItemList} items
|
||||||
|
* @return {ItemList}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
merge(items: this): this {
|
merge(items) {
|
||||||
for (const i in items.items) {
|
for (const i in items.items) {
|
||||||
if (items.items.hasOwnProperty(i) && items.items[i] instanceof Item) {
|
if (items.items.hasOwnProperty(i) && items.items[i] instanceof Item) {
|
||||||
this.items[i] = items.items[i];
|
this.items[i] = items.items[i];
|
||||||
@@ -103,9 +130,12 @@ export default class ItemList {
|
|||||||
* Convert the list into an array of item content arranged by priority. Each
|
* Convert the list into an array of item content arranged by priority. Each
|
||||||
* item's content will be assigned an `itemName` property equal to the item's
|
* item's content will be assigned an `itemName` property equal to the item's
|
||||||
* unique key.
|
* unique key.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
toArray(): any[] {
|
toArray() {
|
||||||
const items: Item[] = [];
|
const items = [];
|
||||||
|
|
||||||
for (const i in this.items) {
|
for (const i in this.items) {
|
||||||
if (this.items.hasOwnProperty(i) && this.items[i] instanceof Item) {
|
if (this.items.hasOwnProperty(i) && this.items[i] instanceof Item) {
|
||||||
@@ -117,15 +147,14 @@ export default class ItemList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items.sort((a, b) => {
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.priority === b.priority) {
|
if (a.priority === b.priority) {
|
||||||
return a.key - b.key;
|
return a.key - b.key;
|
||||||
} else if (a.priority > b.priority) {
|
} else if (a.priority > b.priority) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
})
|
}).map(item => item.content);
|
||||||
.map((item) => item.content);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@@ -1,14 +1,5 @@
|
|||||||
export default class RequestError {
|
export default class RequestError {
|
||||||
status: string;
|
constructor(status, responseText, options, xhr) {
|
||||||
options: object;
|
|
||||||
xhr: XMLHttpRequest;
|
|
||||||
|
|
||||||
responseText: string | null;
|
|
||||||
response: object | null;
|
|
||||||
|
|
||||||
alert: any;
|
|
||||||
|
|
||||||
constructor(status: string, responseText: string | null, options: object, xhr: XMLHttpRequest) {
|
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.responseText = responseText;
|
this.responseText = responseText;
|
||||||
this.options = options;
|
this.options = options;
|
@@ -1,10 +1,9 @@
|
|||||||
const later =
|
const later = window.requestAnimationFrame ||
|
||||||
window.requestAnimationFrame ||
|
|
||||||
window.webkitRequestAnimationFrame ||
|
window.webkitRequestAnimationFrame ||
|
||||||
window.mozRequestAnimationFrame ||
|
window.mozRequestAnimationFrame ||
|
||||||
window.msRequestAnimationFrame ||
|
window.msRequestAnimationFrame ||
|
||||||
window.oRequestAnimationFrame ||
|
window.oRequestAnimationFrame ||
|
||||||
((callback) => window.setTimeout(callback, 1000 / 60));
|
(callback => window.setTimeout(callback, 1000 / 60));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `ScrollListener` class sets up a listener that handles window scroll
|
* The `ScrollListener` class sets up a listener that handles window scroll
|
||||||
@@ -58,7 +57,10 @@ export default class ScrollListener {
|
|||||||
*/
|
*/
|
||||||
start() {
|
start() {
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
|
window.addEventListener(
|
||||||
|
'scroll',
|
||||||
|
this.active = this.loop.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -44,7 +44,7 @@ export default class SubtreeRetainer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return needsRebuild ? false : { subtree: 'retain' };
|
return needsRebuild ? false : {subtree: 'retain'};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,109 +0,0 @@
|
|||||||
/**
|
|
||||||
* A textarea wrapper with powerful helpers for text manipulation.
|
|
||||||
*
|
|
||||||
* This wraps a <textarea> DOM element and allows directly manipulating its text
|
|
||||||
* contents and cursor positions.
|
|
||||||
*
|
|
||||||
* I apologize for the pretentious name. :)
|
|
||||||
*/
|
|
||||||
export default class SuperTextarea {
|
|
||||||
/**
|
|
||||||
* @param {HTMLTextAreaElement} textarea
|
|
||||||
*/
|
|
||||||
constructor(textarea) {
|
|
||||||
this.el = textarea;
|
|
||||||
this.$ = $(textarea);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the value of the text editor.
|
|
||||||
*
|
|
||||||
* @param {String} value
|
|
||||||
*/
|
|
||||||
setValue(value) {
|
|
||||||
this.$.val(value).trigger('input');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focus the textarea and place the cursor at the given index.
|
|
||||||
*
|
|
||||||
* @param {number} position
|
|
||||||
*/
|
|
||||||
moveCursorTo(position) {
|
|
||||||
this.setSelectionRange(position, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the selected range of the textarea.
|
|
||||||
*
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
getSelectionRange() {
|
|
||||||
return [this.el.selectionStart, this.el.selectionEnd];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the textarea at the position of the cursor.
|
|
||||||
*
|
|
||||||
* @param {String} text
|
|
||||||
*/
|
|
||||||
insertAtCursor(text) {
|
|
||||||
this.insertAt(this.el.selectionStart, text);
|
|
||||||
|
|
||||||
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the textarea at the given position.
|
|
||||||
*
|
|
||||||
* @param {number} pos
|
|
||||||
* @param {String} text
|
|
||||||
*/
|
|
||||||
insertAt(pos, text) {
|
|
||||||
this.insertBetween(pos, pos, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the textarea between the given positions.
|
|
||||||
*
|
|
||||||
* If the start and end positions are different, any text between them will be
|
|
||||||
* overwritten.
|
|
||||||
*
|
|
||||||
* @param start
|
|
||||||
* @param end
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
insertBetween(start, end, text) {
|
|
||||||
const value = this.el.value;
|
|
||||||
|
|
||||||
const before = value.slice(0, start);
|
|
||||||
const after = value.slice(end);
|
|
||||||
|
|
||||||
this.setValue(`${before}${text}${after}`);
|
|
||||||
|
|
||||||
// Move the textarea cursor to the end of the content we just inserted.
|
|
||||||
this.moveCursorTo(start + text.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace existing content from the start to the current cursor position.
|
|
||||||
*
|
|
||||||
* @param start
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
replaceBeforeCursor(start, text) {
|
|
||||||
this.insertBetween(start, this.el.selectionStart, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the selected range of the textarea.
|
|
||||||
*
|
|
||||||
* @param {number} start
|
|
||||||
* @param {number} end
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
setSelectionRange(start, end) {
|
|
||||||
this.el.setSelectionRange(start, end);
|
|
||||||
this.$.focus();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,8 +4,11 @@
|
|||||||
* @example
|
* @example
|
||||||
* abbreviateNumber(1234);
|
* abbreviateNumber(1234);
|
||||||
* // "1.2K"
|
* // "1.2K"
|
||||||
|
*
|
||||||
|
* @param {Integer} number
|
||||||
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
export default function abbreviateNumber(number: number): string {
|
export default function abbreviateNumber(number) {
|
||||||
// TODO: translation
|
// TODO: translation
|
||||||
if (number >= 1000000) {
|
if (number >= 1000000) {
|
||||||
return Math.floor(number / 1000000) + app.translator.trans('core.lib.number_suffix.mega_text');
|
return Math.floor(number / 1000000) + app.translator.trans('core.lib.number_suffix.mega_text');
|
@@ -13,7 +13,7 @@ export default function classList(classes) {
|
|||||||
let classNames;
|
let classNames;
|
||||||
|
|
||||||
if (classes instanceof Array) {
|
if (classes instanceof Array) {
|
||||||
classNames = classes.filter((name) => name);
|
classNames = classes.filter(name => name);
|
||||||
} else {
|
} else {
|
||||||
classNames = [];
|
classNames = [];
|
||||||
|
|
||||||
|
@@ -14,12 +14,12 @@ export default function computed(...dependentKeys) {
|
|||||||
const dependentValues = {};
|
const dependentValues = {};
|
||||||
let computedValue;
|
let computedValue;
|
||||||
|
|
||||||
return function () {
|
return function() {
|
||||||
let recompute = false;
|
let recompute = false;
|
||||||
|
|
||||||
// Read all of the dependent values. If any of them have changed since last
|
// Read all of the dependent values. If any of them have changed since last
|
||||||
// time, then we'll want to recompute our output.
|
// time, then we'll want to recompute our output.
|
||||||
keys.forEach((key) => {
|
keys.forEach(key => {
|
||||||
const value = typeof this[key] === 'function' ? this[key]() : this[key];
|
const value = typeof this[key] === 'function' ? this[key]() : this[key];
|
||||||
|
|
||||||
if (dependentValues[key] !== value) {
|
if (dependentValues[key] !== value) {
|
||||||
@@ -29,10 +29,7 @@ export default function computed(...dependentKeys) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (recompute) {
|
if (recompute) {
|
||||||
computedValue = compute.apply(
|
computedValue = compute.apply(this, keys.map(key => dependentValues[key]));
|
||||||
this,
|
|
||||||
keys.map((key) => dependentValues[key])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return computedValue;
|
return computedValue;
|
||||||
|
@@ -34,7 +34,7 @@ export default {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
trigger(event, ...args) {
|
trigger(event, ...args) {
|
||||||
this.getHandlers(event).forEach((handler) => handler.apply(this, args));
|
this.getHandlers(event).forEach(handler => handler.apply(this, args));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +55,7 @@ export default {
|
|||||||
* @param {function} handler The function to handle the event.
|
* @param {function} handler The function to handle the event.
|
||||||
*/
|
*/
|
||||||
one(event, handler) {
|
one(event, handler) {
|
||||||
const wrapper = function () {
|
const wrapper = function() {
|
||||||
handler.apply(this, arguments);
|
handler.apply(this, arguments);
|
||||||
|
|
||||||
this.off(event, wrapper);
|
this.off(event, wrapper);
|
||||||
@@ -77,5 +77,5 @@ export default {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
handlers.splice(index, 1);
|
handlers.splice(index, 1);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
15
js/src/common/utils/extract.js
Normal file
15
js/src/common/utils/extract.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* The `extract` utility deletes a property from an object and returns its
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* @param {Object} object The object that owns the property
|
||||||
|
* @param {String} property The name of the property to extract
|
||||||
|
* @return {*} The value of the property
|
||||||
|
*/
|
||||||
|
export default function extract(object, property) {
|
||||||
|
const value = object[property];
|
||||||
|
|
||||||
|
delete object[property];
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* The `extract` utility deletes a property from an object and returns its
|
|
||||||
* value.
|
|
||||||
*
|
|
||||||
* @param object The object that owns the property
|
|
||||||
* @param property The name of the property to extract
|
|
||||||
* @return The value of the property
|
|
||||||
*/
|
|
||||||
export default function extract<T, K extends keyof T>(object: T, property: K): T[K] {
|
|
||||||
const value = object[property];
|
|
||||||
|
|
||||||
delete object[property];
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
export default function extractText(vdom) {
|
export default function extractText(vdom) {
|
||||||
if (vdom instanceof Array) {
|
if (vdom instanceof Array) {
|
||||||
return vdom.map((element) => extractText(element)).join('');
|
return vdom.map(element => extractText(element)).join('');
|
||||||
} else if (typeof vdom === 'object' && vdom !== null) {
|
} else if (typeof vdom === 'object' && vdom !== null) {
|
||||||
return extractText(vdom.children);
|
return extractText(vdom.children);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -5,7 +5,10 @@
|
|||||||
* @example
|
* @example
|
||||||
* formatNumber(1234);
|
* formatNumber(1234);
|
||||||
* // 1,234
|
* // 1,234
|
||||||
|
*
|
||||||
|
* @param {Number} number
|
||||||
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
export default function formatNumber(number: number): string {
|
export default function formatNumber(number) {
|
||||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
}
|
}
|
@@ -1,33 +1,36 @@
|
|||||||
/**
|
/**
|
||||||
* The `humanTime` utility converts a date to a localized, human-readable time-
|
* The `humanTime` utility converts a date to a localized, human-readable time-
|
||||||
* ago string.
|
* ago string.
|
||||||
|
*
|
||||||
|
* @param {Date} time
|
||||||
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
export default function humanTime(time: Date): string {
|
export default function humanTime(time) {
|
||||||
let d = dayjs(time);
|
let m = moment(time);
|
||||||
const now = dayjs();
|
const now = moment();
|
||||||
|
|
||||||
// To prevent showing things like "in a few seconds" due to small offsets
|
// To prevent showing things like "in a few seconds" due to small offsets
|
||||||
// between client and server time, we always reset future dates to the
|
// between client and server time, we always reset future dates to the
|
||||||
// current time. This will result in "just now" being shown instead.
|
// current time. This will result in "just now" being shown instead.
|
||||||
if (d.isAfter(now)) {
|
if (m.isAfter(now)) {
|
||||||
d = now;
|
m = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
const day = 864e5;
|
const day = 864e5;
|
||||||
const diff = d.diff(dayjs());
|
const diff = m.diff(moment());
|
||||||
let ago: string;
|
let ago = null;
|
||||||
|
|
||||||
// If this date was more than a month ago, we'll show the name of the month
|
// If this date was more than a month ago, we'll show the name of the month
|
||||||
// in the string. If it wasn't this year, we'll show the year as well.
|
// in the string. If it wasn't this year, we'll show the year as well.
|
||||||
if (diff < -30 * day) {
|
if (diff < -30 * day) {
|
||||||
if (d.year() === dayjs().year()) {
|
if (m.year() === moment().year()) {
|
||||||
ago = d.format('D MMM');
|
ago = m.format('D MMM');
|
||||||
} else {
|
} else {
|
||||||
ago = d.format('ll');
|
ago = m.format('MMM \'YY');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ago = d.fromNow();
|
ago = m.fromNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ago;
|
return ago;
|
||||||
}
|
};
|
18
js/src/common/utils/liveHumanTimes.js
Normal file
18
js/src/common/utils/liveHumanTimes.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import humanTimeUtil from './humanTime';
|
||||||
|
|
||||||
|
function updateHumanTimes() {
|
||||||
|
$('[data-humantime]').each(function() {
|
||||||
|
const $this = $(this);
|
||||||
|
const ago = humanTimeUtil($this.attr('datetime'));
|
||||||
|
|
||||||
|
$this.html(ago);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `humanTime` initializer sets up a loop every 1 second to update
|
||||||
|
* timestamps rendered with the `humanTime` helper.
|
||||||
|
*/
|
||||||
|
export default function humanTime() {
|
||||||
|
setInterval(updateHumanTimes, 10000);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user