1
0
mirror of https://github.com/flarum/core.git synced 2025-08-13 11:54:32 +02:00

Compare commits

...

857 Commits

Author SHA1 Message Date
Alexander Skvortsov
984f751c71 Use process isolation for integration tests 2020-12-01 19:33:24 -05:00
flarum-bot
8830e9dd09 Bundled output for commit fe41bc1fdc [skip ci] 2020-12-01 16:22:59 +00:00
Alexander Skvortsov
fe41bc1fdc Remove Deprecated Beta14 Code (#2428) 2020-12-01 11:21:28 -05:00
Nina Pypchenko
5a763050a6 DRY up image uploading code (#2477) 2020-12-01 10:42:05 -05:00
Sami Mazouz
8c813bc340 ApiSerializer Extender (#2438) 2020-11-30 19:24:50 -05:00
flarum-bot
f67dee0a9e Bundled output for commit f968420216 [skip ci] 2020-11-30 19:02:41 +00:00
Alexander Skvortsov
f968420216 Don't use browser scroll restore in DiscussionPage (#2476)
Although native browser scroll restorations have become quite powerful, it interferes with Flarum's PostStream, so if we're on a DiscussionPage, we use manual scroll restoration.
2020-11-30 14:01:08 -05:00
flarum-bot
d5e124b4a2 Bundled output for commit 09e2736cbc [skip ci] 2020-11-29 23:34:50 +00:00
Alexander Skvortsov
09e2736cbc Fix goToIndex to visible end
In the PostStream, `this.visibleEnd` represents the index of the last post + 1, but `loadNearIndex` treated it as if it was the index of the last post. This means that executing `goToIndex` on the post stream's current `this.visiblePost` didn't load new posts, and as a result, the requested scrolling did not occur.
2020-11-29 18:33:29 -05:00
flarum-bot
ddb3d3edb0 Bundled output for commit 28d56f5fc8 [skip ci] 2020-11-29 22:47:21 +00:00
Alexander Skvortsov
28d56f5fc8 Merge pull request #2465 from flarum/0.1.0-beta.14.1 2020-11-29 17:45:58 -05:00
Alexander Skvortsov
9b4012bbb5 Reset dist js 2020-11-29 17:41:16 -05:00
Alexander Skvortsov
1a5e4d454e Move floodgate to middleware, add extender + integration tests (#2170) 2020-11-29 17:13:22 -05:00
sl-kr
387b4fd315 update a user's comment count if deleting a discussion (#2472) 2020-11-29 17:11:05 -05:00
flarum-bot
66482c2815 Bundled output for commit 277a5c3fac [skip ci] 2020-11-26 22:54:38 +00:00
Mohammad Reza
277a5c3fac Clear error alerts in change email modal on success (#2467) 2020-11-26 17:53:38 -05:00
Nina Pypchenko
286d8dec5b Update tsconfig file to include .tsx files (#2457) 2020-11-26 12:00:13 -05:00
flarum-bot
e1c61a0e85 Bundled output for commit 102e76b084 [skip ci] 2020-11-26 06:56:10 +00:00
Alexander Skvortsov
102e76b084 Defer clearing discussion list on discussion start
This prevents an edge case where `app.discussions` is considered empty while the new page is loading, and as a result, the side pane isn't set as "enabled". Then, if the pane has previously been pinned, when the page loads and the side pane appears, it covers up part of the discussion page.

Fixes https://github.com/flarum/core/issues/2471
2020-11-26 01:54:28 -05:00
flarum-bot
d09d4bc507 Bundled output for commit c3989cc952 [skip ci] 2020-11-24 17:46:02 +00:00
Charlie
c3989cc952 AdminUX Overhaul (#2409)
- Extensions now have their own pages
- The API for extensions to register permissions and settings has been overhauled via the `flarum/admin/utils/ExtensionData` util
- An extension grid has been added as a widget to the Dashboard page
2020-11-24 12:44:40 -05:00
flarum-bot
9cb9097b24 Bundled output for commit 571a835be0 [skip ci] 2020-11-14 22:23:04 +00:00
Wadim Kalmykov
571a835be0 Fix mobile PostStream top scroll adjustment & remove App:before (#2385)
- remove App:before so we can use #app-navigation to access the mobile header
- fix mobile postStream scroll top margin adjustment
2020-11-14 17:21:38 -05:00
Alexander Skvortsov
0c95774333 Refactor Route Resolving and Dispatch (#2425)
- Split DispatchRoute. This allows us to run middleware after we figure out which route we're on, but before we actually execute the controller for that route.
- By making the route name explicitly available to middlewares, applications like CSRF and floodgate can set patterns based on route names instead of the path, which is an implementation detail.
- Support using route name match for CSRF extender, deprecate path match
2020-11-10 12:52:12 -05:00
Nina Pypchenko
67741c7a6f Make checkbox switch component background stand out in modals (#2443) 2020-11-09 20:54:21 -05:00
Alexander Skvortsov
f5cfec15e3 Add missing import 2020-11-08 21:49:11 -05:00
Alexander Skvortsov
47d2eee9ce Fix Callables for Extenders (#2423)
- Standardize signatures and variable names for extenders that take callbacks
- Adjust model extender docblock to clarify that default calue can't be an invokable class.
- Make invokable classes provided to Model->relationship
- Add integration tests to ensure Model->relationship and User->groupProcessor extenders accept callbacks
- Extract code for wrapping callbacks into central util
2020-11-08 21:36:38 -05:00
Nina Pypchenko
c10cc92deb Improved Permissions Error Messages for Initial Install (#2435)
- Made the wording of the error more generic
- Added link to the relevant section in the installation guide

Resolves #2327.
2020-11-07 14:48:11 -05:00
Sami Mazouz
529d2edcaf Add Service Provider Extender (#2437) 2020-11-06 13:30:10 -05:00
Sami Mazouz
f0e77a5789 Add Notification Channel Extender (#2432) 2020-11-05 12:09:06 -05:00
Alexander Skvortsov
87c258b2f8 Refactor and improve formatter extender (#2098)
- Deprecated all events involved with Formatter
- Refactor ->configure() method on extender not to use events
- Add extender methods for ->render() and ->parse()
- Add integration tests
2020-11-03 13:05:33 -05:00
Alexander Skvortsov
cee87848fe Added post extender with type method, deprecated ConfigurePostTypes (#2101) 2020-11-03 10:43:49 -05:00
Daniël Klabbers
967cd0e3ca update version constant for beta 14.1 2020-11-02 13:53:20 +01:00
Daniël Klabbers
b79152b977 bundled output for js changes beta 14.1 2020-11-02 11:53:27 +01:00
Daniël Klabbers
ace624db66 changelog for v0.1.0-beta.14.1 2020-11-02 11:51:24 +01:00
Alexander Skvortsov
5842dd1200 Validator extender (#2102)
Added validator extender, integration tests, and deprecated related Validating event
2020-11-01 11:31:16 -05:00
Sami Mazouz
b311512502 Add Notification Type Extender and Tests (#2424) 2020-10-31 17:17:14 -04:00
Alexander Skvortsov
9b9f2c4bb7 Fix exiting composer while in fullscreen mode. 2020-10-30 20:44:52 -04:00
flarum-bot
0b2a5fa5b8 Bundled output for commit 52e45aacad [skip ci] 2020-10-31 00:28:56 +00:00
Lucas Henrique
52e45aacad Convert common time helpers to Typescript (#2391) 2020-10-30 20:27:40 -04:00
Alexander Skvortsov
8b1de457bf Fix broken page title logic on subpath installs
The base path needs to be accounted for when calculating whether we're on the default route.
2020-10-30 14:18:09 -04:00
Alexander Skvortsov
21c2a4b2a4 Updater should show on any subpath, like installer (#2426) 2020-10-30 13:28:20 -04:00
flarum-bot
12c03dc4e1 Bundled output for commit d2927cfdb9 [skip ci] 2020-10-29 16:54:36 +00:00
Alexander Skvortsov
d2927cfdb9 Ensure scripts provided by textformatter are run (#2415) 2020-10-29 12:53:23 -04:00
Daniël Klabbers
24b7a21507 Update Symfony components to v4 (#2407)
This matches the Symfony dependencies of our laravel dependencies.
2020-10-27 17:12:36 -04:00
flarum-bot
c9a04fe009 Bundled output for commit bd7fa11b5a [skip ci] 2020-10-25 17:36:51 +00:00
Alexander Skvortsov
bd7fa11b5a Export SuperTextarea util in compat 2020-10-25 13:35:15 -04:00
Daniël Klabbers
7055f6d941 update version constant 2020-10-20 16:34:54 +02:00
Alexander Skvortsov
f765001f06 Update email 2020-10-20 10:32:24 -04:00
Daniël Klabbers
683739a617 changelog v0.1.0-beta.14 and added core developer @askvortsov1 2020-10-20 16:24:43 +02:00
flarum-bot
69b7fe8d01 Bundled output for commit 1936b9117d [skip ci] 2020-10-17 17:43:39 +00:00
Alexander Skvortsov
1936b9117d Page Scroll Cleanup (#2396)
- Reintroduce cancellable scroll top on page change
- IndexPage: rely on browser to retain scroll position on page reload
- Remove obsolete browser hack
- Fix broken selector
- When on mobile, only retain scroll for IndexPage if we're coming from a discussion
- Move app.cache.scrollTop save into `onbeforeremove` so we make sure to do it before DOM is detached
2020-10-17 13:42:33 -04:00
flarum-bot
d53eeded44 Bundled output for commit 0650788e7c [skip ci] 2020-10-16 20:32:13 +00:00
Alexander Skvortsov
0650788e7c Fix scolling to first post via m.route.set
The default first post number is '1', so we scroll to that if we're calling `m.route.set` without a `near` parameter, as that means we're scrolling to the top.

This was present in beta 13's implementation, but accidentially omitted in 988b6c9.

We also remove unnecessary typecasting for simpler logic and increased consistency with beta 13.
2020-10-16 16:30:27 -04:00
flarum-bot
6a77184611 Bundled output for commit a8b36cb76d [skip ci] 2020-10-16 20:05:15 +00:00
Alexander Skvortsov
a8b36cb76d Fix check for going between discussion pages.
The current implementation for checking whether we are on a discussion page, and going to a discussion page, checks the route we are going to. This is problematic, because the route resolver represents the route being considered, not the route we are currently on. So, if we are currently using a DiscussionPageResolver, we must be going to a route handled by DiscussionPage. Instead, we need to check the route that we are currently on, which is done via `app.current.matches(DiscussionPage)`.
2020-10-16 16:03:32 -04:00
flarum-bot
5cd14d594b Bundled output for commit f4ad9d2d5a [skip ci] 2020-10-16 16:07:09 +00:00
Alexander Skvortsov
f4ad9d2d5a Fix scrolling to reply via 'reply' as near parameter 2020-10-16 12:04:45 -04:00
Alexander Skvortsov
d409484abf Notification: fix wrong external attr for Link 2020-10-16 11:53:45 -04:00
flarum-bot
1fc24635f6 Bundled output for commit ff7ac0b322 [skip ci] 2020-10-16 05:26:03 +00:00
Alexander Skvortsov
ff7ac0b322 Fix PostStream loadRange doesn't return all posts (#2384)
- Also, ensure that posts are ordered by creation timestamp
2020-10-16 01:24:45 -04:00
Wadim Kalmykov
d460aaa3ad order posts by creation date 2020-10-16 01:20:54 -04:00
Wadim Kalmykov
7634a766cb Fix loadRange doesn't return all posts 2020-10-16 01:20:54 -04:00
flarum-bot
e5f53b93a6 Bundled output for commit a38c92d409 [skip ci] 2020-10-16 01:59:56 +00:00
Alexander Skvortsov
a38c92d409 Fix broken import 2020-10-15 21:58:18 -04:00
Alexander Skvortsov
3da655a62f Rename resolver to resolvers for consistency 2020-10-15 21:26:34 -04:00
flarum-bot
46c3124b0b Bundled output for commit e6f59b834f [skip ci] 2020-10-15 22:20:11 +00:00
Alexander Skvortsov
e6f59b834f Default force attr to true on LinkButton
This retains beta 13 behavior.
2020-10-15 18:18:51 -04:00
Alexander Skvortsov
9f5737eb93 Fix routeName attr not being passed into pages 2020-10-15 18:14:20 -04:00
flarum-bot
35cb5b20a0 Bundled output for commit 988b6c9023 [skip ci] 2020-10-15 22:02:46 +00:00
Alexander Skvortsov
988b6c9023 Allow extensions to use route resolvers (#2275)
- mapRoutes: don't wrap components in resolvers if they are already resolvers
- Extract defaultResolver into its own class
- Allow either route resolver instances, or components with an optional resolverClass which should accept the component and route name in its constructor.
- Introduce a resolver for DiscussionPage, so that routing from one post to another on the same discussion triggers a scroll instead of rerendering
2020-10-15 18:01:17 -04:00
flarum-bot
c1d91be2f4 Bundled output for commit f534398645 [skip ci] 2020-10-15 21:47:33 +00:00
Alexander Skvortsov
f534398645 Fix PostStream Reply Scroll (#2366)
- Add an index to reply placeholder so we can scroll to it directly when replying.
- Stop pretending that the currently broken `bottom` scroll functionality works, and explicitly call it `reply` scrolling to be clearer
- Directly get target from state
- Explicitly scroll to placeholder on reply
- Clean up scrollToItem code a bit
- Account for edge case where index is undefined when scrolling to post

Co-authored-by: Wadim Kalmykov <36057469+w-4@users.noreply.github.com>
2020-10-15 17:46:02 -04:00
flarum-bot
cd05ec6589 Bundled output for commit 78be6e2194 [skip ci] 2020-10-15 21:41:56 +00:00
Wadim Kalmykov
78be6e2194 Fix lifecyle method workarounds (#2378)
Essentially, whenever a route is loaded, we add a key to that component. If the key changes, the page completely rerenders. Switching between different routes handled by the same key triggers those rerenders.
2020-10-15 17:40:25 -04:00
flarum-bot
eb498a0a9f Bundled output for commit ac42a5900d [skip ci] 2020-10-15 21:36:46 +00:00
Wadim Kalmykov
ac42a5900d Make PostStreamScrubber work for Posts that have top margin (#2369)
Also fixes incorrect page count when scrolling to bottom (https://github.com/flarum/core/issues/1897)
2020-10-15 17:35:22 -04:00
Alexander Skvortsov
543b136f7c Refactor PostStream animations (#2364)
- If the fadeIn animation is specified on the PostStream class itself, any time we add/remove another animation, it will redo fadeIn. To avoid this, we move fadeIn into it's own css class, which is applied, and then immediately removed after the animation is completed to ensure it only runs once.
- The "fix" for flashItem was actually broken, as it resulted in 'flash' never being removed, so we never went back to .PostStream's fadeIn. We adjust flashItem to ensure that '.flash' is removed. We also remove 'fadeIn' in case it hasn't yet been removed in oncreate.
2020-10-15 17:34:35 -04:00
flarum-bot
8546fb734f Bundled output for commit 20b9455f04 [skip ci] 2020-10-15 18:32:09 +00:00
Wadim Kalmykov
20b9455f04 make scroll listener passive (#2387)
see: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
2020-10-15 14:30:32 -04:00
Wadim Kalmykov
008f1da539 Make header and navigation components redraw after page components (#2390)
Make header and navigation components redraw after page components. Page components manipulate the header (state), but the header redraws before the page on route change. By changing the mount order, we change also the redraw order.
2020-10-15 14:29:42 -04:00
flarum-bot
a3bd431503 Bundled output for commit 6da81a71a4 [skip ci] 2020-10-14 09:39:52 +00:00
Daniël Klabbers
6da81a71a4 npm audit security vulnerabilities patched 2020-10-14 10:11:34 +02:00
flarum-bot
a48d38614b Bundled output for commit 7358437c59 [skip ci] 2020-10-11 21:27:54 +00:00
Sami Mazouz
7358437c59 Fix MarkRead Mobile Gesture (#2383) 2020-10-11 17:26:32 -04:00
flarum-bot
08ec274a24 Bundled output for commit d45478564f [skip ci] 2020-10-09 23:28:51 +00:00
Alexander Skvortsov
d45478564f Fix handling of non-409 errors in ExtensionsPage
If the error isn't a 409, we'll want to re-throw the error so it'll be handled by the default system (showing an alert).

For simplicity, we can also move 409-handling logic out of setTimeout.

Finally, we adjust the timeout to 300 milliseconds to match the modal transition animation length.
2020-10-09 19:27:07 -04:00
flarum-bot
8296ffe8c9 Bundled output for commit 1d2f0ca407 [skip ci] 2020-10-09 23:07:06 +00:00
Alexander Skvortsov
1d2f0ca407 Header UI fixes (#2371)
* Revert "Fix header contents moving when opening modal (#2131)"
* Fix header contents moving when modal opened/closed.

Conditionally apply the navbar-fixed-top class only when needed, so that we can take advantage of it without always having the navbar in position:fixed, as was done in the previous solution. That resulted in a clash with custom headers.

* Show header on refresh of scrolled page

Due to some magic in Mithril 0.1's context:retain flag, some DOM elements were cached across page reloads. Since that has been eliminated, if we refresh the page and we are scrolled down, the "affix" class which makes the header fixed (and as a result, visible) isn't applied until the first scroll. We fix this by running ScrollListener.update() immediately to set initial navbar state.
2020-10-09 19:05:53 -04:00
Wadim Kalmykov
bb69c3bd57 Reduce modal hide timeout (#2367) 2020-10-09 19:04:53 -04:00
Daniël Klabbers
2b1581875a Fixes the queue for beta 14 (#2363)
- rewrite the queue handling for illuminate 6+
- implement missing maintenance mode callable for queue Worker
- Ensure we resolve append the queue commands once the queue bindings are loaded
- Override WorkCommand because it needs the maintenance flag. It tries to use
the isDownForMaintenance method from the Container assuming it is a Laravel
Application. Circumvented this issue by resolving our Config from IOC instead.
2020-10-09 16:06:28 -04:00
Sami Mazouz
a0c36a015b Use @control-bg for Slidable content (#2381) 2020-10-09 14:37:47 -04:00
flarum-bot
656409794c Bundled output for commit 245f3c6846 [skip ci] 2020-10-07 20:25:22 +00:00
Alexander Skvortsov
245f3c6846 DiscussionPage: call onNewRoute properly
When on a discussion page, the URL changing doesn't always mean we've moved to a different page. In our custom rerender logic, we only want to call `this.onNewRoute()` if the page has actually changed.
2020-10-07 16:22:41 -04:00
Alexander Skvortsov
962b49567c Restore stricter email validation
In v5.8, Laravel expanded email validation logic to closer match the RFC. This, however, allows emails that aren't conventional (for example, emails lacking a TLD). This commit changes Flarum's UserValidator to use the `email:filter` validator, which uses PHP's filter_var, and is the pre-5.8 behavior.

See https://laravel.com/docs/5.8/validation#rule-email
2020-10-07 15:33:57 -04:00
flarum-bot
12498b7620 Bundled output for commit 84f7d29d8c [skip ci] 2020-10-07 18:11:32 +00:00
Alexander Skvortsov
84f7d29d8c Slight PostStream scrubber improvement
After we scroll to a post, we redraw to render post content.  We then update the scrubber again so its height is accurate. This commit moves that update to AFTER our adjustment of scroll position, so that scrubber height is based on actual post heights. This fixes some subtle scrubber glitches.
2020-10-07 14:09:53 -04:00
Daniël Klabbers
561e8c6b6a Add test for model object argument in callable for attribute defaults 2020-10-07 11:38:52 +02:00
Daniël Klabbers
ad9917f0d6 Allows callables for default model attribute to gain access
to the current model in order to calculate the value needed.
2020-10-07 11:26:58 +02:00
Daniël Klabbers
6977c24dd8 improve deprecated message for b15 2020-10-07 10:23:46 +02:00
flarum-bot
d1b72429ac Bundled output for commit 63692f12c5 [skip ci] 2020-10-06 15:53:21 +00:00
Wadim Kalmykov
63692f12c5 SubtreeRetainer: fix onbeforeupdate needsRebuild (#2365) 2020-10-06 11:52:05 -04:00
flarum-bot
441ccec8e7 Bundled output for commit 414b0ff6d3 [skip ci] 2020-10-06 00:52:30 +00:00
Alexander Skvortsov
414b0ff6d3 Update mithril request docs link 2020-10-05 20:50:15 -04:00
flarum-bot
2ff0e1efcb Bundled output for commit 8c46b37a6f [skip ci] 2020-10-05 22:07:23 +00:00
Lucas Henrique
8c46b37a6f Convert icon helper to Typescript (#2360) 2020-10-05 18:06:08 -04:00
flarum-bot
9be629cfcc Bundled output for commit df9be1b063 [skip ci] 2020-10-05 20:26:46 +00:00
Alexander Skvortsov
df9be1b063 Move drawer hide and modal close into onNewRoute
Let's stay consistent with previous behavior, and run these on "internal route change" (same component handles different route) as well as on initial render of a page component.
2020-10-05 16:25:23 -04:00
Alexander Skvortsov
97c36f2f7d Use Symfony TranslatorInterface for tests
This seems to be a leftover change missed in https://github.com/flarum/core/pull/2243
2020-10-05 16:02:12 -04:00
flarum-bot
13efd02085 Bundled output for commit 0b44c48433 [skip ci] 2020-10-05 18:41:55 +00:00
Alexander Skvortsov
0b44c48433 Catch promise reject when not logged in on reply 2020-10-05 14:40:33 -04:00
flarum-bot
b562072471 Bundled output for commit 718445cb0c [skip ci] 2020-10-05 14:56:45 +00:00
Wadim Kalmykov
718445cb0c call parent onremove (#2362) 2020-10-05 10:55:14 -04:00
Abhishek Verma
c6f2ff0c80 Fixed Broken Badges in ReadMe.md (#2358) 2020-10-04 16:11:39 -04:00
flarum-bot
67962f48e5 Bundled output for commit f8a0d9459a [skip ci] 2020-10-03 22:48:56 +00:00
Alexander Skvortsov
f8a0d9459a Fix email confirmation alert
Currently, the controls are on a new line due to the container div. We want to wrap ALL children of the alert, including the controls, in the container div.

We need to split it into a separate class so that we can add modify the alert vnode AFTER the alert component's `view` logic has been applied.
2020-10-03 18:47:27 -04:00
David Sevilla Martín
27d562f3fc Remove Mithril namespace export from shims.d.ts (#2347)
From using PhpStorm to try and see if the autocomplete works properly, it appears as it doesn't. The intention was to not have to import Mithril every time we wanted to type something with Mithril.*, but that doesn't seem to be possible - and it's not a big deal anyway
2020-10-02 19:34:06 -04:00
flarum-bot
17a7155f60 Bundled output for commit 4cdce71d65 [skip ci] 2020-10-02 23:06:45 +00:00
Alexander Skvortsov
4cdce71d65 Eliminate temporary BC layers from rewrite
During the frontend rewrite, we introduced mithril patches for a `route` attr, and for `m.stream`. Later, we decided not to go that route, but not to remove the patches yet to avoid breaking extensions while we were finalizing the replacements. We can now remove these.

Other BC layers are for things from before beta 14, so those remain in place.
2020-10-02 19:05:26 -04:00
flarum-bot
eb03f51c4f Bundled output for commit d695d96e00 [skip ci] 2020-10-02 22:50:48 +00:00
Alexander Skvortsov
d695d96e00 Various TypeScript improvements (#2309)
- Use Mithril.Attributes as base for ComponentAttrs, remove =any from class signature for Component
- Convert Alert to TypeScript, introduce AlertAttrs interface
- Convert AlertManagerState to TypeScript, add overload signatures for `show`, introduce AlertState interface for stored Alerts.
- Set ComponentAttrs as default T for Component
- Make attrs in AlertAttrs optional
- Add AlertIdentifier interface, simplify show type signature
- Remove mithril patch shim, as all patches onto m are now deprecated
- Use Mithril.Static for shim
2020-10-02 18:49:40 -04:00
flarum-bot
dc4884485a Bundled output for commit 40548d7c61 [skip ci] 2020-10-02 22:05:01 +00:00
Wadim Kalmykov
40548d7c61 Improve DiscussionListState refresh method (#2322)
- Ensure that the discussion list is cleared before it is updated with fetched results
- Rename `clear` to `deferClear`, improve documentation to make its purpose clearer.
2020-10-02 18:03:44 -04:00
flarum-bot
60714b7ac4 Bundled output for commit 84d14f485a [skip ci] 2020-10-02 21:55:59 +00:00
Alexander Skvortsov
84d14f485a Basic Extension Dependency Support (#2188)
- Don't enable an extension if its dependencies are not enabled
- Don't disable an extension if its dependencies are not disabled
2020-10-02 17:54:28 -04:00
flarum-bot
0a6c5217c1 Bundled output for commit 44a96a82ef [skip ci] 2020-10-02 21:11:38 +00:00
Alexander Skvortsov
44a96a82ef Minor improvements to onNewRoute (#2328)
- Call onNewRoute when page changed with same component in DiscussionPage and UserPage

- Make app.previous and app.current changed in onNewRoute, not in oninit. This way, when the route is changed, but still handled by the same component, a new PageState object will still be created.
2020-10-02 17:10:38 -04:00
flarum-bot
0b3fe10516 Bundled output for commit 5ecb74fb59 [skip ci] 2020-10-02 20:58:06 +00:00
Alexander Skvortsov
5ecb74fb59 Use Link component for links instead of mithril route patch (#2315)
This new component now also supports external links.
2020-10-02 16:56:40 -04:00
flarum-bot
b66d16e44b Bundled output for commit a013d647e0 [skip ci] 2020-10-02 15:15:05 +00:00
Alexander Skvortsov
a013d647e0 Adjust PostStreamScrubber height after scroll (#2333) 2020-10-02 11:13:55 -04:00
Alexander Skvortsov
20b99bcab1 Ensure that modal hide animates (#2332)
We want to return a promise in``onbeforeremove` with arbitrary loading time to ensure that the animateHide animation has time to complete.
2020-10-02 11:12:49 -04:00
flarum-bot
8325b6eed8 Bundled output for commit f9704f9153 [skip ci] 2020-10-01 18:52:08 +00:00
Alexander Skvortsov
f9704f9153 Fix multiple scrolls to same post in PostStream (#2264)
While more pleasant from an FSM standpoint, comparing the current targetPost to the previous one does not work if goToNumber is called twice in a row for the same post. For instance, if a user clicks the mentions link to a post twice, the post stream breaks.
2020-10-01 14:50:54 -04:00
flarum-bot
09a39d5d95 Bundled output for commit a26f01e49c [skip ci] 2020-10-01 01:03:05 +00:00
Alexander Skvortsov
a26f01e49c Use custom event on ALL SuperTextEditor actions 2020-09-30 21:01:24 -04:00
Sami Mazouz
6d826e5b30 Right align discussion controls dropdown on slidable item (#2330) 2020-09-30 16:53:50 -04:00
flarum-bot
f38605b387 Bundled output for commit 93f8ce78b3 [skip ci] 2020-09-30 20:34:47 +00:00
Alexander Skvortsov
93f8ce78b3 Improve PostStreamState.viewingEnd()
In some cases, such as if we've stickied a post, an event post
may have been added / removed.This means that `this.visibleEnd`
and`this.count()` will be out of sync by 1 post, but we are still
"viewing the end" of the post stream, so we should still reload
all posts up until the last one.
2020-09-30 16:33:10 -04:00
Daniël Klabbers
a001736298 Mark keys for Config required only with InstalledSite (#2323)
* use fallback on url to prevent errors in cli during install. The value of the fallback doesn't actually matter, we just need something.
2020-09-30 15:38:19 -04:00
Daniël Klabbers
86d4bf0214 Fix for a bug that would delete the new revision of less/js in case the filenames match. 2020-09-30 09:26:32 +02:00
Daniël Klabbers
c7b67b922b Allow easier overriding of js compiler (#2318) 2020-09-29 19:03:51 -04:00
flarum-bot
3b63d774d3 Bundled output for commit 86f7550bec [skip ci] 2020-09-29 22:42:20 +00:00
Alexander Skvortsov
86f7550bec Merge pull request #2314 from flarum/as/modal-fix
Frontend Rewrite Followup Modal Fixes
2020-09-29 18:41:03 -04:00
Alexander Skvortsov
9d1a87a4c4 Rename onshow and onhide
animateShow and animateHide are more descriptive
2020-09-29 18:37:56 -04:00
Alexander Skvortsov
a2263b8538 Return on animateShow if already loaded 2020-09-29 18:37:56 -04:00
Alexander Skvortsov
1ac09dbc4d Pass ModalManagerState into Modal instances instead of calling the global. 2020-09-29 18:37:56 -04:00
Alexander Skvortsov
be8fe44f0b Ensure that readyCallback is called on modals opened from other modals 2020-09-29 18:37:56 -04:00
Alexander Skvortsov
b7593bc6a8 Prevent hide animation when opening modal from other modal 2020-09-29 18:37:56 -04:00
Alexander Skvortsov
7fc0963e3c Revert "Fix opening modals from other modals. (#2263)"
This reverts commit 5b157f0adb.
2020-09-29 18:37:56 -04:00
flarum-bot
30f3056f70 Bundled output for commit ed23d7d4e7 [skip ci] 2020-09-29 22:35:50 +00:00
Alexander Skvortsov
ed23d7d4e7 Merge pull request #2299 from flarum/as/poststream_improvements
[Frontend Rewrite] PostStream Improvements
2020-09-29 18:34:16 -04:00
Franz Liedke
1e9f7b7d52 README: Update badge to truly show latest release
Refs #2311.
2020-09-29 23:23:37 +02:00
Abolade Eniseyin
08540fd1db Update logo and badges in README (#2311)
Fixes #2296.
2020-09-29 23:22:51 +02:00
flarum-bot
74fa7122ca Bundled output for commit 4b2d20cd85 [skip ci] 2020-09-29 20:56:44 +00:00
Wadim Kalmykov
4b2d20cd85 fix clear search (#2325) 2020-09-29 16:55:26 -04:00
flarum-bot
077eaaa2f9 Bundled output for commit 6668e75019 [skip ci] 2020-09-28 23:18:39 +00:00
Sami Mazouz
6668e75019 Fix mobile controls gesture on discussion deletion/restoration (#2324)
Because the Slidable class was always added on creation, it was lost every time the class list changed (in this case when the discussion was hidden/unhidden which added/removed DiscussionListItem--hidden class). So by determining the Slidable class's presence in elementAttrs() method, it guarantees it always properly set.
2020-09-28 19:07:05 -04:00
Wadim Kalmykov
d6511e0df5 Improve developer experience by forcing LF line endings (#2321) 2020-09-28 14:04:08 -04:00
Alexander Skvortsov
efd68df13a Pass a translator instance to getEmailSubject on MailableInterface (#2244)
* Pass a translator instance to getMailSubject (breaking change)

* Temporarily comment out getEmailSubject to avoid BC breaks
2020-09-28 00:04:28 -04:00
flarum-bot
f1360a1394 Bundled output for commit cc875f3e95 [skip ci] 2020-09-28 03:51:00 +00:00
Alexander Skvortsov
cc875f3e95 Put m.stream in flarum/utils/stream (#2316) 2020-09-27 23:49:33 -04:00
Franz Liedke
6860b24b70 Use reserved TLD for default dev hostname
See https://jdebp.eu/FGA/dns-use-domain-names-that-you-own.html.
2020-09-27 22:55:46 +02:00
flarum-bot
65766a8386 Bundled output for commit c53509d7d0 [skip ci] 2020-09-27 02:14:07 +00:00
Alexander Skvortsov
c53509d7d0 Add warnings to Mithril 2 BC layer (#2313) 2020-09-26 22:12:43 -04:00
Alexander Skvortsov
4c3e1e2625 Fixed noAnimation: previously, the opposite of what was requested happened 2020-09-25 16:02:39 -04:00
Alexander Skvortsov
6508e64f55 DiscussionPage: only set this.discussion after the initial set of posts has loaded, this results in a slightly smoother initial load. 2020-09-25 15:54:54 -04:00
Alexander Skvortsov
963c27ed60 Provide location data to scrubber earlier to avoid unnecessary and confusing scrubber animation on page load. 2020-09-25 15:52:18 -04:00
Alexander Skvortsov
304f05be36 Don't animate the initial Scrubber placement 2020-09-25 15:43:41 -04:00
Alexander Skvortsov
82af307280 Restore fadeIn to 400ms (#2312)
This is the jQuery fadeIn default, which we were relying on before this animation was changed to pure CSS.
2020-09-25 21:15:24 +02:00
Alexander Skvortsov
50cbb7be5c Merge pull request #2271 from flarum/fl/laravel-updates-config
This extracts another real class for dealing with the configuration options stored in `config.php`. The idea is to reduce the scope of the `Application` class and make it easier to inject exactly what's needed (rather than an array, which is complicated, or the bloated `Application` class).
2020-09-25 11:22:53 -04:00
Franz Liedke
9ea57e6329 Use Config class for data from config.php 2020-09-25 11:10:52 +02:00
Franz Liedke
6639678fb2 Inject/use new config class where applicable 2020-09-25 10:58:53 +02:00
Franz Liedke
f869999011 Add a helper class for managing low-level config 2020-09-25 10:58:52 +02:00
flarum-bot
f885cebdc5 Bundled output for commit 54ff6e720c [skip ci] 2020-09-25 02:32:11 +00:00
Alexander Skvortsov
54ff6e720c Add in BC layer for props, initProps, m.withAttr, and m.prop (#2310) 2020-09-24 22:30:55 -04:00
Daniël Klabbers
aea8a3ff1f Changes methods and properties from private to protected (#2308)
The goal of this PR is to offer increased flexibility for integrators and
custom solutions in skeleton modifications.
2020-09-24 14:30:16 -04:00
Alexander Skvortsov
cc48e9ab22 Replace $app->url() with url-generated link to index (#2302) 2020-09-24 11:30:57 -04:00
Alexander Skvortsov
6d38de9c8f Revert https://github.com/flarum/core/pull/1536 (#2305) 2020-09-24 11:30:27 -04:00
flarum-bot
87634449c0 Bundled output for commit b00ca4ef29 [skip ci] 2020-09-24 04:09:18 +00:00
Matteo Contrini
b00ca4ef29 Fix comment for the time gap feature in PostStream (#2294)
The time interval for the time gap feature is 4 days and not 4 hours.
2020-09-24 00:08:12 -04:00
flarum-bot
fd0f0cdf8b Bundled output for commit 5b157f0adb [skip ci] 2020-09-24 03:13:43 +00:00
Alexander Skvortsov
5b157f0adb Fix opening modals from other modals. (#2263)
While seemingly correct, an onremove method in Modal that triggers animateHide is problematic, because if one modal is opened from another, the one currently open will be removed from the DOM, triggering animateHide, and closing the new modal.

To compensate, an onupdate method now closes a modal if one is open but shouldn't be; this supports the functionality of the old method when the modal is closed not from the modal instance itself (e.g. app.modal.close())

This is not ideal, but necessary. We should consider eventually expanding the modal system to support showing multiple modals at the same time (stacked over each other). Then, we can move this back to individual modals.
2020-09-23 23:12:22 -04:00
Alexander Skvortsov
dc8b203037 Only call updateScrubberValues onupdate when necessary
When the page is scrolled, goToIndex is called, or the page is loaded,
various listeners result in the scrubber being updated with a new
position and values. However, if goToNumber is called, the scrubber
will not be updated. Accordingly, we add logic to the scrubber's
onupdate to update itself, but only when needed, as indicated by this

This saves us a LOT of unnecessary calls, and makes scrubber movement smoother.
2020-09-23 23:06:25 -04:00
Alexander Skvortsov
db71f8bf68 Execute oncreate scrubber update after loadPromise has completed
This way, we ensure that the initial position (and data) of the scrubber is correct. Otherwise, we get blank dates / incorrect location.
2020-09-23 23:06:20 -04:00
Alexander Skvortsov
a004b8e057 Fix $(...).offset() is undefined on some scrolls. 2020-09-23 23:06:14 -04:00
flarum-bot
1ff4076f2a Bundled output for commit 6e9db779cd [skip ci] 2020-09-24 03:06:06 +00:00
Alexander Skvortsov
6e9db779cd Fix double fadein for post stream (#2300) 2020-09-23 23:04:56 -04:00
flarum-bot
f4449e962d Bundled output for commit 71f3379fcc [skip ci] 2020-09-24 02:41:41 +00:00
David Sevilla Martín
71f3379fcc Mithril 2 update (#2255)
* Update frontend to Mithril 2

- Update Mithril version to v2.0.4
- Add Typescript typings for Mithril
- Rename "props" to "attrs"; "initProps" to "initAttrs"; "m.prop" to "m.stream"; "m.withAttr" to "utils/withAttr".
- Use Mithril 2's new lifecycle hooks
- SubtreeRetainer has been rewritten to be more useful for the new system
- Utils for forcing page re-initializations have been added (force attr in links, setRouteWithForcedRefresh util)
- Other mechanical changes, following the upgrade guide
- Remove some of the custom stuff in our Component base class
- Introduce "fragments" for non-components that control their own DOM
- Remove Mithril patches, introduce a few new ones (route attrs in <a>; 
- Redesign AlertManagerState `show` with 3 overloads: `show(children)`, `show(attrs, children)`, `show(componentClass, attrs, children)`
- The `affixedSidebar` util has been replaced with an `AffixedSidebar` component

Challenges:
- `children` and `tag` are now reserved, and can not be used as attr names
- Behavior of links to current page changed in Mithril. If moving to a page that is handled by the same component, the page component WILL NOT be re-initialized by default. Additional code to keep track of the current url is needed (See IndexPage, DiscussionPage, and UserPage for examples)
- Native Promise rejections are shown on console when not handled
- Instances of components can no longer be stored. The state pattern should be used instead.

Refs #1821.

Co-authored-by: Alexander Skvortsov <sasha.skvortsov109@gmail.com>
Co-authored-by: Matthew Kilgore <tankerkiller125@gmail.com>
Co-authored-by: Franz Liedke <franz@develophp.org>
2020-09-23 22:40:37 -04:00
Alexander Skvortsov
1321b8cc28 Revert "Use lifecycle interface for frontend extender (#2211)" (#2301)
This reverts commit 3117d2ad7a.
2020-09-23 00:21:45 -04:00
flarum-bot
fa0ff204dd Bundled output for commit 872e3bdc92 [skip ci] 2020-09-18 19:33:18 +00:00
Alexander Skvortsov
872e3bdc92 Add missing exports to compat 2020-09-18 21:31:35 +02:00
Alexander Skvortsov
79f9012694 Fix Post-actions being on top of Post Controls Dropdown
- This was accidentially introduced when an explicit z-index was added to reply-actions to prevent Post-footer from covering it
- Here, we revert that zindex, while making Post-footer inline-block to stop it from covering everything. We also set height=0 to stop implicitly added height
2020-09-18 15:33:01 +02:00
Wadim Kalmykov
633cc14d09 Fix issue where posts API doesn't return the right amount of posts (#2291) 2020-09-17 23:59:34 -04:00
Alexander Skvortsov
c6e85ef330 Allow upper case TLS and SSL for SMTP encryption (#2289) 2020-09-12 17:43:06 -04:00
phanlyhuynh
3f8432a589 Fix SMTP username and password shouldn't be required (#2287) 2020-09-06 15:12:03 -04:00
flarum-bot
96c95f2b6a Bundled output for commit 8e3e8826f9 [skip ci] 2020-09-04 17:00:30 +00:00
Franz Liedke
8e3e8826f9 app.composer.show: Trigger synchronous redraw
This is needed to have access to the newly created SuperTextarea
instance (app.composer.editor) directly after calling show().

Discovered when making ext-mentions work with the Composer state
changes. As far as I could reconstruct, a synchronous redraw was also
triggered in this situation before the changes in #2161.
2020-09-04 18:58:40 +02:00
Johannes Nilsson
384edfa52b Remove unwanted semicolon in assets files (#2280) 2020-08-31 23:52:37 -04:00
Franz Liedke
f939d164b7 Make queue error handler compatible with Laravel 6 (#2270) 2020-08-27 22:41:36 -04:00
flarum-bot
ebbef75cfb Bundled output for commit 2caa5cf19c [skip ci] 2020-08-28 02:41:03 +00:00
fengkx
2caa5cf19c fix: escape regexp from query (#2273)
* fix: escape regexp from query
2020-08-27 22:39:49 -04:00
Alexander Skvortsov
fe718a1490 Bump fontawesome version to ^5.14.0 (#2274) 2020-08-27 23:01:57 +02:00
flarum-bot
beb03b7771 Bundled output for commit 97186e6086 [skip ci] 2020-08-25 14:54:57 +00:00
Karan Sanjeev
97186e6086 Added an empty alt attribute to avatar's img tag (#2269)
fixes #2256
2020-08-25 10:53:44 -04:00
flarum-bot
47f3ee0ce2 Bundled output for commit a9eb14889e [skip ci] 2020-08-16 20:36:13 +00:00
Matteo Contrini
a9eb14889e Fix number abbreviation when the number is n-thousand (#2261)
This commit fixes the method `abbreviateNumber` so that it behaves as stated in the JSDoc.

Previously, an input of `1234` would have produced `1K`. With this change, the output will be `1.2K`.
2020-08-16 16:35:05 -04:00
flarum-bot
5e5a5294c3 Bundled output for commit c39b6a6d2f [skip ci] 2020-08-16 20:34:13 +00:00
Franz Liedke
c39b6a6d2f Extract a few changes from the Mithril 2 upgrade (#2262)
* Fix closing the composer with ESC key

Regression from #2161.

* Remove obsolete method

Regression from #2162.

* Mark method as protected

* Fade in posts in post stream using CSS

This also avoids a double-fade from the JavaScript code, which was
probably introduced in #2160.

* Fix fadeIn for post stream items

Co-authored-by: Alexander Skvortsov <sasha.skvortsov109@gmail.com>
2020-08-16 16:32:59 -04:00
一枚小猿
22bf03d872 Fix less build error. (#2252) 2020-08-15 20:21:06 -04:00
flarum-bot
a2f3534bf7 Bundled output for commit 6953d93c6d [skip ci] 2020-08-08 18:47:16 +00:00
Alexander Skvortsov
6953d93c6d Extract PostStream state (#2160)
Co-authored-by: Franz Liedke <franz@develophp.org>
2020-08-08 14:45:54 -04:00
dependabot[bot]
f9c9b5d5e4 Bump elliptic from 6.5.2 to 6.5.3 in /js (#2251)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-31 01:08:39 +02:00
Alexander Skvortsov
8a73cc522e Fix optional parameters in url generator (#2246)
* Fix route collection getting wrong path when optional parameters present, add unit tests
2020-07-28 20:51:14 -04:00
Franz Liedke
db83003eb5 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-07-27 19:42:23 +00:00
Franz Liedke
4dc4dc624e Merge pull request #2243 from flarum/fl/2055-l6-translator
Upgrade to Laravel 6, finally!
2020-07-27 21:42:01 +02:00
flarum-bot
ad42058a8a Bundled output for commit 5e465f6051 [skip ci] 2020-07-24 22:18:35 +00:00
Alexander Skvortsov
5e465f6051 Extract Composer state (#2161)
Like previous "state PRs", this moves app-wide logic relating to
our "composer" widget to its own "state" class, which can be
referenced and called from all parts of the app. This lets us
avoid storing component instances, which we cannot do any longer
once we update to Mithril v2.

This was not as trivial as some of the other state changes, as we
tried to separate DOM effects (e.g. animations) from actual state
changes (e.g. minimizing or opening the composer).

New features:

- A new `app.screen()` method returns the current responsive screen
  mode. This lets us check what breakpoint we're on in JS land  
  without hardcoding / duplicating the actual breakpoints from CSS.
- A new `SuperTextarea` util exposes useful methods for directly
  interacting with and manipulating the text contents of e.g. our
  post editor.
- A new `ConfirmDocumentUnload` wrapper component encapsulates the
  logic for asking the user for confirmation when trying to close
  the browser window or navigating to another page. This is used in
  the composer to prevent accidentally losing unsaved post content.

There is still potential for future cleanups, but we finally want   
to unblock the Mithril update, so these will have to wait:

- Composer height change logic is very DOM-based, so should maybe
  not sit in the state.
- I would love to experiment with using composition rather than
  inheritance for the `ComposerBody` subclasses.
2020-07-25 00:17:25 +02:00
flarum-bot
62a2e8463d Bundled output for commit 0098c64ebf [skip ci] 2020-07-24 21:53:31 +00:00
Franz Liedke
0098c64ebf Fix an irrelevant export name :P 2020-07-24 23:51:44 +02:00
Franz Liedke
2b5939d538 Simplify a few unnecessary Arr::get() calls 2020-07-24 22:56:31 +02:00
Alexander Skvortsov
2431df5602 Revert "Fixes wrong IP address when using a reverse proxy (#2236)" (#2242)
This reverts commit 451a557532 pending further discussion of https://github.com/flarum/core/pull/2236#issuecomment-663645583
2020-07-24 14:19:10 -04:00
flarum-bot
264ff67304 Bundled output for commit c08a56e9d8 [skip ci] 2020-07-24 17:03:04 +00:00
Alexander Skvortsov
c08a56e9d8 Notifications Dropdown: Remove init method that doesn't do anything (cleanup) 2020-07-24 13:01:45 -04:00
Alexander Skvortsov
4ee6d6fd88 Revert "Inject Url Generator and Translator Interface into notification mailer (#2169)"
This was actually already present and functional, so adding additional code for it
is unnecessary.

This reverts commit e627616750.
2020-07-24 12:44:59 -04:00
Franz Liedke
9c09fe8465 Update to Laravel 6, finally!
Fixes #2055.
2020-07-24 17:34:40 +02:00
Franz Liedke
b46d5e67a3 Make Translator compatible with Laravel 6
It's contract will change in Laravel 6. We extend from Symfony's
translator, but need to be compatible with that from Laravel in
order to use its validation package.

References:
- https://laravel.com/docs/6.x/upgrade#trans-and-trans-choice
- 8557dc56b1 (diff-88bc04a1548d09aa6250d902d1ac2b4c)
2020-07-24 17:32:50 +02:00
Franz Liedke
7fd23ff950 Inject Symfony translator contract, not Laravel's
The Laravel changes with v6, and our translator is primarily an
implementation of the Symfony contract.
2020-07-24 17:31:46 +02:00
Franz Liedke
e4077ab4ad Replace a few forgotten obsolete helpers
- Apparently, I forgot that `array_flatten` comes from Laravel. :)
- When I did this previously, I did not search the views directory.
2020-07-24 17:28:56 +02:00
Franz Liedke
3b39c212e0 Explicitly bundle Carbon library
We have used this transitive dependency (via illuminate/support)
for a while, so let's make this explicit.

Incidentally, we now also explicitly require version 2.x - the
previous 1.x branch will no longer be supported after the
upcoming upgrade to Laravel 6.

Refs #2055.
2020-07-24 16:46:33 +02:00
Franz Liedke
bca833d3f1 Remove Mandrill mail driver
This is in preparation for the upcoming upgrade to Laravel 6,
which dropped this driver.

Refs #2055.
2020-07-24 16:39:28 +02:00
Jake Esser
451a557532 Fixes wrong IP address when using a reverse proxy (#2236)
Added reverse proxy support to preserve forwarded IPs
2020-07-22 08:55:44 -04:00
Alexander Skvortsov
eaac78650f Deprecate AssertPermissionTrait (#2044) 2020-07-17 15:16:15 +02:00
Franz Liedke
2b3dec2be1 Fix deprecation and removal date 2020-07-17 12:19:48 +02:00
Alexander Skvortsov
37ebeb5705 User Extender (prepareGroups functionality) (#2110) 2020-07-17 12:18:35 +02:00
Franz Liedke
71abac0323 Rename view extender
As discussed in my initial review, it seems unlikely that we need
the ability to remove (or otherwise modify) namespaces again.
Therefore, it seems more consistent with other extenders to go
for a "View" extender with a "namespace" method.

Sorry for the back and forth. ;)

Refs #1891, #2134.
2020-07-17 12:05:49 +02:00
Franz Liedke
7e3d71a0a0 View extender: Do not resolve factory
Not all requests need this factory, so there is no need to
instantiate one and load the required files.

Refs #1891, #2134.
2020-07-17 12:05:38 +02:00
Alexander Skvortsov
b5e891df30 View Extender (add namespace) (#2134) 2020-07-17 11:59:00 +02:00
Alexander Skvortsov
3117d2ad7a Use lifecycle interface for frontend extender (#2211) 2020-07-17 11:49:52 +02:00
dependabot[bot]
1ce0b926b6 Bump lodash from 4.17.15 to 4.17.19 in /js (#2235)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-17 10:47:28 +02:00
flarum-bot
24b16f9d7c Bundled output for commit bd40353bcc [skip ci] 2020-07-10 13:42:33 +00:00
Franz Liedke
bd40353bcc Merge pull request #2207 from flarum/ds/typescript-conversion
Convert several files in `common/utils` to TypeScript
2020-07-10 15:41:14 +02:00
David Sevilla Martin
455327cca1 convert: common/utils/stringToColor 2020-07-10 14:13:33 +02:00
David Sevilla Martin
20baa93ca7 convert: common/utils/string 2020-07-10 14:13:33 +02:00
David Sevilla Martin
4f34e326ef convert: common/utils/RequestError 2020-07-10 14:13:33 +02:00
David Sevilla Martin
521cefbc2d convert: common/utils/liveHumanTimes
This file isn't used anywhere. We should be calling it at some point. It has existed for 5 years.

Renamed function because it makes more sense for name to match file name (not that it matters when building)
2020-07-10 14:13:32 +02:00
David Sevilla Martin
dc738d68dc convert: common/utils/abbreviateNumber 2020-07-10 14:13:32 +02:00
David Sevilla Martin
286af7084b convert: common/utils/extract 2020-07-10 14:13:31 +02:00
David Sevilla Martin
4869baea74 convert: common/utils/ItemList 2020-07-10 14:13:31 +02:00
David Sevilla Martin
24a48310ff convert: common/utils/humanTime 2020-07-10 14:05:09 +02:00
David Sevilla Martin
bdb759c558 convert: common/utils/formatNumber 2020-07-10 14:05:07 +02:00
Matt Kilgore
36eb5cc5fb Add port on Url to BaseUrl Test (#2226)
Added Urls with ports to the BaseUrl Test
2020-07-10 12:17:12 +02:00
David Sevilla Martín
d189272473 Initial TypeScript infrastructure (#2206)
This allows us to get started with converting all Flarum JavaScript code to TypeScript.
In addition, we will have time to experiment to find the best Webpack configuration before integrating into flarum-webpack-config.

See flarum/flarum-webpack-config#3.
2020-07-03 14:47:44 +02:00
flarum-bot
7d48c24dda Bundled output for commit 5786f1a10b [skip ci] 2020-07-03 05:17:34 +00:00
Alexander Skvortsov
5786f1a10b Fix discussions user page (#2225)
* Fixed up discussions user page, improve discussion list state signature
2020-07-03 01:16:08 -04:00
flarum-bot
b4421e1cce Bundled output for commit 359b4ab5a3 [skip ci] 2020-07-02 22:33:46 +00:00
Clark Winkelmann
359b4ab5a3 Fix user card issue by reverting to original behavior (#2224)
* Fix user card issue by reverting to original behavior
2020-07-02 18:32:41 -04:00
Alexander Skvortsov
8a686911ff Don't create user bio column on new installations (#2215) 2020-07-01 17:31:52 -04:00
Alexander Skvortsov
0b5a9a2fe6 Make scrubber handle have transparent background (#2222) 2020-07-01 17:07:13 -04:00
flarum-bot
50a9f7ce86 Bundled output for commit 8dd5420405 [skip ci] 2020-07-01 00:34:15 +00:00
David Sevilla Martín
8dd5420405 Switch from 'moment' to 'dayjs' (#2219)
* Switch from 'moment' to 'dayjs'

* Use humanize code from duration plugin (without actual plugin) for time lapsed events
2020-06-30 20:33:00 -04:00
flarum-bot
640cc0989b Bundled output for commit 44376cef61 [skip ci] 2020-07-01 00:00:24 +00:00
Alexander Skvortsov
44376cef61 Extract ModalManagerState from ModalManager (#2162) 2020-06-30 19:59:16 -04:00
flarum-bot
4f181c84fc Bundled output for commit ea9d601338 [skip ci] 2020-06-30 22:08:06 +00:00
Alexander Skvortsov
ea9d601338 Extract AlertManagerState from AlertManager (#2163) 2020-06-30 18:06:59 -04:00
Alexander Skvortsov
aaebd3581f Fix: Use proper variable for display name drivers in user extender 2020-06-29 19:32:08 -04:00
flarum-bot
e2c416903e Bundled output for commit e81159249f [skip ci] 2020-06-28 17:45:26 +00:00
Alexander Skvortsov
e81159249f Add check to register state of '0' as false for checkboxes (#2210)
* Add check to register state of '0' as false for checkboxes
* Add comment explaining state === '0'
2020-06-28 13:44:14 -04:00
flarum-bot
d93cf4a574 Bundled output for commit a33fbbf814 [skip ci] 2020-06-27 18:20:09 +00:00
Alexander Skvortsov
a33fbbf814 Add index page title, add mechanism to clear title from defaultRoute. (#2047)
* Add "All Descriptions title to index

* Added system to clear custom title if we're on the default route
2020-06-27 14:18:49 -04:00
flarum-bot
0c645a6c15 Bundled output for commit b44b79eba9 [skip ci] 2020-06-26 16:25:45 +00:00
Franz Liedke
b44b79eba9 Fix typo and update outdated doc block 2020-06-26 18:23:56 +02:00
flarum-bot
93398b738b Bundled output for commit 7816b61bfb [skip ci] 2020-06-26 14:08:35 +00:00
Franz Liedke
7816b61bfb Remove documentation for obsolete component prop 2020-06-26 16:06:56 +02:00
Franz Liedke
7dc3a194c3 Expose a method for clearing notification list
Needed for pusher extension.

Refs #2185.
2020-06-26 15:10:41 +02:00
flarum-bot
cea7824b57 Bundled output for commit 088eb0c4f2 [skip ci] 2020-06-26 12:32:40 +00:00
Franz Liedke
088eb0c4f2 Move DiscussionListState to correct folder 2020-06-26 12:52:33 +02:00
Franz Liedke
2ba67b021f Expose state classes via compat
This way, they can be extended by extensions.
2020-06-26 12:50:43 +02:00
flarum-bot
92791a253d Bundled output for commit 138c784a50 [skip ci] 2020-06-24 00:51:55 +00:00
David Sevilla Martín
138c784a50 Call liveHumanTimes() to update ago times every 10s (#2208)
This file has existed for 5 years, yet it was never used.
2020-06-23 20:50:57 -04:00
flarum-bot
bb567e5278 Bundled output for commit cf4f2f283e [skip ci] 2020-06-20 14:19:53 +00:00
w-4
cf4f2f283e Fix discussion unreadCount could be higher than commentCount (#2195)
* Fix discussion unreadCount being higher than commentCount if posts have been deleted
2020-06-20 10:18:26 -04:00
flarum-bot
ed01f389a8 Bundled output for commit 71e313e677 [skip ci] 2020-06-19 21:42:28 +00:00
Alexander Skvortsov
71e313e677 Clean up app.current, app.previous in JS (#2156)
- Encapsulate app.current, app.previous in PageState objects
- Reorganize Page classes to use one central base class in common

Co-authored-by: Franz Liedke <franz@develophp.org>
2020-06-19 17:41:26 -04:00
Franz Liedke
88366fe8af Clean up usages / deprecate path helpers (#2155)
* Write source map without creating temp file

Less I/O, and one less place where we access the global path helpers.

* Drop useless app_path() helper

This was probably taken straight from Laravel. There is no equivalent
concept in Flarum, so this should be safe to remove.

* Deprecate global path helpers

Developers using these helpers can inject the `Paths` class instead.

* Stop storing paths as strings in container

* Avoid using path helpers from Application class

* Deprecate path helpers from Application class

* Avoid using public_path() in prerequisite check

a) The comparison was already outdated, as a different path was passed.
b) We're trying to get rid of these global helpers.
2020-06-19 16:16:03 -04:00
flarum-bot
b82504b4b1 Bundled output for commit 898d68d9f3 [skip ci] 2020-06-19 00:30:16 +00:00
Franz Liedke
898d68d9f3 Remove leftover property
Refs #2150.
2020-06-19 02:27:01 +02:00
flarum-bot
69f0172b92 Bundled output for commit 62fe9db732 [skip ci] 2020-06-19 00:11:51 +00:00
Alexander Skvortsov
62fe9db732 Don't store PostUser instance in CommentPost (#2184)
* Don't save component state in CommentPost
2020-06-18 20:10:25 -04:00
flarum-bot
ed566cd18f Bundled output for commit 5c1663d8f1 [skip ci] 2020-06-18 23:54:42 +00:00
Alexander Skvortsov
5c1663d8f1 Move Discussion List State into its own class (#2150)
Extract discussion list state
2020-06-18 19:53:40 -04:00
flarum-bot
c5d3b058ba Bundled output for commit 4a804dbbbc [skip ci] 2020-06-18 22:48:18 +00:00
Alexander Skvortsov
4a804dbbbc Remove app.search instance, cache app.cache.searched (#2151)
* Moved search state logic into search state
2020-06-18 18:47:01 -04:00
flarum-bot
f4afb006ed Bundled output for commit 646b35374d [skip ci] 2020-06-18 21:29:07 +00:00
Alexander Skvortsov
646b35374d Don't store checkbox instances in NotificationGrid (#2183)
* Don't store checkbox states in NotificaitonGrid, use props for loading in Checkbox and Switch, replace preferenceSaver with internal management of loading state
2020-06-18 17:28:05 -04:00
flarum-bot
4fc06336df Bundled output for commit 65f2d5fb75 [skip ci] 2020-06-18 21:09:49 +00:00
Alexander Skvortsov
65f2d5fb75 Extract NotificationList state (#2185)
* Extract NotificationList state
2020-06-18 17:08:06 -04:00
Alexander Skvortsov
5bca4fda9d Return the proper error code when wrong password when changing email (#2171) 2020-06-17 20:43:04 -04:00
Clark Winkelmann
b87c7189cc Remove BioChanged event which is no longer used since beta 8 (#2196) 2020-06-15 00:21:06 -04:00
Clark Winkelmann
17c239388a Fix AvatarChanged event (#2197)
* Fix AvatarChanged event not being dispatched when changing avatar
Also fix the uploader to trigger the event only once
2020-06-15 00:20:24 -04:00
Alexander Skvortsov
4da2994d1f Group Gambit Improvements (#2192)
* - Add ID to fields searched in group gambit
- Use joins instead of looping in group gambit
* Add visibility scoping to group gambit
* call IDs userIds
* If group identifier is numerical, treat it as an ID
2020-06-08 17:35:24 -04:00
Matt Kilgore
293e2251ca Fixes #2157, Explicitly set SameSite value for cookies (#2159)
* Fixes #2157, Explicitly set SameSite value for cookies by making samesite a config option in config.php. Also contains an update for the cookie library dependency
2020-06-03 22:53:30 -04:00
flarum-bot
3b1f5ca07b Bundled output for commit d1750fecc0 [skip ci] 2020-05-31 02:50:39 +00:00
Alexander Skvortsov
d1750fecc0 Send Test Mail Feature (#2023)
- Add UI, backend for sending test emails
- Change mail settings endpoint to /api/mail/settings
2020-05-30 22:49:36 -04:00
flarum-bot
63242edeb3 Bundled output for commit 0aed3764c4 [skip ci] 2020-05-31 02:29:29 +00:00
Hasan Özbey
0aed3764c4 Scroll to edited post or inform the user (#2108)
* scroll to edit or inform the user
2020-05-30 22:28:08 -04:00
Alexander Skvortsov
7b1269207e Get rid of Laravel Gate contract (#2181)
* Get rid of unnecessary uses of gate

* Move gate off of Laravel's gate contract
2020-05-28 18:00:44 -04:00
Sami Mazouz
bab084a75f Fix Paths test failing on Windows (#2187)
* Fix directory separator for windows os

* Change Paths to use a forward slash instead
2020-05-28 12:42:54 -04:00
Alexander Skvortsov
3c87f800dd Instances of models should not matter when checking permissions (#2186) 2020-05-27 12:22:08 -04:00
Matt Kilgore
26256c436f Fix installer removing URL port (#2182)
* Fix installer removing URL port
2020-05-25 14:35:22 +02:00
Franz Liedke
63397bb466 Allow manipulating error handler through extender
By giving each middleware a name, they can now be replaced or moved
around using the Middleware extender.

Fixes #2115.
2020-05-24 08:47:26 +02:00
w-4
4b6864534b Fix header contents moving when opening modal (#2131)
* add navbar-fixed-top css class

* App-header position:fixed
2020-05-23 14:41:54 -04:00
Franz Liedke
c4f4f218bf Tests: Actually accept multiple extenders
We did pass multiple extenders to this method in the tests for the
`Model` extender - now this actually has the desired effect.
2020-05-23 02:00:25 +02:00
Franz Liedke
4866e7d9ba Stop using app() helper in tests 2020-05-23 01:56:21 +02:00
Sami Mazouz
d6acf28fcb Add z-index rule as part of fixing replies dropdown menu width (#2178) 2020-05-22 18:50:39 -04:00
Alexander Skvortsov
e627616750 Inject Url Generator and Translator Interface into notification mailer (#2169) 2020-05-22 18:10:31 -04:00
flarum-bot
bbd815a9ab Bundled output for commit acf4e9c80d [skip ci] 2020-05-20 00:53:05 +00:00
Alexander Skvortsov
acf4e9c80d Removed excess Widget class in favor of DashboardWidget (#2164) 2020-05-19 20:52:07 -04:00
flarum-bot
1bb5f99a27 Bundled output for commit b0822df759 [skip ci] 2020-05-19 22:46:59 +00:00
Alexander Skvortsov
b0822df759 Use drivers for display names, add display name extender (#2174)
* Deprecate GetDisplayName event

* Add interface for display name driver

* Add username driver as default

* Add code to register supported drivers / used driver as singletons

* Configured User class to use new driver-based system for display names

* Add extender for adding display name driver

* Add integration test for user display name driver

* Add frontend UI for selecting display name driver
2020-05-19 18:45:56 -04:00
flarum-bot
998e32c208 Bundled output for commit f89f114fad [skip ci] 2020-05-16 00:11:53 +00:00
julakali
f89f114fad Don't use body as tooltip container, allow notification area overflow (#2166)
* Don't use body as tooltip container, allow notification area overflow

Badge tooltips are using container: 'body', so they can overflow the
notification area. When the user navigates back while a badge tooltip is
showing, the tooltip remains visible.
This commit removes the body container attribute and instead allows the
notificationDropDown to overflow, so badge tooltips aren't cut off.
Instead, this adds overflow: hidden to NotificationList.
Fixes #2118.

* Remove newline
2020-05-15 20:10:40 -04:00
flarum-bot
9b936d4baa Bundled output for commit 7e661df15d [skip ci] 2020-05-12 16:24:38 +00:00
David Sevilla Martín
7e661df15d Some improvements to request error handling and modal error formatting (#1929)
* Use decodeURI instead of unescape & don't close modals

* Add comment

* Don't use a try/catch, clean up the group log code

* Remove double negative

* Format; fix issues from rebasing
2020-05-12 12:23:13 -04:00
Franz Liedke
b7355db2b7 Merge pull request #2154 from flarum/fl/2055-l58
Upgrade to Laravel 5.8
2020-05-12 15:20:01 +02:00
Franz Liedke
5dc9451c21 Fix notification query with DB prefix
This was fixed in https://github.com/laravel/framework/pull/28400.
See commit 7f1048352d.
2020-05-09 14:45:57 +02:00
Franz Liedke
220c8c66b0 Fix signature of HandleErrors middleware
In Laravel 5.8, the `Container::tagged()` method was changed to return
an iterator [1].

We only use the result for iteration, or, in this case, to pass a bunch
of "reporters" to the error handler middleware, therefore we need to
accept an iterable here.

[1]: https://laravel.com/docs/5.8/upgrade#container-generators
2020-05-08 23:30:17 +02:00
Franz Liedke
484933db7d Test setup: Do not use env() helper
Not needed, and not working without a full Laravel installation.
2020-05-08 23:30:17 +02:00
Franz Liedke
f6347dcc46 Update Laravel components to v5.8
First part of #2055.
2020-05-08 21:46:13 +02:00
Franz Liedke
107b4be726 Remove empty comment 2020-05-08 16:05:25 +02:00
Franz Liedke
93d4192b54 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-05-08 14:03:48 +00:00
Franz Liedke
ecdce44d55 Fix container configuration when not installed 2020-05-08 16:03:20 +02:00
Franz Liedke
a5e286e662 Drop MigrationServiceProvider 2020-05-08 12:04:24 +02:00
Franz Liedke
443949f7b9 Fix generate:migration command for extensions
Apparently, this code was from back when we had a special "extensions"
directory for Composer packages marked as Flarum extensions.

While we're at it, we now inject the Paths instance instead of using one
of the global helpers (which I am trying to get rid of).

Refs #2055.
2020-05-08 12:01:11 +02:00
Franz Liedke
4884aad2f0 Update beta.13 changelog 2020-05-08 11:35:46 +02:00
Franz Liedke
365eb15d29 Merge pull request #2142 from flarum/fl/2055-prepare-for-laravel-58
Split up Application and Container
2020-05-07 22:49:36 +02:00
flarum-bot
85e2623622 Bundled output for commit 7d99727168 [skip ci] 2020-05-07 07:20:06 +00:00
Daniël Klabbers
7d99727168 commit version constant 2020-05-07 09:17:26 +02:00
Daniël Klabbers
84784c9839 Release v0.1.0-beta.13 2020-05-07 09:18:04 +02:00
Franz Liedke
a9470b463f Make two more tests compatible with PHPUnit 8 2020-05-07 09:18:04 +02:00
Franz Liedke
deb48bd173 Remove obsolete method 2020-05-07 09:18:04 +02:00
Alexander Skvortsov
b38bd60362 Added simply confirmation popup for hiding / deleting posts (#2135) 2020-05-07 09:18:04 +02:00
Franz Liedke
260e7cd48f Inject new Paths class instead of Application
This (and similar work in other areas) will allow us to further
reduce the API surface of the Application class.

Separation of concerns etc.
2020-05-01 15:47:35 +02:00
Franz Liedke
41a56c4ad1 Split up Application and Container
- Stop trying to implement Laravel's Application contract, which
  has no value for us.
- Stop inheriting from the Container, injecting one works equally
  well and does not clutter up the interfaces.
- Inject the Paths collection instead of unwrapping it again, for
  better encapsulation.

This brings us one step closer toward upgrading our Laravel
components (#2055), because we no longer need to adopt the changes
to the Application contract.
2020-05-01 15:47:35 +02:00
Franz Liedke
d0ae2839f0 Extract a class to hold / determine paths 2020-05-01 15:24:20 +02:00
flarum-bot
d31a747631 Bundled output for commit 526081bd06 [skip ci] 2020-05-01 09:53:55 +00:00
Franz Liedke
526081bd06 Update Webpack 2020-05-01 11:52:26 +02:00
Franz Liedke
cbdd3c5cc7 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-04-27 20:04:41 +00:00
Franz Liedke
7d1ef9d891 Remove a bunch of deprecated events
Use extenders instead!

Refs #1891.
2020-04-27 22:04:08 +02:00
Alexander Skvortsov
7794546845 Model extender: Fix inheritance (#2132)
This ensures that default values, date attributes and relationships are properly inherited, when we have deeper model class hierarchies.

This also adds test cases to ensure that inheritance order is honored for relationship and default attribute extender. As there's no way to remove date attributes, the order of evaluation there doesn't matter.
2020-04-24 21:17:31 +02:00
Franz Liedke
c43cc874ee Model extender: Add failing test
We determined that child classes are not properly affected when
extending the parent classes.

Refs #2100.
2020-04-24 17:54:30 +02:00
Franz Liedke
33cf94c192 Fix test to match its description
Refs #2100.
2020-04-24 17:31:08 +02:00
Franz Liedke
036e519865 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-04-24 14:56:37 +00:00
Franz Liedke
9386c91af9 Tweak model extender tests
- Format code
- Reorder methods
- Test a different scenario to avoid the use of sleep()

Refs #2100.
2020-04-24 16:55:04 +02:00
Franz Liedke
8306cef963 Clean up model extender
- Remove unused private attributes
- Complete docblocks
- Add scalar type hints
- Format code
- Reorder methods

Refs #2100.
2020-04-24 16:33:08 +02:00
Franz Liedke
51ea326959 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-04-24 13:10:36 +00:00
Alexander Skvortsov
15bed971e6 Add model extender (#2100)
This covers default attribute values, date attributes and custom relationships.
2020-04-24 15:10:24 +02:00
Franz Liedke
c896cd8696 npm audit fix 2020-04-24 14:30:16 +02:00
flarum-bot
54ac83d0b6 Bundled output for commit 1592cd1013 [skip ci] 2020-04-22 21:38:57 +00:00
Franz Liedke
1592cd1013 CI: Shorten the lint job name 2020-04-22 23:37:37 +02:00
Alexander Skvortsov
6e8884f190 Implement hidden permission groups (#2129)
Only users that have the new `viewHiddenGroups` permissions will be able to see these groups.

You might want this when you want to give certain users special permissions, but don't want to make your authorization scheme public to regular users.

Co-authored-by: luceos <daniel+github@klabbers.email>
2020-04-21 17:49:53 +02:00
Franz Liedke
df8f73bd3d Statically access Flarum version everywhere
One less reason to inject the huge Application class.

Refs #2055.
2020-04-21 16:48:36 +02:00
Franz Liedke
3f0f89afb1 Use Container contract where easily possible
Less usages of the Application god-class simplifies splitting it up.

Refs #2055.
2020-04-21 16:48:06 +02:00
Franz Liedke
f0f301c5f4 Add compatiblity with Composer 2.0
- The structure of vendor/composer/installed.json will change.
- The same file will now contain the relative path to package locations.

References:
- https://github.com/composer/composer/blob/master/UPGRADE-2.0.md
- https://php.watch/articles/composer-2
2020-04-21 15:47:58 +02:00
Franz Liedke
3045bde167 Format code
- Early returns
- Comments
- Write variables only when needed

Refs #2020.
2020-04-19 16:53:52 +02:00
Robert Korulczyk
ee7a4627d8 Load only translations for enabled extensions from language packs (#2020)
fix #1837

Co-authored-by: Daniel Klabbers <daniel+git@klabbers.email>
2020-04-19 16:29:45 +02:00
Franz Liedke
b9fb92d49a Inline test class
Refs #1977.
2020-04-19 15:55:10 +02:00
Clark Winkelmann
b5accca957 Make AbstractPolicy compatible with both object and class as $model (#1977) 2020-04-19 15:52:59 +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
Daniël Klabbers
fb0a875c6d added author Daniel Klabbers 2019-09-16 10:37:51 +02:00
David Sevilla Martín
74b6b9935b Prepare beta.10 release (#1885)
* Update Application version string to beta 10
* Add beta.10 changelog
2019-09-16 09:28:17 +02:00
Franz Liedke
3b5691ee28 Restore beta.9 behavior of assertCan()
In flarum/core#1854, I changed the implementation of `assertCan()` to be
more aware of the user's log-in status. I came across this when unifying
our API's response status code when actors are not authenticated or not
authorized to do something.

@luceos rightfully had to tweak this again in ea84fc4, because the
behavior changed for one of the few API endpoints that checked for a
permission that even guests can have.

It turns out having this complex behavior in `assertCan()` is quite
misleading, because the name suggests a simple permission check and
nothing more.

Where we actually want to differ between HTTP 401 and 403, we can do
this using two method calls, and enforce it with our tests.

If this turns out to be problematic or extremely common, we can revisit
this and introduce a method with a different, better name in the future.

This commit restores the method's behavior in the last release, so we
also avoid another breaking change for extensions.
2019-09-14 21:32:00 +02:00
Franz Liedke
18593e0d7d Add a test for viewUserList guest permission
This test would have failed without commit ea84fc4. Next, I will revert
that commit and most of my PR #1854, so we need this test to ensure the
API continues to behave as desired.
2019-09-14 21:30:09 +02:00
Franz Liedke
40e1b61fe6 Apply fixes from StyleCI
[ci skip] [skip ci]
2019-09-14 18:57:28 +00:00
Franz Liedke
95dcb45d65 Convert more controller tests to feature tests 2019-09-14 13:09:56 +02:00
Franz Liedke
bd989df769 Update vulnerable JS dependencies 2019-09-13 15:26:10 +02:00
Franz Liedke
538136153c Send a HTTP 401 for incorrect login credentials
This fixes a regression from #1843 and #1854. Now, the frontend again
shows the proper "Incorrect login details" message instead of "You
do not have permission to do that".
2019-09-13 15:03:03 +02:00
Franz Liedke
c330662241 Convert another controller test to feature test
Decouple from implementation, test closer to HTTP...
2019-09-13 14:58:45 +02:00
flarum-bot
588cbaee2d Bundled output for commit a9557c399a [skip ci] 2019-09-12 22:36:12 +00:00
David Sevilla Martín
a9557c399a Fix errors caused by deletion alert when deleting users (#1883)
Refs #1788

TypeError: t.showDeletionAlert is not a function
  at onSuccess(./src/forum/utils/UserControls.js:104:12)

Also, don't override 'this' param with user object for editAction
2019-09-13 00:34:05 +02:00
Daniël Klabbers
14e7bc73ee moved the artisan binary override and commented some of the bindings for queue 2019-09-12 09:11:12 +02:00
flarum-bot
edc579fa6f Bundled output for commit 119831e51c [skip ci] 2019-09-11 22:16:20 +00:00
David Sevilla Martin
119831e51c Fixes an issue where deleting a nonexistent model would error instead of resolving gracefully 2019-09-11 18:14:37 -04:00
Daniël Klabbers
2aee020c14 prevent constant to be duplicated during tests 2019-09-11 12:20:35 +02:00
Daniël Klabbers
f20696210e Merge branch 'master' of github.com:flarum/core 2019-09-11 11:59:10 +02:00
Daniël Klabbers
ea84fc4836 Fixes an issue where permission checks aren't made for guest users,
due to the gate being accessed after the check whether the user
is registered/signed in.
2019-09-11 11:58:27 +02:00
luceos
5ff04d0c68 Apply fixes from StyleCI
[ci skip] [skip ci]
2019-09-11 09:43:46 +00:00
Daniël Klabbers
e2ec52c28c Fixes the queue listen command. We might need to rectify this implementation before stable. 2019-09-11 11:42:52 +02:00
Daniël Klabbers
6196081bdf Fixes an issue where a different cache driver is used and Formatter
attempts to load the s9e Renderer from the wrong cache. It has
to be saved locally so that it can be properly loaded using
the spl auto register functionality.
2019-09-10 12:33:25 +02:00
Franz Liedke
6d8e6583c8 Fix instructions in PR template 2019-09-10 00:18:04 +02:00
flarum-bot
c2b0060852 Bundled output for commit 24964b94bf [skip ci] 2019-09-09 21:37:54 +00:00
David Sevilla Martín
24964b94bf Mark notification as read without visiting discussion (#1874) 2019-09-09 23:36:06 +02:00
flarum-bot
2b624c935d Bundled output for commit 2e647cdda8 [skip ci] 2019-09-09 21:07:00 +00:00
David Sevilla Martín
2e647cdda8 Fix error thrown if textarea doesn't exist in TextEditor (#1852)
* Prevent textarea not existing from causing errors to be thrown

* Replace [0] with .length
2019-09-09 17:05:11 -04:00
Daniël Klabbers
ba175144f4 listen and restart currently fail in the queue, see #1879 2019-09-09 15:47:56 +02:00
flarum-bot
e9af36ab47 Bundled output for commit 8b3913339a [skip ci] 2019-09-08 17:33:39 +00:00
Matthew Kilgore
8b3913339a Fix the new edit user permission label (#1870) 2019-09-08 13:31:57 -04:00
David Sevilla Martín
3cced4156f Add DB prefix to PHP tests (#1855)
* Add test job with PHP 7.3, MySQL & custom prefix

* Add prefix MariaDB test

* Add PHP 7.4 to tests

* Remove PHP 7.4 from tests

This reverts commit 270cba2f5f.
2019-09-08 13:28:39 -04:00
David Sevilla Martín
e88a9394ed Add back defaults for language and direction attributes (#1860) 2019-09-05 08:28:52 +02:00
flarum-bot
ba73c59601 Bundled output for commit 0191babb05 [skip ci] 2019-09-05 00:34:59 +00:00
Franz Liedke
0191babb05 Optimize ScrollListener performance
Listen to "scroll" event and throttle callback executions instead
of actively polling for changes to the scroll position.

Fixes #1222.
2019-09-05 02:17:09 +02:00
Franz Liedke
ed51f9ff0a Fix failing test 2019-09-05 00:07:40 +02:00
Franz Liedke
0a2bdbaa09 Debug mode: Include stacktrace in JSON-API errors
Refs #1843, #1865.
2019-09-04 23:35:32 +02:00
Franz Liedke
26229db1fd Refactor JSON-API error formatter 2019-09-04 23:30:22 +02:00
Franz Liedke
1aef3162be Apply fixes from StyleCI (#1867)
[ci skip] [skip ci]
2019-09-04 01:44:59 +02:00
Franz Liedke
dcf88df0c7 Restore error details in JSON-API error formatter
Fixes #1865. Refs #1843.
2019-09-04 01:44:22 +02:00
Franz Liedke
3eb28dfb16 Convert controller test to request test
This further decouples these tests from the implementation (i.e. which
controller are we calling?).
2019-09-04 01:27:24 +02:00
Matteo Contrini
1d43371fa9 Allow formatting post content without a request (#1848) 2019-09-04 00:12:28 +02:00
Matthew Kilgore
4df455cf04 Add Edit User permission to permissions grid (#1859) 2019-09-03 23:54:38 +02:00
Franz Liedke
2c43ccf66c Merge pull request #1854 from flarum/fl/1641-fix-status-codes
Error handling: Fix status codes
2019-09-02 16:33:48 +02:00
dependabot[bot]
1d010efbca Bump lodash from 4.17.11 to 4.17.15 in /js (#1863)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-28 09:11:25 +02:00
dependabot[bot]
2135d5908e Bump mixin-deep from 1.3.1 to 1.3.2 in /js (#1862)
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-28 09:06:38 +02:00
Franz Liedke
9640dd6419 Remove unnecessary dependency
Refs #1773.
2019-08-22 10:04:38 +02:00
Franz Liedke
98464a8a33 Remove superfluous ForbiddenException
It has the same effect as the PermissionDeniedException, so let's
just use that.

Refs #1641.
2019-08-22 00:06:26 +02:00
Franz Liedke
2b6535525b When signups are prohibited, respond with HTTP 403 2019-08-21 23:48:24 +02:00
Franz Liedke
b60617b849 Move authentication check into assertCan() method
This will cause the right error (HTTP 401) to be thrown whenever
we're checking for a specific permission, but the user is not even
logged in. Authenticated users will still get HTTP 403.
2019-08-21 23:48:03 +02:00
Franz Liedke
0836d99e83 Remove unnecessary indirection 2019-08-21 00:06:32 +02:00
Franz Liedke
279c7df9b9 Document permission check methods 2019-08-21 00:06:31 +02:00
Franz Liedke
04bcf1eef6 Fix inconsistent status codes
HTTP 401 should be used when logging in (i.e. authenticating) would make
a difference; HTTP 403 is reserved for requests that fail because the
already authenticated user is not authorized (i.e. lacking permissions)
to do something.
2019-08-21 00:06:31 +02:00
Franz Liedke
70e98f810c Travis: Remove deploy key 2019-08-21 00:06:16 +02:00
David Sevilla Martín
3851d805f7 Move to GitHub Actions (#1853) 2019-08-21 00:05:04 +02:00
Franz Liedke
085468382a Error handling: Document another interface 2019-08-20 22:20:11 +02:00
Franz Liedke
7dbdd8c024 Rename method 2019-08-20 20:08:01 +02:00
Franz Liedke
ad25307e68 Error handling: Tweak Reporter interface
Because reporters are used for exceptions we were not able to handle, it
makes sense to simply pass the exception, not the "handled error".
2019-08-20 20:07:56 +02:00
Franz Liedke
6c454b8279 Error handling: Document classes and interfaces 2019-08-20 20:07:52 +02:00
Franz Liedke
9f15e9ba86 Error handling: Rename renderers to formatters
Refs #1641.
2019-08-20 20:07:47 +02:00
Franz Liedke
41009dba74 Remove obsolete queue config 2019-08-19 22:33:32 +02:00
Daniël Klabbers
a045f8bef9 Queue support (#1773)
Implementation of clean queue handling, by default sync is used
2019-08-19 21:44:06 +02:00
Franz Liedke
689d767f82 Don't fail when extend.php doesn't return an array
Refs #1607.
2019-08-16 12:29:31 +02:00
Franz Liedke
77fff9fde8 #1607: Show more details when catching boot errors 2019-08-16 12:13:47 +02:00
Franz Liedke
c6c1ae32e6 Bubble up exception for invalid confirmation token
This way, the error handler can simply be amended to deal with this
exception type with a dedicated error message or page.

Refs #1337.
Closes #1528.
2019-08-14 19:47:56 +02:00
Franz Liedke
bdac88b573 Determine error view and message based on type
...not based on status code.

To simplify this logic, we now use the same error "type" both when
routes are not found and specific models are not found. One exception is
ours, one is from Laravel, but for the purposes of error handling they
should be treated the same.

Fixes flarum/core#1641.
2019-08-14 19:47:56 +02:00
flarum-bot
31ee65be93 Bundled output for commit 29df6b60be [skip ci] 2019-08-14 06:41:45 +00:00
Franz Liedke
29df6b60be Tweak translation keys, always use full keys
Makes them easier to grep when editing / removing.

Refs #1750, #1788.
2019-08-14 08:34:36 +02:00
Franz Liedke
1e6f175379 Extract real method
Refs #1750, #1788.
2019-08-14 08:34:14 +02:00
flarum-bot
065ff3456f Bundled output for commit 37e0a5579b [skip ci] 2019-08-13 21:02:10 +00:00
Tobias Karlsson
37e0a5579b Improve feedback on user deletion
Fixes #1750, #1777
2019-08-13 22:56:24 +02:00
Franz Liedke
cd9aa0096e Merge pull request #1843 from flarum/fl/1641-exception-handling
Implement new error handling stack
2019-08-13 22:45:46 +02:00
Franz Liedke
d06493c61e Support multiple error reporters
The error handling middleware now expects an array of reporters.
Extensions can register new reporters in the container like this:

    use Flarum\Foundation\ErrorHandling\Reporter;

    $container->tag(NewReporter::class, Reporter::class);

Note that this is just an implementation detail and will be hidden
behind an extender.
2019-08-10 11:04:12 +02:00
Franz Liedke
9f71e2c3cb Remove old error handler, middleware and tests 2019-08-10 00:26:24 +02:00
Franz Liedke
81a8736ba9 API Client: Use new error handling mechanism 2019-08-10 00:26:24 +02:00
Franz Liedke
57ce25301d Use new error handler middleware 2019-08-10 00:26:24 +02:00
Franz Liedke
cfbaa84fbc Wire up new error handling stack 2019-08-10 00:26:23 +02:00
Franz Liedke
3417f5a77e Make existing extensions compatible with new stack 2019-08-10 00:26:22 +02:00
Franz Liedke
1035636d0f Implement new error handling stack
This separates the error registry (mapping exception types to status
codes) from actual handling (the middleware) as well as error formatting
(Whoops, pretty error pages or JSON-API?) and reporting (log? Sentry?).

The components can be reused in different places (e.g. the API client
and the error handler middleware both need the registry to understand
all the exceptions Flarum knows how to handle), while still allowing to
change only the parts that need to change (the API stack always uses the
JSON-API formatter, and the forum stack switches between Whoops and
pretty error pages based on debug mode).

Finally, this paves the way for some planned features and extensibility:
- A console error handler can build on top of the registry.
- Extensions can register new exceptions and how to handle them.
- Extensions can change how we report exceptions (e.g. Sentry).
- We can build more pretty error pages, even different ones for
  exceptions having the same status code.
2019-08-10 00:26:22 +02:00
Franz Liedke
d00fc2c49d Remove obsolete constructor parameter
This was removed in commit 484c6d2e.
2019-08-10 00:16:21 +02:00
flarum-bot
f3b889a665 Bundled output for commit c5122bf5d5 [skip ci] 2019-08-08 23:12:56 +00:00
Franz Liedke
c5122bf5d5 a11y: Try to make screenreaders read tooltips
Refs #1835.
2019-08-08 22:40:30 +02:00
David Sevilla Martín
5ed55195e1 Add canonical URL to discussion list (#1814) 2019-08-08 00:01:25 +02:00
Franz Liedke
8604ea3020 Bypass CSRF token check when using access tokens
Fixes #1828.
2019-08-01 22:53:31 +02:00
Franz Liedke
2648e960a7 Make exception message dynamic as well 2019-08-01 22:25:28 +02:00
Daniël Klabbers
f0dff95d62 Merge branch 'master' of github.com:flarum/core 2019-07-31 17:10:47 +02:00
Daniël Klabbers
894db01ad8 Allows configuration of where the language files live. So that
language packs can optionally decide for themselves if they want
to use a different directory.
2019-07-31 17:10:13 +02:00
Franz Liedke
bd04023359 Determine default route after extensions
Fixes #1819.
2019-07-30 00:56:29 +02:00
Franz Liedke
f357434a72 PHPUnit: Get rid of deprecated annotation
Refs #1795.
2019-07-30 00:09:10 +02:00
Daniël Klabbers
c2586586c4 fixes #1695, take into consideration is_private with counts on User stats 2019-07-28 20:59:12 +02:00
Daniël Klabbers
06cd062a1b fixed ci, make green again; mysql service wasnt booted 2019-07-27 22:24:39 +02:00
Franz Liedke
1502fc98d8 Prevent MySQL search operators from taking effect
We do not want to inherit MySQL's fulltext query language, so let's
just drop all non-word characters from the search term.

Fixes #1498.
2019-07-23 23:55:06 +02:00
Franz Liedke
ed97989ca2 Revert "Remove deprecated bootstrap.php fallback"
This reverts commit f8061bbca1.

We will keep this fallback in place, to avoid unnecessary breakage of
backwards compatibility for extension authors.

Removal is planned for the final 0.1 release.
2019-07-14 22:22:06 +02:00
Franz Liedke
7f1048352d Clean up database query
- Use existing `selectRaw()` method to avoid using the global `app()`
  helper as a service locator, which hides dependencies.
- Do the same for the join.
- The `Expression` is necessary to prevent the aliased column from being
  prefixed with the database table prefix, if configured.
2019-07-11 22:35:19 +02:00
dependabot[bot]
d2700961ba Bump lodash-es from 4.17.11 to 4.17.14 in /js (#1818)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-11 09:21:17 +02:00
Daniël Klabbers
b2dbb0439c fixed class property hint for event post content attribute 2019-07-09 08:31:48 +02:00
Daniël Klabbers
085c924a07 fix return type hint for event post content attribute 2019-07-09 08:30:10 +02:00
David Sevilla Martín
f31f02d4cc Set Whoops middleware HTTP status to error code (#1648)
* Use error code for HTTP status, defaults to 500
* Use logic from HandleErrorsWithView, make sure status is valid
2019-07-07 14:57:40 +02:00
Daniël Klabbers
797f6eea50 moved GetDisplayName event to User namespace (#1768) 2019-07-06 19:27:44 +02:00
Franz Liedke
9fb3a31b51 Get rid of unnecessary method 2019-07-06 02:25:06 +02:00
Franz Liedke
f8061bbca1 Remove deprecated bootstrap.php fallback
Closes #1557.
2019-07-06 02:11:09 +02:00
Franz Liedke
de67927ef2 Travis: Allow PHP 7.4 to fail for now
We are currently experiencing issues due to Mockery not yet being 100%
compatible with PHP.

See https://github.com/mockery/mockery/pull/980.
2019-07-06 01:56:54 +02:00
Franz Liedke
8c841c3266 Update test libraries 2019-07-06 01:49:55 +02:00
Franz Liedke
2f656146a7 Travis: Test against the upcoming PHP 7.4 2019-07-06 01:30:59 +02:00
Franz Liedke
d66d2aa26e Convert more helpers in tests 2019-07-06 01:30:59 +02:00
Franz Liedke
f4c0d4ba87 Type hint contract, not implementation 2019-07-06 01:30:58 +02:00
Franz Liedke
646bd40bca Use Laravel's class-based Str and Arr helpers
Starting with version 5.9, the global funtions will be deprecated.

* https://laravel-news.com/laravel-5-8-deprecates-string-and-array-helpers
* https://github.com/laravel/framework/pull/26898
2019-07-06 01:30:58 +02:00
Franz Liedke
307b912019 Issue templates: Remove vulnerability information
GitHub now automatically displays this information (or rather, links to
the Security Policy) at the issue type selection page.
2019-07-06 00:08:55 +02:00
Franz Liedke
cbc896eba7 Use class constant instead of strings 2019-07-06 00:03:25 +02:00
Franz Liedke
cc4e4a068b Add descriptions to custom Composer scripts 2019-07-05 23:34:23 +02:00
David Sevilla Martín
a720f6f651 Update Application version string to beta 9 (#1784) 2019-07-05 12:37:02 +02:00
flarum-bot
54d7c0d3b6 Bundled output for commit b5876d9f31 [skip ci] 2019-06-27 19:23:54 +00:00
David Sevilla Martín
b5876d9f31 Merge pull request #1803 from flarum/ds/1777-previous-route-default
Visit home page if previous route does not exist when going back in history
2019-06-27 15:17:41 -04:00
David Sevilla Martín
25ef4c10bd Update CHANGELOG.md 2019-06-27 15:07:53 -04:00
David Sevilla Martín
985b87da6c Visit home page if no previous route exists
Fixes #1777
2019-06-27 14:58:05 -04:00
Daniël Klabbers
a6aa28566c added changelog item for mediumText fix in posts.content 2019-06-24 14:57:13 +02:00
Daniël Klabbers
e3340ba3e1 Merge branch 'master' of github.com:flarum/core 2019-06-24 14:55:05 +02:00
Daniël Klabbers
590b311570 fixes #1801, increasing the size of posts.content to mediumText correctly 2019-06-24 14:53:56 +02:00
Daniël Klabbers
935a968257 fixed tests on master, missing views directory and suppressing notices from tempnam when storing files in tmp 2019-06-24 13:00:36 +02:00
Daniël Klabbers
fe558eb0ba Merge branch 'master' into advisory-fix-1 2019-06-24 12:53:37 +02:00
Daniël Klabbers
fda9cba4ce Merge branch 'master' of github.com:flarum/core 2019-06-24 10:49:39 +02:00
Daniël Klabbers
89f6cfd949 removed link to home, go back, which is always the case with csrf token invalidation 2019-06-24 10:49:31 +02:00
Daniël Klabbers
803582c437 Apply fixes from StyleCI (#1800)
[ci skip] [skip ci]
2019-06-24 09:15:15 +02:00
Franz Liedke
8e86d38804 Merge pull request from GHSA-3wjh-93gr-chh6
* Integration tests: Memoize request handler as well

This is useful to send HTTP requests (or their PSR-7 equivalents)
through the entire application's middleware stack (instead of
talking to specific controllers, which should be considered
implementation detail).

* Add tests for CSRF token check

* Integration tests: Configure vendor path

Now that this is possible, make the easy change...

* Implement middleware for CSRF token verification

This fixes a rather large oversight in Flarum's codebase, which was that
we had no explicit CSRF protection using the traditional token approach.

The JS frontend was actually sending these tokens, but the backend did
not require them.

* Accept CSRF token in request body as well

* Refactor tests to shorten HTTP requests

Multiple tests now provide JSON request bodies, and others copy cookies
from previous responses, so let's provide convenient helpers for these.

* Fixed issue with tmp/storage/views not existing, this caused tmpname to notice.
Fixed csrf test that assumed an access token allows application access, which is actually api token.
Improved return type hinting in the StartSession middleware

* Using a different setting key now, so that it won't break tests whenever you re-run them once smtp is set.
Fixed, badly, the test to create users etc caused by the prepareDatabase flushing all settings by default.

* added custom view, now needs translation
2019-06-24 09:14:38 +02:00
Daniël Klabbers
fd66722945 added custom view, now needs translation 2019-06-22 19:40:20 +02:00
Daniël Klabbers
ce42b5e035 Using a different setting key now, so that it won't break tests whenever you re-run them once smtp is set.
Fixed, badly, the test to create users etc caused by the prepareDatabase flushing all settings by default.
2019-06-18 17:45:29 +02:00
Daniël Klabbers
bfd3a667dd Fixed issue with tmp/storage/views not existing, this caused tmpname to notice.
Fixed csrf test that assumed an access token allows application access, which is actually api token.
Improved return type hinting in the StartSession middleware
2019-06-18 17:22:23 +02:00
Daniël Klabbers
b669490d33 Update CHANGELOG.md
clarifying reason for change on the `like` fix
2019-06-13 09:13:31 +02:00
Franz Liedke
ba956f51ac Update changelog 2019-06-13 01:03:39 +02:00
Franz Liedke
c126b95451 Refactor tests to shorten HTTP requests
Multiple tests now provide JSON request bodies, and others copy cookies
from previous responses, so let's provide convenient helpers for these.
2019-06-13 00:13:59 +02:00
Franz Liedke
7f7484e790 Accept CSRF token in request body as well 2019-06-13 00:13:58 +02:00
Franz Liedke
5d64056e89 Implement middleware for CSRF token verification
This fixes a rather large oversight in Flarum's codebase, which was that
we had no explicit CSRF protection using the traditional token approach.

The JS frontend was actually sending these tokens, but the backend did
not require them.
2019-06-13 00:13:58 +02:00
Franz Liedke
e927254e99 Add tests for CSRF token check 2019-06-13 00:13:57 +02:00
Franz Liedke
8061bfd74a Integration tests: Configure vendor path
Now that this is possible, make the easy change...
2019-06-13 00:13:57 +02:00
Franz Liedke
4c309d2ad7 Integration tests: Memoize request handler as well
This is useful to send HTTP requests (or their PSR-7 equivalents)
through the entire application's middleware stack (instead of
talking to specific controllers, which should be considered
implementation detail).
2019-06-13 00:13:57 +02:00
Franz Liedke
54876cfbd6 Integration tests: Fix test setup 2019-06-13 00:13:38 +02:00
Franz Liedke
9e2b796a7c Fix syntax error 2019-06-13 00:11:57 +02:00
Franz Liedke
7f5bd1e96b Apply fixes from StyleCI (#1793)
[ci skip] [skip ci]
2019-06-12 23:50:21 +02:00
Franz Liedke
5e1680c458 Introduce a vendor path
This lets us or anyone modify the path from where dependencies (usually
installed into /vendor by Composer) are loaded. We need to be able to
tweak this in our integration tests, where the application code under
test needs access to certain dependencies.
2019-06-12 23:48:22 +02:00
Franz Liedke
6e26b988bd Inject app, not container, to avoid global helpers 2019-06-12 23:48:22 +02:00
Daniël Klabbers
2e8d4e4b6b Update CHANGELOG.md
added fix for js compiler tmp path fix to changelog
2019-06-12 17:18:21 +02:00
Daniël Klabbers
14bede2847 Merge branch 'master' of github.com:flarum/core 2019-06-12 16:47:15 +02:00
Daniël Klabbers
54660ebd63 fixed issue with the Js compiler being unable to use the system tmp directory, using the one in storage is much safer across different operating systems 2019-06-12 16:46:53 +02:00
Daniël Klabbers
1a62b7e07a Update CHANGELOG.md
fixed missing link markdown
2019-06-12 00:43:57 +02:00
Daniël Klabbers
4b04c0e0ce Update CHANGELOG.md
added missing changelog item for #1738
2019-06-12 00:43:09 +02:00
Daniël Klabbers
4d45ce389b Update CHANGELOG.md
referenced incorrect (parent) commit in changelog
2019-06-12 00:38:54 +02:00
Daniël Klabbers
d2674fb309 patched constraint for components/font-awesome, fixes #1790 2019-06-11 20:22:35 +02:00
Annim Banerjee
5eb69e1f59 Updated names to match components in fontawsome (#1791)
fa-* named components are not present, hence updated to matching names.
2019-06-11 20:17:59 +02:00
Franz Liedke
f42142979d Load LESS variables via path traversal
Since these files are part of the same package, there is no need
to assume a Composer context to load these from. Instead, we can
just load them via the path relative to the current PHP file.

This assumption may break in certain environments, and it is
already broken when running (integration) tests.
2019-06-09 00:19:06 +02:00
Franz Liedke
5f79d3b499 This method should be private 2019-06-09 00:19:05 +02:00
Franz Liedke
8e4d97260f Do not rely on extensions_enabled being present
This mostly simplifies setup in complex integration tests.
2019-06-09 00:19:05 +02:00
Daniël Klabbers
ee3640e160 remove use of like which might cause unwanted side effects (#1787) 2019-06-03 12:04:17 +02:00
Franz Liedke
bd584802e5 Update changelog 2019-06-01 20:12:30 +02:00
flarum-bot
f4dd045326 Bundled output for commit 24522943f6 [skip ci] 2019-06-01 18:10:13 +00:00
Franz Liedke
24522943f6 Update insecure jQuery version
Thanks, GitHub security alerts!
2019-06-01 20:03:07 +02:00
Franz Liedke
56fde28e43 Restore "originalUri" request attribute
This is helpful when Flarum is installed in subfolders.

Fixes #778.
2019-06-01 12:51:05 +02:00
Franz Liedke
1c1d661bdd Use the settings repository's default value
Updates commit bf2c5a5564.
2019-05-24 20:11:34 +02:00
Franz Liedke
d3be186fb6 Update changelog 2019-05-24 20:11:31 +02:00
Daniël Klabbers
8f8cc558be Update SECURITY.md
fixed typo
2019-05-23 11:15:55 +02:00
Franz Liedke
5ea9e1cf5e Add a security policy 2019-05-23 11:10:53 +02:00
Toby Zerner
99a6066f96 Merge pull request #1779 from clarkwinkelmann/fix-userpage-card-dropdown
Fix dropdown icon not showing in UserCard when on UserPage
2019-05-02 19:21:39 +09:30
Toby Zerner
8b7db726dc Merge pull request #1780 from clarkwinkelmann/remove-notification-id
Remove notification id from serializer attributes
2019-05-02 19:20:42 +09:30
Clark Winkelmann
7a44086bf3 Remove notification id from serializer attributes 2019-05-01 23:05:25 +02:00
Clark Winkelmann
12fdfc9b54 Fix dropdown icon not showing in UserCard when on UserPage
The rule hiding the icon in the UserHero was too broad and applied to UserCard in the list of posts as well
The float rule was redundant
2019-05-01 22:54:13 +02:00
Clark Winkelmann
ecc3b5e227 Remove post id from serializer attributes (#1775) 2019-04-19 21:37:14 +02:00
Daniël Klabbers
bf2c5a5564 This small fix prevents that the forum frontend breaks whenever
custom_less is NULL or unavailable in the database. We cannot rely
on this value to exist or is incorrectly set to null and thus
completely bricking the app.
2019-04-12 14:10:20 +02:00
Toby Zerner
d3a5c91845 Update changelog 2019-03-24 12:26:02 +10:30
Toby Zerner
e17bb0b433 Fix is:unread gambit 2019-03-24 12:24:44 +10:30
flarum-bot
c4ba41f850 Bundled output for commit 0c4de6f163 [skip ci] 2019-03-20 21:09:11 +00:00
Franz Liedke
0c4de6f163 Fix storing dynamic mail settings
Refs #1169.
2019-03-20 22:02:06 +01:00
flarum-bot
cd313952c7 Bundled output for commit 5154d7e5a6 [skip ci] 2019-03-19 09:06:21 +00:00
Franz Liedke
ef57b443c1 Apply fixes from StyleCI (#1761)
[ci skip] [skip ci]
2019-03-19 09:59:09 +01:00
Franz Liedke
5154d7e5a6 Allow configuring all drivers via frontend (#1169)
This includes an API endpoint for fetching the list of possible
drivers and their configuration fields. In the future, this can
be extended to include more meta information about each field.
2019-03-19 09:56:20 +01:00
Franz Liedke
2bd40b50c7 Remove dead code
Probably a leftover from copy-pasting the BasicsPage.
2019-03-17 19:02:46 +01:00
Franz Liedke
c50d58d0f4 Add drivers for Mailgun, Mandrill, SES (#1169) 2019-03-16 12:58:35 +01:00
Franz Liedke
8c65316961 Rely on default contribution guidelines
See https://help.github.com/en/articles/creating-a-default-community-health-file-for-your-organization.
2019-03-16 12:32:29 +01:00
flarum-bot
0a818cfdf3 Bundled output for commit a21052c903 [skip ci] 2019-03-15 17:01:09 +00:00
Franz Liedke
57204c6ed0 Fix last commit 2019-03-15 17:57:11 +01:00
Franz Liedke
a21052c903 Mail settings: Only show necessary fields (#1169) 2019-03-15 17:54:14 +01:00
Franz Liedke
441ebacfd7 Apply fixes from StyleCI (#1760)
[ci skip] [skip ci]
2019-03-13 21:32:18 +01:00
Franz Liedke
46acfb6c23 Implement mail driver classes (#1169)
This adds an interface for mail drivers to implement, defining several
methods that we need throughout Flarum to configure, validate and use
the various email drivers we can support through Laravel.

More mail drivers can be added by `extend()`ing the container binding
"mail.supported_drivers" with an arbitrary key and the name of a class
that implements our new `DriverInterface`.

This will ensure that drivers added by extensions can be properly built
and validated, even in the frontend.
2019-03-13 21:31:19 +01:00
Daniël Klabbers
9910e884fc Allow fallback to check for bound mail drivers (#1757) 2019-03-12 19:45:42 +01:00
Franz Liedke
d292aaabf8 Fix another documentation link
Forgotten in #1699, closes #1736.
2019-03-07 00:33:25 +01:00
Franz Liedke
d822a6f84c Apply fixes from StyleCI (#1756)
[ci skip] [skip ci]
2019-03-07 00:22:15 +01:00
Franz Liedke
26c3bcdb74 Add regression test for #1738
This should ensure we can always search for search terms that appear
either only in the subject or only in the text of discussions.
2019-03-07 00:21:43 +01:00
bdumaspilhou
33deea4791 Fixes #1738 : Search Title within discussions (#1741) 2019-03-07 00:20:37 +01:00
flarum-bot
20227a2201 Bundled output for commit 0493682dba [skip ci] 2019-03-03 19:45:38 +00:00
Franz Liedke
0493682dba Travis: Fix build job 2019-03-03 20:39:30 +01:00
Franz Liedke
49dda87e86 npm audit fix 2019-03-03 20:29:50 +01:00
Franz Liedke
d959d08561 NPM: Update bootstrap package
The old version had a vulnerability.

See https://nvd.nist.gov/vuln/detail/CVE-2019-8331.
2019-03-03 20:27:14 +01:00
Franz Liedke
e8ab49abc1 Merge pull request #1743 from flarum/fl/test-structure
Improve test suite structure
2019-03-03 20:17:35 +01:00
flarum-bot
296677b5fc Bundled output for commit f3931b537c [skip ci] 2019-02-18 07:51:00 +00:00
Daniël Klabbers
f3931b537c Copied over logic from EditTagModal to allow additional attributes to be send
to the API based on additional fields rendered by extending the fields.
2019-02-18 08:43:47 +01:00
Franz Liedke
d0ba4e5268 Update changelog 2019-02-14 23:40:18 +01:00
Daniël Klabbers
654ab4cc29 prefixes indices when installing too 2019-02-05 09:50:15 +01:00
Daniël Klabbers
e0becd0c7b Capsule manager (#1744)
Refactored to use the Capsule Database manager for setting up the
Flarum (mysql) connection.

This will introduce the reconnector automatically, fixing #1740
2019-02-04 23:31:12 +01:00
Franz Liedke
ed43ad9c3f Properly wrap error bag in session
Second part of fixing #1683.
2019-02-03 21:16:43 +01:00
Franz Liedke
4611abe5db Fix error redirect when resetting passwords
This was an oversight from the large database column renamings.

Fixes #1683.
2019-02-03 21:06:47 +01:00
Franz Liedke
df0bd52283 Add helpful (?) output to test setup script 2019-02-03 20:39:33 +01:00
Franz Liedke
d387a9ff02 travis: Configure setup for integration tests 2019-02-03 20:39:33 +01:00
Franz Liedke
5556df54f9 Setup Composer commands for testing and setup 2019-02-03 20:39:33 +01:00
Franz Liedke
cf746079ed Make integration tests independent
This creates a dedicated test suite for integration tests. All of them
can be run independently, and there is no order dependency - previously,
all integration tests needed the installer test to run first, and they
would fail if installation failed.

Now, the developer will have to set up a Flarum database to be used by
these tests. A setup script to make this simple will be added in the
next commit.

Small tradeoff: the installer is NOT tested in our test suite anymore,
only implicitly through the setup script. If we decide that this is a
problem, we can still set up separate, dedicated installer tests which
should probably test the web installer.
2019-02-03 20:39:32 +01:00
Franz Liedke
4d10536d35 Move integration tests to separate directory
Again, we do all of this to prepare for creating "real" test suites for
each type of tests.
2019-02-01 19:01:12 +01:00
Franz Liedke
ba16ebe61f Extract pure unit tests so that they can run fast
- Move to separate directory (base for a separate test suite)
- Inherit directly from PhpUnit
- Configure test suite with dedicated XML file
2019-02-01 19:01:09 +01:00
Franz Liedke
6484dc4982 Merge pull request #1617 from flarum/fl/installer-cleanup
Split up the installer logic
2019-02-01 17:43:59 +01:00
Franz Liedke
1a9f1f7a3d Use Collection class rather than collect() helper 2019-02-01 14:12:29 +01:00
Franz Liedke
4d1411e2a8 Improve problem description for wrong PHP version 2019-02-01 13:00:25 +01:00
Franz Liedke
968152b740 DatabaseConfig: Implement Arrayable contract 2019-02-01 13:00:07 +01:00
Franz Liedke
af185fd3d1 Fix tests 2019-02-01 10:33:21 +01:00
Franz Liedke
ed9591c16f Installer: Support reverting asset publication 2019-01-31 22:43:07 +01:00
Franz Liedke
8ad326941f Migrator: Fix resetting core migrations 2019-01-31 22:42:35 +01:00
Franz Liedke
8e4f02d994 Fix table name in migration 2019-01-31 22:01:05 +01:00
Franz Liedke
8ae85bc49f Remove obsolete dropForeign() migration
Forgotten in commit 5a04635e7a.
2019-01-31 22:00:41 +01:00
Franz Liedke
7ff9a90204 Check MariaDB version, update MySQL constraint
See flarum/docs#43.
2019-01-31 21:52:10 +01:00
Franz Liedke
f4fb1ab272 Simplify DataProviderInterface
Instead of passing all these objects / arrays from one object to the
next, simply pass an Installation instance around for configuration.
2019-01-31 21:52:10 +01:00
Franz Liedke
484c6d2edb Extract DatabaseConfig class with validation 2019-01-31 21:52:09 +01:00
Franz Liedke
8b68ff6232 Extract AdminUser class that enforces invariants 2019-01-31 21:52:09 +01:00
Franz Liedke
0a59b7164e Move password confirmation validation to frontends
Since this is not strictly speaking a domain invariant, but rather
specific to the user interface where passwords are not displayed, and
should therefore be entered twice to prevent mistakes going unnoticed,
this stuff should be checked in the frontend, not in the install steps.

Next step: Ensure that all domain-specific validation is done in the
installer's domain layer. This will ensure these validations cannot be
forgotten, and keep the frontends DRY.
2019-01-31 21:52:08 +01:00
Franz Liedke
0879829dc4 Use dedicated temporary variable instead of array 2019-01-31 21:52:08 +01:00
Franz Liedke
78ba3bd854 Combine building and storing config in one step 2019-01-31 21:52:08 +01:00
Franz Liedke
44c91099cd Get rid of DefaultsDataProvider
Since we do not provide a development VM anymore, it does not make sense
to have "default" credentials etc.

To reproduce something similar, I'd suggest using a YAML or JSON file
together with the `--file` option.
2019-01-31 21:52:07 +01:00
Franz Liedke
4585f03ee3 Switch to a whitelist for enabling extensions 2019-01-31 21:52:07 +01:00
Franz Liedke
bc9e8f68f1 Move default settings to install step
The various installation "frontends" (such as GUI and console) can now
provide custom overrides, if they want to.
2019-01-31 21:52:06 +01:00
Franz Liedke
f5a21584c2 Collapse namespace imports 2019-01-31 21:52:06 +01:00
Franz Liedke
e0a508a765 Catch pipeline's own exception 2019-01-31 21:52:06 +01:00
Franz Liedke
89e018a4f0 Simplify PrerequisiteInterface
I went with a return type of Collection, because it is easier to call
methods such as isEmpty() directly on those objects.
2019-01-31 21:52:05 +01:00
Franz Liedke
de6001f4cf Fix the test setup and installer tests
We are still testing the installation logic, but not testing the
actual CLI task. I would love to do that, but IMO we first need to
find a way to do this fully from the outside, by invoking and
talking to the installer through the shell.

Because acceptance tests are easier to do when fully decoupled from
the application. (After all, they are intended to save us from
breaking things when changing code; and we cannot prove that when
we change the tests at the same time.)

It might be easier to start with acceptance tests for the web
installer, though.
2019-01-31 21:52:05 +01:00
Franz Liedke
790d5beee5 Split up the installer logic
This is probably the most complicated way I could find to fix #1587.

Jokes aside, this was done with a few goals in mind:
- Reduce coupling between the installer and the rest of Flarum's
  "Application", which we are building during installation.
- Move the installer logic to several smaller classes, which can then
  be used by the web frontend and the console task, instead of the
  former hacking its way into the latter to be "DRY".
- Separate installer infrastructure (the "pipeline", with the ability
  to revert steps upon failure) from the actual steps being taken.

The problem was conceptual, and would certainly re-occur in a similar
fashion if we wouldn't tackle it at its roots.

It is fixed now, because we no longer use the ExtensionManager for
enabling extensions, but instead duplicate some of its logic. That is
fine because we don't want to do everything it does, e.g. omit
extenders' lifecycle hooks (which depend on the Application instance
being complete).

> for each desired change, make the change easy (warning: this may be
> hard), then make the easy change

- Kent Beck, https://twitter.com/kentbeck/status/250733358307500032

Fixes #1587.
2019-01-31 21:52:04 +01:00
flarum-bot
abf224bb0a Bundled output for commit c7d2e165d7 [skip ci] 2019-01-25 04:46:37 +00:00
Daniël Klabbers
c7d2e165d7 Fixes #1686
- further cleaned up the toggle action
- there's no way to remove the redraws because then the jquery isn't being fired properly
2019-01-25 05:37:45 +01:00
Toby Zerner
0ab9facc4b Make the Request available to the Formatter\Rendering event (#1721)
This is important because extensions may wish to render post content
differently depending on Request factors such as the actor. For example,
an attachments extension might wish to hide attachments from guests.

This solution is a bit of a hack-job for now, but soon when we refactor
the API layer to use tobscure/json-api-server, and also refactor the
Formatter layer, it can be revised.
2019-01-22 23:33:49 +01:00
Daniël Klabbers
9b68bbe44e Merge branch 'master' of github.com:flarum/core 2019-01-16 11:59:04 +01:00
Daniël Klabbers
862404f052 update symfony/console, because illuminate/console needs a higher version 2019-01-16 11:58:42 +01:00
flarum-bot
b9a93f3440 Bundled output for commit c67fb2d4b6 [skip ci] 2019-01-16 09:05:46 +00:00
Daniël Klabbers
c67fb2d4b6 fixes #1686, unable to edit user password 2019-01-16 09:58:22 +01:00
Daniël Klabbers
1b2d4f1e1d set prefixing indices to be done automatically, now that illuminate can take care of that 2019-01-15 20:49:33 +01:00
Daniël Klabbers
54fdc40d87 further revert #96e2824 2019-01-15 20:49:06 +01:00
Daniël Klabbers
390148456c reverts #96e2824 2019-01-15 20:39:38 +01:00
Daniël Klabbers
167059027e Increasing test coverage (#1711)
* added a few more tests, renamed singular to plural to match controller

* increase error reporting

* removed debugging and wait for tests
2019-01-01 21:02:18 +01:00
Franz Liedke
208bad393f Mail: Add an array of supported drivers
This can be used for e.g. validation, or a dropdown in the frontend.
It can also be extended by extensions, such as flagrow/mail-drivers.

Refs #1169.
2018-12-20 13:36:08 +01:00
Franz Liedke
8a93f8b6b6 Apply fixes from StyleCI (#1714)
[ci skip] [skip ci]
2018-12-20 13:13:58 +01:00
Franz Liedke
9db04a4e19 Register service providers alphabetically
Order should not matter - and this is the only one that can
realistically stay consistent.
2018-12-20 13:13:04 +01:00
Franz Liedke
ac5e26a254 Use a custom service provider for email configuration 2018-12-20 13:10:30 +01:00
Daniël Klabbers
9794a08f39 updated constraint for 5.7 (#1698) 2018-12-20 08:20:52 +10:30
Franz Liedke
ababb8ebef Don't resolve services when binding listeners
Refs #1578.
2018-12-19 22:47:58 +01:00
Franz Liedke
cb3baf9955 Apply fixes from StyleCI (#1713)
[ci skip] [skip ci]
2018-12-19 22:42:54 +01:00
Franz Liedke
dbe8cba14e Avoid unnecessary event subscribers
Refs #1578.
2018-12-19 22:27:32 +01:00
Franz Liedke
9fe671c9bb Fix UpdateServiceProvider
- Shorten registration of routes
- Do not resolve view factory before booting
2018-12-19 22:17:44 +01:00
Franz Liedke
0e5f334a0b Locale: Don't resolve manager just to configure it
Refs #1578.
2018-12-19 22:07:31 +01:00
Franz Liedke
e4514d8413 Shorten registration of routes 2018-12-19 21:57:59 +01:00
Franz Liedke
1080d25561 Frontends: Populate default routes only when they are resolved 2018-12-19 21:55:58 +01:00
Franz Liedke
ba594de13a Make site extenders run after extensions
Fixes #1708.
2018-12-19 21:30:29 +01:00
Daniël Klabbers
209d13affd add 7.3 to travis (#1710) 2018-12-19 18:09:36 +01:00
Daniel Klabbers
671fdec8d0 fixes #1695, post comment count is incorrectly calculated based on all posts, including events 2018-12-19 15:07:32 +01:00
Daniel Klabbers
9eca9192ca fixes a notice due to the forum variable not being defined before compacting 2018-12-19 11:40:48 +01:00
Franz Liedke
3468bdf511 Run local extenders before booting service providers
We still need to discuss the priority of local extenders vs. those
from enabled extensions, but let's first fix the actual bug.

Refs #1708.
2018-12-18 11:16:57 +01:00
Franz Liedke
54503d2c29 API: Populate default routes only when they are resolved
Refs #1708.
2018-12-18 10:33:01 +01:00
Franz Liedke
565131e2a7 Allow passing strings (names of invokable classes) to Formatter extender
In preparation for fixing #1703.
2018-12-15 12:05:17 +01:00
Toby Zerner
f0da3cf304 Remove obsolete binding 2018-12-14 11:28:11 +10:30
Franz Liedke
6acc91577d Apply fixes from StyleCI (#1701)
[ci skip] [skip ci]
2018-12-14 01:48:19 +01:00
Franz Liedke
3e0cd3a21f Use class constant to get qualified class names 2018-12-14 01:47:54 +01:00
Franz Liedke
5c9fa4c62d Get rid of docblocks that don't add information 2018-12-13 23:08:49 +01:00
Franz Liedke
4b00f7996b Early returns 2018-12-13 23:06:59 +01:00
Kirill
b58380e224 Fix incorrect docs link (#1699) 2018-12-13 20:19:13 +01:00
Franz Liedke
b0e996e7ff Merge pull request #1697 from flarum/fl/1578-speed-up-extenders
Do not resolve services in extenders
2018-12-13 10:33:00 +01:00
Franz Liedke
b41d9fb0e7 Inject dependencies when firing events, not before
The event subscriber approach means that dependencies have to be
injected (and thus instantiated, along with all *their* dependencies) at
the time of registering event listeners - even when events are never
fired within a request's lifecycle.

This is unnecessary and causes more classes than necessary to be loaded.

In this case, we can explicitly register event listeners that will
resolve their dependencies when the event is fired, not before.

Refs #1578.
2018-12-13 02:01:50 +01:00
Franz Liedke
ed02eed88f Do not resolve services when extending them
Refs #1578.
2018-12-13 01:58:54 +01:00
David Sevilla Martín
c761802900 Fix DELETE /api/extensions/* returning 500 (#1580)
* Use extension string as parameter for ::disable & ::uninstall

* Remove repeated 'ExtensionManager::disable' call

* Fix StyleCI
2018-12-13 00:16:03 +01:00
Arda Çebi
16eb1fa63b Profile group badge overlapping fix (#1506) 2018-12-12 22:24:30 +01:00
Franz Liedke
0ceb8d64df Update changelog 2018-12-10 22:52:50 +01:00
Franz Liedke
9712eccb03 Add an issue template for security vulnerabilities 2018-12-10 22:43:03 +01:00
David Sevilla Martín
9684fbc4da Add 'hasPermission' helper to Group (#1688)
* Add Group@hasPermission helper

* Improve performance of method
2018-12-10 22:32:21 +01:00
Franz Liedke
67f9375d47 Fix incorrect column name for registration token
Oversight from the database renamings, I suppose.

Fixes #1691.
2018-12-09 23:17:04 +01:00
Toby Zerner
0d16fac001 Performance: Actually make use of the translator cache
We had added a `storage/locale` directory to our skeleton, but we had
forgotten to hook it up with the translator. Enabling caching saves
parsing that locale YAML files on every pageload which should be good
for performance.

The locale cache will be cleared whenever an extension that uses the
`Locales` or `LanguagePack` extenders is enabled/disabled. If debug
mode is ON, then the caching mechanism will automatically check if any
of the loaded YAML files are dirty and update accordingly.
2018-12-07 09:38:08 +10:30
Toby Zerner
a8f5ca8d97 Add another commit 2018-12-07 09:13:44 +10:30
904 changed files with 25090 additions and 15360 deletions

Binary file not shown.

2
.gitattributes vendored
View File

@@ -11,3 +11,5 @@ phpunit.xml export-ignore
tests export-ignore
js/dist/* -diff
* text=auto eol=lf

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

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

View File

@@ -3,9 +3,6 @@ name: "🐛 Bug Report"
about: "If something isn't working as expected"
---
<!--
IMPORTANT: If you discover a security vulnerability within Flarum, please send an email to [security@flarum.org](mailto:security@flarum.org) instead. We will address these with the utmost urgency and it will prevent vulnerabilities, which may be abused, from popping up on our issue tracker.
-->
## Bug Report
**Current Behavior**

View File

@@ -16,7 +16,7 @@ IMPORTANT: We applaud pull requests, they excite us every single time. As we hav
**Confirmed**
- [ ] Frontend changes: tested on a local Flarum installation.
- [ ] Backend changes: tests are green (run `php vendor/bin/phpunit`).
- [ ] Backend changes: tests are green (run `composer test`).
**Required changes:**

13
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
During the beta phase, we will only patch security vulnerabilities in the latest beta release.
## Reporting a Vulnerability
If you discover a security vulnerability within Flarum, please send an email to security@flarum.org so we can address it promptly.
We will get back to you as time allows.
Discussions may commence internally, so you may not hear back immediately.
When reporting a vulnerability, please provide your GitHub username (if available), so that we can invite you to collaborate on a [security advisory on GitHub](https://help.github.com/en/articles/about-maintainer-security-advisories).

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.

16
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: JavaScript
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: flarum/action-build@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

@@ -0,0 +1,31 @@
name: Lint
on:
push:
paths:
- 'js/src/**'
pull_request:
paths:
- 'js/src/**'
jobs:
prettier:
runs-on: ubuntu-latest
name: JS / Prettier
steps:
- uses: actions/checkout@master
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: "12"
- name: Install JS dependencies
run: npm ci
working-directory: ./js
- name: Check JS code for formatting
run: node_modules/.bin/prettier --check src
working-directory: ./js

67
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: [7.2, 7.3, 7.4]
service: ['mysql:5.7', mariadb]
prefix: ['', flarum_]
include:
- service: 'mysql:5.7'
db: MySQL
- service: mariadb
db: MariaDB
- prefix: flarum_
prefixStr: (prefix)
exclude:
- php: 7.2
service: 'mysql:5.7'
prefix: flarum_
- php: 7.2
service: mariadb
prefix: flarum_
- php: 7.3
service: 'mysql:5.7'
prefix: flarum_
- php: 7.3
service: mariadb
prefix: flarum_
services:
mysql:
image: ${{ matrix.service }}
ports:
- 13306:3306
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
steps:
- uses: actions/checkout@master
- name: Select PHP version
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
- name: Create MySQL Database
run: |
sudo systemctl start mysql
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
- name: Install Composer dependencies
run: composer install
- name: Setup Composer tests
run: composer test:setup
env:
DB_PORT: 13306
DB_PASSWORD: root
DB_PREFIX: ${{ matrix.prefix }}
- name: Run Composer tests
run: composer test

2
.gitignore vendored
View File

@@ -4,6 +4,6 @@ composer.phar
node_modules
.DS_Store
Thumbs.db
/tests/tmp
/tests/integration/tmp
.vagrant
.idea/*

View File

@@ -1,46 +0,0 @@
language: php
cache:
directories:
- $HOME/.composer/cache
- $HOME/.npm
install:
- composer install
- mysql -e 'CREATE DATABASE flarum;'
script:
- vendor/bin/phpunit --coverage-clover=coverage.xml
after_success:
- bash <(curl -s https://codecov.io/bash)
jobs:
include:
- php: 7.1
env: DB=mysql
- php: 7.2
env: DB=mysql
- php: 7.2
env: DB=mysql PREFIX=forum_
- php: 7.1
addons:
mariadb: '10.2'
env: DB=mariadb
- php: 7.2
addons:
mariadb: '10.2'
env: DB=mariadb
- stage: build
language: generic
if: branch = master AND type = push
install: skip
script: bash .travis/build.sh
-k $encrypted_678139e2bc67_key
-i $encrypted_678139e2bc67_iv
after_success: skip

View File

@@ -1,33 +0,0 @@
#!/bin/bash
main() {
while getopts ":k:i:" opt; do
case $opt in
k) encrypted_key="$OPTARG"
;;
i) encrypted_iv="$OPTARG"
;;
\?) echo "Invalid option -$OPTARG" >&2
;;
esac
done
git checkout -f $TRAVIS_BRANCH
git config user.name "flarum-bot"
git config user.email "bot@flarum.org"
cd js
npm i -g npm@6.1.0
npm ci
npm run build
git add dist/* -f
git commit -m "Bundled output for commit $TRAVIS_COMMIT [skip ci]"
eval `ssh-agent -s`
openssl aes-256-cbc -K $encrypted_key -iv $encrypted_iv -in ../.deploy.enc -d | ssh-add -
git push git@github.com:$TRAVIS_REPO_SLUG.git $TRAVIS_BRANCH
}
main "$@"

View File

@@ -1,12 +1,302 @@
# Changelog
## [0.1.0-beta.14.1](https://github.com/flarum/core/compare/v0.1.0-beta.14...v0.1.0-beta.14.1)
### Fixed
- SuperTextarea component is not exported.
- Symfony dependencies do not match those depended on by Laravel (#2407)
- Scripts from textformatter aren't executed (#2415)
- Sub path installations have no page title.
- Losing focus of Composer area when coming from fullscreen.
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
### Added
- Check dependencies before enabling / disabling extensions (https://github.com/flarum/core/pull/2188)
- Set up temporary infrastructure for TypeScript in core (https://github.com/flarum/core/pull/2206)
- Better UI for request error modals (https://github.com/flarum/core/pull/1929)
- Display name extender, tests, frontend UI (https://github.com/flarum/core/pull/2174)
- Scroll to post or show alert when editing a post from another page (https://github.com/flarum/core/pull/2108)
- Feature to test email config by sending an email to the current user (https://github.com/flarum/core/pull/2023)
- Allow searching users by group ID using the group gambit (https://github.com/flarum/core/pull/2192)
- Use `liveHumanTimes` helper to update times without reload/rerender (https://github.com/flarum/core/pull/2208)
- View extender, tests (https://github.com/flarum/core/pull/2134)
- User extender to replace `PrepareUserGroups` (https://github.com/flarum/core/pull/2110)
- Increase extensibility of skeleton PHP (https://github.com/flarum/core/pull/2308, https://github.com/flarum/core/pull/2318)
- Pass a translator instance to `getEmailSubject` in `MailableInterface` (https://github.com/flarum/core/pull/2244)
- Force LF line endings on windows (https://github.com/flarum/core/pull/2321)
- Add a `Link` component for internal and external links (https://github.com/flarum/core/pull/2315)
- `ConfirmDocumentUnload` component
- Error handler middleware can now be manipulated by the middleware extender
### Changed
- Update to Mithril 2 (https://github.com/flarum/core/pull/2255)
- Stop storing component instances (https://github.com/flarum/core/issues/1821, https://github.com/flarum/core/issues/2144)
- Update to Laravel 6.x (https://github.com/flarum/core/issues/2055)
- `Flarum\Foundation\Application` no longer implements `Illuminate\Contracts\Foundation\Application` (#2142)
- `Flarum\Foundation\Application` no longer inherits `Illuminate\Container\Container` (#2142)
- `paths` have been split off from `Flarum\Foundation\Application` into `Flarum\Foundation\Paths`, which can be injected where needed (#2142)
- `Flarum\User\Gate` no longer implements `Illuminate\Contracts\Auth\Access\Gate` (https://github.com/flarum/core/pull/2181)
- Improve Group Gambit performance (https://github.com/flarum/core/pull/2192)
- Switch to `dayjs` from `momentjs` (https://github.com/flarum/core/pull/2219)
- Don't create a `bio` column in `users` for new installations (https://github.com/flarum/core/pull/2215)
- Start converting core JS to TypeScript (https://github.com/flarum/core/pull/2207)
- Make Carbon an explicit dependency (https://github.com/flarum/core/commit/3b39c212e0fef7522e7d541a9214ff3817138d5d)
- Use Symfony's translator interface instead of Laravel's (https://github.com/flarum/core/pull/2243)
- Use newer versions of fontawesome (https://github.com/flarum/core/pull/2274)
- Use URL generator instead of `app()->url()` where possible (https://github.com/flarum/core/pull/2302)
- Move config from `config.php` into an injectable helper class (https://github.com/flarum/core/pull/2271)
- Use reserved TLD for bogus and test urls (https://github.com/flarum/core/commit/6860b24b70bd04544dde90e537ce021a5fc5a689)
- Replace `m.stream` with `flarum/utils/Stream` (https://github.com/flarum/core/pull/2316)
- Replace `affixedSidebar` util with `AffixedSidebar` component
- Replace `m.withAttr` with `flarum/utils/withAttr`
- Scroll Listener is now passive, performance improvement (https://github.com/flarum/core/pull/2387)
### Fixed
- `generate:migration` command for extensions (https://github.com/flarum/core/commit/443949f7b9d7558dbc1e0994cb898cbac59bec87)
- Container config for `UninstalledSite` (https://github.com/flarum/core/commit/ecdce44d555dd36a365fd472b2916e677ef173cf)
- Tooltip glitch on page chang (https://github.com/flarum/core/issues/2118)
- Using multiple extenders in tests (https://github.com/flarum/core/commit/c4f4f218bf4b175a30880b807f9ccb1a37a25330)
- Header glitch when opening modals (https://github.com/flarum/core/pull/2131)
- Ensure `SameSite` is explicitly set for cookies (https://github.com/flarum/core/pull/2159)
- Ensure `Flarum\User\Event\AvatarChanged` event is properly dispatched (https://github.com/flarum/core/pull/2197)
- Show correct error message on wrong password when changing email (https://github.com/flarum/core/pull/2171)
- Discussion unreadCount could be higher than commentCount if posts deleted (https://github.com/flarum/core/pull/2195)
- Don't show page title on the default route (https://github.com/flarum/core/pull/2047)
- Add page title to `All Discussions` page when it isn't the default route (https://github.com/flarum/core/pull/2047)
- Accept `'0'` as `false` for `flarum/components/Checkbox` (https://github.com/flarum/core/pull/2210)
- Fix PostStreamScrubber background (https://github.com/flarum/core/pull/2222)
- Test port on BaseUrl tests (https://github.com/flarum/core/pull/2226)
- `UrlGenerator` can now generate urls with optional parameters (https://github.com/flarum/core/pull/2246)
- Allow `less` to be compiled independently of Flarum (https://github.com/flarum/core/pull/2252)
- Use correct number abbreviation (https://github.com/flarum/core/pull/2261)
- Ensure avatar html uses alt tags for accessibility (https://github.com/flarum/core/pull/2269)
- Escape regex when searching (https://github.com/flarum/core/pull/2273)
- Remove unneeded semicolons inserted during JS compilation (https://github.com/flarum/core/pull/2280)
- Don't require a username/password for SMTP (https://github.com/flarum/core/pull/2287)
- Allow uppercase entries for SMTP encryption validation (https://github.com/flarum/core/pull/2289)
- Ensure that the right number of posts is returned from list posts API (https://github.com/flarum/core/pull/2291)
- Fix a variety of PostStream bugs (https://github.com/flarum/core/pull/2160, https://github.com/flarum/core/pull/2160)
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2324)
- Sliding discussion button in wrong place (https://github.com/flarum/core/pull/2330, https://github.com/flarum/core/pull/2383)
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2381)
- Fix PostStream for posts with top margins, and scrubber position when scrolling below posts (https://github.com/flarum/core/pull/2369)
### Removed
- `Flarum\Event\AbstractConfigureRoutes` event class
- `Flarum\Event\ConfigureApiRoutes` event class
- `Flarum\Event\ConfigureForumRoutes` event class
- `Flarum\Console\Event\Configuring` event class
- `Flarum\Event\ConfigureModelDates` event class
- `Flarum\Event\ConfigureLocales` event class
- `Flarum\Event\ConfigureModelDefaultAttributes` event class
- `Flarum\Event\GetModelRelationship` event class
- `Flarum\User\Event\BioChanged` event class
- `Flarum\Database\MigrationServiceProvider` moved into `Flarum\Database\DatabaseServiceProvider`
- Unused `admin/components/Widget` component (`admin/component/DashboardWidget` should be used instead)
- Mandrill mail driver (https://github.com/flarum/core/commit/bca833d3f1c34d45d95bf905902368a2753b8908)
### Deprecated
- `Flarum\User\Event\GetDisplayName` event class
- Global path helpers, `Flarum\Foundation\Application` path methods (https://github.com/flarum/core/pull/2155)
- `Flarum\User\AssertPermissionTrait` (https://github.com/flarum/core/pull/2044)
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
### Added
- Console extender (#2057)
- CSRF extender (#2095)
- Event extender (#2097)
- Mail extender (#2012)
- Model extender (#2100)
- Posts by users that started a discussion now have the CSS class `.Post--by-start-user`
- PHPUnit 8 compatibility
- Composer 2 compatibility
- Permission groups can now be hidden (#2129)
- Confirmation popup when hiding or deleting posts (#2135)
### Changed
- Updated less.php dependency version to 3.0
- Updated JS dependencies
- All notifications and other emails now processed through the queue, if enabled (#978, #1928, #1931, #2096)
- Simplified uploads, removing need to store intermediate files (#2117)
- Improved date handling for dates older than 1 year (#2034)
- Linting and automatic formatting for JS (#2099)
- Translation files from Language Packs are only loaded for extensions that are enabled (#2020)
- PHP extenders' properties are now `private` instead of `protected`, intentionally making it harder to extend these classes (#1958)
- Preparation for upgrading Laravel components to 5.8 and then 6.0 (#2055, #2117)
- Allowed permission checks based on model classes in addition to instances (#1977)
### Fixed
- Users can no longer restore discussions hidden by admins (#2037)
- Issues of the Modal not showing or auto hiding (#1504, #1813, #2080)
- Columnar layout on admin extensions page was broken in Firefox (#2029, #2111)
- Non-dismissible modals could still be dismissed using the ESC key (#1917)
- New discussions were added to the discussion list above unread sticky posts (#1751, #1868)
- New discussions not visible to users when using Pusher (#2076, #2077)
- Permission icons were aligned unevenly in admin permissions list (#2016, #2018)
- Notification bubble not inversed on mobile with colored header (#1983, #2109)
- Post stream scrubber clicks jumped back to first post (#1945)
- Loading state of Switch toggle component was hard to see (#2039, #1491)
- `Flarum\Extend\Middleware`: The methods `insertBefore()` and `insertAfter()` did not work as described (#2063, #2084)
### Removed
- Support for PHP 7.1 (#2014)
- Zend compatibility bridge (#2010)
- SES mail support (#2011)
- Backward compatibility layer for `Flarum\Mail\DriverInterface`, new methods from beta.12 are now required
- `Flarum\Util\Str` helper class
- `Flarum\Event\ConfigureMiddleware` event
### Deprecated
- `Flarum\Event\AbstractConfigureRoutes` event class
- `Flarum\Event\ConfigureApiRoutes` event class
- `Flarum\Event\ConfigureForumRoutes` event class
- `Flarum\Event\ConfigureLocales` event class
## [0.1.0-beta.12](https://github.com/flarum/core/compare/v0.1.0-beta.11.1...v0.1.0-beta.12)
### Added
- Full support for PHP 7.4 (#1980)
- Mail settings: Configure region for the Mailgun driver (#1834, #1850)
- Mail settings: Alert admins about incomplete settings (#1763, #1921)
- New permission that allows users to post without throttling (#1255, #1938)
- Basic transliteration of discussion "slugs" / pretty URLs (#194, #1975)
- User profiles: Render basic content on server side (#1901)
- New extender for configuring middleware (#1919, #1952, #1957, #1971)
- New extender for configuring error handling (#1781, #1970)
- Automated tests for PHP extenders to guarantee their backwards compatibility
### Changed
- Profile URLs for non-existing users properly return HTTP 404 (#1846, #1901)
- Confirmation email subject no longer contains the forum title (#1613)
- Improved error handling during Flarum's early boot phase (#1607)
- Updated deprecated "Zend" libraries to their new "Laminas" equivalents (#1963)
### Fixed
- Update page did not work when installed in subdirectories (#1947)
- Avatar upload did not work in IE11 / Edge (#1125, #1570)
- Translation fallback was ignored for client-rendered pages (#1774, #1961)
- The success alert when posting replies was invisible (#1976)
## [0.1.0-beta.11.1](https://github.com/flarum/core/compare/v0.1.0-beta.11...v0.1.0-beta.11.1)
### Fixed
- Saving custom css in admin failed (#1946)
## [0.1.0-beta.11](https://github.com/flarum/core/compare/v0.1.0-beta.10...v0.1.0-beta.11)
### Added
- Comments have an additional class `Post--by-actor` when posted by the user (#1927)
### Changed
- Improved support for URL identification during installation (#1861)
- KeyboardNavigatable now has a callback ability (#1922)
- Links are no longer opened with target `_blank` but in the same window (#859)
- Links now have `nofollow ugc` by default as their `rel` attribute (#859, #1884)
- Improved performance of the full text gambit when searching for users (#1877)
- The Queue implementation is now available under its Illuminate contract
### Fixed
- No error handling was possible in the console/cli (#1789)
- Enable scrollbars in log in modals so it fits for GitHub (#1716)
- Reduce log in modal for SSO so it fits for Facebook (#1727)
- Deleting discussions permanently did not delete its posts (#1909)
- Fixed the queue:restart command (#1932)
- Deleted posts were visible to all visitors (#1827)
- Old avatars weren't being deleted when replaced (#1918)
- The search performance regression was reverted (#1764)
- No profile background could be set for remote images (#445)
- Back button sends to home even though it could actually go back (#1942)
- Debug button no longer visible (#1687)
- Modals on smaller screens use the whole width of the page
## [0.1.0-beta.10](https://github.com/flarum/core/compare/v0.1.0-beta.9...v0.1.0-beta.10)
### Added
- Initial queue support: Infrastructure for offloading long-running tasks (e.g. email sending) to background workers (#1773)
- Notifications can now be marked as read without visiting a discussion (#151)
- SEO: The discussion list now has a `rel="canonical"` meta tag, preventing duplicate content (#1134, #1814)
- The "Edit User" permission can now be edited in the UI (#1845)
- New status message and redirect after user deletion (#1750, #1777)
- Errors in Flarum's boot process are now presented with more detailed information (#1607)
### Changed
- Better, more detailed and extensible error handling (#1641, #1843)
- Error pages in debug mode now return the same HTTP status codes as in production (#1648)
- Tweak HTTP status codes for authentication / authorization errors (#1854)
- Already-used links from account activation emails now show a better error message (#1337)
### Fixed
- Security vulnerabilities in dependencies
- Performance: High CPU usage when scrolling in a discussion (#1222)
- Special characters crashed the search (#1498)
- Missing declarations for language and text direction in HTML output (#1772)
- Private messages were counted in user post counts (#1695)
- Extensions could not change the forum's default page (#1819)
- API requests authenticated using access tokens needed to provide a CSRF token (#1828)
- Accessibility: Screenreaders did not read the "Back to discussion list" link (#1835)
## [0.1.0-beta.9](https://github.com/flarum/core/compare/v0.1.0-beta.8.2...v0.1.0-beta.9)
### Added
- New `hasPermission()` helper method for `Group` objects ([9684fbc](https://github.com/flarum/core/commit/9684fbc4da07d32aa322d9228302a23418412cb9))
- Expose supported mail drivers in IoC container ([208bad3](https://github.com/flarum/core/commit/208bad393f37bfdb76007afcddfa4b7451563e9d))
- More test for some API endpoints ([1670590](https://github.com/flarum/core/commit/167059027e5a066d618599c90164ef1b5a509148))
- The `Formatter\Rendering` event now receives the HTTP request instance as well ([0ab9fac](https://github.com/flarum/core/commit/0ab9facc4bd59a260575e6fc650793c663e5866a))
- More and better validation in installer UIs
- Check and enforce minimum MariaDB ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
- Revert publication of assets when installation fails ([ed9591c](https://github.com/flarum/core/commit/ed9591c16fb2ea7a4be3387b805d855a53e0a7d5))
- Benefit from Laravel's database reconnection logic in long-running tasks ([e0becd0](https://github.com/flarum/core/commit/e0becd0c7bda939048923c1f86648793feee78d5))
- The "vendor path" (where Composer dependencies can be found) can now be configured ([5e1680c](https://github.com/flarum/core/commit/5e1680c458cd3ba274faeb92de3ac2053789131e))
### Changed
- Performance: Actually cache translations on disk ([0d16fac](https://github.com/flarum/core/commit/0d16fac001bb735ee66e82871183516aeac269b7))
- Allow per-site extenders to override extension extenders ([ba594de](https://github.com/flarum/core/commit/ba594de13a033480834d53d73f747b05fe9796f8))
- Do not resolve objects from the IoC container (in service providers and extenders) until they are actually used
- Replace event subscribers (that resolve objects from the IoC container) with listeners (that resolve lazily)
- Use custom service provider for Mail component ([ac5e26a](https://github.com/flarum/core/commit/ac5e26a254d89e21bd4c115b6cbd40338e2e4b4b))
- Update to Laravel 5.7, revert custom logic for building database index names
- Refactored installer, extracted Installation class and pipeline for reuse in CLI and web installers ([790d5be](https://github.com/flarum/core/commit/790d5beee5e283178716bc8f9901c758d9e5b6a0))
- Use whitelist for enabling pre-installed extensions during installation ([4585f03](https://github.com/flarum/core/commit/4585f03ee356c92942fbc2ae8c683c651b473954))
- Update minimum MySQL version ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
### Fixed
- Signing up via OAuth providers was broken ([67f9375](https://github.com/flarum/core/commit/67f9375d4745add194ae3249d526197c32fd5461))
- Group badges were overlapping ([16eb1fa](https://github.com/flarum/core/commit/16eb1fa63b6d7b80ec30c24c0e406a2b7ab09934))
- API: Endpoint for uninstalling extensions returned an error ([c761802](https://github.com/flarum/core/commit/c76180290056ddbab67baf5ede814fcedf1dcf14))
- Documentation links in installer were outdated ([b58380e](https://github.com/flarum/core/commit/b58380e224ee54abdade3d0a4cc107ef5c91c9a9))
- Event posts where counted when aggregating user posts ([671fdec](https://github.com/flarum/core/commit/671fdec8d0a092ccceb5d4d5f657d0f4287fc4c7))
- Admins could not reset user passwords ([c67fb2d](https://github.com/flarum/core/commit/c67fb2d4b6a128c71d65dc6703310c0b62f91be2))
- Several down migrations were invalid
- Validation errors on reset password page resulted in HTTP 404 ([4611abe](https://github.com/flarum/core/commit/4611abe5db8b94ca3dc7bf9c447fca7c67358ee3))
- `is:unread` gambit generated an invalid query ([e17bb0b](https://github.com/flarum/core/commit/e17bb0b4331f2c92459292195c6b7db8cde1f9f3))
- Entire forum was breaking when the `custom_less` setting was missing from the database ([bf2c5a5](https://github.com/flarum/core/commit/bf2c5a5564dff3f5ef13efe7a8d69f2617570ce6))
- Dropdown icon was not showing in user card when on user page ([12fdfc9](https://github.com/flarum/core/commit/12fdfc9b544a27f6fe59c82ad6bddd3420cc0181))
- Requests were missing the `original*` attributes, which broke installations in subfolders ([56fde28](https://github.com/flarum/core/commit/56fde28e436f52fee0c03c538f0a6049bc584b53))
- Special characters such as `%` and `_` could return incorrect results ([ee3640e](https://github.com/flarum/core/commit/ee3640e1605ff67fef4b3d5cd0596f14a6ae73c9))
- FontAwesome component package changed paths in version 5.9.0 ([5eb69e1](https://github.com/flarum/core/commit/5eb69e1f59fa73fdfd5badbf41a05a6a040e7426))
- Some server environments had problems accessing the system-wide tmp path for storing JS file maps ([54660eb](https://github.com/flarum/core/commit/54660ebd6311f9ea142f1b573263d0d907400786))
- Content length of posts.content was not migrated to mediumText in 2017 ([590b311](https://github.com/flarum/core/commit/590b3115708bf94a9c7f169d98c6126380c7056e))
- An error occurred when going to the previous route if there was no previous route found ([985b87da](https://github.com/flarum/core/commit/985b87da6c9942c568a1a192e2fdcfde72e030ee))
### Removed
- `php flarum install --defaults` - this was meant to be used in our old development VM ([44c9109](https://github.com/flarum/core/commit/44c91099cd77138bb5fc29f14fb1e81a9781272d))
- Obsolete `id` attributes in JSON-API responses ([ecc3b5e](https://github.com/flarum/core/commit/ecc3b5e2271f8d9b38d52cd54476d86995dbe32e) and [7a44086](https://github.com/flarum/core/commit/7a44086bf3a0e3ba907dceb13d07ac695eca05ea))
## [0.1.0-beta.8.1](https://github.com/flarum/core/compare/v0.1.0-beta.8...v0.1.0-beta.8.1)
### Fixed
- Fix live output in `migrate:reset` command ([f591585](https://github.com/flarum/core/commit/f591585d02f8c4ff0211c5bf4413dd6baa724c05))
- Fix search with database prefix ([7705a2b](https://github.com/flarum/core/commit/7705a2b7d751943ef9d0c7379ec34f8530b99310))
- Fix invalid join time of admin user created by installer ([57f73c9](https://github.com/flarum/core/commit/57f73c9638eeb825f9e336ed3c443afccfd8995e))
- Ensure InnoDB engine is used for all tables ([fb6b51b](https://github.com/flarum/core/commit/fb6b51b1cfef0af399607fe038603c8240800b2b))
- Ensure InnoDB engine is used for all tables ([fb6b51b](https://github.com/flarum/core/commit/fb6b51b1cfef0af399607fe038603c8240800b2b), [6370f7e](https://github.com/flarum/core/commit/6370f7ecffa9ea7d5fb64d9551400edbc63318db))
- Fix dropping foreign keys in `down` migrations ([57d5846](https://github.com/flarum/core/commit/57d5846b647881009d9e60f9ffca20b1bb77776e))
- Fix discussion list scroll position not being maintained when hero is not visible ([40dc6ac](https://github.com/flarum/core/commit/40dc6ac604c2a0973356b38217aa8d09352daae5))
- Fix empty meta description tag ([88e43cc](https://github.com/flarum/core/commit/88e43cc6940ee30d6529e9ce659471ec4fb1c474))

View File

@@ -1,3 +0,0 @@
# Contributing to Flarum
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://flarum.org/docs/contributing.html)** to learn how you can help.

View File

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

@@ -1,12 +1,14 @@
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
<p align="center"><img src="https://flarum.org/assets/img/logo.png"></p>
<p align="center">
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
<a href="https://github.com/flarum/core/actions?query=workflow%3ATests"><img src="https://github.com/flarum/core/workflows/Tests/badge.svg" alt="PHP Tests"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/dt/flarum/core" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/github/v/release/flarum/core?sort=semver" alt="Latest Version"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/l/flarum/core" alt="License"></a>
<a href="https://github.styleci.io/repos/28257573"><img src="https://github.styleci.io/repos/28257573/shield?style=flat" alt="StyleCI"></a>
</p>
## About Flarum
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
@@ -27,9 +29,8 @@ 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
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).

View File

@@ -5,13 +5,32 @@
"homepage": "https://flarum.org/",
"license": "MIT",
"authors": [
{
"name": "Toby Zerner",
"email": "toby.zerner@gmail.com"
},
{
"name": "Franz Liedke",
"email": "franz@develophp.org"
},
{
"name": "Daniël Klabbers",
"email": "daniel@klabbers.email",
"homepage": "https://luceos.com"
},
{
"name": "David Sevilla Martin",
"email": "me+flarum@datitisev.me",
"homepage": "https://datitisev.me"
},
{
"name": "Clark Winkelmann",
"email": "clark.winkelmann@gmail.com",
"homepage": "https://clarkwinkelmann.com"
},
{
"name": "Matthew Kilgore",
"email": "matthew@kilgore.dev"
},
{
"name": "Alexander (Sasha) Skvortsov",
"email": "askvortsov@flarum.org"
}
],
"support": {
@@ -20,52 +39,54 @@
"docs": "https://flarum.org/docs/"
},
"require": {
"php": ">=7.1",
"php": ">=7.2",
"axy/sourcemap": "^0.1.4",
"components/font-awesome": "^5.4.2",
"dflydev/fig-cookies": "^1.0.2",
"components/font-awesome": "^5.14.0",
"dflydev/fig-cookies": "^2.0.1",
"doctrine/dbal": "^2.7",
"franzl/whoops-middleware": "^0.4.0",
"illuminate/bus": "5.5.*",
"illuminate/cache": "5.5.*",
"illuminate/config": "5.5.*",
"illuminate/container": "5.5.*",
"illuminate/contracts": "5.5.*",
"illuminate/database": "5.5.*",
"illuminate/events": "5.5.*",
"illuminate/filesystem": "5.5.*",
"illuminate/hashing": "5.5.*",
"illuminate/mail": "5.5.*",
"illuminate/session": "5.5.*",
"illuminate/support": "5.5.*",
"illuminate/validation": "5.5.*",
"illuminate/view": "5.5.*",
"intervention/image": "^2.3.0",
"illuminate/bus": "^6.0",
"illuminate/cache": "^6.0",
"illuminate/config": "^6.0",
"illuminate/container": "^6.0",
"illuminate/contracts": "^6.0",
"illuminate/database": "^6.0",
"illuminate/events": "^6.0",
"illuminate/filesystem": "^6.0",
"illuminate/hashing": "^6.0",
"illuminate/mail": "^6.0",
"illuminate/queue": "^6.0",
"illuminate/session": "^6.0",
"illuminate/support": "^6.0",
"illuminate/validation": "^6.0",
"illuminate/view": "^6.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",
"middlewares/base-path-router": "^0.2.1",
"middlewares/request-handler": "^1.2",
"monolog/monolog": "^1.16.0",
"nesbot/carbon": "^2.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",
"symfony/config": "^3.3",
"symfony/console": "^3.3",
"symfony/http-foundation": "^3.3",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"s9e/text-formatter": "^2.3.6",
"symfony/config": "^4.3.4",
"symfony/console": "^4.3.4",
"symfony/event-dispatcher": "^4.3.4",
"symfony/translation": "^4.3.4",
"symfony/yaml": "^4.3.4",
"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": "^0.9.4",
"phpunit/phpunit": "^6.0"
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0"
},
"autoload": {
"psr-4": {
@@ -87,5 +108,20 @@
"branch-alias": {
"dev-master": "0.1.x-dev"
}
},
"scripts": {
"test": [
"@test:unit",
"@test:integration"
],
"test:unit": "phpunit -c tests/phpunit.unit.xml",
"test:integration": "phpunit -c tests/phpunit.integration.xml",
"test:setup": "@php tests/integration/setup.php"
},
"scripts-descriptions": {
"test": "Runs all tests.",
"test:unit": "Runs all unit tests.",
"test:integration": "Runs all integration tests.",
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
}
}

6
js/.prettierrc.json Normal file
View File

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

30
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

32
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3028
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,25 +2,38 @@
"private": true,
"name": "@flarum/core",
"dependencies": {
"bootstrap": "^3.3.7",
"@babel/preset-typescript": "^7.10.1",
"@types/mithril": "^2.0.3",
"bootstrap": "^3.4.1",
"classnames": "^2.2.5",
"color-thief-browser": "^2.0.2",
"dayjs": "^1.8.28",
"expose-loader": "^0.7.5",
"flarum-webpack-config": "0.1.0-beta.10",
"jquery": "^3.3.1",
"jquery": "^3.5.1",
"jquery.hotkeys": "^0.1.0",
"lodash-es": "^4.17.11",
"lodash-es": "^4.17.14",
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
"mithril": "^0.2.8",
"moment": "^2.22.2",
"mithril": "^2.0.4",
"punycode": "^2.1.1",
"spin.js": "^3.1.0",
"webpack": "^4.26.0",
"webpack-cli": "^3.1.2",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"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"
}
}
}

32
js/shims.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
// Mithril
import Mithril from 'mithril';
// Other third-party libs
import * as _dayjs from 'dayjs';
import * as _$ from 'jquery';
// Globals from flarum/core
import Application from './src/common/Application';
/**
* flarum/core exposes several extensions globally:
*
* - jQuery for convenient DOM manipulation
* - Mithril for VDOM and components
* - dayjs for date/time operations
*
* Since these are already part of the global namespace, extensions won't need
* to (and should not) bundle these themselves.
*/
declare global {
const $: typeof _$;
const m: Mithril.Static;
const dayjs: typeof _dayjs;
}
/**
* All global variables owned by flarum/core.
*/
declare global {
const app: Application;
}

View File

@@ -1,20 +1,36 @@
import HeaderPrimary from './components/HeaderPrimary';
import HeaderSecondary from './components/HeaderSecondary';
import routes from './routes';
import ExtensionPage from './components/ExtensionPage';
import Application from '../common/Application';
import Navigation from '../common/components/Navigation';
import AdminNav from './components/AdminNav';
import ExtensionData from './utils/ExtensionData';
export default class AdminApplication extends Application {
// Deprecated as of beta 15
extensionSettings = {};
extensionData = new ExtensionData();
extensionCategories = {
discussion: 70,
moderation: 60,
feature: 50,
formatting: 40,
theme: 30,
authentication: 20,
language: 10,
other: 0,
};
history = {
canGoBack: () => true,
getPrevious: () => {},
backUrl: () => this.forum.attribute('baseUrl'),
back: function() {
back: function () {
window.location = this.backUrl();
}
},
};
constructor() {
@@ -27,19 +43,29 @@ 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('header-navigation'), Navigation.component());
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
m.mount(document.getElementById('admin-navigation'), AdminNav.component());
// Mithril does not render the home route on https://example.com/admin, so
// we need to go to https://example.com/admin#/ explicitly.
if (!document.location.hash) document.location.hash = '#/';
m.route.mode = 'hash';
m.route.prefix = '#';
super.mount();
m.mount(document.getElementById('app-navigation'), {
view: () =>
Navigation.component({
className: 'App-backControl',
drawer: true,
}),
});
m.mount(document.getElementById('header-navigation'), Navigation);
m.mount(document.getElementById('header-primary'), HeaderPrimary);
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
m.mount(document.getElementById('admin-navigation'), AdminNav);
// If an extension has just been enabled, then we will run its settings
// callback.
const enabled = localStorage.getItem('enabledExtension');
if (enabled && this.extensionSettings[enabled]) {
if (enabled && this.extensionSettings[enabled] && typeof this.extensionSettings[enabled] === 'function') {
this.extensionSettings[enabled]();
localStorage.removeItem('enabledExtension');
}
@@ -59,5 +85,5 @@ export default class AdminApplication extends Application {
}
return required;
};
}
}

View File

@@ -1,21 +1,23 @@
import compat from '../common/compat';
import saveSettings from './utils/saveSettings';
import ExtensionData from './utils/ExtensionData';
import isExtensionEnabled from './utils/isExtensionEnabled';
import getCategorizedExtensions from './utils/getCategorizedExtensions';
import SettingDropdown from './components/SettingDropdown';
import EditCustomFooterModal from './components/EditCustomFooterModal';
import SessionDropdown from './components/SessionDropdown';
import HeaderPrimary from './components/HeaderPrimary';
import AppearancePage from './components/AppearancePage';
import Page from './components/Page';
import StatusWidget from './components/StatusWidget';
import ExtensionsWidget from './components/ExtensionsWidget';
import HeaderSecondary from './components/HeaderSecondary';
import SettingsModal from './components/SettingsModal';
import DashboardWidget from './components/DashboardWidget';
import AddExtensionModal from './components/AddExtensionModal';
import ExtensionsPage from './components/ExtensionsPage';
import ExtensionPage from './components/ExtensionPage';
import ExtensionLinkButton from './components/ExtensionLinkButton';
import AdminLinkButton from './components/AdminLinkButton';
import PermissionGrid from './components/PermissionGrid';
import Widget from './components/Widget';
import MailPage from './components/MailPage';
import UploadImageButton from './components/UploadImageButton';
import LoadingModal from './components/LoadingModal';
@@ -25,6 +27,7 @@ import EditCustomHeaderModal from './components/EditCustomHeaderModal';
import PermissionsPage from './components/PermissionsPage';
import PermissionDropdown from './components/PermissionDropdown';
import AdminNav from './components/AdminNav';
import AdminHeader from './components/AdminHeader';
import EditCustomCssModal from './components/EditCustomCssModal';
import EditGroupModal from './components/EditGroupModal';
import routes from './routes';
@@ -32,21 +35,23 @@ import AdminApplication from './AdminApplication';
export default Object.assign(compat, {
'utils/saveSettings': saveSettings,
'utils/ExtensionData': ExtensionData,
'utils/isExtensionEnabled': isExtensionEnabled,
'utils/getCategorizedExtensions': getCategorizedExtensions,
'components/SettingDropdown': SettingDropdown,
'components/EditCustomFooterModal': EditCustomFooterModal,
'components/SessionDropdown': SessionDropdown,
'components/HeaderPrimary': HeaderPrimary,
'components/AppearancePage': AppearancePage,
'components/Page': Page,
'components/StatusWidget': StatusWidget,
'components/ExtensionsWidget': ExtensionsWidget,
'components/HeaderSecondary': HeaderSecondary,
'components/SettingsModal': SettingsModal,
'components/DashboardWidget': DashboardWidget,
'components/AddExtensionModal': AddExtensionModal,
'components/ExtensionsPage': ExtensionsPage,
'components/ExtensionPage': ExtensionPage,
'components/ExtensionLinkButton': ExtensionLinkButton,
'components/AdminLinkButton': AdminLinkButton,
'components/PermissionGrid': PermissionGrid,
'components/Widget': Widget,
'components/MailPage': MailPage,
'components/UploadImageButton': UploadImageButton,
'components/LoadingModal': LoadingModal,
@@ -56,8 +61,9 @@ export default Object.assign(compat, {
'components/PermissionsPage': PermissionsPage,
'components/PermissionDropdown': PermissionDropdown,
'components/AdminNav': AdminNav,
'components/AdminHeader': AdminHeader,
'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

@@ -0,0 +1,19 @@
import Component from '../../common/Component';
import classList from '../../common/utils/classList';
import icon from '../../common/helpers/icon';
export default class AdminHeader extends Component {
view(vnode) {
return [
<div className={classList(['AdminHeader', this.attrs.className])}>
<div className="container">
<h2>
{icon(this.attrs.icon)}
{vnode.children}
</h2>
<div className="AdminHeader-description">{this.attrs.description}</div>
</div>
</div>,
];
}
}

View File

@@ -10,15 +10,7 @@
import LinkButton from '../../common/components/LinkButton';
export default class AdminLinkButton extends LinkButton {
getButtonContent() {
const content = super.getButtonContent();
content.push(
<div className="AdminLinkButton-description">
{this.props.description}
</div>
);
return content;
getButtonContent(children) {
return [...super.getButtonContent(children), <div className="AdminLinkButton-description">{this.attrs.description}</div>];
}
}

View File

@@ -1,77 +1,119 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import ExtensionLinkButton from './ExtensionLinkButton';
import Component from '../../common/Component';
import AdminLinkButton from './AdminLinkButton';
import LinkButton from '../../common/components/LinkButton';
import SelectDropdown from '../../common/components/SelectDropdown';
import getCategorizedExtensions from '../utils/getCategorizedExtensions';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
export default class AdminNav extends Component {
oninit(vnode) {
super.oninit(vnode);
this.query = Stream('');
}
view() {
return (
<SelectDropdown
className="AdminNav App-titleControl"
buttonClassName="Button">
{this.items().toArray()}
<SelectDropdown className="AdminNav App-titleControl AdminNav-Main" buttonClassName="Button">
{this.items().toArray().concat(this.extensionItems().toArray())}
</SelectDropdown>
);
}
/**
* Build an item list of links to show in the admin navigation.
* Build an item list of main links to show in the admin navigation.
*
* @return {ItemList}
*/
items() {
const items = new ItemList();
items.add('dashboard', AdminLinkButton.component({
href: app.route('dashboard'),
icon: 'far fa-chart-bar',
children: app.translator.trans('core.admin.nav.dashboard_button'),
description: app.translator.trans('core.admin.nav.dashboard_text')
}));
items.add(
'dashboard',
<LinkButton href={app.route('dashboard')} icon="far fa-chart-bar" title={app.translator.trans('core.admin.nav.dashboard_title')}>
{app.translator.trans('core.admin.nav.dashboard_button')}
</LinkButton>
);
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',
<LinkButton href={app.route('basics')} icon="fas fa-pencil-alt" title={app.translator.trans('core.admin.nav.basics_title')}>
{app.translator.trans('core.admin.nav.basics_button')}
</LinkButton>
);
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',
<LinkButton href={app.route('mail')} icon="fas fa-envelope" title={app.translator.trans('core.admin.nav.email_title')}>
{app.translator.trans('core.admin.nav.email_button')}
</LinkButton>
);
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',
<LinkButton href={app.route('permissions')} icon="fas fa-key" title={app.translator.trans('core.admin.nav.permissions_title')}>
{app.translator.trans('core.admin.nav.permissions_button')}
</LinkButton>
);
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',
<LinkButton href={app.route('appearance')} icon="fas fa-paint-brush" title={app.translator.trans('core.admin.nav.appearance_title')}>
{app.translator.trans('core.admin.nav.appearance_button')}
</LinkButton>
);
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(
'search',
<div className="Search-input">
<input
className="FormControl SearchBar"
bidi={this.query}
type="search"
placeholder={app.translator.trans('core.admin.nav.search_placeholder')}
/>
</div>
);
return items;
}
extensionItems() {
const items = new ItemList();
const categorizedExtensions = getCategorizedExtensions();
const categories = app.extensionCategories;
Object.keys(categorizedExtensions).map((category) => {
if (!this.query()) {
items.add(
category,
<h4 className="ExtensionListTitle">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>,
categories[category]
);
}
categorizedExtensions[category].map((extension) => {
const query = this.query().toUpperCase();
const title = extension.extra['flarum-extension'].title;
if (!query || title.toUpperCase().includes(query) || extension.description.toUpperCase().includes(query)) {
items.add(
extension.id,
<ExtensionLinkButton
href={app.route('extension', { id: extension.id })}
extensionId={extension.id}
className="ExtensionNavButton"
title={extension.description}
>
{title}
</ExtensionLinkButton>,
categories[category]
);
}
});
});
return items;
}

View File

@@ -1,109 +1,118 @@
import Page from './Page';
import Page from '../../common/components/Page';
import Button from '../../common/components/Button';
import Switch from '../../common/components/Switch';
import Stream from '../../common/utils/Stream';
import EditCustomCssModal from './EditCustomCssModal';
import EditCustomHeaderModal from './EditCustomHeaderModal';
import EditCustomFooterModal from './EditCustomFooterModal';
import UploadImageButton from './UploadImageButton';
import saveSettings from '../utils/saveSettings';
import AdminHeader from './AdminHeader';
export default class AppearancePage extends Page {
init() {
super.init();
oninit(vnode) {
super.oninit(vnode);
this.primaryColor = m.prop(app.data.settings.theme_primary_color);
this.secondaryColor = m.prop(app.data.settings.theme_secondary_color);
this.darkMode = m.prop(app.data.settings.theme_dark_mode === '1');
this.coloredHeader = m.prop(app.data.settings.theme_colored_header === '1');
this.primaryColor = Stream(app.data.settings.theme_primary_color);
this.secondaryColor = Stream(app.data.settings.theme_secondary_color);
this.darkMode = Stream(app.data.settings.theme_dark_mode);
this.coloredHeader = Stream(app.data.settings.theme_colored_header);
}
view() {
return (
<div className="AppearancePage">
<AdminHeader
icon="fas fa-paint-brush"
description={app.translator.trans('core.admin.appearance.description')}
className="AppearancePage-header"
>
{app.translator.trans('core.admin.appearance.title')}
</AdminHeader>
<div className="container">
<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" bidi={this.primaryColor} />
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.secondaryColor} />
</div>
{Switch.component({
state: this.darkMode(),
children: app.translator.trans('core.admin.appearance.dark_mode_label'),
onchange: this.darkMode
})}
{Switch.component(
{
state: this.darkMode(),
onchange: this.darkMode,
},
app.translator.trans('core.admin.appearance.dark_mode_label')
)}
{Switch.component({
state: this.coloredHeader(),
children: app.translator.trans('core.admin.appearance.colored_header_label'),
onchange: this.coloredHeader
})}
{Switch.component(
{
state: this.coloredHeader(),
onchange: this.coloredHeader,
},
app.translator.trans('core.admin.appearance.colored_header_label')
)}
{Button.component({
className: 'Button Button--primary',
type: 'submit',
children: app.translator.trans('core.admin.appearance.submit_button'),
loading: this.loading
})}
{Button.component(
{
className: 'Button Button--primary',
type: 'submit',
loading: this.loading,
},
app.translator.trans('core.admin.appearance.submit_button')
)}
</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>
{Button.component({
className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_header_button'),
onclick: () => app.modal.show(new EditCustomHeaderModal())
})}
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomHeaderModal),
},
app.translator.trans('core.admin.appearance.edit_header_button')
)}
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText">
{app.translator.trans('core.admin.appearance.custom_footer_text')}
</div>
{Button.component({
className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_footer_button'),
onclick: () => app.modal.show(new EditCustomFooterModal())
})}
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomFooterModal),
},
app.translator.trans('core.admin.appearance.edit_footer_button')
)}
</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>
{Button.component({
className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_css_button'),
onclick: () => app.modal.show(new EditCustomCssModal())
})}
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomCssModal),
},
app.translator.trans('core.admin.appearance.edit_css_button')
)}
</fieldset>
</div>
</div>
@@ -126,7 +135,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

@@ -1,15 +1,17 @@
import Page from './Page';
import Page from '../../common/components/Page';
import FieldSet from '../../common/components/FieldSet';
import Select from '../../common/components/Select';
import Button from '../../common/components/Button';
import Alert from '../../common/components/Alert';
import saveSettings from '../utils/saveSettings';
import ItemList from '../../common/utils/ItemList';
import Switch from '../../common/components/Switch';
import Stream from '../../common/utils/Stream';
import withAttr from '../../common/utils/withAttr';
import AdminHeader from './AdminHeader';
export default class BasicsPage extends Page {
init() {
super.init();
oninit(vnode) {
super.oninit(vnode);
this.loading = false;
@@ -20,12 +22,13 @@ export default class BasicsPage extends Page {
'show_language_selector',
'default_route',
'welcome_title',
'welcome_message'
'welcome_message',
'display_name_driver',
];
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] = Stream(settings[key])));
this.localeOptions = {};
const locales = app.data.locales;
@@ -33,86 +36,126 @@ 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);
this.displayNameOptions = {};
const displayNameDrivers = app.data.displayNameDrivers;
displayNameDrivers.forEach(function (identifier) {
this.displayNameOptions[identifier] = identifier;
}, this);
if (!this.values.display_name_driver() && displayNameDrivers.includes('username')) this.values.display_name_driver('username');
if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1);
}
view() {
return (
<div className="BasicsPage">
<AdminHeader icon="fas fa-pencil-alt" description={app.translator.trans('core.admin.basics.description')} className="BasicsPage-header">
{app.translator.trans('core.admin.basics.title')}
</AdminHeader>
<div className="container">
<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)}/>
]
})}
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.forum_title_heading'),
},
[<input className="FormControl" bidi={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)}/>
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.forum_description_heading'),
},
[
<div className="helpText">{app.translator.trans('core.admin.basics.forum_description_text')}</div>,
<textarea className="FormControl" bidi={this.values.forum_description} />,
]
})}
)}
{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'),
})
]
})
? FieldSet.component(
{
label: app.translator.trans('core.admin.basics.default_language_heading'),
},
[
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,
},
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>
)
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.home_page_heading'),
className: 'BasicsPage-homePage',
},
[
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>,
this.homePageItems()
.toArray()
.map(({ path, label }) => (
<label className="checkbox">
<input
type="radio"
name="homePage"
value={path}
checked={this.values.default_route() === path}
onclick={withAttr('value', this.values.default_route)}
/>
{label}
</label>
)),
]
})}
)}
{FieldSet.component({
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
className: 'BasicsPage-welcomeBanner',
children: [
<div className="helpText">
{app.translator.trans('core.admin.basics.welcome_banner_text')}
</div>,
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
className: 'BasicsPage-welcomeBanner',
},
[
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>,
<div className="BasicsPage-welcomeBanner-input">
<input className="FormControl" 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" bidi={this.values.welcome_title} />
<textarea className="FormControl" bidi={this.values.welcome_message} />
</div>,
]
})}
)}
{Button.component({
type: 'submit',
className: 'Button Button--primary',
children: app.translator.trans('core.admin.basics.submit_button'),
loading: this.loading,
disabled: !this.changed()
})}
{Object.keys(this.displayNameOptions).length > 1
? FieldSet.component(
{
label: app.translator.trans('core.admin.basics.display_name_heading'),
},
[
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>,
Select.component({
options: this.displayNameOptions,
bidi: this.values.display_name_driver,
}),
]
)
: ''}
{Button.component(
{
type: 'submit',
className: 'Button Button--primary',
loading: this.loading,
disabled: !this.changed(),
},
app.translator.trans('core.admin.basics.submit_button')
)}
</form>
</div>
</div>
@@ -120,7 +163,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 +178,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 +194,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')}));
this.successAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
})
.catch(() => {})
.then(() => {

View File

@@ -1,18 +1,29 @@
import Page from './Page';
import Page from '../../common/components/Page';
import StatusWidget from './StatusWidget';
import ExtensionsWidget from './ExtensionsWidget';
import AdminHeader from './AdminHeader';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
export default class DashboardPage extends Page {
view() {
return (
<div className="DashboardPage">
<div className="container">
{this.availableWidgets()}
</div>
<AdminHeader icon="fas fa-chart-bar" description={app.translator.trans('core.admin.dashboard.description')} className="DashboardPage-header">
{app.translator.trans('core.admin.dashboard.title')}
</AdminHeader>
<div className="container">{this.availableWidgets().toArray()}</div>
</div>
);
}
availableWidgets() {
return [<StatusWidget/>];
const items = new ItemList();
items.add('status', <StatusWidget />, 30);
items.add('extensions', <ExtensionsWidget />, 10);
return items;
}
}

View File

@@ -1,21 +1,8 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import Component from '../../common/Component';
export default class Widget extends Component {
export default class DashboardWidget extends Component {
view() {
return (
<div className={"Widget "+this.className()}>
{this.content()}
</div>
);
return <div className={'DashboardWidget 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

@@ -3,19 +3,24 @@ import Button from '../../common/components/Button';
import Badge from '../../common/components/Badge';
import Group from '../../common/models/Group';
import ItemList from '../../common/utils/ItemList';
import Switch from '../../common/components/Switch';
import Stream from '../../common/utils/Stream';
/**
* The `EditGroupModal` component shows a modal dialog which allows the user
* to create or edit a group.
*/
export default class EditGroupModal extends Modal {
init() {
this.group = this.props.group || app.store.createRecord('groups');
oninit(vnode) {
super.oninit(vnode);
this.nameSingular = m.prop(this.group.nameSingular() || '');
this.namePlural = m.prop(this.group.namePlural() || '');
this.icon = m.prop(this.group.icon() || '');
this.color = m.prop(this.group.color() || '');
this.group = this.attrs.group || app.store.createRecord('groups');
this.nameSingular = Stream(this.group.nameSingular() || '');
this.namePlural = Stream(this.group.namePlural() || '');
this.icon = Stream(this.group.icon() || '');
this.color = Stream(this.group.color() || '');
this.isHidden = Stream(this.group.isHidden() || false);
}
className() {
@@ -24,21 +29,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,55 +51,95 @@ 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')} bidi={this.nameSingular} />
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} bidi={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" bidi={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" bidi={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(
'hidden',
<div className="Form-group">
{Switch.component(
{
state: !!Number(this.isHidden()),
onchange: this.isHidden,
},
app.translator.trans('core.admin.edit_group.hide_label')
)}
</div>,
10
);
items.add(
'submit',
<div className="Form-group">
{Button.component(
{
type: 'submit',
className: 'Button Button--primary EditGroupModal-save',
loading: this.loading,
},
app.translator.trans('core.admin.edit_group.submit_button')
)}
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
{app.translator.trans('core.admin.edit_group.delete_button')}
</button>
) : (
''
)}
</div>,
-10
);
return items;
}
submitData() {
return {
nameSingular: this.nameSingular(),
namePlural: this.namePlural(),
color: this.color(),
icon: this.icon(),
isHidden: this.isHidden(),
};
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
this.group.save({
nameSingular: this.nameSingular(),
namePlural: this.namePlural(),
color: this.color(),
icon: this.icon()
}, {errorHandler: this.onerror.bind(this)})
this.group
.save(this.submitData(), { errorHandler: this.onerror.bind(this) })
.then(this.hide.bind(this))
.catch(() => {
this.loading = false;

View File

@@ -0,0 +1,29 @@
import isExtensionEnabled from '../utils/isExtensionEnabled';
import LinkButton from '../../common/components/LinkButton';
import icon from '../../common/helpers/icon';
import ItemList from '../../common/utils/ItemList';
export default class ExtensionLinkButton extends LinkButton {
getButtonContent(children) {
const content = super.getButtonContent(children);
const extension = app.data.extensions[this.attrs.extensionId];
const statuses = this.statusItems(extension.id).toArray();
content.unshift(
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
</span>
);
content.push(statuses);
return content;
}
statusItems(name) {
const items = new ItemList();
items.add('enabled', <span class={'ExtensionListItem-Dot ' + (isExtensionEnabled(name) ? 'enabled' : 'disabled')} />);
return items;
}
}

View File

@@ -0,0 +1,376 @@
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
import LinkButton from '../../common/components/LinkButton';
import Page from '../../common/components/Page';
import Select from '../../common/components/Select';
import Switch from '../../common/components/Switch';
import icon from '../../common/helpers/icon';
import punctuateSeries from '../../common/helpers/punctuateSeries';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
import LoadingModal from './LoadingModal';
import ExtensionPermissionGrid from './ExtensionPermissionGrid';
import saveSettings from '../utils/saveSettings';
import ExtensionData from '../utils/ExtensionData';
import isExtensionEnabled from '../utils/isExtensionEnabled';
export default class ExtensionPage extends Page {
oninit(vnode) {
super.oninit(vnode);
this.loading = false;
this.extension = app.data.extensions[this.attrs.id];
this.changingState = false;
this.settings = {};
this.infoFields = {
discuss: 'fas fa-comment-alt',
documentation: 'fas fa-book',
support: 'fas fa-life-ring',
website: 'fas fa-link',
donate: 'fas fa-donate',
};
// Backwards compatibility layer will be removed in
// Beta 16
if (app.extensionSettings[this.extension.id]) {
app.extensionData[this.extension.id] = app.extensionSettings[this.extension.id];
}
}
className() {
return this.extension.id + '-Page';
}
view() {
return (
<div className={'ExtensionPage ' + this.className()}>
{this.header()}
{!this.isEnabled() ? (
<div className="container">
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h2>
</div>
) : (
<div className="ExtensionPage-body">{this.sections().toArray()}</div>
)}
</div>
);
}
header() {
return [
<div className="ExtensionPage-header">
<div className="container">
<div className="ExtensionTitle">
<span className="ExtensionIcon" style={this.extension.icon}>
{this.extension.icon ? icon(this.extension.icon.name) : ''}
</span>
<div className="ExtensionName">
<h2>{this.extension.extra['flarum-extension'].title}</h2>
</div>
<div className="ExtensionPage-headerTopItems">
<ul>{listItems(this.topItems().toArray())}</ul>
</div>
</div>
<div className="helpText">{this.extension.description}</div>
<div className="ExtensionPage-headerItems">
<Switch state={this.isEnabled()} onchange={this.toggle.bind(this, this.extension.id)}>
{this.isEnabled(this.extension.id)
? app.translator.trans('core.admin.extension.enabled')
: app.translator.trans('core.admin.extension.disabled')}
</Switch>
<aside className="ExtensionInfo">
<ul>{listItems(this.infoItems().toArray())}</ul>
</aside>
</div>
</div>
</div>,
];
}
sections() {
const items = new ItemList();
items.add('content', this.content());
items.add('permissions', [
<div className="ExtensionPage-permissions">
<div className="ExtensionPage-permissions-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('core.admin.extension.permissions_title')}</h2>
</div>
</div>
<div className="container">
{app.extensionData.extensionHasPermissions(this.extension.id) ? (
ExtensionPermissionGrid.component({ extensionId: this.extension.id })
) : (
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h2>
)}
</div>
</div>,
]);
return items;
}
content() {
const settings = app.extensionData.getSettings(this.extension.id);
return (
<div className="ExtensionPage-settings">
<div className="container">
{typeof app.extensionData[this.extension.id] === 'function' ? (
<Button onclick={app.extensionData[this.extension.id].bind(this)} className="Button Button--primary">
{app.translator.trans('core.admin.extension.open_modal')}
</Button>
) : settings ? (
<div className="Form">
{settings.map(this.buildSettingComponent.bind(this))}
<div className="Form-group">{this.submitButton()}</div>
</div>
) : (
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h2>
)}
</div>
</div>
);
}
topItems() {
const items = new ItemList();
items.add('version', <span className="ExtensionVersion">{this.extension.version}</span>);
if (!this.isEnabled()) {
const uninstall = () => {
if (confirm(app.translator.trans('core.admin.extension.confirm_uninstall'))) {
app
.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
method: 'DELETE',
})
.then(() => window.location.reload());
app.modal.show(LoadingModal);
}
};
items.add(
'uninstall',
<Button icon="fas fa-trash-alt" className="Button Button--primary" onclick={uninstall.bind(this)}>
{app.translator.trans('core.admin.extension.uninstall_button')}
</Button>
);
}
return items;
}
infoItems() {
const items = new ItemList();
if (this.extension.authors) {
let authors = [];
Object.keys(this.extension.authors).map((author, i) => {
const link = this.extension.authors[author].homepage
? this.extension.authors[author].homepage
: 'mailto:' + this.extension.authors[author].email;
authors.push(
<Link href={link} external={true} target="_blank">
{this.extension.authors[author].name}
</Link>
);
});
items.add('authors', [icon('fas fa-user'), <span>{punctuateSeries(authors)}</span>]);
}
const infoData = {};
if (this.extension.source || this.extension.support) {
infoData.source = {
icon: 'fas fa-code',
href: this.extension.source ? this.extension.source.url : this.extension.support.source,
};
}
Object.keys(this.infoFields).map((field) => {
const info = this.extension.extra['flarum-extension'].info;
if (info && info[field]) {
infoData[field] = {
icon: this.infoFields[field],
href: info[field],
};
}
});
Object.entries(infoData).map(([field, value]) => {
items.add(
field,
<LinkButton href={value.href} icon={value.icon} external={true} target="_blank">
{app.translator.trans(`core.admin.extension.info_links.${field}`)}
</LinkButton>
);
});
return items;
}
submitButton() {
return (
<Button onclick={this.saveSettings.bind(this)} className="Button Button--primary" loading={this.loading} disabled={!this.isChanged()}>
{app.translator.trans('core.admin.settings.submit_button')}
</Button>
);
}
/**
* getSetting takes a settings object and turns it into a component.
* Depending on the type of input, you can set the type to 'bool', 'select', or
* any standard <input> type.
*
* @example
*
* {
* setting: 'acme.checkbox',
* label: app.translator.trans('acme.admin.setting_label'),
* type: 'bool'
* }
*
* @example
*
* {
* setting: 'acme.select',
* label: app.translator.trans('acme.admin.setting_label'),
* type: 'select',
* options: {
* 'option1': 'Option 1 label',
* 'option2': 'Option 2 label',
* },
* default: 'option1',
* }
*
* @param setting
* @returns {JSX.Element}
*/
buildSettingComponent(entry) {
const setting = entry.setting;
const value = this.setting([setting])();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(entry.type)) {
return (
<div className="Form-group">
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]}>
{entry.label}
</Switch>
</div>
);
} else if (['select', 'dropdown', 'selectdropdown'].includes(entry.type)) {
return (
<div className="Form-group">
<label>{entry.label}</label>
<Select value={value || entry.default} options={entry.options} buttonClassName="Button" onchange={this.settings[setting]} />
</div>
);
} else {
return (
<div className="Form-group">
<label>{entry.label}</label>
<input type={entry.type} className="FormControl" bidi={this.setting(setting)} />
</div>
);
}
}
toggle() {
const enabled = this.isEnabled();
this.changingState = true;
app
.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
method: 'PATCH',
body: { enabled: !enabled },
errorHandler: this.onerror.bind(this),
})
.then(() => {
if (!enabled) localStorage.setItem('enabledExtension', this.extension.id);
window.location.reload();
});
app.modal.show(LoadingModal);
}
dirty() {
const dirty = {};
Object.keys(this.settings).forEach((key) => {
const value = this.settings[key]();
if (value !== app.data.settings[key]) {
dirty[key] = value;
}
});
return dirty;
}
isChanged() {
return Object.keys(this.dirty()).length;
}
saveSettings(e) {
e.preventDefault();
app.alerts.clear();
this.loading = true;
saveSettings(this.dirty()).then(this.onsaved.bind(this));
}
onsaved() {
this.loading = false;
app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.extension.saved_message'));
}
setting(key, fallback = '') {
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
return this.settings[key];
}
isEnabled() {
let isEnabled = isExtensionEnabled(this.extension.id);
return this.changingState ? !isEnabled : isEnabled;
}
onerror(e) {
// We need to give the modal animation time to start; if we close the modal too early,
// it breaks the bootstrap modal library.
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
setTimeout(() => {
app.modal.close();
}, 300); // Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
if (e.status !== 409) {
throw e;
}
const error = e.response.errors[0];
app.alerts.show(
{ type: 'error' },
app.translator.trans(`core.lib.error.${error.code}_message`, {
extension: error.extension,
extensions: error.extensions.join(', '),
})
);
}
}

View File

@@ -0,0 +1,39 @@
import PermissionGrid from './PermissionGrid';
import ItemList from '../../common/utils/ItemList';
export default class ExtensionPermissionGrid extends PermissionGrid {
oninit(vnode) {
super.oninit(vnode);
this.extensionId = this.attrs.extensionId;
}
permissionItems() {
const permissionCategories = super.permissionItems();
permissionCategories.items = Object.entries(permissionCategories.items)
.filter(([category, info]) => info.content.children.length > 0)
.reduce((obj, [category, info]) => {
obj[category] = info;
return obj;
}, {});
return permissionCategories;
}
viewItems() {
return app.extensionData.getExtensionPermissions(this.extensionId, 'view') || new ItemList();
}
startItems() {
return app.extensionData.getExtensionPermissions(this.extensionId, 'start') || new ItemList();
}
replyItems() {
return app.extensionData.getExtensionPermissions(this.extensionId, 'reply') || new ItemList();
}
moderateItems() {
return app.extensionData.getExtensionPermissions(this.extensionId, 'moderate') || new ItemList();
}
}

View File

@@ -1,117 +0,0 @@
import Page from './Page';
import LinkButton from '../../common/components/LinkButton';
import Button from '../../common/components/Button';
import Dropdown from '../../common/components/Dropdown';
import Separator from '../../common/components/Separator';
import AddExtensionModal from './AddExtensionModal';
import LoadingModal from './LoadingModal';
import ItemList from '../../common/utils/ItemList';
import icon from '../../common/helpers/icon';
import listItems from '../../common/helpers/listItems';
export default class ExtensionsPage extends Page {
view() {
return (
<div className="ExtensionsPage">
<div className="ExtensionsPage-header">
<div className="container">
{Button.component({
children: app.translator.trans('core.admin.extensions.add_button'),
icon: 'fas fa-plus',
className: 'Button Button--primary',
onclick: () => app.modal.show(new AddExtensionModal())
})}
</div>
</div>
<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();
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) : ''}
</span>
{controls.length ? (
<Dropdown
className="ExtensionListItem-controls"
buttonClassName="Button Button--icon Button--flat"
menuClassName="Dropdown-menu--right"
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}
</label>
<div className="ExtensionListItem-version">{extension.version}</div>
<div className="ExtensionListItem-description">{extension.description}</div>
</div>
</div>
</li>;
})}
</ul>
</div>
</div>
</div>
);
}
controlItems(name) {
const items = new ItemList();
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]
}));
}
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());
app.modal.show(new LoadingModal());
}
}));
}
return items;
}
isEnabled(name) {
const enabled = JSON.parse(app.data.settings.extensions_enabled);
return enabled.indexOf(name) !== -1;
}
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.modal.show(new LoadingModal());
}
}

View File

@@ -0,0 +1,48 @@
import DashboardWidget from './DashboardWidget';
import isExtensionEnabled from '../utils/isExtensionEnabled';
import getCategorizedExtensions from '../utils/getCategorizedExtensions';
import Link from '../../common/components/Link';
import icon from '../../common/helpers/icon';
export default class ExtensionsWidget extends DashboardWidget {
className() {
return 'ExtensionsWidget';
}
content() {
const categorizedExtensions = getCategorizedExtensions();
const categories = app.extensionCategories;
return (
<div className="ExtensionsWidget-list">
<div className="container">
{Object.keys(categories).map((category) => {
if (categorizedExtensions[category]) {
return (
<div className="ExtensionList-Category">
<h4 className="ExtensionList-Label">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>
<ul className="ExtensionList">
{categorizedExtensions[category].map((extension) => {
return (
<li className={'ExtensionListItem ' + (!isExtensionEnabled(extension.id) ? 'disabled' : '')}>
<Link href={app.route('extension', { id: extension.id })}>
<div className="ExtensionListItem-content">
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
</span>
<span className="ExtensionListItem-title">{extension.extra['flarum-extension'].title}</span>
</div>
</Link>
</li>
);
})}
</ul>
</div>
);
}
})}
</div>
</div>
);
}
}

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

@@ -1,4 +1,5 @@
import Component from '../../common/Component';
import LinkButton from '../../common/components/LinkButton';
import SessionDropdown from './SessionDropdown';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
@@ -8,18 +9,7 @@ import listItems from '../../common/helpers/listItems';
*/
export default class HeaderSecondary extends Component {
view() {
return (
<ul className="Header-controls">
{listItems(this.items().toArray())}
</ul>
);
}
config(isInitialized, context) {
// Since this component is 'above' the content of the page (that is, it is a
// part of the global UI that persists between routes), we will flag the DOM
// to be retained across route changes.
context.retain = true;
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
}
/**
@@ -30,6 +20,13 @@ export default class HeaderSecondary extends Component {
items() {
const items = new ItemList();
items.add(
'help',
<LinkButton href="https://docs.flarum.org/troubleshoot.html" icon="fas fa-question-circle" external={true} target="_blank">
{app.translator.trans('core.admin.header.get_help')}
</LinkButton>
);
items.add('session', SessionDropdown.component());
return items;

View File

@@ -1,9 +1,10 @@
import Modal from '../../common/components/Modal';
export default class LoadingModal extends Modal {
isDismissible() {
return false;
}
/**
* @inheritdoc
*/
static isDismissible = false;
className() {
return 'LoadingModal Modal--small';

View File

@@ -1,124 +1,230 @@
import Page from './Page';
import Page from '../../common/components/Page';
import FieldSet from '../../common/components/FieldSet';
import Button from '../../common/components/Button';
import Alert from '../../common/components/Alert';
import Select from '../../common/components/Select';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import saveSettings from '../utils/saveSettings';
import Stream from '../../common/utils/Stream';
import icon from '../../common/helpers/icon';
import AdminHeader from './AdminHeader';
export default class MailPage extends Page {
init() {
super.init();
oninit(vnode) {
super.oninit(vnode);
this.loading = false;
this.saving = false;
this.sendingTest = false;
this.refresh();
}
this.fields = [
'mail_driver',
'mail_host',
'mail_from',
'mail_port',
'mail_username',
'mail_password',
'mail_encryption'
];
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] = Stream(settings[key])));
this.localeOptions = {};
const locales = app.locales;
for (const i in locales) {
this.localeOptions[i] = `${locales[i]} (${i})`;
}
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'];
for (const driver in this.driverFields) {
for (const field in this.driverFields[driver]) {
this.fields.push(field);
this.values[field] = Stream(settings[field]);
}
}
this.loading = false;
m.redraw();
});
}
view() {
if (this.loading || this.saving) {
return (
<div className="MailPage">
<div className="container">
<LoadingIndicator />
</div>
</div>
);
}
const fields = this.driverFields[this.values.mail_driver()];
const fieldKeys = Object.keys(fields);
return (
<div className="MailPage">
<AdminHeader icon="fas fa-envelope" description={app.translator.trans('core.admin.email.description')} className="MailPage-header">
{app.translator.trans('core.admin.email.title')}
</AdminHeader>
<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>
{FieldSet.component({
label: app.translator.trans('core.admin.email.server_heading'),
className: 'MailPage-MailSettings',
children: [
{FieldSet.component(
{
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
},
[
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
<input className="FormControl" value={this.values.mail_driver() || ''} oninput={m.withAttr('value', this.values.mail_driver)} />
<label>{app.translator.trans('core.admin.email.host_label')}</label>
<input className="FormControl" value={this.values.mail_host() || ''} oninput={m.withAttr('value', this.values.mail_host)} />
<label>{app.translator.trans('core.admin.email.port_label')}</label>
<input className="FormControl" value={this.values.mail_port() || ''} oninput={m.withAttr('value', this.values.mail_port)} />
<label>{app.translator.trans('core.admin.email.encryption_label')}</label>
<input className="FormControl" value={this.values.mail_encryption() || ''} oninput={m.withAttr('value', this.values.mail_encryption)} />
</div>
<label>
{app.translator.trans('core.admin.email.from_label')}
<input className="FormControl" bidi={this.values.mail_from} />
</label>
</div>,
]
})}
)}
{FieldSet.component({
label: app.translator.trans('core.admin.email.account_heading'),
className: 'MailPage-MailSettings',
children: [
{FieldSet.component(
{
label: app.translator.trans('core.admin.email.driver_heading'),
className: 'MailPage-MailSettings',
},
[
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.username_label')}</label>
<input className="FormControl" value={this.values.mail_username() || ''} oninput={m.withAttr('value', this.values.mail_username)} />
<label>{app.translator.trans('core.admin.email.password_label')}</label>
<input className="FormControl" value={this.values.mail_password() || ''} oninput={m.withAttr('value', this.values.mail_password)} />
</div>
<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>,
]
})}
)}
{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>
{this.status.sending ||
Alert.component(
{
dismissible: false,
},
app.translator.trans('core.admin.email.not_sending_message')
)}
{fieldKeys.length > 0 &&
FieldSet.component(
{
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
className: 'MailPage-MailSettings',
},
[
<div className="MailPage-MailSettings-input">
{fieldKeys.map((field) => [
<label>
{app.translator.trans(`core.admin.email.${field}_label`)}
{this.renderField(field)}
</label>,
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
])}
</div>,
]
)}
<FieldSet>
{Button.component(
{
type: 'submit',
className: 'Button Button--primary',
disabled: !this.changed(),
},
app.translator.trans('core.admin.email.submit_button')
)}
</FieldSet>
{FieldSet.component(
{
label: app.translator.trans('core.admin.email.send_test_mail_heading'),
className: 'MailPage-MailSettings',
},
[
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user.email() })}</div>,
Button.component(
{
className: 'Button Button--primary',
disabled: this.sendingTest || this.changed(),
onclick: () => this.sendTestEmail(),
},
app.translator.trans('core.admin.email.send_test_mail_button')
),
]
})}
{Button.component({
type: 'submit',
className: 'Button Button--primary',
children: app.translator.trans('core.admin.email.submit_button'),
loading: this.loading,
disabled: !this.changed()
})}
)}
</form>
</div>
</div>
);
}
renderField(name) {
const driver = this.values.mail_driver();
const field = this.driverFields[driver][name];
const prop = this.values[name];
if (typeof field === 'string') {
return <input className="FormControl" bidi={prop} />;
} else {
return <Select value={prop()} options={field} onchange={prop} />;
}
}
changed() {
return this.fields.some(key => this.values[key]() !== app.data.settings[key]);
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
}
sendTestEmail() {
if (this.saving || this.sendingTest) return;
this.sendingTest = true;
app.alerts.dismiss(this.testEmailSuccessAlert);
app
.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/mail/test',
})
.then((response) => {
this.sendingTest = false;
this.testEmailSuccessAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.email.send_test_mail_success'));
})
.catch((error) => {
this.sendingTest = false;
m.redraw();
throw error;
});
}
onsubmit(e) {
e.preventDefault();
if (this.loading) return;
if (this.saving || this.sendingTest) return;
this.loading = true;
this.saving = true;
app.alerts.dismiss(this.successAlert);
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')}));
this.successAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
this.saving = false;
this.refresh();
});
}
}

View File

@@ -1,32 +0,0 @@
import Component from '../../common/Component';
/**
* The `Page` component
*
* @abstract
*/
export default class Page extends Component {
init() {
app.previous = app.current;
app.current = this;
app.modal.close();
/**
* A class name to apply to the body while the route is active.
*
* @type {String}
*/
this.bodyClass = '';
}
config(isInitialized, context) {
if (isInitialized) return;
if (this.bodyClass) {
$('#app').addClass(this.bodyClass);
context.onunload = () => $('#app').removeClass(this.bodyClass);
}
}
}

View File

@@ -8,126 +8,133 @@ 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;
}
export default class PermissionDropdown extends Dropdown {
static initProps(props) {
super.initProps(props);
static initAttrs(attrs) {
super.initAttrs(attrs);
props.className = 'PermissionDropdown';
props.buttonClassName = 'Button Button--text';
attrs.className = 'PermissionDropdown';
attrs.buttonClassName = 'Button Button--text';
}
view() {
this.props.children = [];
view(vnode) {
const children = [];
let groupIds = app.data.permissions[this.props.permission] || [];
let groupIds = app.data.permissions[this.attrs.permission] || [];
groupIds = filterByRequiredPermissions(groupIds, this.props.permission);
groupIds = filterByRequiredPermissions(groupIds, this.attrs.permission);
const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
if (everyone) {
this.props.label = Badge.component({icon: 'fas fa-globe'});
this.attrs.label = Badge.component({ icon: 'fas fa-globe' });
} else if (members) {
this.props.label = Badge.component({icon: 'fas fa-user'});
this.attrs.label = Badge.component({ icon: 'fas fa-user' });
} else {
this.props.label = [
badgeForId(Group.ADMINISTRATOR_ID),
groupIds.map(badgeForId)
];
this.attrs.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')],
icon: everyone ? 'fas fa-check' : true,
onclick: () => this.save([Group.GUEST_ID]),
disabled: this.isGroupDisabled(Group.GUEST_ID)
})
if (this.attrs.allowGuest) {
children.push(
Button.component(
{
icon: everyone ? 'fas fa-check' : true,
onclick: () => this.save([Group.GUEST_ID]),
disabled: this.isGroupDisabled(Group.GUEST_ID),
},
[Badge.component({ icon: 'fas fa-globe' }), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')]
)
);
}
this.props.children.push(
Button.component({
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)
}),
children.push(
Button.component(
{
icon: members ? 'fas fa-check' : true,
onclick: () => this.save([Group.MEMBER_ID]),
disabled: this.isGroupDisabled(Group.MEMBER_ID),
},
[Badge.component({ icon: 'fas fa-user' }), ' ', app.translator.trans('core.admin.permissions_controls.members_button')]
),
Separator.component(),
Button.component({
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
icon: !everyone && !members ? 'fas fa-check' : true,
disabled: !everyone && !members,
onclick: e => {
if (e.shiftKey) e.stopPropagation();
this.save([]);
}
})
Button.component(
{
icon: !everyone && !members ? 'fas fa-check' : true,
disabled: !everyone && !members,
onclick: (e) => {
if (e.shiftKey) e.stopPropagation();
this.save([]);
},
},
[badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()]
)
);
[].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)
}))
children,
app.store
.all('groups')
.filter((group) => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.map((group) =>
Button.component(
{
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),
},
[badgeForId(group.id()), ' ', group.namePlural()]
)
)
);
}
return super.view();
return super.view({ ...vnode, children });
}
save(groupIds) {
const permission = this.props.permission;
const permission = this.attrs.permission;
app.data.permissions[permission] = groupIds;
app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/permission',
data: {permission, groupIds}
body: { permission, groupIds },
});
}
toggle(groupId) {
const permission = this.props.permission;
const permission = this.attrs.permission;
let groupIds = app.data.permissions[permission] || [];
@@ -137,13 +144,13 @@ 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);
}
isGroupDisabled(id) {
return filterByRequiredPermissions([id], this.props.permission).indexOf(id) === -1;
return filterByRequiredPermissions([id], this.attrs.permission).indexOf(id) === -1;
}
}

View File

@@ -6,19 +6,11 @@ import ItemList from '../../common/utils/ItemList';
import icon from '../../common/helpers/icon';
export default class PermissionGrid extends Component {
init() {
this.permissions = this.permissionItems().toArray();
}
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,31 +18,38 @@ 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 => (
<tbody>
<tr className="PermissionGrid-section">
<th>{section.label}</th>
{permissionCells(section)}
<td/>
</tr>
{section.children.map(child => (
<tr className="PermissionGrid-child">
<th>{icon(child.icon)}{child.label}</th>
{permissionCells(child)}
<td/>
{this.permissionItems()
.toArray()
.map((section) => (
<tbody>
<tr className="PermissionGrid-section">
<th>{section.label}</th>
{permissionCells(section)}
<td />
</tr>
))}
</tbody>
))}
{section.children.map((child) => (
<tr className="PermissionGrid-child">
<th>
{icon(child.icon)}
{child.label}
</th>
{permissionCells(child)}
<td />
</tr>
))}
</tbody>
))}
</table>
);
}
@@ -58,25 +57,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 +99,54 @@ 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(
'viewHiddenGroups',
{
icon: 'fas fa-users',
label: app.translator.trans('core.admin.permissions.view_hidden_groups_label'),
permission: 'viewHiddenGroups',
},
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(
'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('viewLastSeenAt', {
icon: 'far fa-clock',
@@ -116,37 +154,49 @@ export default class PermissionGrid extends Component {
permission: 'user.viewLastSeenAt',
});
items.merge(app.extensionData.getAllExtensionPermissions('view'));
return items;
}
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
);
items.merge(app.extensionData.getAllExtensionPermissions('start'));
return items;
}
@@ -154,31 +204,41 @@ 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
);
items.merge(app.extensionData.getAllExtensionPermissions('reply'));
return items;
}
@@ -186,47 +246,97 @@ 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.merge(app.extensionData.getAllExtensionPermissions('moderate'));
return items;
}
@@ -234,21 +344,25 @@ export default class PermissionGrid extends Component {
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

@@ -1,39 +1,42 @@
import Page from './Page';
import Page from '../../common/components/Page';
import GroupBadge from '../../common/components/GroupBadge';
import EditGroupModal from './EditGroupModal';
import Group from '../../common/models/Group';
import icon from '../../common/helpers/icon';
import PermissionGrid from './PermissionGrid';
import AdminHeader from './AdminHeader';
export default class PermissionsPage extends Page {
view() {
return (
<div className="PermissionsPage">
<AdminHeader icon="fas fa-key" description={app.translator.trans('core.admin.permissions.description')} className="PermissionsPage-header">
{app.translator.trans('core.admin.permissions.title')}
</AdminHeader>
<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(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'})}
<button className="Button Group Group--add" onclick={() => app.modal.show(EditGroupModal)}>
{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

@@ -9,27 +9,22 @@ import ItemList from '../../common/utils/ItemList';
* avatar/name, with a dropdown of session controls.
*/
export default class SessionDropdown extends Dropdown {
static initProps(props) {
super.initProps(props);
static initAttrs(attrs) {
super.initAttrs(attrs);
props.className = 'SessionDropdown';
props.buttonClassName = 'Button Button--user Button--flat';
props.menuClassName = 'Dropdown-menu--right';
attrs.className = 'SessionDropdown';
attrs.buttonClassName = 'Button Button--user Button--flat';
attrs.menuClassName = 'Dropdown-menu--right';
}
view() {
this.props.children = this.items().toArray();
return super.view();
view(vnode) {
return super.view({ ...vnode, children: this.items().toArray() });
}
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,12 +35,15 @@ export default class SessionDropdown extends Dropdown {
items() {
const items = new ItemList();
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)
}),
items.add(
'logOut',
Button.component(
{
icon: 'fas fa-sign-out-alt',
onclick: app.session.logout.bind(app.session),
},
app.translator.trans('core.admin.header.log_out_button')
),
-100
);

View File

@@ -3,23 +3,30 @@ import Button from '../../common/components/Button';
import saveSettings from '../utils/saveSettings';
export default class SettingDropdown extends SelectDropdown {
static initProps(props) {
super.initProps(props);
static initAttrs(attrs) {
super.initAttrs(attrs);
props.className = 'SettingDropdown';
props.buttonClassName = 'Button Button--text';
props.caretIcon = 'fas fa-caret-down';
props.defaultLabel = 'Custom';
attrs.className = 'SettingDropdown';
attrs.buttonClassName = 'Button Button--text';
attrs.caretIcon = 'fas fa-caret-down';
attrs.defaultLabel = 'Custom';
}
props.children = props.options.map(({value, label}) => {
const active = app.data.settings[props.key] === value;
view(vnode) {
return super.view({
...vnode,
children: this.attrs.options.map(({ value, label }) => {
const active = app.data.settings[this.attrs.key] === value;
return Button.component({
children: label,
icon: active ? 'fas fa-check' : true,
onclick: saveSettings.bind(this, {[props.key]: value}),
active
});
return Button.component(
{
icon: active ? 'fas fa-check' : true,
onclick: saveSettings.bind(this, { [this.attrs.key]: value }),
active,
},
label
);
}),
});
}
}

View File

@@ -1,9 +1,12 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Stream from '../../common/utils/Stream';
import saveSettings from '../utils/saveSettings';
export default class SettingsModal extends Modal {
init() {
oninit(vnode) {
super.oninit(vnode);
this.settings = {};
this.loading = false;
}
@@ -18,9 +21,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,18 +29,14 @@ 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>
);
}
setting(key, fallback = '') {
this.settings[key] = this.settings[key] || m.prop(app.data.settings[key] || fallback);
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
return this.settings[key];
}
@@ -47,7 +44,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 +64,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,39 +20,39 @@ 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;
}
handleClearCache(e) {
app.modal.show(new LoadingModal());
app.modal.show(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

@@ -1,30 +1,28 @@
import Button from '../../common/components/Button';
export default class UploadImageButton extends Button {
init() {
this.loading = false;
}
loading = false;
view() {
this.props.loading = this.loading;
this.props.className = (this.props.className || '') + ' Button';
view(vnode) {
this.attrs.loading = this.loading;
this.attrs.className = (this.attrs.className || '') + ' Button';
if (app.data.settings[this.props.name + '_path']) {
this.props.onclick = this.remove.bind(this);
this.props.children = app.translator.trans('core.admin.upload_image.remove_button');
if (app.data.settings[this.attrs.name + '_path']) {
this.attrs.onclick = this.remove.bind(this);
return (
<div>
<p><img src={app.forum.attribute(this.props.name+'Url')} alt=""/></p>
<p>{super.view()}</p>
<p>
<img src={app.forum.attribute(this.attrs.name + 'Url')} alt="" />
</p>
<p>{super.view({ ...vnode, children: app.translator.trans('core.admin.upload_image.remove_button') })}</p>
</div>
);
} else {
this.props.onclick = this.upload.bind(this);
this.props.children = app.translator.trans('core.admin.upload_image.upload_button');
this.attrs.onclick = this.upload.bind(this);
}
return super.view();
return super.view({ ...vnode, children: app.translator.trans('core.admin.upload_image.upload_button') });
}
/**
@@ -35,23 +33,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 body = new FormData();
body.append(this.attrs.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,
body,
})
.then(this.success.bind(this), this.failure.bind(this));
});
}
/**
@@ -61,17 +62,16 @@ 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() {
return app.forum.attribute('apiUrl') + '/' + this.props.name;
return app.forum.attribute('apiUrl') + '/' + this.attrs.name;
}
/**

View File

@@ -1,38 +0,0 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import Component from '../../common/Component';
export default class DashboardWidget extends Component {
view() {
return (
<div className={"DashboardWidget "+this.className()}>
{this.content()}
</div>
);
}
/**
* Get the class name to apply to the widget.
*
* @return {String}
*/
className() {
return '';
}
/**
* Get the content of the widget.
*
* @return {VirtualElement}
*/
content() {
return [];
}
}

View File

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

View File

@@ -0,0 +1,19 @@
import DefaultResolver from '../../common/resolvers/DefaultResolver';
/**
* A custom route resolver for ExtensionPage that generates handles routes
* to default extension pages or a page provided by an extension.
*/
export default class ExtensionPageResolver extends DefaultResolver {
static extension: string | null = null;
onmatch(args, requestedPath, route) {
const extensionPage = app.extensionData.getPage(args.id);
if (extensionPage) {
return extensionPage;
}
return super.onmatch(args, requestedPath, route);
}
}

View File

@@ -2,21 +2,22 @@ import DashboardPage from './components/DashboardPage';
import BasicsPage from './components/BasicsPage';
import PermissionsPage from './components/PermissionsPage';
import AppearancePage from './components/AppearancePage';
import ExtensionsPage from './components/ExtensionsPage';
import MailPage from './components/MailPage';
import ExtensionPage from './components/ExtensionPage';
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
/**
* The `routes` initializer defines the forum app's routes.
*
* @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 },
basics: { path: '/basics', component: BasicsPage },
permissions: { path: '/permissions', component: PermissionsPage },
appearance: { path: '/appearance', component: AppearancePage },
mail: { path: '/mail', component: MailPage },
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },
};
}

View File

@@ -0,0 +1,167 @@
import ItemList from '../../common/utils/ItemList';
export default class ExtensionData {
constructor() {
this.data = {};
this.currentExtension = null;
}
/**
* This function simply takes the extension id
*
* @example
* app.extensionData.load('flarum-tags')
*
* flarum/flags -> flarum-flags | acme/extension -> acme-extension
*
* @param extension
*/
for(extension) {
this.currentExtension = extension;
this.data[extension] = this.data[extension] || {};
return this;
}
/**
* This function registers your settings with Flarum
*
* @example
*
* .registerSetting({
* setting: 'flarum-flags.guidelines_url',
* type: 'text', // This will be inputted into the input tag for the setting (text/number/etc)
* label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label')
* }, 15) // priority is optional (ItemList)
*
*
* @param content
* @param priority
* @returns {ExtensionData}
*/
registerSetting(content, priority = 0) {
this.data[this.currentExtension].settings = this.data[this.currentExtension].settings || new ItemList();
this.data[this.currentExtension].settings.add(content.setting, content, priority);
return this;
}
/**
* This function registers your permission with Flarum
*
* @example
*
* .registerPermission('permissions', {
* icon: 'fas fa-flag',
* label: app.translator.trans('flarum-flags.admin.permissions.view_flags_label'),
* permission: 'discussion.viewFlags'
* }, 'moderate', 65)
*
* @param content
* @param permissionType
* @param priority
* @returns {ExtensionData}
*/
registerPermission(content, permissionType = null, priority = 0) {
this.data[this.currentExtension].permissions = this.data[this.currentExtension].permissions || {};
if (!this.data[this.currentExtension].permissions[permissionType]) {
this.data[this.currentExtension].permissions[permissionType] = new ItemList();
}
this.data[this.currentExtension].permissions[permissionType].add(content.permission, content, priority);
return this;
}
/**
* Replace the default extension page with a custom component.
* This component would typically extend ExtensionPage
*
* @param component
* @returns {ExtensionData}
*/
registerPage(component) {
this.data[this.currentExtension].page = component;
return this;
}
/**
* Get an extension's registered settings
*
* @param extensionId
* @returns {boolean|*}
*/
getSettings(extensionId) {
if (this.data[extensionId] && this.data[extensionId].settings) {
return this.data[extensionId].settings.toArray();
}
return false;
}
/**
*
* Get an ItemList of all extensions' registered permissions
*
* @param extension
* @param type
* @returns {ItemList}
*/
getAllExtensionPermissions(type) {
const items = new ItemList();
Object.keys(this.data).map((extension) => {
if (this.extensionHasPermissions(extension) && this.data[extension].permissions[type]) {
items.merge(this.data[extension].permissions[type]);
}
});
return items;
}
/**
* Get a singular extension's registered permissions
*
* @param extension
* @param type
* @returns {boolean|*}
*/
getExtensionPermissions(extension, type) {
if (this.extensionHasPermissions(extension) && this.data[extension].permissions[type]) {
return this.data[extension].permissions[type];
}
return new ItemList();
}
/**
* Checks whether a given extension has registered permissions.
*
* @param extension
* @returns {boolean}
*/
extensionHasPermissions(extension) {
if (this.data[extension] && this.data[extension].permissions) {
return true;
}
return false;
}
/**
* Returns an extension's custom page component if it exists.
*
* @param extension
* @returns {boolean|*}
*/
getPage(extension) {
if (this.data[extension]) {
return this.data[extension].page;
}
return false;
}
}

View File

@@ -0,0 +1,25 @@
export default function getCategorizedExtensions() {
let extensions = {};
Object.keys(app.data.extensions).map((id) => {
const extension = app.data.extensions[id];
let category = extension.extra['flarum-extension'].category;
// Wrap languages packs into new system
if (extension.extra['flarum-locale']) {
category = 'language';
}
if (category in app.extensionCategories) {
extensions[category] = extensions[category] || [];
extensions[category].push(extension);
} else {
extensions.other = extensions.other || [];
extensions.other.push(extension);
}
});
return extensions;
}

View File

@@ -0,0 +1,5 @@
export default function isExtensionEnabled(name) {
const enabled = JSON.parse(app.data.settings.extensions_enabled);
return enabled.includes(name);
}

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

View File

@@ -1,7 +1,8 @@
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';
@@ -10,6 +11,7 @@ import Drawer from './utils/Drawer';
import mapRoutes from './utils/mapRoutes';
import RequestError from './utils/RequestError';
import ScrollListener from './utils/ScrollListener';
import liveHumanTimes from './utils/liveHumanTimes';
import { extend } from './extend';
import Forum from './models/Forum';
@@ -19,6 +21,9 @@ import Post from './models/Post';
import Group from './models/Group';
import Notification from './models/Notification';
import { flattenDeep } from 'lodash-es';
import PageState from './states/PageState';
import ModalManagerState from './states/ModalManagerState';
import AlertManagerState from './states/AlertManagerState';
/**
* The `App` class provides a container for an application, as well as various
@@ -84,7 +89,7 @@ export default class Application {
discussions: Discussion,
posts: Post,
groups: Group,
notifications: Notification
notifications: Notification,
});
/**
@@ -105,13 +110,49 @@ export default class Application {
booted = false;
/**
* An Alert that was shown as a result of an AJAX request error. If present,
* it will be dismissed on the next successful request.
* The key for an Alert that was shown as a result of an AJAX request error.
* If present, it will be dismissed on the next successful request.
*
* @type {null|Alert}
* @type {int}
* @private
*/
requestError = null;
requestErrorAlert = null;
/**
* The page the app is currently on.
*
* This object holds information about the type of page we are currently
* visiting, and sometimes additional arbitrary page state that may be
* relevant to lower-level components.
*
* @type {PageState}
*/
current = new PageState(null);
/**
* The page the app was on before the current page.
*
* Once the application navigates to another page, the object previously
* assigned to this.current will be moved to this.previous, while this.current
* is re-initialized.
*
* @type {PageState}
*/
previous = new PageState(null);
/*
* An object that manages modal state.
*
* @type {ModalManagerState}
*/
modal = new ModalManagerState();
/**
* An object that manages the state of active alerts.
*
* @type {AlertManagerState}
*/
alerts = new AlertManagerState();
data;
@@ -124,24 +165,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,31 +189,34 @@ export default class Application {
}
mount(basePath = '') {
this.modal = m.mount(document.getElementById('modal'), <ModalManager/>);
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>);
// An object with a callable view property is used in order to pass arguments to the component; see https://mithril.js.org/mount.html
m.mount(document.getElementById('modal'), { view: () => ModalManager.component({ state: this.modal }) });
m.mount(document.getElementById('alerts'), { view: () => AlertManager.component({ state: this.alerts }) });
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 => {
// down. When this happens, we'll add classes to the header and app body
// which will set the navbar's position to fixed. We don't want to always
// have it fixed, as that could overlap with custom headers.
const scrollListener = new ScrollListener((top) => {
const $app = $('#app');
const offset = $app.offset().top;
$app
.toggleClass('affix', top >= offset)
.toggleClass('scrolled', top > offset);
}).start();
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
$('.App-header').toggleClass('navbar-fixed-top', top >= offset);
});
scrollListener.start();
scrollListener.update();
$(() => {
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
});
liveHumanTimes();
}
/**
@@ -196,6 +237,16 @@ export default class Application {
return null;
}
/**
* Determine the current screen mode, based on our media queries.
*
* @returns {String} - one of "phone", "tablet", "desktop" or "desktop-hd"
*/
screen() {
const styles = getComputedStyle(document.documentElement);
return styles.getPropertyValue('--flarum-screen');
}
/**
* Set the <title> of the page.
*
@@ -218,15 +269,16 @@ export default class Application {
}
updateTitle() {
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') +
(this.title ? this.title + ' - ' : '') +
this.forum.attribute('title');
const count = this.titleCount ? `(${this.titleCount}) ` : '';
const pageTitleWithSeparator = this.title && m.route.get() !== this.forum.attribute('basePath') + '/' ? this.title + ' - ' : '';
const title = this.forum.attribute('title');
document.title = count + pageTitleWithSeparator + title;
}
/**
* Make an AJAX request, handling any low-level errors that may occur.
*
* @see https://lhorie.github.io/mithril/mithril.request.html
* @see https://mithril.js.org/request.html
* @param {Object} options
* @return {Promise}
* @public
@@ -254,17 +306,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) {
@@ -291,58 +345,88 @@ export default class Application {
}
};
if (this.requestError) this.alerts.dismiss(this.requestError.alert);
if (this.requestErrorAlert) this.alerts.dismiss(this.requestErrorAlert);
// Now make the request. If it's a failure, inspect the error that was
// returned and show an alert containing its contents.
const deferred = m.deferred();
return m.request(options).then(
(response) => response,
(error) => {
let content;
m.request(options).then(response => deferred.resolve(response), error => {
this.requestError = error;
switch (error.status) {
case 422:
content = error.response.errors
.map((error) => [error.detail, <br />])
.reduce((a, b) => a.concat(b), [])
.slice(0, -1);
break;
let children;
case 401:
case 403:
content = app.translator.trans('core.lib.error.permission_denied_message');
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 404:
case 410:
content = app.translator.trans('core.lib.error.not_found_message');
break;
case 401:
case 403:
children = app.translator.trans('core.lib.error.permission_denied_message');
break;
case 429:
content = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
break;
case 404:
case 410:
children = app.translator.trans('core.lib.error.not_found_message');
break;
default:
content = app.translator.trans('core.lib.error.generic_message');
}
case 429:
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
break;
const isDebug = app.forum.attribute('debug');
// contains a formatted errors if possible, response must be an JSON API array of errors
// the details property is decoded to transform escaped characters such as '\n'
const errors = error.response && error.response.errors;
const formattedError = Array.isArray(errors) && errors[0] && errors[0].detail && errors.map((e) => decodeURI(e.detail));
default:
children = app.translator.trans('core.lib.error.generic_message');
error.alert = {
type: 'error',
content,
controls: isDebug && [
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error, formattedError)}>
Debug
</Button>,
],
};
try {
options.errorHandler(error);
} catch (error) {
if (isDebug && error.xhr) {
const { method, url } = error.options;
const { status = '' } = error.xhr;
console.group(`${method} ${url} ${status}`);
console.error(...(formattedError || [error]));
console.groupEnd();
}
this.requestErrorAlert = this.alerts.show(error.alert, error.alert.content);
}
return Promise.reject(error);
}
);
}
error.alert = new Alert({
type: 'error',
children
});
/**
* @param {RequestError} error
* @param {string[]} [formattedError]
* @private
*/
showDebug(error, formattedError) {
this.alerts.dismiss(this.requestErrorAlert);
try {
options.errorHandler(error);
} catch (error) {
this.alerts.show(error.alert);
}
deferred.reject(error);
});
return deferred.promise;
this.modal.show(RequestErrorModal, { error, formattedError });
}
/**
@@ -354,9 +438,19 @@ export default class Application {
* @public
*/
route(name, params = {}) {
const url = this.routes[name].path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
const queryString = m.route.buildQueryString(params);
const prefix = m.route.mode === 'pathname' ? app.forum.attribute('basePath') : '';
const route = this.routes[name];
if (!route) throw new Error(`Route '${name}' does not exist`);
const url = route.path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
// Remove falsy values in params to avoid having urls like '/?sort&q'
for (const key in params) {
if (params.hasOwnProperty(key) && !params[key]) delete params[key];
}
const queryString = m.buildQueryString(params);
const prefix = m.route.prefix === '' ? this.forum.attribute('basePath') : '';
return prefix + url + (queryString ? '?' + queryString : '');
}

View File

@@ -1,225 +0,0 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* The `Component` class defines a user interface 'building block'. A component
* can generate a virtual DOM to be rendered on each redraw.
*
* An instance's virtual DOM can be retrieved directly using the {@link
* Component#render} method.
*
* @example
* this.myComponentInstance = new MyComponent({foo: 'bar'});
* return m('div', this.myComponentInstance.render());
*
* Alternatively, components can be nested, letting Mithril take care of
* instance persistence. For this, the static {@link Component.component} method
* can be used.
*
* @example
* return m('div', MyComponent.component({foo: 'bar'));
*
* @see https://lhorie.github.io/mithril/mithril.component.html
* @abstract
*/
export default class Component {
/**
* @param {Object} props
* @param {Array|Object} children
* @public
*/
constructor(props = {}, children = null) {
if (children) props.children = children;
this.constructor.initProps(props);
/**
* The properties passed into the component.
*
* @type {Object}
*/
this.props = props;
/**
* The root DOM element for the component.
*
* @type DOMElement
* @public
*/
this.element = null;
/**
* Whether or not to retain the component's subtree on redraw.
*
* @type {boolean}
* @public
*/
this.retain = false;
this.init();
}
/**
* Called when the component is constructed.
*
* @protected
*/
init() {
}
/**
* Called when the component is destroyed, i.e. after a redraw where it is no
* longer a part of the view.
*
* @see https://lhorie.github.io/mithril/mithril.component.html#unloading-components
* @param {Object} e
* @public
*/
onunload() {
}
/**
* Get the renderable virtual DOM that represents the component's view.
*
* This should NOT be overridden by subclasses. Subclasses wishing to define
* their virtual DOM should override Component#view instead.
*
* @example
* this.myComponentInstance = new MyComponent({foo: 'bar'});
* return m('div', this.myComponentInstance.render());
*
* @returns {Object}
* @final
* @public
*/
render() {
const vdom = this.retain ? {subtree: 'retain'} : this.view();
// Override the root element's config attribute with our own function, which
// will set the component instance's element property to the root DOM
// element, and then run the component class' config method.
vdom.attrs = vdom.attrs || {};
const originalConfig = vdom.attrs.config;
vdom.attrs.config = (...args) => {
this.element = args[0];
this.config.apply(this, args.slice(1));
if (originalConfig) originalConfig.apply(this, args);
};
return vdom;
}
/**
* Returns a jQuery object for this component's element. If you pass in a
* selector string, this method will return a jQuery object, using the current
* element as its buffer.
*
* For example, calling `component.$('li')` will return a jQuery object
* containing all of the `li` elements inside the DOM element of this
* component.
*
* @param {String} [selector] a jQuery-compatible selector string
* @returns {jQuery} the jQuery object for the DOM node
* @final
* @public
*/
$(selector) {
const $element = $(this.element);
return selector ? $element.find(selector) : $element;
}
/**
* Called after the component's root element is redrawn. This hook can be used
* to perform any actions on the DOM, both on the initial draw and any
* subsequent redraws. See Mithril's documentation for more information.
*
* @see https://lhorie.github.io/mithril/mithril.html#the-config-attribute
* @param {Boolean} isInitialized
* @param {Object} context
* @param {Object} vdom
* @public
*/
config() {
}
/**
* Get the virtual DOM that represents the component's view.
*
* @return {Object} The virtual DOM
* @protected
*/
view() {
throw new Error('Component#view must be implemented by subclass');
}
/**
* Get a Mithril component object for this component, preloaded with props.
*
* @see https://lhorie.github.io/mithril/mithril.component.html
* @param {Object} [props] Properties to set on the component
* @param children
* @return {Object} The Mithril component object
* @property {function} controller
* @property {function} view
* @property {Object} component The class of this component
* @property {Object} props The props that were passed to the component
* @public
*/
static component(props = {}, children = null) {
const componentProps = Object.assign({}, props);
if (children) componentProps.children = children;
this.initProps(componentProps);
// Set up a function for Mithril to get the component's view. It will accept
// the component's controller (which happens to be the component itself, in
// our case), update its props with the ones supplied, and then render the view.
const view = (component) => {
component.props = componentProps;
return component.render();
};
// Mithril uses this property on the view function to cache component
// controllers between redraws, thus persisting component state.
view.$original = this.prototype.view;
// Our output object consists of a controller constructor + a view function
// which Mithril will use to instantiate and render the component. We also
// attach a reference to the props that were passed through and the
// component's class for reference.
const output = {
controller: this.bind(undefined, componentProps),
view: view,
props: componentProps,
component: this
};
// If a `key` prop was set, then we'll assume that we want that to actually
// show up as an attribute on the component object so that Mithril's key
// algorithm can be applied.
if (componentProps.key) {
output.attrs = {key: componentProps.key};
}
return output;
}
/**
* Initialize the component's props.
*
* @param {Object} props
* @public
*/
static initProps(props) {
}
}

132
js/src/common/Component.ts Normal file
View File

@@ -0,0 +1,132 @@
import * as Mithril from 'mithril';
export interface ComponentAttrs extends Mithril.Attributes {}
/**
* The `Component` class defines a user interface 'building block'. A component
* generates a virtual DOM to be rendered on each redraw.
*
* Essentially, this is a wrapper for Mithril's components that adds several useful features:
*
* - In the `oninit` and `onbeforeupdate` lifecycle hooks, we store vnode attrs in `this.attrs.
* This allows us to use attrs across components without having to pass the vnode to every single
* method.
* - The static `initAttrs` method allows a convenient way to provide defaults (or to otherwise modify)
* the attrs that have been passed into a component.
* - When the component is created in the DOM, we store its DOM element under `this.element`; this lets
* us use jQuery to modify child DOM state from internal methods via the `this.$()` method.
* - A convenience `component` method, which serves as an alternative to hyperscript and JSX.
*
* As with other Mithril components, components extending Component can be initialized
* and nested using JSX, hyperscript, or a combination of both. The `component` method can also
* be used.
*
* @example
* return m('div', <MyComponent foo="bar"><p>Hello World</p></MyComponent>);
*
* @example
* return m('div', MyComponent.component({foo: 'bar'), m('p', 'Hello World!'));
*
* @see https://mithril.js.org/components.html
*/
export default abstract class Component<T extends ComponentAttrs = ComponentAttrs> implements Mithril.ClassComponent<T> {
/**
* The root DOM element for the component.
*/
protected element!: Element;
/**
* The attributes passed into the component.
*
* @see https://mithril.js.org/components.html#passing-data-to-components
*/
protected attrs!: T;
/**
* @inheritdoc
*/
abstract view(vnode: Mithril.Vnode<T, this>): Mithril.Children;
/**
* @inheritdoc
*/
oninit(vnode: Mithril.Vnode<T, this>) {
this.setAttrs(vnode.attrs);
}
/**
* @inheritdoc
*/
oncreate(vnode: Mithril.VnodeDOM<T, this>) {
this.element = vnode.dom;
}
/**
* @inheritdoc
*/
onbeforeupdate(vnode: Mithril.VnodeDOM<T, this>) {
this.setAttrs(vnode.attrs);
}
/**
* Returns a jQuery object for this component's element. If you pass in a
* selector string, this method will return a jQuery object, using the current
* element as its buffer.
*
* For example, calling `component.$('li')` will return a jQuery object
* containing all of the `li` elements inside the DOM element of this
* component.
*
* @param {String} [selector] a jQuery-compatible selector string
* @returns {jQuery} the jQuery object for the DOM node
* @final
*/
protected $(selector) {
const $element = $(this.element);
return selector ? $element.find(selector) : $element;
}
/**
* Convenience method to attach a component without JSX.
* Has the same effect as calling `m(THIS_CLASS, attrs, children)`.
*
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
*/
static component(attrs = {}, children = null): Mithril.Vnode {
const componentAttrs = Object.assign({}, attrs);
return m(this as any, componentAttrs, children);
}
/**
* Saves a reference to the vnode attrs after running them through initAttrs,
* and checking for common issues.
*/
private setAttrs(attrs: T = {} as T): void {
(this.constructor as typeof Component).initAttrs(attrs);
if (attrs) {
if ('children' in attrs) {
throw new Error(
`[${
(this.constructor as any).name
}] The "children" attribute of attrs should never be used. Either pass children in as the vnode children or rename the attribute`
);
}
if ('tag' in attrs) {
throw new Error(`[${(this.constructor as any).name}] You cannot use the "tag" attribute name with Mithril 2.`);
}
}
this.attrs = attrs;
}
/**
* Initialize the component's attrs.
*
* This can be used to assign default values for missing, optional attrs.
*/
protected static initAttrs<T>(attrs: T): void {}
}

74
js/src/common/Fragment.ts Normal file
View File

@@ -0,0 +1,74 @@
import * as Mithril from 'mithril';
/**
* The `Fragment` class represents a chunk of DOM that is rendered once with Mithril and then takes
* over control of its own DOM and lifecycle.
*
* This is very similar to the `Component` wrapper class, but is used for more fine-grained control over
* the rendering and display of some significant chunks of the DOM. In contrast to components, fragments
* do not offer Mithril's lifecycle hooks.
*
* Use this when you want to enjoy the benefits of JSX / VDOM for initial rendering, combined with
* small helper methods that then make updates to that DOM directly, instead of fully redrawing
* everything through Mithril.
*
* This should only be used when necessary, and only with `m.render`. If you are unsure whether you need
* this or `Component, you probably need `Component`.
*/
export default abstract class Fragment {
/**
* The root DOM element for the fragment.
*/
protected element!: Element;
/**
* Returns a jQuery object for this fragment's element. If you pass in a
* selector string, this method will return a jQuery object, using the current
* element as its buffer.
*
* For example, calling `fragment.$('li')` will return a jQuery object
* containing all of the `li` elements inside the DOM element of this
* fragment.
*
* @param {String} [selector] a jQuery-compatible selector string
* @returns {jQuery} the jQuery object for the DOM node
* @final
*/
public $(selector) {
const $element = $(this.element);
return selector ? $element.find(selector) : $element;
}
/**
* Get the renderable virtual DOM that represents the fragment's view.
*
* This should NOT be overridden by subclasses. Subclasses wishing to define
* their virtual DOM should override Fragment#view instead.
*
* @example
* const fragment = new MyFragment();
* m.render(document.body, fragment.render());
*
* @final
*/
public render(): Mithril.Vnode<Mithril.Attributes, this> {
const vdom = this.view();
vdom.attrs = vdom.attrs || {};
const originalOnCreate = vdom.attrs.oncreate;
vdom.attrs.oncreate = (vnode) => {
this.element = vnode.dom;
if (originalOnCreate) originalOnCreate.apply(this, [vnode]);
};
return vdom;
}
/**
* Creates a view out of virtual elements.
*/
abstract view(): Mithril.Vnode<Mithril.Attributes, this>;
}

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,52 +152,66 @@ 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(),
body: 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.redraw();
throw response;
}
);
}
/**
* Send a request to delete the resource.
*
* @param {Object} data Data to send along with the DELETE request.
* @param {Object} body Data to send along with the DELETE request.
* @param {Object} [options]
* @return {Promise}
* @public
*/
delete(data, options = {}) {
if (!this.exists) return m.deferred.resolve().promise;
delete(body, options = {}) {
if (!this.exists) return Promise.resolve();
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(),
body,
},
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

@@ -30,12 +30,17 @@ export default class Session {
* @return {Promise}
* @public
*/
login(data, options = {}) {
return app.request(Object.assign({
method: 'POST',
url: app.forum.attribute('baseUrl') + '/login',
data
}, options));
login(body, options = {}) {
return app.request(
Object.assign(
{
method: 'POST',
url: `${app.forum.attribute('baseUrl')}/login`,
body,
},
options
)
);
}
/**
@@ -44,6 +49,6 @@ export default class Session {
* @public
*/
logout() {
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
window.location = `${app.forum.attribute('baseUrl')}/logout?token=${this.csrfToken}`;
}
}

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);
@@ -84,22 +82,29 @@ export default class Store {
* @public
*/
find(type, id, query = {}, options = {}) {
let data = query;
let params = query;
let url = app.forum.attribute('apiUrl') + '/' + type;
if (id instanceof Array) {
url += '?filter[id]=' + id.join(',');
} else if (typeof id === 'object') {
data = id;
params = id;
} else if (id) {
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,
params,
},
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

@@ -1,4 +1,3 @@
import User from './models/User';
import username from './helpers/username';
import extract from './utils/extract';
@@ -67,27 +66,43 @@ 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) {
// Either an opening or closing tag.
if (match[1]) {
open[0].push(input[match[1]]);
} else if (match[3]) {
if (match[2]) {
// Closing tag. We start by removing all raw children (generally in the form of strings) from the temporary
// holding array, then run them through m.fragment to convert them to vnodes. Usually this will just give us a
// text vnode, but using m.fragment as opposed to an explicit conversion should be more flexible. This is necessary because
// otherwise, our generated vnode will have raw strings as its children, and mithril expects vnodes.
// Finally, we add the now-processed vnodes back onto the holding array (which is the same object in memory as the
// children array of the vnode we are currently processing), and remove the reference to the holding array so that
// further text will be added to the full set of returned elements.
const rawChildren = open[0].splice(0, open[0].length);
open[0].push(...m.fragment(rawChildren).children);
open.shift();
} else {
let tag = input[match[3]] || {tag: match[3], children: []};
// If a vnode with a matching tag was provided in the translator input, we use that. Otherwise, we create a new vnode
// with this tag, and an empty children array (since we're expecting to insert children, as that's the point of having this in translator)
let tag = input[match[3]] || { tag: match[3], children: [] };
open[0].push(tag);
// Insert the tag's children array as the first element of open, so that text in between the opening
// and closing tags will be added to the tag's children, not to the full set of returned elements.
open.unshift(tag.children || tag);
}
}
} else {
// Not an html tag, we add it to open[0], which is either the full set of returned elements (vnodes and text),
// or if an html tag is currently being processed, the children attribute of that html tag's vnode.
open[0].push(part);
}
});
return hydrated.filter(part => part);
return hydrated.filter((part) => part);
}
pluralize(translation, number) {
@@ -97,7 +112,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 +137,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 +240,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 +254,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 +262,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

@@ -12,15 +12,20 @@ import anchorScroll from './utils/anchorScroll';
import RequestError from './utils/RequestError';
import abbreviateNumber from './utils/abbreviateNumber';
import * as string from './utils/string';
import Stream from './utils/Stream';
import SubtreeRetainer from './utils/SubtreeRetainer';
import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh';
import extract from './utils/extract';
import ScrollListener from './utils/ScrollListener';
import stringToColor from './utils/stringToColor';
import subclassOf from './utils/subclassOf';
import SuperTextarea from './utils/SuperTextarea';
import patchMithril from './utils/patchMithril';
import classList from './utils/classList';
import extractText from './utils/extractText';
import formatNumber from './utils/formatNumber';
import mapRoutes from './utils/mapRoutes';
import withAttr from './utils/withAttr';
import Notification from './models/Notification';
import User from './models/User';
import Post from './models/Post';
@@ -30,6 +35,7 @@ import Forum from './models/Forum';
import Component from './Component';
import Translator from './Translator';
import AlertManager from './components/AlertManager';
import Page from './components/Page';
import Switch from './components/Switch';
import Badge from './components/Badge';
import LoadingIndicator from './components/LoadingIndicator';
@@ -37,10 +43,12 @@ 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';
import Alert from './components/Alert';
import Link from './components/Link';
import LinkButton from './components/LinkButton';
import Checkbox from './components/Checkbox';
import SelectDropdown from './components/SelectDropdown';
@@ -59,11 +67,13 @@ import highlight from './helpers/highlight';
import username from './helpers/username';
import userOnline from './helpers/userOnline';
import listItems from './helpers/listItems';
import Fragment from './Fragment';
import DefaultResolver from './resolvers/DefaultResolver';
export default {
'extend': extend,
'Session': Session,
'Store': Store,
extend: extend,
Session: Session,
Store: Store,
'utils/evented': evented,
'utils/liveHumanTimes': liveHumanTimes,
'utils/ItemList': ItemList,
@@ -79,20 +89,27 @@ export default {
'utils/extract': extract,
'utils/ScrollListener': ScrollListener,
'utils/stringToColor': stringToColor,
'utils/Stream': Stream,
'utils/subclassOf': subclassOf,
'utils/SuperTextarea': SuperTextarea,
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
'utils/patchMithril': patchMithril,
'utils/classList': classList,
'utils/extractText': extractText,
'utils/formatNumber': formatNumber,
'utils/mapRoutes': mapRoutes,
'utils/withAttr': withAttr,
'models/Notification': Notification,
'models/User': User,
'models/Post': Post,
'models/Discussion': Discussion,
'models/Group': Group,
'models/Forum': Forum,
'Component': Component,
'Translator': Translator,
Component: Component,
Fragment: Fragment,
Translator: Translator,
'components/AlertManager': AlertManager,
'components/Page': Page,
'components/Switch': Switch,
'components/Badge': Badge,
'components/LoadingIndicator': LoadingIndicator,
@@ -100,10 +117,12 @@ export default {
'components/Separator': Separator,
'components/Dropdown': Dropdown,
'components/SplitDropdown': SplitDropdown,
'components/RequestErrorModal': RequestErrorModal,
'components/FieldSet': FieldSet,
'components/Select': Select,
'components/Navigation': Navigation,
'components/Alert': Alert,
'components/Link': Link,
'components/LinkButton': LinkButton,
'components/Checkbox': Checkbox,
'components/SelectDropdown': SelectDropdown,
@@ -111,8 +130,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 +140,6 @@ export default {
'helpers/highlight': highlight,
'helpers/username': username,
'helpers/userOnline': userOnline,
'helpers/listItems': listItems
'helpers/listItems': listItems,
'resolvers/DefaultResolver': DefaultResolver,
};

View File

@@ -1,57 +0,0 @@
import Component from '../Component';
import Button from './Button';
import listItems from '../helpers/listItems';
import extract from '../utils/extract';
/**
* The `Alert` component represents an alert box, which contains a message,
* some controls, and may be dismissible.
*
* The alert may have the following special props:
*
* - `type` The type of alert this is. Will be used to give the alert a class
* name of `Alert--{type}`.
* - `controls` An array of controls to show in the alert.
* - `dismissible` Whether or not the alert can be dismissed.
* - `ondismiss` A callback to run when the alert is dismissed.
*
* All other props will be assigned as attributes on the alert element.
*/
export default class Alert extends Component {
view() {
const attrs = Object.assign({}, this.props);
const type = extract(attrs, 'type');
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
const children = extract(attrs, 'children');
const controls = extract(attrs, 'controls') || [];
// If the alert is meant to be dismissible (which is the case by default),
// then we will create a dismiss button to append as the final control in
// the alert.
const dismissible = extract(attrs, 'dismissible');
const ondismiss = extract(attrs, 'ondismiss');
const dismissControl = [];
if (dismissible || dismissible === undefined) {
dismissControl.push(
<Button
icon="fas fa-times"
className="Button Button--link Button--icon Alert-dismiss"
onclick={ondismiss}/>
);
}
return (
<div {...attrs}>
<span className="Alert-body">
{children}
</span>
<ul className="Alert-controls">
{listItems(controls.concat(dismissControl))}
</ul>
</div>
);
}
}

View File

@@ -0,0 +1,50 @@
import Component, { ComponentAttrs } from '../Component';
import Button from './Button';
import listItems from '../helpers/listItems';
import extract from '../utils/extract';
import Mithril from 'mithril';
export interface AlertAttrs extends ComponentAttrs {
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
type?: string;
/** An array of controls to show in the alert. */
controls?: Mithril.Children;
/** Whether or not the alert can be dismissed. */
dismissible?: boolean;
/** A callback to run when the alert is dismissed */
ondismiss?: Function;
}
/**
* The `Alert` component represents an alert box, which contains a message,
* some controls, and may be dismissible.
*/
export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<T> {
view(vnode: Mithril.Vnode) {
const attrs = Object.assign({}, this.attrs);
const type = extract(attrs, 'type');
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
const content = extract(attrs, 'content') || vnode.children;
const controls = (extract(attrs, 'controls') || []) as Mithril.ChildArray;
// If the alert is meant to be dismissible (which is the case by default),
// then we will create a dismiss button to append as the final control in
// the alert.
const dismissible = extract(attrs, 'dismissible');
const ondismiss = extract(attrs, 'ondismiss');
const dismissControl = [];
if (dismissible || dismissible === undefined) {
dismissControl.push(<Button icon="fas fa-times" className="Button Button--link Button--icon Alert-dismiss" onclick={ondismiss} />);
}
return (
<div {...attrs}>
<span className="Alert-body">{content}</span>
<ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
</div>
);
}
}

View File

@@ -6,70 +6,23 @@ import Alert from './Alert';
* be shown and dismissed.
*/
export default class AlertManager extends Component {
init() {
/**
* An array of Alert components which are currently showing.
*
* @type {Alert[]}
* @protected
*/
this.components = [];
oninit(vnode) {
super.oninit(vnode);
this.state = this.attrs.state;
}
view() {
return (
<div className="AlertManager">
{this.components.map(component => <div className="AlertManager-alert">{component}</div>)}
{Object.entries(this.state.getActiveAlerts()).map(([key, alert]) => (
<div className="AlertManager-alert">
<alert.componentClass {...alert.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}>
{alert.children}
</alert.componentClass>
</div>
))}
</div>
);
}
config(isInitialized, context) {
// Since this component is 'above' the content of the page (that is, it is a
// part of the global UI that persists between routes), we will flag the DOM
// to be retained across route changes.
context.retain = true;
}
/**
* Show an Alert in the alerts area.
*
* @param {Alert} component
* @public
*/
show(component) {
if (!(component instanceof Alert)) {
throw new Error('The AlertManager component can only show Alert components');
}
component.props.ondismiss = this.dismiss.bind(this, component);
this.components.push(component);
m.redraw();
}
/**
* Dismiss an alert.
*
* @param {Alert} component
* @public
*/
dismiss(component) {
const index = this.components.indexOf(component);
if (index !== -1) {
this.components.splice(index, 1);
m.redraw();
}
}
/**
* Clear all alerts.
*
* @public
*/
clear() {
this.components = [];
m.redraw();
}
}

View File

@@ -6,34 +6,30 @@ import extract from '../utils/extract';
* The `Badge` component represents a user/discussion badge, indicating some
* status (e.g. a discussion is stickied, a user is an admin).
*
* A badge may have the following special props:
* A badge may have the following special attrs:
*
* - `type` The type of badge this is. This will be used to give the badge a
* class name of `Badge--{type}`.
* - `icon` The name of an icon to show inside the badge.
* - `label`
*
* All other props will be assigned as attributes on the badge element.
* All other attrs will be assigned as attributes on the badge element.
*/
export default class Badge extends Component {
view() {
const attrs = Object.assign({}, this.props);
const attrs = Object.assign({}, this.attrs);
const type = extract(attrs, 'type');
const iconName = extract(attrs, 'icon');
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;
oncreate(vnode) {
super.oncreate(vnode);
if (this.props.label) this.$().tooltip({container: 'body'});
if (this.attrs.label) this.$().tooltip();
}
}

View File

@@ -1,12 +1,15 @@
import Component from '../Component';
import icon from '../helpers/icon';
import classList from '../utils/classList';
import extract from '../utils/extract';
import extractText from '../utils/extractText';
import LoadingIndicator from './LoadingIndicator';
/**
* The `Button` component defines an element which, when clicked, performs an
* action. The button may have the following special props:
* action.
*
* ### Attrs
*
* - `icon` The name of the icon class. If specified, the button will be given a
* 'has-icon' class name.
@@ -15,35 +18,43 @@ import LoadingIndicator from './LoadingIndicator';
* removed.
* - `loading` Whether or not the button should be in a disabled loading state.
*
* All other props will be assigned as attributes on the button element.
* All other attrs will be assigned as attributes on the button element.
*
* Note that a Button has no default class names. This is because a Button can
* be used to represent any generic clickable control, like a menu item.
*/
export default class Button extends Component {
view() {
const attrs = Object.assign({}, this.props);
view(vnode) {
const attrs = Object.assign({}, this.attrs);
delete attrs.children;
attrs.className = attrs.className || '';
attrs.type = attrs.type || 'button';
// If a tooltip was provided for buttons without additional content, we also
// use this tooltip as text for screen readers
if (attrs.title && !vnode.children) {
attrs['aria-label'] = attrs.title;
}
// If given a translation object, extract the text.
if (typeof attrs.title === 'object') {
attrs.title = extractText(attrs.title);
}
// If nothing else is provided, we use the textual button content as tooltip
if (!attrs.title && this.props.children) {
attrs.title = extractText(this.props.children);
if (!attrs.title && vnode.children) {
attrs.title = extractText(vnode.children);
}
const iconName = extract(attrs, 'icon');
if (iconName) attrs.className += ' hasIcon';
const loading = extract(attrs, 'loading');
if (attrs.disabled || loading) {
attrs.className += ' disabled' + (loading ? ' loading' : '');
delete attrs.onclick;
}
return <button {...attrs}>{this.getButtonContent()}</button>;
attrs.className = classList([attrs.className, iconName && 'hasIcon', (attrs.disabled || loading) && 'disabled', loading && 'loading']);
return <button {...attrs}>{this.getButtonContent(vnode.children)}</button>;
}
/**
@@ -52,13 +63,13 @@ export default class Button extends Component {
* @return {*}
* @protected
*/
getButtonContent() {
const iconName = this.props.icon;
getButtonContent(children) {
const iconName = this.attrs.icon;
return [
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'}) : ''
iconName && iconName !== true ? icon(iconName, { className: 'Button-icon' }) : '',
children ? <span className="Button-label">{children}</span> : '',
this.attrs.loading ? <LoadingIndicator size="tiny" className="LoadingIndicator--inline" /> : '',
];
}
}

View File

@@ -1,44 +1,40 @@
import Component from '../Component';
import LoadingIndicator from './LoadingIndicator';
import icon from '../helpers/icon';
import classList from '../utils/classList';
import withAttr from '../utils/withAttr';
/**
* The `Checkbox` component defines a checkbox input.
*
* ### Props
* ### Attrs
*
* - `state` Whether or not the checkbox is checked.
* - `className` The class name for the root element.
* - `disabled` Whether or not the checkbox is disabled.
* - `loading` Whether or not the checkbox is loading.
* - `onchange` A callback to run when the checkbox is checked/unchecked.
* - `children` A text label to display next to the checkbox.
*/
export default class Checkbox extends Component {
init() {
/**
* Whether or not the checkbox's value is in the process of being saved.
*
* @type {Boolean}
* @public
*/
this.loading = false;
}
view(vnode) {
// Sometimes, false is stored in the DB as '0'. This is a temporary
// conversion layer until a more robust settings encoding is introduced
if (this.attrs.state === '0') this.attrs.state = false;
view() {
let className = 'Checkbox ' + (this.props.state ? 'on' : 'off') + ' ' + (this.props.className || '');
if (this.loading) className += ' loading';
if (this.props.disabled) className += ' disabled';
const className = classList([
'Checkbox',
this.attrs.state ? 'on' : 'off',
this.attrs.className,
this.attrs.loading && 'loading',
this.attrs.disabled && 'disabled',
]);
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>
{this.props.children}
<input type="checkbox" checked={this.attrs.state} disabled={this.attrs.disabled} onchange={withAttr('checked', this.onchange.bind(this))} />
<div className="Checkbox-display">{this.getDisplay()}</div>
{vnode.children}
</label>
);
}
@@ -50,9 +46,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.attrs.loading ? <LoadingIndicator size="tiny" /> : icon(this.attrs.state ? 'fas fa-check' : 'fas fa-times');
}
/**
@@ -62,6 +56,6 @@ export default class Checkbox extends Component {
* @protected
*/
onchange(checked) {
if (this.props.onchange) this.props.onchange(checked, this);
if (this.attrs.onchange) this.attrs.onchange(checked, this);
}
}

View File

@@ -0,0 +1,40 @@
import Component from '../Component';
/**
* The `ConfirmDocumentUnload` component can be used to register a global
* event handler that prevents closing the browser window/tab based on the
* return value of a given callback prop.
*
* ### Attrs
*
* - `when` - a callback returning true when the browser should prompt for
* confirmation before closing the window/tab
*
* ### Children
*
* NOTE: Only the first child will be rendered. (Use this component to wrap
* another component / DOM element.)
*
*/
export default class ConfirmDocumentUnload extends Component {
handler() {
return this.attrs.when() || undefined;
}
oncreate(vnode) {
super.oncreate(vnode);
this.boundHandler = this.handler.bind(this);
$(window).on('beforeunload', this.boundHandler);
}
onremove() {
$(window).off('beforeunload', this.boundHandler);
}
view(vnode) {
// To avoid having to render another wrapping <div> here, we assume that
// this component is only wrapped around a single element / component.
return vnode.children[0];
}
}

View File

@@ -6,7 +6,7 @@ import listItems from '../helpers/listItems';
* The `Dropdown` component displays a button which, when clicked, shows a
* dropdown menu beneath it.
*
* ### Props
* ### Attrs
*
* - `buttonClassName` A class name to apply to the dropdown toggle button.
* - `menuClassName` A class name to apply to the dropdown menu.
@@ -19,33 +19,33 @@ import listItems from '../helpers/listItems';
* The children will be displayed as a list inside of the dropdown menu.
*/
export default class Dropdown extends Component {
static initProps(props) {
super.initProps(props);
props.className = props.className || '';
props.buttonClassName = props.buttonClassName || '';
props.menuClassName = props.menuClassName || '';
props.label = props.label || '';
props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'fas fa-caret-down';
static initAttrs(attrs) {
attrs.className = attrs.className || '';
attrs.buttonClassName = attrs.buttonClassName || '';
attrs.menuClassName = attrs.menuClassName || '';
attrs.label = attrs.label || '';
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
}
init() {
oninit(vnode) {
super.oninit(vnode);
this.showing = false;
}
view() {
const items = this.props.children ? listItems(this.props.children) : [];
view(vnode) {
const items = vnode.children ? listItems(vnode.children) : [];
return (
<div className={'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
{this.getButton()}
<div className={'ButtonGroup Dropdown dropdown ' + this.attrs.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
{this.getButton(vnode.children)}
{this.getMenu(items)}
</div>
);
}
config(isInitialized) {
if (isInitialized) return;
oncreate(vnode) {
super.oncreate(vnode);
// When opening the dropdown menu, work out if the menu goes beyond the
// bottom of the viewport. If it does, we will apply class to make it show
@@ -53,8 +53,8 @@ export default class Dropdown extends Component {
this.$().on('shown.bs.dropdown', () => {
this.showing = true;
if (this.props.onshow) {
this.props.onshow();
if (this.attrs.onshow) {
this.attrs.onshow();
}
m.redraw();
@@ -64,26 +64,20 @@ 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', () => {
this.showing = false;
if (this.props.onhide) {
this.props.onhide();
if (this.attrs.onhide) {
this.attrs.onhide();
}
m.redraw();
@@ -96,13 +90,10 @@ export default class Dropdown extends Component {
* @return {*}
* @protected
*/
getButton() {
getButton(children) {
return (
<button
className={'Dropdown-toggle ' + this.props.buttonClassName}
data-toggle="dropdown"
onclick={this.props.onclick}>
{this.getButtonContent()}
<button className={'Dropdown-toggle ' + this.attrs.buttonClassName} data-toggle="dropdown" onclick={this.attrs.onclick}>
{this.getButtonContent(children)}
</button>
);
}
@@ -113,19 +104,15 @@ export default class Dropdown extends Component {
* @return {*}
* @protected
*/
getButtonContent() {
getButtonContent(children) {
return [
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.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : '',
<span className="Button-label">{this.attrs.label}</span>,
this.attrs.caretIcon ? icon(this.attrs.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.attrs.menuClassName}>{items}</ul>;
}
}

View File

@@ -11,11 +11,11 @@ import listItems from '../helpers/listItems';
* The children should be an array of items to show in the fieldset.
*/
export default class FieldSet extends Component {
view() {
view(vnode) {
return (
<fieldset className={this.props.className}>
<legend>{this.props.label}</legend>
<ul>{listItems(this.props.children)}</ul>
<fieldset className={this.attrs.className}>
<legend>{this.attrs.label}</legend>
<ul>{listItems(vnode.children)}</ul>
</fieldset>
);
}

View File

@@ -1,16 +1,16 @@
import Badge from './Badge';
export default class GroupBadge extends Badge {
static initProps(props) {
super.initProps(props);
static initAttrs(attrs) {
super.initAttrs(attrs);
if (props.group) {
props.icon = props.group.icon();
props.style = {backgroundColor: props.group.color()};
props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label;
props.type = 'group--' + props.group.id();
if (attrs.group) {
attrs.icon = attrs.group.icon();
attrs.style = { backgroundColor: attrs.group.color() };
attrs.label = typeof attrs.label === 'undefined' ? attrs.group.nameSingular() : attrs.label;
attrs.type = 'group--' + attrs.group.id();
delete props.group;
delete attrs.group;
}
}
}

View File

@@ -0,0 +1,47 @@
import Component from '../Component';
import extract from '../utils/extract';
/**
* The link component enables both internal and external links.
* It will return a regular HTML link for any links to external sites,
* and it will use Mithril's m.route.Link for any internal links.
*
* Links will default to internal; the 'external' attr must be set to
* `true` for the link to be external.
*/
export default class Link extends Component {
view(vnode) {
let { options = {}, ...attrs } = vnode.attrs;
attrs.href = attrs.href || '';
// For some reason, m.route.Link does not like vnode.text, so if present, we
// need to convert it to text vnodes and store it in children.
const children = vnode.children || { tag: '#', children: vnode.text };
if (attrs.external) {
return <a {...attrs}>{children}</a>;
}
// If the href URL of the link is the same as the current page path
// we will not add a new entry to the browser history.
// This allows us to still refresh the Page component
// without adding endless history entries.
if (attrs.href === m.route.get()) {
if (!('replace' in options)) options.replace = true;
}
// Mithril 2 does not completely rerender the page if a route change leads to the same route
// (or the same component handling a different route).
// Here, the `force` parameter will use Mithril's key system to force a full rerender
// see https://mithril.js.org/route.html#key-parameter
if (extract(attrs, 'force')) {
if (!('state' in options)) options.state = {};
if (!('key' in options.state)) options.state.key = Date.now();
}
attrs.options = options;
return <m.route.Link {...attrs}>{children}</m.route.Link>;
}
}

View File

@@ -1,40 +1,43 @@
import Button from './Button';
import Link from './Link';
/**
* The `LinkButton` component defines a `Button` which links to a route.
*
* ### Props
* ### Attrs
*
* All of the props accepted by `Button`, plus:
* All of the attrs accepted by `Button`, plus:
*
* - `active` Whether or not the page that this button links to is currently
* active.
* - `href` The URL to link to. If the current URL `m.route()` matches this,
* the `active` prop will automatically be set to true.
* - `force` Whether the page should be fully rerendered. Defaults to `true`.
*/
export default class LinkButton extends Button {
static initProps(props) {
props.active = this.isActive(props);
props.config = props.config || m.route;
static initAttrs(attrs) {
super.initAttrs(attrs);
attrs.active = this.isActive(attrs);
if (attrs.force === undefined) attrs.force = true;
}
view() {
const vdom = super.view();
view(vnode) {
const vdom = super.view(vnode);
vdom.tag = 'a';
vdom.tag = Link;
vdom.attrs.active = String(vdom.attrs.active);
return vdom;
}
/**
* Determine whether a component with the given props is 'active'.
* Determine whether a component with the given attrs is 'active'.
*
* @param {Object} props
* @param {Object} attrs
* @return {Boolean}
*/
static isActive(props) {
return typeof props.active !== 'undefined'
? props.active
: m.route() === props.href;
static isActive(attrs) {
return typeof attrs.active !== 'undefined' ? attrs.active : m.route.get() === attrs.href;
}
}

View File

@@ -2,16 +2,17 @@ import Component from '../Component';
import { Spinner } from 'spin.js';
/**
* The `LoadingIndicator` component displays a loading spinner with spin.js. It
* may have the following special props:
* The `LoadingIndicator` component displays a loading spinner with spin.js.
*
* ### Attrs
*
* - `size` The spin.js size preset to use. Defaults to 'small'.
*
* All other props will be assigned as attributes on the element.
* All other attrs will be assigned as attributes on the DOM element.
*/
export default class LoadingIndicator extends Component {
view() {
const attrs = Object.assign({}, this.props);
const attrs = Object.assign({}, this.attrs);
attrs.className = 'LoadingIndicator ' + (attrs.className || '');
delete attrs.size;
@@ -19,12 +20,12 @@ export default class LoadingIndicator extends Component {
return <div {...attrs}>{m.trust('&nbsp;')}</div>;
}
config(isInitialized) {
if (isInitialized) return;
oncreate(vnode) {
super.oncreate(vnode);
const options = { zIndex: 'auto', color: this.$().css('color') };
switch (this.props.size) {
switch (this.attrs.size) {
case 'large':
Object.assign(options, { lines: 10, length: 8, width: 4, radius: 8 });
break;

View File

@@ -9,39 +9,63 @@ import Button from './Button';
* @abstract
*/
export default class Modal extends Component {
init() {
/**
* An alert component to show below the header.
*
* @type {Alert}
*/
this.alert = null;
/**
* Determine whether or not the modal should be dismissible via an 'x' button.
*/
static isDismissible = true;
/**
* Attributes for an alert component to show below the header.
*
* @type {object}
*/
alertAttrs = null;
oncreate(vnode) {
super.oncreate(vnode);
this.attrs.animateShow(() => this.onready());
}
onbeforeremove() {
// If the global modal state currently contains a modal,
// we've just opened up a new one, and accordingly,
// we don't need to show a hide animation.
if (!this.attrs.state.modal) {
this.attrs.animateHide();
// Here, we ensure that the animation has time to complete.
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
return new Promise((resolve) => setTimeout(resolve, 300));
}
}
view() {
if (this.alert) {
this.alert.props.dismissible = false;
if (this.alertAttrs) {
this.alertAttrs.dismissible = false;
}
return (
<div className={'Modal modal-dialog ' + this.className()}>
<div className="Modal-content">
{this.isDismissible() ? (
{this.constructor.isDismissible ? (
<div className="Modal-close App-backControl">
{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">
<h3 className="App-titleControl App-titleControl--text">{this.title()}</h3>
</div>
{alert ? <div className="Modal-alert">{this.alert}</div> : ''}
{this.alertAttrs ? <div className="Modal-alert">{Alert.component(this.alertAttrs)}</div> : ''}
{this.content()}
</form>
@@ -50,23 +74,13 @@ export default class Modal extends Component {
);
}
/**
* Determine whether or not the modal should be dismissible via an 'x' button.
*
* @return {Boolean}
*/
isDismissible() {
return true;
}
/**
* Get the class name to apply to the modal.
*
* @return {String}
* @abstract
*/
className() {
}
className() {}
/**
* Get the title of the modal dialog.
@@ -74,8 +88,7 @@ export default class Modal extends Component {
* @return {String}
* @abstract
*/
title() {
}
title() {}
/**
* Get the content of the modal.
@@ -83,16 +96,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,14 +112,11 @@ export default class Modal extends Component {
this.$('form').find('input, select, textarea').first().focus().select();
}
onhide() {
}
/**
* Hide the modal.
*/
hide() {
app.modal.close();
this.attrs.state.close();
}
/**
@@ -126,7 +134,7 @@ export default class Modal extends Component {
* @param {RequestError} error
*/
onerror(error) {
this.alert = error.alert;
this.alertAttrs = error.alert;
m.redraw();

View File

@@ -1,5 +1,4 @@
import Component from '../Component';
import Modal from './Modal';
/**
* The `ModalManager` component manages a modal dialog. Only one modal dialog
@@ -7,100 +6,53 @@ import Modal from './Modal';
* overwrite the previous one.
*/
export default class ModalManager extends Component {
init() {
this.showing = false;
this.component = null;
}
view() {
const modal = this.attrs.state.modal;
return (
<div className="ModalManager modal fade">
{this.component && this.component.render()}
{modal
? modal.componentClass.component({
...modal.attrs,
animateShow: this.animateShow.bind(this),
animateHide: this.animateHide.bind(this),
state: this.attrs.state,
})
: ''}
</div>
);
}
config(isInitialized, context) {
if (isInitialized) return;
oncreate(vnode) {
super.oncreate(vnode);
// Since this component is 'above' the content of the page (that is, it is a
// part of the global UI that persists between routes), we will flag the DOM
// to be retained across route changes.
context.retain = true;
// Ensure the modal state is notified about a closed modal, even when the
// DOM-based Bootstrap JavaScript code triggered the closing of the modal,
// e.g. via ESC key or a click on the modal backdrop.
this.$().on('hidden.bs.modal', this.attrs.state.close.bind(this.attrs.state));
}
animateShow(readyCallback) {
const dismissible = !!this.attrs.state.modal.componentClass.isDismissible;
// If we are opening this modal while another modal is already open,
// the shown event will not run, because the modal is already open.
// So, we need to manually trigger the readyCallback.
if (this.$().hasClass('in')) {
readyCallback();
return;
}
this.$()
.on('hidden.bs.modal', this.clear.bind(this))
.on('shown.bs.modal', this.onready.bind(this));
.one('shown.bs.modal', readyCallback)
.modal({
backdrop: dismissible || 'static',
keyboard: dismissible,
})
.modal('show');
}
/**
* Show a modal dialog.
*
* @param {Modal} component
* @public
*/
show(component) {
if (!(component instanceof Modal)) {
throw new Error('The ModalManager component can only show Modal components');
}
clearTimeout(this.hideTimeout);
this.showing = true;
this.component = component;
if (app.current) app.current.retain = true;
m.redraw(true);
this.$().modal({backdrop: this.component.isDismissible() ? true : 'static'}).modal('show');
this.onready();
}
/**
* Close the modal dialog.
*
* @public
*/
close() {
if (!this.showing) return;
// Don't hide the modal immediately, because if the consumer happens to call
// the `show` method straight after to show another modal dialog, it will
// cause Bootstrap's modal JS to misbehave. Instead we will wait for a tiny
// bit to give the `show` method the opportunity to prevent this from going
// ahead.
this.hideTimeout = setTimeout(() => {
this.$().modal('hide');
this.showing = false;
});
}
/**
* Clear content from the modal area.
*
* @protected
*/
clear() {
if (this.component) {
this.component.onhide();
}
this.component = null;
app.current.retain = false;
m.lazyRedraw();
}
/**
* When the modal dialog is ready to be used, tell it!
*
* @protected
*/
onready() {
if (this.component && this.component.onready) {
this.component.onready(this.$());
}
animateHide() {
this.$().modal('hide');
}
}

View File

@@ -11,7 +11,7 @@ import LinkButton from './LinkButton';
* If the app has a pane, it will also include a 'pin' button which toggles the
* pinned state of the pane.
*
* Accepts the following props:
* Accepts the following attrs:
*
* - `className` The name of a class to set on the root element.
* - `drawer` Whether or not to show a button to toggle the app's drawer if
@@ -19,26 +19,19 @@ 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.attrs.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>
);
}
config(isInitialized, context) {
// Since this component is 'above' the content of the page (that is, it is a
// part of the global UI that persists between routes), we will flag the DOM
// to be retained across route changes.
context.retain = true;
}
/**
* Get the back button.
*
@@ -46,7 +39,7 @@ export default class Navigation extends Component {
* @protected
*/
getBackButton() {
const {history} = app;
const { history } = app;
const previous = history.getPrevious() || {};
return LinkButton.component({
@@ -54,12 +47,11 @@ export default class Navigation extends Component {
href: history.backUrl(),
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 +62,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',
});
}
@@ -88,19 +80,18 @@ export default class Navigation extends Component {
* @protected
*/
getDrawerButton() {
if (!this.props.drawer) return '';
if (!this.attrs.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,62 @@
import Component from '../Component';
import PageState from '../states/PageState';
/**
* The `Page` component
*
* @abstract
*/
export default class Page extends Component {
oninit(vnode) {
super.oninit(vnode);
app.previous = app.current;
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
app.drawer.hide();
app.modal.close();
/**
* A class name to apply to the body while the route is active.
*
* @type {String}
*/
this.bodyClass = '';
/**
* Whether we should scroll to the top of the page when its rendered.
*
* @type {Boolean}
*/
this.scrollTopOnCreate = true;
/**
* Whether the browser should restore scroll state on refreshes.
*
* @type {Boolean}
*/
this.useBrowserScrollRestoration = true;
}
oncreate(vnode) {
super.oncreate(vnode);
if (this.bodyClass) {
$('#app').addClass(this.bodyClass);
}
if (this.scrollTopOnCreate) {
$(window).scrollTop(0);
}
if ('scrollRestoration' in history) {
history.scrollRestoration = this.useBrowserScrollRestoration ? 'auto' : 'manual';
}
}
onremove() {
if (this.bodyClass) {
$('#app').removeClass(this.bodyClass);
}
}
}

View File

@@ -4,7 +4,7 @@ import Component from '../Component';
* The `Placeholder` component displays a muted text with some call to action,
* usually used as an empty state.
*
* ### Props
* ### Attrs
*
* - `text`
*/
@@ -12,7 +12,7 @@ export default class Placeholder extends Component {
view() {
return (
<div className="Placeholder">
<p>{this.props.text}</p>
<p>{this.attrs.text}</p>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import Modal from './Modal';
export default class RequestErrorModal extends Modal {
className() {
return 'RequestErrorModal Modal--large';
}
title() {
return this.attrs.error.xhr ? `${this.attrs.error.xhr.status} ${this.attrs.error.xhr.statusText}` : '';
}
content() {
const { error, formattedError } = this.attrs;
let responseText;
// If the error is already formatted, just add line endings;
// else try to parse it as JSON and stringify it with indentation
if (formattedError) {
responseText = formattedError.join('\n\n');
} else {
try {
const json = error.response || JSON.parse(error.responseText);
responseText = JSON.stringify(json, null, 2);
} catch (e) {
responseText = error.responseText;
}
}
return (
<div className="Modal-body">
<pre>
{this.attrs.error.options.method} {this.attrs.error.options.url}
<br />
<br />
{responseText}
</pre>
</div>
);
}
}

View File

@@ -1,24 +1,33 @@
import Component from '../Component';
import icon from '../helpers/icon';
import withAttr from '../utils/withAttr';
/**
* The `Select` component displays a <select> input, surrounded with some extra
* elements for styling. It accepts the following props:
* elements for styling. It accepts the following attrs:
*
* - `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.attrs;
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 ? 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

@@ -1,34 +1,52 @@
import Dropdown from './Dropdown';
import icon from '../helpers/icon';
/**
* Determines via a vnode is currently "active".
* Due to changes in Mithril 2, attrs will not be instantiated until AFTER view()
* is initially called on the parent component, so we can not always depend on the
* active attr to determine which element should be displayed as the "active child".
*
* This is a temporary patch, and as so, is not exported / placed in utils.
*/
function isActive(vnode) {
const tag = vnode.tag;
// Allow non-selectable dividers/headers to be added.
if (typeof tag === 'string' && tag !== 'a' && tag !== 'button') return false;
if ('initAttrs' in tag) {
tag.initAttrs(vnode.attrs);
}
return 'isActive' in tag ? tag.isActive(vnode.attrs) : vnode.attrs.active;
}
/**
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
* button's label is set as the label of the first child which has a truthy
* `active` prop.
*
* ### Props
* ### Attrs
*
* - `caretIcon`
* - `defaultLabel`
*/
export default class SelectDropdown extends Dropdown {
static initProps(props) {
props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'fas fa-sort';
static initAttrs(attrs) {
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-sort';
super.initProps(props);
super.initAttrs(attrs);
props.className += ' Dropdown--select';
attrs.className += ' Dropdown--select';
}
getButtonContent() {
const activeChild = this.props.children.filter(child => child.props.active)[0];
let label = activeChild && activeChild.props.children || this.props.defaultLabel;
getButtonContent(children) {
const activeChild = children.find(isActive);
let label = (activeChild && activeChild.children) || this.attrs.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.attrs.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

@@ -7,29 +7,27 @@ import icon from '../helpers/icon';
* is displayed as its own button prior to the toggle button.
*/
export default class SplitDropdown extends Dropdown {
static initProps(props) {
super.initProps(props);
static initAttrs(attrs) {
super.initAttrs(attrs);
props.className += ' Dropdown--split';
props.menuClassName += ' Dropdown-menu--right';
attrs.className += ' Dropdown--split';
attrs.menuClassName += ' Dropdown-menu--right';
}
getButton() {
// Make a copy of the props of the first child component. We will assign
// these props to a new button, so that it has exactly the same behaviour as
getButton(children) {
// Make a copy of the attrs of the first child component. We will assign
// these attrs to a new button, so that it has exactly the same behaviour as
// the first child.
const firstChild = this.getFirstChild();
const buttonProps = Object.assign({}, firstChild.props);
buttonProps.className = (buttonProps.className || '') + ' SplitDropdown-button Button ' + this.props.buttonClassName;
const firstChild = this.getFirstChild(children);
const buttonAttrs = Object.assign({}, firstChild.attrs);
buttonAttrs.className = (buttonAttrs.className || '') + ' SplitDropdown-button Button ' + this.attrs.buttonClassName;
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.component(buttonAttrs, firstChild.children),
<button className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName} data-toggle="dropdown">
{icon(this.attrs.icon, { className: 'Button-icon' })}
{icon('fas fa-caret-down', { className: 'Button-caret' })}
</button>,
];
}
@@ -40,8 +38,8 @@ export default class SplitDropdown extends Dropdown {
* @return {*}
* @protected
*/
getFirstChild() {
let firstChild = this.props.children;
getFirstChild(children) {
let firstChild = children;
while (firstChild instanceof Array) firstChild = firstChild[0];

View File

@@ -5,13 +5,13 @@ import Checkbox from './Checkbox';
* a tick/cross one.
*/
export default class Switch extends Checkbox {
static initProps(props) {
super.initProps(props);
static initAttrs(attrs) {
super.initAttrs(attrs);
props.className = (props.className || '') + ' Checkbox--switch';
attrs.className = (attrs.className || '') + ' Checkbox--switch';
}
getDisplay() {
return this.loading ? super.getDisplay() : '';
return this.attrs.loading ? super.getDisplay() : '';
}
}

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

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