1
0
mirror of https://github.com/flarum/core.git synced 2025-08-13 20:04:24 +02:00

Compare commits

..

219 Commits

Author SHA1 Message Date
Franz Liedke
6e8999090e Clean up code 2020-04-17 14:16:23 +02:00
David Sevilla Martin
eac5390058 Add load previous page button, fix IE issues 2020-04-17 14:14:57 +02:00
David Sevilla Martin
b321585ee8 Change 'page' param in DiscussionList when loading more results, properly use existing param 2020-04-17 14:13:00 +02:00
flarum-bot
798a3486bf Bundled output for commit 89ef14faf1 [skip ci] 2020-04-17 09:59:47 +00:00
Franz Liedke
89ef14faf1 Run prettier for all JS files 2020-04-17 11:57:55 +02:00
Franz Liedke
84cf938379 Merge pull request #2099 from flarum/fl/prettier
Install prettier for consistent JS styling
2020-04-17 11:20:52 +02:00
Franz Liedke
899cdfda4e CI: Run prettier to check for JS code formatting 2020-04-17 11:14:37 +02:00
Franz Liedke
72ed4faa83 Setup husky for automatic formatting before commit 2020-04-17 10:45:36 +02:00
Franz Liedke
64ad21e5da Add NPM shortcut for running prettier 2020-04-17 10:45:05 +02:00
Franz Liedke
14e8e9a7cb Configure prettier via JSON file 2020-04-17 10:44:36 +02:00
Franz Liedke
ee996e2cae Install prettier 2020-04-17 10:44:31 +02:00
Franz Liedke
7b35674e4a Merge pull request #2117 from flarum/fl/2055-streamline-uploads
Simplify uploads, avoid Application contract
2020-04-15 22:52:03 +02:00
Franz Liedke
1d953b3514 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-04-13 09:59:07 +00:00
Franz Liedke
b7d8f77529 Tweak event extender (tests)
- Inject contract, not implementation
- Do not dispatch event in test, let the core do that
- Ensure the relevant database tables are reset prior to the test
- Use correct parameter order for assertions

Refs #2097.
2020-04-13 11:58:47 +02:00
Franz Liedke
b343206c7b Tweak mail extender (tests)
- Use private over protected
- Use "public" API for building requests in tests
- Add more assertions
- Formatting
- Use correct parameter order for assertions

