1
0
mirror of https://github.com/flarum/core.git synced 2025-08-15 12:54:47 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Daniël Klabbers
34079108c8 update version to v0.1.0-beta.8.2 2019-06-12 16:12:01 +02:00
Daniël Klabbers
f7a8b76fa8 fixes font awesome issues in flarum 0.1.0-beta.8.1 2019-06-12 16:11:00 +02:00
784 changed files with 9472 additions and 14023 deletions

BIN
.deploy.enc Normal file

Binary file not shown.

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: flarum
open_collective: flarum
tidelift: packagist/flarum/core

View File

@@ -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**

View File

@@ -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
View File

@@ -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
View File

@@ -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.

View File

@@ -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 }}

View File

@@ -1,31 +0,0 @@
name: Lint code
on:
push:
paths:
- 'js/src/**'
pull_request:
paths:
- 'js/src/**'
jobs:
prettier:
runs-on: ubuntu-latest
name: Lint JS code with 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

View File

@@ -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
View File

@@ -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
View 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
View 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 "$@"

View File

@@ -1,142 +1,12 @@
# Changelog # Changelog
## [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
View 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.

View File

@@ -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

View File

@@ -27,7 +27,7 @@ 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

View File

@@ -5,28 +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": "Daniel 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"
} }
], ],
"support": { "support": {
@@ -35,31 +20,27 @@
"docs": "https://flarum.org/docs/" "docs": "https://flarum.org/docs/"
}, },
"require": { "require": {
"php": ">=7.2", "php": ">=7.1",
"axy/sourcemap": "^0.1.4", "axy/sourcemap": "^0.1.4",
"components/font-awesome": "5.9.*", "components/font-awesome": "5.9.*",
"dflydev/fig-cookies": "^1.0.2", "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": "5.7.*", "illuminate/bus": "5.5.*",
"illuminate/cache": "5.7.*", "illuminate/cache": "5.5.*",
"illuminate/config": "5.7.*", "illuminate/config": "5.5.*",
"illuminate/container": "5.7.*", "illuminate/container": "5.5.*",
"illuminate/contracts": "5.7.*", "illuminate/contracts": "5.5.*",
"illuminate/database": "5.7.*", "illuminate/database": "5.5.*",
"illuminate/events": "5.7.*", "illuminate/events": "5.5.*",
"illuminate/filesystem": "5.7.*", "illuminate/filesystem": "5.5.*",
"illuminate/hashing": "5.7.*", "illuminate/hashing": "5.5.*",
"illuminate/mail": "5.7.*", "illuminate/mail": "5.5.*",
"illuminate/queue": "5.7.*", "illuminate/session": "5.5.*",
"illuminate/session": "5.7.*", "illuminate/support": "5.5.*",
"illuminate/support": "5.7.*", "illuminate/validation": "5.5.*",
"illuminate/validation": "5.7.*", "illuminate/view": "5.5.*",
"illuminate/view": "5.7.*", "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",
@@ -67,21 +48,24 @@
"middlewares/request-handler": "^1.2", "middlewares/request-handler": "^1.2",
"monolog/monolog": "^1.16.0", "monolog/monolog": "^1.16.0",
"nikic/fast-route": "^0.6", "nikic/fast-route": "^0.6",
"oyejorge/less.php": "^1.7",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
"psr/http-server-handler": "^1.0", "psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0", "psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^2.3.6", "s9e/text-formatter": "^1.2.0",
"symfony/config": "^3.3", "symfony/config": "^3.3",
"symfony/console": "^4.2", "symfony/console": "^3.3",
"symfony/event-dispatcher": "^4.3.2", "symfony/http-foundation": "^3.3",
"symfony/translation": "^3.3", "symfony/translation": "^3.3",
"symfony/yaml": "^3.3", "symfony/yaml": "^3.3",
"tobscure/json-api": "^0.3.0", "tobscure/json-api": "^0.3.0",
"wikimedia/less.php": "^3.0" "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": {
@@ -103,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."
} }
} }

View File

@@ -1,6 +0,0 @@
{
"printWidth": 150,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}

30
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

32
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2319
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,36 +2,25 @@
"private": true, "private": true,
"name": "@flarum/core", "name": "@flarum/core",
"dependencies": { "dependencies": {
"bootstrap": "^3.4.1", "bootstrap": "^3.3.7",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"color-thief-browser": "^2.0.2", "color-thief-browser": "^2.0.2",
"expose-loader": "^0.7.5", "expose-loader": "^0.7.5",
"flarum-webpack-config": "0.1.0-beta.10", "flarum-webpack-config": "0.1.0-beta.10",
"jquery": "^3.4.1", "jquery": "^3.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": "^0.2.8", "mithril": "^0.2.8",
"moment": "^2.22.2", "moment": "^2.22.2",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"spin.js": "^3.1.0", "spin.js": "^3.1.0",
"webpack": "^4.41.2", "webpack": "^4.26.0",
"webpack-cli": "^3.1.2", "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"
}
} }
} }

View File

@@ -12,9 +12,9 @@ export default class AdminApplication extends Application {
canGoBack: () => true, canGoBack: () => true,
getPrevious: () => {}, getPrevious: () => {},
backUrl: () => this.forum.attribute('baseUrl'), backUrl: () => this.forum.attribute('baseUrl'),
back: function () { back: function() {
window.location = this.backUrl(); window.location = this.backUrl();
}, }
}; };
constructor() { constructor() {
@@ -27,7 +27,7 @@ export default class AdminApplication extends Application {
* @inheritdoc * @inheritdoc
*/ */
mount() { mount() {
m.mount(document.getElementById('app-navigation'), Navigation.component({ className: 'App-backControl', drawer: true })); m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
m.mount(document.getElementById('header-navigation'), Navigation.component()); m.mount(document.getElementById('header-navigation'), Navigation.component());
m.mount(document.getElementById('header-primary'), HeaderPrimary.component()); m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component()); m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
@@ -59,5 +59,5 @@ export default class AdminApplication extends Application {
} }
return required; return required;
} };
} }

View File

@@ -58,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
}); });

View File

@@ -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>
); );
} }

View File

@@ -13,7 +13,11 @@ export default class AdminLinkButton extends LinkButton {
getButtonContent() { getButtonContent() {
const content = super.getButtonContent(); const content = super.getButtonContent();
content.push(<div className="AdminLinkButton-description">{this.props.description}</div>); content.push(
<div className="AdminLinkButton-description">
{this.props.description}
</div>
);
return content; return content;
} }

View File

@@ -15,7 +15,9 @@ import ItemList from '../../common/utils/ItemList';
export default class AdminNav extends Component { export default class AdminNav extends Component {
view() { view() {
return ( return (
<SelectDropdown className="AdminNav App-titleControl" buttonClassName="Button"> <SelectDropdown
className="AdminNav App-titleControl"
buttonClassName="Button">
{this.items().toArray()} {this.items().toArray()}
</SelectDropdown> </SelectDropdown>
); );
@@ -29,65 +31,47 @@ export default class AdminNav extends Component {
items() { items() {
const items = new ItemList(); const items = new ItemList();
items.add( items.add('dashboard', AdminLinkButton.component({
'dashboard', href: app.route('dashboard'),
AdminLinkButton.component({ icon: 'far fa-chart-bar',
href: app.route('dashboard'), children: app.translator.trans('core.admin.nav.dashboard_button'),
icon: 'far fa-chart-bar', description: app.translator.trans('core.admin.nav.dashboard_text')
children: app.translator.trans('core.admin.nav.dashboard_button'), }));
description: app.translator.trans('core.admin.nav.dashboard_text'),
})
);
items.add( items.add('basics', AdminLinkButton.component({
'basics', href: app.route('basics'),
AdminLinkButton.component({ icon: 'fas fa-pencil-alt',
href: app.route('basics'), children: app.translator.trans('core.admin.nav.basics_button'),
icon: 'fas fa-pencil-alt', description: app.translator.trans('core.admin.nav.basics_text')
children: app.translator.trans('core.admin.nav.basics_button'), }));
description: app.translator.trans('core.admin.nav.basics_text'),
})
);
items.add( items.add('mail', AdminLinkButton.component({
'mail', href: app.route('mail'),
AdminLinkButton.component({ icon: 'fas fa-envelope',
href: app.route('mail'), children: app.translator.trans('core.admin.nav.email_button'),
icon: 'fas fa-envelope', description: app.translator.trans('core.admin.nav.email_text')
children: app.translator.trans('core.admin.nav.email_button'), }));
description: app.translator.trans('core.admin.nav.email_text'),
})
);
items.add( items.add('permissions', AdminLinkButton.component({
'permissions', href: app.route('permissions'),
AdminLinkButton.component({ icon: 'fas fa-key',
href: app.route('permissions'), children: app.translator.trans('core.admin.nav.permissions_button'),
icon: 'fas fa-key', description: app.translator.trans('core.admin.nav.permissions_text')
children: app.translator.trans('core.admin.nav.permissions_button'), }));
description: app.translator.trans('core.admin.nav.permissions_text'),
})
);
items.add( items.add('appearance', AdminLinkButton.component({
'appearance', href: app.route('appearance'),
AdminLinkButton.component({ icon: 'fas fa-paint-brush',
href: app.route('appearance'), children: app.translator.trans('core.admin.nav.appearance_button'),
icon: 'fas fa-paint-brush', description: app.translator.trans('core.admin.nav.appearance_text')
children: app.translator.trans('core.admin.nav.appearance_button'), }));
description: app.translator.trans('core.admin.nav.appearance_text'),
})
);
items.add( items.add('extensions', AdminLinkButton.component({
'extensions', href: app.route('extensions'),
AdminLinkButton.component({ icon: 'fas fa-puzzle-piece',
href: app.route('extensions'), children: app.translator.trans('core.admin.nav.extensions_button'),
icon: 'fas fa-puzzle-piece', description: app.translator.trans('core.admin.nav.extensions_text')
children: app.translator.trans('core.admin.nav.extensions_button'), }));
description: app.translator.trans('core.admin.nav.extensions_text'),
})
);
return items; return items;
} }

View File

@@ -24,85 +24,85 @@ export default class AppearancePage extends Page {
<form onsubmit={this.onsubmit.bind(this)}> <form onsubmit={this.onsubmit.bind(this)}>
<fieldset className="AppearancePage-colors"> <fieldset className="AppearancePage-colors">
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div> <div className="helpText">
{app.translator.trans('core.admin.appearance.colors_text')}
</div>
<div className="AppearancePage-colors-input"> <div className="AppearancePage-colors-input">
<input <input className="FormControl" type="text" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
className="FormControl" <input className="FormControl" type="text" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
type="text"
placeholder="#aaaaaa"
value={this.primaryColor()}
onchange={m.withAttr('value', this.primaryColor)}
/>
<input
className="FormControl"
type="text"
placeholder="#aaaaaa"
value={this.secondaryColor()}
onchange={m.withAttr('value', this.secondaryColor)}
/>
</div> </div>
{Switch.component({ {Switch.component({
state: this.darkMode(), state: this.darkMode(),
children: app.translator.trans('core.admin.appearance.dark_mode_label'), children: app.translator.trans('core.admin.appearance.dark_mode_label'),
onchange: this.darkMode, onchange: this.darkMode
})} })}
{Switch.component({ {Switch.component({
state: this.coloredHeader(), state: this.coloredHeader(),
children: app.translator.trans('core.admin.appearance.colored_header_label'), children: app.translator.trans('core.admin.appearance.colored_header_label'),
onchange: this.coloredHeader, onchange: this.coloredHeader
})} })}
{Button.component({ {Button.component({
className: 'Button Button--primary', className: 'Button Button--primary',
type: 'submit', type: 'submit',
children: app.translator.trans('core.admin.appearance.submit_button'), children: app.translator.trans('core.admin.appearance.submit_button'),
loading: this.loading, loading: this.loading
})} })}
</fieldset> </fieldset>
</form> </form>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div> <div className="helpText">
<UploadImageButton name="logo" /> {app.translator.trans('core.admin.appearance.logo_text')}
</div>
<UploadImageButton name="logo"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div> <div className="helpText">
<UploadImageButton name="favicon" /> {app.translator.trans('core.admin.appearance.favicon_text')}
</div>
<UploadImageButton name="favicon"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div> <div className="helpText">
{app.translator.trans('core.admin.appearance.custom_header_text')}
</div>
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_header_button'), children: app.translator.trans('core.admin.appearance.edit_header_button'),
onclick: () => app.modal.show(new EditCustomHeaderModal()), onclick: () => app.modal.show(new EditCustomHeaderModal())
})} })}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div> <div className="helpText">
{app.translator.trans('core.admin.appearance.custom_footer_text')}
</div>
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_footer_button'), children: app.translator.trans('core.admin.appearance.edit_footer_button'),
onclick: () => app.modal.show(new EditCustomFooterModal()), onclick: () => app.modal.show(new EditCustomFooterModal())
})} })}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div> <div className="helpText">
{app.translator.trans('core.admin.appearance.custom_styles_text')}
</div>
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_css_button'), children: app.translator.trans('core.admin.appearance.edit_css_button'),
onclick: () => app.modal.show(new EditCustomCssModal()), onclick: () => app.modal.show(new EditCustomCssModal())
})} })}
</fieldset> </fieldset>
</div> </div>
@@ -126,7 +126,7 @@ export default class AppearancePage extends Page {
theme_primary_color: this.primaryColor(), theme_primary_color: this.primaryColor(),
theme_secondary_color: this.secondaryColor(), theme_secondary_color: this.secondaryColor(),
theme_dark_mode: this.darkMode(), theme_dark_mode: this.darkMode(),
theme_colored_header: this.coloredHeader(), theme_colored_header: this.coloredHeader()
}).then(() => window.location.reload()); }).then(() => window.location.reload());
} }
} }

