mirror of
https://github.com/flarum/core.git
synced 2025-08-17 22:01:44 +02:00
Compare commits
2 Commits
v0.1.0-bet
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
34079108c8 | ||
|
f7a8b76fa8 |
BIN
.deploy.enc
Normal file
BIN
.deploy.enc
Normal file
Binary file not shown.
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -11,5 +11,3 @@ phpunit.xml export-ignore
|
|||||||
tests export-ignore
|
tests export-ignore
|
||||||
|
|
||||||
js/dist/* -diff
|
js/dist/* -diff
|
||||||
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
|||||||
github: flarum
|
|
||||||
open_collective: flarum
|
|
||||||
tidelift: packagist/flarum/core
|
|
3
.github/ISSUE_TEMPLATE/bug-report.md
vendored
3
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -3,6 +3,9 @@ name: "🐛 Bug Report"
|
|||||||
about: "If something isn't working as expected"
|
about: "If something isn't working as expected"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<!--
|
||||||
|
IMPORTANT: If you discover a security vulnerability within Flarum, please send an email to [security@flarum.org](mailto:security@flarum.org) instead. We will address these with the utmost urgency and it will prevent vulnerabilities, which may be abused, from popping up on our issue tracker.
|
||||||
|
-->
|
||||||
## Bug Report
|
## Bug Report
|
||||||
|
|
||||||
**Current Behavior**
|
**Current Behavior**
|
||||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -16,7 +16,7 @@ IMPORTANT: We applaud pull requests, they excite us every single time. As we hav
|
|||||||
**Confirmed**
|
**Confirmed**
|
||||||
|
|
||||||
- [ ] Frontend changes: tested on a local Flarum installation.
|
- [ ] Frontend changes: tested on a local Flarum installation.
|
||||||
- [ ] Backend changes: tests are green (run `composer test`).
|
- [ ] Backend changes: tests are green (run `php vendor/bin/phpunit`).
|
||||||
|
|
||||||
**Required changes:**
|
**Required changes:**
|
||||||
|
|
||||||
|
13
.github/SECURITY.md
vendored
13
.github/SECURITY.md
vendored
@@ -1,13 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
During the beta phase, we will only patch security vulnerabilities in the latest beta release.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If you discover a security vulnerability within Flarum, please send an email to security@flarum.org so we can address it promptly.
|
|
||||||
|
|
||||||
We will get back to you as time allows.
|
|
||||||
Discussions may commence internally, so you may not hear back immediately.
|
|
||||||
When reporting a vulnerability, please provide your GitHub username (if available), so that we can invite you to collaborate on a [security advisory on GitHub](https://help.github.com/en/articles/about-maintainer-security-advisories).
|
|
26
.github/stale.yml
vendored
26
.github/stale.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
daysUntilStale: 90
|
|
||||||
daysUntilClose: 30
|
|
||||||
|
|
||||||
staleLabel: stale
|
|
||||||
|
|
||||||
exemptLabels:
|
|
||||||
- org/keep
|
|
||||||
- type/bug
|
|
||||||
- type/regression
|
|
||||||
- critical
|
|
||||||
- security
|
|
||||||
exemptAssignees: true
|
|
||||||
exemptMilestones: true
|
|
||||||
exemptProjects: true
|
|
||||||
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had
|
|
||||||
recent activity. It will be closed if no further activity occurs. We do this
|
|
||||||
to keep the amount of open issues to a manageable minimum.
|
|
||||||
|
|
||||||
In any case, thanks for taking an interest in this software and contributing
|
|
||||||
by opening the issue in the first place!
|
|
||||||
|
|
||||||
closeComment: >
|
|
||||||
We are closing this issue as it seems to have grown stale. If you still
|
|
||||||
encounter this problem with the latest version, feel free to re-open it.
|
|
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -1,16 +0,0 @@
|
|||||||
name: JavaScript
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
- uses: flarum/action-build@master
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
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
|
|
67
.github/workflows/test.yml
vendored
67
.github/workflows/test.yml
vendored
@@ -1,67 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
php: [7.2, 7.3, 7.4]
|
|
||||||
service: ['mysql:5.7', mariadb]
|
|
||||||
prefix: ['', flarum_]
|
|
||||||
|
|
||||||
include:
|
|
||||||
- service: 'mysql:5.7'
|
|
||||||
db: MySQL
|
|
||||||
- service: mariadb
|
|
||||||
db: MariaDB
|
|
||||||
- prefix: flarum_
|
|
||||||
prefixStr: (prefix)
|
|
||||||
|
|
||||||
exclude:
|
|
||||||
- php: 7.2
|
|
||||||
service: 'mysql:5.7'
|
|
||||||
prefix: flarum_
|
|
||||||
- php: 7.2
|
|
||||||
service: mariadb
|
|
||||||
prefix: flarum_
|
|
||||||
- php: 7.3
|
|
||||||
service: 'mysql:5.7'
|
|
||||||
prefix: flarum_
|
|
||||||
- php: 7.3
|
|
||||||
service: mariadb
|
|
||||||
prefix: flarum_
|
|
||||||
|
|
||||||
services:
|
|
||||||
mysql:
|
|
||||||
image: ${{ matrix.service }}
|
|
||||||
ports:
|
|
||||||
- 13306:3306
|
|
||||||
|
|
||||||
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Select PHP version
|
|
||||||
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
|
|
||||||
|
|
||||||
- name: Create MySQL Database
|
|
||||||
run: |
|
|
||||||
sudo systemctl start mysql
|
|
||||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
|
||||||
|
|
||||||
- name: Install Composer dependencies
|
|
||||||
run: composer install
|
|
||||||
|
|
||||||
- name: Setup Composer tests
|
|
||||||
run: composer test:setup
|
|
||||||
env:
|
|
||||||
DB_PORT: 13306
|
|
||||||
DB_PASSWORD: root
|
|
||||||
DB_PREFIX: ${{ matrix.prefix }}
|
|
||||||
|
|
||||||
- name: Run Composer tests
|
|
||||||
run: composer test
|
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,6 @@ composer.phar
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
/tests/integration/tmp
|
/tests/tmp
|
||||||
.vagrant
|
.vagrant
|
||||||
.idea/*
|
.idea/*
|
||||||
|
46
.travis.yml
Normal file
46
.travis.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
language: php
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.composer/cache
|
||||||
|
- $HOME/.npm
|
||||||
|
|
||||||
|
install:
|
||||||
|
- composer install
|
||||||
|
- mysql -e 'CREATE DATABASE flarum;'
|
||||||
|
|
||||||
|
script:
|
||||||
|
- vendor/bin/phpunit --coverage-clover=coverage.xml
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
include:
|
||||||
|
- php: 7.1
|
||||||
|
env: DB=mysql
|
||||||
|
|
||||||
|
- php: 7.2
|
||||||
|
env: DB=mysql
|
||||||
|
|
||||||
|
- php: 7.2
|
||||||
|
env: DB=mysql PREFIX=forum_
|
||||||
|
|
||||||
|
- php: 7.1
|
||||||
|
addons:
|
||||||
|
mariadb: '10.2'
|
||||||
|
env: DB=mariadb
|
||||||
|
|
||||||
|
- php: 7.2
|
||||||
|
addons:
|
||||||
|
mariadb: '10.2'
|
||||||
|
env: DB=mariadb
|
||||||
|
|
||||||
|
- stage: build
|
||||||
|
language: generic
|
||||||
|
if: branch = master AND type = push
|
||||||
|
install: skip
|
||||||
|
script: bash .travis/build.sh
|
||||||
|
-k $encrypted_678139e2bc67_key
|
||||||
|
-i $encrypted_678139e2bc67_iv
|
||||||
|
after_success: skip
|
33
.travis/build.sh
Executable file
33
.travis/build.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
main() {
|
||||||
|
while getopts ":k:i:" opt; do
|
||||||
|
case $opt in
|
||||||
|
k) encrypted_key="$OPTARG"
|
||||||
|
;;
|
||||||
|
i) encrypted_iv="$OPTARG"
|
||||||
|
;;
|
||||||
|
\?) echo "Invalid option -$OPTARG" >&2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
git checkout -f $TRAVIS_BRANCH
|
||||||
|
git config user.name "flarum-bot"
|
||||||
|
git config user.email "bot@flarum.org"
|
||||||
|
|
||||||
|
cd js
|
||||||
|
npm i -g npm@6.1.0
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
git add dist/* -f
|
||||||
|
git commit -m "Bundled output for commit $TRAVIS_COMMIT [skip ci]"
|
||||||
|
|
||||||
|
eval `ssh-agent -s`
|
||||||
|
openssl aes-256-cbc -K $encrypted_key -iv $encrypted_iv -in ../.deploy.enc -d | ssh-add -
|
||||||
|
|
||||||
|
git push git@github.com:$TRAVIS_REPO_SLUG.git $TRAVIS_BRANCH
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
292
CHANGELOG.md
292
CHANGELOG.md
@@ -1,302 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [0.1.0-beta.14.1](https://github.com/flarum/core/compare/v0.1.0-beta.14...v0.1.0-beta.14.1)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- SuperTextarea component is not exported.
|
|
||||||
- Symfony dependencies do not match those depended on by Laravel (#2407)
|
|
||||||
- Scripts from textformatter aren't executed (#2415)
|
|
||||||
- Sub path installations have no page title.
|
|
||||||
- Losing focus of Composer area when coming from fullscreen.
|
|
||||||
|
|
||||||
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Check dependencies before enabling / disabling extensions (https://github.com/flarum/core/pull/2188)
|
|
||||||
- Set up temporary infrastructure for TypeScript in core (https://github.com/flarum/core/pull/2206)
|
|
||||||
- Better UI for request error modals (https://github.com/flarum/core/pull/1929)
|
|
||||||
- Display name extender, tests, frontend UI (https://github.com/flarum/core/pull/2174)
|
|
||||||
- Scroll to post or show alert when editing a post from another page (https://github.com/flarum/core/pull/2108)
|
|
||||||
- Feature to test email config by sending an email to the current user (https://github.com/flarum/core/pull/2023)
|
|
||||||
- Allow searching users by group ID using the group gambit (https://github.com/flarum/core/pull/2192)
|
|
||||||
- Use `liveHumanTimes` helper to update times without reload/rerender (https://github.com/flarum/core/pull/2208)
|
|
||||||
- View extender, tests (https://github.com/flarum/core/pull/2134)
|
|
||||||
- User extender to replace `PrepareUserGroups` (https://github.com/flarum/core/pull/2110)
|
|
||||||
- Increase extensibility of skeleton PHP (https://github.com/flarum/core/pull/2308, https://github.com/flarum/core/pull/2318)
|
|
||||||
- Pass a translator instance to `getEmailSubject` in `MailableInterface` (https://github.com/flarum/core/pull/2244)
|
|
||||||
- Force LF line endings on windows (https://github.com/flarum/core/pull/2321)
|
|
||||||
- Add a `Link` component for internal and external links (https://github.com/flarum/core/pull/2315)
|
|
||||||
- `ConfirmDocumentUnload` component
|
|
||||||
- Error handler middleware can now be manipulated by the middleware extender
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Update to Mithril 2 (https://github.com/flarum/core/pull/2255)
|
|
||||||
- Stop storing component instances (https://github.com/flarum/core/issues/1821, https://github.com/flarum/core/issues/2144)
|
|
||||||
- Update to Laravel 6.x (https://github.com/flarum/core/issues/2055)
|
|
||||||
- `Flarum\Foundation\Application` no longer implements `Illuminate\Contracts\Foundation\Application` (#2142)
|
|
||||||
- `Flarum\Foundation\Application` no longer inherits `Illuminate\Container\Container` (#2142)
|
|
||||||
- `paths` have been split off from `Flarum\Foundation\Application` into `Flarum\Foundation\Paths`, which can be injected where needed (#2142)
|
|
||||||
- `Flarum\User\Gate` no longer implements `Illuminate\Contracts\Auth\Access\Gate` (https://github.com/flarum/core/pull/2181)
|
|
||||||
- Improve Group Gambit performance (https://github.com/flarum/core/pull/2192)
|
|
||||||
- Switch to `dayjs` from `momentjs` (https://github.com/flarum/core/pull/2219)
|
|
||||||
- Don't create a `bio` column in `users` for new installations (https://github.com/flarum/core/pull/2215)
|
|
||||||
- Start converting core JS to TypeScript (https://github.com/flarum/core/pull/2207)
|
|
||||||
- Make Carbon an explicit dependency (https://github.com/flarum/core/commit/3b39c212e0fef7522e7d541a9214ff3817138d5d)
|
|
||||||
- Use Symfony's translator interface instead of Laravel's (https://github.com/flarum/core/pull/2243)
|
|
||||||
- Use newer versions of fontawesome (https://github.com/flarum/core/pull/2274)
|
|
||||||
- Use URL generator instead of `app()->url()` where possible (https://github.com/flarum/core/pull/2302)
|
|
||||||
- Move config from `config.php` into an injectable helper class (https://github.com/flarum/core/pull/2271)
|
|
||||||
- Use reserved TLD for bogus and test urls (https://github.com/flarum/core/commit/6860b24b70bd04544dde90e537ce021a5fc5a689)
|
|
||||||
- Replace `m.stream` with `flarum/utils/Stream` (https://github.com/flarum/core/pull/2316)
|
|
||||||
- Replace `affixedSidebar` util with `AffixedSidebar` component
|
|
||||||
- Replace `m.withAttr` with `flarum/utils/withAttr`
|
|
||||||
- Scroll Listener is now passive, performance improvement (https://github.com/flarum/core/pull/2387)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `generate:migration` command for extensions (https://github.com/flarum/core/commit/443949f7b9d7558dbc1e0994cb898cbac59bec87)
|
|
||||||
- Container config for `UninstalledSite` (https://github.com/flarum/core/commit/ecdce44d555dd36a365fd472b2916e677ef173cf)
|
|
||||||
- Tooltip glitch on page chang (https://github.com/flarum/core/issues/2118)
|
|
||||||
- Using multiple extenders in tests (https://github.com/flarum/core/commit/c4f4f218bf4b175a30880b807f9ccb1a37a25330)
|
|
||||||
- Header glitch when opening modals (https://github.com/flarum/core/pull/2131)
|
|
||||||
- Ensure `SameSite` is explicitly set for cookies (https://github.com/flarum/core/pull/2159)
|
|
||||||
- Ensure `Flarum\User\Event\AvatarChanged` event is properly dispatched (https://github.com/flarum/core/pull/2197)
|
|
||||||
- Show correct error message on wrong password when changing email (https://github.com/flarum/core/pull/2171)
|
|
||||||
- Discussion unreadCount could be higher than commentCount if posts deleted (https://github.com/flarum/core/pull/2195)
|
|
||||||
- Don't show page title on the default route (https://github.com/flarum/core/pull/2047)
|
|
||||||
- Add page title to `All Discussions` page when it isn't the default route (https://github.com/flarum/core/pull/2047)
|
|
||||||
- Accept `'0'` as `false` for `flarum/components/Checkbox` (https://github.com/flarum/core/pull/2210)
|
|
||||||
- Fix PostStreamScrubber background (https://github.com/flarum/core/pull/2222)
|
|
||||||
- Test port on BaseUrl tests (https://github.com/flarum/core/pull/2226)
|
|
||||||
- `UrlGenerator` can now generate urls with optional parameters (https://github.com/flarum/core/pull/2246)
|
|
||||||
- Allow `less` to be compiled independently of Flarum (https://github.com/flarum/core/pull/2252)
|
|
||||||
- Use correct number abbreviation (https://github.com/flarum/core/pull/2261)
|
|
||||||
- Ensure avatar html uses alt tags for accessibility (https://github.com/flarum/core/pull/2269)
|
|
||||||
- Escape regex when searching (https://github.com/flarum/core/pull/2273)
|
|
||||||
- Remove unneeded semicolons inserted during JS compilation (https://github.com/flarum/core/pull/2280)
|
|
||||||
- Don't require a username/password for SMTP (https://github.com/flarum/core/pull/2287)
|
|
||||||
- Allow uppercase entries for SMTP encryption validation (https://github.com/flarum/core/pull/2289)
|
|
||||||
- Ensure that the right number of posts is returned from list posts API (https://github.com/flarum/core/pull/2291)
|
|
||||||
- Fix a variety of PostStream bugs (https://github.com/flarum/core/pull/2160, https://github.com/flarum/core/pull/2160)
|
|
||||||
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2324)
|
|
||||||
- Sliding discussion button in wrong place (https://github.com/flarum/core/pull/2330, https://github.com/flarum/core/pull/2383)
|
|
||||||
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2381)
|
|
||||||
- Fix PostStream for posts with top margins, and scrubber position when scrolling below posts (https://github.com/flarum/core/pull/2369)
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- `Flarum\Event\AbstractConfigureRoutes` event class
|
|
||||||
- `Flarum\Event\ConfigureApiRoutes` event class
|
|
||||||
- `Flarum\Event\ConfigureForumRoutes` event class
|
|
||||||
- `Flarum\Console\Event\Configuring` event class
|
|
||||||
- `Flarum\Event\ConfigureModelDates` event class
|
|
||||||
- `Flarum\Event\ConfigureLocales` event class
|
|
||||||
- `Flarum\Event\ConfigureModelDefaultAttributes` event class
|
|
||||||
- `Flarum\Event\GetModelRelationship` event class
|
|
||||||
- `Flarum\User\Event\BioChanged` event class
|
|
||||||
- `Flarum\Database\MigrationServiceProvider` moved into `Flarum\Database\DatabaseServiceProvider`
|
|
||||||
- Unused `admin/components/Widget` component (`admin/component/DashboardWidget` should be used instead)
|
|
||||||
- Mandrill mail driver (https://github.com/flarum/core/commit/bca833d3f1c34d45d95bf905902368a2753b8908)
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
|
|
||||||
- `Flarum\User\Event\GetDisplayName` event class
|
|
||||||
- Global path helpers, `Flarum\Foundation\Application` path methods (https://github.com/flarum/core/pull/2155)
|
|
||||||
- `Flarum\User\AssertPermissionTrait` (https://github.com/flarum/core/pull/2044)
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- 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)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Comments have an additional class `Post--by-actor` when posted by the user (#1927)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Improved support for URL identification during installation (#1861)
|
|
||||||
- KeyboardNavigatable now has a callback ability (#1922)
|
|
||||||
- Links are no longer opened with target `_blank` but in the same window (#859)
|
|
||||||
- Links now have `nofollow ugc` by default as their `rel` attribute (#859, #1884)
|
|
||||||
- Improved performance of the full text gambit when searching for users (#1877)
|
|
||||||
- The Queue implementation is now available under its Illuminate contract
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- No error handling was possible in the console/cli (#1789)
|
|
||||||
- Enable scrollbars in log in modals so it fits for GitHub (#1716)
|
|
||||||
- Reduce log in modal for SSO so it fits for Facebook (#1727)
|
|
||||||
- Deleting discussions permanently did not delete its posts (#1909)
|
|
||||||
- Fixed the queue:restart command (#1932)
|
|
||||||
- Deleted posts were visible to all visitors (#1827)
|
|
||||||
- Old avatars weren't being deleted when replaced (#1918)
|
|
||||||
- The search performance regression was reverted (#1764)
|
|
||||||
- No profile background could be set for remote images (#445)
|
|
||||||
- Back button sends to home even though it could actually go back (#1942)
|
|
||||||
- Debug button no longer visible (#1687)
|
|
||||||
- Modals on smaller screens use the whole width of the page
|
|
||||||
|
|
||||||
## [0.1.0-beta.10](https://github.com/flarum/core/compare/v0.1.0-beta.9...v0.1.0-beta.10)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Initial queue support: Infrastructure for offloading long-running tasks (e.g. email sending) to background workers (#1773)
|
|
||||||
- Notifications can now be marked as read without visiting a discussion (#151)
|
|
||||||
- SEO: The discussion list now has a `rel="canonical"` meta tag, preventing duplicate content (#1134, #1814)
|
|
||||||
- The "Edit User" permission can now be edited in the UI (#1845)
|
|
||||||
- New status message and redirect after user deletion (#1750, #1777)
|
|
||||||
- Errors in Flarum's boot process are now presented with more detailed information (#1607)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Better, more detailed and extensible error handling (#1641, #1843)
|
|
||||||
- Error pages in debug mode now return the same HTTP status codes as in production (#1648)
|
|
||||||
- Tweak HTTP status codes for authentication / authorization errors (#1854)
|
|
||||||
- Already-used links from account activation emails now show a better error message (#1337)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Security vulnerabilities in dependencies
|
|
||||||
- Performance: High CPU usage when scrolling in a discussion (#1222)
|
|
||||||
- Special characters crashed the search (#1498)
|
|
||||||
- Missing declarations for language and text direction in HTML output (#1772)
|
|
||||||
- Private messages were counted in user post counts (#1695)
|
|
||||||
- Extensions could not change the forum's default page (#1819)
|
|
||||||
- API requests authenticated using access tokens needed to provide a CSRF token (#1828)
|
|
||||||
- Accessibility: Screenreaders did not read the "Back to discussion list" link (#1835)
|
|
||||||
|
|
||||||
## [0.1.0-beta.9](https://github.com/flarum/core/compare/v0.1.0-beta.8.2...v0.1.0-beta.9)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- New `hasPermission()` helper method for `Group` objects ([9684fbc](https://github.com/flarum/core/commit/9684fbc4da07d32aa322d9228302a23418412cb9))
|
|
||||||
- Expose supported mail drivers in IoC container ([208bad3](https://github.com/flarum/core/commit/208bad393f37bfdb76007afcddfa4b7451563e9d))
|
|
||||||
- More test for some API endpoints ([1670590](https://github.com/flarum/core/commit/167059027e5a066d618599c90164ef1b5a509148))
|
|
||||||
- The `Formatter\Rendering` event now receives the HTTP request instance as well ([0ab9fac](https://github.com/flarum/core/commit/0ab9facc4bd59a260575e6fc650793c663e5866a))
|
|
||||||
- More and better validation in installer UIs
|
|
||||||
- Check and enforce minimum MariaDB ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
|
|
||||||
- Revert publication of assets when installation fails ([ed9591c](https://github.com/flarum/core/commit/ed9591c16fb2ea7a4be3387b805d855a53e0a7d5))
|
|
||||||
- Benefit from Laravel's database reconnection logic in long-running tasks ([e0becd0](https://github.com/flarum/core/commit/e0becd0c7bda939048923c1f86648793feee78d5))
|
|
||||||
- The "vendor path" (where Composer dependencies can be found) can now be configured ([5e1680c](https://github.com/flarum/core/commit/5e1680c458cd3ba274faeb92de3ac2053789131e))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Performance: Actually cache translations on disk ([0d16fac](https://github.com/flarum/core/commit/0d16fac001bb735ee66e82871183516aeac269b7))
|
|
||||||
- Allow per-site extenders to override extension extenders ([ba594de](https://github.com/flarum/core/commit/ba594de13a033480834d53d73f747b05fe9796f8))
|
|
||||||
- Do not resolve objects from the IoC container (in service providers and extenders) until they are actually used
|
|
||||||
- Replace event subscribers (that resolve objects from the IoC container) with listeners (that resolve lazily)
|
|
||||||
- Use custom service provider for Mail component ([ac5e26a](https://github.com/flarum/core/commit/ac5e26a254d89e21bd4c115b6cbd40338e2e4b4b))
|
|
||||||
- Update to Laravel 5.7, revert custom logic for building database index names
|
|
||||||
- Refactored installer, extracted Installation class and pipeline for reuse in CLI and web installers ([790d5be](https://github.com/flarum/core/commit/790d5beee5e283178716bc8f9901c758d9e5b6a0))
|
|
||||||
- Use whitelist for enabling pre-installed extensions during installation ([4585f03](https://github.com/flarum/core/commit/4585f03ee356c92942fbc2ae8c683c651b473954))
|
|
||||||
- Update minimum MySQL version ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Signing up via OAuth providers was broken ([67f9375](https://github.com/flarum/core/commit/67f9375d4745add194ae3249d526197c32fd5461))
|
|
||||||
- Group badges were overlapping ([16eb1fa](https://github.com/flarum/core/commit/16eb1fa63b6d7b80ec30c24c0e406a2b7ab09934))
|
|
||||||
- API: Endpoint for uninstalling extensions returned an error ([c761802](https://github.com/flarum/core/commit/c76180290056ddbab67baf5ede814fcedf1dcf14))
|
|
||||||
- Documentation links in installer were outdated ([b58380e](https://github.com/flarum/core/commit/b58380e224ee54abdade3d0a4cc107ef5c91c9a9))
|
|
||||||
- Event posts where counted when aggregating user posts ([671fdec](https://github.com/flarum/core/commit/671fdec8d0a092ccceb5d4d5f657d0f4287fc4c7))
|
|
||||||
- Admins could not reset user passwords ([c67fb2d](https://github.com/flarum/core/commit/c67fb2d4b6a128c71d65dc6703310c0b62f91be2))
|
|
||||||
- Several down migrations were invalid
|
|
||||||
- Validation errors on reset password page resulted in HTTP 404 ([4611abe](https://github.com/flarum/core/commit/4611abe5db8b94ca3dc7bf9c447fca7c67358ee3))
|
|
||||||
- `is:unread` gambit generated an invalid query ([e17bb0b](https://github.com/flarum/core/commit/e17bb0b4331f2c92459292195c6b7db8cde1f9f3))
|
|
||||||
- Entire forum was breaking when the `custom_less` setting was missing from the database ([bf2c5a5](https://github.com/flarum/core/commit/bf2c5a5564dff3f5ef13efe7a8d69f2617570ce6))
|
|
||||||
- Dropdown icon was not showing in user card when on user page ([12fdfc9](https://github.com/flarum/core/commit/12fdfc9b544a27f6fe59c82ad6bddd3420cc0181))
|
|
||||||
- Requests were missing the `original*` attributes, which broke installations in subfolders ([56fde28](https://github.com/flarum/core/commit/56fde28e436f52fee0c03c538f0a6049bc584b53))
|
|
||||||
- Special characters such as `%` and `_` could return incorrect results ([ee3640e](https://github.com/flarum/core/commit/ee3640e1605ff67fef4b3d5cd0596f14a6ae73c9))
|
|
||||||
- FontAwesome component package changed paths in version 5.9.0 ([5eb69e1](https://github.com/flarum/core/commit/5eb69e1f59fa73fdfd5badbf41a05a6a040e7426))
|
|
||||||
- Some server environments had problems accessing the system-wide tmp path for storing JS file maps ([54660eb](https://github.com/flarum/core/commit/54660ebd6311f9ea142f1b573263d0d907400786))
|
|
||||||
- Content length of posts.content was not migrated to mediumText in 2017 ([590b311](https://github.com/flarum/core/commit/590b3115708bf94a9c7f169d98c6126380c7056e))
|
|
||||||
- An error occurred when going to the previous route if there was no previous route found ([985b87da](https://github.com/flarum/core/commit/985b87da6c9942c568a1a192e2fdcfde72e030ee))
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- `php flarum install --defaults` - this was meant to be used in our old development VM ([44c9109](https://github.com/flarum/core/commit/44c91099cd77138bb5fc29f14fb1e81a9781272d))
|
|
||||||
- Obsolete `id` attributes in JSON-API responses ([ecc3b5e](https://github.com/flarum/core/commit/ecc3b5e2271f8d9b38d52cd54476d86995dbe32e) and [7a44086](https://github.com/flarum/core/commit/7a44086bf3a0e3ba907dceb13d07ac695eca05ea))
|
|
||||||
|
|
||||||
## [0.1.0-beta.8.1](https://github.com/flarum/core/compare/v0.1.0-beta.8...v0.1.0-beta.8.1)
|
## [0.1.0-beta.8.1](https://github.com/flarum/core/compare/v0.1.0-beta.8...v0.1.0-beta.8.1)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix live output in `migrate:reset` command ([f591585](https://github.com/flarum/core/commit/f591585d02f8c4ff0211c5bf4413dd6baa724c05))
|
- Fix live output in `migrate:reset` command ([f591585](https://github.com/flarum/core/commit/f591585d02f8c4ff0211c5bf4413dd6baa724c05))
|
||||||
- Fix search with database prefix ([7705a2b](https://github.com/flarum/core/commit/7705a2b7d751943ef9d0c7379ec34f8530b99310))
|
- Fix search with database prefix ([7705a2b](https://github.com/flarum/core/commit/7705a2b7d751943ef9d0c7379ec34f8530b99310))
|
||||||
- Fix invalid join time of admin user created by installer ([57f73c9](https://github.com/flarum/core/commit/57f73c9638eeb825f9e336ed3c443afccfd8995e))
|
- Fix invalid join time of admin user created by installer ([57f73c9](https://github.com/flarum/core/commit/57f73c9638eeb825f9e336ed3c443afccfd8995e))
|
||||||
- Ensure InnoDB engine is used for all tables ([fb6b51b](https://github.com/flarum/core/commit/fb6b51b1cfef0af399607fe038603c8240800b2b), [6370f7e](https://github.com/flarum/core/commit/6370f7ecffa9ea7d5fb64d9551400edbc63318db))
|
- Ensure InnoDB engine is used for all tables ([fb6b51b](https://github.com/flarum/core/commit/fb6b51b1cfef0af399607fe038603c8240800b2b))
|
||||||
- Fix dropping foreign keys in `down` migrations ([57d5846](https://github.com/flarum/core/commit/57d5846b647881009d9e60f9ffca20b1bb77776e))
|
- Fix dropping foreign keys in `down` migrations ([57d5846](https://github.com/flarum/core/commit/57d5846b647881009d9e60f9ffca20b1bb77776e))
|
||||||
- Fix discussion list scroll position not being maintained when hero is not visible ([40dc6ac](https://github.com/flarum/core/commit/40dc6ac604c2a0973356b38217aa8d09352daae5))
|
- Fix discussion list scroll position not being maintained when hero is not visible ([40dc6ac](https://github.com/flarum/core/commit/40dc6ac604c2a0973356b38217aa8d09352daae5))
|
||||||
- Fix empty meta description tag ([88e43cc](https://github.com/flarum/core/commit/88e43cc6940ee30d6529e9ce659471ec4fb1c474))
|
- Fix empty meta description tag ([88e43cc](https://github.com/flarum/core/commit/88e43cc6940ee30d6529e9ce659471ec4fb1c474))
|
||||||
|
3
CONTRIBUTING.md
Normal file
3
CONTRIBUTING.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Contributing to Flarum
|
||||||
|
|
||||||
|
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://flarum.org/docs/contributing.html)** to learn how you can help.
|
3
LICENSE
3
LICENSE
@@ -1,7 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2019-2020 Stichting Flarum (Flarum Foundation)
|
Copyright (c) Toby Zerner
|
||||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
15
README.md
15
README.md
@@ -1,14 +1,12 @@
|
|||||||
<p align="center"><img src="https://flarum.org/assets/img/logo.png"></p>
|
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/flarum/core/actions?query=workflow%3ATests"><img src="https://github.com/flarum/core/workflows/Tests/badge.svg" alt="PHP Tests"></a>
|
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
|
||||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/dt/flarum/core" alt="Total Downloads"></a>
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
|
||||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/github/v/release/flarum/core?sort=semver" alt="Latest Version"></a>
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
|
||||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/l/flarum/core" alt="License"></a>
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
|
||||||
<a href="https://github.styleci.io/repos/28257573"><img src="https://github.styleci.io/repos/28257573/shield?style=flat" alt="StyleCI"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## About Flarum
|
## About Flarum
|
||||||
|
|
||||||
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
|
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
|
||||||
@@ -29,8 +27,9 @@ Thank you for considering contributing to Flarum! Please read the **[Contributin
|
|||||||
|
|
||||||
## Security Vulnerabilities
|
## Security Vulnerabilities
|
||||||
|
|
||||||
If you discover a security vulnerability within Flarum, please send an e-mail to [security@flarum.org](mailto:security@flarum.org). All security vulnerabilities will be promptly addressed. More details can be found in our [security policy](https://github.com/flarum/core/security/policy).
|
If you discover a security vulnerability within Flarum, please send an e-mail to [security@flarum.org](mailto:security@flarum.org). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
|
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
|
||||||
|
|
||||||
|
104
composer.json
104
composer.json
@@ -5,32 +5,13 @@
|
|||||||
"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": "Daniël Klabbers",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Alexander (Sasha) Skvortsov",
|
|
||||||
"email": "askvortsov@flarum.org"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
@@ -39,54 +20,52 @@
|
|||||||
"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.14.0",
|
"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.5.*",
|
||||||
"illuminate/cache": "^6.0",
|
"illuminate/cache": "5.5.*",
|
||||||
"illuminate/config": "^6.0",
|
"illuminate/config": "5.5.*",
|
||||||
"illuminate/container": "^6.0",
|
"illuminate/container": "5.5.*",
|
||||||
"illuminate/contracts": "^6.0",
|
"illuminate/contracts": "5.5.*",
|
||||||
"illuminate/database": "^6.0",
|
"illuminate/database": "5.5.*",
|
||||||
"illuminate/events": "^6.0",
|
"illuminate/events": "5.5.*",
|
||||||
"illuminate/filesystem": "^6.0",
|
"illuminate/filesystem": "5.5.*",
|
||||||
"illuminate/hashing": "^6.0",
|
"illuminate/hashing": "5.5.*",
|
||||||
"illuminate/mail": "^6.0",
|
"illuminate/mail": "5.5.*",
|
||||||
"illuminate/queue": "^6.0",
|
"illuminate/session": "5.5.*",
|
||||||
"illuminate/session": "^6.0",
|
"illuminate/support": "5.5.*",
|
||||||
"illuminate/support": "^6.0",
|
"illuminate/validation": "5.5.*",
|
||||||
"illuminate/validation": "^6.0",
|
"illuminate/view": "5.5.*",
|
||||||
"illuminate/view": "^6.0",
|
"intervention/image": "^2.3.0",
|
||||||
"intervention/image": "^2.5.0",
|
|
||||||
"laminas/laminas-diactoros": "^1.8.4",
|
|
||||||
"laminas/laminas-httphandlerrunner": "^1.0",
|
|
||||||
"laminas/laminas-stratigility": "^3.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": "^4.3.4",
|
"symfony/config": "^3.3",
|
||||||
"symfony/console": "^4.3.4",
|
"symfony/console": "^3.3",
|
||||||
"symfony/event-dispatcher": "^4.3.4",
|
"symfony/http-foundation": "^3.3",
|
||||||
"symfony/translation": "^4.3.4",
|
"symfony/translation": "^3.3",
|
||||||
"symfony/yaml": "^4.3.4",
|
"symfony/yaml": "^3.3",
|
||||||
"tobscure/json-api": "^0.3.0",
|
"tobscure/json-api": "^0.3.0",
|
||||||
"wikimedia/less.php": "^3.0"
|
"zendframework/zend-diactoros": "^1.8.4",
|
||||||
|
"zendframework/zend-httphandlerrunner": "^1.0",
|
||||||
|
"zendframework/zend-stratigility": "^3.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mockery/mockery": "^1.0",
|
"mockery/mockery": "^0.9.4",
|
||||||
"phpunit/phpunit": "^7.0"
|
"phpunit/phpunit": "^6.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -108,20 +87,5 @@
|
|||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "0.1.x-dev"
|
"dev-master": "0.1.x-dev"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test": [
|
|
||||||
"@test:unit",
|
|
||||||
"@test:integration"
|
|
||||||
],
|
|
||||||
"test:unit": "phpunit -c tests/phpunit.unit.xml",
|
|
||||||
"test:integration": "phpunit -c tests/phpunit.integration.xml",
|
|
||||||
"test:setup": "@php tests/integration/setup.php"
|
|
||||||
},
|
|
||||||
"scripts-descriptions": {
|
|
||||||
"test": "Runs all tests.",
|
|
||||||
"test:unit": "Runs all unit tests.",
|
|
||||||
"test:integration": "Runs all integration tests.",
|
|
||||||
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 150,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
30
js/dist/admin.js
vendored
30
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
32
js/dist/forum.js
vendored
32
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
3028
js/package-lock.json
generated
3028
js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,38 +2,25 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"name": "@flarum/core",
|
"name": "@flarum/core",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.10.1",
|
"bootstrap": "^3.3.7",
|
||||||
"@types/mithril": "^2.0.3",
|
|
||||||
"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.5.1",
|
"jquery": "^3.3.1",
|
||||||
"jquery.hotkeys": "^0.1.0",
|
"jquery.hotkeys": "^0.1.0",
|
||||||
"lodash-es": "^4.17.14",
|
"lodash-es": "^4.17.11",
|
||||||
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
|
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
|
||||||
"mithril": "^2.0.4",
|
"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.26.0",
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
js/shims.d.ts
vendored
32
js/shims.d.ts
vendored
@@ -1,32 +0,0 @@
|
|||||||
// Mithril
|
|
||||||
import Mithril from 'mithril';
|
|
||||||
|
|
||||||
// Other third-party libs
|
|
||||||
import * as _dayjs from 'dayjs';
|
|
||||||
import * as _$ from 'jquery';
|
|
||||||
|
|
||||||
// Globals from flarum/core
|
|
||||||
import Application from './src/common/Application';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* flarum/core exposes several extensions globally:
|
|
||||||
*
|
|
||||||
* - jQuery for convenient DOM manipulation
|
|
||||||
* - Mithril for VDOM and components
|
|
||||||
* - dayjs for date/time operations
|
|
||||||
*
|
|
||||||
* Since these are already part of the global namespace, extensions won't need
|
|
||||||
* to (and should not) bundle these themselves.
|
|
||||||
*/
|
|
||||||
declare global {
|
|
||||||
const $: typeof _$;
|
|
||||||
const m: Mithril.Static;
|
|
||||||
const dayjs: typeof _dayjs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All global variables owned by flarum/core.
|
|
||||||
*/
|
|
||||||
declare global {
|
|
||||||
const app: Application;
|
|
||||||
}
|
|
@@ -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,19 +27,15 @@ export default class AdminApplication extends Application {
|
|||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
mount() {
|
mount() {
|
||||||
// Mithril does not render the home route on https://example.com/admin, so
|
m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
|
||||||
// we need to go to https://example.com/admin#/ explicitly.
|
m.mount(document.getElementById('header-navigation'), Navigation.component());
|
||||||
if (!document.location.hash) document.location.hash = '#/';
|
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
|
||||||
|
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
|
||||||
|
m.mount(document.getElementById('admin-navigation'), AdminNav.component());
|
||||||
|
|
||||||
m.route.prefix = '#';
|
m.route.mode = 'hash';
|
||||||
super.mount();
|
super.mount();
|
||||||
|
|
||||||
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
|
|
||||||
m.mount(document.getElementById('header-navigation'), Navigation);
|
|
||||||
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
|
||||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
|
||||||
m.mount(document.getElementById('admin-navigation'), AdminNav);
|
|
||||||
|
|
||||||
// If an extension has just been enabled, then we will run its settings
|
// If an extension has just been enabled, then we will run its settings
|
||||||
// callback.
|
// callback.
|
||||||
const enabled = localStorage.getItem('enabledExtension');
|
const enabled = localStorage.getItem('enabledExtension');
|
||||||
@@ -63,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,15 @@
|
|||||||
import LinkButton from '../../common/components/LinkButton';
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
|
|
||||||
export default class AdminLinkButton extends LinkButton {
|
export default class AdminLinkButton extends LinkButton {
|
||||||
getButtonContent(children) {
|
getButtonContent() {
|
||||||
return [...super.getButtonContent(children), <div className="AdminLinkButton-description">{this.attrs.description}</div>];
|
const content = super.getButtonContent();
|
||||||
|
|
||||||
|
content.push(
|
||||||
|
<div className="AdminLinkButton-description">
|
||||||
|
{this.props.description}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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,77 +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',
|
href: app.route('dashboard'),
|
||||||
AdminLinkButton.component(
|
icon: 'far fa-chart-bar',
|
||||||
{
|
children: app.translator.trans('core.admin.nav.dashboard_button'),
|
||||||
href: app.route('dashboard'),
|
description: app.translator.trans('core.admin.nav.dashboard_text')
|
||||||
icon: 'far fa-chart-bar',
|
}));
|
||||||
description: app.translator.trans('core.admin.nav.dashboard_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.dashboard_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('basics', AdminLinkButton.component({
|
||||||
'basics',
|
href: app.route('basics'),
|
||||||
AdminLinkButton.component(
|
icon: 'fas fa-pencil-alt',
|
||||||
{
|
children: app.translator.trans('core.admin.nav.basics_button'),
|
||||||
href: app.route('basics'),
|
description: app.translator.trans('core.admin.nav.basics_text')
|
||||||
icon: 'fas fa-pencil-alt',
|
}));
|
||||||
description: app.translator.trans('core.admin.nav.basics_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.basics_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('mail', AdminLinkButton.component({
|
||||||
'mail',
|
href: app.route('mail'),
|
||||||
AdminLinkButton.component(
|
icon: 'fas fa-envelope',
|
||||||
{
|
children: app.translator.trans('core.admin.nav.email_button'),
|
||||||
href: app.route('mail'),
|
description: app.translator.trans('core.admin.nav.email_text')
|
||||||
icon: 'fas fa-envelope',
|
}));
|
||||||
description: app.translator.trans('core.admin.nav.email_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.email_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('permissions', AdminLinkButton.component({
|
||||||
'permissions',
|
href: app.route('permissions'),
|
||||||
AdminLinkButton.component(
|
icon: 'fas fa-key',
|
||||||
{
|
children: app.translator.trans('core.admin.nav.permissions_button'),
|
||||||
href: app.route('permissions'),
|
description: app.translator.trans('core.admin.nav.permissions_text')
|
||||||
icon: 'fas fa-key',
|
}));
|
||||||
description: app.translator.trans('core.admin.nav.permissions_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.permissions_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('appearance', AdminLinkButton.component({
|
||||||
'appearance',
|
href: app.route('appearance'),
|
||||||
AdminLinkButton.component(
|
icon: 'fas fa-paint-brush',
|
||||||
{
|
children: app.translator.trans('core.admin.nav.appearance_button'),
|
||||||
href: app.route('appearance'),
|
description: app.translator.trans('core.admin.nav.appearance_text')
|
||||||
icon: 'fas fa-paint-brush',
|
}));
|
||||||
description: app.translator.trans('core.admin.nav.appearance_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.appearance_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('extensions', AdminLinkButton.component({
|
||||||
'extensions',
|
href: app.route('extensions'),
|
||||||
AdminLinkButton.component(
|
icon: 'fas fa-puzzle-piece',
|
||||||
{
|
children: app.translator.trans('core.admin.nav.extensions_button'),
|
||||||
href: app.route('extensions'),
|
description: app.translator.trans('core.admin.nav.extensions_text')
|
||||||
icon: 'fas fa-puzzle-piece',
|
}));
|
||||||
description: app.translator.trans('core.admin.nav.extensions_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.extensions_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
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 Stream from '../../common/utils/Stream';
|
|
||||||
import EditCustomCssModal from './EditCustomCssModal';
|
import EditCustomCssModal from './EditCustomCssModal';
|
||||||
import EditCustomHeaderModal from './EditCustomHeaderModal';
|
import EditCustomHeaderModal from './EditCustomHeaderModal';
|
||||||
import EditCustomFooterModal from './EditCustomFooterModal';
|
import EditCustomFooterModal from './EditCustomFooterModal';
|
||||||
@@ -9,13 +8,13 @@ import UploadImageButton from './UploadImageButton';
|
|||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
export default class AppearancePage extends Page {
|
export default class AppearancePage extends Page {
|
||||||
oninit(vnode) {
|
init() {
|
||||||
super.oninit(vnode);
|
super.init();
|
||||||
|
|
||||||
this.primaryColor = Stream(app.data.settings.theme_primary_color);
|
this.primaryColor = m.prop(app.data.settings.theme_primary_color);
|
||||||
this.secondaryColor = Stream(app.data.settings.theme_secondary_color);
|
this.secondaryColor = m.prop(app.data.settings.theme_secondary_color);
|
||||||
this.darkMode = Stream(app.data.settings.theme_dark_mode);
|
this.darkMode = m.prop(app.data.settings.theme_dark_mode === '1');
|
||||||
this.coloredHeader = Stream(app.data.settings.theme_colored_header);
|
this.coloredHeader = m.prop(app.data.settings.theme_colored_header === '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
@@ -25,86 +24,86 @@ 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 className="AppearancePage-colors-input">
|
|
||||||
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.primaryColor} />
|
|
||||||
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.secondaryColor} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Switch.component(
|
<div className="AppearancePage-colors-input">
|
||||||
{
|
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
||||||
state: this.darkMode(),
|
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
|
||||||
onchange: this.darkMode,
|
</div>
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.appearance.dark_mode_label')
|
|
||||||
)}
|
|
||||||
|
|
||||||
{Switch.component(
|
{Switch.component({
|
||||||
{
|
state: this.darkMode(),
|
||||||
state: this.coloredHeader(),
|
children: app.translator.trans('core.admin.appearance.dark_mode_label'),
|
||||||
onchange: this.coloredHeader,
|
onchange: this.darkMode
|
||||||
},
|
})}
|
||||||
app.translator.trans('core.admin.appearance.colored_header_label')
|
|
||||||
)}
|
|
||||||
|
|
||||||
{Button.component(
|
{Switch.component({
|
||||||
{
|
state: this.coloredHeader(),
|
||||||
className: 'Button Button--primary',
|
children: app.translator.trans('core.admin.appearance.colored_header_label'),
|
||||||
type: 'submit',
|
onchange: this.coloredHeader
|
||||||
loading: this.loading,
|
})}
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.appearance.submit_button')
|
{Button.component({
|
||||||
)}
|
className: 'Button Button--primary',
|
||||||
|
type: 'submit',
|
||||||
|
children: app.translator.trans('core.admin.appearance.submit_button'),
|
||||||
|
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">
|
||||||
{Button.component(
|
{app.translator.trans('core.admin.appearance.custom_header_text')}
|
||||||
{
|
</div>
|
||||||
className: 'Button',
|
{Button.component({
|
||||||
onclick: () => app.modal.show(EditCustomHeaderModal),
|
className: 'Button',
|
||||||
},
|
children: app.translator.trans('core.admin.appearance.edit_header_button'),
|
||||||
app.translator.trans('core.admin.appearance.edit_header_button')
|
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">
|
||||||
{Button.component(
|
{app.translator.trans('core.admin.appearance.custom_footer_text')}
|
||||||
{
|
</div>
|
||||||
className: 'Button',
|
{Button.component({
|
||||||
onclick: () => app.modal.show(EditCustomFooterModal),
|
className: 'Button',
|
||||||
},
|
children: app.translator.trans('core.admin.appearance.edit_footer_button'),
|
||||||
app.translator.trans('core.admin.appearance.edit_footer_button')
|
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">
|
||||||
{Button.component(
|
{app.translator.trans('core.admin.appearance.custom_styles_text')}
|
||||||
{
|
</div>
|
||||||
className: 'Button',
|
{Button.component({
|
||||||
onclick: () => app.modal.show(EditCustomCssModal),
|
className: 'Button',
|
||||||
},
|
children: app.translator.trans('core.admin.appearance.edit_css_button'),
|
||||||
app.translator.trans('core.admin.appearance.edit_css_button')
|
onclick: () => app.modal.show(new EditCustomCssModal())
|
||||||
)}
|
})}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,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,16 +1,15 @@
|
|||||||
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';
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
import withAttr from '../../common/utils/withAttr';
|
|
||||||
|
|
||||||
export default class BasicsPage extends Page {
|
export default class BasicsPage extends Page {
|
||||||
oninit(vnode) {
|
init() {
|
||||||
super.oninit(vnode);
|
super.init();
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
@@ -21,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] = Stream(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;
|
||||||
@@ -35,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,107 +41,78 @@ export default class BasicsPage extends Page {
|
|||||||
<div className="BasicsPage">
|
<div className="BasicsPage">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<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)}/>
|
||||||
[<input className="FormControl" bidi={this.values.forum_title} />]
|
|
||||||
)}
|
|
||||||
|
|
||||||
{FieldSet.component(
|
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
|
||||||
},
|
|
||||||
[
|
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.forum_description_text')}</div>,
|
|
||||||
<textarea className="FormControl" bidi={this.values.forum_description} />,
|
|
||||||
]
|
]
|
||||||
)}
|
})}
|
||||||
|
|
||||||
|
{FieldSet.component({
|
||||||
|
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
||||||
|
children: [
|
||||||
|
<div className="helpText">
|
||||||
|
{app.translator.trans('core.admin.basics.forum_description_text')}
|
||||||
|
</div>,
|
||||||
|
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
{Object.keys(this.localeOptions).length > 1
|
{Object.keys(this.localeOptions).length > 1
|
||||||
? FieldSet.component(
|
? FieldSet.component({
|
||||||
{
|
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
||||||
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
children: [
|
||||||
},
|
Select.component({
|
||||||
[
|
options: this.localeOptions,
|
||||||
Select.component({
|
value: this.values.default_locale(),
|
||||||
options: this.localeOptions,
|
onchange: this.values.default_locale
|
||||||
value: this.values.default_locale(),
|
}),
|
||||||
onchange: this.values.default_locale,
|
Switch.component({
|
||||||
}),
|
state: this.values.show_language_selector(),
|
||||||
Switch.component(
|
onchange: this.values.show_language_selector,
|
||||||
{
|
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
||||||
state: this.values.show_language_selector(),
|
})
|
||||||
onchange: this.values.show_language_selector,
|
]
|
||||||
},
|
})
|
||||||
app.translator.trans('core.admin.basics.show_language_selector_label')
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
{FieldSet.component(
|
{FieldSet.component({
|
||||||
{
|
label: app.translator.trans('core.admin.basics.home_page_heading'),
|
||||||
label: app.translator.trans('core.admin.basics.home_page_heading'),
|
className: 'BasicsPage-homePage',
|
||||||
className: 'BasicsPage-homePage',
|
children: [
|
||||||
},
|
<div className="helpText">
|
||||||
[
|
{app.translator.trans('core.admin.basics.home_page_text')}
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>,
|
|
||||||
this.homePageItems()
|
|
||||||
.toArray()
|
|
||||||
.map(({ path, label }) => (
|
|
||||||
<label className="checkbox">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="homePage"
|
|
||||||
value={path}
|
|
||||||
checked={this.values.default_route() === path}
|
|
||||||
onclick={withAttr('value', this.values.default_route)}
|
|
||||||
/>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)),
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
|
|
||||||
{FieldSet.component(
|
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
|
||||||
className: 'BasicsPage-welcomeBanner',
|
|
||||||
},
|
|
||||||
[
|
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>,
|
|
||||||
<div className="BasicsPage-welcomeBanner-input">
|
|
||||||
<input className="FormControl" bidi={this.values.welcome_title} />
|
|
||||||
<textarea className="FormControl" bidi={this.values.welcome_message} />
|
|
||||||
</div>,
|
</div>,
|
||||||
]
|
this.homePageItems().toArray().map(({path, label}) =>
|
||||||
)}
|
<label className="checkbox">
|
||||||
|
<input type="radio" name="homePage" value={path} checked={this.values.default_route() === path} onclick={m.withAttr('value', this.values.default_route)}/>
|
||||||
{Object.keys(this.displayNameOptions).length > 1
|
{label}
|
||||||
? FieldSet.component(
|
</label>
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
|
||||||
},
|
|
||||||
[
|
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>,
|
|
||||||
Select.component({
|
|
||||||
options: this.displayNameOptions,
|
|
||||||
bidi: this.values.display_name_driver,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
: ''}
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
{Button.component(
|
{FieldSet.component({
|
||||||
{
|
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
||||||
type: 'submit',
|
className: 'BasicsPage-welcomeBanner',
|
||||||
className: 'Button Button--primary',
|
children: [
|
||||||
loading: this.loading,
|
<div className="helpText">
|
||||||
disabled: !this.changed(),
|
{app.translator.trans('core.admin.basics.welcome_banner_text')}
|
||||||
},
|
</div>,
|
||||||
app.translator.trans('core.admin.basics.submit_button')
|
<div className="BasicsPage-welcomeBanner-input">
|
||||||
)}
|
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
|
||||||
|
<textarea className="FormControl" value={this.values.welcome_message()} oninput={m.withAttr('value', this.values.welcome_message)}/>
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Button.component({
|
||||||
|
type: 'submit',
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
children: app.translator.trans('core.admin.basics.submit_button'),
|
||||||
|
loading: this.loading,
|
||||||
|
disabled: !this.changed()
|
||||||
|
})}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,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;
|
||||||
@@ -190,11 +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({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
|
app.alerts.show(this.successAlert = new Alert({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,24 +3,19 @@ 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';
|
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `EditGroupModal` component shows a modal dialog which allows the user
|
* The `EditGroupModal` component shows a modal dialog which allows the user
|
||||||
* to create or edit a group.
|
* to create or edit a group.
|
||||||
*/
|
*/
|
||||||
export default class EditGroupModal extends Modal {
|
export default class EditGroupModal extends Modal {
|
||||||
oninit(vnode) {
|
init() {
|
||||||
super.oninit(vnode);
|
this.group = this.props.group || app.store.createRecord('groups');
|
||||||
|
|
||||||
this.group = this.attrs.group || app.store.createRecord('groups');
|
this.nameSingular = m.prop(this.group.nameSingular() || '');
|
||||||
|
this.namePlural = m.prop(this.group.namePlural() || '');
|
||||||
this.nameSingular = Stream(this.group.nameSingular() || '');
|
this.icon = m.prop(this.group.icon() || '');
|
||||||
this.namePlural = Stream(this.group.namePlural() || '');
|
this.color = m.prop(this.group.color() || '');
|
||||||
this.icon = Stream(this.group.icon() || '');
|
|
||||||
this.color = Stream(this.group.color() || '');
|
|
||||||
this.isHidden = Stream(this.group.isHidden() || false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
@@ -29,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -51,95 +46,55 @@ 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',
|
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
|
||||||
<div className="Form-group">
|
<div className="EditGroupModal-name-input">
|
||||||
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
|
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
|
||||||
<div className="EditGroupModal-name-input">
|
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
|
||||||
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} bidi={this.nameSingular} />
|
</div>
|
||||||
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} bidi={this.namePlural} />
|
</div>, 30);
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
30
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('color', <div className="Form-group">
|
||||||
'color',
|
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
|
||||||
<div className="Form-group">
|
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
|
||||||
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
|
</div>, 20);
|
||||||
<input className="FormControl" placeholder="#aaaaaa" bidi={this.color} />
|
|
||||||
</div>,
|
|
||||||
20
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('icon', <div className="Form-group">
|
||||||
'icon',
|
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
|
||||||
<div className="Form-group">
|
<div className="helpText">
|
||||||
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
|
{app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1"/>})}
|
||||||
<div className="helpText">
|
</div>
|
||||||
{app.translator.trans('core.admin.edit_group.icon_text', { a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1" /> })}
|
<input className="FormControl" placeholder="fas fa-bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
|
||||||
</div>
|
</div>, 10);
|
||||||
<input className="FormControl" placeholder="fas fa-bolt" bidi={this.icon} />
|
|
||||||
</div>,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('submit', <div className="Form-group">
|
||||||
'hidden',
|
{Button.component({
|
||||||
<div className="Form-group">
|
type: 'submit',
|
||||||
{Switch.component(
|
className: 'Button Button--primary EditGroupModal-save',
|
||||||
{
|
loading: this.loading,
|
||||||
state: !!Number(this.isHidden()),
|
children: app.translator.trans('core.admin.edit_group.submit_button')
|
||||||
onchange: this.isHidden,
|
})}
|
||||||
},
|
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
||||||
app.translator.trans('core.admin.edit_group.hide_label')
|
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
|
||||||
)}
|
{app.translator.trans('core.admin.edit_group.delete_button')}
|
||||||
</div>,
|
</button>
|
||||||
10
|
) : ''}
|
||||||
);
|
</div>, -10);
|
||||||
|
|
||||||
items.add(
|
|
||||||
'submit',
|
|
||||||
<div className="Form-group">
|
|
||||||
{Button.component(
|
|
||||||
{
|
|
||||||
type: 'submit',
|
|
||||||
className: 'Button Button--primary EditGroupModal-save',
|
|
||||||
loading: this.loading,
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.edit_group.submit_button')
|
|
||||||
)}
|
|
||||||
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
|
||||||
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
|
|
||||||
{app.translator.trans('core.admin.edit_group.delete_button')}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</div>,
|
|
||||||
-10
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitData() {
|
|
||||||
return {
|
|
||||||
nameSingular: this.nameSingular(),
|
|
||||||
namePlural: this.namePlural(),
|
|
||||||
color: this.color(),
|
|
||||||
icon: this.icon(),
|
|
||||||
isHidden: this.isHidden(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onsubmit(e) {
|
onsubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
this.group
|
this.group.save({
|
||||||
.save(this.submitData(), { errorHandler: this.onerror.bind(this) })
|
nameSingular: this.nameSingular(),
|
||||||
|
namePlural: this.namePlural(),
|
||||||
|
color: this.color(),
|
||||||
|
icon: this.icon()
|
||||||
|
}, {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() {
|
||||||
@@ -12,26 +15,24 @@ export default class ExtensionsPage extends Page {
|
|||||||
<div className="ExtensionsPage">
|
<div className="ExtensionsPage">
|
||||||
<div className="ExtensionsPage-header">
|
<div className="ExtensionsPage-header">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{Button.component(
|
{Button.component({
|
||||||
{
|
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())
|
||||||
},
|
})}
|
||||||
app.translator.trans('core.admin.extensions.add_button')
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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)
|
||||||
const extension = app.data.extensions[id];
|
.map(id => {
|
||||||
const controls = this.controlItems(extension.id).toArray();
|
const extension = app.data.extensions[id];
|
||||||
|
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) : ''}
|
||||||
@@ -41,25 +42,21 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,38 +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',
|
icon: 'fas fa-cog',
|
||||||
Button.component(
|
children: app.translator.trans('core.admin.extensions.settings_button'),
|
||||||
{
|
onclick: app.extensionSettings[name]
|
||||||
icon: 'fas fa-cog',
|
}));
|
||||||
onclick: app.extensionSettings[name],
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.extensions.settings_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
items.add(
|
items.add('uninstall', Button.component({
|
||||||
'uninstall',
|
icon: 'far fa-trash-alt',
|
||||||
Button.component(
|
children: app.translator.trans('core.admin.extensions.uninstall_button'),
|
||||||
{
|
onclick: () => {
|
||||||
icon: 'far fa-trash-alt',
|
app.request({
|
||||||
onclick: () => {
|
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
|
||||||
app
|
method: 'DELETE'
|
||||||
.request({
|
}).then(() => window.location.reload());
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
.then(() => window.location.reload());
|
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
app.modal.show(new LoadingModal());
|
||||||
},
|
}
|
||||||
},
|
}));
|
||||||
app.translator.trans('core.admin.extensions.uninstall_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -118,41 +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}
|
||||||
body: { enabled: !enabled },
|
}).then(() => {
|
||||||
errorHandler: this.onerror.bind(this),
|
if (!enabled) localStorage.setItem('enabledExtension', id);
|
||||||
})
|
window.location.reload();
|
||||||
.then(() => {
|
});
|
||||||
if (!enabled) localStorage.setItem('enabledExtension', id);
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
app.modal.show(new LoadingModal());
|
||||||
}
|
|
||||||
|
|
||||||
onerror(e) {
|
|
||||||
// We need to give the modal animation time to start; if we close the modal too early,
|
|
||||||
// it breaks the bootstrap modal library.
|
|
||||||
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
|
|
||||||
setTimeout(() => {
|
|
||||||
app.modal.close();
|
|
||||||
}, 300); // Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
|
||||||
|
|
||||||
if (e.status !== 409) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = e.response.errors[0];
|
|
||||||
|
|
||||||
app.alerts.show(
|
|
||||||
{ type: 'error' },
|
|
||||||
app.translator.trans(`core.lib.error.${error.code}_message`, {
|
|
||||||
extension: error.extension,
|
|
||||||
extensions: error.extensions.join(', '),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,18 @@ 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) {
|
||||||
|
// Since this component is 'above' the content of the page (that is, it is a
|
||||||
|
// part of the global UI that persists between routes), we will flag the DOM
|
||||||
|
// to be retained across route changes.
|
||||||
|
context.retain = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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,228 +1,124 @@
|
|||||||
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';
|
||||||
import Select from '../../common/components/Select';
|
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
|
||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
|
|
||||||
export default class MailPage extends Page {
|
export default class MailPage extends Page {
|
||||||
oninit(vnode) {
|
init() {
|
||||||
super.oninit(vnode);
|
super.init();
|
||||||
|
|
||||||
this.saving = false;
|
this.loading = false;
|
||||||
this.sendingTest = false;
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
this.fields = [
|
||||||
this.loading = true;
|
'mail_driver',
|
||||||
|
'mail_host',
|
||||||
this.driverFields = {};
|
'mail_from',
|
||||||
this.fields = ['mail_driver', 'mail_from'];
|
'mail_port',
|
||||||
|
'mail_username',
|
||||||
|
'mail_password',
|
||||||
|
'mail_encryption'
|
||||||
|
];
|
||||||
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] = Stream(settings[key])));
|
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
|
||||||
|
|
||||||
app
|
this.localeOptions = {};
|
||||||
.request({
|
const locales = app.locales;
|
||||||
method: 'GET',
|
for (const i in locales) {
|
||||||
url: app.forum.attribute('apiUrl') + '/mail/settings',
|
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||||
})
|
}
|
||||||
.then((response) => {
|
|
||||||
this.driverFields = response['data']['attributes']['fields'];
|
|
||||||
this.status.sending = response['data']['attributes']['sending'];
|
|
||||||
this.status.errors = response['data']['attributes']['errors'];
|
|
||||||
|
|
||||||
for (const driver in this.driverFields) {
|
|
||||||
for (const field in this.driverFields[driver]) {
|
|
||||||
this.fields.push(field);
|
|
||||||
this.values[field] = Stream(settings[field]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
if (this.loading || this.saving) {
|
|
||||||
return (
|
|
||||||
<div className="MailPage">
|
|
||||||
<div className="container">
|
|
||||||
<LoadingIndicator />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.server_heading'),
|
||||||
label: app.translator.trans('core.admin.email.addresses_heading'),
|
className: 'MailPage-MailSettings',
|
||||||
className: 'MailPage-MailSettings',
|
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.from_label')}
|
<input className="FormControl" value={this.values.mail_driver() || ''} oninput={m.withAttr('value', this.values.mail_driver)} />
|
||||||
<input className="FormControl" bidi={this.values.mail_from} />
|
<label>{app.translator.trans('core.admin.email.host_label')}</label>
|
||||||
</label>
|
<input className="FormControl" value={this.values.mail_host() || ''} oninput={m.withAttr('value', this.values.mail_host)} />
|
||||||
</div>,
|
<label>{app.translator.trans('core.admin.email.port_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_port() || ''} oninput={m.withAttr('value', this.values.mail_port)} />
|
||||||
|
<label>{app.translator.trans('core.admin.email.encryption_label')}</label>
|
||||||
|
<input className="FormControl" value={this.values.mail_encryption() || ''} oninput={m.withAttr('value', this.values.mail_encryption)} />
|
||||||
|
</div>
|
||||||
]
|
]
|
||||||
)}
|
})}
|
||||||
|
|
||||||
{FieldSet.component(
|
{FieldSet.component({
|
||||||
{
|
label: app.translator.trans('core.admin.email.account_heading'),
|
||||||
label: app.translator.trans('core.admin.email.driver_heading'),
|
className: 'MailPage-MailSettings',
|
||||||
className: 'MailPage-MailSettings',
|
children: [
|
||||||
},
|
|
||||||
[
|
|
||||||
<div className="MailPage-MailSettings-input">
|
<div className="MailPage-MailSettings-input">
|
||||||
<label>
|
<label>{app.translator.trans('core.admin.email.username_label')}</label>
|
||||||
{app.translator.trans('core.admin.email.driver_label')}
|
<input className="FormControl" value={this.values.mail_username() || ''} oninput={m.withAttr('value', this.values.mail_username)} />
|
||||||
<Select
|
<label>{app.translator.trans('core.admin.email.password_label')}</label>
|
||||||
value={this.values.mail_driver()}
|
<input className="FormControl" value={this.values.mail_password() || ''} oninput={m.withAttr('value', this.values.mail_password)} />
|
||||||
options={Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {})}
|
</div>
|
||||||
onchange={this.values.mail_driver}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>,
|
|
||||||
]
|
]
|
||||||
)}
|
})}
|
||||||
|
|
||||||
{this.status.sending ||
|
{FieldSet.component({
|
||||||
Alert.component(
|
label: app.translator.trans('core.admin.email.addresses_heading'),
|
||||||
{
|
className: 'MailPage-MailSettings',
|
||||||
dismissible: false,
|
children: [
|
||||||
},
|
<div className="MailPage-MailSettings-input">
|
||||||
app.translator.trans('core.admin.email.not_sending_message')
|
<label>{app.translator.trans('core.admin.email.from_label')}</label>
|
||||||
)}
|
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
|
||||||
|
</div>
|
||||||
{fieldKeys.length > 0 &&
|
|
||||||
FieldSet.component(
|
|
||||||
{
|
|
||||||
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
|
|
||||||
className: 'MailPage-MailSettings',
|
|
||||||
},
|
|
||||||
[
|
|
||||||
<div className="MailPage-MailSettings-input">
|
|
||||||
{fieldKeys.map((field) => [
|
|
||||||
<label>
|
|
||||||
{app.translator.trans(`core.admin.email.${field}_label`)}
|
|
||||||
{this.renderField(field)}
|
|
||||||
</label>,
|
|
||||||
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
|
|
||||||
])}
|
|
||||||
</div>,
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FieldSet>
|
|
||||||
{Button.component(
|
|
||||||
{
|
|
||||||
type: 'submit',
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
disabled: !this.changed(),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.email.submit_button')
|
|
||||||
)}
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
{FieldSet.component(
|
|
||||||
{
|
|
||||||
label: app.translator.trans('core.admin.email.send_test_mail_heading'),
|
|
||||||
className: 'MailPage-MailSettings',
|
|
||||||
},
|
|
||||||
[
|
|
||||||
<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',
|
|
||||||
disabled: this.sendingTest || this.changed(),
|
|
||||||
onclick: () => this.sendTestEmail(),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.email.send_test_mail_button')
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)}
|
})}
|
||||||
|
|
||||||
|
{Button.component({
|
||||||
|
type: 'submit',
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
children: app.translator.trans('core.admin.email.submit_button'),
|
||||||
|
loading: this.loading,
|
||||||
|
disabled: !this.changed()
|
||||||
|
})}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderField(name) {
|
|
||||||
const driver = this.values.mail_driver();
|
|
||||||
const field = this.driverFields[driver][name];
|
|
||||||
const prop = this.values[name];
|
|
||||||
|
|
||||||
if (typeof field === 'string') {
|
|
||||||
return <input className="FormControl" bidi={prop} />;
|
|
||||||
} else {
|
|
||||||
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' }, 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.loading) return;
|
||||||
|
|
||||||
this.saving = true;
|
this.loading = 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({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
|
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.saving = false;
|
this.loading = 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,133 +8,126 @@ 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)
|
||||||
const restrictToGroupIds = app.data.permissions[required] || [];
|
.forEach(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);
|
||||||
});
|
});
|
||||||
|
|
||||||
return groupIds;
|
return groupIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PermissionDropdown extends Dropdown {
|
export default class PermissionDropdown extends Dropdown {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
super.initAttrs(attrs);
|
super.initProps(props);
|
||||||
|
|
||||||
attrs.className = 'PermissionDropdown';
|
props.className = 'PermissionDropdown';
|
||||||
attrs.buttonClassName = 'Button Button--text';
|
props.buttonClassName = 'Button Button--text';
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view() {
|
||||||
const children = [];
|
this.props.children = [];
|
||||||
|
|
||||||
let groupIds = app.data.permissions[this.attrs.permission] || [];
|
let groupIds = app.data.permissions[this.props.permission] || [];
|
||||||
|
|
||||||
groupIds = filterByRequiredPermissions(groupIds, this.attrs.permission);
|
groupIds = filterByRequiredPermissions(groupIds, this.props.permission);
|
||||||
|
|
||||||
const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
|
const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
|
||||||
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
|
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
|
||||||
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
||||||
|
|
||||||
if (everyone) {
|
if (everyone) {
|
||||||
this.attrs.label = Badge.component({ icon: 'fas fa-globe' });
|
this.props.label = Badge.component({icon: 'fas fa-globe'});
|
||||||
} else if (members) {
|
} else if (members) {
|
||||||
this.attrs.label = Badge.component({ icon: 'fas fa-user' });
|
this.props.label = Badge.component({icon: 'fas fa-user'});
|
||||||
} else {
|
} else {
|
||||||
this.attrs.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.attrs.allowGuest) {
|
if (this.props.allowGuest) {
|
||||||
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')],
|
||||||
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)
|
||||||
},
|
})
|
||||||
[Badge.component({ icon: 'fas fa-globe' }), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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')],
|
||||||
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)
|
||||||
},
|
}),
|
||||||
[Badge.component({ icon: 'fas fa-user' }), ' ', app.translator.trans('core.admin.permissions_controls.members_button')]
|
|
||||||
),
|
|
||||||
|
|
||||||
Separator.component(),
|
Separator.component(),
|
||||||
|
|
||||||
Button.component(
|
Button.component({
|
||||||
{
|
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([]);
|
||||||
},
|
}
|
||||||
},
|
})
|
||||||
[badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
[].push.apply(
|
[].push.apply(
|
||||||
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) =>
|
children: [badgeForId(group.id()), ' ', group.namePlural()],
|
||||||
Button.component(
|
icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
|
||||||
{
|
onclick: (e) => {
|
||||||
icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
|
if (e.shiftKey) e.stopPropagation();
|
||||||
onclick: (e) => {
|
this.toggle(group.id());
|
||||||
if (e.shiftKey) e.stopPropagation();
|
},
|
||||||
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),
|
|
||||||
},
|
|
||||||
[badgeForId(group.id()), ' ', group.namePlural()]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.view({ ...vnode, children });
|
return super.view();
|
||||||
}
|
}
|
||||||
|
|
||||||
save(groupIds) {
|
save(groupIds) {
|
||||||
const permission = this.attrs.permission;
|
const permission = this.props.permission;
|
||||||
|
|
||||||
app.data.permissions[permission] = groupIds;
|
app.data.permissions[permission] = groupIds;
|
||||||
|
|
||||||
app.request({
|
app.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: app.forum.attribute('apiUrl') + '/permission',
|
url: app.forum.attribute('apiUrl') + '/permission',
|
||||||
body: { permission, groupIds },
|
data: {permission, groupIds}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle(groupId) {
|
toggle(groupId) {
|
||||||
const permission = this.attrs.permission;
|
const permission = this.props.permission;
|
||||||
|
|
||||||
let groupIds = app.data.permissions[permission] || [];
|
let groupIds = app.data.permissions[permission] || [];
|
||||||
|
|
||||||
@@ -144,13 +137,13 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
isGroupDisabled(id) {
|
isGroupDisabled(id) {
|
||||||
return filterByRequiredPermissions([id], this.attrs.permission).indexOf(id) === -1;
|
return filterByRequiredPermissions([id], this.props.permission).indexOf(id) === -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,17 +6,19 @@ import ItemList from '../../common/utils/ItemList';
|
|||||||
import icon from '../../common/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
|
|
||||||
export default class PermissionGrid extends Component {
|
export default class PermissionGrid extends Component {
|
||||||
oninit(vnode) {
|
init() {
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.permissions = this.permissionItems().toArray();
|
this.permissions = this.permissionItems().toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
@@ -24,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>
|
||||||
@@ -61,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'),
|
||||||
{
|
children: this.viewItems().toArray()
|
||||||
label: app.translator.trans('core.admin.permissions.read_heading'),
|
}, 100);
|
||||||
children: this.viewItems().toArray(),
|
|
||||||
},
|
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('start', {
|
||||||
'start',
|
label: app.translator.trans('core.admin.permissions.create_heading'),
|
||||||
{
|
children: this.startItems().toArray()
|
||||||
label: app.translator.trans('core.admin.permissions.create_heading'),
|
}, 90);
|
||||||
children: this.startItems().toArray(),
|
|
||||||
},
|
|
||||||
90
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('reply', {
|
||||||
'reply',
|
label: app.translator.trans('core.admin.permissions.participate_heading'),
|
||||||
{
|
children: this.replyItems().toArray()
|
||||||
label: app.translator.trans('core.admin.permissions.participate_heading'),
|
}, 80);
|
||||||
children: this.replyItems().toArray(),
|
|
||||||
},
|
|
||||||
80
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('moderate', {
|
||||||
'moderate',
|
label: app.translator.trans('core.admin.permissions.moderate_heading'),
|
||||||
{
|
children: this.moderateItems().toArray()
|
||||||
label: app.translator.trans('core.admin.permissions.moderate_heading'),
|
}, 70);
|
||||||
children: this.moderateItems().toArray(),
|
|
||||||
},
|
|
||||||
70
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -103,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',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
|
||||||
icon: 'fas fa-eye',
|
permission: 'viewDiscussions',
|
||||||
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
|
allowGuest: true
|
||||||
permission: 'viewDiscussions',
|
}, 100);
|
||||||
allowGuest: true,
|
|
||||||
},
|
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('viewUserList', {
|
||||||
'viewHiddenGroups',
|
icon: 'fas fa-users',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
|
||||||
icon: 'fas fa-users',
|
permission: 'viewUserList',
|
||||||
label: app.translator.trans('core.admin.permissions.view_hidden_groups_label'),
|
allowGuest: true
|
||||||
permission: 'viewHiddenGroups',
|
}, 100);
|
||||||
},
|
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('signUp', {
|
||||||
'viewUserList',
|
icon: 'fas fa-user-plus',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.sign_up_label'),
|
||||||
icon: 'fas fa-users',
|
setting: () => SettingDropdown.component({
|
||||||
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
|
key: 'allow_sign_up',
|
||||||
permission: 'viewUserList',
|
options: [
|
||||||
allowGuest: true,
|
{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')}
|
||||||
100
|
]
|
||||||
);
|
})
|
||||||
|
}, 90);
|
||||||
items.add(
|
|
||||||
'signUp',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-user-plus',
|
|
||||||
label: app.translator.trans('core.admin.permissions.sign_up_label'),
|
|
||||||
setting: () =>
|
|
||||||
SettingDropdown.component({
|
|
||||||
key: 'allow_sign_up',
|
|
||||||
options: [
|
|
||||||
{ value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button') },
|
|
||||||
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button') },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
90
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add('viewLastSeenAt', {
|
items.add('viewLastSeenAt', {
|
||||||
icon: 'far fa-clock',
|
icon: 'far fa-clock',
|
||||||
@@ -164,39 +122,31 @@ 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',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
|
||||||
icon: 'fas fa-edit',
|
permission: 'startDiscussion'
|
||||||
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
|
}, 100);
|
||||||
permission: 'startDiscussion',
|
|
||||||
},
|
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('allowRenaming', {
|
||||||
'allowRenaming',
|
icon: 'fas fa-i-cursor',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
|
||||||
icon: 'fas fa-i-cursor',
|
setting: () => {
|
||||||
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
|
const minutes = parseInt(app.data.settings.allow_renaming, 10);
|
||||||
setting: () => {
|
|
||||||
const minutes = parseInt(app.data.settings.allow_renaming, 10);
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -204,39 +154,31 @@ 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',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
|
||||||
icon: 'fas fa-reply',
|
permission: 'discussion.reply'
|
||||||
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
|
}, 100);
|
||||||
permission: 'discussion.reply',
|
|
||||||
},
|
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('allowPostEditing', {
|
||||||
'allowPostEditing',
|
icon: 'fas fa-pencil-alt',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
|
||||||
icon: 'fas fa-pencil-alt',
|
setting: () => {
|
||||||
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
|
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
|
||||||
setting: () => {
|
|
||||||
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -244,95 +186,47 @@ 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',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
|
||||||
icon: 'fas fa-bullseye',
|
permission: 'discussion.viewIpsPosts'
|
||||||
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
|
}, 110);
|
||||||
permission: 'discussion.viewIpsPosts',
|
|
||||||
},
|
|
||||||
110
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('renameDiscussions', {
|
||||||
'renameDiscussions',
|
icon: 'fas fa-i-cursor',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
|
||||||
icon: 'fas fa-i-cursor',
|
permission: 'discussion.rename'
|
||||||
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
|
}, 100);
|
||||||
permission: 'discussion.rename',
|
|
||||||
},
|
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('hideDiscussions', {
|
||||||
'hideDiscussions',
|
icon: 'far fa-trash-alt',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
|
||||||
icon: 'far fa-trash-alt',
|
permission: 'discussion.hide'
|
||||||
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
|
}, 90);
|
||||||
permission: 'discussion.hide',
|
|
||||||
},
|
|
||||||
90
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('deleteDiscussions', {
|
||||||
'deleteDiscussions',
|
icon: 'fas fa-times',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
|
||||||
icon: 'fas fa-times',
|
permission: 'discussion.delete'
|
||||||
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
|
}, 80);
|
||||||
permission: 'discussion.delete',
|
|
||||||
},
|
|
||||||
80
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('editPosts', {
|
||||||
'postWithoutThrottle',
|
icon: 'fas fa-pencil-alt',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
|
||||||
icon: 'fas fa-swimmer',
|
permission: 'discussion.editPosts'
|
||||||
label: app.translator.trans('core.admin.permissions.post_without_throttle_label'),
|
}, 70);
|
||||||
permission: 'postWithoutThrottle',
|
|
||||||
},
|
|
||||||
70
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('hidePosts', {
|
||||||
'editPosts',
|
icon: 'far fa-trash-alt',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
|
||||||
icon: 'fas fa-pencil-alt',
|
permission: 'discussion.hidePosts'
|
||||||
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
|
}, 60);
|
||||||
permission: 'discussion.editPosts',
|
|
||||||
},
|
|
||||||
70
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add('deletePosts', {
|
||||||
'hidePosts',
|
icon: 'fas fa-times',
|
||||||
{
|
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
|
||||||
icon: 'far fa-trash-alt',
|
permission: 'discussion.deletePosts'
|
||||||
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
|
}, 60);
|
||||||
permission: 'discussion.hidePosts',
|
|
||||||
},
|
|
||||||
60
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
'deletePosts',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-times',
|
|
||||||
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
|
|
||||||
permission: 'discussion.deletePosts',
|
|
||||||
},
|
|
||||||
60
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
'userEdit',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-user-cog',
|
|
||||||
label: app.translator.trans('core.admin.permissions.edit_users_label'),
|
|
||||||
permission: 'user.edit',
|
|
||||||
},
|
|
||||||
60
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -340,25 +234,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'),
|
||||||
{
|
render: item => {
|
||||||
label: app.translator.trans('core.admin.permissions.global_heading'),
|
if (item.setting) {
|
||||||
render: (item) => {
|
return item.setting();
|
||||||
if (item.setting) {
|
} else if (item.permission) {
|
||||||
return item.setting();
|
return PermissionDropdown.component({
|
||||||
} else if (item.permission) {
|
permission: item.permission,
|
||||||
return PermissionDropdown.component({
|
allowGuest: item.allowGuest
|
||||||
permission: item.permission,
|
});
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
@@ -9,22 +9,27 @@ import ItemList from '../../common/utils/ItemList';
|
|||||||
* avatar/name, with a dropdown of session controls.
|
* avatar/name, with a dropdown of session controls.
|
||||||
*/
|
*/
|
||||||
export default class SessionDropdown extends Dropdown {
|
export default class SessionDropdown extends Dropdown {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
super.initAttrs(attrs);
|
super.initProps(props);
|
||||||
|
|
||||||
attrs.className = 'SessionDropdown';
|
props.className = 'SessionDropdown';
|
||||||
attrs.buttonClassName = 'Button Button--user Button--flat';
|
props.buttonClassName = 'Button Button--user Button--flat';
|
||||||
attrs.menuClassName = 'Dropdown-menu--right';
|
props.menuClassName = 'Dropdown-menu--right';
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view() {
|
||||||
return super.view({ ...vnode, children: this.items().toArray() });
|
this.props.children = this.items().toArray();
|
||||||
|
|
||||||
|
return super.view();
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,15 +40,12 @@ 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',
|
||||||
{
|
children: app.translator.trans('core.admin.header.log_out_button'),
|
||||||
icon: 'fas fa-sign-out-alt',
|
onclick: app.session.logout.bind(app.session)
|
||||||
onclick: app.session.logout.bind(app.session),
|
}),
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.header.log_out_button')
|
|
||||||
),
|
|
||||||
-100
|
-100
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -3,30 +3,23 @@ import Button from '../../common/components/Button';
|
|||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
export default class SettingDropdown extends SelectDropdown {
|
export default class SettingDropdown extends SelectDropdown {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
super.initAttrs(attrs);
|
super.initProps(props);
|
||||||
|
|
||||||
attrs.className = 'SettingDropdown';
|
props.className = 'SettingDropdown';
|
||||||
attrs.buttonClassName = 'Button Button--text';
|
props.buttonClassName = 'Button Button--text';
|
||||||
attrs.caretIcon = 'fas fa-caret-down';
|
props.caretIcon = 'fas fa-caret-down';
|
||||||
attrs.defaultLabel = 'Custom';
|
props.defaultLabel = 'Custom';
|
||||||
}
|
|
||||||
|
|
||||||
view(vnode) {
|
props.children = props.options.map(({value, label}) => {
|
||||||
return super.view({
|
const active = app.data.settings[props.key] === value;
|
||||||
...vnode,
|
|
||||||
children: this.attrs.options.map(({ value, label }) => {
|
|
||||||
const active = app.data.settings[this.attrs.key] === value;
|
|
||||||
|
|
||||||
return Button.component(
|
return Button.component({
|
||||||
{
|
children: label,
|
||||||
icon: active ? 'fas fa-check' : true,
|
icon: active ? 'fas fa-check' : true,
|
||||||
onclick: saveSettings.bind(this, { [this.attrs.key]: value }),
|
onclick: saveSettings.bind(this, {[props.key]: value}),
|
||||||
active,
|
active
|
||||||
},
|
});
|
||||||
label
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,9 @@
|
|||||||
import Modal from '../../common/components/Modal';
|
import Modal from '../../common/components/Modal';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
export default class SettingsModal extends Modal {
|
export default class SettingsModal extends Modal {
|
||||||
oninit(vnode) {
|
init() {
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.settings = {};
|
this.settings = {};
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -21,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>
|
||||||
);
|
);
|
||||||
@@ -29,14 +28,18 @@ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setting(key, fallback = '') {
|
setting(key, fallback = '') {
|
||||||
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
|
this.settings[key] = this.settings[key] || m.prop(app.data.settings[key] || fallback);
|
||||||
|
|
||||||
return this.settings[key];
|
return this.settings[key];
|
||||||
}
|
}
|
||||||
@@ -44,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]) {
|
||||||
@@ -64,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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,28 +1,30 @@
|
|||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
|
|
||||||
export default class UploadImageButton extends Button {
|
export default class UploadImageButton extends Button {
|
||||||
loading = false;
|
init() {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view() {
|
||||||
this.attrs.loading = this.loading;
|
this.props.loading = this.loading;
|
||||||
this.attrs.className = (this.attrs.className || '') + ' Button';
|
this.props.className = (this.props.className || '') + ' Button';
|
||||||
|
|
||||||
if (app.data.settings[this.attrs.name + '_path']) {
|
if (app.data.settings[this.props.name + '_path']) {
|
||||||
this.attrs.onclick = this.remove.bind(this);
|
this.props.onclick = this.remove.bind(this);
|
||||||
|
this.props.children = app.translator.trans('core.admin.upload_image.remove_button');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p><img src={app.forum.attribute(this.props.name+'Url')} alt=""/></p>
|
||||||
<img src={app.forum.attribute(this.attrs.name + 'Url')} alt="" />
|
<p>{super.view()}</p>
|
||||||
</p>
|
|
||||||
<p>{super.view({ ...vnode, children: app.translator.trans('core.admin.upload_image.remove_button') })}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.attrs.onclick = this.upload.bind(this);
|
this.props.onclick = this.upload.bind(this);
|
||||||
|
this.props.children = app.translator.trans('core.admin.upload_image.upload_button');
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.view({ ...vnode, children: app.translator.trans('core.admin.upload_image.upload_button') });
|
return super.view();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,26 +35,23 @@ 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')
|
const data = new FormData();
|
||||||
.hide()
|
data.append(this.props.name, $(e.target)[0].files[0]);
|
||||||
.click()
|
|
||||||
.on('change', (e) => {
|
|
||||||
const body = new FormData();
|
|
||||||
body.append(this.attrs.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
|
||||||
body,
|
}).then(
|
||||||
})
|
this.success.bind(this),
|
||||||
.then(this.success.bind(this), this.failure.bind(this));
|
this.failure.bind(this)
|
||||||
});
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,16 +61,17 @@ 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(
|
||||||
})
|
this.success.bind(this),
|
||||||
.then(this.success.bind(this), this.failure.bind(this));
|
this.failure.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceUrl() {
|
resourceUrl() {
|
||||||
return app.forum.attribute('apiUrl') + '/' + this.attrs.name;
|
return app.forum.attribute('apiUrl') + '/' + this.props.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
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 },
|
'dashboard': {path: '/', component: DashboardPage.component()},
|
||||||
basics: { path: '/basics', component: BasicsPage },
|
'basics': {path: '/basics', component: BasicsPage.component()},
|
||||||
permissions: { path: '/permissions', component: PermissionsPage },
|
'permissions': {path: '/permissions', component: PermissionsPage.component()},
|
||||||
appearance: { path: '/appearance', component: AppearancePage },
|
'appearance': {path: '/appearance', component: AppearancePage.component()},
|
||||||
extensions: { path: '/extensions', component: ExtensionsPage },
|
'extensions': {path: '/extensions', component: ExtensionsPage.component()},
|
||||||
mail: { path: '/mail', component: MailPage },
|
'mail': {path: '/mail', component: MailPage.component()}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -3,14 +3,12 @@ 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
|
||||||
body: settings,
|
}).catch(error => {
|
||||||
})
|
app.data.settings = oldSettings;
|
||||||
.catch((error) => {
|
throw error;
|
||||||
app.data.settings = oldSettings;
|
});
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import ItemList from './utils/ItemList';
|
import ItemList from './utils/ItemList';
|
||||||
import Button from './components/Button';
|
import Alert from './components/Alert';
|
||||||
import ModalManager from './components/ModalManager';
|
import ModalManager from './components/ModalManager';
|
||||||
import AlertManager from './components/AlertManager';
|
import AlertManager from './components/AlertManager';
|
||||||
import RequestErrorModal from './components/RequestErrorModal';
|
|
||||||
import Translator from './Translator';
|
import Translator from './Translator';
|
||||||
import Store from './Store';
|
import Store from './Store';
|
||||||
import Session from './Session';
|
import Session from './Session';
|
||||||
@@ -11,7 +10,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 +19,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 +84,7 @@ export default class Application {
|
|||||||
discussions: Discussion,
|
discussions: Discussion,
|
||||||
posts: Post,
|
posts: Post,
|
||||||
groups: Group,
|
groups: Group,
|
||||||
notifications: Notification,
|
notifications: Notification
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,49 +105,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,21 +124,24 @@ 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);
|
||||||
|
|
||||||
for (const extender of extenders) {
|
for (const extender of extenders) {
|
||||||
@@ -189,34 +151,31 @@ export default class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mount(basePath = '') {
|
mount(basePath = '') {
|
||||||
// An object with a callable view property is used in order to pass arguments to the component; see https://mithril.js.org/mount.html
|
this.modal = m.mount(document.getElementById('modal'), <ModalManager/>);
|
||||||
m.mount(document.getElementById('modal'), { view: () => ModalManager.component({ state: this.modal }) });
|
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>);
|
||||||
m.mount(document.getElementById('alerts'), { view: () => AlertManager.component({ state: this.alerts }) });
|
|
||||||
|
|
||||||
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. When this happens, we'll add classes to the header and app body
|
// down.
|
||||||
// which will set the navbar's position to fixed. We don't want to always
|
new ScrollListener(top => {
|
||||||
// have it fixed, as that could overlap with custom headers.
|
|
||||||
const scrollListener = 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
|
||||||
$('.App-header').toggleClass('navbar-fixed-top', top >= offset);
|
.toggleClass('affix', top >= offset)
|
||||||
});
|
.toggleClass('scrolled', top > offset);
|
||||||
|
}).start();
|
||||||
scrollListener.start();
|
|
||||||
scrollListener.update();
|
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
||||||
});
|
});
|
||||||
|
|
||||||
liveHumanTimes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,16 +196,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.
|
||||||
*
|
*
|
||||||
@@ -269,16 +218,15 @@ export default class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTitle() {
|
updateTitle() {
|
||||||
const count = this.titleCount ? `(${this.titleCount}) ` : '';
|
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') +
|
||||||
const pageTitleWithSeparator = this.title && m.route.get() !== this.forum.attribute('basePath') + '/' ? this.title + ' - ' : '';
|
(this.title ? this.title + ' - ' : '') +
|
||||||
const title = this.forum.attribute('title');
|
this.forum.attribute('title');
|
||||||
document.title = count + pageTitleWithSeparator + title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an AJAX request, handling any low-level errors that may occur.
|
* Make an AJAX request, handling any low-level errors that may occur.
|
||||||
*
|
*
|
||||||
* @see https://mithril.js.org/request.html
|
* @see https://lhorie.github.io/mithril/mithril.request.html
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
@@ -306,19 +254,17 @@ 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 ||
|
throw error;
|
||||||
((error) => {
|
});
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
// When extracting the data from the response, we can check the server
|
// When extracting the data from the response, we can check the server
|
||||||
// response code and show an error message to the user if something's gone
|
// 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) {
|
||||||
@@ -345,88 +291,58 @@ 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.
|
||||||
return m.request(options).then(
|
const deferred = m.deferred();
|
||||||
(response) => response,
|
|
||||||
(error) => {
|
|
||||||
let content;
|
|
||||||
|
|
||||||
switch (error.status) {
|
m.request(options).then(response => deferred.resolve(response), error => {
|
||||||
case 422:
|
this.requestError = error;
|
||||||
content = error.response.errors
|
|
||||||
.map((error) => [error.detail, <br />])
|
|
||||||
.reduce((a, b) => a.concat(b), [])
|
|
||||||
.slice(0, -1);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 401:
|
let children;
|
||||||
case 403:
|
|
||||||
content = app.translator.trans('core.lib.error.permission_denied_message');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 404:
|
switch (error.status) {
|
||||||
case 410:
|
case 422:
|
||||||
content = app.translator.trans('core.lib.error.not_found_message');
|
children = error.response.errors
|
||||||
break;
|
.map(error => [error.detail, <br/>])
|
||||||
|
.reduce((a, b) => a.concat(b), [])
|
||||||
|
.slice(0, -1);
|
||||||
|
break;
|
||||||
|
|
||||||
case 429:
|
case 401:
|
||||||
content = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
|
case 403:
|
||||||
break;
|
children = app.translator.trans('core.lib.error.permission_denied_message');
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
case 404:
|
||||||
content = app.translator.trans('core.lib.error.generic_message');
|
case 410:
|
||||||
}
|
children = app.translator.trans('core.lib.error.not_found_message');
|
||||||
|
break;
|
||||||
|
|
||||||
const isDebug = app.forum.attribute('debug');
|
case 429:
|
||||||
// contains a formatted errors if possible, response must be an JSON API array of errors
|
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
|
||||||
// the details property is decoded to transform escaped characters such as '\n'
|
break;
|
||||||
const errors = error.response && error.response.errors;
|
|
||||||
const formattedError = Array.isArray(errors) && errors[0] && errors[0].detail && errors.map((e) => decodeURI(e.detail));
|
|
||||||
|
|
||||||
error.alert = {
|
default:
|
||||||
type: 'error',
|
children = app.translator.trans('core.lib.error.generic_message');
|
||||||
content,
|
|
||||||
controls: isDebug && [
|
|
||||||
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error, formattedError)}>
|
|
||||||
Debug
|
|
||||||
</Button>,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
options.errorHandler(error);
|
|
||||||
} catch (error) {
|
|
||||||
if (isDebug && error.xhr) {
|
|
||||||
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, error.alert.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
error.alert = new Alert({
|
||||||
* @param {RequestError} error
|
type: 'error',
|
||||||
* @param {string[]} [formattedError]
|
children
|
||||||
* @private
|
});
|
||||||
*/
|
|
||||||
showDebug(error, formattedError) {
|
|
||||||
this.alerts.dismiss(this.requestErrorAlert);
|
|
||||||
|
|
||||||
this.modal.show(RequestErrorModal, { error, formattedError });
|
try {
|
||||||
|
options.errorHandler(error);
|
||||||
|
} catch (error) {
|
||||||
|
this.alerts.show(error.alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
deferred.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -438,19 +354,9 @@ export default class Application {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
route(name, params = {}) {
|
route(name, params = {}) {
|
||||||
const route = this.routes[name];
|
const url = this.routes[name].path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
|
||||||
|
const queryString = m.route.buildQueryString(params);
|
||||||
if (!route) throw new Error(`Route '${name}' does not exist`);
|
const prefix = m.route.mode === 'pathname' ? app.forum.attribute('basePath') : '';
|
||||||
|
|
||||||
const url = route.path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
|
|
||||||
|
|
||||||
// Remove falsy values in params to avoid having urls like '/?sort&q'
|
|
||||||
for (const key in params) {
|
|
||||||
if (params.hasOwnProperty(key) && !params[key]) delete params[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryString = m.buildQueryString(params);
|
|
||||||
const prefix = m.route.prefix === '' ? this.forum.attribute('basePath') : '';
|
|
||||||
|
|
||||||
return prefix + url + (queryString ? '?' + queryString : '');
|
return prefix + url + (queryString ? '?' + queryString : '');
|
||||||
}
|
}
|
||||||
|
225
js/src/common/Component.js
Normal file
225
js/src/common/Component.js
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Component` class defines a user interface 'building block'. A component
|
||||||
|
* can generate a virtual DOM to be rendered on each redraw.
|
||||||
|
*
|
||||||
|
* An instance's virtual DOM can be retrieved directly using the {@link
|
||||||
|
* Component#render} method.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* this.myComponentInstance = new MyComponent({foo: 'bar'});
|
||||||
|
* return m('div', this.myComponentInstance.render());
|
||||||
|
*
|
||||||
|
* Alternatively, components can be nested, letting Mithril take care of
|
||||||
|
* instance persistence. For this, the static {@link Component.component} method
|
||||||
|
* can be used.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* return m('div', MyComponent.component({foo: 'bar'));
|
||||||
|
*
|
||||||
|
* @see https://lhorie.github.io/mithril/mithril.component.html
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
export default class Component {
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array|Object} children
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
constructor(props = {}, children = null) {
|
||||||
|
if (children) props.children = children;
|
||||||
|
|
||||||
|
this.constructor.initProps(props);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties passed into the component.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
this.props = props;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root DOM element for the component.
|
||||||
|
*
|
||||||
|
* @type DOMElement
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.element = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to retain the component's subtree on redraw.
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.retain = false;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the component is constructed.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the component is destroyed, i.e. after a redraw where it is no
|
||||||
|
* longer a part of the view.
|
||||||
|
*
|
||||||
|
* @see https://lhorie.github.io/mithril/mithril.component.html#unloading-components
|
||||||
|
* @param {Object} e
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
onunload() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the renderable virtual DOM that represents the component's view.
|
||||||
|
*
|
||||||
|
* This should NOT be overridden by subclasses. Subclasses wishing to define
|
||||||
|
* their virtual DOM should override Component#view instead.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* this.myComponentInstance = new MyComponent({foo: 'bar'});
|
||||||
|
* return m('div', this.myComponentInstance.render());
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
* @final
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const vdom = this.retain ? {subtree: 'retain'} : this.view();
|
||||||
|
|
||||||
|
// Override the root element's config attribute with our own function, which
|
||||||
|
// will set the component instance's element property to the root DOM
|
||||||
|
// element, and then run the component class' config method.
|
||||||
|
vdom.attrs = vdom.attrs || {};
|
||||||
|
|
||||||
|
const originalConfig = vdom.attrs.config;
|
||||||
|
|
||||||
|
vdom.attrs.config = (...args) => {
|
||||||
|
this.element = args[0];
|
||||||
|
this.config.apply(this, args.slice(1));
|
||||||
|
if (originalConfig) originalConfig.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
return vdom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a jQuery object for this component's element. If you pass in a
|
||||||
|
* selector string, this method will return a jQuery object, using the current
|
||||||
|
* element as its buffer.
|
||||||
|
*
|
||||||
|
* For example, calling `component.$('li')` will return a jQuery object
|
||||||
|
* containing all of the `li` elements inside the DOM element of this
|
||||||
|
* component.
|
||||||
|
*
|
||||||
|
* @param {String} [selector] a jQuery-compatible selector string
|
||||||
|
* @returns {jQuery} the jQuery object for the DOM node
|
||||||
|
* @final
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
$(selector) {
|
||||||
|
const $element = $(this.element);
|
||||||
|
|
||||||
|
return selector ? $element.find(selector) : $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the component's root element is redrawn. This hook can be used
|
||||||
|
* to perform any actions on the DOM, both on the initial draw and any
|
||||||
|
* subsequent redraws. See Mithril's documentation for more information.
|
||||||
|
*
|
||||||
|
* @see https://lhorie.github.io/mithril/mithril.html#the-config-attribute
|
||||||
|
* @param {Boolean} isInitialized
|
||||||
|
* @param {Object} context
|
||||||
|
* @param {Object} vdom
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
config() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the virtual DOM that represents the component's view.
|
||||||
|
*
|
||||||
|
* @return {Object} The virtual DOM
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
view() {
|
||||||
|
throw new Error('Component#view must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a Mithril component object for this component, preloaded with props.
|
||||||
|
*
|
||||||
|
* @see https://lhorie.github.io/mithril/mithril.component.html
|
||||||
|
* @param {Object} [props] Properties to set on the component
|
||||||
|
* @param children
|
||||||
|
* @return {Object} The Mithril component object
|
||||||
|
* @property {function} controller
|
||||||
|
* @property {function} view
|
||||||
|
* @property {Object} component The class of this component
|
||||||
|
* @property {Object} props The props that were passed to the component
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static component(props = {}, children = null) {
|
||||||
|
const componentProps = Object.assign({}, props);
|
||||||
|
|
||||||
|
if (children) componentProps.children = children;
|
||||||
|
|
||||||
|
this.initProps(componentProps);
|
||||||
|
|
||||||
|
// Set up a function for Mithril to get the component's view. It will accept
|
||||||
|
// the component's controller (which happens to be the component itself, in
|
||||||
|
// our case), update its props with the ones supplied, and then render the view.
|
||||||
|
const view = (component) => {
|
||||||
|
component.props = componentProps;
|
||||||
|
return component.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mithril uses this property on the view function to cache component
|
||||||
|
// controllers between redraws, thus persisting component state.
|
||||||
|
view.$original = this.prototype.view;
|
||||||
|
|
||||||
|
// Our output object consists of a controller constructor + a view function
|
||||||
|
// which Mithril will use to instantiate and render the component. We also
|
||||||
|
// attach a reference to the props that were passed through and the
|
||||||
|
// component's class for reference.
|
||||||
|
const output = {
|
||||||
|
controller: this.bind(undefined, componentProps),
|
||||||
|
view: view,
|
||||||
|
props: componentProps,
|
||||||
|
component: this
|
||||||
|
};
|
||||||
|
|
||||||
|
// If a `key` prop was set, then we'll assume that we want that to actually
|
||||||
|
// show up as an attribute on the component object so that Mithril's key
|
||||||
|
// algorithm can be applied.
|
||||||
|
if (componentProps.key) {
|
||||||
|
output.attrs = {key: componentProps.key};
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component's props.
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static initProps(props) {
|
||||||
|
}
|
||||||
|
}
|
@@ -1,168 +0,0 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
|
|
||||||
let deprecatedPropsWarned = false;
|
|
||||||
let deprecatedInitPropsWarned = false;
|
|
||||||
|
|
||||||
export interface ComponentAttrs extends Mithril.Attributes {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Component` class defines a user interface 'building block'. A component
|
|
||||||
* generates a virtual DOM to be rendered on each redraw.
|
|
||||||
*
|
|
||||||
* Essentially, this is a wrapper for Mithril's components that adds several useful features:
|
|
||||||
*
|
|
||||||
* - In the `oninit` and `onbeforeupdate` lifecycle hooks, we store vnode attrs in `this.attrs.
|
|
||||||
* This allows us to use attrs across components without having to pass the vnode to every single
|
|
||||||
* method.
|
|
||||||
* - The static `initAttrs` method allows a convenient way to provide defaults (or to otherwise modify)
|
|
||||||
* the attrs that have been passed into a component.
|
|
||||||
* - When the component is created in the DOM, we store its DOM element under `this.element`; this lets
|
|
||||||
* us use jQuery to modify child DOM state from internal methods via the `this.$()` method.
|
|
||||||
* - A convenience `component` method, which serves as an alternative to hyperscript and JSX.
|
|
||||||
*
|
|
||||||
* As with other Mithril components, components extending Component can be initialized
|
|
||||||
* and nested using JSX, hyperscript, or a combination of both. The `component` method can also
|
|
||||||
* be used.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* return m('div', <MyComponent foo="bar"><p>Hello World</p></MyComponent>);
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* return m('div', MyComponent.component({foo: 'bar'), m('p', 'Hello World!'));
|
|
||||||
*
|
|
||||||
* @see https://mithril.js.org/components.html
|
|
||||||
*/
|
|
||||||
export default abstract class Component<T extends ComponentAttrs = ComponentAttrs> implements Mithril.ClassComponent<T> {
|
|
||||||
/**
|
|
||||||
* The root DOM element for the component.
|
|
||||||
*/
|
|
||||||
protected element!: Element;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The attributes passed into the component.
|
|
||||||
*
|
|
||||||
* @see https://mithril.js.org/components.html#passing-data-to-components
|
|
||||||
*/
|
|
||||||
protected attrs!: T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
abstract view(vnode: Mithril.Vnode<T, this>): Mithril.Children;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
oninit(vnode: Mithril.Vnode<T, this>) {
|
|
||||||
this.setAttrs(vnode.attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
oncreate(vnode: Mithril.VnodeDOM<T, this>) {
|
|
||||||
this.element = vnode.dom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
onbeforeupdate(vnode: Mithril.VnodeDOM<T, this>) {
|
|
||||||
this.setAttrs(vnode.attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a jQuery object for this component's element. If you pass in a
|
|
||||||
* selector string, this method will return a jQuery object, using the current
|
|
||||||
* element as its buffer.
|
|
||||||
*
|
|
||||||
* For example, calling `component.$('li')` will return a jQuery object
|
|
||||||
* containing all of the `li` elements inside the DOM element of this
|
|
||||||
* component.
|
|
||||||
*
|
|
||||||
* @param {String} [selector] a jQuery-compatible selector string
|
|
||||||
* @returns {jQuery} the jQuery object for the DOM node
|
|
||||||
* @final
|
|
||||||
*/
|
|
||||||
protected $(selector) {
|
|
||||||
const $element = $(this.element);
|
|
||||||
|
|
||||||
return selector ? $element.find(selector) : $element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience method to attach a component without JSX.
|
|
||||||
* Has the same effect as calling `m(THIS_CLASS, attrs, children)`.
|
|
||||||
*
|
|
||||||
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
|
|
||||||
*/
|
|
||||||
static component(attrs = {}, children = null): Mithril.Vnode {
|
|
||||||
const componentAttrs = Object.assign({}, attrs);
|
|
||||||
|
|
||||||
return m(this as any, componentAttrs, children);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a reference to the vnode attrs after running them through initAttrs,
|
|
||||||
* and checking for common issues.
|
|
||||||
*/
|
|
||||||
private setAttrs(attrs: T = {} as T): void {
|
|
||||||
(this.constructor as typeof Component).initAttrs(attrs);
|
|
||||||
|
|
||||||
if (attrs) {
|
|
||||||
if ('children' in attrs) {
|
|
||||||
throw new Error(
|
|
||||||
`[${
|
|
||||||
(this.constructor as any).name
|
|
||||||
}] The "children" attribute of attrs should never be used. Either pass children in as the vnode children or rename the attribute`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('tag' in attrs) {
|
|
||||||
throw new Error(`[${(this.constructor as any).name}] You cannot use the "tag" attribute name with Mithril 2.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.attrs = attrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the component's attrs.
|
|
||||||
*
|
|
||||||
* This can be used to assign default values for missing, optional attrs.
|
|
||||||
*/
|
|
||||||
protected static initAttrs<T>(attrs: T): void {
|
|
||||||
// Deprecated, part of Mithril 2 BC layer
|
|
||||||
if ('initProps' in this && !deprecatedInitPropsWarned) {
|
|
||||||
deprecatedInitPropsWarned = true;
|
|
||||||
console.warn('initProps is deprecated, please use initAttrs instead.');
|
|
||||||
(this as any).initProps(attrs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The attributes passed into the component.
|
|
||||||
*
|
|
||||||
* @see https://mithril.js.org/components.html#passing-data-to-components
|
|
||||||
*
|
|
||||||
* @deprecated, use attrs instead.
|
|
||||||
*/
|
|
||||||
get props() {
|
|
||||||
if (!deprecatedPropsWarned) {
|
|
||||||
deprecatedPropsWarned = true;
|
|
||||||
console.warn('this.props is deprecated, please use this.attrs instead.');
|
|
||||||
}
|
|
||||||
return this.attrs;
|
|
||||||
}
|
|
||||||
set props(props) {
|
|
||||||
if (!deprecatedPropsWarned) {
|
|
||||||
deprecatedPropsWarned = true;
|
|
||||||
console.warn('this.props is deprecated, please use this.attrs instead.');
|
|
||||||
}
|
|
||||||
this.attrs = props;
|
|
||||||
}
|
|
||||||
|
|
||||||
// END DEPRECATED MITHRIL 2 BC LAYER
|
|
||||||
}
|
|
@@ -1,74 +0,0 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Fragment` class represents a chunk of DOM that is rendered once with Mithril and then takes
|
|
||||||
* over control of its own DOM and lifecycle.
|
|
||||||
*
|
|
||||||
* This is very similar to the `Component` wrapper class, but is used for more fine-grained control over
|
|
||||||
* the rendering and display of some significant chunks of the DOM. In contrast to components, fragments
|
|
||||||
* do not offer Mithril's lifecycle hooks.
|
|
||||||
*
|
|
||||||
* Use this when you want to enjoy the benefits of JSX / VDOM for initial rendering, combined with
|
|
||||||
* small helper methods that then make updates to that DOM directly, instead of fully redrawing
|
|
||||||
* everything through Mithril.
|
|
||||||
*
|
|
||||||
* This should only be used when necessary, and only with `m.render`. If you are unsure whether you need
|
|
||||||
* this or `Component, you probably need `Component`.
|
|
||||||
*/
|
|
||||||
export default abstract class Fragment {
|
|
||||||
/**
|
|
||||||
* The root DOM element for the fragment.
|
|
||||||
*/
|
|
||||||
protected element!: Element;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a jQuery object for this fragment's element. If you pass in a
|
|
||||||
* selector string, this method will return a jQuery object, using the current
|
|
||||||
* element as its buffer.
|
|
||||||
*
|
|
||||||
* For example, calling `fragment.$('li')` will return a jQuery object
|
|
||||||
* containing all of the `li` elements inside the DOM element of this
|
|
||||||
* fragment.
|
|
||||||
*
|
|
||||||
* @param {String} [selector] a jQuery-compatible selector string
|
|
||||||
* @returns {jQuery} the jQuery object for the DOM node
|
|
||||||
* @final
|
|
||||||
*/
|
|
||||||
public $(selector) {
|
|
||||||
const $element = $(this.element);
|
|
||||||
|
|
||||||
return selector ? $element.find(selector) : $element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the renderable virtual DOM that represents the fragment's view.
|
|
||||||
*
|
|
||||||
* This should NOT be overridden by subclasses. Subclasses wishing to define
|
|
||||||
* their virtual DOM should override Fragment#view instead.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const fragment = new MyFragment();
|
|
||||||
* m.render(document.body, fragment.render());
|
|
||||||
*
|
|
||||||
* @final
|
|
||||||
*/
|
|
||||||
public render(): Mithril.Vnode<Mithril.Attributes, this> {
|
|
||||||
const vdom = this.view();
|
|
||||||
|
|
||||||
vdom.attrs = vdom.attrs || {};
|
|
||||||
|
|
||||||
const originalOnCreate = vdom.attrs.oncreate;
|
|
||||||
|
|
||||||
vdom.attrs.oncreate = (vnode) => {
|
|
||||||
this.element = vnode.dom;
|
|
||||||
if (originalOnCreate) originalOnCreate.apply(this, [vnode]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return vdom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a view out of virtual elements.
|
|
||||||
*/
|
|
||||||
abstract view(): Mithril.Vnode<Mithril.Attributes, this>;
|
|
||||||
}
|
|
@@ -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,66 +154,52 @@ 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(
|
method: this.exists ? 'PATCH' : 'POST',
|
||||||
Object.assign(
|
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
||||||
{
|
data: request
|
||||||
method: this.exists ? 'PATCH' : 'POST',
|
}, options)).then(
|
||||||
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
// If everything went well, we'll make sure the store knows that this
|
||||||
body: request,
|
// model exists now (if it didn't already), and we'll push the data that
|
||||||
},
|
// the API returned into the store.
|
||||||
options
|
payload => {
|
||||||
)
|
this.store.data[payload.data.type] = this.store.data[payload.data.type] || {};
|
||||||
)
|
this.store.data[payload.data.type][payload.data.id] = this;
|
||||||
.then(
|
return this.store.pushPayload(payload);
|
||||||
// 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
|
|
||||||
// the API returned into the store.
|
|
||||||
(payload) => {
|
|
||||||
this.store.data[payload.data.type] = this.store.data[payload.data.type] || {};
|
|
||||||
this.store.data[payload.data.type][payload.data.id] = this;
|
|
||||||
return this.store.pushPayload(payload);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 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.redraw();
|
m.lazyRedraw();
|
||||||
throw response;
|
throw response;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a request to delete the resource.
|
* Send a request to delete the resource.
|
||||||
*
|
*
|
||||||
* @param {Object} body Data to send along with the DELETE request.
|
* @param {Object} data Data to send along with the DELETE request.
|
||||||
* @param {Object} [options]
|
* @param {Object} [options]
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
delete(body, options = {}) {
|
delete(data, options = {}) {
|
||||||
if (!this.exists) return Promise.resolve();
|
if (!this.exists) return m.deferred.resolve().promise;
|
||||||
|
|
||||||
return app
|
return app.request(Object.assign({
|
||||||
.request(
|
method: 'DELETE',
|
||||||
Object.assign(
|
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
||||||
{
|
data
|
||||||
method: 'DELETE',
|
}, options)).then(() => {
|
||||||
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
this.exists = false;
|
||||||
body,
|
this.store.remove(this);
|
||||||
},
|
});
|
||||||
options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.exists = false;
|
|
||||||
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -30,17 +30,12 @@ export default class Session {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
login(body, options = {}) {
|
login(data, options = {}) {
|
||||||
return app.request(
|
return app.request(Object.assign({
|
||||||
Object.assign(
|
method: 'POST',
|
||||||
{
|
url: app.forum.attribute('baseUrl') + '/login',
|
||||||
method: 'POST',
|
data
|
||||||
url: `${app.forum.attribute('baseUrl')}/login`,
|
}, options));
|
||||||
body,
|
|
||||||
},
|
|
||||||
options
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +44,6 @@ export default class Session {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
logout() {
|
logout() {
|
||||||
window.location = `${app.forum.attribute('baseUrl')}/logout?token=${this.csrfToken}`;
|
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
@@ -82,29 +84,22 @@ export default class Store {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
find(type, id, query = {}, options = {}) {
|
find(type, id, query = {}, options = {}) {
|
||||||
let params = query;
|
let data = query;
|
||||||
let url = app.forum.attribute('apiUrl') + '/' + type;
|
let url = app.forum.attribute('apiUrl') + '/' + type;
|
||||||
|
|
||||||
if (id instanceof Array) {
|
if (id instanceof Array) {
|
||||||
url += '?filter[id]=' + id.join(',');
|
url += '?filter[id]=' + id.join(',');
|
||||||
} else if (typeof id === 'object') {
|
} else if (typeof id === 'object') {
|
||||||
params = id;
|
data = id;
|
||||||
} else if (id) {
|
} else if (id) {
|
||||||
url += '/' + id;
|
url += '/' + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app.request(Object.assign({
|
||||||
.request(
|
method: 'GET',
|
||||||
Object.assign(
|
url,
|
||||||
{
|
data
|
||||||
method: 'GET',
|
}, options)).then(this.pushPayload.bind(this));
|
||||||
url,
|
|
||||||
params,
|
|
||||||
},
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import User from './models/User';
|
||||||
import username from './helpers/username';
|
import username from './helpers/username';
|
||||||
import extract from './utils/extract';
|
import extract from './utils/extract';
|
||||||
|
|
||||||
@@ -66,43 +67,27 @@ 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) {
|
||||||
// Either an opening or closing tag.
|
|
||||||
if (match[1]) {
|
if (match[1]) {
|
||||||
open[0].push(input[match[1]]);
|
open[0].push(input[match[1]]);
|
||||||
} else if (match[3]) {
|
} else if (match[3]) {
|
||||||
if (match[2]) {
|
if (match[2]) {
|
||||||
// Closing tag. We start by removing all raw children (generally in the form of strings) from the temporary
|
|
||||||
// holding array, then run them through m.fragment to convert them to vnodes. Usually this will just give us a
|
|
||||||
// text vnode, but using m.fragment as opposed to an explicit conversion should be more flexible. This is necessary because
|
|
||||||
// otherwise, our generated vnode will have raw strings as its children, and mithril expects vnodes.
|
|
||||||
// Finally, we add the now-processed vnodes back onto the holding array (which is the same object in memory as the
|
|
||||||
// children array of the vnode we are currently processing), and remove the reference to the holding array so that
|
|
||||||
// further text will be added to the full set of returned elements.
|
|
||||||
const rawChildren = open[0].splice(0, open[0].length);
|
|
||||||
open[0].push(...m.fragment(rawChildren).children);
|
|
||||||
open.shift();
|
open.shift();
|
||||||
} else {
|
} else {
|
||||||
// If a vnode with a matching tag was provided in the translator input, we use that. Otherwise, we create a new vnode
|
let tag = input[match[3]] || {tag: match[3], children: []};
|
||||||
// with this tag, and an empty children array (since we're expecting to insert children, as that's the point of having this in translator)
|
|
||||||
let tag = input[match[3]] || { tag: match[3], children: [] };
|
|
||||||
open[0].push(tag);
|
open[0].push(tag);
|
||||||
// Insert the tag's children array as the first element of open, so that text in between the opening
|
|
||||||
// and closing tags will be added to the tag's children, not to the full set of returned elements.
|
|
||||||
open.unshift(tag.children || tag);
|
open.unshift(tag.children || tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not an html tag, we add it to open[0], which is either the full set of returned elements (vnodes and text),
|
|
||||||
// or if an html tag is currently being processed, the children attribute of that html tag's vnode.
|
|
||||||
open[0].push(part);
|
open[0].push(part);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return hydrated.filter((part) => part);
|
return hydrated.filter(part => part);
|
||||||
}
|
}
|
||||||
|
|
||||||
pluralize(translation, number) {
|
pluralize(translation, number) {
|
||||||
@@ -112,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];
|
||||||
@@ -137,13 +122,11 @@ export default class Translator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,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':
|
||||||
@@ -254,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':
|
||||||
@@ -262,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;
|
||||||
|
@@ -12,20 +12,15 @@ import anchorScroll from './utils/anchorScroll';
|
|||||||
import RequestError from './utils/RequestError';
|
import RequestError from './utils/RequestError';
|
||||||
import abbreviateNumber from './utils/abbreviateNumber';
|
import abbreviateNumber from './utils/abbreviateNumber';
|
||||||
import * as string from './utils/string';
|
import * as string from './utils/string';
|
||||||
import Stream from './utils/Stream';
|
|
||||||
import SubtreeRetainer from './utils/SubtreeRetainer';
|
import SubtreeRetainer from './utils/SubtreeRetainer';
|
||||||
import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh';
|
|
||||||
import extract from './utils/extract';
|
import extract from './utils/extract';
|
||||||
import ScrollListener from './utils/ScrollListener';
|
import ScrollListener from './utils/ScrollListener';
|
||||||
import stringToColor from './utils/stringToColor';
|
import stringToColor from './utils/stringToColor';
|
||||||
import subclassOf from './utils/subclassOf';
|
|
||||||
import SuperTextarea from './utils/SuperTextarea';
|
|
||||||
import patchMithril from './utils/patchMithril';
|
import patchMithril from './utils/patchMithril';
|
||||||
import classList from './utils/classList';
|
import classList from './utils/classList';
|
||||||
import extractText from './utils/extractText';
|
import extractText from './utils/extractText';
|
||||||
import formatNumber from './utils/formatNumber';
|
import formatNumber from './utils/formatNumber';
|
||||||
import mapRoutes from './utils/mapRoutes';
|
import mapRoutes from './utils/mapRoutes';
|
||||||
import withAttr from './utils/withAttr';
|
|
||||||
import Notification from './models/Notification';
|
import Notification from './models/Notification';
|
||||||
import User from './models/User';
|
import User from './models/User';
|
||||||
import Post from './models/Post';
|
import Post from './models/Post';
|
||||||
@@ -35,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';
|
||||||
@@ -43,12 +37,10 @@ import Placeholder from './components/Placeholder';
|
|||||||
import Separator from './components/Separator';
|
import Separator from './components/Separator';
|
||||||
import Dropdown from './components/Dropdown';
|
import Dropdown from './components/Dropdown';
|
||||||
import SplitDropdown from './components/SplitDropdown';
|
import SplitDropdown from './components/SplitDropdown';
|
||||||
import RequestErrorModal from './components/RequestErrorModal';
|
|
||||||
import FieldSet from './components/FieldSet';
|
import FieldSet from './components/FieldSet';
|
||||||
import Select from './components/Select';
|
import Select from './components/Select';
|
||||||
import Navigation from './components/Navigation';
|
import Navigation from './components/Navigation';
|
||||||
import Alert from './components/Alert';
|
import Alert from './components/Alert';
|
||||||
import Link from './components/Link';
|
|
||||||
import LinkButton from './components/LinkButton';
|
import LinkButton from './components/LinkButton';
|
||||||
import Checkbox from './components/Checkbox';
|
import Checkbox from './components/Checkbox';
|
||||||
import SelectDropdown from './components/SelectDropdown';
|
import SelectDropdown from './components/SelectDropdown';
|
||||||
@@ -67,13 +59,11 @@ import highlight from './helpers/highlight';
|
|||||||
import username from './helpers/username';
|
import username from './helpers/username';
|
||||||
import userOnline from './helpers/userOnline';
|
import userOnline from './helpers/userOnline';
|
||||||
import listItems from './helpers/listItems';
|
import listItems from './helpers/listItems';
|
||||||
import Fragment from './Fragment';
|
|
||||||
import DefaultResolver from './resolvers/DefaultResolver';
|
|
||||||
|
|
||||||
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,
|
||||||
@@ -89,27 +79,20 @@ export default {
|
|||||||
'utils/extract': extract,
|
'utils/extract': extract,
|
||||||
'utils/ScrollListener': ScrollListener,
|
'utils/ScrollListener': ScrollListener,
|
||||||
'utils/stringToColor': stringToColor,
|
'utils/stringToColor': stringToColor,
|
||||||
'utils/Stream': Stream,
|
|
||||||
'utils/subclassOf': subclassOf,
|
|
||||||
'utils/SuperTextarea': SuperTextarea,
|
|
||||||
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
|
|
||||||
'utils/patchMithril': patchMithril,
|
'utils/patchMithril': patchMithril,
|
||||||
'utils/classList': classList,
|
'utils/classList': classList,
|
||||||
'utils/extractText': extractText,
|
'utils/extractText': extractText,
|
||||||
'utils/formatNumber': formatNumber,
|
'utils/formatNumber': formatNumber,
|
||||||
'utils/mapRoutes': mapRoutes,
|
'utils/mapRoutes': mapRoutes,
|
||||||
'utils/withAttr': withAttr,
|
|
||||||
'models/Notification': Notification,
|
'models/Notification': Notification,
|
||||||
'models/User': User,
|
'models/User': User,
|
||||||
'models/Post': Post,
|
'models/Post': Post,
|
||||||
'models/Discussion': Discussion,
|
'models/Discussion': Discussion,
|
||||||
'models/Group': Group,
|
'models/Group': Group,
|
||||||
'models/Forum': Forum,
|
'models/Forum': Forum,
|
||||||
Component: Component,
|
'Component': Component,
|
||||||
Fragment: Fragment,
|
'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,
|
||||||
@@ -117,12 +100,10 @@ export default {
|
|||||||
'components/Separator': Separator,
|
'components/Separator': Separator,
|
||||||
'components/Dropdown': Dropdown,
|
'components/Dropdown': Dropdown,
|
||||||
'components/SplitDropdown': SplitDropdown,
|
'components/SplitDropdown': SplitDropdown,
|
||||||
'components/RequestErrorModal': RequestErrorModal,
|
|
||||||
'components/FieldSet': FieldSet,
|
'components/FieldSet': FieldSet,
|
||||||
'components/Select': Select,
|
'components/Select': Select,
|
||||||
'components/Navigation': Navigation,
|
'components/Navigation': Navigation,
|
||||||
'components/Alert': Alert,
|
'components/Alert': Alert,
|
||||||
'components/Link': Link,
|
|
||||||
'components/LinkButton': LinkButton,
|
'components/LinkButton': LinkButton,
|
||||||
'components/Checkbox': Checkbox,
|
'components/Checkbox': Checkbox,
|
||||||
'components/SelectDropdown': SelectDropdown,
|
'components/SelectDropdown': SelectDropdown,
|
||||||
@@ -130,8 +111,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,
|
||||||
@@ -140,6 +121,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
|
||||||
'resolvers/DefaultResolver': DefaultResolver,
|
|
||||||
};
|
};
|
||||||
|
57
js/src/common/components/Alert.js
Normal file
57
js/src/common/components/Alert.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import Component from '../Component';
|
||||||
|
import Button from './Button';
|
||||||
|
import listItems from '../helpers/listItems';
|
||||||
|
import extract from '../utils/extract';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Alert` component represents an alert box, which contains a message,
|
||||||
|
* some controls, and may be dismissible.
|
||||||
|
*
|
||||||
|
* The alert may have the following special props:
|
||||||
|
*
|
||||||
|
* - `type` The type of alert this is. Will be used to give the alert a class
|
||||||
|
* name of `Alert--{type}`.
|
||||||
|
* - `controls` An array of controls to show in the alert.
|
||||||
|
* - `dismissible` Whether or not the alert can be dismissed.
|
||||||
|
* - `ondismiss` A callback to run when the alert is dismissed.
|
||||||
|
*
|
||||||
|
* All other props will be assigned as attributes on the alert element.
|
||||||
|
*/
|
||||||
|
export default class Alert extends Component {
|
||||||
|
view() {
|
||||||
|
const attrs = Object.assign({}, this.props);
|
||||||
|
|
||||||
|
const type = extract(attrs, 'type');
|
||||||
|
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
|
||||||
|
|
||||||
|
const children = extract(attrs, 'children');
|
||||||
|
const controls = extract(attrs, 'controls') || [];
|
||||||
|
|
||||||
|
// If the alert is meant to be dismissible (which is the case by default),
|
||||||
|
// then we will create a dismiss button to append as the final control in
|
||||||
|
// the alert.
|
||||||
|
const dismissible = extract(attrs, 'dismissible');
|
||||||
|
const ondismiss = extract(attrs, 'ondismiss');
|
||||||
|
const dismissControl = [];
|
||||||
|
|
||||||
|
if (dismissible || dismissible === undefined) {
|
||||||
|
dismissControl.push(
|
||||||
|
<Button
|
||||||
|
icon="fas fa-times"
|
||||||
|
className="Button Button--link Button--icon Alert-dismiss"
|
||||||
|
onclick={ondismiss}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...attrs}>
|
||||||
|
<span className="Alert-body">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
<ul className="Alert-controls">
|
||||||
|
{listItems(controls.concat(dismissControl))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,50 +0,0 @@
|
|||||||
import Component, { ComponentAttrs } from '../Component';
|
|
||||||
import Button from './Button';
|
|
||||||
import listItems from '../helpers/listItems';
|
|
||||||
import extract from '../utils/extract';
|
|
||||||
import Mithril from 'mithril';
|
|
||||||
|
|
||||||
export interface AlertAttrs extends ComponentAttrs {
|
|
||||||
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
|
|
||||||
type?: string;
|
|
||||||
/** An array of controls to show in the alert. */
|
|
||||||
controls?: Mithril.Children;
|
|
||||||
/** Whether or not the alert can be dismissed. */
|
|
||||||
dismissible?: boolean;
|
|
||||||
/** A callback to run when the alert is dismissed */
|
|
||||||
ondismiss?: Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Alert` component represents an alert box, which contains a message,
|
|
||||||
* some controls, and may be dismissible.
|
|
||||||
*/
|
|
||||||
export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<T> {
|
|
||||||
view(vnode: Mithril.Vnode) {
|
|
||||||
const attrs = Object.assign({}, this.attrs);
|
|
||||||
|
|
||||||
const type = extract(attrs, 'type');
|
|
||||||
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
|
|
||||||
|
|
||||||
const content = extract(attrs, 'content') || vnode.children;
|
|
||||||
const controls = (extract(attrs, 'controls') || []) as Mithril.ChildArray;
|
|
||||||
|
|
||||||
// If the alert is meant to be dismissible (which is the case by default),
|
|
||||||
// then we will create a dismiss button to append as the final control in
|
|
||||||
// the alert.
|
|
||||||
const dismissible = extract(attrs, 'dismissible');
|
|
||||||
const ondismiss = extract(attrs, 'ondismiss');
|
|
||||||
const dismissControl = [];
|
|
||||||
|
|
||||||
if (dismissible || dismissible === undefined) {
|
|
||||||
dismissControl.push(<Button icon="fas fa-times" className="Button Button--link Button--icon Alert-dismiss" onclick={ondismiss} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...attrs}>
|
|
||||||
<span className="Alert-body">{content}</span>
|
|
||||||
<ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -6,23 +6,70 @@ import Alert from './Alert';
|
|||||||
* be shown and dismissed.
|
* be shown and dismissed.
|
||||||
*/
|
*/
|
||||||
export default class AlertManager extends Component {
|
export default class AlertManager extends Component {
|
||||||
oninit(vnode) {
|
init() {
|
||||||
super.oninit(vnode);
|
/**
|
||||||
|
* An array of Alert components which are currently showing.
|
||||||
this.state = this.attrs.state;
|
*
|
||||||
|
* @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.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}>
|
|
||||||
{alert.children}
|
|
||||||
</alert.componentClass>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
// Since this component is 'above' the content of the page (that is, it is a
|
||||||
|
// part of the global UI that persists between routes), we will flag the DOM
|
||||||
|
// to be retained across route changes.
|
||||||
|
context.retain = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,30 +6,34 @@ import extract from '../utils/extract';
|
|||||||
* The `Badge` component represents a user/discussion badge, indicating some
|
* The `Badge` component represents a user/discussion badge, indicating some
|
||||||
* status (e.g. a discussion is stickied, a user is an admin).
|
* status (e.g. a discussion is stickied, a user is an admin).
|
||||||
*
|
*
|
||||||
* A badge may have the following special attrs:
|
* A badge may have the following special props:
|
||||||
*
|
*
|
||||||
* - `type` The type of badge this is. This will be used to give the badge a
|
* - `type` The type of badge this is. This will be used to give the badge a
|
||||||
* class name of `Badge--{type}`.
|
* class name of `Badge--{type}`.
|
||||||
* - `icon` The name of an icon to show inside the badge.
|
* - `icon` The name of an icon to show inside the badge.
|
||||||
* - `label`
|
* - `label`
|
||||||
*
|
*
|
||||||
* All other attrs will be assigned as attributes on the badge element.
|
* All other props will be assigned as attributes on the badge element.
|
||||||
*/
|
*/
|
||||||
export default class Badge extends Component {
|
export default class Badge extends Component {
|
||||||
view() {
|
view() {
|
||||||
const attrs = Object.assign({}, this.attrs);
|
const attrs = Object.assign({}, this.props);
|
||||||
const type = extract(attrs, 'type');
|
const type = extract(attrs, 'type');
|
||||||
const iconName = extract(attrs, 'icon');
|
const iconName = extract(attrs, 'icon');
|
||||||
|
|
||||||
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
config(isInitialized) {
|
||||||
super.oncreate(vnode);
|
if (isInitialized) return;
|
||||||
|
|
||||||
if (this.attrs.label) this.$().tooltip();
|
if (this.props.label) this.$().tooltip({container: 'body'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,12 @@
|
|||||||
import Component from '../Component';
|
import Component from '../Component';
|
||||||
import icon from '../helpers/icon';
|
import icon from '../helpers/icon';
|
||||||
import classList from '../utils/classList';
|
|
||||||
import extract from '../utils/extract';
|
import extract from '../utils/extract';
|
||||||
import extractText from '../utils/extractText';
|
import extractText from '../utils/extractText';
|
||||||
import LoadingIndicator from './LoadingIndicator';
|
import LoadingIndicator from './LoadingIndicator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Button` component defines an element which, when clicked, performs an
|
* The `Button` component defines an element which, when clicked, performs an
|
||||||
* action.
|
* action. The button may have the following special props:
|
||||||
*
|
|
||||||
* ### Attrs
|
|
||||||
*
|
*
|
||||||
* - `icon` The name of the icon class. If specified, the button will be given a
|
* - `icon` The name of the icon class. If specified, the button will be given a
|
||||||
* 'has-icon' class name.
|
* 'has-icon' class name.
|
||||||
@@ -18,38 +15,35 @@ import LoadingIndicator from './LoadingIndicator';
|
|||||||
* removed.
|
* removed.
|
||||||
* - `loading` Whether or not the button should be in a disabled loading state.
|
* - `loading` Whether or not the button should be in a disabled loading state.
|
||||||
*
|
*
|
||||||
* All other attrs will be assigned as attributes on the button element.
|
* All other props will be assigned as attributes on the button element.
|
||||||
*
|
*
|
||||||
* Note that a Button has no default class names. This is because a Button can
|
* Note that a Button has no default class names. This is because a Button can
|
||||||
* be used to represent any generic clickable control, like a menu item.
|
* be used to represent any generic clickable control, like a menu item.
|
||||||
*/
|
*/
|
||||||
export default class Button extends Component {
|
export default class Button extends Component {
|
||||||
view(vnode) {
|
view() {
|
||||||
const attrs = Object.assign({}, this.attrs);
|
const attrs = Object.assign({}, this.props);
|
||||||
|
|
||||||
|
delete attrs.children;
|
||||||
|
|
||||||
|
attrs.className = attrs.className || '';
|
||||||
attrs.type = attrs.type || 'button';
|
attrs.type = attrs.type || 'button';
|
||||||
|
|
||||||
// If a tooltip was provided for buttons without additional content, we also
|
|
||||||
// use this tooltip as text for screen readers
|
|
||||||
if (attrs.title && !vnode.children) {
|
|
||||||
attrs['aria-label'] = attrs.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If nothing else is provided, we use the textual button content as tooltip
|
// If nothing else is provided, we use the textual button content as tooltip
|
||||||
if (!attrs.title && vnode.children) {
|
if (!attrs.title && this.props.children) {
|
||||||
attrs.title = extractText(vnode.children);
|
attrs.title = extractText(this.props.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconName = extract(attrs, 'icon');
|
const iconName = extract(attrs, 'icon');
|
||||||
|
if (iconName) attrs.className += ' hasIcon';
|
||||||
|
|
||||||
const loading = extract(attrs, 'loading');
|
const loading = extract(attrs, 'loading');
|
||||||
if (attrs.disabled || loading) {
|
if (attrs.disabled || loading) {
|
||||||
|
attrs.className += ' disabled' + (loading ? ' loading' : '');
|
||||||
delete attrs.onclick;
|
delete attrs.onclick;
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs.className = classList([attrs.className, iconName && 'hasIcon', (attrs.disabled || loading) && 'disabled', loading && 'loading']);
|
return <button {...attrs}>{this.getButtonContent()}</button>;
|
||||||
|
|
||||||
return <button {...attrs}>{this.getButtonContent(vnode.children)}</button>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,13 +52,13 @@ export default class Button extends Component {
|
|||||||
* @return {*}
|
* @return {*}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getButtonContent(children) {
|
getButtonContent() {
|
||||||
const iconName = this.attrs.icon;
|
const iconName = this.props.icon;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
iconName && iconName !== true ? icon(iconName, { className: 'Button-icon' }) : '',
|
iconName && iconName !== true ? icon(iconName, {className: 'Button-icon'}) : '',
|
||||||
children ? <span className="Button-label">{children}</span> : '',
|
this.props.children ? <span className="Button-label">{this.props.children}</span> : '',
|
||||||
this.attrs.loading ? <LoadingIndicator size="tiny" className="LoadingIndicator--inline" /> : '',
|
this.props.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : ''
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,40 +1,44 @@
|
|||||||
import Component from '../Component';
|
import Component from '../Component';
|
||||||
import LoadingIndicator from './LoadingIndicator';
|
import LoadingIndicator from './LoadingIndicator';
|
||||||
import icon from '../helpers/icon';
|
import icon from '../helpers/icon';
|
||||||
import classList from '../utils/classList';
|
|
||||||
import withAttr from '../utils/withAttr';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Checkbox` component defines a checkbox input.
|
* The `Checkbox` component defines a checkbox input.
|
||||||
*
|
*
|
||||||
* ### Attrs
|
* ### Props
|
||||||
*
|
*
|
||||||
* - `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 {
|
||||||
view(vnode) {
|
init() {
|
||||||
// Sometimes, false is stored in the DB as '0'. This is a temporary
|
/**
|
||||||
// conversion layer until a more robust settings encoding is introduced
|
* Whether or not the checkbox's value is in the process of being saved.
|
||||||
if (this.attrs.state === '0') this.attrs.state = false;
|
*
|
||||||
|
* @type {Boolean}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
const className = classList([
|
view() {
|
||||||
'Checkbox',
|
let className = 'Checkbox ' + (this.props.state ? 'on' : 'off') + ' ' + (this.props.className || '');
|
||||||
this.attrs.state ? 'on' : 'off',
|
if (this.loading) className += ' loading';
|
||||||
this.attrs.className,
|
if (this.props.disabled) className += ' disabled';
|
||||||
this.attrs.loading && 'loading',
|
|
||||||
this.attrs.disabled && 'disabled',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={className}>
|
<label className={className}>
|
||||||
<input type="checkbox" checked={this.attrs.state} disabled={this.attrs.disabled} onchange={withAttr('checked', this.onchange.bind(this))} />
|
<input type="checkbox"
|
||||||
<div className="Checkbox-display">{this.getDisplay()}</div>
|
checked={this.props.state}
|
||||||
{vnode.children}
|
disabled={this.props.disabled}
|
||||||
|
onchange={m.withAttr('checked', this.onchange.bind(this))}/>
|
||||||
|
<div className="Checkbox-display">
|
||||||
|
{this.getDisplay()}
|
||||||
|
</div>
|
||||||
|
{this.props.children}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,7 +50,9 @@ export default class Checkbox extends Component {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getDisplay() {
|
getDisplay() {
|
||||||
return this.attrs.loading ? <LoadingIndicator size="tiny" /> : icon(this.attrs.state ? 'fas fa-check' : 'fas fa-times');
|
return this.loading
|
||||||
|
? LoadingIndicator.component({size: 'tiny'})
|
||||||
|
: icon(this.props.state ? 'fas fa-check' : 'fas fa-times');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,6 +62,6 @@ export default class Checkbox extends Component {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
onchange(checked) {
|
onchange(checked) {
|
||||||
if (this.attrs.onchange) this.attrs.onchange(checked, this);
|
if (this.props.onchange) this.props.onchange(checked, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,40 +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.
|
|
||||||
*
|
|
||||||
* ### Attrs
|
|
||||||
*
|
|
||||||
* - `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 {
|
|
||||||
handler() {
|
|
||||||
return this.attrs.when() || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
this.boundHandler = this.handler.bind(this);
|
|
||||||
$(window).on('beforeunload', this.boundHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
onremove() {
|
|
||||||
$(window).off('beforeunload', this.boundHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
// To avoid having to render another wrapping <div> here, we assume that
|
|
||||||
// this component is only wrapped around a single element / component.
|
|
||||||
return vnode.children[0];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -6,7 +6,7 @@ import listItems from '../helpers/listItems';
|
|||||||
* The `Dropdown` component displays a button which, when clicked, shows a
|
* The `Dropdown` component displays a button which, when clicked, shows a
|
||||||
* dropdown menu beneath it.
|
* dropdown menu beneath it.
|
||||||
*
|
*
|
||||||
* ### Attrs
|
* ### Props
|
||||||
*
|
*
|
||||||
* - `buttonClassName` A class name to apply to the dropdown toggle button.
|
* - `buttonClassName` A class name to apply to the dropdown toggle button.
|
||||||
* - `menuClassName` A class name to apply to the dropdown menu.
|
* - `menuClassName` A class name to apply to the dropdown menu.
|
||||||
@@ -19,33 +19,33 @@ import listItems from '../helpers/listItems';
|
|||||||
* The children will be displayed as a list inside of the dropdown menu.
|
* The children will be displayed as a list inside of the dropdown menu.
|
||||||
*/
|
*/
|
||||||
export default class Dropdown extends Component {
|
export default class Dropdown extends Component {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
attrs.className = attrs.className || '';
|
super.initProps(props);
|
||||||
attrs.buttonClassName = attrs.buttonClassName || '';
|
|
||||||
attrs.menuClassName = attrs.menuClassName || '';
|
props.className = props.className || '';
|
||||||
attrs.label = attrs.label || '';
|
props.buttonClassName = props.buttonClassName || '';
|
||||||
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
|
props.menuClassName = props.menuClassName || '';
|
||||||
|
props.label = props.label || '';
|
||||||
|
props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'fas fa-caret-down';
|
||||||
}
|
}
|
||||||
|
|
||||||
oninit(vnode) {
|
init() {
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.showing = false;
|
this.showing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view() {
|
||||||
const items = vnode.children ? listItems(vnode.children) : [];
|
const items = this.props.children ? listItems(this.props.children) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'ButtonGroup Dropdown dropdown ' + this.attrs.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
|
<div className={'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
|
||||||
{this.getButton(vnode.children)}
|
{this.getButton()}
|
||||||
{this.getMenu(items)}
|
{this.getMenu(items)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
config(isInitialized) {
|
||||||
super.oncreate(vnode);
|
if (isInitialized) return;
|
||||||
|
|
||||||
// When opening the dropdown menu, work out if the menu goes beyond the
|
// When opening the dropdown menu, work out if the menu goes beyond the
|
||||||
// bottom of the viewport. If it does, we will apply class to make it show
|
// bottom of the viewport. If it does, we will apply class to make it show
|
||||||
@@ -53,8 +53,8 @@ export default class Dropdown extends Component {
|
|||||||
this.$().on('shown.bs.dropdown', () => {
|
this.$().on('shown.bs.dropdown', () => {
|
||||||
this.showing = true;
|
this.showing = true;
|
||||||
|
|
||||||
if (this.attrs.onshow) {
|
if (this.props.onshow) {
|
||||||
this.attrs.onshow();
|
this.props.onshow();
|
||||||
}
|
}
|
||||||
|
|
||||||
m.redraw();
|
m.redraw();
|
||||||
@@ -64,20 +64,26 @@ 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', () => {
|
||||||
this.showing = false;
|
this.showing = false;
|
||||||
|
|
||||||
if (this.attrs.onhide) {
|
if (this.props.onhide) {
|
||||||
this.attrs.onhide();
|
this.props.onhide();
|
||||||
}
|
}
|
||||||
|
|
||||||
m.redraw();
|
m.redraw();
|
||||||
@@ -90,10 +96,13 @@ export default class Dropdown extends Component {
|
|||||||
* @return {*}
|
* @return {*}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getButton(children) {
|
getButton() {
|
||||||
return (
|
return (
|
||||||
<button className={'Dropdown-toggle ' + this.attrs.buttonClassName} data-toggle="dropdown" onclick={this.attrs.onclick}>
|
<button
|
||||||
{this.getButtonContent(children)}
|
className={'Dropdown-toggle ' + this.props.buttonClassName}
|
||||||
|
data-toggle="dropdown"
|
||||||
|
onclick={this.props.onclick}>
|
||||||
|
{this.getButtonContent()}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,15 +113,19 @@ export default class Dropdown extends Component {
|
|||||||
* @return {*}
|
* @return {*}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getButtonContent(children) {
|
getButtonContent() {
|
||||||
return [
|
return [
|
||||||
this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : '',
|
this.props.icon ? icon(this.props.icon, {className: 'Button-icon'}) : '',
|
||||||
<span className="Button-label">{this.attrs.label}</span>,
|
<span className="Button-label">{this.props.label}</span>,
|
||||||
this.attrs.caretIcon ? icon(this.attrs.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.attrs.menuClassName}>{items}</ul>;
|
return (
|
||||||
|
<ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
|
||||||
|
{items}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,11 +11,11 @@ import listItems from '../helpers/listItems';
|
|||||||
* The children should be an array of items to show in the fieldset.
|
* The children should be an array of items to show in the fieldset.
|
||||||
*/
|
*/
|
||||||
export default class FieldSet extends Component {
|
export default class FieldSet extends Component {
|
||||||
view(vnode) {
|
view() {
|
||||||
return (
|
return (
|
||||||
<fieldset className={this.attrs.className}>
|
<fieldset className={this.props.className}>
|
||||||
<legend>{this.attrs.label}</legend>
|
<legend>{this.props.label}</legend>
|
||||||
<ul>{listItems(vnode.children)}</ul>
|
<ul>{listItems(this.props.children)}</ul>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import Badge from './Badge';
|
import Badge from './Badge';
|
||||||
|
|
||||||
export default class GroupBadge extends Badge {
|
export default class GroupBadge extends Badge {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
super.initAttrs(attrs);
|
super.initProps(props);
|
||||||
|
|
||||||
if (attrs.group) {
|
if (props.group) {
|
||||||
attrs.icon = attrs.group.icon();
|
props.icon = props.group.icon();
|
||||||
attrs.style = { backgroundColor: attrs.group.color() };
|
props.style = {backgroundColor: props.group.color()};
|
||||||
attrs.label = typeof attrs.label === 'undefined' ? attrs.group.nameSingular() : attrs.label;
|
props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label;
|
||||||
attrs.type = 'group--' + attrs.group.id();
|
props.type = 'group--' + props.group.id();
|
||||||
|
|
||||||
delete attrs.group;
|
delete props.group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,47 +0,0 @@
|
|||||||
import Component from '../Component';
|
|
||||||
import extract from '../utils/extract';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The link component enables both internal and external links.
|
|
||||||
* It will return a regular HTML link for any links to external sites,
|
|
||||||
* and it will use Mithril's m.route.Link for any internal links.
|
|
||||||
*
|
|
||||||
* Links will default to internal; the 'external' attr must be set to
|
|
||||||
* `true` for the link to be external.
|
|
||||||
*/
|
|
||||||
export default class Link extends Component {
|
|
||||||
view(vnode) {
|
|
||||||
let { options = {}, ...attrs } = vnode.attrs;
|
|
||||||
|
|
||||||
attrs.href = attrs.href || '';
|
|
||||||
|
|
||||||
// For some reason, m.route.Link does not like vnode.text, so if present, we
|
|
||||||
// need to convert it to text vnodes and store it in children.
|
|
||||||
const children = vnode.children || { tag: '#', children: vnode.text };
|
|
||||||
|
|
||||||
if (attrs.external) {
|
|
||||||
return <a {...attrs}>{children}</a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the href URL of the link is the same as the current page path
|
|
||||||
// we will not add a new entry to the browser history.
|
|
||||||
// This allows us to still refresh the Page component
|
|
||||||
// without adding endless history entries.
|
|
||||||
if (attrs.href === m.route.get()) {
|
|
||||||
if (!('replace' in options)) options.replace = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mithril 2 does not completely rerender the page if a route change leads to the same route
|
|
||||||
// (or the same component handling a different route).
|
|
||||||
// Here, the `force` parameter will use Mithril's key system to force a full rerender
|
|
||||||
// see https://mithril.js.org/route.html#key-parameter
|
|
||||||
if (extract(attrs, 'force')) {
|
|
||||||
if (!('state' in options)) options.state = {};
|
|
||||||
if (!('key' in options.state)) options.state.key = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs.options = options;
|
|
||||||
|
|
||||||
return <m.route.Link {...attrs}>{children}</m.route.Link>;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,43 +1,40 @@
|
|||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import Link from './Link';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `LinkButton` component defines a `Button` which links to a route.
|
* The `LinkButton` component defines a `Button` which links to a route.
|
||||||
*
|
*
|
||||||
* ### Attrs
|
* ### Props
|
||||||
*
|
*
|
||||||
* All of the attrs accepted by `Button`, plus:
|
* All of the props accepted by `Button`, plus:
|
||||||
*
|
*
|
||||||
* - `active` Whether or not the page that this button links to is currently
|
* - `active` Whether or not the page that this button links to is currently
|
||||||
* active.
|
* active.
|
||||||
* - `href` The URL to link to. If the current URL `m.route()` matches this,
|
* - `href` The URL to link to. If the current URL `m.route()` matches this,
|
||||||
* the `active` prop will automatically be set to true.
|
* the `active` prop will automatically be set to true.
|
||||||
* - `force` Whether the page should be fully rerendered. Defaults to `true`.
|
|
||||||
*/
|
*/
|
||||||
export default class LinkButton extends Button {
|
export default class LinkButton extends Button {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
super.initAttrs(attrs);
|
props.active = this.isActive(props);
|
||||||
|
props.config = props.config || m.route;
|
||||||
attrs.active = this.isActive(attrs);
|
|
||||||
if (attrs.force === undefined) attrs.force = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view() {
|
||||||
const vdom = super.view(vnode);
|
const vdom = super.view();
|
||||||
|
|
||||||
vdom.tag = Link;
|
vdom.tag = 'a';
|
||||||
vdom.attrs.active = String(vdom.attrs.active);
|
|
||||||
|
|
||||||
return vdom;
|
return vdom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether a component with the given attrs is 'active'.
|
* Determine whether a component with the given props is 'active'.
|
||||||
*
|
*
|
||||||
* @param {Object} attrs
|
* @param {Object} props
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
static isActive(attrs) {
|
static isActive(props) {
|
||||||
return typeof attrs.active !== 'undefined' ? attrs.active : m.route.get() === attrs.href;
|
return typeof props.active !== 'undefined'
|
||||||
|
? props.active
|
||||||
|
: m.route() === props.href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,17 +2,16 @@ import Component from '../Component';
|
|||||||
import { Spinner } from 'spin.js';
|
import { Spinner } from 'spin.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `LoadingIndicator` component displays a loading spinner with spin.js.
|
* The `LoadingIndicator` component displays a loading spinner with spin.js. It
|
||||||
*
|
* may have the following special props:
|
||||||
* ### Attrs
|
|
||||||
*
|
*
|
||||||
* - `size` The spin.js size preset to use. Defaults to 'small'.
|
* - `size` The spin.js size preset to use. Defaults to 'small'.
|
||||||
*
|
*
|
||||||
* All other attrs will be assigned as attributes on the DOM element.
|
* All other props will be assigned as attributes on the element.
|
||||||
*/
|
*/
|
||||||
export default class LoadingIndicator extends Component {
|
export default class LoadingIndicator extends Component {
|
||||||
view() {
|
view() {
|
||||||
const attrs = Object.assign({}, this.attrs);
|
const attrs = Object.assign({}, this.props);
|
||||||
|
|
||||||
attrs.className = 'LoadingIndicator ' + (attrs.className || '');
|
attrs.className = 'LoadingIndicator ' + (attrs.className || '');
|
||||||
delete attrs.size;
|
delete attrs.size;
|
||||||
@@ -20,12 +19,12 @@ export default class LoadingIndicator extends Component {
|
|||||||
return <div {...attrs}>{m.trust(' ')}</div>;
|
return <div {...attrs}>{m.trust(' ')}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
config(isInitialized) {
|
||||||
super.oncreate(vnode);
|
if (isInitialized) return;
|
||||||
|
|
||||||
const options = { zIndex: 'auto', color: this.$().css('color') };
|
const options = { zIndex: 'auto', color: this.$().css('color') };
|
||||||
|
|
||||||
switch (this.attrs.size) {
|
switch (this.props.size) {
|
||||||
case 'large':
|
case 'large':
|
||||||
Object.assign(options, { lines: 10, length: 8, width: 4, radius: 8 });
|
Object.assign(options, { lines: 10, length: 8, width: 4, radius: 8 });
|
||||||
break;
|
break;
|
||||||
|
@@ -9,63 +9,39 @@ import Button from './Button';
|
|||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
export default class Modal extends Component {
|
export default class Modal extends Component {
|
||||||
/**
|
init() {
|
||||||
* Determine whether or not the modal should be dismissible via an 'x' button.
|
/**
|
||||||
*/
|
* An alert component to show below the header.
|
||||||
static isDismissible = true;
|
*
|
||||||
|
* @type {Alert}
|
||||||
/**
|
*/
|
||||||
* Attributes for an alert component to show below the header.
|
this.alert = null;
|
||||||
*
|
|
||||||
* @type {object}
|
|
||||||
*/
|
|
||||||
alertAttrs = null;
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
this.attrs.animateShow(() => this.onready());
|
|
||||||
}
|
|
||||||
|
|
||||||
onbeforeremove() {
|
|
||||||
// If the global modal state currently contains a modal,
|
|
||||||
// we've just opened up a new one, and accordingly,
|
|
||||||
// we don't need to show a hide animation.
|
|
||||||
if (!this.attrs.state.modal) {
|
|
||||||
this.attrs.animateHide();
|
|
||||||
// Here, we ensure that the animation has time to complete.
|
|
||||||
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
|
|
||||||
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
@@ -74,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.
|
||||||
@@ -88,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.
|
||||||
@@ -96,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.
|
||||||
@@ -112,11 +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() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the modal.
|
* Hide the modal.
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
this.attrs.state.close();
|
app.modal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,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
|
||||||
@@ -6,53 +7,100 @@ import Component from '../Component';
|
|||||||
* overwrite the previous one.
|
* overwrite the previous one.
|
||||||
*/
|
*/
|
||||||
export default class ModalManager extends Component {
|
export default class ModalManager extends Component {
|
||||||
view() {
|
init() {
|
||||||
const modal = this.attrs.state.modal;
|
this.showing = false;
|
||||||
|
this.component = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="ModalManager modal fade">
|
<div className="ModalManager modal fade">
|
||||||
{modal
|
{this.component && this.component.render()}
|
||||||
? modal.componentClass.component({
|
|
||||||
...modal.attrs,
|
|
||||||
animateShow: this.animateShow.bind(this),
|
|
||||||
animateHide: this.animateHide.bind(this),
|
|
||||||
state: this.attrs.state,
|
|
||||||
})
|
|
||||||
: ''}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
config(isInitialized, context) {
|
||||||
super.oncreate(vnode);
|
if (isInitialized) return;
|
||||||
|
|
||||||
// Ensure the modal state is notified about a closed modal, even when the
|
// Since this component is 'above' the content of the page (that is, it is a
|
||||||
// DOM-based Bootstrap JavaScript code triggered the closing of the modal,
|
// part of the global UI that persists between routes), we will flag the DOM
|
||||||
// e.g. via ESC key or a click on the modal backdrop.
|
// to be retained across route changes.
|
||||||
this.$().on('hidden.bs.modal', this.attrs.state.close.bind(this.attrs.state));
|
context.retain = true;
|
||||||
}
|
|
||||||
|
|
||||||
animateShow(readyCallback) {
|
|
||||||
const dismissible = !!this.attrs.state.modal.componentClass.isDismissible;
|
|
||||||
|
|
||||||
// If we are opening this modal while another modal is already open,
|
|
||||||
// the shown event will not run, because the modal is already open.
|
|
||||||
// So, we need to manually trigger the readyCallback.
|
|
||||||
if (this.$().hasClass('in')) {
|
|
||||||
readyCallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
/**
|
||||||
this.$().modal('hide');
|
* 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.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.$());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ import LinkButton from './LinkButton';
|
|||||||
* If the app has a pane, it will also include a 'pin' button which toggles the
|
* If the app has a pane, it will also include a 'pin' button which toggles the
|
||||||
* pinned state of the pane.
|
* pinned state of the pane.
|
||||||
*
|
*
|
||||||
* Accepts the following attrs:
|
* Accepts the following props:
|
||||||
*
|
*
|
||||||
* - `className` The name of a class to set on the root element.
|
* - `className` The name of a class to set on the root element.
|
||||||
* - `drawer` Whether or not to show a button to toggle the app's drawer if
|
* - `drawer` Whether or not to show a button to toggle the app's drawer if
|
||||||
@@ -19,19 +19,26 @@ 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.attrs.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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
// Since this component is 'above' the content of the page (that is, it is a
|
||||||
|
// part of the global UI that persists between routes), we will flag the DOM
|
||||||
|
// to be retained across route changes.
|
||||||
|
context.retain = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the back button.
|
* Get the back button.
|
||||||
*
|
*
|
||||||
@@ -39,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({
|
||||||
@@ -47,11 +54,12 @@ export default class Navigation extends Component {
|
|||||||
href: history.backUrl(),
|
href: history.backUrl(),
|
||||||
icon: 'fas fa-chevron-left',
|
icon: 'fas fa-chevron-left',
|
||||||
title: previous.title,
|
title: previous.title,
|
||||||
onclick: (e) => {
|
config: () => {},
|
||||||
|
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();
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,18 +88,19 @@ export default class Navigation extends Component {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getDrawerButton() {
|
getDrawerButton() {
|
||||||
if (!this.attrs.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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,51 +0,0 @@
|
|||||||
import Component from '../Component';
|
|
||||||
import PageState from '../states/PageState';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Page` component
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export default class Page extends Component {
|
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
app.previous = app.current;
|
|
||||||
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
|
|
||||||
|
|
||||||
app.drawer.hide();
|
|
||||||
app.modal.close();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class name to apply to the body while the route is active.
|
|
||||||
*
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
this.bodyClass = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether we should scroll to the top of the page when its rendered.
|
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
|
||||||
this.scrollTopOnCreate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
if (this.bodyClass) {
|
|
||||||
$('#app').addClass(this.bodyClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.scrollTopOnCreate) {
|
|
||||||
$(window).scrollTop(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onremove() {
|
|
||||||
if (this.bodyClass) {
|
|
||||||
$('#app').removeClass(this.bodyClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,7 +4,7 @@ import Component from '../Component';
|
|||||||
* The `Placeholder` component displays a muted text with some call to action,
|
* The `Placeholder` component displays a muted text with some call to action,
|
||||||
* usually used as an empty state.
|
* usually used as an empty state.
|
||||||
*
|
*
|
||||||
* ### Attrs
|
* ### Props
|
||||||
*
|
*
|
||||||
* - `text`
|
* - `text`
|
||||||
*/
|
*/
|
||||||
@@ -12,7 +12,7 @@ export default class Placeholder extends Component {
|
|||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="Placeholder">
|
<div className="Placeholder">
|
||||||
<p>{this.attrs.text}</p>
|
<p>{this.props.text}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,42 +0,0 @@
|
|||||||
import Modal from './Modal';
|
|
||||||
|
|
||||||
export default class RequestErrorModal extends Modal {
|
|
||||||
className() {
|
|
||||||
return 'RequestErrorModal Modal--large';
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
return this.attrs.error.xhr ? `${this.attrs.error.xhr.status} ${this.attrs.error.xhr.statusText}` : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
const { error, formattedError } = this.attrs;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
const json = error.response || JSON.parse(error.responseText);
|
|
||||||
|
|
||||||
responseText = JSON.stringify(json, null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
responseText = error.responseText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="Modal-body">
|
|
||||||
<pre>
|
|
||||||
{this.attrs.error.options.method} {this.attrs.error.options.url}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
{responseText}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,33 +1,24 @@
|
|||||||
import Component from '../Component';
|
import Component from '../Component';
|
||||||
import icon from '../helpers/icon';
|
import icon from '../helpers/icon';
|
||||||
import withAttr from '../utils/withAttr';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Select` component displays a <select> input, surrounded with some extra
|
* The `Select` component displays a <select> input, surrounded with some extra
|
||||||
* elements for styling. It accepts the following attrs:
|
* elements for styling. It accepts the following props:
|
||||||
*
|
*
|
||||||
* - `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.attrs;
|
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 ? 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,49 +1,34 @@
|
|||||||
import Dropdown from './Dropdown';
|
import Dropdown from './Dropdown';
|
||||||
import icon from '../helpers/icon';
|
import icon from '../helpers/icon';
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines via a vnode is currently "active".
|
|
||||||
* Due to changes in Mithril 2, attrs will not be instantiated until AFTER view()
|
|
||||||
* is initially called on the parent component, so we can not always depend on the
|
|
||||||
* active attr to determine which element should be displayed as the "active child".
|
|
||||||
*
|
|
||||||
* This is a temporary patch, and as so, is not exported / placed in utils.
|
|
||||||
*/
|
|
||||||
function isActive(vnode) {
|
|
||||||
const tag = vnode.tag;
|
|
||||||
|
|
||||||
if ('initAttrs' in tag) {
|
|
||||||
tag.initAttrs(vnode.attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'isActive' in tag ? tag.isActive(vnode.attrs) : vnode.attrs.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
|
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
|
||||||
* button's label is set as the label of the first child which has a truthy
|
* button's label is set as the label of the first child which has a truthy
|
||||||
* `active` prop.
|
* `active` prop.
|
||||||
*
|
*
|
||||||
* ### Attrs
|
* ### Props
|
||||||
*
|
*
|
||||||
* - `caretIcon`
|
* - `caretIcon`
|
||||||
* - `defaultLabel`
|
* - `defaultLabel`
|
||||||
*/
|
*/
|
||||||
export default class SelectDropdown extends Dropdown {
|
export default class SelectDropdown extends Dropdown {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-sort';
|
props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'fas fa-sort';
|
||||||
|
|
||||||
super.initAttrs(attrs);
|
super.initProps(props);
|
||||||
|
|
||||||
attrs.className += ' Dropdown--select';
|
props.className += ' Dropdown--select';
|
||||||
}
|
}
|
||||||
|
|
||||||
getButtonContent(children) {
|
getButtonContent() {
|
||||||
const activeChild = children.find(isActive);
|
const activeChild = this.props.children.filter(child => child.props.active)[0];
|
||||||
let label = (activeChild && activeChild.children) || this.attrs.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.attrs.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"/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,27 +7,29 @@ import icon from '../helpers/icon';
|
|||||||
* is displayed as its own button prior to the toggle button.
|
* is displayed as its own button prior to the toggle button.
|
||||||
*/
|
*/
|
||||||
export default class SplitDropdown extends Dropdown {
|
export default class SplitDropdown extends Dropdown {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
super.initAttrs(attrs);
|
super.initProps(props);
|
||||||
|
|
||||||
attrs.className += ' Dropdown--split';
|
props.className += ' Dropdown--split';
|
||||||
attrs.menuClassName += ' Dropdown-menu--right';
|
props.menuClassName += ' Dropdown-menu--right';
|
||||||
}
|
}
|
||||||
|
|
||||||
getButton(children) {
|
getButton() {
|
||||||
// Make a copy of the attrs of the first child component. We will assign
|
// Make a copy of the props of the first child component. We will assign
|
||||||
// these attrs to a new button, so that it has exactly the same behaviour as
|
// these props to a new button, so that it has exactly the same behaviour as
|
||||||
// the first child.
|
// the first child.
|
||||||
const firstChild = this.getFirstChild(children);
|
const firstChild = this.getFirstChild();
|
||||||
const buttonAttrs = Object.assign({}, firstChild.attrs);
|
const buttonProps = Object.assign({}, firstChild.props);
|
||||||
buttonAttrs.className = (buttonAttrs.className || '') + ' SplitDropdown-button Button ' + this.attrs.buttonClassName;
|
buttonProps.className = (buttonProps.className || '') + ' SplitDropdown-button Button ' + this.props.buttonClassName;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Button.component(buttonAttrs, firstChild.children),
|
Button.component(buttonProps),
|
||||||
<button className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName} data-toggle="dropdown">
|
<button
|
||||||
{icon(this.attrs.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>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +40,8 @@ export default class SplitDropdown extends Dropdown {
|
|||||||
* @return {*}
|
* @return {*}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getFirstChild(children) {
|
getFirstChild() {
|
||||||
let firstChild = children;
|
let firstChild = this.props.children;
|
||||||
|
|
||||||
while (firstChild instanceof Array) firstChild = firstChild[0];
|
while (firstChild instanceof Array) firstChild = firstChild[0];
|
||||||
|
|
||||||
|
@@ -5,13 +5,13 @@ import Checkbox from './Checkbox';
|
|||||||
* a tick/cross one.
|
* a tick/cross one.
|
||||||
*/
|
*/
|
||||||
export default class Switch extends Checkbox {
|
export default class Switch extends Checkbox {
|
||||||
static initAttrs(attrs) {
|
static initProps(props) {
|
||||||
super.initAttrs(attrs);
|
super.initProps(props);
|
||||||
|
|
||||||
attrs.className = (attrs.className || '') + ' Checkbox--switch';
|
props.className = (props.className || '') + ' Checkbox--switch';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplay() {
|
getDisplay() {
|
||||||
return this.attrs.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 = [];
|
||||||
@@ -31,11 +31,11 @@ export default class Model {
|
|||||||
if (this.model) {
|
if (this.model) {
|
||||||
app.store.models[this.type] = this.model;
|
app.store.models[this.type] = this.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -10,4 +10,4 @@ export default class PostTypes {
|
|||||||
extend(app, extension) {
|
extend(app, extension) {
|
||||||
Object.assign(app.postComponents, this.postComponents);
|
Object.assign(app.postComponents, this.postComponents);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -10,4 +10,4 @@ export default class Routes {
|
|||||||
extend(app, extension) {
|
extend(app, extension) {
|
||||||
Object.assign(app.routes, this.routes);
|
Object.assign(app.routes, this.routes);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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} alt="" />;
|
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
12
js/src/common/helpers/icon.js
Normal file
12
js/src/common/helpers/icon.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* The `icon` helper displays an icon.
|
||||||
|
*
|
||||||
|
* @param {String} fontClass The full icon class, prefix and the icon’s name.
|
||||||
|
* @param {Object} attrs Any other attributes to apply.
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
export default function icon(fontClass, attrs = {}) {
|
||||||
|
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
|
||||||
|
|
||||||
|
return <i {...attrs}/>;
|
||||||
|
}
|
@@ -1,13 +0,0 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `icon` helper displays an icon.
|
|
||||||
*
|
|
||||||
* @param fontClass The full icon class, prefix and the icon’s name.
|
|
||||||
* @param attrs Any other attributes to apply.
|
|
||||||
*/
|
|
||||||
export default function icon(fontClass: string, attrs: Mithril.Attributes = {}): Mithril.Vnode {
|
|
||||||
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
|
|
||||||
|
|
||||||
return <i {...attrs} />;
|
|
||||||
}
|
|
@@ -2,14 +2,14 @@ import Separator from '../components/Separator';
|
|||||||
import classList from '../utils/classList';
|
import classList from '../utils/classList';
|
||||||
|
|
||||||
function isSeparator(item) {
|
function isSeparator(item) {
|
||||||
return item.tag === Separator;
|
return item && item.component === Separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withoutUnnecessarySeparators(items) {
|
function withoutUnnecessarySeparators(items) {
|
||||||
const newItems = [];
|
const newItems = [];
|
||||||
let prevItem;
|
let prevItem;
|
||||||
|
|
||||||
items.filter(Boolean).forEach((item, i) => {
|
items.forEach((item, i) => {
|
||||||
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
|
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
|
||||||
prevItem = item;
|
prevItem = item;
|
||||||
newItems.push(item);
|
newItems.push(item);
|
||||||
@@ -29,28 +29,25 @@ 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.tag && item.tag.isListItem;
|
const isListItem = item.component && item.component.isListItem;
|
||||||
const active = item.tag && item.tag.isActive && item.tag.isActive(item.attrs);
|
const active = item.component && item.component.isActive && item.component.isActive(item.props);
|
||||||
const className = (item.attrs && item.attrs.itemClassName) || item.itemClassName;
|
const className = item.props ? item.props.itemClassName : item.itemClassName;
|
||||||
|
|
||||||
if (isListItem) {
|
if (isListItem) {
|
||||||
item.attrs = item.attrs || {};
|
item.attrs = item.attrs || {};
|
||||||
item.attrs.key = item.attrs.key || item.itemName;
|
item.attrs.key = item.attrs.key || item.itemName;
|
||||||
item.key = item.attrs.key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = isListItem ? (
|
return isListItem
|
||||||
item
|
? item
|
||||||
) : (
|
: <li className={classList([
|
||||||
<li
|
(item.itemName ? 'item-' + item.itemName : ''),
|
||||||
className={classList([className, item.itemName && `item-${item.itemName}`, active && 'active'])}
|
className,
|
||||||
key={(item.attrs && item.attrs.key) || item.itemName}
|
(active ? 'active' : '')
|
||||||
>
|
])}
|
||||||
{item}
|
key={item.itemName}>
|
||||||
</li>
|
{item}
|
||||||
);
|
</li>;
|
||||||
|
|
||||||
return node;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user