Refs #2012.
2020-04-13 11:58:46 +02:00
flarum-bot
2aead54aea Bundled output for commit dbfae0b55e [skip ci] 2020-04-13 09:22:40 +00:00
Alexander Skvortsov
dbfae0b55e Add year, localization support for displaying things older than 1 year (#2034) 2020-04-13 11:21:27 +02:00
Alexander Skvortsov
2d86eb9b9f Mail Extender (#2012)
This allows registering new drivers, or overwriting existing ones.
2020-04-13 10:46:33 +02:00
Alexander Skvortsov
3ac5e58fa1 Add event extender (used for domain events) (#2097) 2020-04-13 10:45:34 +02:00
Alexander Skvortsov
ffa56595c3 Improved UI of Switch with loading indicator (#2039)
* Moved loading indicator outside of checkboxes to improve ui
* Made loading indicator more visible, fade label when switch is loading
2020-04-10 22:51:58 +02:00
flarum-bot
453c44632d Bundled output for commit 117c2f65ac [skip ci] 2020-04-10 19:18:00 +00:00
w-4
117c2f65ac Fix PostStreamScrubber click (#1945) 2020-04-10 21:16:57 +02:00
Franz Liedke
cd9edf656b ForumSerializer: Use UrlGenerator for base URLs
The test from the previous commit proves this works as intended. :)

This is one more step in trying to avoid the widespread usage of the
`Application` godclass.

Refs #2055.
2020-04-10 17:46:15 +02:00
Franz Liedke
8c19ba1aaa Add integration test for API root endpoint 2020-04-10 17:46:15 +02:00
Hasan Özbey
3f5554816e Fix mobile notification bubble on colored header (#2109) 2020-04-10 12:50:36 +02:00
flarum-bot
cb9801a324 Bundled output for commit fd4c0d30d8 [skip ci] 2020-04-10 10:32:46 +00:00
Taraflex
fd4c0d30d8 Protect dismissible modals from closing by ESC key 2020-04-10 12:30:56 +02:00
Franz Liedke
922e294668 Permissions page: Tweak icon styling
- Give them a fixed width (independent of font library)
- Center the icons in their column
- De-emphasize the icons by applying a muted color

Fixes #2016, closes #2018.
2020-04-10 12:01:04 +02:00
Franz Liedke
1fa37a7a6a Simplify uploads, inject filesystem instances
This avoids injecting the Application god class and assembling default
file locations in multiple places.

In addition, we no longer use the `MountManager` for these uploads. It
only added complexity (by moving tmp files around) and will not be
available in the next major release of Flysystem.

Note: Passing PSR upload streams to Intervention Image requires an
explicit upgrade of the library. (Very likely, users have already
updated to the newer versions, as the old constraint allowed it, but
we should be explicit for correctness' sake.)
2020-04-10 11:38:57 +02:00
Franz Liedke
1cbb2a365e Validate PSR-compatible file upload
Instead of converting the uploaded file object to an UploadedFile
instance from Symfony, because the latter is compatible with
Laravel's validation, let's re-implement the validation for the
three rules we were using.

The benefit: we can now avoid copying the uploaded file to a
temporary location just to do the wrapping.

In the next step, we will remove the temporary file and let the
uploader / Intervention Image handle the PSR stream directly.
2020-04-10 11:38:55 +02:00
Charlie
4c50c8d77a Change default discussion comment count
This allows new public discussions to be immediately visible by users.
2020-04-08 01:13:52 +02:00
Alexander Skvortsov
0d57820b50 Added CSRF Extender (#2095) 2020-04-03 21:32:18 +02:00
flarum-bot
ecdd7a2b49 Bundled output for commit 30942bdf38 [skip ci] 2020-04-03 19:27:57 +00:00
Sami Mazouz
30942bdf38 Fix new post injected above unread sticky (#1868)
Refresh the discussion list instead of prepending the new post
2020-04-03 21:26:51 +02:00
Alexander Skvortsov
345ad4bc6d Add console extender (#2057)
* Made the console command system extender-friendly

* Added console extender

* Added ConsoleTestCase to integration tests

* Added integration tests for console extender

* Marked event-based console extension system as deprecated

* Moved trimming command output of whitespace into superclass

* Renamed 'add' to 'command'

* Added special processing for laravel commands

* Code style fixes

* More style fixes

* Fixed $this->container
2020-04-03 19:38:54 +02:00
Alexander Skvortsov
03a4997a1c Send emails through the queue 2020-04-03 13:47:12 +02:00
flarum-bot
857fd95b5e Bundled output for commit dd43e49d0a [skip ci] 2020-04-03 10:03:45 +00:00
Franz Liedke
dd43e49d0a Update JS dependencies to secure versions 2020-04-03 12:02:18 +02:00
Franz Liedke
4efdd2a4f2 Deprecations: Add removal dates and replacements 2020-04-03 11:46:32 +02:00
Hasan Özbey
b286e39429 fix extensions page layout 2020-04-03 11:44:02 +02:00
Franz Liedke
1cda9dca4f Revert BC breaks around notification blueprints
No need for breaking backwards compatibility here - encapsulating the
logic for `getAttributes()` in one place turns out to be quite useful.

Refs #1931.
2020-04-03 11:33:33 +02:00
flarum-bot
e16d57d4e2 Bundled output for commit 2e2aa8747e [skip ci] 2020-04-01 12:42:05 +00:00
Daniël Klabbers
2e2aa8747e fixed an issue with Post--by-start-user for discussions that contain posts of deleted users 2020-04-01 14:40:40 +02:00
flarum-bot
44ac2ec8ee Bundled output for commit 6bbd603a41 [skip ci] 2020-03-30 19:19:56 +00:00
Hasan Özbey
6bbd603a41 Update ModalManager.js 2020-03-30 21:18:48 +02:00
Hasan Özbey
a4910f3d94 Update Modal.less 2020-03-30 21:18:48 +02:00
Hasan Özbey
f003f6e04a fix modals 2020-03-30 21:18:48 +02:00
Franz Liedke
2fe3987c19 Use UrlGenerator over Application for base URL
We need to get rid of this god class, as Laravel's Application contract
gets even bigger with 5.8. To avoid having to add all these methods, we
should try to stop using it where we can.
2020-03-28 11:17:45 +01:00
Franz Liedke
f4ab6f4b1f Laravel: Stop calling deprecated fire() method
This has been deprecated and removed from the contract for a long time,
and it will be completely dropped in v5.8, our next upgrade target.
2020-03-28 11:08:44 +01:00
Franz Liedke
9ae8bcdffe Make tests compatible with PHPUnit 8 2020-03-28 11:06:47 +01:00
Franz Liedke
29bdd471bc Merge pull request #1931 from flarum/dk/1869-queue-notifications
Notifications into the queue
2020-03-27 23:06:36 +01:00
Franz Liedke
fb70826469 Add new method to DiscussionRenamedBlueprint 2020-03-27 16:22:39 +01:00
Franz Liedke
bbe7e97ba1 Add BC layer for notification blueprints
This gives extension authors time to add the new `getAttributes()`
method to their `BlueprintInterface` implementations.

The layer itself is easy to remove in beta.14.
2020-03-27 16:22:38 +01:00
Franz Liedke
310065fb1c Remove unnecessary constructor parameter 2020-03-27 16:22:38 +01:00
Franz Liedke
23da7b3373 Remove Notifying event for now
As discussed with @luceos, let's add this once the use case comes up. It
might be a left-over from a previous state of this PR anyway.
2020-03-27 16:22:37 +01:00
Daniël Klabbers
2ba29a9088 Moved sending emails to the syncer
This separates sending each individual mail, thus hardening the app.
There are still many improvements possible in this code, e.g. chaining
these commands, making emails just another notification type and
listening to the Notify event instead. We can postpone this to a later
stable release.
2020-03-27 16:22:37 +01:00
Daniël Klabbers
cd8a8e9dd7 Notifications into the queue
Forces notifications into a dedicated SendNotificationsJob and passed
to the queue.

- One static method re-used in the job ::getAttributes, is that okay or
  use a trait?
- Do we want to use this solution and refactor into a better Hub after
  stable, postpone this implementation or use it in b11?
2020-03-27 16:16:36 +01:00
Franz Liedke
6b3d634917 Convert last two controller tests to request tests 2020-03-27 13:39:38 +01:00
Daniël Klabbers
8c6fac62d6 fixes checking for enabled extension and correct pointer of 30c6ea9912 2020-03-27 13:29:16 +01:00
Franz Liedke
02e72f4b03 Rename API tests for more consistency
I could not come up with a noun for the new "UpdateTest" for users, so
this is easier in terms of consistency.
2020-03-27 13:22:27 +01:00
Franz Liedke
e3f1e69748 Convert more controller tests to request tests 2020-03-27 13:21:10 +01:00
Matt Kilgore
bc7cea6e61 Fix test and extender for middleware (#2084) 2020-03-27 11:00:30 +01:00
Daniël Klabbers
30c6ea9912 Resolved enabled extension test
The getEnabled method returns all extensions (previously) enabled, yet manually
uninstalled through composer. This does not reference the exact, current state
of the forum. getEnabledExtensions returns a list where the getEnabled list
is filtered on the extensions found in the composer installed.json file.
2020-03-25 11:47:39 +01:00
Matt Kilgore
0bc06e1bb1 fix insertAfter and insertBefore middleware extender functions (#2063) 2020-03-20 22:59:57 +01:00
Franz Liedke
b10a17529d Convert more controller tests to request tests 2020-03-20 18:54:20 +01:00
Franz Liedke
bc80085ce4 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-03-20 17:28:58 +00:00
Franz Liedke
f31fbc5bcf Tests: Use new authenticatedAs option where useful
There are two more API integration tests that explicitly add the
"Authorization" header right now:

- `Flarum\Tests\integration\api\authentication\WithApiKeyTest`
- `Flarum\Tests\integration\api\csrf_protection\RequireCsrfTokenTest`

These two specifically test authentication, so in those cases the
explicitness seems desirable.
2020-03-20 18:28:35 +01:00
Franz Liedke
25f772c1ea Replace authenticatedRequest() by request() option
I feel this makes the parameters a bit more clear, does not rely on
inheritance (you can only inherit from one class, but we might want more
of these helpers in the future), and has less side effects (e.g. no
creation and, more importantly, deletion of users in the database).

Refs #2052.
2020-03-20 18:23:06 +01:00
Franz Liedke
a13c0bb612 Tests: Extract trait for building requests 2020-03-20 17:51:03 +01:00
Alexander Skvortsov
4791cc77b3 Add Authenticated Test Case utility 2020-03-20 17:18:35 +01:00
Alexander Skvortsov
e10da825d4 Users should not be able to restore discussions if deleted by admins (#2037) 2020-03-20 15:57:03 +01:00
Franz Liedke
a2d1d2b819 Update less.php to version 3.0
Now that we require PHP 7.2, this ensures we get the latest updates and
fixes as well.

Refs #1988.
2020-03-17 23:12:23 +01:00
Matt Kilgore
fb277df3b0 Change Extenders properties to private (#1958) 2020-03-17 22:37:17 +01:00
dependabot[bot]
a854fa8bcb Bump acorn from 6.4.0 to 6.4.1 in /js (#2065)
Bumps [acorn](https://github.com/acornjs/acorn) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.4.0...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-03-17 22:29:36 +01:00
Franz Liedke
bc69588785 CI: Fix broken build on GitHub Actions
The MySQL service is no longer started by default on these agents.

See https://github.blog/changelog/2020-02-21-github-actions-breaking-change-ubuntu-virtual-environments-will-no-longer-start-the-mysql-service-automatically/.
2020-03-17 22:23:11 +01:00
flarum-bot
a2d1245e90 Bundled output for commit 090b05736a [skip ci] 2020-03-09 12:41:19 +00:00
Daniël Klabbers
090b05736a showing start user in class list now 2020-03-09 13:39:26 +01:00
Franz Liedke
4b45ce0a58 Add a baseline test for the middleware extender
Refs #2017.
2020-03-06 15:05:16 +01:00
Franz Liedke
9f8ee7dc94 Fix typo 2020-03-06 15:05:15 +01:00
Franz Liedke
4413848c11 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-03-06 13:55:39 +00:00
Matt Kilgore
9212330ac2 Test Middleware extender (#2017) 2020-03-06 14:55:21 +01:00
Daniël Klabbers
455d070599 start using a dev stability version constant during the cycle 2020-03-05 10:50:30 +01:00
Franz Liedke
84ae88794f Remove deprecated ConfigureMiddleware Event (#2032) 2020-03-04 23:02:05 +01:00
Franz Liedke
ec3e9c722b Remove deprecated Flarum\Util\Str class 2020-03-04 22:59:14 +01:00
Franz Liedke
2e6cd584aa Remove mail settings backwards compatibility layer 2020-03-04 22:58:15 +01:00
Franz Liedke
27b0d1802e Merge branch 'refs/heads/v0.1.0-beta.12'
# Conflicts:
#	composer.json
2020-03-04 22:56:37 +01:00
Daniël Klabbers
2c02702d60 updated core developers in authors 2020-03-03 15:47:00 +01:00
Daniël Klabbers
9c3a016123 Update Application.php
updated version constant for b12
2020-03-03 15:38:15 +01:00
Alexander Skvortsov
0d208dc443 Drop support for PHP 7.1 (#2014)
* Updated PHP requirement to 7.2

* Set wikimedia less version to 1.8

* Indentation fix on composer json

* Revert "Set wikimedia less version to 1.8"

This reverts commit 22d862fd98.
2020-02-27 00:52:03 +01:00
Franz Liedke
e7c71ec445 Re-add mail settings backwards compatibility layer 2020-02-26 23:11:22 +01:00
Franz Liedke
46e2e17c3c Require new mail driver methods, remove BC layer 2020-02-26 22:56:09 +01:00
Alexander Skvortsov
f574f97174 Removed support for SES Mail Driver (#2011) 2020-02-26 22:36:27 +01:00
Alexander Skvortsov
674303b997 Remove Zend compatability bridge (#2010) 2020-02-26 22:29:44 +01:00
Franz Liedke
0fba2c0c0a Re-add util class and mark it as deprecated
This would be the only breaking change in beta.12. Let's stick to our
backwards compatibility promise / intentions as much as possible, even
if we assume the class has not been used anywhere.

This BC layer will be removed again for beta.13.

Refs #1975.
2020-02-26 22:14:23 +01:00
Franz Liedke
0666448ef5 Prepare changelog for beta.12 release 2020-02-26 21:10:52 +01:00
Matt Kilgore
08e40bc693 Force fixed version of text formatter 2020-02-25 11:27:59 +01:00
Franz Liedke
eaf1767008 Merge pull request #2002 from flarum/fl/extender-tests
Start testing extenders
2020-02-14 18:47:58 +01:00
flarum-bot
9f1eca555f Bundled output for commit 72fd32dbf6 [skip ci] 2020-02-14 14:57:19 +00:00
Clark Winkelmann
72fd32dbf6 Add disabled prop to the Select component (#1978) 2020-02-14 15:56:04 +01:00
Clark Winkelmann
d5ebbab3a7 Rename dead is_activated references with the new is_email_confirmed (#1974) 2020-02-14 15:34:32 +01:00
Matt Kilgore
17257aacaf Updates s9e/text-formatter to 2.x (#1982)
No additional changes required, tested with fof/formatting extension.
2020-02-14 12:34:40 +01:00
flarum-bot
f87c8c6dcd Bundled output for commit f9556d9d6a [skip ci] 2020-02-10 22:55:40 +00:00
D Mata
f9556d9d6a Update AvatarEditor.js onchange to oninput (#1570)
onchange does not work in IE11 and other IE browsers. This change works with all modern browsers as well.
2020-02-10 23:54:41 +01:00
Franz Liedke
fdfc6c0de2 CI: Test on PHP 7.4 as well
If we're lucky, this should fail right now, due to #1980.
2020-02-09 06:46:33 +01:00
Franz Liedke
64e4132c92 Switch to Wikimedia's less.php fork
The original library is no longer maintained. The fork supports PHP 7.4.
2020-02-09 06:46:33 +01:00
Franz Liedke
4b78a3114f Try to fix installer in PHP 7.4 2020-02-09 06:46:33 +01:00
Franz Liedke
c01eea58b6 Start testing Route extender 2020-02-08 00:04:32 +01:00
Franz Liedke
19cb74c856 Integration tests: Allow registering extenders 2020-02-07 23:29:14 +01:00
Franz Liedke
27bcdb949b Integration tests: Add lazy server helper
This allows sending requests directly in an integration test, without
having *explicitly* booted the app.
2020-02-07 23:28:37 +01:00
Franz Liedke
94fc460240 Integration tests: Create app lazily when needed
This will allow registering extenders in test scenarios. Previously,
this would not have had any effect as the app would have booted already.
2020-02-07 23:22:22 +01:00
flarum-bot
fc59f0fdd8 Bundled output for commit b91e903284 [skip ci] 2020-02-07 14:35:37 +00:00
Franz Liedke
b91e903284 Merge pull request #1938 from flarum/ds/1255-throttling-bypass-permission
Add permission to bypass throttling
2020-02-07 15:34:25 +01:00
David Sevilla Martín
711e775de7 Add permission to bypass throttling 2020-02-07 15:30:09 +01:00
flarum-bot
736e90d423 Bundled output for commit 2f3d9995d1 [skip ci] 2020-02-07 11:18:30 +00:00
Franz Liedke
2f3d9995d1 Fix race condition in post preview
The post composer could have been closed in between scheduling and
executing the callback.

Fixes flarum/org#58.
Refs #1881.
2020-02-07 12:17:11 +01:00
flarum-bot
ac14f84a9a Bundled output for commit 1d7641cbb0 [skip ci] 2020-02-07 11:06:58 +00:00
Franz Liedke
1d7641cbb0 Merge pull request #1921 from flarum/ds/1763-handle-incomplete-email-configuration
Improve handling of incomplete mail configuration
2020-02-07 12:05:41 +01:00
Franz Liedke
dce36cbeed New extender for error handling (#1970)
This extender implements several methods for extending the new error
handling stack implemented in #1843.

Most use-cases should be covered, but I expect some challenges for more
complex setups. We can tackle those once they come up, though. Basic
use-cases should be covered.

Fixes #1781.
2020-01-31 14:01:12 +01:00
flarum-bot
7e1087cba5 Bundled output for commit 8877bf97c4 [skip ci] 2020-01-31 12:34:20 +00:00
Franz Liedke
8877bf97c4 Merge pull request #1975 from flarum/fl/194-better-slugs
Use Laravel's slugger for basic transliteration
2020-01-31 13:32:55 +01:00
flarum-bot
7e74f5a03c Bundled output for commit 02ceed4fed [skip ci] 2020-01-26 22:38:29 +00:00
Clark Winkelmann
02ceed4fed Fix the "reply posted" alert empty body 2020-01-26 23:37:19 +01:00
Franz Liedke
27f159f6b8 Remove unnecessary use statement 2020-01-26 20:21:19 +01:00
ozzzzzzzam
499f33fbb6 Remove forum title from confirmation email subject (#1613)
The forum title is already used as the display name for the sender email address, so having it in the subject is just a duplication and waste of space.
2020-01-25 14:35:47 +01:00
Matthew Kilgore
8dd3bd420b Additional functionality for Middleware extender
Implements the remove, insertBefore, insertAfter and replace
functionality for middlewares.

The IoC container now holds one array of middleware (bindings) per
frontend stack - the extender operates on that array, before it is
wrapped in a middleware "pipe".

Fixes #1957, closes #1971.
2020-01-24 21:20:33 +01:00
Franz Liedke
2ca3188eff Add BC layer for mail driver configuration
By commenting out the new methods on the `DriverInterface` and checking
for these methods' existence before calling them, old implementations in
extensions will not break right away.

This will be removed after beta.12 is released, giving extension authors
about two months time to update their extensions.
2020-01-24 18:04:16 +01:00
Franz Liedke
f275bcdd2c Clarify the use-case of the JS slug helper 2020-01-24 17:42:14 +01:00
Franz Liedke
64c702aaf7 Use Laravel's slugger for basic transliteration
This is better than the current system, as it adds transliteration rules
for special characters, rather than just throwing all of them away.

For languages that cannot be transliterated to ASCII in a reasonable
manner, more possible improvements are outlined in #194.
2020-01-24 17:40:09 +01:00
Franz Liedke
833ea4e06e Connect labels with their form fields 2020-01-24 15:41:26 +01:00
Franz Liedke
5643ee649b Style validation errors 2020-01-24 15:41:26 +01:00
Franz Liedke
97b2db84c6 Mail drivers: Separate definition from validation 2020-01-24 15:41:26 +01:00
David Sevilla Martin
4fea25959c Change implementation to add validation rules, of which 'required' is shown in the frontend 2020-01-24 15:41:25 +01:00
David Sevilla Martin
8b70cec6a1 Add required fields, incomplete configuration warning, and null transport 2020-01-24 15:41:25 +01:00
flarum-bot
a330a8fa28 Bundled output for commit 02899d4f68 [skip ci] 2020-01-22 23:02:59 +00:00
David Sevilla Martín
02899d4f68 Add Content for User page, preload user & throw 404 accordingly (#1901) 2020-01-23 00:01:26 +01:00
Franz Liedke
76f7d566b2 Convert another test
Test the request, not a controller (implementation detail). This also
focuses on the observable behavior instead of hacking our way into the
middleware pipeline in order to observe internal behavior.

The authenticated user is now determined by looking at the API response
to compare permissions and (non-)existing JSON keys.
2020-01-22 23:39:41 +01:00
David Sevilla Martín
e296bbf0aa Initial template for Stale bot configuration (#1841) 2020-01-18 02:06:36 +01:00
Julian Berger
0a4ee93fde Get translations from fallback catalogues (#1961) 2020-01-17 23:37:50 +01:00
Franz Liedke
1e7fbf1ed9 Add backwards compatibility layer for mail drivers
Support the old format (a simple list of available fields), in addition
to the new format (a map from field names to their types + metadata).

This will be removed after beta.12 is released.
2020-01-14 11:45:44 +01:00
Franz Liedke
1170d5c2cf Document changes in mail driver interface 2020-01-14 11:44:29 +01:00
flarum-bot
fcbbedd884 Bundled output for commit 4c89e2eb77 [skip ci] 2020-01-10 17:18:42 +00:00
Vladimir Vinogradov
4c89e2eb77 Add Mailgun region setting
Fixes #1834.
2020-01-10 18:17:04 +01:00
Franz Liedke
809f353c52 Ensure page parameters are always integers 2020-01-09 00:45:50 +01:00
Matt Kilgore
d7a5a6ad14 Change Zend namespace to Laminas (#1963)
Also ensure backwards compatibility for extensions that use the Zend framework but don't explicitly require it.
2020-01-06 22:29:34 +01:00
luceos
ca0c52d60a Apply fixes from StyleCI
[ci skip] [skip ci]
2020-01-05 21:28:46 +00:00
Daniël Klabbers
2325e33e38 Update LICENSE 2020-01-05 22:28:27 +01:00
Matt Kilgore
aba291c542 Middleware extender (#1952) 2019-12-12 09:22:04 +01:00
flarum-bot
9b00244454 Bundled output for commit c1878fe29b [skip ci] 2019-12-10 14:40:37 +00:00
Franz Liedke
c1878fe29b Update Webpack 2019-12-10 15:38:56 +01:00
Franz Liedke
43c551929b Catch more exceptions during boot process
This extends our boot exception handling block to also catch and format
all exceptions that could be thrown while building our request handler,
i.e. the middleware stack handling requests.

The only exceptions that would now not be handled in this way could be
raised by Zend's `RequestHandlerRunner` and its delegates, which we
should be able to rely on.

Exceptions on request execution will be handled by the error handler in
the middleware stack.

Fixes #1607.
2019-12-07 01:16:48 +01:00
w-4
840e740309 Fix update page with custom base path (#1947)
Calling UpdateHandler causes RouteNotFoundException when basepath is not /.
2019-12-04 23:37:33 +01:00
Franz Liedke
babb36d375 Link to security policy from README 2019-12-04 21:51:53 +01:00
Franz Liedke
25b9d88469 FUNDING.yml does not inherit 2019-12-04 21:42:40 +01:00
Franz Liedke
b5c2285167 Add a custom FUNDING.yml file for this repository
Let's hope GitHub inherits the lines from our default community health
files at https://github.com/flarum/.github.
2019-12-04 21:41:41 +01:00
Daniël Klabbers
beaaa21f58 Update CHANGELOG.md 2019-12-02 10:36:41 +01:00
Daniel Klabbers
8a1bcf30d2 releasing beta 11.1 2019-12-02 09:28:48 +01:00
Franz Liedke
ff384569f8 Fix implementations of settings repo interface 2019-12-01 22:10:58 +01:00
Daniel Klabbers
f64a253450 Revert "7.4 release, forcing tests to work with them"
This reverts commit da5628d125.
2019-11-29 13:01:51 +01:00
Daniel Klabbers
da5628d125 7.4 release, forcing tests to work with them 2019-11-29 13:00:34 +01:00
David Sevilla Martín
a9c18c4753 Update Application version string to beta 11 2019-11-28 11:40:42 +01:00
Franz Liedke
d492579638 Apply fixes from StyleCI
[ci skip] [skip ci]
2019-11-28 00:16:50 +00:00
Franz Liedke
19188e3eda Update copyright claims in LICENSE 2019-11-28 01:14:16 +01:00
Daniel Klabbers
8cc44a695f preparing the changelog for beta 11, part 2 2019-11-26 13:23:09 +01:00
Daniel Klabbers
7bb8b66596 preparing the changelog for beta 11 2019-11-26 12:59:29 +01:00
Clark Winkelmann
40f709e7c6 Fix tests to include expectation count and run user saving events 2019-11-26 10:13:18 +01:00
Clark Winkelmann
264ff9f7bb Add unit test for AvatarUploader 2019-11-26 10:13:18 +01:00
Clark Winkelmann
308f2c9efd Fix avatar files not being deleted. Fixes #1918 2019-11-26 10:13:18 +01:00
flarum-bot
2a8ed53934 Bundled output for commit 17c86b82bf [skip ci] 2019-11-24 19:01:17 +00:00
w-4
17c86b82bf history back function fix
it shouldn't check for canGoBack again after the array pop()
2019-11-24 13:59:51 -05:00
Daniel Klabbers
63b039a800 incorrect ability used, drop prefix discussion. 2019-11-22 08:17:02 +01:00
Daniel Klabbers
213045aa03 test only on the hidePosts policy ability 2019-11-22 08:17:02 +01:00
Daniel Klabbers
6d10dbe9af resume chain in query builder 2019-11-22 08:17:02 +01:00
Daniël Klabbers
4adf342ce3 [review] using orWhere to allow any where to follow in extensions 2019-11-22 08:17:02 +01:00
Daniël Klabbers
b150636906 fixes #1827
- set default statement to block access
- added tests to confirm all scenarios work as intended
2019-11-22 08:17:02 +01:00
Franz Liedke
4f1adba387 Automatically set up Mockery for unit tests
- Use provided PhpUnit listener to enforce verification of expectations.
- Include Mockery's trait to auto-close Mockery after each test.
2019-11-21 00:51:11 +01:00
Franz Liedke
879b801600 Actually return null
Nullable return types require an explicit null return value; not
returning or returning without value is the "void" type.
2019-11-21 00:46:01 +01:00
David Sevilla Martin
c712d23e9c Add test for discussion posts being deleted on discussion delete from DB 2019-11-18 09:23:53 +01:00
David Sevilla Martin
d69c4035d9 Fix failing tests 2019-11-18 09:23:53 +01:00
datitisev
b83adbccfd Apply fixes from StyleCI
[ci skip] [skip ci]
2019-11-18 09:23:53 +01:00
David Sevilla Martin
7b6c666e7b Remove 'or' from 'orWhereNotExists' 2019-11-18 09:23:53 +01:00
David Sevilla Martin
8b9f03e998 Add discussion_id foreign key to posts table 2019-11-18 09:23:53 +01:00
flarum-bot
e69f8965c7 Bundled output for commit 6d2b50722a [skip ci] 2019-11-15 14:10:15 +00:00
Clark Winkelmann
6d2b50722a Pass event to KeyboardNavigatable whenCallback (#1922)
This way the callback can know which key is pressed.
2019-11-15 15:08:36 +01:00
Daniël Klabbers
99a05900b1 Fix the queue:restart command (#1932)
Adding a proxy callStatic on our simple implementation of the Manager class allows passing through calls like `forever()` to the underlying cache driver instance.
2019-11-15 15:01:31 +01:00
Franz Liedke
cc5e586d38 Add a docblock 2019-11-13 21:19:21 +01:00
Daniël Klabbers
17074b8aab only show queue commands if using another driver than sync 2019-11-13 13:17:09 +01:00
flarum-bot
406c8ff834 Bundled output for commit 1ba4a0b87e [skip ci] 2019-11-12 19:27:29 +00:00
Daniël Klabbers
1ba4a0b87e Fix existing Post component classes being dropped 2019-11-12 20:26:07 +01:00
flarum-bot
36017f89fe Bundled output for commit 1f2566c32c [skip ci] 2019-11-11 12:00:45 +00:00
Daniël Klabbers
1f2566c32c Improved naming of class for post by actor.
Made class list for post extensible by using a separate method.
2019-11-11 12:59:26 +01:00
flarum-bot
0c74927eab Bundled output for commit 19ecd968c6 [skip ci] 2019-11-11 11:15:09 +00:00
Matthew Kilgore
19ecd968c6 Removed LESS changes 2019-11-11 12:13:36 +01:00
Matthew Kilgore
fc64660f5d Set border to left side only 2019-11-11 12:13:36 +01:00
Matthew Kilgore
d5d769ebb1 Added border around post made by active user 2019-11-11 12:13:36 +01:00
flarum-bot
f5ee37b394 Bundled output for commit 54c5c09693 [skip ci] 2019-11-09 13:51:55 +00:00
David Sevilla Martin
54c5c09693 Cleanup some code and fix alert dismiss not working 2019-11-09 08:50:24 -05:00
Moritz Stueckler
c87ebaef08 feat: re-add debug button/modal
Fixes #1687
2019-11-09 08:50:24 -05:00
David Sevilla Martin
9c0d921f49 Fix Modal width on <768px screens not occupying the whole page 2019-11-06 17:48:00 -05:00
flarum-bot
d7bdc173a4 Bundled output for commit 937354512b [skip ci] 2019-10-28 15:37:34 +00:00
Daniël Klabbers
937354512b Update User.js
Use recommended `anonymous`, see https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/crossOrigin
2019-10-28 16:35:45 +01:00
J.C.Ködel
2dedfe4b92 Fix Color Thief cross origin bug
When users have external avatar urls (for instance: in a SSO environment where the avatar is provided by another domain), color thief fails to get the avatar dominant color because the canvas would be tainted. 

Following the instructions here (https://lokeshdhakar.com/projects/color-thief/ on the "Does it work if the image is hosted on another domain?"), adding an `image.crossOrigin = 'Anonymous';` solves the issue.

Tested on my forum which before suffered from a JS error and works fine (without this fix, the canvas remain in the `body` while an script error is thrown by color thief)
2019-10-28 16:35:45 +01:00
Franz Liedke
9f6ec80432 Revert search performance regression
We decided it is better to have a less intelligent search (that does not
match search terms in titles) for some people than a bad-performing
search for everyone.

We will revisit the search performance topic in the next release cycle,
possibly with larger changes around indexing.

Refs #1738, #1741, #1764.
2019-10-26 15:41:39 +02:00
Daniël Klabbers
aa31b8307d improve queue error handling 2019-10-18 13:13:30 +02:00
Daniël Klabbers
dc06d5b5c9 added return type hint to memory cache 2019-10-08 15:51:19 +02:00
Daniël Klabbers
2c867d2292 add type hinting to settings repository 2019-10-08 15:39:01 +02:00
luceos
b09ac3f3f8 Apply fixes from StyleCI
[ci skip] [skip ci]
2019-10-07 09:56:10 +02:00
Daniël Klabbers
21f54c5562 added ability to re-use existing error handling stack 2019-10-07 09:56:10 +02:00
David Sevilla Martin
a0ace316e8 Alias 'flarum.queue.connection' to Queue contract 2019-10-05 16:14:27 -04:00
Tariq Hussein
6c96c932e0 Fixes #1877 Replace getIdsForUsername() with subquery instead. (#1878) 2019-10-02 01:04:01 +02:00
flarum-bot
522e41aa71 Bundled output for commit bbd891965f [skip ci] 2019-10-01 21:50:25 +00:00
Madalin Tache
bbd891965f Update window size (#1894)
This small change attempts to fix #1727, as i just got my eye on it and figured i could simply fix it while seeing it.
2019-10-01 23:48:54 +02:00
flarum-bot
0f43445a90 Bundled output for commit 7a684660e9 [skip ci] 2019-10-01 21:39:07 +00:00
David Sevilla Martín
7a684660e9 Enable scrollbars in login button popups (#1900)
Fixes #1716
2019-10-01 23:37:42 +02:00
Daniël Klabbers
12dc4fff57 works towards #1789 by allowing event subscribing (#1810) 2019-10-01 11:12:46 +02:00
Franz Liedke
1b5a200781 Amend the existing rel attribute of links
...instead of overwriting. This will play more nicely with extensions.

Refs #859.
2019-09-26 23:02:39 +02:00
Franz Liedke
1bdf7764a9 Stop opening external links in new tabs
We accept that this may be desired by forum owners and will offer an
extension to enable this feature. By default, we will not make any
assumptions and simply adopt the web's and browsers' default behavior.

Fixes #859.
2019-09-26 23:01:24 +02:00
Franz Liedke
3417c0cbee Cleanup code from #1876
- Extract a method for email address generation
- Consistent types
- No docblocks for types where superfluous
- Tweak console output
- Don't inherit from integration test's base class in unit test
2019-09-24 01:00:22 +02:00
Stefan Totev
738ca405fe Normalize Base URL during installation
- Fix base url when is appended with a script filename
- Add default base url http://flarum.local when CLI wizard used
- Remove some code duplication
- Add minor improvement to the UX when CLI wizard used
- Add tests
- Extract base url normalisation into its own value object
2019-09-24 00:26:51 +02:00
Matteo Contrini
09609a9f20 Change rel for external links to nofollow ugc (#1884) 2019-09-23 23:37:49 +02:00
727 changed files with 8317 additions and 6285 deletions

3
.github/FUNDING.yml vendored Normal file
View File

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

26
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
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.

31
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
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

@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
php: [7.1, 7.2, 7.3]
php: [7.2, 7.3, 7.4]
service: ['mysql:5.7', mariadb]
prefix: ['', flarum_]
@@ -21,16 +21,16 @@ jobs:
prefixStr: (prefix)
exclude:
- php: 7.1
- php: 7.2
service: 'mysql:5.7'
prefix: flarum_
- php: 7.1
- php: 7.2
service: mariadb
prefix: flarum_
- php: 7.2
- php: 7.3
service: 'mysql:5.7'
prefix: flarum_
- php: 7.2
- php: 7.3
service: mariadb
prefix: flarum_
@@ -49,7 +49,9 @@ jobs:
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
- name: Create MySQL Database
run: mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
run: |
sudo systemctl start mysql
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
- name: Install Composer dependencies
run: composer install

View File

@@ -1,5 +1,62 @@
# 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

View File

@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) Toby Zerner
Copyright (c) 2019-2020 Stichting Flarum (Flarum Foundation)
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
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
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.
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).
## License

View File

@@ -5,17 +5,28 @@
"homepage": "https://flarum.org/",
"license": "MIT",
"authors": [
{
"name": "Toby Zerner",
"email": "toby.zerner@gmail.com"
},
{
"name": "Franz Liedke",
"email": "franz@develophp.org"
},
{
"name": "Daniel Klabbers",
"email": "daniel@klabbers.email"
"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": {
@@ -24,7 +35,7 @@
"docs": "https://flarum.org/docs/"
},
"require": {
"php": ">=7.1",
"php": ">=7.2",
"axy/sourcemap": "^0.1.4",
"components/font-awesome": "5.9.*",
"dflydev/fig-cookies": "^1.0.2",
@@ -45,7 +56,10 @@
"illuminate/support": "5.7.*",
"illuminate/validation": "5.7.*",
"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",
"matthiasmullie/minify": "^1.3",
"middlewares/base-path": "^1.1",
@@ -53,19 +67,17 @@
"middlewares/request-handler": "^1.2",
"monolog/monolog": "^1.16.0",
"nikic/fast-route": "^0.6",
"oyejorge/less.php": "^1.7",
"psr/http-message": "^1.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^1.2.0",
"s9e/text-formatter": "^2.3.6",
"symfony/config": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"tobscure/json-api": "^0.3.0",
"zendframework/zend-diactoros": "^1.8.4",
"zendframework/zend-httphandlerrunner": "^1.0",
"zendframework/zend-stratigility": "^3.0"
"wikimedia/less.php": "^3.0"
},
"require-dev": {
"mockery/mockery": "^1.0",

6
js/.prettierrc.json Normal file
View File

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

20
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

22
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

1468
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,12 +15,23 @@
"moment": "^2.22.2",
"punycode": "^2.1.1",
"spin.js": "^3.1.0",
"webpack": "^4.26.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.1.2",
"webpack-merge": "^4.1.4"
},
"devDependencies": {
"husky": "^4.2.5",
"prettier": "2.0.2"
},
"scripts": {
"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,
getPrevious: () => {},
backUrl: () => this.forum.attribute('baseUrl'),
back: function() {
back: function () {
window.location = this.backUrl();
}
},
};
constructor() {
@@ -27,7 +27,7 @@ export default class AdminApplication extends Application {
* @inheritdoc
*/
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-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
@@ -59,5 +59,5 @@ export default class AdminApplication extends Application {
}
return required;
};
}
}

View File

@@ -58,6 +58,6 @@ export default Object.assign(compat, {
'components/AdminNav': AdminNav,
'components/EditCustomCssModal': EditCustomCssModal,
'components/EditGroupModal': EditGroupModal,
'routes': routes,
'AdminApplication': AdminApplication
routes: routes,
AdminApplication: AdminApplication,
});

View File

@@ -22,8 +22,10 @@ export default class AddExtensionModal extends Modal {
return (
<div className="Modal-body">
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
<p>{app.translator.trans('core.admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p>
<p>{app.translator.trans('core.admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p>
<p>
{app.translator.trans('core.admin.add_extension.install_text', { a: <a href="https://discuss.flarum.org/t/extensions" target="_blank" /> })}
</p>
<p>{app.translator.trans('core.admin.add_extension.developer_text', { a: <a href="http://flarum.org/docs/extend" target="_blank" /> })}</p>
</div>
);
}

View File

@@ -13,11 +13,7 @@ export default class AdminLinkButton extends LinkButton {
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;
}

View File

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

View File

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

View File

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

View File

@@ -5,14 +5,12 @@ export default class DashboardPage extends Page {
view() {
return (
<div className="DashboardPage">
<div className="container">
{this.availableWidgets()}
</div>
<div className="container">{this.availableWidgets()}</div>
</div>
);
}
availableWidgets() {
return [<StatusWidget/>];
return [<StatusWidget />];
}
}

View File

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

View File

@@ -11,10 +11,14 @@ export default class EditCustomCssModal extends SettingsModal {
form() {
return [
<p>{app.translator.trans('core.admin.edit_css.customize_text', {a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank"/>})}</p>,
<p>
{app.translator.trans('core.admin.edit_css.customize_text', {
a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank" />,
})}
</p>,
<div className="Form-group">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')}/>
</div>
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')} />
</div>,
];
}

View File

@@ -13,8 +13,8 @@ export default class EditCustomFooterModal extends SettingsModal {
return [
<p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>,
<div className="Form-group">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')}/>
</div>
<textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')} />
</div>,
];
}

View File

@@ -13,8 +13,8 @@ export default class EditCustomHeaderModal extends SettingsModal {
return [
<p>{app.translator.trans('core.admin.edit_header.customize_text')}</p>,
<div className="Form-group">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_header')}/>
</div>
<textarea className="FormControl" rows="30" bidi={this.setting('custom_header')} />
</div>,
];
}

View File

@@ -24,21 +24,21 @@ export default class EditGroupModal extends Modal {
title() {
return [
this.color() || this.icon() ? Badge.component({
icon: this.icon(),
style: {backgroundColor: this.color()}
}) : '',
this.color() || this.icon()
? Badge.component({
icon: this.icon(),
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() {
return (
<div className="Modal-body">
<div className="Form">
{this.fields().toArray()}
</div>
<div className="Form">{this.fields().toArray()}</div>
</div>
);
}
@@ -46,40 +46,68 @@ export default class EditGroupModal extends Modal {
fields() {
const items = new ItemList();
items.add('name', <div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
<div className="EditGroupModal-name-input">
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
</div>
</div>, 30);
items.add(
'name',
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
<div className="EditGroupModal-name-input">
<input
className="FormControl"
placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')}
value={this.nameSingular()}
oninput={m.withAttr('value', this.nameSingular)}
/>
<input
className="FormControl"
placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')}
value={this.namePlural()}
oninput={m.withAttr('value', this.namePlural)}
/>
</div>
</div>,
30
);
items.add('color', <div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
</div>, 20);
items.add(
'color',
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)} />
</div>,
20
);
items.add('icon', <div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
<div className="helpText">
{app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1"/>})}
</div>
<input className="FormControl" placeholder="fas fa-bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
</div>, 10);
items.add(
'icon',
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
<div className="helpText">
{app.translator.trans('core.admin.edit_group.icon_text', { a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1" /> })}
</div>
<input className="FormControl" placeholder="fas fa-bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)} />
</div>,
10
);
items.add('submit', <div className="Form-group">
{Button.component({
type: 'submit',
className: 'Button Button--primary EditGroupModal-save',
loading: this.loading,
children: app.translator.trans('core.admin.edit_group.submit_button')
})}
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
{app.translator.trans('core.admin.edit_group.delete_button')}
</button>
) : ''}
</div>, -10);
items.add(
'submit',
<div className="Form-group">
{Button.component({
type: 'submit',
className: 'Button Button--primary EditGroupModal-save',
loading: this.loading,
children: app.translator.trans('core.admin.edit_group.submit_button'),
})}
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
{app.translator.trans('core.admin.edit_group.delete_button')}
</button>
) : (
''
)}
</div>,
-10
);
return items;
}
@@ -89,7 +117,7 @@ export default class EditGroupModal extends Modal {
nameSingular: this.nameSingular(),
namePlural: this.namePlural(),
color: this.color(),
icon: this.icon()
icon: this.icon(),
};
}
@@ -98,7 +126,8 @@ export default class EditGroupModal extends Modal {
this.loading = true;
this.group.save(this.submitData(), {errorHandler: this.onerror.bind(this)})
this.group
.save(this.submitData(), { errorHandler: this.onerror.bind(this) })
.then(this.hide.bind(this))
.catch(() => {
this.loading = false;

View File

@@ -19,7 +19,7 @@ export default class ExtensionsPage extends Page {
children: app.translator.trans('core.admin.extensions.add_button'),
icon: 'fas fa-plus',
className: 'Button Button--primary',
onclick: () => app.modal.show(new AddExtensionModal())
onclick: () => app.modal.show(new AddExtensionModal()),
})}
</div>
</div>
@@ -27,12 +27,12 @@ export default class ExtensionsPage extends Page {
<div className="ExtensionsPage-list">
<div className="container">
<ul className="ExtensionList">
{Object.keys(app.data.extensions)
.map(id => {
const extension = app.data.extensions[id];
const controls = this.controlItems(extension.id).toArray();
{Object.keys(app.data.extensions).map((id) => {
const extension = app.data.extensions[id];
const controls = this.controlItems(extension.id).toArray();
return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
return (
<li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
<div className="ExtensionListItem-content">
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
@@ -42,21 +42,25 @@ export default class ExtensionsPage extends Page {
className="ExtensionListItem-controls"
buttonClassName="Button Button--icon Button--flat"
menuClassName="Dropdown-menu--right"
icon="fas fa-ellipsis-h">
icon="fas fa-ellipsis-h"
>
{controls}
</Dropdown>
) : ''}
) : (
''
)}
<div className="ExtensionListItem-main">
<label className="ExtensionListItem-title">
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '}
{extension.extra['flarum-extension'].title}
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)} />{' '}
{extension.extra['flarum-extension'].title}
</label>
<div className="ExtensionListItem-version">{extension.version}</div>
<div className="ExtensionListItem-description">{extension.description}</div>
</div>
</div>
</li>;
})}
</li>
);
})}
</ul>
</div>
</div>
@@ -69,26 +73,34 @@ export default class ExtensionsPage extends Page {
const enabled = this.isEnabled(name);
if (app.extensionSettings[name]) {
items.add('settings', Button.component({
icon: 'fas fa-cog',
children: app.translator.trans('core.admin.extensions.settings_button'),
onclick: app.extensionSettings[name]
}));
items.add(
'settings',
Button.component({
icon: 'fas fa-cog',
children: app.translator.trans('core.admin.extensions.settings_button'),
onclick: app.extensionSettings[name],
})
);
}
if (!enabled) {
items.add('uninstall', Button.component({
icon: 'far fa-trash-alt',
children: app.translator.trans('core.admin.extensions.uninstall_button'),
onclick: () => {
app.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
method: 'DELETE'
}).then(() => window.location.reload());
items.add(
'uninstall',
Button.component({
icon: 'far fa-trash-alt',
children: app.translator.trans('core.admin.extensions.uninstall_button'),
onclick: () => {
app
.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
method: 'DELETE',
})
.then(() => window.location.reload());
app.modal.show(new LoadingModal());
}
}));
app.modal.show(new LoadingModal());
},
})
);
}
return items;
@@ -103,14 +115,16 @@ export default class ExtensionsPage extends Page {
toggle(id) {
const enabled = this.isEnabled(id);
app.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
method: 'PATCH',
data: {enabled: !enabled}
}).then(() => {
if (!enabled) localStorage.setItem('enabledExtension', id);
window.location.reload();
});
app
.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
method: 'PATCH',
data: { enabled: !enabled },
})
.then(() => {
if (!enabled) localStorage.setItem('enabledExtension', id);
window.location.reload();
});
app.modal.show(new LoadingModal());
}

View File

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

View File

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

View File

@@ -10,38 +10,45 @@ export default class MailPage extends Page {
init() {
super.init();
this.loading = true;
this.saving = false;
this.refresh();
}
refresh() {
this.loading = true;
this.driverFields = {};
this.fields = ['mail_driver', 'mail_from'];
this.values = {};
this.status = { sending: false, errors: {} };
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.request({
method: 'GET',
url: app.forum.attribute('apiUrl') + '/mail-drivers'
}).then(response => {
this.driverFields = response['data'].reduce(
(hash, driver) => ({...hash, [driver['id']]: driver['attributes']['fields']}),
{}
);
app
.request({
method: 'GET',
url: app.forum.attribute('apiUrl') + '/mail-settings',
})
.then((response) => {
this.driverFields = response['data']['attributes']['fields'];
this.status.sending = response['data']['attributes']['sending'];
this.status.errors = response['data']['attributes']['errors'];
Object.keys(this.driverFields).flatMap(key => this.driverFields[key]).forEach(
key => {
this.fields.push(key);
this.values[key] = m.prop(settings[key]);
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();
});
this.loading = false;
m.redraw();
});
}
view() {
if (this.loading) {
if (this.loading || this.saving) {
return (
<div className="MailPage">
<div className="container">
@@ -51,24 +58,27 @@ export default class MailPage extends Page {
);
}
const fields = this.driverFields[this.values.mail_driver()];
const fieldKeys = Object.keys(fields);
return (
<div className="MailPage">
<div className="container">
<form onsubmit={this.onsubmit.bind(this)}>
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
<div className="helpText">
{app.translator.trans('core.admin.email.text')}
</div>
<div className="helpText">{app.translator.trans('core.admin.email.text')}</div>
{FieldSet.component({
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.from_label')}</label>
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
</div>
]
<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)} />
</label>
</div>,
],
})}
{FieldSet.component({
@@ -76,31 +86,46 @@ export default class MailPage extends Page {
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
<Select value={this.values.mail_driver()} options={Object.keys(this.driverFields).reduce((memo, val) => ({...memo, [val]: val}), {})} onchange={this.values.mail_driver} />
</div>
]
<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>,
],
})}
{Object.keys(this.driverFields[this.values.mail_driver()]).length > 0 && FieldSet.component({
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
{this.driverFields[this.values.mail_driver()].flatMap(field => [
<label>{app.translator.trans(`core.admin.email.${field}_label`)}</label>,
<input className="FormControl" value={this.values[field]() || ''} oninput={m.withAttr('value', this.values[field])} />
])}
</div>
]
})}
{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({
type: 'submit',
className: 'Button Button--primary',
children: app.translator.trans('core.admin.email.submit_button'),
loading: this.saving,
disabled: !this.changed()
disabled: !this.changed(),
})}
</form>
</div>
@@ -108,8 +133,20 @@ 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() {
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) {
@@ -122,16 +159,16 @@ export default class MailPage extends Page {
const settings = {};
this.fields.forEach(key => settings[key] = this.values[key]());
this.fields.forEach((key) => (settings[key] = this.values[key]()));
saveSettings(settings)
.then(() => {
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
app.alerts.show((this.successAlert = new Alert({ type: 'success', children: app.translator.trans('core.admin.basics.saved_message') })));
})
.catch(() => {})
.then(() => {
this.saving = false;
m.redraw();
this.refresh();
});
}
}

View File

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

View File

@@ -13,12 +13,8 @@ export default class PermissionGrid extends Component {
view() {
const scopes = this.scopeItems().toArray();
const permissionCells = permission => {
return scopes.map(scope => (
<td>
{scope.render(permission)}
</td>
));
const permissionCells = (permission) => {
return scopes.map((scope) => <td>{scope.render(permission)}</td>);
};
return (
@@ -26,27 +22,32 @@ export default class PermissionGrid extends Component {
<thead>
<tr>
<td></td>
{scopes.map(scope => (
{scopes.map((scope) => (
<th>
{scope.label}{' '}
{scope.onremove ? Button.component({icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
{scope.onremove
? Button.component({ icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove })
: ''}
</th>
))}
<th>{this.scopeControlItems().toArray()}</th>
</tr>
</thead>
{this.permissions.map(section => (
{this.permissions.map((section) => (
<tbody>
<tr className="PermissionGrid-section">
<th>{section.label}</th>
{permissionCells(section)}
<td/>
<td />
</tr>
{section.children.map(child => (
{section.children.map((child) => (
<tr className="PermissionGrid-child">
<th>{icon(child.icon)}{child.label}</th>
<th>
{icon(child.icon)}
{child.label}
</th>
{permissionCells(child)}
<td/>
<td />
</tr>
))}
</tbody>
@@ -58,25 +59,41 @@ export default class PermissionGrid extends Component {
permissionItems() {
const items = new ItemList();
items.add('view', {
label: app.translator.trans('core.admin.permissions.read_heading'),
children: this.viewItems().toArray()
}, 100);
items.add(
'view',
{
label: app.translator.trans('core.admin.permissions.read_heading'),
children: this.viewItems().toArray(),
},
100
);
items.add('start', {
label: app.translator.trans('core.admin.permissions.create_heading'),
children: this.startItems().toArray()
}, 90);
items.add(
'start',
{
label: app.translator.trans('core.admin.permissions.create_heading'),
children: this.startItems().toArray(),
},
90
);
items.add('reply', {
label: app.translator.trans('core.admin.permissions.participate_heading'),
children: this.replyItems().toArray()
}, 80);
items.add(
'reply',
{
label: app.translator.trans('core.admin.permissions.participate_heading'),
children: this.replyItems().toArray(),
},
80
);
items.add('moderate', {
label: app.translator.trans('core.admin.permissions.moderate_heading'),
children: this.moderateItems().toArray()
}, 70);
items.add(
'moderate',
{
label: app.translator.trans('core.admin.permissions.moderate_heading'),
children: this.moderateItems().toArray(),
},
70
);
return items;
}
@@ -84,31 +101,44 @@ export default class PermissionGrid extends Component {
viewItems() {
const items = new ItemList();
items.add('viewDiscussions', {
icon: 'fas fa-eye',
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
permission: 'viewDiscussions',
allowGuest: true
}, 100);
items.add(
'viewDiscussions',
{
icon: 'fas fa-eye',
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
permission: 'viewDiscussions',
allowGuest: true,
},
100
);
items.add('viewUserList', {
icon: 'fas fa-users',
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
permission: 'viewUserList',
allowGuest: true
}, 100);
items.add(
'viewUserList',
{
icon: 'fas fa-users',
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
permission: 'viewUserList',
allowGuest: true,
},
100
);
items.add('signUp', {
icon: 'fas fa-user-plus',
label: app.translator.trans('core.admin.permissions.sign_up_label'),
setting: () => SettingDropdown.component({
key: 'allow_sign_up',
options: [
{value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button')},
{value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button')}
]
})
}, 90);
items.add(
'signUp',
{
icon: 'fas fa-user-plus',
label: app.translator.trans('core.admin.permissions.sign_up_label'),
setting: () =>
SettingDropdown.component({
key: 'allow_sign_up',
options: [
{ value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button') },
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button') },
],
}),
},
90
);
items.add('viewLastSeenAt', {
icon: 'far fa-clock',
@@ -122,31 +152,39 @@ export default class PermissionGrid extends Component {
startItems() {
const items = new ItemList();
items.add('start', {
icon: 'fas fa-edit',
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
permission: 'startDiscussion'
}, 100);
items.add(
'start',
{
icon: 'fas fa-edit',
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
permission: 'startDiscussion',
},
100
);
items.add('allowRenaming', {
icon: 'fas fa-i-cursor',
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
setting: () => {
const minutes = parseInt(app.data.settings.allow_renaming, 10);
items.add(
'allowRenaming',
{
icon: 'fas fa-i-cursor',
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
setting: () => {
const minutes = parseInt(app.data.settings.allow_renaming, 10);
return SettingDropdown.component({
defaultLabel: minutes
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_renaming',
options: [
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
]
});
}
}, 90);
return SettingDropdown.component({
defaultLabel: minutes
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, { count: minutes })
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_renaming',
options: [
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
],
});
},
},
90
);
return items;
}
@@ -154,31 +192,39 @@ export default class PermissionGrid extends Component {
replyItems() {
const items = new ItemList();
items.add('reply', {
icon: 'fas fa-reply',
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
permission: 'discussion.reply'
}, 100);
items.add(
'reply',
{
icon: 'fas fa-reply',
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
permission: 'discussion.reply',
},
100
);
items.add('allowPostEditing', {
icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
setting: () => {
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
items.add(
'allowPostEditing',
{
icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
setting: () => {
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
return SettingDropdown.component({
defaultLabel: minutes
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_post_editing',
options: [
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
]
});
}
}, 90);
return SettingDropdown.component({
defaultLabel: minutes
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, { count: minutes })
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_post_editing',
options: [
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
],
});
},
},
90
);
return items;
}
@@ -186,75 +232,121 @@ export default class PermissionGrid extends Component {
moderateItems() {
const items = new ItemList();
items.add('viewIpsPosts', {
icon: 'fas fa-bullseye',
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
permission: 'discussion.viewIpsPosts'
}, 110);
items.add(
'viewIpsPosts',
{
icon: 'fas fa-bullseye',
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
permission: 'discussion.viewIpsPosts',
},
110
);
items.add('renameDiscussions', {
icon: 'fas fa-i-cursor',
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
permission: 'discussion.rename'
}, 100);
items.add(
'renameDiscussions',
{
icon: 'fas fa-i-cursor',
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
permission: 'discussion.rename',
},
100
);
items.add('hideDiscussions', {
icon: 'far fa-trash-alt',
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
permission: 'discussion.hide'
}, 90);
items.add(
'hideDiscussions',
{
icon: 'far fa-trash-alt',
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
permission: 'discussion.hide',
},
90
);
items.add('deleteDiscussions', {
icon: 'fas fa-times',
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
permission: 'discussion.delete'
}, 80);
items.add(
'deleteDiscussions',
{
icon: 'fas fa-times',
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
permission: 'discussion.delete',
},
80
);
items.add('editPosts', {
icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
permission: 'discussion.editPosts'
}, 70);
items.add(
'postWithoutThrottle',
{
icon: 'fas fa-swimmer',
label: app.translator.trans('core.admin.permissions.post_without_throttle_label'),
permission: 'postWithoutThrottle',
},
70
);
items.add('hidePosts', {
icon: 'far fa-trash-alt',
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
permission: 'discussion.hidePosts'
}, 60);
items.add(
'editPosts',
{
icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
permission: 'discussion.editPosts',
},
70
);
items.add('deletePosts', {
icon: 'fas fa-times',
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
permission: 'discussion.deletePosts'
}, 60);
items.add(
'hidePosts',
{
icon: 'far fa-trash-alt',
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
permission: 'discussion.hidePosts',
},
60
);
items.add(
'deletePosts',
{
icon: 'fas fa-times',
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
permission: 'discussion.deletePosts',
},
60
);
items.add(
'userEdit',
{
icon: 'fas fa-user-cog',
label: app.translator.trans('core.admin.permissions.edit_users_label'),
permission: 'user.edit',
},
60
);
items.add('userEdit', {
icon: 'fas fa-user-cog',
label: app.translator.trans('core.admin.permissions.edit_users_label'),
permission: 'user.edit'
}, 60);
return items;
}
scopeItems() {
const items = new ItemList();
items.add('global', {
label: app.translator.trans('core.admin.permissions.global_heading'),
render: item => {
if (item.setting) {
return item.setting();
} else if (item.permission) {
return PermissionDropdown.component({
permission: item.permission,
allowGuest: item.allowGuest
});
}
items.add(
'global',
{
label: app.translator.trans('core.admin.permissions.global_heading'),
render: (item) => {
if (item.setting) {
return item.setting();
} else if (item.permission) {
return PermissionDropdown.component({
permission: item.permission,
allowGuest: item.allowGuest,
});
}
return '';
}
}, 100);
return '';
},
},
100
);
return items;
}

View File

@@ -11,29 +11,28 @@ export default class PermissionsPage extends Page {
<div className="PermissionsPage">
<div className="PermissionsPage-groups">
<div className="container">
{app.store.all('groups')
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.map(group => (
<button className="Button Group" onclick={() => app.modal.show(new EditGroupModal({group}))}>
{app.store
.all('groups')
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.map((group) => (
<button className="Button Group" onclick={() => app.modal.show(new EditGroupModal({ group }))}>
{GroupBadge.component({
group,
className: 'Group-icon',
label: null
label: null,
})}
<span className="Group-name">{group.namePlural()}</span>
</button>
))}
<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>
</button>
</div>
</div>
<div className="PermissionsPage-permissions">
<div className="container">
{PermissionGrid.component()}
</div>
<div className="container">{PermissionGrid.component()}</div>
</div>
</div>
);

View File

@@ -26,10 +26,7 @@ export default class SessionDropdown extends Dropdown {
getButtonContent() {
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>];
}
/**
@@ -40,11 +37,12 @@ export default class SessionDropdown extends Dropdown {
items() {
const items = new ItemList();
items.add('logOut',
items.add(
'logOut',
Button.component({
icon: 'fas fa-sign-out-alt',
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
);

View File

@@ -11,14 +11,14 @@ export default class SettingDropdown extends SelectDropdown {
props.caretIcon = 'fas fa-caret-down';
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;
return Button.component({
children: label,
icon: active ? 'fas fa-check' : true,
onclick: saveSettings.bind(this, {[props.key]: value}),
active
onclick: saveSettings.bind(this, { [props.key]: value }),
active,
});
});
}

View File

@@ -18,9 +18,7 @@ export default class SettingsModal extends Modal {
<div className="Form">
{this.form()}
<div className="Form-group">
{this.submitButton()}
</div>
<div className="Form-group">{this.submitButton()}</div>
</div>
</div>
);
@@ -28,11 +26,7 @@ export default class SettingsModal extends Modal {
submitButton() {
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')}
</Button>
);
@@ -47,7 +41,7 @@ export default class SettingsModal extends Modal {
dirty() {
const dirty = {};
Object.keys(this.settings).forEach(key => {
Object.keys(this.settings).forEach((key) => {
const value = this.settings[key]();
if (value !== app.data.settings[key]) {
@@ -67,10 +61,7 @@ export default class SettingsModal extends Modal {
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() {

View File

@@ -20,29 +20,27 @@ export default class StatusWidget extends DashboardWidget {
}
content() {
return (
<ul>{listItems(this.items().toArray())}</ul>
);
return <ul>{listItems(this.items().toArray())}</ul>;
}
items() {
const items = new ItemList();
items.add('tools', (
items.add(
'tools',
<Dropdown
label={app.translator.trans('core.admin.dashboard.tools_button')}
icon="fas fa-cog"
buttonClassName="Button"
menuClassName="Dropdown-menu--right">
<Button onclick={this.handleClearCache.bind(this)}>
{app.translator.trans('core.admin.dashboard.clear_cache_button')}
</Button>
menuClassName="Dropdown-menu--right"
>
<Button onclick={this.handleClearCache.bind(this)}>{app.translator.trans('core.admin.dashboard.clear_cache_button')}</Button>
</Dropdown>
));
);
items.add('version-flarum', [<strong>Flarum</strong>, <br/>, app.forum.attribute('version')]);
items.add('version-php', [<strong>PHP</strong>, <br/>, app.data.phpVersion]);
items.add('version-mysql', [<strong>MySQL</strong>, <br/>, app.data.mysqlVersion]);
items.add('version-flarum', [<strong>Flarum</strong>, <br />, app.forum.attribute('version')]);
items.add('version-php', [<strong>PHP</strong>, <br />, app.data.phpVersion]);
items.add('version-mysql', [<strong>MySQL</strong>, <br />, app.data.mysqlVersion]);
return items;
}
@@ -50,9 +48,11 @@ export default class StatusWidget extends DashboardWidget {
handleClearCache(e) {
app.modal.show(new LoadingModal());
app.request({
method: 'DELETE',
url: app.forum.attribute('apiUrl') + '/cache'
}).then(() => window.location.reload());
app
.request({
method: 'DELETE',
url: app.forum.attribute('apiUrl') + '/cache',
})
.then(() => window.location.reload());
}
}

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ export { app };
// Export public API
// Export compat API
import compat from './compat';

View File

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

View File

@@ -3,12 +3,14 @@ export default function saveSettings(settings) {
Object.assign(app.data.settings, settings);
return app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/settings',
data: settings
}).catch(error => {
app.data.settings = oldSettings;
throw error;
});
return app
.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/settings',
data: settings,
})
.catch((error) => {
app.data.settings = oldSettings;
throw error;
});
}

View File

@@ -1,7 +1,9 @@
import ItemList from './utils/ItemList';
import Alert from './components/Alert';
import Button from './components/Button';
import ModalManager from './components/ModalManager';
import AlertManager from './components/AlertManager';
import RequestErrorModal from './components/RequestErrorModal';
import Translator from './Translator';
import Store from './Store';
import Session from './Session';
@@ -84,7 +86,7 @@ export default class Application {
discussions: Discussion,
posts: Post,
groups: Group,
notifications: Notification
notifications: Notification,
});
/**
@@ -124,24 +126,21 @@ export default class Application {
}
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.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();
}
bootExtensions(extensions) {
Object.keys(extensions).forEach(name => {
Object.keys(extensions).forEach((name) => {
const extension = extensions[name];
const extenders = flattenDeep(extension.extend);
for (const extender of extenders) {
@@ -151,26 +150,20 @@ export default class Application {
}
mount(basePath = '') {
this.modal = m.mount(document.getElementById('modal'), <ModalManager/>);
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>);
this.modal = m.mount(document.getElementById('modal'), <ModalManager />);
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager />);
this.drawer = new Drawer();
m.route(
document.getElementById('content'),
basePath + '/',
mapRoutes(this.routes, basePath)
);
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
// Add a class to the body which indicates that the page has been scrolled
// down.
new ScrollListener(top => {
new ScrollListener((top) => {
const $app = $('#app');
const offset = $app.offset().top;
$app
.toggleClass('affix', top >= offset)
.toggleClass('scrolled', top > offset);
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
}).start();
$(() => {
@@ -218,9 +211,7 @@ export default class Application {
}
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');
}
/**
@@ -254,17 +245,19 @@ export default class Application {
// When we deserialize JSON data, if for some reason the server has provided
// a dud response, we don't want the application to crash. We'll show an
// error message to the user instead.
options.deserialize = options.deserialize || (responseText => responseText);
options.deserialize = options.deserialize || ((responseText) => responseText);
options.errorHandler = options.errorHandler || (error => {
throw error;
});
options.errorHandler =
options.errorHandler ||
((error) => {
throw error;
});
// When extracting the data from the response, we can check the server
// response code and show an error message to the user if something's gone
// awry.
const original = options.extract;
options.extract = xhr => {
options.extract = (xhr) => {
let responseText;
if (original) {
@@ -297,54 +290,74 @@ export default class Application {
// returned and show an alert containing its contents.
const deferred = m.deferred();
m.request(options).then(response => deferred.resolve(response), error => {
this.requestError = error;
m.request(options).then(
(response) => deferred.resolve(response),
(error) => {
this.requestError = error;
let children;
let children;
switch (error.status) {
case 422:
children = error.response.errors
.map(error => [error.detail, <br/>])
.reduce((a, b) => a.concat(b), [])
.slice(0, -1);
break;
switch (error.status) {
case 422:
children = error.response.errors
.map((error) => [error.detail, <br />])
.reduce((a, b) => a.concat(b), [])
.slice(0, -1);
break;
case 401:
case 403:
children = app.translator.trans('core.lib.error.permission_denied_message');
break;
case 401:
case 403:
children = app.translator.trans('core.lib.error.permission_denied_message');
break;
case 404:
case 410:
children = app.translator.trans('core.lib.error.not_found_message');
break;
case 404:
case 410:
children = app.translator.trans('core.lib.error.not_found_message');
break;
case 429:
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
break;
case 429:
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
break;
default:
children = app.translator.trans('core.lib.error.generic_message');
default:
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;
}
/**
* @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.
*

View File

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

View File

@@ -88,7 +88,7 @@ export default class Model {
// relationship data object.
for (const innerKey in data[key]) {
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];
}
@@ -109,7 +109,7 @@ export default class Model {
* @public
*/
pushAttributes(attributes) {
this.pushData({attributes});
this.pushData({ attributes });
}
/**
@@ -125,7 +125,7 @@ export default class Model {
const data = {
type: this.data.type,
id: this.data.id,
attributes
attributes,
};
// If a 'relationships' key exists, extract it from the attributes hash and
@@ -138,9 +138,7 @@ export default class Model {
const model = attributes.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),
};
}
@@ -154,31 +152,38 @@ export default class Model {
this.pushData(data);
const request = {data};
const request = { data };
if (options.meta) request.meta = options.meta;
return app.request(Object.assign({
method: this.exists ? 'PATCH' : 'POST',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data: request
}, options)).then(
// 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);
},
return app
.request(
Object.assign(
{
method: this.exists ? 'PATCH' : 'POST',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data: request,
},
options
)
)
.then(
// 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
// old data! We'll revert to that and let others handle the error.
response => {
this.pushData(oldData);
m.lazyRedraw();
throw response;
}
);
// 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.
(response) => {
this.pushData(oldData);
m.lazyRedraw();
throw response;
}
);
}
/**
@@ -192,14 +197,21 @@ export default class Model {
delete(data, options = {}) {
if (!this.exists) return m.deferred().resolve().promise;
return app.request(Object.assign({
method: 'DELETE',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data
}, options)).then(() => {
this.exists = false;
this.store.remove(this);
});
return app
.request(
Object.assign(
{
method: 'DELETE',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data,
},
options
)
)
.then(() => {
this.exists = false;
this.store.remove(this);
});
}
/**
@@ -225,7 +237,7 @@ export default class Model {
* @public
*/
static attribute(name, transform) {
return function() {
return function () {
const value = this.data.attributes && this.data.attributes[name];
return transform ? transform(value) : value;
@@ -243,7 +255,7 @@ export default class Model {
* @public
*/
static hasOne(name) {
return function() {
return function () {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
@@ -267,12 +279,12 @@ export default class Model {
* @public
*/
static hasMany(name) {
return function() {
return function () {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
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));
}
}
@@ -301,7 +313,7 @@ export default class Model {
static getIdentifier(model) {
return {
type: model.data.type,
id: model.data.id
id: model.data.id,
};
}
}

View File

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

View File

@@ -34,9 +34,7 @@ export default class Store {
pushPayload(payload) {
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
// useful to consumers as it allows them to access meta information
@@ -58,7 +56,7 @@ export default class Store {
pushObject(data) {
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]) {
type[data.id].pushData(data);
@@ -95,11 +93,18 @@ export default class Store {
url += '/' + id;
}
return app.request(Object.assign({
method: 'GET',
url,
data
}, options)).then(this.pushPayload.bind(this));
return app
.request(
Object.assign(
{
method: 'GET',
url,
data,
},
options
)
)
.then(this.pushPayload.bind(this));
}
/**
@@ -124,7 +129,7 @@ export default class Store {
* @public
*/
getBy(type, key, value) {
return this.all(type).filter(model => model[key]() === value)[0];
return this.all(type).filter((model) => model[key]() === value)[0];
}
/**
@@ -137,7 +142,7 @@ export default class Store {
all(type) {
const records = this.data[type];
return records ? Object.keys(records).map(id => records[id]) : [];
return records ? Object.keys(records).map((id) => records[id]) : [];
}
/**
@@ -160,6 +165,6 @@ export default class Store {
createRecord(type, data = {}) {
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 open = [hydrated];
translation.forEach(part => {
translation.forEach((part) => {
const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));
if (match) {
@@ -77,7 +77,7 @@ export default class Translator {
if (match[2]) {
open.shift();
} else {
let tag = input[match[3]] || {tag: match[3], children: []};
let tag = input[match[3]] || { tag: match[3], children: [] };
open[0].push(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) {
@@ -97,7 +97,7 @@ export default class Translator {
standardRules = [],
explicitRules = [];
translation.split('|').forEach(part => {
translation.split('|').forEach((part) => {
if (cPluralRegex.test(part)) {
const matches = part.match(cPluralRegex);
explicitRules[matches[0]] = matches[matches.length - 1];
@@ -122,11 +122,13 @@ export default class Translator {
}
}
} else {
var leftNumber = this.convertNumber(matches[4]);
var leftNumber = this.convertNumber(matches[4]);
var rightNumber = this.convertNumber(matches[5]);
if (('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
(']' === matches[6] ? number <= rightNumber : number < rightNumber)) {
if (
('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
(']' === matches[6] ? number <= rightNumber : number < rightNumber)
) {
return explicitRules[e];
}
}
@@ -223,7 +225,7 @@ export default class Translator {
case 'tr':
case 'ur':
case 'zu':
return (number == 1) ? 0 : 1;
return number == 1 ? 0 : 1;
case 'am':
case 'bh':
@@ -237,7 +239,7 @@ export default class Translator {
case 'xbr':
case 'ti':
case 'wa':
return ((number === 0) || (number == 1)) ? 0 : 1;
return number === 0 || number == 1 ? 0 : 1;
case 'be':
case 'bs':
@@ -245,41 +247,41 @@ export default class Translator {
case 'ru':
case 'sr':
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 'sk':
return (number == 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2);
return number == 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;
case 'ga':
return (number == 1) ? 0 : ((number == 2) ? 1 : 2);
return number == 1 ? 0 : number == 2 ? 1 : 2;
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':
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':
return (number % 10 == 1) ? 0 : 1;
return number % 10 == 1 ? 0 : 1;
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':
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':
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':
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':
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':
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:
return 0;

View File

@@ -37,6 +37,7 @@ import Placeholder from './components/Placeholder';
import Separator from './components/Separator';
import Dropdown from './components/Dropdown';
import SplitDropdown from './components/SplitDropdown';
import RequestErrorModal from './components/RequestErrorModal';
import FieldSet from './components/FieldSet';
import Select from './components/Select';
import Navigation from './components/Navigation';
@@ -61,9 +62,9 @@ import userOnline from './helpers/userOnline';
import listItems from './helpers/listItems';
export default {
'extend': extend,
'Session': Session,
'Store': Store,
extend: extend,
Session: Session,
Store: Store,
'utils/evented': evented,
'utils/liveHumanTimes': liveHumanTimes,
'utils/ItemList': ItemList,
@@ -90,8 +91,8 @@ export default {
'models/Discussion': Discussion,
'models/Group': Group,
'models/Forum': Forum,
'Component': Component,
'Translator': Translator,
Component: Component,
Translator: Translator,
'components/AlertManager': AlertManager,
'components/Switch': Switch,
'components/Badge': Badge,
@@ -100,6 +101,7 @@ export default {
'components/Separator': Separator,
'components/Dropdown': Dropdown,
'components/SplitDropdown': SplitDropdown,
'components/RequestErrorModal': RequestErrorModal,
'components/FieldSet': FieldSet,
'components/Select': Select,
'components/Navigation': Navigation,
@@ -111,8 +113,8 @@ export default {
'components/Button': Button,
'components/Modal': Modal,
'components/GroupBadge': GroupBadge,
'Model': Model,
'Application': Application,
Model: Model,
Application: Application,
'helpers/fullTime': fullTime,
'helpers/avatar': avatar,
'helpers/icon': icon,
@@ -121,5 +123,5 @@ export default {
'helpers/highlight': highlight,
'helpers/username': username,
'helpers/userOnline': userOnline,
'helpers/listItems': listItems
'helpers/listItems': listItems,
};

View File

@@ -35,22 +35,13 @@ export default class Alert extends Component {
const dismissControl = [];
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 (
<div {...attrs}>
<span className="Alert-body">
{children}
</span>
<ul className="Alert-controls">
{listItems(controls.concat(dismissControl))}
</ul>
<span className="Alert-body">{children}</span>
<ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
</div>
);
}

View File

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

View File

@@ -24,16 +24,12 @@ export default class Badge extends Component {
attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
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) {
if (isInitialized) return;
if (this.props.label) this.$().tooltip({container: 'body'});
if (this.props.label) this.$().tooltip({ container: 'body' });
}
}

View File

@@ -62,9 +62,9 @@ export default class Button extends Component {
const iconName = this.props.icon;
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.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : ''
this.props.loading ? LoadingIndicator.component({ size: 'tiny', className: 'LoadingIndicator--inline' }) : '',
];
}
}

View File

@@ -31,13 +31,8 @@ export default class Checkbox extends Component {
return (
<label className={className}>
<input type="checkbox"
checked={this.props.state}
disabled={this.props.disabled}
onchange={m.withAttr('checked', this.onchange.bind(this))}/>
<div className="Checkbox-display">
{this.getDisplay()}
</div>
<input type="checkbox" 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}
</label>
);
@@ -50,9 +45,7 @@ export default class Checkbox extends Component {
* @protected
*/
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,19 +64,13 @@ export default class Dropdown extends Component {
$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) {
$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', () => {
@@ -98,10 +92,7 @@ export default class Dropdown extends Component {
*/
getButton() {
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()}
</button>
);
@@ -115,17 +106,13 @@ export default class Dropdown extends Component {
*/
getButtonContent() {
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>,
this.props.caretIcon ? icon(this.props.caretIcon, {className: 'Button-caret'}) : ''
this.props.caretIcon ? icon(this.props.caretIcon, { className: 'Button-caret' }) : '',
];
}
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) {
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.type = 'group--' + props.group.id();

View File

@@ -33,8 +33,6 @@ export default class LinkButton extends Button {
* @return {Boolean}
*/
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,10 +31,12 @@ export default class Modal extends Component {
{Button.component({
icon: 'fas fa-times',
onclick: this.hide.bind(this),
className: 'Button Button--icon Button--link'
className: 'Button Button--icon Button--link',
})}
</div>
) : ''}
) : (
''
)}
<form onsubmit={this.onsubmit.bind(this)}>
<div className="Modal-header">
@@ -65,8 +67,7 @@ export default class Modal extends Component {
* @return {String}
* @abstract
*/
className() {
}
className() {}
/**
* Get the title of the modal dialog.
@@ -74,8 +75,7 @@ export default class Modal extends Component {
* @return {String}
* @abstract
*/
title() {
}
title() {}
/**
* Get the content of the modal.
@@ -83,16 +83,14 @@ export default class Modal extends Component {
* @return {VirtualElement}
* @abstract
*/
content() {
}
content() {}
/**
* Handle the modal form's submit event.
*
* @param {Event} e
*/
onsubmit() {
}
onsubmit() {}
/**
* Focus on the first input when the modal is ready to be used.
@@ -101,8 +99,7 @@ export default class Modal extends Component {
this.$('form').find('input, select, textarea').first().focus().select();
}
onhide() {
}
onhide() {}
/**
* Hide the modal.

View File

@@ -13,11 +13,7 @@ export default class ModalManager extends Component {
}
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) {
@@ -28,9 +24,7 @@ export default class ModalManager extends Component {
// to be retained across route changes.
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));
}
/**
@@ -53,8 +47,13 @@ export default class ModalManager extends Component {
m.redraw(true);
this.$().modal({backdrop: this.component.isDismissible() ? true : 'static'}).modal('show');
this.onready();
const dismissible = !!this.component.isDismissible();
this.$()
.modal({
backdrop: dismissible || 'static',
keyboard: dismissible,
})
.modal('show');
}
/**

View File

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

View File

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

View File

@@ -21,14 +21,11 @@ export default class SelectDropdown extends Dropdown {
}
getButtonContent() {
const activeChild = this.props.children.filter(child => child.props.active)[0];
let label = activeChild && activeChild.props.children || this.props.defaultLabel;
const activeChild = this.props.children.filter((child) => child.props.active)[0];
let label = (activeChild && activeChild.props.children) || this.props.defaultLabel;
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 {
view() {
return <li className="Dropdown-separator"/>;
return <li className="Dropdown-separator" />;
}
}

View File

@@ -24,12 +24,10 @@ export default class SplitDropdown extends Dropdown {
return [
Button.component(buttonProps),
<button
className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName}
data-toggle="dropdown">
{icon(this.props.icon, {className: 'Button-icon'})}
{icon('fas fa-caret-down', {className: 'Button-caret'})}
</button>
<button className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName} data-toggle="dropdown">
{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) {
const original = object[method];
object[method] = function(...args) {
object[method] = function (...args) {
const value = original ? original.apply(this, args) : undefined;
callback.apply(this, [value].concat(args));
@@ -57,7 +57,7 @@ export function extend(object, method, callback) {
export function override(object, method, newMethod) {
const original = object[method];
object[method] = function(...args) {
object[method] = function (...args) {
return newMethod.apply(this, [original.bind(this)].concat(args));
};

View File

@@ -31,11 +31,11 @@ export default class Routes {
if (this.model) {
app.store.models[this.type] = this.model;
}
const model = app.store.models[this.type];
this.attributes.forEach(name => model.prototype[name] = model.attribute(name));
this.hasOnes.forEach(name => model.prototype[name] = model.hasOne(name));
this.hasManys.forEach(name => model.prototype[name] = model.hasMany(name));
this.attributes.forEach((name) => (model.prototype[name] = model.attribute(name)));
this.hasOnes.forEach((name) => (model.prototype[name] = model.hasOne(name)));
this.hasManys.forEach((name) => (model.prototype[name] = model.hasMany(name)));
}
}
}

View File

@@ -10,4 +10,4 @@ export default class PostTypes {
extend(app, extension) {
Object.assign(app.postComponents, this.postComponents);
}
}
}

View File

@@ -10,4 +10,4 @@ export default class Routes {
extend(app, extension) {
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 (avatarUrl) {
return <img {...attrs} src={avatarUrl}/>;
return <img {...attrs} src={avatarUrl} />;
}
content = username.charAt(0).toUpperCase();
attrs.style = {background: user.color()};
attrs.style = { background: user.color() };
}
return <span {...attrs}>{content}</span>;

View File

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

View File

@@ -15,5 +15,9 @@ export default function humanTime(time) {
const full = mo.format('LLLL');
const ago = humanTimeUtil(time);
return <time pubdate datetime={datetime} title={full} data-humantime>{ago}</time>;
return (
<time pubdate datetime={datetime} title={full} data-humantime>
{ago}
</time>
);
}

View File

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

View File

@@ -13,7 +13,7 @@ export default function punctuateSeries(items) {
if (items.length === 2) {
return app.translator.trans('core.lib.series.two_text', {
first: items[0],
second: items[1]
second: items[1],
});
} else if (items.length >= 3) {
// 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', {
first: items[0],
second,
third: items[items.length - 1]
third: items[items.length - 1],
});
}

View File

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

View File

@@ -19,18 +19,18 @@ Object.assign(Discussion.prototype, {
lastPostNumber: Model.attribute('lastPostNumber'),
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'),
mostRelevantPost: Model.hasOne('mostRelevantPost'),
lastReadAt: Model.attribute('lastReadAt', Model.transformDate),
lastReadPostNumber: Model.attribute('lastReadPostNumber'),
isUnread: computed('unreadCount', unreadCount => !!unreadCount),
isRead: computed('unreadCount', unreadCount => app.session.user && !unreadCount),
isUnread: computed('unreadCount', (unreadCount) => !!unreadCount),
isRead: computed('unreadCount', (unreadCount) => app.session.user && !unreadCount),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
canReply: Model.attribute('canReply'),
canRename: Model.attribute('canRename'),
@@ -84,7 +84,7 @@ Object.assign(Discussion.prototype, {
const items = new ItemList();
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;
@@ -99,6 +99,6 @@ Object.assign(Discussion.prototype, {
postIds() {
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'),
namePlural: Model.attribute('namePlural'),
color: Model.attribute('color'),
icon: Model.attribute('icon')
icon: Model.attribute('icon'),
});
Group.ADMINISTRATOR_ID = '1';

View File

@@ -11,5 +11,5 @@ Object.assign(Notification.prototype, {
user: Model.hasOne('user'),
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),
editedUser: Model.hasOne('editedUser'),
isEdited: computed('editedAt', editedAt => !!editedAt),
isEdited: computed('editedAt', (editedAt) => !!editedAt),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
canEdit: Model.attribute('canEdit'),
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'),
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
// 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
@@ -67,8 +67,8 @@ Object.assign(User.prototype, {
const groups = this.groups();
if (groups) {
groups.forEach(group => {
items.add('group' + group.id(), GroupBadge.component({group}));
groups.forEach((group) => {
items.add('group' + group.id(), GroupBadge.component({ group }));
});
}
@@ -85,12 +85,13 @@ Object.assign(User.prototype, {
const image = new Image();
const user = this;
image.onload = function() {
image.onload = function () {
const colorThief = new ColorThief();
user.avatarColor = colorThief.getColor(this);
user.freshness = new Date();
m.redraw();
};
image.crossOrigin = 'anonymous';
image.src = this.avatarUrl();
},
@@ -105,6 +106,6 @@ Object.assign(User.prototype, {
Object.assign(preferences, newPreferences);
return this.save({preferences});
}
return this.save({ preferences });
},
});

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
const later = window.requestAnimationFrame ||
const later =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
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
@@ -57,10 +58,7 @@ export default class ScrollListener {
*/
start() {
if (!this.active) {
window.addEventListener(
'scroll',
this.active = this.loop.bind(this)
);
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
}
}

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;
if (classes instanceof Array) {
classNames = classes.filter(name => name);
classNames = classes.filter((name) => name);
} else {
classNames = [];

View File

@@ -14,12 +14,12 @@ export default function computed(...dependentKeys) {
const dependentValues = {};
let computedValue;
return function() {
return function () {
let recompute = false;
// Read all of the dependent values. If any of them have changed since last
// 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];
if (dependentValues[key] !== value) {
@@ -29,7 +29,10 @@ export default function computed(...dependentKeys) {
});
if (recompute) {
computedValue = compute.apply(this, keys.map(key => dependentValues[key]));
computedValue = compute.apply(
this,
keys.map((key) => dependentValues[key])
);
}
return computedValue;

View File

@@ -34,7 +34,7 @@ export default {
* @public
*/
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.
*/
one(event, handler) {
const wrapper = function() {
const wrapper = function () {
handler.apply(this, arguments);
this.off(event, wrapper);
@@ -77,5 +77,5 @@ export default {
if (index !== -1) {
handlers.splice(index, 1);
}
}
}
},
};

View File

@@ -6,7 +6,7 @@
*/
export default function extractText(vdom) {
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) {
return extractText(vdom.children);
} else {

View File

@@ -26,11 +26,11 @@ export default function humanTime(time) {
if (m.year() === moment().year()) {
ago = m.format('D MMM');
} else {
ago = m.format('MMM \'YY');
ago = m.format('ll');
}
} else {
ago = m.fromNow();
}
return ago;
};
}

View File

@@ -1,7 +1,7 @@
import humanTimeUtil from './humanTime';
function updateHumanTimes() {
$('[data-humantime]').each(function() {
$('[data-humantime]').each(function () {
const $this = $(this);
const ago = humanTimeUtil($this.attr('datetime'));

View File

@@ -12,7 +12,7 @@
export default function mixin(Parent, ...mixins) {
class Mixed extends Parent {}
mixins.forEach(object => {
mixins.forEach((object) => {
Object.assign(Mixed.prototype, object);
});

View File

@@ -3,11 +3,11 @@ import Component from '../Component';
export default function patchMithril(global) {
const mo = global.m;
const m = function(comp, ...args) {
const m = function (comp, ...args) {
if (comp.prototype && comp.prototype instanceof Component) {
let children = args.slice(1);
if (children.length === 1 && Array.isArray(children[0])) {
children = children[0]
children = children[0];
}
return comp.component(args[0], children);
@@ -29,14 +29,14 @@ export default function patchMithril(global) {
return node;
};
Object.keys(mo).forEach(key => m[key] = mo[key]);
Object.keys(mo).forEach((key) => (m[key] = mo[key]));
/**
* Redraw only if not in the middle of a computation (e.g. a route change).
*
* @return {void}
*/
m.lazyRedraw = function() {
m.lazyRedraw = function () {
m.startComputation();
m.endComputation();
};

View File

@@ -7,20 +7,23 @@
* @return {String}
*/
export function truncate(string, length, start = 0) {
return (start > 0 ? '...' : '') +
string.substring(start, start + length) +
(string.length > start + length ? '...' : '');
return (start > 0 ? '...' : '') + string.substring(start, start + length) + (string.length > start + length ? '...' : '');
}
/**
* Create a slug out of the given string. Non-alphanumeric characters are
* converted to hyphens.
*
* NOTE: This method does not use the comparably sophisticated transliteration
* mechanism that is employed in the backend. Therefore, it should only be used
* to *suggest* slugs that can be overridden by the user.
*
* @param {String} string
* @return {String}
*/
export function slug(string) {
return string.toLowerCase()
return string
.toLowerCase()
.replace(/[^a-z0-9]/gi, '-')
.replace(/-+/g, '-')
.replace(/-$|^-/g, '');
@@ -34,9 +37,7 @@ export function slug(string) {
* @return {String}
*/
export function getPlainContent(string) {
const html = string
.replace(/(<\/p>|<br>)/g, '$1 &nbsp;')
.replace(/<img\b[^>]*>/ig, ' ');
const html = string.replace(/(<\/p>|<br>)/g, '$1 &nbsp;').replace(/<img\b[^>]*>/gi, ' ');
const dom = $('<div/>').html(html);

View File

@@ -10,18 +10,42 @@ function hsvToRgb(h, s, v) {
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
return {
r: Math.floor(r * 255),
g: Math.floor(g * 255),
b: Math.floor(b * 255)
b: Math.floor(b * 255),
};
}

View File

@@ -22,7 +22,7 @@ export default class ForumApplication extends Application {
* @type {Object}
*/
notificationComponents = {
discussionRenamed: DiscussionRenamedNotification
discussionRenamed: DiscussionRenamedNotification,
};
/**
* A map of post types to their components.
@@ -31,7 +31,7 @@ export default class ForumApplication extends Application {
*/
postComponents = {
comment: CommentPost,
discussionRenamed: DiscussionRenamedPost
discussionRenamed: DiscussionRenamedPost,
};
/**
@@ -87,7 +87,7 @@ export default class ForumApplication extends Application {
this.routes[defaultAction].path = '/';
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
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-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
@@ -102,7 +102,7 @@ export default class ForumApplication extends Application {
// Route the home link back home when clicked. We do not want it to register
// if the user is opening it in a new tab, however.
$('#home-link').click(e => {
$('#home-link').click((e) => {
if (e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault();
app.history.home();
@@ -123,9 +123,11 @@ export default class ForumApplication extends Application {
* @return {Boolean}
*/
composingReplyTo(discussion) {
return this.composer.component instanceof ReplyComposer &&
return (
this.composer.component instanceof ReplyComposer &&
this.composer.component.props.discussion === discussion &&
this.composer.position !== Composer.PositionEnum.HIDDEN;
this.composer.position !== Composer.PositionEnum.HIDDEN
);
}
/**
@@ -135,8 +137,7 @@ export default class ForumApplication extends Application {
* @return {Boolean}
*/
viewingDiscussion(discussion) {
return this.current instanceof DiscussionPage &&
this.current.discussion === discussion;
return this.current instanceof DiscussionPage && this.current.discussion === discussion;
}
/**

View File

@@ -134,6 +134,6 @@ export default Object.assign(compat, {
'components/DiscussionListItem': DiscussionListItem,
'components/LoadingPost': LoadingPost,
'components/PostsUserPage': PostsUserPage,
'routes': routes,
'ForumApplication': ForumApplication
routes: routes,
ForumApplication: ForumApplication,
});

View File

@@ -44,7 +44,8 @@ export default class AvatarEditor extends Component {
return (
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}>
{avatar(user)}
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" }
<a
className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}
@@ -52,12 +53,11 @@ export default class AvatarEditor extends Component {
ondragenter={this.enableDragover.bind(this)}
ondragleave={this.disableDragover.bind(this)}
ondragend={this.disableDragover.bind(this)}
ondrop={this.dropUpload.bind(this)}>
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle'))}
ondrop={this.dropUpload.bind(this)}
>
{this.loading ? LoadingIndicator.component() : user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle')}
</a>
<ul className="Dropdown-menu Menu">
{listItems(this.controlItems().toArray())}
</ul>
<ul className="Dropdown-menu Menu">{listItems(this.controlItems().toArray())}</ul>
</div>
);
}
@@ -70,19 +70,21 @@ export default class AvatarEditor extends Component {
controlItems() {
const items = new ItemList();
items.add('upload',
items.add(
'upload',
Button.component({
icon: 'fas fa-upload',
children: app.translator.trans('core.forum.user.avatar_upload_button'),
onclick: this.openPicker.bind(this)
onclick: this.openPicker.bind(this),
})
);
items.add('remove',
items.add(
'remove',
Button.component({
icon: 'fas fa-times',
children: app.translator.trans('core.forum.user.avatar_remove_button'),
onclick: this.remove.bind(this)
onclick: this.remove.bind(this),
})
);
@@ -150,9 +152,13 @@ export default class AvatarEditor extends Component {
const user = this.props.user;
const $input = $('<input type="file">');
$input.appendTo('body').hide().click().on('change', e => {
this.upload($(e.target)[0].files[0]);
});
$input
.appendTo('body')
.hide()
.click()
.on('input', (e) => {
this.upload($(e.target)[0].files[0]);
});
}
/**
@@ -170,15 +176,14 @@ export default class AvatarEditor extends Component {
this.loading = true;
m.redraw();
app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
serialize: raw => raw,
data
}).then(
this.success.bind(this),
this.failure.bind(this)
);
app
.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
serialize: (raw) => raw,
data,
})
.then(this.success.bind(this), this.failure.bind(this));
}
/**
@@ -190,13 +195,12 @@ export default class AvatarEditor extends Component {
this.loading = true;
m.redraw();
app.request({
method: 'DELETE',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar'
}).then(
this.success.bind(this),
this.failure.bind(this)
);
app
.request({
method: 'DELETE',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
})
.then(this.success.bind(this), this.failure.bind(this));
}
/**

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