View File

@@ -20,12 +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'
]; ];
this.values = {}; this.values = {};
const settings = app.data.settings; const settings = app.data.settings;
this.fields.forEach((key) => (this.values[key] = m.prop(settings[key]))); this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
this.localeOptions = {}; this.localeOptions = {};
const locales = app.data.locales; const locales = app.data.locales;
@@ -33,7 +33,7 @@ export default class BasicsPage extends Page {
this.localeOptions[i] = `${locales[i]} (${i})`; this.localeOptions[i] = `${locales[i]} (${i})`;
} }
if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1); if (typeof this.values.show_language_selector() !== "number") this.values.show_language_selector(1);
} }
view() { view() {
@@ -43,75 +43,67 @@ export default class BasicsPage extends Page {
<form onsubmit={this.onsubmit.bind(this)}> <form onsubmit={this.onsubmit.bind(this)}>
{FieldSet.component({ {FieldSet.component({
label: app.translator.trans('core.admin.basics.forum_title_heading'), label: app.translator.trans('core.admin.basics.forum_title_heading'),
children: [<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)} />], children: [
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
]
})} })}
{FieldSet.component({ {FieldSet.component({
label: app.translator.trans('core.admin.basics.forum_description_heading'), label: app.translator.trans('core.admin.basics.forum_description_heading'),
children: [ children: [
<div className="helpText">{app.translator.trans('core.admin.basics.forum_description_text')}</div>, <div className="helpText">
<textarea {app.translator.trans('core.admin.basics.forum_description_text')}
className="FormControl" </div>,
value={this.values.forum_description()} <textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
oninput={m.withAttr('value', this.values.forum_description)} ]
/>,
],
})} })}
{Object.keys(this.localeOptions).length > 1 {Object.keys(this.localeOptions).length > 1
? 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: [ children: [
Select.component({ Select.component({
options: this.localeOptions, options: this.localeOptions,
value: this.values.default_locale(), value: this.values.default_locale(),
onchange: this.values.default_locale, onchange: this.values.default_locale
}), }),
Switch.component({ Switch.component({
state: this.values.show_language_selector(), state: this.values.show_language_selector(),
onchange: this.values.show_language_selector, onchange: this.values.show_language_selector,
children: app.translator.trans('core.admin.basics.show_language_selector_label'), children: app.translator.trans('core.admin.basics.show_language_selector_label'),
}), })
], ]
}) })
: ''} : ''}
{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: [ children: [
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>, <div className="helpText">
this.homePageItems() {app.translator.trans('core.admin.basics.home_page_text')}
.toArray() </div>,
.map(({ path, label }) => ( this.homePageItems().toArray().map(({path, label}) =>
<label className="checkbox"> <label className="checkbox">
<input <input type="radio" name="homePage" value={path} checked={this.values.default_route() === path} onclick={m.withAttr('value', this.values.default_route)}/>
type="radio" {label}
name="homePage" </label>
value={path} )
checked={this.values.default_route() === path} ]
onclick={m.withAttr('value', this.values.default_route)}
/>
{label}
</label>
)),
],
})} })}
{FieldSet.component({ {FieldSet.component({
label: app.translator.trans('core.admin.basics.welcome_banner_heading'), label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
className: 'BasicsPage-welcomeBanner', className: 'BasicsPage-welcomeBanner',
children: [ children: [
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>, <div className="helpText">
<div className="BasicsPage-welcomeBanner-input"> {app.translator.trans('core.admin.basics.welcome_banner_text')}
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)} />
<textarea
className="FormControl"
value={this.values.welcome_message()}
oninput={m.withAttr('value', this.values.welcome_message)}
/>
</div>, </div>,
], <div className="BasicsPage-welcomeBanner-input">
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
<textarea className="FormControl" value={this.values.welcome_message()} oninput={m.withAttr('value', this.values.welcome_message)}/>
</div>
]
})} })}
{Button.component({ {Button.component({
@@ -119,7 +111,7 @@ export default class BasicsPage extends Page {
className: 'Button Button--primary', className: 'Button Button--primary',
children: app.translator.trans('core.admin.basics.submit_button'), children: app.translator.trans('core.admin.basics.submit_button'),
loading: this.loading, loading: this.loading,
disabled: !this.changed(), disabled: !this.changed()
})} })}
</form> </form>
</div> </div>
@@ -128,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]);
} }
/** /**
@@ -143,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;
@@ -159,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(() => {
app.alerts.show((this.successAlert = new Alert({ type: 'success', children: 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(() => {

View File

@@ -5,12 +5,14 @@ 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/>];
} }
} }

View File

@@ -11,7 +11,11 @@ import Component from '../../common/Component';
export default class Widget extends Component { export default class Widget extends Component {
view() { view() {
return <div className={'Widget ' + this.className()}>{this.content()}</div>; return (
<div className={"Widget "+this.className()}>
{this.content()}
</div>
);
} }
/** /**

View File

@@ -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>
]; ];
} }

View File

@@ -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>
]; ];
} }

View File

@@ -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>
]; ];
} }

View File

@@ -24,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>
); );
} }
@@ -46,88 +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 </div>
className="FormControl" </div>, 30);
placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')}
value={this.nameSingular()}
oninput={m.withAttr('value', this.nameSingular)}
/>
<input
className="FormControl"
placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')}
value={this.namePlural()}
oninput={m.withAttr('value', this.namePlural)}
/>
</div>
</div>,
30
);
items.add( 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" value={this.color()} oninput={m.withAttr('value', 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" value={this.icon()} oninput={m.withAttr('value', this.icon)} />
</div>,
10
);
items.add( items.add('submit', <div className="Form-group">
'submit', {Button.component({
<div className="Form-group"> type: 'submit',
{Button.component({ className: 'Button Button--primary EditGroupModal-save',
type: 'submit', loading: this.loading,
className: 'Button Button--primary EditGroupModal-save', children: app.translator.trans('core.admin.edit_group.submit_button')
loading: this.loading, })}
children: app.translator.trans('core.admin.edit_group.submit_button'), {this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
})} <button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? ( {app.translator.trans('core.admin.edit_group.delete_button')}
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}> </button>
{app.translator.trans('core.admin.edit_group.delete_button')} ) : ''}
</button> </div>, -10);
) : (
''
)}
</div>,
-10
);
return items; return items;
} }
submitData() {
return {
nameSingular: this.nameSingular(),
namePlural: this.namePlural(),
color: this.color(),
icon: this.icon(),
};
}
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;

View File

@@ -19,7 +19,7 @@ export default class ExtensionsPage extends Page {
children: app.translator.trans('core.admin.extensions.add_button'), children: app.translator.trans('core.admin.extensions.add_button'),
icon: 'fas fa-plus', icon: 'fas fa-plus',
className: 'Button Button--primary', className: 'Button Button--primary',
onclick: () => app.modal.show(new AddExtensionModal()), onclick: () => app.modal.show(new AddExtensionModal())
})} })}
</div> </div>
</div> </div>
@@ -27,12 +27,12 @@ export default class ExtensionsPage extends Page {
<div className="ExtensionsPage-list"> <div className="ExtensionsPage-list">
<div className="container"> <div className="container">
<ul className="ExtensionList"> <ul className="ExtensionList">
{Object.keys(app.data.extensions).map((id) => { {Object.keys(app.data.extensions)
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) : ''}
@@ -42,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>
@@ -73,34 +69,26 @@ export default class ExtensionsPage extends Page {
const enabled = this.isEnabled(name); const enabled = this.isEnabled(name);
if (app.extensionSettings[name]) { if (app.extensionSettings[name]) {
items.add( items.add('settings', Button.component({
'settings', icon: 'fas fa-cog',
Button.component({ children: app.translator.trans('core.admin.extensions.settings_button'),
icon: 'fas fa-cog', onclick: app.extensionSettings[name]
children: app.translator.trans('core.admin.extensions.settings_button'), }));
onclick: app.extensionSettings[name],
})
);
} }
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'),
icon: 'far fa-trash-alt', onclick: () => {
children: app.translator.trans('core.admin.extensions.uninstall_button'), 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(new LoadingModal()); app.modal.show(new LoadingModal());
}, }
}) }));
);
} }
return items; return items;
@@ -115,16 +103,14 @@ export default class ExtensionsPage extends Page {
toggle(id) { toggle(id) {
const enabled = this.isEnabled(id); const enabled = this.isEnabled(id);
app app.request({
.request({ url: app.forum.attribute('apiUrl') + '/extensions/' + id,
url: app.forum.attribute('apiUrl') + '/extensions/' + id, method: 'PATCH',
method: 'PATCH', data: {enabled: !enabled}
data: { enabled: !enabled }, }).then(() => {
}) if (!enabled) localStorage.setItem('enabledExtension', id);
.then(() => { window.location.reload();
if (!enabled) localStorage.setItem('enabledExtension', id); });
window.location.reload();
});
app.modal.show(new LoadingModal()); app.modal.show(new LoadingModal());
} }

View File

@@ -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) {

View File

@@ -8,7 +8,11 @@ import listItems from '../../common/helpers/listItems';
*/ */
export default class HeaderSecondary extends Component { export default class HeaderSecondary extends Component {
view() { view() {
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>; return (
<ul className="Header-controls">
{listItems(this.items().toArray())}
</ul>
);
} }
config(isInitialized, context) { config(isInitialized, context) {

View File

@@ -2,130 +2,92 @@ 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';
export default class MailPage extends Page { export default class MailPage extends Page {
init() { init() {
super.init(); super.init();
this.saving = false; this.loading = 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] = m.prop(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] = m.prop(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({
label: app.translator.trans('core.admin.email.server_heading'),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
<input className="FormControl" value={this.values.mail_driver() || ''} oninput={m.withAttr('value', this.values.mail_driver)} />
<label>{app.translator.trans('core.admin.email.host_label')}</label>
<input className="FormControl" value={this.values.mail_host() || ''} oninput={m.withAttr('value', this.values.mail_host)} />
<label>{app.translator.trans('core.admin.email.port_label')}</label>
<input className="FormControl" value={this.values.mail_port() || ''} oninput={m.withAttr('value', this.values.mail_port)} />
<label>{app.translator.trans('core.admin.email.encryption_label')}</label>
<input className="FormControl" value={this.values.mail_encryption() || ''} oninput={m.withAttr('value', this.values.mail_encryption)} />
</div>
]
})}
{FieldSet.component({
label: app.translator.trans('core.admin.email.account_heading'),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.username_label')}</label>
<input className="FormControl" value={this.values.mail_username() || ''} oninput={m.withAttr('value', this.values.mail_username)} />
<label>{app.translator.trans('core.admin.email.password_label')}</label>
<input className="FormControl" value={this.values.mail_password() || ''} oninput={m.withAttr('value', this.values.mail_password)} />
</div>
]
})}
{FieldSet.component({ {FieldSet.component({
label: app.translator.trans('core.admin.email.addresses_heading'), label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings', className: 'MailPage-MailSettings',
children: [ children: [
<div className="MailPage-MailSettings-input"> <div className="MailPage-MailSettings-input">
<label> <label>{app.translator.trans('core.admin.email.from_label')}</label>
{app.translator.trans('core.admin.email.from_label')} <input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} /> </div>
</label> ]
</div>,
],
})} })}
{FieldSet.component({
label: app.translator.trans('core.admin.email.driver_heading'),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<label>
{app.translator.trans('core.admin.email.driver_label')}
<Select
value={this.values.mail_driver()}
options={Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {})}
onchange={this.values.mail_driver}
/>
</label>
</div>,
],
})}
{this.status.sending ||
Alert.component({
children: app.translator.trans('core.admin.email.not_sending_message'),
dismissible: false,
})}
{fieldKeys.length > 0 &&
FieldSet.component({
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
className: 'MailPage-MailSettings',
children: [
<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>,
],
})}
{Button.component({ {Button.component({
type: 'submit', type: 'submit',
className: 'Button Button--primary', className: 'Button Button--primary',
children: app.translator.trans('core.admin.email.submit_button'), children: app.translator.trans('core.admin.email.submit_button'),
disabled: !this.changed(), loading: this.loading,
disabled: !this.changed()
})} })}
</form> </form>
</div> </div>
@@ -133,42 +95,30 @@ export default class MailPage extends Page {
); );
} }
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" value={prop() || ''} oninput={m.withAttr('value', 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]);
} }
onsubmit(e) { onsubmit(e) {
e.preventDefault(); e.preventDefault();
if (this.saving) 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(() => {
app.alerts.show((this.successAlert = new Alert({ type: 'success', children: 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();
}); });
} }
} }

View File

@@ -8,25 +8,26 @@ 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;
} }
@@ -51,31 +52,34 @@ export default class PermissionDropdown extends Dropdown {
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID); const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
if (everyone) { if (everyone) {
this.props.label = Badge.component({ icon: 'fas fa-globe' }); this.props.label = Badge.component({icon: 'fas fa-globe'});
} else if (members) { } else if (members) {
this.props.label = Badge.component({ icon: 'fas fa-user' }); this.props.label = Badge.component({icon: 'fas fa-user'});
} else { } else {
this.props.label = [badgeForId(Group.ADMINISTRATOR_ID), groupIds.map(badgeForId)]; this.props.label = [
badgeForId(Group.ADMINISTRATOR_ID),
groupIds.map(badgeForId)
];
} }
if (this.showing) { if (this.showing) {
if (this.props.allowGuest) { if (this.props.allowGuest) {
this.props.children.push( this.props.children.push(
Button.component({ Button.component({
children: [Badge.component({ icon: 'fas fa-globe' }), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')], children: [Badge.component({icon: 'fas fa-globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
icon: everyone ? 'fas fa-check' : true, icon: everyone ? 'fas fa-check' : true,
onclick: () => this.save([Group.GUEST_ID]), onclick: () => this.save([Group.GUEST_ID]),
disabled: this.isGroupDisabled(Group.GUEST_ID), disabled: this.isGroupDisabled(Group.GUEST_ID)
}) })
); );
} }
this.props.children.push( this.props.children.push(
Button.component({ Button.component({
children: [Badge.component({ icon: 'fas fa-user' }), ' ', app.translator.trans('core.admin.permissions_controls.members_button')], children: [Badge.component({icon: 'fas fa-user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
icon: members ? 'fas fa-check' : true, icon: members ? 'fas fa-check' : true,
onclick: () => this.save([Group.MEMBER_ID]), onclick: () => this.save([Group.MEMBER_ID]),
disabled: this.isGroupDisabled(Group.MEMBER_ID), disabled: this.isGroupDisabled(Group.MEMBER_ID)
}), }),
Separator.component(), Separator.component(),
@@ -84,29 +88,26 @@ export default class PermissionDropdown extends Dropdown {
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()], children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
icon: !everyone && !members ? 'fas fa-check' : true, icon: !everyone && !members ? 'fas fa-check' : true,
disabled: !everyone && !members, disabled: !everyone && !members,
onclick: (e) => { onclick: e => {
if (e.shiftKey) e.stopPropagation(); if (e.shiftKey) e.stopPropagation();
this.save([]); this.save([]);
}, }
}) })
); );
[].push.apply( [].push.apply(
this.props.children, this.props.children,
app.store app.store.all('groups')
.all('groups') .filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.filter((group) => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) .map(group => Button.component({
.map((group) => children: [badgeForId(group.id()), ' ', group.namePlural()],
Button.component({ icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
children: [badgeForId(group.id()), ' ', group.namePlural()], 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),
})
)
); );
} }
@@ -121,7 +122,7 @@ export default class PermissionDropdown extends Dropdown {
app.request({ app.request({
method: 'POST', method: 'POST',
url: app.forum.attribute('apiUrl') + '/permission', url: app.forum.attribute('apiUrl') + '/permission',
data: { permission, groupIds }, data: {permission, groupIds}
}); });
} }
@@ -136,7 +137,7 @@ export default class PermissionDropdown extends Dropdown {
groupIds.splice(index, 1); groupIds.splice(index, 1);
} else { } else {
groupIds.push(groupId); groupIds.push(groupId);
groupIds = groupIds.filter((id) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1); groupIds = groupIds.filter(id => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1);
} }
this.save(groupIds); this.save(groupIds);

View File

@@ -13,8 +13,12 @@ export default class PermissionGrid extends Component {
view() { view() {
const scopes = this.scopeItems().toArray(); const scopes = this.scopeItems().toArray();
const permissionCells = (permission) => { const permissionCells = permission => {
return scopes.map((scope) => <td>{scope.render(permission)}</td>); return scopes.map(scope => (
<td>
{scope.render(permission)}
</td>
));
}; };
return ( return (
@@ -22,32 +26,27 @@ export default class PermissionGrid extends Component {
<thead> <thead>
<tr> <tr>
<td></td> <td></td>
{scopes.map((scope) => ( {scopes.map(scope => (
<th> <th>
{scope.label}{' '} {scope.label}{' '}
{scope.onremove {scope.onremove ? Button.component({icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
? Button.component({ icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove })
: ''}
</th> </th>
))} ))}
<th>{this.scopeControlItems().toArray()}</th> <th>{this.scopeControlItems().toArray()}</th>
</tr> </tr>
</thead> </thead>
{this.permissions.map((section) => ( {this.permissions.map(section => (
<tbody> <tbody>
<tr className="PermissionGrid-section"> <tr className="PermissionGrid-section">
<th>{section.label}</th> <th>{section.label}</th>
{permissionCells(section)} {permissionCells(section)}
<td /> <td/>
</tr> </tr>
{section.children.map((child) => ( {section.children.map(child => (
<tr className="PermissionGrid-child"> <tr className="PermissionGrid-child">
<th> <th>{icon(child.icon)}{child.label}</th>
{icon(child.icon)}
{child.label}
</th>
{permissionCells(child)} {permissionCells(child)}
<td /> <td/>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -59,41 +58,25 @@ export default class PermissionGrid extends Component {
permissionItems() { permissionItems() {
const items = new ItemList(); const items = new ItemList();
items.add( items.add('view', {
'view', label: app.translator.trans('core.admin.permissions.read_heading'),
{ 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;
} }
@@ -101,44 +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', {
'viewUserList', 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_user_list_label'), allowGuest: true
permission: 'viewUserList', }, 100);
allowGuest: true,
},
100
);
items.add( items.add('signUp', {
'signUp', icon: 'fas fa-user-plus',
{ label: app.translator.trans('core.admin.permissions.sign_up_label'),
icon: 'fas fa-user-plus', setting: () => SettingDropdown.component({
label: app.translator.trans('core.admin.permissions.sign_up_label'), key: 'allow_sign_up',
setting: () => options: [
SettingDropdown.component({ {value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button')},
key: 'allow_sign_up', {value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button')}
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);
],
}),
},
90
);
items.add('viewLastSeenAt', { items.add('viewLastSeenAt', {
icon: 'far fa-clock', icon: 'far fa-clock',
@@ -152,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;
} }
@@ -192,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;
} }
@@ -232,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;
} }
@@ -328,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;
} }

View File

@@ -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(new 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(new 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>
); );

View File

@@ -26,7 +26,10 @@ export default class SessionDropdown extends Dropdown {
getButtonContent() { getButtonContent() {
const user = app.session.user; const user = app.session.user;
return [avatar(user), ' ', <span className="Button-label">{username(user)}</span>]; return [
avatar(user), ' ',
<span className="Button-label">{username(user)}</span>
];
} }
/** /**
@@ -37,12 +40,11 @@ export default class SessionDropdown extends Dropdown {
items() { items() {
const items = new ItemList(); const items = new ItemList();
items.add( items.add('logOut',
'logOut',
Button.component({ Button.component({
icon: 'fas fa-sign-out-alt', icon: 'fas fa-sign-out-alt',
children: app.translator.trans('core.admin.header.log_out_button'), children: app.translator.trans('core.admin.header.log_out_button'),
onclick: app.session.logout.bind(app.session), onclick: app.session.logout.bind(app.session)
}), }),
-100 -100
); );

View File

@@ -11,14 +11,14 @@ export default class SettingDropdown extends SelectDropdown {
props.caretIcon = 'fas fa-caret-down'; props.caretIcon = 'fas fa-caret-down';
props.defaultLabel = 'Custom'; props.defaultLabel = 'Custom';
props.children = props.options.map(({ value, label }) => { props.children = props.options.map(({value, label}) => {
const active = app.data.settings[props.key] === value; const active = app.data.settings[props.key] === value;
return Button.component({ return Button.component({
children: label, children: label,
icon: active ? 'fas fa-check' : true, icon: active ? 'fas fa-check' : true,
onclick: saveSettings.bind(this, { [props.key]: value }), onclick: saveSettings.bind(this, {[props.key]: value}),
active, active
}); });
}); });
} }

View File

@@ -18,7 +18,9 @@ export default class SettingsModal extends Modal {
<div className="Form"> <div className="Form">
{this.form()} {this.form()}
<div className="Form-group">{this.submitButton()}</div> <div className="Form-group">
{this.submitButton()}
</div>
</div> </div>
</div> </div>
); );
@@ -26,7 +28,11 @@ export default class SettingsModal extends Modal {
submitButton() { submitButton() {
return ( return (
<Button type="submit" className="Button Button--primary" loading={this.loading} disabled={!this.changed()}> <Button
type="submit"
className="Button Button--primary"
loading={this.loading}
disabled={!this.changed()}>
{app.translator.trans('core.admin.settings.submit_button')} {app.translator.trans('core.admin.settings.submit_button')}
</Button> </Button>
); );
@@ -41,7 +47,7 @@ export default class SettingsModal extends Modal {
dirty() { dirty() {
const dirty = {}; const dirty = {};
Object.keys(this.settings).forEach((key) => { Object.keys(this.settings).forEach(key => {
const value = this.settings[key](); const value = this.settings[key]();
if (value !== app.data.settings[key]) { if (value !== app.data.settings[key]) {
@@ -61,7 +67,10 @@ export default class SettingsModal extends Modal {
this.loading = true; this.loading = true;
saveSettings(this.dirty()).then(this.onsaved.bind(this), this.loaded.bind(this)); saveSettings(this.dirty()).then(
this.onsaved.bind(this),
this.loaded.bind(this)
);
} }
onsaved() { onsaved() {

View File

@@ -20,27 +20,29 @@ 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;
} }
@@ -48,11 +50,9 @@ export default class StatusWidget extends DashboardWidget {
handleClearCache(e) { handleClearCache(e) {
app.modal.show(new 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());
} }
} }

View File

@@ -15,9 +15,7 @@ export default class UploadImageButton extends Button {
return ( return (
<div> <div>
<p> <p><img src={app.forum.attribute(this.props.name+'Url')} alt=""/></p>
<img src={app.forum.attribute(this.props.name + 'Url')} alt="" />
</p>
<p>{super.view()}</p> <p>{super.view()}</p>
</div> </div>
); );
@@ -37,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 data = new FormData();
data.append(this.props.name, $(e.target)[0].files[0]);
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app app.request({
.request({ method: 'POST',
method: 'POST', url: this.resourceUrl(),
url: this.resourceUrl(), serialize: raw => raw,
serialize: (raw) => raw, data
data, }).then(
}) this.success.bind(this),
.then(this.success.bind(this), this.failure.bind(this)); this.failure.bind(this)
}); );
});
} }
/** /**
@@ -66,12 +61,13 @@ export default class UploadImageButton extends Button {
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app app.request({
.request({ method: 'DELETE',
method: 'DELETE', url: this.resourceUrl()
url: this.resourceUrl(), }).then(
}) this.success.bind(this),
.then(this.success.bind(this), this.failure.bind(this)); this.failure.bind(this)
);
} }
resourceUrl() { resourceUrl() {

View File

@@ -11,7 +11,11 @@ import Component from '../../common/Component';
export default class DashboardWidget extends Component { export default class DashboardWidget extends Component {
view() { view() {
return <div className={'DashboardWidget ' + this.className()}>{this.content()}</div>; return (
<div className={"DashboardWidget "+this.className()}>
{this.content()}
</div>
);
} }
/** /**

View File

@@ -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';

View File

@@ -10,13 +10,13 @@ import MailPage from './components/MailPage';
* *
* @param {App} app * @param {App} app
*/ */
export default function (app) { export default function(app) {
app.routes = { app.routes = {
dashboard: { path: '/', component: DashboardPage.component() }, 'dashboard': {path: '/', component: DashboardPage.component()},
basics: { path: '/basics', component: BasicsPage.component() }, 'basics': {path: '/basics', component: BasicsPage.component()},
permissions: { path: '/permissions', component: PermissionsPage.component() }, 'permissions': {path: '/permissions', component: PermissionsPage.component()},
appearance: { path: '/appearance', component: AppearancePage.component() }, 'appearance': {path: '/appearance', component: AppearancePage.component()},
extensions: { path: '/extensions', component: ExtensionsPage.component() }, 'extensions': {path: '/extensions', component: ExtensionsPage.component()},
mail: { path: '/mail', component: MailPage.component() }, 'mail': {path: '/mail', component: MailPage.component()}
}; };
} }

View File

@@ -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
data: settings, }).catch(error => {
}) app.data.settings = oldSettings;
.catch((error) => { throw error;
app.data.settings = oldSettings; });
throw error;
});
} }

View File

@@ -1,9 +1,7 @@
import ItemList from './utils/ItemList'; import ItemList from './utils/ItemList';
import Alert from './components/Alert'; import Alert from './components/Alert';
import Button from './components/Button';
import ModalManager from './components/ModalManager'; import ModalManager from './components/ModalManager';
import AlertManager from './components/AlertManager'; import AlertManager from './components/AlertManager';
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';
@@ -86,7 +84,7 @@ export default class Application {
discussions: Discussion, discussions: Discussion,
posts: Post, posts: Post,
groups: Group, groups: Group,
notifications: Notification, notifications: Notification
}); });
/** /**
@@ -126,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) {
@@ -150,20 +151,26 @@ export default class Application {
} }
mount(basePath = '') { mount(basePath = '') {
this.modal = m.mount(document.getElementById('modal'), <ModalManager />); this.modal = m.mount(document.getElementById('modal'), <ModalManager/>);
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager />); this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>);
this.drawer = new Drawer(); this.drawer = new Drawer();
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath)); m.route(
document.getElementById('content'),
basePath + '/',
mapRoutes(this.routes, basePath)
);
// Add a class to the body which indicates that the page has been scrolled // Add a class to the body which indicates that the page has been scrolled
// down. // down.
new ScrollListener((top) => { new ScrollListener(top => {
const $app = $('#app'); const $app = $('#app');
const offset = $app.offset().top; const offset = $app.offset().top;
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset); $app
.toggleClass('affix', top >= offset)
.toggleClass('scrolled', top > offset);
}).start(); }).start();
$(() => { $(() => {
@@ -211,7 +218,9 @@ export default class Application {
} }
updateTitle() { updateTitle() {
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') + (this.title ? this.title + ' - ' : '') + this.forum.attribute('title'); document.title = (this.titleCount ? `(${this.titleCount}) ` : '') +
(this.title ? this.title + ' - ' : '') +
this.forum.attribute('title');
} }
/** /**
@@ -245,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) {
@@ -290,74 +297,54 @@ export default class Application {
// returned and show an alert containing its contents. // returned and show an alert containing its contents.
const deferred = m.deferred(); const deferred = m.deferred();
m.request(options).then( m.request(options).then(response => deferred.resolve(response), error => {
(response) => deferred.resolve(response), this.requestError = error;
(error) => {
this.requestError = error;
let children; let children;
switch (error.status) { switch (error.status) {
case 422: case 422:
children = error.response.errors children = error.response.errors
.map((error) => [error.detail, <br />]) .map(error => [error.detail, <br/>])
.reduce((a, b) => a.concat(b), []) .reduce((a, b) => a.concat(b), [])
.slice(0, -1); .slice(0, -1);
break; break;
case 401: case 401:
case 403: case 403:
children = app.translator.trans('core.lib.error.permission_denied_message'); children = app.translator.trans('core.lib.error.permission_denied_message');
break; break;
case 404: case 404:
case 410: case 410:
children = app.translator.trans('core.lib.error.not_found_message'); children = app.translator.trans('core.lib.error.not_found_message');
break; break;
case 429: case 429:
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message'); children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
break; break;
default: default:
children = app.translator.trans('core.lib.error.generic_message'); children = app.translator.trans('core.lib.error.generic_message');
}
const isDebug = app.forum.attribute('debug');
error.alert = new Alert({
type: 'error',
children,
controls: isDebug && [
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>
Debug
</Button>,
],
});
try {
options.errorHandler(error);
} catch (error) {
this.alerts.show(error.alert);
}
deferred.reject(error);
} }
);
error.alert = new Alert({
type: 'error',
children
});
try {
options.errorHandler(error);
} catch (error) {
this.alerts.show(error.alert);
}
deferred.reject(error);
});
return deferred.promise; return deferred.promise;
} }
/**
* @param {RequestError} error
* @private
*/
showDebug(error) {
this.alerts.dismiss(this.requestError.alert);
this.modal.show(new RequestErrorModal({ error }));
}
/** /**
* Construct a URL to the route with the given name. * Construct a URL to the route with the given name.
* *

View File

@@ -70,7 +70,8 @@ export default class Component {
* *
* @protected * @protected
*/ */
init() {} init() {
}
/** /**
* Called when the component is destroyed, i.e. after a redraw where it is no * Called when the component is destroyed, i.e. after a redraw where it is no
@@ -80,7 +81,8 @@ export default class Component {
* @param {Object} e * @param {Object} e
* @public * @public
*/ */
onunload() {} onunload() {
}
/** /**
* Get the renderable virtual DOM that represents the component's view. * Get the renderable virtual DOM that represents the component's view.
@@ -97,7 +99,7 @@ export default class Component {
* @public * @public
*/ */
render() { render() {
const vdom = this.retain ? { subtree: 'retain' } : this.view(); const vdom = this.retain ? {subtree: 'retain'} : this.view();
// Override the root element's config attribute with our own function, which // Override the root element's config attribute with our own function, which
// will set the component instance's element property to the root DOM // will set the component instance's element property to the root DOM
@@ -146,7 +148,8 @@ export default class Component {
* @param {Object} vdom * @param {Object} vdom
* @public * @public
*/ */
config() {} config() {
}
/** /**
* Get the virtual DOM that represents the component's view. * Get the virtual DOM that represents the component's view.
@@ -198,14 +201,14 @@ export default class Component {
controller: this.bind(undefined, componentProps), controller: this.bind(undefined, componentProps),
view: view, view: view,
props: componentProps, props: componentProps,
component: this, component: this
}; };
// If a `key` prop was set, then we'll assume that we want that to actually // If a `key` prop was set, then we'll assume that we want that to actually
// show up as an attribute on the component object so that Mithril's key // show up as an attribute on the component object so that Mithril's key
// algorithm can be applied. // algorithm can be applied.
if (componentProps.key) { if (componentProps.key) {
output.attrs = { key: componentProps.key }; output.attrs = {key: componentProps.key};
} }
return output; return output;
@@ -217,5 +220,6 @@ export default class Component {
* @param {Object} props * @param {Object} props
* @public * @public
*/ */
static initProps(props) {} static initProps(props) {
}
} }

View File

@@ -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,38 +154,31 @@ 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
data: 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.lazyRedraw(); m.lazyRedraw();
throw response; throw response;
} }
); );
} }
/** /**
@@ -195,23 +190,16 @@ export default class Model {
* @public * @public
*/ */
delete(data, options = {}) { delete(data, options = {}) {
if (!this.exists) return m.deferred().resolve().promise; if (!this.exists) return m.deferred.resolve().promise;
return app return app.request(Object.assign({
.request( 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;
data, 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
}; };
} }
} }

View File

@@ -31,16 +31,11 @@ export default class Session {
* @public * @public
*/ */
login(data, options = {}) { login(data, options = {}) {
return app.request( return app.request(Object.assign({
Object.assign( method: 'POST',
{ url: app.forum.attribute('baseUrl') + '/login',
method: 'POST', data
url: app.forum.attribute('baseUrl') + '/login', }, options));
data,
},
options
)
);
} }
/** /**

View File

@@ -34,7 +34,9 @@ export default class Store {
pushPayload(payload) { pushPayload(payload) {
if (payload.included) payload.included.map(this.pushObject.bind(this)); if (payload.included) payload.included.map(this.pushObject.bind(this));
const result = payload.data instanceof Array ? payload.data.map(this.pushObject.bind(this)) : this.pushObject(payload.data); const result = payload.data instanceof Array
? payload.data.map(this.pushObject.bind(this))
: this.pushObject(payload.data);
// Attach the original payload to the model that we give back. This is // Attach the original payload to the model that we give back. This is
// useful to consumers as it allows them to access meta information // useful to consumers as it allows them to access meta information
@@ -56,7 +58,7 @@ export default class Store {
pushObject(data) { pushObject(data) {
if (!this.models[data.type]) return null; if (!this.models[data.type]) return null;
const type = (this.data[data.type] = this.data[data.type] || {}); const type = this.data[data.type] = this.data[data.type] || {};
if (type[data.id]) { if (type[data.id]) {
type[data.id].pushData(data); type[data.id].pushData(data);
@@ -93,18 +95,11 @@ export default class Store {
url += '/' + id; url += '/' + id;
} }
return app return app.request(Object.assign({
.request( method: 'GET',
Object.assign( url,
{ data
method: 'GET', }, options)).then(this.pushPayload.bind(this));
url,
data,
},
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);
} }
} }

View File

@@ -67,7 +67,7 @@ export default class Translator {
const hydrated = []; const hydrated = [];
const open = [hydrated]; const open = [hydrated];
translation.forEach((part) => { translation.forEach(part => {
const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i')); const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));
if (match) { if (match) {
@@ -77,7 +77,7 @@ export default class Translator {
if (match[2]) { if (match[2]) {
open.shift(); open.shift();
} else { } else {
let tag = input[match[3]] || { tag: match[3], children: [] }; let tag = input[match[3]] || {tag: match[3], children: []};
open[0].push(tag); open[0].push(tag);
open.unshift(tag.children || tag); open.unshift(tag.children || tag);
} }
@@ -87,7 +87,7 @@ export default class Translator {
} }
}); });
return hydrated.filter((part) => part); return hydrated.filter(part => part);
} }
pluralize(translation, number) { pluralize(translation, number) {
@@ -97,7 +97,7 @@ export default class Translator {
standardRules = [], standardRules = [],
explicitRules = []; explicitRules = [];
translation.split('|').forEach((part) => { translation.split('|').forEach(part => {
if (cPluralRegex.test(part)) { if (cPluralRegex.test(part)) {
const matches = part.match(cPluralRegex); const matches = part.match(cPluralRegex);
explicitRules[matches[0]] = matches[matches.length - 1]; explicitRules[matches[0]] = matches[matches.length - 1];
@@ -122,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];
} }
} }
@@ -225,7 +223,7 @@ export default class Translator {
case 'tr': case 'tr':
case 'ur': case 'ur':
case 'zu': case 'zu':
return number == 1 ? 0 : 1; return (number == 1) ? 0 : 1;
case 'am': case 'am':
case 'bh': case 'bh':
@@ -239,7 +237,7 @@ export default class Translator {
case 'xbr': case 'xbr':
case 'ti': case 'ti':
case 'wa': case 'wa':
return number === 0 || number == 1 ? 0 : 1; return ((number === 0) || (number == 1)) ? 0 : 1;
case 'be': case 'be':
case 'bs': case 'bs':
@@ -247,41 +245,41 @@ export default class Translator {
case 'ru': case 'ru':
case 'sr': case 'sr':
case 'uk': case 'uk':
return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2; return ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2);
case 'cs': case 'cs':
case 'sk': case 'sk':
return number == 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2; return (number == 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2);
case 'ga': case 'ga':
return number == 1 ? 0 : number == 2 ? 1 : 2; return (number == 1) ? 0 : ((number == 2) ? 1 : 2);
case 'lt': case 'lt':
return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2; return ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2);
case 'sl': case 'sl':
return number % 100 == 1 ? 0 : number % 100 == 2 ? 1 : number % 100 == 3 || number % 100 == 4 ? 2 : 3; return (number % 100 == 1) ? 0 : ((number % 100 == 2) ? 1 : (((number % 100 == 3) || (number % 100 == 4)) ? 2 : 3));
case 'mk': case 'mk':
return number % 10 == 1 ? 0 : 1; return (number % 10 == 1) ? 0 : 1;
case 'mt': case 'mt':
return number == 1 ? 0 : number === 0 || (number % 100 > 1 && number % 100 < 11) ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3; return (number == 1) ? 0 : (((number === 0) || ((number % 100 > 1) && (number % 100 < 11))) ? 1 : (((number % 100 > 10) && (number % 100 < 20)) ? 2 : 3));
case 'lv': case 'lv':
return number === 0 ? 0 : number % 10 == 1 && number % 100 != 11 ? 1 : 2; return (number === 0) ? 0 : (((number % 10 == 1) && (number % 100 != 11)) ? 1 : 2);
case 'pl': case 'pl':
return number == 1 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2; return (number == 1) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12) || (number % 100 > 14))) ? 1 : 2);
case 'cy': case 'cy':
return number == 1 ? 0 : number == 2 ? 1 : number == 8 || number == 11 ? 2 : 3; return (number == 1) ? 0 : ((number == 2) ? 1 : (((number == 8) || (number == 11)) ? 2 : 3));
case 'ro': case 'ro':
return number == 1 ? 0 : number === 0 || (number % 100 > 0 && number % 100 < 20) ? 1 : 2; return (number == 1) ? 0 : (((number === 0) || ((number % 100 > 0) && (number % 100 < 20))) ? 1 : 2);
case 'ar': case 'ar':
return number === 0 ? 0 : number == 1 ? 1 : number == 2 ? 2 : number >= 3 && number <= 10 ? 3 : number >= 11 && number <= 99 ? 4 : 5; return (number === 0) ? 0 : ((number == 1) ? 1 : ((number == 2) ? 2 : (((number >= 3) && (number <= 10)) ? 3 : (((number >= 11) && (number <= 99)) ? 4 : 5))));
default: default:
return 0; return 0;

View File

@@ -37,7 +37,6 @@ 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';
@@ -62,9 +61,9 @@ import userOnline from './helpers/userOnline';
import listItems from './helpers/listItems'; import listItems from './helpers/listItems';
export default { export default {
extend: extend, 'extend': extend,
Session: Session, 'Session': Session,
Store: Store, 'Store': Store,
'utils/evented': evented, 'utils/evented': evented,
'utils/liveHumanTimes': liveHumanTimes, 'utils/liveHumanTimes': liveHumanTimes,
'utils/ItemList': ItemList, 'utils/ItemList': ItemList,
@@ -91,8 +90,8 @@ export default {
'models/Discussion': Discussion, 'models/Discussion': Discussion,
'models/Group': Group, 'models/Group': Group,
'models/Forum': Forum, 'models/Forum': Forum,
Component: Component, 'Component': Component,
Translator: Translator, 'Translator': Translator,
'components/AlertManager': AlertManager, 'components/AlertManager': AlertManager,
'components/Switch': Switch, 'components/Switch': Switch,
'components/Badge': Badge, 'components/Badge': Badge,
@@ -101,7 +100,6 @@ 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,
@@ -113,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,
@@ -123,5 +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
}; };

View File

@@ -35,13 +35,22 @@ export default class Alert extends Component {
const dismissControl = []; const dismissControl = [];
if (dismissible || dismissible === undefined) { if (dismissible || dismissible === undefined) {
dismissControl.push(<Button icon="fas fa-times" className="Button Button--link Button--icon Alert-dismiss" onclick={ondismiss} />); dismissControl.push(
<Button
icon="fas fa-times"
className="Button Button--link Button--icon Alert-dismiss"
onclick={ondismiss}/>
);
} }
return ( return (
<div {...attrs}> <div {...attrs}>
<span className="Alert-body">{children}</span> <span className="Alert-body">
<ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul> {children}
</span>
<ul className="Alert-controls">
{listItems(controls.concat(dismissControl))}
</ul>
</div> </div>
); );
} }

View File

@@ -19,9 +19,7 @@ export default class AlertManager extends Component {
view() { view() {
return ( return (
<div className="AlertManager"> <div className="AlertManager">
{this.components.map((component) => ( {this.components.map(component => <div className="AlertManager-alert">{component}</div>)}
<div className="AlertManager-alert">{component}</div>
))}
</div> </div>
); );
} }

View File

@@ -24,12 +24,16 @@ export default class Badge extends Component {
attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || ''); attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
attrs.title = extract(attrs, 'label') || ''; attrs.title = extract(attrs, 'label') || '';
return <span {...attrs}>{iconName ? icon(iconName, { className: 'Badge-icon' }) : m.trust('&nbsp;')}</span>; return (
<span {...attrs}>
{iconName ? icon(iconName, {className: 'Badge-icon'}) : m.trust('&nbsp;')}
</span>
);
} }
config(isInitialized) { config(isInitialized) {
if (isInitialized) return; if (isInitialized) return;
if (this.props.label) this.$().tooltip({ container: 'body' }); if (this.props.label) this.$().tooltip({container: 'body'});
} }
} }

View File

@@ -29,12 +29,6 @@ export default class Button extends Component {
attrs.className = attrs.className || ''; 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 && !this.props.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 && this.props.children) { if (!attrs.title && this.props.children) {
attrs.title = extractText(this.props.children); attrs.title = extractText(this.props.children);
@@ -62,9 +56,9 @@ export default class Button extends Component {
const iconName = this.props.icon; const iconName = this.props.icon;
return [ return [
iconName && iconName !== true ? icon(iconName, { className: 'Button-icon' }) : '', iconName && iconName !== true ? icon(iconName, {className: 'Button-icon'}) : '',
this.props.children ? <span className="Button-label">{this.props.children}</span> : '', this.props.children ? <span className="Button-label">{this.props.children}</span> : '',
this.props.loading ? LoadingIndicator.component({ size: 'tiny', className: 'LoadingIndicator--inline' }) : '', this.props.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : ''
]; ];
} }
} }

View File

@@ -31,8 +31,13 @@ export default class Checkbox extends Component {
return ( return (
<label className={className}> <label className={className}>
<input type="checkbox" checked={this.props.state} disabled={this.props.disabled} onchange={m.withAttr('checked', this.onchange.bind(this))} /> <input type="checkbox"
<div className="Checkbox-display">{this.getDisplay()}</div> checked={this.props.state}
disabled={this.props.disabled}
onchange={m.withAttr('checked', this.onchange.bind(this))}/>
<div className="Checkbox-display">
{this.getDisplay()}
</div>
{this.props.children} {this.props.children}
</label> </label>
); );
@@ -45,7 +50,9 @@ export default class Checkbox extends Component {
* @protected * @protected
*/ */
getDisplay() { getDisplay() {
return this.loading ? LoadingIndicator.component({ size: 'tiny' }) : icon(this.props.state ? 'fas fa-check' : 'fas fa-times'); return this.loading
? LoadingIndicator.component({size: 'tiny'})
: icon(this.props.state ? 'fas fa-check' : 'fas fa-times');
} }
/** /**

View File

@@ -64,13 +64,19 @@ export default class Dropdown extends Component {
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right'); $menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()); $menu.toggleClass(
'Dropdown-menu--top',
$menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
);
if ($menu.offset().top < 0) { if ($menu.offset().top < 0) {
$menu.removeClass('Dropdown-menu--top'); $menu.removeClass('Dropdown-menu--top');
} }
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()); $menu.toggleClass(
'Dropdown-menu--right',
isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()
);
}); });
this.$().on('hidden.bs.dropdown', () => { this.$().on('hidden.bs.dropdown', () => {
@@ -92,7 +98,10 @@ export default class Dropdown extends Component {
*/ */
getButton() { getButton() {
return ( return (
<button className={'Dropdown-toggle ' + this.props.buttonClassName} data-toggle="dropdown" onclick={this.props.onclick}> <button
className={'Dropdown-toggle ' + this.props.buttonClassName}
data-toggle="dropdown"
onclick={this.props.onclick}>
{this.getButtonContent()} {this.getButtonContent()}
</button> </button>
); );
@@ -106,13 +115,17 @@ export default class Dropdown extends Component {
*/ */
getButtonContent() { getButtonContent() {
return [ return [
this.props.icon ? icon(this.props.icon, { className: 'Button-icon' }) : '', this.props.icon ? icon(this.props.icon, {className: 'Button-icon'}) : '',
<span className="Button-label">{this.props.label}</span>, <span className="Button-label">{this.props.label}</span>,
this.props.caretIcon ? icon(this.props.caretIcon, { className: 'Button-caret' }) : '', this.props.caretIcon ? icon(this.props.caretIcon, {className: 'Button-caret'}) : ''
]; ];
} }
getMenu(items) { getMenu(items) {
return <ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>{items}</ul>; return (
<ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
{items}
</ul>
);
} }
} }

View File

@@ -6,7 +6,7 @@ export default class GroupBadge extends Badge {
if (props.group) { if (props.group) {
props.icon = props.group.icon(); props.icon = props.group.icon();
props.style = { backgroundColor: props.group.color() }; props.style = {backgroundColor: props.group.color()};
props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label; props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label;
props.type = 'group--' + props.group.id(); props.type = 'group--' + props.group.id();

View File

@@ -33,6 +33,8 @@ export default class LinkButton extends Button {
* @return {Boolean} * @return {Boolean}
*/ */
static isActive(props) { static isActive(props) {
return typeof props.active !== 'undefined' ? props.active : m.route() === props.href; return typeof props.active !== 'undefined'
? props.active
: m.route() === props.href;
} }
} }

View File

@@ -31,12 +31,10 @@ export default class Modal extends Component {
{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">
@@ -67,7 +65,8 @@ export default class Modal extends Component {
* @return {String} * @return {String}
* @abstract * @abstract
*/ */
className() {} className() {
}
/** /**
* Get the title of the modal dialog. * Get the title of the modal dialog.
@@ -75,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.
@@ -83,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.
@@ -99,7 +101,8 @@ export default class Modal extends Component {
this.$('form').find('input, select, textarea').first().focus().select(); this.$('form').find('input, select, textarea').first().focus().select();
} }
onhide() {} onhide() {
}
/** /**
* Hide the modal. * Hide the modal.

View File

@@ -13,7 +13,11 @@ export default class ModalManager extends Component {
} }
view() { view() {
return <div className="ModalManager modal fade">{this.component && this.component.render()}</div>; return (
<div className="ModalManager modal fade">
{this.component && this.component.render()}
</div>
);
} }
config(isInitialized, context) { config(isInitialized, context) {
@@ -24,7 +28,9 @@ export default class ModalManager extends Component {
// to be retained across route changes. // to be retained across route changes.
context.retain = true; context.retain = true;
this.$().on('hidden.bs.modal', this.clear.bind(this)).on('shown.bs.modal', this.onready.bind(this)); this.$()
.on('hidden.bs.modal', this.clear.bind(this))
.on('shown.bs.modal', this.onready.bind(this));
} }
/** /**
@@ -47,13 +53,8 @@ export default class ModalManager extends Component {
m.redraw(true); m.redraw(true);
const dismissible = !!this.component.isDismissible(); this.$().modal({backdrop: this.component.isDismissible() ? true : 'static'}).modal('show');
this.$() this.onready();
.modal({
backdrop: dismissible || 'static',
keyboard: dismissible,
})
.modal('show');
} }
/** /**

View File

@@ -19,15 +19,15 @@ import LinkButton from './LinkButton';
*/ */
export default class Navigation extends Component { export default class Navigation extends Component {
view() { view() {
const { history, pane } = app; const {history, pane} = app;
return ( return (
<div <div className={'Navigation ButtonGroup ' + (this.props.className || '')}
className={'Navigation ButtonGroup ' + (this.props.className || '')}
onmouseenter={pane && pane.show.bind(pane)} onmouseenter={pane && pane.show.bind(pane)}
onmouseleave={pane && pane.onmouseleave.bind(pane)} onmouseleave={pane && pane.onmouseleave.bind(pane)}>
> {history.canGoBack()
{history.canGoBack() ? [this.getBackButton(), this.getPaneButton()] : this.getDrawerButton()} ? [this.getBackButton(), this.getPaneButton()]
: this.getDrawerButton()}
</div> </div>
); );
} }
@@ -46,7 +46,7 @@ export default class Navigation extends Component {
* @protected * @protected
*/ */
getBackButton() { getBackButton() {
const { history } = app; const {history} = app;
const previous = history.getPrevious() || {}; const previous = history.getPrevious() || {};
return LinkButton.component({ return LinkButton.component({
@@ -55,11 +55,11 @@ export default class Navigation extends Component {
icon: 'fas fa-chevron-left', icon: 'fas fa-chevron-left',
title: previous.title, title: previous.title,
config: () => {}, config: () => {},
onclick: (e) => { onclick: e => {
if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return; if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault(); e.preventDefault();
history.back(); history.back();
}, }
}); });
} }
@@ -70,14 +70,14 @@ export default class Navigation extends Component {
* @protected * @protected
*/ */
getPaneButton() { getPaneButton() {
const { pane } = app; const {pane} = app;
if (!pane || !pane.active) return ''; if (!pane || !pane.active) return '';
return Button.component({ return Button.component({
className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''), className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''),
onclick: pane.togglePinned.bind(pane), onclick: pane.togglePinned.bind(pane),
icon: 'fas fa-thumbtack', icon: 'fas fa-thumbtack'
}); });
} }
@@ -90,16 +90,17 @@ export default class Navigation extends Component {
getDrawerButton() { getDrawerButton() {
if (!this.props.drawer) return ''; if (!this.props.drawer) return '';
const { drawer } = app; const {drawer} = app;
const user = app.session.user; const user = app.session.user;
return Button.component({ return Button.component({
className: 'Button Button--icon Navigation-drawer' + (user && user.newNotificationCount() ? ' new' : ''), className: 'Button Button--icon Navigation-drawer' +
onclick: (e) => { (user && user.newNotificationCount() ? ' new' : ''),
onclick: e => {
e.stopPropagation(); e.stopPropagation();
drawer.show(); drawer.show();
}, },
icon: 'fas fa-bars', icon: 'fas fa-bars'
}); });
} }
} }

View File

@@ -1,32 +0,0 @@
import Modal from './Modal';
export default class RequestErrorModal extends Modal {
className() {
return 'RequestErrorModal Modal--large';
}
title() {
return this.props.error.xhr ? this.props.error.xhr.status + ' ' + this.props.error.xhr.statusText : '';
}
content() {
let responseText;
try {
responseText = JSON.stringify(JSON.parse(this.props.error.responseText), null, 2);
} catch (e) {
responseText = this.props.error.responseText;
}
return (
<div className="Modal-body">
<pre>
{this.props.error.options.method} {this.props.error.options.url}
<br />
<br />
{responseText}
</pre>
</div>
);
}
}

View File

@@ -8,25 +8,17 @@ import icon from '../helpers/icon';
* - `options` A map of option values to labels. * - `options` A map of option values to labels.
* - `onchange` A callback to run when the selected value is changed. * - `onchange` A callback to run when the selected value is changed.
* - `value` The value of the selected option. * - `value` The value of the selected option.
* - `disabled` Disabled state for the input.
*/ */
export default class Select extends Component { export default class Select extends Component {
view() { view() {
const { options, onchange, value, disabled } = this.props; const {options, onchange, value} = this.props;
return ( return (
<span className="Select"> <span className="Select">
<select <select className="Select-input FormControl" onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined} value={value}>
className="Select-input FormControl" {Object.keys(options).map(key => <option value={key}>{options[key]}</option>)}
onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined}
value={value}
disabled={disabled}
>
{Object.keys(options).map((key) => (
<option value={key}>{options[key]}</option>
))}
</select> </select>
{icon('fas fa-sort', { className: 'Select-caret' })} {icon('fas fa-sort', {className: 'Select-caret'})}
</span> </span>
); );
} }

View File

@@ -21,11 +21,14 @@ export default class SelectDropdown extends Dropdown {
} }
getButtonContent() { getButtonContent() {
const activeChild = this.props.children.filter((child) => child.props.active)[0]; const activeChild = this.props.children.filter(child => child.props.active)[0];
let label = (activeChild && activeChild.props.children) || this.props.defaultLabel; let label = activeChild && activeChild.props.children || this.props.defaultLabel;
if (label instanceof Array) label = label[0]; if (label instanceof Array) label = label[0];
return [<span className="Button-label">{label}</span>, icon(this.props.caretIcon, { className: 'Button-caret' })]; return [
<span className="Button-label">{label}</span>,
icon(this.props.caretIcon, {className: 'Button-caret'})
];
} }
} }

View File

@@ -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"/>;
} }
} }

View File

@@ -24,10 +24,12 @@ export default class SplitDropdown extends Dropdown {
return [ return [
Button.component(buttonProps), Button.component(buttonProps),
<button className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName} data-toggle="dropdown"> <button
{icon(this.props.icon, { className: 'Button-icon' })} className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName}
{icon('fas fa-caret-down', { className: 'Button-caret' })} data-toggle="dropdown">
</button>, {icon(this.props.icon, {className: 'Button-icon'})}
{icon('fas fa-caret-down', {className: 'Button-caret'})}
</button>
]; ];
} }

View File

@@ -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));
}; };

View File

@@ -31,11 +31,11 @@ export default class Routes {
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));
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -25,11 +25,11 @@ export default function avatar(user, attrs = {}) {
if (hasTitle) attrs.title = attrs.title || username; if (hasTitle) attrs.title = attrs.title || username;
if (avatarUrl) { if (avatarUrl) {
return <img {...attrs} src={avatarUrl} />; return <img {...attrs} src={avatarUrl}/>;
} }
content = username.charAt(0).toUpperCase(); content = username.charAt(0).toUpperCase();
attrs.style = { background: user.color() }; attrs.style = {background: user.color()};
} }
return <span {...attrs}>{content}</span>; return <span {...attrs}>{content}</span>;

View File

@@ -11,9 +11,5 @@ export default function fullTime(time) {
const datetime = mo.format(); const datetime = mo.format();
const full = mo.format('LLLL'); const full = mo.format('LLLL');
return ( return <time pubdate datetime={datetime}>{full}</time>;
<time pubdate datetime={datetime}>
{full}
</time>
);
} }

View File

@@ -15,9 +15,5 @@ export default function humanTime(time) {
const full = mo.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>
);
} }

View File

@@ -8,5 +8,5 @@
export default function icon(fontClass, attrs = {}) { export default function icon(fontClass, attrs = {}) {
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || ''); attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
return <i {...attrs} />; return <i {...attrs}/>;
} }

View File

@@ -29,7 +29,7 @@ function withoutUnnecessarySeparators(items) {
export default function listItems(items) { export default function listItems(items) {
if (!(items instanceof Array)) items = [items]; if (!(items instanceof Array)) items = [items];
return withoutUnnecessarySeparators(items).map((item) => { return withoutUnnecessarySeparators(items).map(item => {
const isListItem = item.component && item.component.isListItem; const isListItem = item.component && item.component.isListItem;
const active = item.component && item.component.isActive && item.component.isActive(item.props); const active = item.component && item.component.isActive && item.component.isActive(item.props);
const className = item.props ? item.props.itemClassName : item.itemClassName; const className = item.props ? item.props.itemClassName : item.itemClassName;
@@ -39,12 +39,15 @@ export default function listItems(items) {
item.attrs.key = item.attrs.key || item.itemName; item.attrs.key = item.attrs.key || item.itemName;
} }
return isListItem ? ( return isListItem
item ? item
) : ( : <li className={classList([
<li className={classList([item.itemName ? 'item-' + item.itemName : '', className, active ? 'active' : ''])} key={item.itemName}> (item.itemName ? 'item-' + item.itemName : ''),
{item} className,
</li> (active ? 'active' : '')
); ])}
key={item.itemName}>
{item}
</li>;
}); });
} }

View File

@@ -13,7 +13,7 @@ export default function punctuateSeries(items) {
if (items.length === 2) { if (items.length === 2) {
return app.translator.trans('core.lib.series.two_text', { return app.translator.trans('core.lib.series.two_text', {
first: items[0], first: items[0],
second: items[1], second: items[1]
}); });
} else if (items.length >= 3) { } else if (items.length >= 3) {
// If there are three or more items, we will join all but the first and // If there are three or more items, we will join all but the first and
@@ -27,7 +27,7 @@ export default function punctuateSeries(items) {
return app.translator.trans('core.lib.series.three_text', { return app.translator.trans('core.lib.series.three_text', {
first: items[0], first: items[0],
second, second,
third: items[items.length - 1], third: items[items.length - 1]
}); });
} }

View File

@@ -7,7 +7,7 @@ import icon from './icon';
* @return {Object} * @return {Object}
*/ */
export default function userOnline(user) { export default function userOnline(user) {
if (user.lastSeenAt() && user.isOnline()) { if (user.lastSeenAt() && user.isOnline()) {
return <span className="UserOnline">{icon('fas fa-circle')}</span>; return <span className="UserOnline">{icon('fas fa-circle')}</span>;
} }
} }

View File

@@ -19,18 +19,18 @@ Object.assign(Discussion.prototype, {
lastPostNumber: Model.attribute('lastPostNumber'), lastPostNumber: Model.attribute('lastPostNumber'),
commentCount: Model.attribute('commentCount'), commentCount: Model.attribute('commentCount'),
replyCount: computed('commentCount', (commentCount) => Math.max(0, commentCount - 1)), replyCount: computed('commentCount', commentCount => Math.max(0, commentCount - 1)),
posts: Model.hasMany('posts'), posts: Model.hasMany('posts'),
mostRelevantPost: Model.hasOne('mostRelevantPost'), mostRelevantPost: Model.hasOne('mostRelevantPost'),
lastReadAt: Model.attribute('lastReadAt', Model.transformDate), lastReadAt: Model.attribute('lastReadAt', Model.transformDate),
lastReadPostNumber: Model.attribute('lastReadPostNumber'), lastReadPostNumber: Model.attribute('lastReadPostNumber'),
isUnread: computed('unreadCount', (unreadCount) => !!unreadCount), isUnread: computed('unreadCount', unreadCount => !!unreadCount),
isRead: computed('unreadCount', (unreadCount) => app.session.user && !unreadCount), isRead: computed('unreadCount', unreadCount => app.session.user && !unreadCount),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate), hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'), hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt), isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt),
canReply: Model.attribute('canReply'), canReply: Model.attribute('canReply'),
canRename: Model.attribute('canRename'), canRename: Model.attribute('canRename'),
@@ -84,7 +84,7 @@ Object.assign(Discussion.prototype, {
const items = new ItemList(); const items = new ItemList();
if (this.isHidden()) { if (this.isHidden()) {
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')} />); items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')}/>);
} }
return items; return items;
@@ -99,6 +99,6 @@ Object.assign(Discussion.prototype, {
postIds() { postIds() {
const posts = this.data.relationships.posts; const posts = this.data.relationships.posts;
return posts ? posts.data.map((link) => link.id) : []; return posts ? posts.data.map(link => link.id) : [];
}, }
}); });

View File

@@ -6,7 +6,7 @@ Object.assign(Group.prototype, {
nameSingular: Model.attribute('nameSingular'), nameSingular: Model.attribute('nameSingular'),
namePlural: Model.attribute('namePlural'), namePlural: Model.attribute('namePlural'),
color: Model.attribute('color'), color: Model.attribute('color'),
icon: Model.attribute('icon'), icon: Model.attribute('icon')
}); });
Group.ADMINISTRATOR_ID = '1'; Group.ADMINISTRATOR_ID = '1';

View File

@@ -11,5 +11,5 @@ Object.assign(Notification.prototype, {
user: Model.hasOne('user'), user: Model.hasOne('user'),
fromUser: Model.hasOne('fromUser'), fromUser: Model.hasOne('fromUser'),
subject: Model.hasOne('subject'), subject: Model.hasOne('subject')
}); });

View File

@@ -17,13 +17,13 @@ Object.assign(Post.prototype, {
editedAt: Model.attribute('editedAt', Model.transformDate), editedAt: Model.attribute('editedAt', Model.transformDate),
editedUser: Model.hasOne('editedUser'), editedUser: Model.hasOne('editedUser'),
isEdited: computed('editedAt', (editedAt) => !!editedAt), isEdited: computed('editedAt', editedAt => !!editedAt),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate), hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'), hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt), isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt),
canEdit: Model.attribute('canEdit'), canEdit: Model.attribute('canEdit'),
canHide: Model.attribute('canHide'), canHide: Model.attribute('canHide'),
canDelete: Model.attribute('canDelete'), canDelete: Model.attribute('canDelete')
}); });

View File

@@ -32,7 +32,7 @@ Object.assign(User.prototype, {
canDelete: Model.attribute('canDelete'), canDelete: Model.attribute('canDelete'),
avatarColor: null, avatarColor: null,
color: computed('username', 'avatarUrl', 'avatarColor', function (username, avatarUrl, avatarColor) { color: computed('username', 'avatarUrl', 'avatarColor', function(username, avatarUrl, avatarColor) {
// If we've already calculated and cached the dominant color of the user's // If we've already calculated and cached the dominant color of the user's
// avatar, then we can return that in RGB format. If we haven't, we'll want // avatar, then we can return that in RGB format. If we haven't, we'll want
// to calculate it. Unless the user doesn't have an avatar, in which case // to calculate it. Unless the user doesn't have an avatar, in which case
@@ -67,8 +67,8 @@ Object.assign(User.prototype, {
const groups = this.groups(); const groups = this.groups();
if (groups) { if (groups) {
groups.forEach((group) => { groups.forEach(group => {
items.add('group' + group.id(), GroupBadge.component({ group })); items.add('group' + group.id(), GroupBadge.component({group}));
}); });
} }
@@ -85,13 +85,12 @@ Object.assign(User.prototype, {
const image = new Image(); const image = new Image();
const user = this; const user = this;
image.onload = function () { image.onload = function() {
const colorThief = new ColorThief(); const colorThief = new ColorThief();
user.avatarColor = colorThief.getColor(this); user.avatarColor = colorThief.getColor(this);
user.freshness = new Date(); user.freshness = new Date();
m.redraw(); m.redraw();
}; };
image.crossOrigin = 'anonymous';
image.src = this.avatarUrl(); image.src = this.avatarUrl();
}, },
@@ -106,6 +105,6 @@ Object.assign(User.prototype, {
Object.assign(preferences, newPreferences); Object.assign(preferences, newPreferences);
return this.save({ preferences }); return this.save({preferences});
}, }
}); });

View File

@@ -7,7 +7,7 @@ export default class Drawer {
constructor() { constructor() {
// Set up an event handler so that whenever the content area is tapped, // Set up an event handler so that whenever the content area is tapped,
// the drawer will close. // the drawer will close.
$('#content').click((e) => { $('#content').click(e => {
if (this.isOpen()) { if (this.isOpen()) {
e.preventDefault(); e.preventDefault();
this.hide(); this.hide();

View File

@@ -28,7 +28,7 @@ export default class ItemList {
*/ */
isEmpty() { isEmpty() {
for (const i in this.items) { for (const i in this.items) {
if (this.items.hasOwnProperty(i)) { if(this.items.hasOwnProperty(i)) {
return false; return false;
} }
} }
@@ -147,15 +147,14 @@ export default class ItemList {
} }
} }
return items return items.sort((a, b) => {
.sort((a, b) => { if (a.priority === b.priority) {
if (a.priority === b.priority) { return a.key - b.key;
return a.key - b.key; } else if (a.priority > b.priority) {
} else if (a.priority > b.priority) { return -1;
return -1; }
} return 1;
return 1; }).map(item => item.content);
})
.map((item) => item.content);
} }
} }

View File

@@ -1,10 +1,9 @@
const later = const scroll = window.requestAnimationFrame ||
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame || window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame || window.msRequestAnimationFrame ||
window.oRequestAnimationFrame || window.oRequestAnimationFrame ||
((callback) => window.setTimeout(callback, 1000 / 60)); (callback => window.setTimeout(callback, 1000 / 60));
/** /**
* The `ScrollListener` class sets up a listener that handles window scroll * The `ScrollListener` class sets up a listener that handles window scroll
@@ -18,7 +17,7 @@ export default class ScrollListener {
*/ */
constructor(callback) { constructor(callback) {
this.callback = callback; this.callback = callback;
this.ticking = false; this.lastTop = -1;
} }
/** /**
@@ -28,27 +27,27 @@ export default class ScrollListener {
* @protected * @protected
*/ */
loop() { loop() {
// THROTTLE: If the callback is still running (or hasn't yet run), we ignore if (!this.active) return;
// further scroll events.
if (this.ticking) return;
// Schedule the callback to be executed soon (TM), and stop throttling once this.update();
// the callback is done.
later(() => {
this.update();
this.ticking = false;
});
this.ticking = true; scroll(this.loop.bind(this));
} }
/** /**
* Run the callback, whether there was a scroll event or not. * Check if the scroll position has changed; if it has, run the handler.
* *
* @param {Boolean} [force=false] Whether or not to force the handler to be
* run, even if the scroll position hasn't changed.
* @public * @public
*/ */
update() { update(force) {
this.callback(window.pageYOffset); const top = window.pageYOffset;
if (this.lastTop !== top || force) {
this.callback(top);
this.lastTop = top;
}
} }
/** /**
@@ -58,7 +57,8 @@ export default class ScrollListener {
*/ */
start() { start() {
if (!this.active) { if (!this.active) {
window.addEventListener('scroll', (this.active = this.loop.bind(this))); this.active = true;
this.loop();
} }
} }
@@ -68,8 +68,6 @@ export default class ScrollListener {
* @public * @public
*/ */
stop() { stop() {
window.removeEventListener('scroll', this.active); this.active = false;
this.active = null;
} }
} }

View File

@@ -44,7 +44,7 @@ export default class SubtreeRetainer {
} }
}); });
return needsRebuild ? false : { subtree: 'retain' }; return needsRebuild ? false : {subtree: 'retain'};
} }
/** /**

View File

@@ -13,7 +13,7 @@ export default function classList(classes) {
let classNames; let classNames;
if (classes instanceof Array) { if (classes instanceof Array) {
classNames = classes.filter((name) => name); classNames = classes.filter(name => name);
} else { } else {
classNames = []; classNames = [];

View File

@@ -14,12 +14,12 @@ export default function computed(...dependentKeys) {
const dependentValues = {}; const dependentValues = {};
let computedValue; let computedValue;
return function () { return function() {
let recompute = false; let recompute = false;
// Read all of the dependent values. If any of them have changed since last // Read all of the dependent values. If any of them have changed since last
// time, then we'll want to recompute our output. // time, then we'll want to recompute our output.
keys.forEach((key) => { keys.forEach(key => {
const value = typeof this[key] === 'function' ? this[key]() : this[key]; const value = typeof this[key] === 'function' ? this[key]() : this[key];
if (dependentValues[key] !== value) { if (dependentValues[key] !== value) {
@@ -29,10 +29,7 @@ export default function computed(...dependentKeys) {
}); });
if (recompute) { if (recompute) {
computedValue = compute.apply( computedValue = compute.apply(this, keys.map(key => dependentValues[key]));
this,
keys.map((key) => dependentValues[key])
);
} }
return computedValue; return computedValue;

View File

@@ -34,7 +34,7 @@ export default {
* @public * @public
*/ */
trigger(event, ...args) { trigger(event, ...args) {
this.getHandlers(event).forEach((handler) => handler.apply(this, args)); this.getHandlers(event).forEach(handler => handler.apply(this, args));
}, },
/** /**
@@ -55,7 +55,7 @@ export default {
* @param {function} handler The function to handle the event. * @param {function} handler The function to handle the event.
*/ */
one(event, handler) { one(event, handler) {
const wrapper = function () { const wrapper = function() {
handler.apply(this, arguments); handler.apply(this, arguments);
this.off(event, wrapper); this.off(event, wrapper);
@@ -77,5 +77,5 @@ export default {
if (index !== -1) { if (index !== -1) {
handlers.splice(index, 1); handlers.splice(index, 1);
} }
}, }
}; }

View File

@@ -6,7 +6,7 @@
*/ */
export default function extractText(vdom) { export default function extractText(vdom) {
if (vdom instanceof Array) { if (vdom instanceof Array) {
return vdom.map((element) => extractText(element)).join(''); return vdom.map(element => extractText(element)).join('');
} else if (typeof vdom === 'object' && vdom !== null) { } else if (typeof vdom === 'object' && vdom !== null) {
return extractText(vdom.children); return extractText(vdom.children);
} else { } else {

Some files were not shown because too many files have changed in this diff Show More