1
0
mirror of https://github.com/flarum/core.git synced 2025-08-21 23:56:33 +02:00

Compare commits

..

313 Commits

Author SHA1 Message Date
Franz Liedke
dd17fa3342 Remove unnecessary change 2020-09-18 22:27:11 +02:00
Franz Liedke
db8755d5d9 Remove unrelated change 2020-09-18 22:27:11 +02:00
Franz Liedke
5c2a233b74 Remove unnecessary conditional
The filter(Boolean) covers this.
2020-09-18 22:27:10 +02:00
Alexander Skvortsov
4dd0361ccf Remove unnecessary class name 2020-09-18 22:27:10 +02:00
Alexander Skvortsov
ccd2b0ee0b Revert diff reduction change
Sometimes, items passed to listItems might be null. We still need that filter check
2020-09-18 22:27:10 +02:00
Alexander Skvortsov
b954e79ec8 Move data from vnode to vnode.state
`vnode.state` is persisted, `vnode` might not be
2020-09-18 22:27:09 +02:00
Franz Liedke
bdc64f98a5 I want my small diff back! 2020-09-18 22:27:09 +02:00
Franz Liedke
f9e5aa4193 Email confirmation alert: Simplify implementation
Now that we don't mess with component instances anymore, we can follow
normal Mithril patterns.
2020-09-18 22:27:09 +02:00
Franz Liedke
801220cc10 Fix type annotation 2020-09-18 22:27:08 +02:00
Franz Liedke
bd52aaaa61 Fragment: Update documentation 2020-09-18 22:27:08 +02:00
Franz Liedke
4a5d12626b Revert "Remove deprecated "BC Layers", since BC is broken anyway"
This reverts commit 37c2956efae828e4f25448f4fdff33a79ab0aace.
2020-09-18 22:27:08 +02:00
Franz Liedke
1e4a7f3282 props -> attrs in code 2020-09-18 22:27:07 +02:00
Franz Liedke
cbcb0429d0 props -> attrs in comments 2020-09-18 22:27:07 +02:00
Franz Liedke
14b12f8ae8 Revert "Fix Post-actions being on top of Post Controls Dropdown"
This reverts commit fa29937d9e4dd868198bc2b003320c8431afed6f.
2020-09-18 22:27:07 +02:00
Franz Liedke
37d7068569 Revert "Run npm audit fix"
This reverts commit 791fd306e999066180cb67337566e17fa5e4bb1b.
2020-09-18 22:27:07 +02:00
Franz Liedke
6e1a9193ac Revert "infrastructure: Run npm audit fix"
This reverts commit ff1c52dcf84a4408fdc156a140f8718b401e7d7f.
2020-09-18 22:27:06 +02:00
Franz Liedke
ea6aea409d TS: Merge shims and tsconfig for now
This makes the structure less confusing, as long as the two parts are
not needed.

Once we actually have decided where to move the shared parts (to share
them with bundled and community extensions), and which parts to share,
this can easily be extracted again.
2020-09-18 22:27:06 +02:00
Franz Liedke
d572200cfb TS: Document shims files 2020-09-18 22:27:06 +02:00
Franz Liedke
e10ebf0489 TS config / shims: Fix indentation / whitespace 2020-09-18 22:27:05 +02:00
Franz Liedke
b251ff0469 Extend documentation 2020-09-18 22:27:05 +02:00
Franz Liedke
4a0947db4b TS: Be more specific in type assertion
See https://stackoverflow.com/a/33404548.
2020-09-18 22:27:05 +02:00
Alexander Skvortsov
32767d6321 Fix typescript errors 2020-09-18 22:27:04 +02:00
Alexander Skvortsov
27017b7181 Register jQuery with shims 2020-09-18 22:27:04 +02:00
Franz Liedke
bdd30ceecc Add documentation, keep lifecycle methods on top 2020-09-18 22:27:04 +02:00
Franz Liedke
abc98645a0 Fix typo, reduce diff 2020-09-18 22:27:03 +02:00
Franz Liedke
5d538adc33 Tweak Mithril documentation link 2020-09-18 22:27:03 +02:00
Franz Liedke
6f11567f91 Update comment
Who cares about Mithril 0.2? ;)
2020-09-18 22:27:03 +02:00
Alexander Skvortsov
537e2690e8 Replace get first element from filter with find 2020-09-18 22:27:03 +02:00
Franz Liedke
0e715a8c40 Restore comment
I love small diffs.
2020-09-18 22:27:02 +02:00
Franz Liedke
383a7e559f DiscussionListPane: Inject state, extract method 2020-09-18 22:27:02 +02:00
Franz Liedke
aeaa9a4b73 Reuse ComposerPostPreview in ReplyPlaceholder 2020-09-18 22:27:02 +02:00
Franz Liedke
85f8fc52b7 ComposerPostPreview: Add scroll anchoring callback
This will be needed to replace the `ReplyPlaceholderPreview` component.
2020-09-18 22:27:01 +02:00
Franz Liedke
174f3aba90 ComposerPostPreview: Allow injecting CSS class 2020-09-18 22:27:01 +02:00
Franz Liedke
47ce93d2fd Stop rendering previews when composer is hidden
This brings this component in line with the `ReplyPlaceholder` one.
2020-09-18 22:27:01 +02:00
Franz Liedke
24935eacaf ComposerPostPreview: Inject entire composer 2020-09-18 22:27:00 +02:00
Franz Liedke
0f99e7c015 Rename post preview component again
I want to reuse it for the reply placeholder as well, and that will
require injecting a composer state instance, so this naming seems to
make more sense.
2020-09-18 22:27:00 +02:00
Franz Liedke
691ae85e50 TextFormatterPreview: Add documentation 2020-09-18 22:27:00 +02:00
Franz Liedke
fb83f8c59c TextFormatterPreview: Pass in content callback
This makes the component itself less dependent on global state, which
has benefits for testability and reusability.
2020-09-18 22:26:59 +02:00
Franz Liedke
a6e6c54972 TextFormatterPreview: Expose in compat 2020-09-18 22:26:59 +02:00
Franz Liedke
6b5bdb5c41 Rename CommentPostPreview -> TextFormatterPreview
The idea is to be a bit more precise as to what this component does.
Also, there's already the `PostPreview` component which is actually a
link *to* a preview. Very confusing.
2020-09-18 22:26:59 +02:00
Alexander Skvortsov
d7ef260c54 Run code to rebuild tooltip oncreate AND onupdate 2020-09-18 22:26:58 +02:00
Alexander Skvortsov
571ed8d8e5 Don't store stuff on DOM elements. 2020-09-18 22:26:58 +02:00
Franz Liedke
42ad490096 mapRoutes: Fix outdated link to Mithril docs 2020-09-18 22:26:58 +02:00
Franz Liedke
09bead3ba2 Extend / correct documentation of SubtreeRetainer 2020-09-18 22:26:58 +02:00
Franz Liedke
abb896d430 Extend documentation of withAttr 2020-09-18 22:26:57 +02:00
Alexander Skvortsov
6e7c86ac50 Format 2020-09-18 22:26:57 +02:00
Alexander Skvortsov
2673dd2ee3 Don't show an error in console on promise rejection when not logged in and trying to reply / start a discussion 2020-09-18 22:26:57 +02:00
Alexander Skvortsov
cbd9c8dd4f Add a workaround for the blank SelectDropdown issue.
Workaround explained in the code.
2020-09-18 22:26:56 +02:00
Alexander Skvortsov
29d995de45 Fix NotificationList storing jquery objects 2020-09-18 22:26:42 +02:00
Franz Liedke
a5b2768836 Revert "Use some for needsRebuild in SubtreeRetainer"
This reverts commit 5318bd456856407dd92f44583bed5c88f0813a80.

My bad, I did not realize there was a side effect (storing stuff in
this.data) when suggesting this change. With that, using some() is much
less useful - plus it probably causes a bug because it does not always
iterate over all items once the callback returns true.
2020-09-18 22:26:42 +02:00
Alexander Skvortsov
398951f282 Fix SubtreeRetainer docblock 2020-09-18 22:26:42 +02:00
Alexander Skvortsov
07f04c7ba7 Use some for needsRebuild in SubtreeRetainer 2020-09-18 22:26:42 +02:00
Alexander Skvortsov
c8d5ca51bb Document AdminApplication location fix 2020-09-18 22:26:41 +02:00
Alexander Skvortsov
60dbd3f26c Significantly increase documentation for Component and Fragment 2020-09-18 22:26:41 +02:00
Alexander Skvortsov
e7f6e37799 Store $scrollParent and $notifications in the component state so we don't need to reobtain them 3 separate times 2020-09-18 22:26:41 +02:00
Alexander Skvortsov
27ffeb204e Add AffixedSidebar to compat 2020-09-18 22:26:40 +02:00
Alexander Skvortsov
884a5cf3b9 Run npm audit fix 2020-09-18 22:26:40 +02:00
Alexander Skvortsov
e895ca738d Move setRouteWithForcedRefresh util to common, add it to compat 2020-09-18 22:26:40 +02:00
Alexander Skvortsov
f41aec1043 Replace stray m.lazyRedraw and m.deferred on Model.js 2020-09-18 22:26:09 +02:00
Alexander Skvortsov
4c73d76668 Fix SuperTextarea instantiation: provide a DOM element, not a jquery collection 2020-09-18 22:26:09 +02:00
Alexander Skvortsov
f67194484b Fix going from one user page directly to another 2020-09-18 22:26:08 +02:00
Alexander Skvortsov
c8f47d519d Use the current route when when applying changeSort instead of always going to index 2020-09-18 22:26:08 +02:00
Alexander Skvortsov
e9b267a33a Call onRoute before refreshParams, so we can use app.current.get('routeName') in discussion params 2020-09-18 22:26:08 +02:00
Alexander Skvortsov
554c72c6db IndexPage: change routeName on app.current 2020-09-18 22:26:07 +02:00
Alexander Skvortsov
fb2b0a1d3e Replace leftover m.redraw(true) with m.redraw.sync() 2020-09-18 22:26:07 +02:00
Alexander Skvortsov
2ac18a39ed Make routeName available to pages again 2020-09-18 22:26:07 +02:00
Alexander Skvortsov
6ed3cb56d4 Fix ellipses button on comment post
The subtree retainer should take revealContent into account, so that when revealContent is toggled, it redraws the post
2020-09-18 22:26:06 +02:00
Alexander Skvortsov
038744f092 Introduce setRouteWithForcedRefresh util 2020-09-18 22:26:06 +02:00
Alexander Skvortsov
2edbd4508a Remove unnecessary vnode argument 2020-09-18 22:26:06 +02:00
Alexander Skvortsov
a6d4658dff Simplify TextEditor by moving oncreate from the textarea vnode in view to the component itself, and removing the redundant vnode lifecycle hooks (since those can be applied on the component as well) 2020-09-18 22:26:05 +02:00
Alexander Skvortsov
955c8121d3 Resolve import typo 2020-09-18 22:26:05 +02:00
Alexander Skvortsov
fd2dcd38d6 format 2020-09-18 22:26:05 +02:00
Alexander Skvortsov
520c7e7d0f Made texteditor actual textarea lifecycle methods extensible 2020-09-18 22:26:05 +02:00
Alexander Skvortsov
5ec9c52b04 formatting fix 2020-09-18 22:26:04 +02:00
Alexander Skvortsov
0341e64057 Make app.composer.show redraw synchronous, implement promises in reply, edit, and new discussion actions properly 2020-09-18 22:26:04 +02:00
Alexander Skvortsov
f1a480d3d7 Rename leftover props to attrs. ALthough this piece of code will probably be removed soon as per https://github.com/flarum/core/pull/2263 2020-09-18 22:25:42 +02:00
Alexander Skvortsov
aceac88013 Store bound handlers in this 2020-09-18 22:25:42 +02:00
Alexander Skvortsov
8cc9e18990 Fix undefined $scrollParent 2020-09-18 22:25:42 +02:00
Alexander Skvortsov
94d3bea53e Fix initAttrs being made static 2020-09-18 22:25:41 +02:00
Alexander Skvortsov
9e6cfcf05a Revert fix for opening modal from modal. Will be split to separate PR. 2020-09-18 22:25:41 +02:00
Alexander Skvortsov
b8abd2522e Add documentation for withAttr util 2020-09-18 22:25:41 +02:00
Alexander Skvortsov
376a00f24f make initAttrs static again 2020-09-18 22:25:40 +02:00
Alexander Skvortsov
43bfaa7400 Update SubtreeRetainer docblock 2020-09-18 22:25:40 +02:00
Alexander Skvortsov
7ba8a7122b Don't pass attrs to this.isProvided unnecessarily 2020-09-18 22:25:40 +02:00
Alexander Skvortsov
88bc291c86 Don't accept unnecessary vnode arguments in view 2020-09-18 22:25:39 +02:00
Alexander Skvortsov
e699ada1cc Don't pass attrs to Modal.content (we forgot to remove it, were experimenting with something else previously) 2020-09-18 22:25:39 +02:00
Alexander Skvortsov
2186584878 Fix m.route that should be m.route.set 2020-09-18 22:25:39 +02:00
Alexander Skvortsov
7beeae6269 typo fix 2020-09-18 22:25:38 +02:00
Alexander Skvortsov
e0e3d6ecae provide vnode to onupdate where it was actually needed 2020-09-18 22:25:38 +02:00
Alexander Skvortsov
f8bc58fd1a format 2020-09-18 22:25:38 +02:00
Alexander Skvortsov
764f50f469 Re-implement UsersSearchSource highlight logic without overriding vnode.children 2020-09-18 22:25:38 +02:00
Alexander Skvortsov
e8485db484 Revert removal of "controls" postfix in UserControls 2020-09-18 22:25:37 +02:00
Alexander Skvortsov
f017d7afbe Fix method that should have been oncreate initially being made as onupdate 2020-09-18 22:25:37 +02:00
Alexander Skvortsov
44c1e91f05 Call logic from onupdate in oncreate where relevant 2020-09-18 22:25:37 +02:00
Alexander Skvortsov
4b05e0073a Don't accept unnecessary vnode argument where it isn't used 2020-09-18 22:25:36 +02:00
Alexander Skvortsov
3764abee51 format 2020-09-18 22:25:36 +02:00
Alexander Skvortsov
015cedb29d Create new objects with spread when modifying vnode children instead of directly setting children when possible. 2020-09-18 22:25:36 +02:00
Alexander Skvortsov
32e9fa1f0b format 2020-09-18 22:25:35 +02:00
Alexander Skvortsov
2bdf0d7096 More consistent use of bidi (don't unecessarily define value 2020-09-18 22:25:35 +02:00
Alexander Skvortsov
b889fa1bbf format 2020-09-18 22:25:35 +02:00
Alexander Skvortsov
76c3494f9b Alert.js: move content definition back where it used to be 2020-09-18 22:25:35 +02:00
Alexander Skvortsov
6bcb76b914 Fix typo (and remove the whole unnecessary method) 2020-09-18 22:25:34 +02:00
Alexander Skvortsov
63984b43f9 Revert changes that have been separated out into https://github.com/flarum/core/pull/2262/files 2020-09-18 22:25:34 +02:00
Alexander Skvortsov
389bc59745 Formatting, docblock for Fragment 2020-09-18 22:25:18 +02:00
Alexander Skvortsov
2eb28ea396 Add fragment as parallel to component 2020-09-18 22:25:18 +02:00
Alexander Skvortsov
169b0fbd9b Fix modal backdrop not closing on app.modal.close() 2020-09-18 22:25:17 +02:00
Alexander Skvortsov
6b178b8204 Get rid of index filter route resolved (no longer necessary since filter route isn't a thing anymore) 2020-09-18 22:25:17 +02:00
Alexander Skvortsov
1d6e985107 Get rid of index.filter route 2020-09-18 22:24:27 +02:00
Alexander Skvortsov
f1fc0fecb7 Move title setting for index page to its own method so it can be used in reloads caused by route changes 2020-09-18 22:24:27 +02:00
David Sevilla Martin
94d8f7e726 remove unused attribute in DiscussionComposer input 2020-09-18 22:24:27 +02:00
David Sevilla Martin
b0891c42da create route resolver for /:filter (route overrides any others such as /tags, etc) 2020-09-18 22:24:26 +02:00
David Sevilla Martin
7d5bebb70a do not use custom route resolver solution for plain index page 2020-09-18 22:22:57 +02:00
David Sevilla Martin
b78db0268a update Component to error if 'tag' attribute is passed - messes with mithril 2020-09-18 22:22:56 +02:00
David Sevilla Martin
fcda092558 fix tsconfig.json 2020-09-18 22:22:56 +02:00
Alexander Skvortsov
f6dd87f72f Fix double-fade of posts in post stream by moving fadeIn to css 2020-09-18 22:22:56 +02:00
Alexander Skvortsov
d8d43d95e0 Remove unnecessary options=true, which breaks with patchMithril (used for a previous but now discarded patch) 2020-09-18 22:21:37 +02:00
David Sevilla Martin
51c7b17305 fix: add 'force' to UserPage sidebar buttons to refresh post/discussion lists 2020-09-18 22:21:37 +02:00
David Sevilla Martin
b592ceb199 update patchMithril to add 'force' attribute & not add endless browser history for same page 2020-09-18 22:21:36 +02:00
David Sevilla Martin
a083876b5f fix: DiscussionPage not updating when switching to a separate discussion (using pane) 2020-09-18 22:21:18 +02:00
David Sevilla Martin
b0cbe277c2 update IndexPage to clear discussions & refresh params using custom route solution 2020-09-18 22:21:18 +02:00
David Sevilla Martin
b1d948becc revert patchMithril changes for all links to re-call oninit 2020-09-18 22:21:18 +02:00
Alexander Skvortsov
a48568b17a Fix back button moving between posts in discussion 2020-09-18 22:21:17 +02:00
Alexander Skvortsov
e5cebd85ed update: admin/components/ExtensionsPage 2020-09-18 22:21:17 +02:00
Alexander Skvortsov
fbd5f6245b update: admin/components/AppearancePage 2020-09-18 22:21:17 +02:00
Alexander Skvortsov
3ce63bc035 update admin/components/SettingsModal 2020-09-18 22:21:16 +02:00
Alexander Skvortsov
bac5e7c94c update: admin/components/UploadImageButton 2020-09-18 22:21:16 +02:00
Alexander Skvortsov
97b0e61f61 update: admin/components/EditGroupModal 2020-09-18 22:21:16 +02:00
Alexander Skvortsov
604989be72 update: admin/components/PermissionGrid 2020-09-18 22:21:16 +02:00
Alexander Skvortsov
f9b1dfe499 update: admin/components/PermissionDropdown 2020-09-18 22:21:15 +02:00
Alexander Skvortsov
f611a44a08 update: admin/components/SettingDropdown 2020-09-18 22:21:15 +02:00
Alexander Skvortsov
911f1fd5c9 update: admin/components/MailPage 2020-09-18 22:21:15 +02:00
Alexander Skvortsov
0087b956ef update: admin/components/BasicsPage
- Not tested: locale selector, homepage selector, display name driver selector
2020-09-18 22:21:14 +02:00
Alexander Skvortsov
99b119f1fa update: admin/utils/saveSettings 2020-09-18 22:21:14 +02:00
Alexander Skvortsov
6be37fd376 update: admin/routes 2020-09-18 22:21:14 +02:00
Alexander Skvortsov
46741f63fe update: admin/components/AdminLinkButton 2020-09-18 22:21:13 +02:00
Alexander Skvortsov
cec00c0dd6 update: admin/components/AdminNav 2020-09-18 22:21:13 +02:00
Alexander Skvortsov
5f2c9da2f5 update: admin/components/SessionDropdown 2020-09-18 22:21:13 +02:00
Alexander Skvortsov
92de05e911 update: admin/components/HeaderSecondary 2020-09-18 22:21:12 +02:00
Alexander Skvortsov
68aa6e26da update: admin/AdminApplication 2020-09-18 22:21:12 +02:00
Alexander Skvortsov
8b7fa012c7 Remove workaround for m.route.Link vnode.text issue, as a more global patch has been added to patchMithril 2020-09-18 22:21:12 +02:00
Alexander Skvortsov
61231debd3 fix forum/components/DiscussionPage: page title not being updated when opening a discussion 2020-09-18 22:21:12 +02:00
Alexander Skvortsov
e771ec90c4 Remove patch for double oninit on DiscussionPage: we need to find and fix the underlying issue, or it will affect ALL links to posts/discussions 2020-09-18 22:21:11 +02:00
Alexander Skvortsov
ab85b49845 fix patchMithril: add Fix for m.route.Link with vnode.children not showing anything. 2020-09-18 22:21:11 +02:00
Alexander Skvortsov
04e5d5884f Patch DiscussionListItem to avoid double re-oninit when going to a discussion page 2020-09-18 22:21:11 +02:00
Alexander Skvortsov
ddc1141106 Fix empty error alerts. We might want to bring back the old order for alerts.show (children after attrs) 2020-09-18 22:21:10 +02:00
Alexander Skvortsov
a48cc19814 Remove unused config method from SessionDropdown 2020-09-18 22:21:10 +02:00
Alexander Skvortsov
6d18b700ec Rename config to oncreate for NotificationGrid 2020-09-18 22:21:10 +02:00
Alexander Skvortsov
b2bc427b3f fix: forum/components/ForgotPasswordModal (fix alert content) 2020-09-18 22:21:09 +02:00
Alexander Skvortsov
1f94ffc842 cleanup: common/components/Navigation (remove unused config 2020-09-18 22:21:09 +02:00
Alexander Skvortsov
3d91268493 Fix showing alert on user deletion (UserControls) 2020-09-18 22:21:09 +02:00
Alexander Skvortsov
81fd986881 forum/components/Notification: Support external links 2020-09-18 22:21:09 +02:00
Alexander Skvortsov
d1b0030292 update: forum/utils/alertEmailConfirmation
- ResendButton has been expanded into a full(er) component to avoid storing component instances, ContainedAlert has been removed
2020-09-18 22:21:08 +02:00
Alexander Skvortsov
2c395a781c update: forum/components/NotificationsPage 2020-09-18 22:21:08 +02:00
Alexander Skvortsov
21861f231b Remove deprecated "BC Layers", since BC is broken anyway 2020-09-18 22:21:08 +02:00
Alexander Skvortsov
537f5e833e update: common/components/Select 2020-09-18 22:21:07 +02:00
Alexander Skvortsov
4d45aaa9ae update: forum/components/LoginButton 2020-09-18 22:21:07 +02:00
Alexander Skvortsov
529f8e5f32 SubtreeRetainer: change docblock "this.props" to "this.attrs" 2020-09-18 22:21:07 +02:00
Alexander Skvortsov
991d90bf4a LoginModal: change references from props to attrs 2020-09-18 22:21:06 +02:00
Alexander Skvortsov
38fed603f8 Remove deprecated preferenceSaver from settingspage 2020-09-18 22:21:06 +02:00
Alexander Skvortsov
9691a6ab92 SignupModal: rename props to attrs 2020-09-18 22:21:06 +02:00
Alexander Skvortsov
e8e4b64d7d mapRoutes: remove obsolete setting of routeName attr on components 2020-09-18 22:21:06 +02:00
Alexander Skvortsov
35d76515d3 update: forum/components/TextEditorButton 2020-09-18 22:21:05 +02:00
Alexander Skvortsov
5d34124a02 replace config and href with route on Notification and NotificationList 2020-09-18 22:21:05 +02:00
Alexander Skvortsov
16a6f82e8f replace href and config with route on PostPreview (which is unused, also, update the component 2020-09-18 22:21:05 +02:00
Alexander Skvortsov
8a0c241a8e remove affixSidebar from compat.js 2020-09-18 22:21:04 +02:00
Alexander Skvortsov
a376c0e596 Rename UserPageSidebar to AffixedSidebar, make it a much more reusable component 2020-09-18 22:21:04 +02:00
Alexander Skvortsov
5ccf9d420e Remove affixSidebar util (now contained in UserPageSidebar component 2020-09-18 22:21:04 +02:00
Alexander Skvortsov
742f89f660 add: forum/components/UserPageSidebar
- extracted from UserPage, replaces affixSidebar util
2020-09-18 22:21:03 +02:00
Alexander Skvortsov
916cf4b546 fix: Composer height handle 2020-09-18 22:21:03 +02:00
Alexander Skvortsov
f127e67fd4 Update: EventPost, DiscussionRenamedPost 2020-09-18 22:21:03 +02:00
Alexander Skvortsov
3e79c3e3ff update: forum/components/EditUserModal 2020-09-18 22:21:02 +02:00
Alexander Skvortsov
0172dfd79c update: forum/components/ChangeEmailModal 2020-09-18 22:21:02 +02:00
Alexander Skvortsov
eb627544fa update:forum/components/ChangePasswordModal 2020-09-18 22:21:02 +02:00
Alexander Skvortsov
73c0a90da7 update: forum/components/RenameDiscussionModal 2020-09-18 22:21:01 +02:00
Alexander Skvortsov
ca0f8f2d72 Fix preview functionality of ReplyComposer and EditPostComposer
- options= true means that the init method of DiscussionPage will not be recalled (as per patchMithril
- DiscussionPage now caches the previous route, and compares it to the current one onupdate. If the route has change. We update this cached prevRoute in positionChanged to avoid unneessary goToNumber calls.
2020-09-18 22:21:01 +02:00
Alexander Skvortsov
090df13e7f fix: ComposerState (use m.redraw.sync() instead of m.redraw(true)) 2020-09-18 22:21:01 +02:00
Alexander Skvortsov
d1a1277f88 update: forum/components/EditPostComponent 2020-09-18 22:21:01 +02:00
Alexander Skvortsov
31b2ab1b2b update: forum/components/ReplyComposer 2020-09-18 22:21:00 +02:00
Alexander Skvortsov
2590073a50 fix: forum/components/TextEditor 2020-09-18 22:21:00 +02:00
Alexander Skvortsov
4402dc81ac wip fix: forum/components/DiscussionPage
- rename onunload to onremove. We are still unable to cancel the unload though.
2020-09-18 22:21:00 +02:00
Alexander Skvortsov
f93a255a2f fix: ConfirmDocumentUnload 2020-09-18 22:20:59 +02:00
Alexander Skvortsov
ddb0a9f1ce update/fix: forum/components/SplitDropdown 2020-09-18 22:20:59 +02:00
Alexander Skvortsov
f44caf1600 update: forum/components/PostStreamScrubber 2020-09-18 22:20:59 +02:00
Alexander Skvortsov
89b6847710 update: forum/components/PostStream 2020-09-18 22:20:58 +02:00
Alexander Skvortsov
2fb885175a update: forum/components/TextEditor 2020-09-18 22:20:04 +02:00
Alexander Skvortsov
7d9db2f4ae update: forum/components/CommentPost
- removed now irrelevant Mithril 0.1 workaround for ul jsx
- Move CommentPostPreview into its own class
2020-09-18 22:20:04 +02:00
Alexander Skvortsov
5d073941c9 formatting fix 2020-09-18 22:20:03 +02:00
Alexander Skvortsov
f664fa5be7 update: forum/utils/PostControls 2020-09-18 22:20:03 +02:00
Alexander Skvortsov
aa4b58d7aa update: forum/components/PostMeta 2020-09-18 22:20:03 +02:00
Alexander Skvortsov
3120eb6f63 update: forum/components/PostEdited 2020-09-18 22:20:02 +02:00
Alexander Skvortsov
aac54a1d28 update: forum/components/DiscussionHero 2020-09-18 22:20:02 +02:00
Alexander Skvortsov
0d0841d019 formatting fix 2020-09-18 22:20:02 +02:00
Alexander Skvortsov
83d2dbd290 Fix: extend Component in DiscussionListPane 2020-09-18 22:20:02 +02:00
Alexander Skvortsov
7e5b40c532 Fix: extend Component in ReplyPlaceholderComponent 2020-09-18 22:20:01 +02:00
Alexander Skvortsov
6c9971eeba add: forum/components/CommentPostPreview
- This has been extracted from CommentPost
2020-09-18 22:20:01 +02:00
Alexander Skvortsov
37a690833a update: forum/components/Post
- attrs has been renamed to elementAttrs
- As subtree retention is now implemented via onbeforeupdate, view no longer needs to return a retain vnode, and as such, has been significantly simplified
2020-09-18 22:20:01 +02:00
Alexander Skvortsov
fa2301b5c1 update: forum/components/DiscussionPage
- The discussion list pane has been extracted to the DiscussionListPane component
2020-09-18 22:20:00 +02:00
Alexander Skvortsov
edca7b93ec add forum/components/DiscussionListPane
- Extract this from DiscussionPage
2020-09-18 22:20:00 +02:00
Alexander Skvortsov
095dce9a3e Mount AlertManager in Application 2020-09-18 22:20:00 +02:00
Alexander Skvortsov
479f655bb3 update: common/components/AlertManager 2020-09-18 22:19:59 +02:00
Alexander Skvortsov
41d6e91318 update: common/states/AlertManagerState
- Children is now the first argument of the alerts.show function, since it's no longer managed through attrs. It's also more relvant than attrs, which is why I put it first, but we can definitely reverse the two.
2020-09-18 22:19:59 +02:00
Alexander Skvortsov
87414995b6 fix: forum/components/UserSearchSource
- Use route attr instead of href and config
2020-09-18 22:19:59 +02:00
Alexander Skvortsov
2c93b5f801 update: forum/components/PostUser 2020-09-18 22:19:59 +02:00
Alexander Skvortsov
7498f5e506 fix: forum/components/UserCard
- Use route attr instead of href and config
2020-09-18 22:19:58 +02:00
Alexander Skvortsov
79f5291f04 update: forum/states/PostStreamState
- Change m.deferred to native promise
2020-09-18 22:19:58 +02:00
Alexander Skvortsov
ed3b923f58 update: forum/components/ReplyPlaceholder
- The preview has been extracted to ReplyPlaceholderPreview
2020-09-18 22:19:58 +02:00
Alexander Skvortsov
1ce06611ce add: Extract ReplyPlaceholderPreview from ReplyPlaceholder 2020-09-18 22:19:57 +02:00
Alexander Skvortsov
27bacd779b Update patchMithril modified link documentation with a better link to mithril's docs. 2020-09-18 22:19:57 +02:00
Alexander Skvortsov
02acacfdcb Merge branch 'mithril-2-update' of github.com:flarum/core into mithril-2-update 2020-09-18 22:19:57 +02:00
Alexander Skvortsov
23d95a7566 Add Pane to ForumApplication 2020-09-18 22:19:56 +02:00
Alexander Skvortsov
43164df79e formatting fix 2020-09-18 22:19:56 +02:00
Alexander Skvortsov
46e704b27b update: IndexPage (move onunload contents into onremove) 2020-09-18 22:19:56 +02:00
David Sevilla Martin
e55867acb4 format 2020-09-18 22:19:55 +02:00
Alexander Skvortsov
3596425bde update: History (re-init component even if already on index page) 2020-09-18 22:19:55 +02:00
David Sevilla Martin
b43452223f fix: Button not appearing disabled when loading 2020-09-18 22:19:55 +02:00
Alexander Skvortsov
70697be8c0 infrastructure: (mostly) force re-calling oninit when a route change is handled by the same component
- Due to mithril 2.0, setting a route will not re-call oninit if the component handling the route has not changed.
- Mithril allows us to provide an options parameter to m.route.set, which, if has the state.key parameter changed, will force a re-oninit. However, manually implementing this on every button and component is both tedious, and will make further changes in functionality difficult
- To that end, we can add in this patch here, which will take care of most cases. Code that explicitly calls m.route.set will still need to include this options parameter.
2020-09-18 22:19:55 +02:00
Alexander Skvortsov
c20ae678f5 Move config method of IndexPage into oncreate and onremove 2020-09-18 22:19:54 +02:00
Alexander Skvortsov
30a61b8b42 fix: forum/components/DiscussionListItem (use route instead of config) 2020-09-18 22:19:54 +02:00
Alexander Skvortsov
824fe95346 update: DisussionSearchSource (use route attr to support linking without refreshing) 2020-09-18 22:19:54 +02:00
David Sevilla Martin
8475d176e0 fix: forum/routes /:filter handling /settings 2020-09-18 22:19:53 +02:00
David Sevilla Martin
95f4dc771d update: forum/components/SettingsPage 2020-09-18 22:19:53 +02:00
David Sevilla Martin
68caf45f33 update: forum/components/NotificationGrid 2020-09-18 22:19:53 +02:00
David Sevilla Martin
f72d118bec cleanup: common/components/Button 2020-09-18 22:19:52 +02:00
David Sevilla Martin
652d961907 update: common/components/FieldSet 2020-09-18 22:19:52 +02:00
David Sevilla Martin
71178245fc update: common/components/Switch 2020-09-18 22:19:52 +02:00
David Sevilla Martin
cfd1f01299 update: common/components/Checkbox 2020-09-18 22:19:52 +02:00
David Sevilla Martin
674f55e91d fix: support text-only vnodes with extractText
Also seems to fix not having a space between 'Posts' and the number of posts in the UserPage tooltips
2020-09-18 22:19:51 +02:00
David Sevilla Martin
f897b58f29 add: common/utils/withAttr
Replaces m.withAttr
2020-09-18 22:19:51 +02:00
David Sevilla Martin
5c49b71c02 update: forum/utils/UserControls 2020-09-18 22:19:51 +02:00
David Sevilla Martin
b9ba5b63f1 update: forum/components/DiscussionsUserPage 2020-09-18 22:19:50 +02:00
David Sevilla Martin
6e88dfb2cb update: forum/components/PostsUserPage 2020-09-18 22:19:50 +02:00
David Sevilla Martin
c899e11070 update: forum/components/UserPage 2020-09-18 22:19:50 +02:00
David Sevilla Martin
784b5cc03c update: forum/components/AvatarEditor 2020-09-18 22:19:49 +02:00
David Sevilla Martin
c17d7cd23f fix: Application#updateTitle using m.route() instead of m.route.get() 2020-09-18 22:19:49 +02:00
David Sevilla Martin
4af34265cc update: common/components/GroupBadge 2020-09-18 22:19:49 +02:00
Alexander Skvortsov
1616d8f1c7 Fix patchMithril for route 2020-09-18 22:19:48 +02:00
Alexander Skvortsov
57b85f501a Add composer mount to ForumApplication 2020-09-18 22:19:48 +02:00
Alexander Skvortsov
91609b8a71 update: forum/components/DiscussionComposer 2020-09-18 22:19:48 +02:00
Alexander Skvortsov
9d8b466d57 update: forum/states/ComposerState 2020-09-18 22:19:48 +02:00
Alexander Skvortsov
02154d05e5 update: forum/components/TextEditor 2020-09-18 22:19:47 +02:00
Alexander Skvortsov
a454b185e9 update forum/components/ComposerButton 2020-09-18 22:19:47 +02:00
Alexander Skvortsov
84c5248872 update: forum/components/ComposerBody 2020-09-18 22:19:47 +02:00
Alexander Skvortsov
2a784009fb update: Composer 2020-09-18 22:19:46 +02:00
Alexander Skvortsov
070865f825 update: IndexPage to fix newDiscussionAction (use promise instead of deferred) 2020-09-18 22:19:46 +02:00
Alexander Skvortsov
9615fd3e39 update: common/components/ConfirmDocumentUnload 2020-09-18 22:19:46 +02:00
Alexander Skvortsov
dde9c9c51b Mount navigation in ForumApplication 2020-09-18 22:19:45 +02:00
Alexander Skvortsov
6547290472 update: common/components/Navigation 2020-09-18 22:19:45 +02:00
Alexander Skvortsov
74f6a3e6ce Set empty object as default attrs for all components 2020-09-18 22:19:45 +02:00
Alexander Skvortsov
98740472a8 update: common/Model (use body instead of data on m.request) 2020-09-18 22:19:45 +02:00
Alexander Skvortsov
1e9825de4f update: forum/components/WelcomeHero 2020-09-18 22:19:44 +02:00
Alexander Skvortsov
d14ca79dfa update: listItems (filter out null/falsy values) 2020-09-18 22:19:44 +02:00
Alexander Skvortsov
5cd5f27769 update: forum/components/HeaderSecondary (add search, finishing the header) 2020-09-18 22:19:44 +02:00
Alexander Skvortsov
d05b63eddd update: forum/states/GlobalSearchState 2020-09-18 22:19:43 +02:00
Alexander Skvortsov
48c7354c08 update: forum/components/Search 2020-09-18 22:19:43 +02:00
Alexander Skvortsov
0fe635d32c update: common/components/Badge 2020-09-18 22:19:43 +02:00
Alexander Skvortsov
2c2a42030a update: UsersSearchSource
- mithril 2 prefers to store single text children in vnode.text, so we need to move it back to children to give us a consistent API
2020-09-18 22:19:42 +02:00
Alexander Skvortsov
37d2902cd1 Fix m.route => m.route.set in NotificationsDropdown 2020-09-18 22:19:42 +02:00
Alexander Skvortsov
66c2f6b76a update: forum/components/DiscussionSearchSource 2020-09-18 22:19:42 +02:00
Alexander Skvortsov
4d68636544 update: forum/components/SessionDropdown 2020-09-18 22:19:41 +02:00
Alexander Skvortsov
73891b751b update: forum/components/Notification, forum/components/DiscussionRenamedNotification
- Not yet tested
2020-09-18 22:19:41 +02:00
Alexander Skvortsov
fe011bf285 Add notifications to HeaderSecondary 2020-09-18 22:19:41 +02:00
Alexander Skvortsov
524dde31d4 update: forum/components/NotificationList 2020-09-18 22:19:41 +02:00
Alexander Skvortsov
b69fc01ab3 Uncomment notifications in ForumApplication 2020-09-18 22:19:40 +02:00
Alexander Skvortsov
20e69e6351 update: NotificationsDropdown 2020-09-18 22:19:40 +02:00
Alexander Skvortsov
d097e7ed4b Remove stray console.log 2020-09-18 22:19:40 +02:00
Alexander Skvortsov
63e07b2044 update: forum/utils/DiscussionControls
- replyAction has not yet been tested.
2020-09-18 22:19:39 +02:00
Alexander Skvortsov
60c3f23667 update: forum/components/DiscussionListItem 2020-09-18 22:19:39 +02:00
Alexander Skvortsov
9793c10610 infrastructure: For SubtreeRetainer, return a boolean, not a vnode, as subtree retaining is now managed by onbeforeupdate 2020-09-18 22:18:32 +02:00
Alexander Skvortsov
29af3e6d8b infrastructure: use params instead of data for get requests in common/Store 2020-09-18 22:18:32 +02:00
Alexander Skvortsov
703c3442da update: forum/components/UserCard 2020-09-18 22:18:32 +02:00
Alexander Skvortsov
e0d3a8c733 update: common/helpers/listItems: fix isSeparator 2020-09-18 22:18:32 +02:00
Alexander Skvortsov
527748ff66 update: forum/components/TerminalPost 2020-09-18 22:18:31 +02:00
Alexander Skvortsov
eb440bb9b6 update: forum/components/DiscussionList 2020-09-18 22:18:31 +02:00
Matthew Kilgore
d71f77d592 update: common/components/Placeholder.js 2020-09-18 22:18:31 +02:00
Matthew Kilgore
8b891abb2b update: forum/components/IndexPage.js 2020-09-18 22:18:30 +02:00
Matthew Kilgore
5f0dcc71ba update: common/components.Page.js 2020-09-18 22:18:30 +02:00
Matthew Kilgore
4fcafe3b2f Set application drawer 2020-09-18 22:18:30 +02:00
Matthew Kilgore
29065e1ee9 infrastructure: provide component classes instead of instances to routes.js 2020-09-18 22:18:29 +02:00
Matthew Kilgore
2b39bd7a0d Fix classList usage in listItems 2020-09-18 22:18:29 +02:00
Matthew Kilgore
94fe3236d7 infrastructure: Add routing to common/Application 2020-09-18 22:18:29 +02:00
Matthew Kilgore
3b6e5a0caf infrastructure: Add routes, history, search to ForumApplication 2020-09-18 22:18:29 +02:00
Matthew Kilgore
9ac8f1543a update: forum/utils/History.js 2020-09-18 22:18:28 +02:00
Matthew Kilgore
439e3a5a9a update: common/components/LinkButton 2020-09-18 22:18:28 +02:00
Matthew Kilgore
883e1a9d6a infrastruture: include the class name of the component where children is provided 2020-09-18 22:18:28 +02:00
Matthew Kilgore
2ac2edbbad Replace mithril-stream with mithril/stream, set it as a global via m.stream 2020-09-18 22:18:27 +02:00
Matthew Kilgore
18148141c3 update: common/components/RequestErrorModal 2020-09-18 22:18:27 +02:00
Matthew Kilgore
10f4223028 Add error warning if children attr is ever used 2020-09-18 22:18:27 +02:00
Matthew Kilgore
e10220ae47 update: Fix Alert on modals 2020-09-18 22:18:26 +02:00
Matthew Kilgore
dcd14821c2 infrastructure: revert to using this.attrs 2020-09-18 22:18:26 +02:00
Alexander Skvortsov
edeaa5855c update: SignUpModal 2020-09-18 22:18:26 +02:00
Alexander Skvortsov
cc91244f1a update: LoginModal 2020-09-18 22:18:25 +02:00
Alexander Skvortsov
5faab8bdbd update: ForgotPasswordModal 2020-09-18 22:18:25 +02:00
Alexander Skvortsov
99d79e7571 update: Modal 2020-09-18 22:18:25 +02:00
Alexander Skvortsov
5606eae0f1 update: ModalManager 2020-09-18 22:18:25 +02:00
Alexander Skvortsov
62cb71d4e1 update: ModalManagerState 2020-09-18 22:18:24 +02:00
Alexander Skvortsov
e28ba4acff infrastructure: fix broken translation of strings with html tags (vnodes) passed into them
Under the old system, we would set the "childen" property of the vnode in the translation to an array containing the text between the tag as a raw string. Mithril 2 expects everything to be a vnode, and as such, errors when processing this raw string. This commit runs that "children" array of raw strings (and possibly other vnodes) through m.fragment to convert everything to vnodes.
2020-09-18 22:18:24 +02:00
Alexander Skvortsov
5b3914535d infrastructure: Replace m.deferred in Application with native promise functionality 2020-09-18 22:18:24 +02:00
Alexander Skvortsov
44975bc606 update: common/Session.js
- Use body instead of data
- Use template literal instead of concatenation
2020-09-18 22:18:23 +02:00
Alexander Skvortsov
f7931e8a30 infrastructure: uncomment modal manager mount in application mount 2020-09-18 22:18:23 +02:00
Alexander Skvortsov
3ba655b2f9 infrastructure: install mithril streams package 2020-09-18 22:18:23 +02:00
Alexander Skvortsov
735ecab446 infrastructure: Run npm audit fix 2020-09-18 22:18:22 +02:00
Matthew Kilgore
64d4eb8c4c update: forum/components/HeaderSecondary (no search, session, notifs) 2020-09-18 22:18:22 +02:00
Matthew Kilgore
e4f1a397d6 update: forum/components/HeaderPrimary 2020-09-18 22:18:22 +02:00
Matthew Kilgore
5a244dcfd2 update: common/components/SelectDropdown 2020-09-18 22:18:21 +02:00
Matthew Kilgore
b60309284c update: common/components/Dropdown 2020-09-18 22:18:21 +02:00
Matthew Kilgore
392fe98c02 comment routes and components and states in forum application 2020-09-18 22:18:21 +02:00
Matthew Kilgore
873f489fec update: common/helpers/listItems 2020-09-18 22:18:21 +02:00
Matthew Kilgore
9d8374208f update: common/components/Button 2020-09-18 22:18:20 +02:00
Matthew Kilgore
07551b4890 update: common/components/Button 2020-09-18 22:18:20 +02:00
Matthew Kilgore
b2c147c147 update: common/components/LoadingIndicator 2020-09-18 22:18:20 +02:00
Matthew Kilgore
5bf5bd36ee update: common/utils/patchMithril (remove unnecessary hacks)
Removed m.prop AND m.withAttr, no alternatives yet (m.prop is mithril/stream import)
2020-09-18 22:18:19 +02:00
Matthew Kilgore
4435ff193a update: common/Component (& converted a tad) 2020-09-18 22:18:19 +02:00
Matthew Kilgore
5c30f8fa67 Setup Mithril 2 2020-09-18 22:18:19 +02:00
543 changed files with 11214 additions and 25469 deletions

View File

@@ -15,5 +15,5 @@ indent_size = 2
[*.{diff,md}]
trim_trailing_whitespace = false
[*.{php,xml,json}]
[*.{php,xml}]
indent_size = 4

2
.gitattributes vendored
View File

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

View File

@@ -7,24 +7,10 @@ on:
jobs:
build:
name: JS / Build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Restore npm cache
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('js/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Our action will install npm, cd into `./js`, run `npm run build`,
# then commit and upload any changes
- name: Build production JS
uses: flarum/action-build@master
- uses: actions/checkout@master
- uses: flarum/action-build@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,76 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
# Run on:
# - pushes to master, or
# - PRs with a base of `master`
# - which do not **only** consist of changes to .md or .less files
on:
push:
branches: [ master ]
paths-ignore:
- '**/*.md'
- '**/*.less'
pull_request:
branches: [ master ]
paths-ignore:
- '**/*.md'
- '**/*.less'
schedule:
- cron: '0 0 * * 1,3,5'
jobs:
analyze:
name: Analyze / ${{ matrix.language }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,7 +1,6 @@
name: Lint
on:
workflow_dispatch:
push:
paths:
- 'js/src/**'
@@ -11,18 +10,22 @@ on:
jobs:
prettier:
name: JS / Prettier
runs-on: ubuntu-latest
name: JS / Prettier
steps:
- name: Check out code
uses: actions/checkout@v2
- uses: actions/checkout@master
- name: Set up Node
uses: actions/setup-node@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: "14"
node-version: "12"
- name: Check JS formatting
run: npx prettier --check src
- name: Install JS dependencies
run: npm ci
working-directory: ./js
- name: Check JS code for formatting
run: node_modules/.bin/prettier --check src
working-directory: ./js

View File

@@ -1,45 +0,0 @@
name: Bundle size checker
on:
workflow_dispatch:
push:
paths:
- "js/**"
pull_request:
paths:
- "js/**"
jobs:
bundlewatch:
runs-on: ubuntu-latest
name: Bundlewatch
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Use npm v7
run: sudo npm install -g npm@7.x.x
- name: Install JS dependencies
# We need to use `npm install` here. If we don't, the workflow will fail.
run: npm install
working-directory: ./js
- name: Build production assets
run: npm run build
working-directory: ./js
- name: Check bundle size change
run: node_modules/.bin/bundlewatch --config .bundlewatch.config.json
working-directory: ./js
env:
BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}

View File

@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
php: [7.3, 7.4, '8.0']
php: [7.2, 7.3, 7.4]
service: ['mysql:5.7', mariadb]
prefix: ['', flarum_]
@@ -21,16 +21,16 @@ jobs:
prefixStr: (prefix)
exclude:
- php: 7.3
- php: 7.2
service: 'mysql:5.7'
prefix: flarum_
- php: 7.3
- php: 7.2
service: mariadb
prefix: flarum_
- php: 8.0
- php: 7.3
service: 'mysql:5.7'
prefix: flarum_
- php: 8.0
- php: 7.3
service: mariadb
prefix: flarum_
@@ -43,25 +43,15 @@ jobs:
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
steps:
- name: Check out code
uses: actions/checkout@v2
- uses: actions/checkout@master
- name: Setup PHP
uses: shivammathur/setup-php@0b9d33cd0782337377999751fc10ea079fdd7104 # pin@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
extensions: curl, dom, gd, json, mbstring, openssl, pdo_mysql, tokenizer, zip
tools: phpunit, composer:v2
- name: Select PHP version
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
# The authentication alter is necessary because newer mysql versions use the `caching_sha2_password` driver,
# which isn't supported prior to PHP7.4
# When we drop support for PHP7.3, we should remove this from the setup.
- name: Create MySQL Database
run: |
sudo systemctl start mysql
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
mysql -uroot -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';" --port 13306
- name: Install Composer dependencies
run: composer install
@@ -75,5 +65,3 @@ jobs:
- name: Run Composer tests
run: composer test
env:
COMPOSER_PROCESS_TIMEOUT: 600

2
.gitignore vendored
View File

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

View File

@@ -12,3 +12,7 @@ disabled:
- phpdoc_order
- phpdoc_separation
- phpdoc_types
finder:
exclude:
- "stubs"

View File

@@ -1,261 +1,5 @@
# Changelog
## [0.1.0-beta.16](https://github.com/flarum/core/compare/v0.1.0-beta.15...v0.1.0-beta.16)
### Added
- Allow event subscribers (https://github.com/flarum/core/pull/2535)
- Allow Settings extender to have a default value (https://github.com/flarum/core/pull/2495)
- Allow hooking into the sending of notifications before being send (https://github.com/flarum/core/pull/2533)
- PHP 8 support (https://github.com/flarum/core/pull/2507)
- Search extender (https://github.com/flarum/core/pull/2483)
- User badges to post preview (https://github.com/flarum/core/pull/2555)
- Optional extension dependencies allow a booting order (https://github.com/flarum/core/pull/2579)
- Auth extender (https://github.com/flarum/core/pull/2176)
- `X-Powered-By` header added to allow indexers easier data aggregation of Flarum adoption (https://github.com/flarum/core/pull/2618)
### Changed
- Run integration tests in transaction (https://github.com/flarum/core/pull/2304)
- Allow policies to return a boolean for simplified allow/deny (https://github.com/flarum/core/pull/2534)
- Converted highlight helper to typescript (https://github.com/flarum/core/pull/2532)
- Add accessibility attributes to Mark as Read button (https://github.com/flarum/core/pull/2564)
- Dismiss errors on change email modal upon a new request ([00913d5](https://github.com/flarum/core/commit/00913d5b0be2172cfce1f16aaf64a24f3d2e6d4b))
- Disabled extensions now are marked with a red circle instead of a red dot (https://github.com/flarum/core/pull/2562)
- Extension dependency errors now show the extension title instead of the ID (https://github.com/flarum/core/pull/2563)
- Change `mutate` method on ApiSerializer extender to `attributes` (https://github.com/flarum/core/pull/2578)
- Moved locale files to the core from the language pack (https://github.com/flarum/core/pull/2408)
- AdminPage extensibility and generic improvements (https://github.com/flarum/core/pull/2593)
- Remove entry of authors, link to https://flarum.org/team (https://github.com/flarum/core/pull/2625)
- Search and filtering are split (https://github.com/flarum/core/pull/2454)
- Move IP identification into a middleware (https://github.com/flarum/core/pull/2624)
- Editor Driver abstraction introduced (https://github.com/flarum/core/pull/2594)
- Allow overriding routes (https://github.com/flarum/core/pull/2577)
- Split user edit permissions into permissions for editing of user credentials, username, groups and suspending (https://github.com/flarum/core/pull/2620)
- Reduced number of admin extension categories (https://github.com/flarum/core/pull/2604)
- Move search related classes to a dedicated Query namespace (https://github.com/flarum/core/pull/2645)
- Rewrite common helpers into typescript (https://github.com/flarum/core/pull/2541)
- `TextEditor` is moved to the common namespace for use in the admin frontend (https://github.com/flarum/core/pull/2649)
- Update Laravel/Illuminate components to 8 (https://github.com/flarum/core/pull/2576)
- Eager load relations in discussion listing to improve performance (https://github.com/flarum/core/pull/2639)
- Adopt flarum/testing package (https://github.com/flarum/core/pull/2545)
- Replace `user` gambit with `author` gambit ([612a57c](https://github.com/flarum/core/commit/612a57c4664415a3ea120103483645c32acc6f12))
- Posts page of on user profile loads posts using username instead of id ([30017ee](https://github.com/flarum/core/commit/30017eef09ae9e78640c4e2cacd4909fffa8d775))
### Fixed
- Transform css breaks iOS scroll functionality (https://github.com/flarum/core/pull/2527)
- Composer header is hidden on mobile devices (https://github.com/flarum/core/pull/2279)
- Cannot delete a post or discussion of a deleted user (https://github.com/flarum/core/pull/2521)
- DiscussionListPane jumps around not keeping the scroll position (https://github.com/flarum/core/pull/2402)
- Infinite scroll on notifications dropdown broken (https://github.com/flarum/core/pull/2524)
- The show language selector switch remains toggled on ([9347b12](https://github.com/flarum/core/commit/9347b12b47bf4ab97ffb7ca92673604b237c1012))
- Model Visibility extender throws exception on extensions that aren't installed or enabled (https://github.com/flarum/core/pull/2580)
- Extensions are marked as enabled when enabling fails to unmet extension dependencies (https://github.com/flarum/core/pull/2558)
- Routes to admin extension pages without a valid ID break the admin page (https://github.com/flarum/core/pull/2584)
- Disabled fieldset use an incorrect CSS property `disallowed` (https://github.com/flarum/core/pull/2585)
- Scrolling to a post that is already loaded the Load More button shows and does not trigger (https://github.com/flarum/core/pull/2388)
- Opening discussions on some mobile devices require a double tap (https://github.com/flarum/core/pull/2607)
- iOS devices show erratic behavior in the post stream while updating (https://github.com/flarum/core/pull/2548)
- Small mobile screens partially hides the composer when the keyboard is open (https://github.com/flarum/core/pull/2631)
- Clearing cache does not clear the template cache in storage/views (https://github.com/flarum/core/pull/2648)
- Boot errors show critical information (https://github.com/flarum/core/pull/2633)
- List user endpoint discloses last online even if user choose against it (https://github.com/flarum/core/pull/2634)
- Group gambit disclosed hidden groups (https://github.com/flarum/core/pull/2657)
- Search results on small windows not fully visible (https://github.com/flarum/core/pull/2650)
- Composer goes off screen on Safari when starting to type (https://github.com/flarum/core/pull/2660)
- A search that has no results shows the search results dropdown ([b88a7cb](https://github.com/flarum/core/commit/b88a7cb33b56e318f11670e9e2d563aef94db039))
- The composer modal moves around when typing on Safari ([a64c398](https://github.com/flarum/core/commit/a64c39835aba43e831209609f4a9638ae589aa41))
### Removed
- Deprecated CSRF wildcard path match
- Deprecated policy and visibility scoping events
- Deprecated post types event
- Deprecated validation events
- Deprecated notification events
- Deprecated floodgate
- Deprecated user preferences event
- Deprecated formatting events
- Deprecated api events
- Deprecated bootstrap.php support
- PHP 7.2 support (https://github.com/flarum/core/pull/2507)
- Bidi attribute in the rendered HTML (https://github.com/flarum/core/pull/2602)
- `AccessToken::find`, use `AccessToken::findValid` instead (https://github.com/flarum/core/pull/2651)
### Deprecated
- `GetModelIsPrivate` event (https://github.com/flarum/core/pull/2587)
- `CheckingPassword` event (https://github.com/flarum/core/pull/2176)
- `event()` helper (https://github.com/flarum/core/pull/2608)
- `AccessToken::generate` argument `$lifetime` (https://github.com/flarum/core/pull/2651)
- `Rememberer::remember` argument `$token` should receive an instance of `RememberAccessToken` with `AccessToken` being deprecated (https://github.com/flarum/core/pull/2651)
- `Rememberer::rememberUser` (https://github.com/flarum/core/pull/2651)
- `SessionAuthenticator::logIn` argument `$userId`, should be replaced with `AccessToken` (https://github.com/flarum/core/pull/2651)
- `TextEditor` has been moved to `common` (https://github.com/flarum/core/pull/2649)
- `UserFilter` ([91e8b56](https://github.com/flarum/core/commit/91e8b569618957c86757ef89bac666e9102db5ae))
## [0.1.0-beta.15](https://github.com/flarum/core/compare/v0.1.0-beta.14.1...v0.1.0-beta.15)
### Added
- Slug drivers support (https://github.com/flarum/core/pull/2456).
- Notification type extender (https://github.com/flarum/core/pull/2424).
- Validation extender (https://github.com/flarum/core/pull/2102).
- Post extender (https://github.com/flarum/core/pull/2101).
- Notification channel extender (https://github.com/flarum/core/pull/2432).
- Service provider extender (https://github.com/flarum/core/pull/2437).
- API serializer extender (https://github.com/flarum/core/pull/2438).
- User preferences extender (https://github.com/flarum/core/pull/2463).
- Settings extender (https://github.com/flarum/core/pull/2452).
- ApiController extender (https://github.com/flarum/core/pull/2451).
- Model visibility extender (https://github.com/flarum/core/pull/2460).
- Policy extender (https://github.com/flarum/core/pull/2461).
### Changed
- Time helpers converted to Typescript (https://github.com/flarum/core/pull/2391).
- Improved the formatter extender (https://github.com/flarum/core/pull/2098).
- Improve wording on installer when facing file permission issues (https://github.com/flarum/core/pull/2435).
- Background color of checkbox toggles improved for better usability (https://github.com/flarum/core/pull/2443).
- Route resolving refactored (https://github.com/flarum/core/pull/2425).
- Administration panel UX refactored (https://github.com/flarum/core/pull/2409).
- Floodgate moved to middleware and extender added (https://github.com/flarum/core/pull/2170).
- DRY up image uploading logic (https://github.com/flarum/core/pull/2477).
- Process isolation on testing (https://github.com/flarum/core/commit/984f751c718c89501cc09857bc271efa2c7eea8c).
- Forum and admin javascript exports namespaced (https://github.com/flarum/core/pull/2488).
### Fixed
- Web updater does not take into account subfolder installations (https://github.com/flarum/core/pull/2426).
- Callables handling in extenders failed (https://github.com/flarum/core/pull/2423).
- Scrolling on mobile from PostSteam changes didn't work correctly (https://github.com/flarum/core/pull/2385).
- Side pane covers part of the discussion page due to `app.discussions` being empty (https://github.com/flarum/core/commit/102e76b084bf47fdfb4c73f95e1fbb322537f7aa).
- Change email modal keeps showing the previous error message even on success (https://github.com/flarum/core/pull/2467).
- Comment count not updated when discussions are deleted (https://github.com/flarum/core/pull/2472).
- `goToIndex` in PostStream does not trigger an xhr to retrieve new data (https://github.com/flarum/core/commit/09e2736cbcc267594b660beabbd001d9030f9880).
- On refresh the post number is reduced by one (https://github.com/flarum/core/pull/2476).
- Queue worker would instantiate a new Queue factory, not the bound one (https://github.com/flarum/core/pull/2481).
- Header accidentally has a border bottom (https://github.com/flarum/core/pull/2489).
- Namespace mentioned in docblock is incorrect (https://github.com/flarum/core/pull/2494).
- Scrolling inside longer discussions (especially Firefox) skips posts (https://github.com/flarum/core/commit/210a6b3e253d7917bd1eacd3ed8d2f95073ae99d).
- Uploading avatars that are jpg/jpeg fails with a validation error (https://github.com/flarum/core/pull/2497).
### Removed
- MomentJS alias (https://github.com/flarum/core/pull/2428).
- Deprecated user events `GetDisplayName` and `PrepareUserGroups` (https://github.com/flarum/core/pull/2428).
- AssertPermissionTrait (https://github.com/flarum/core/pull/2428).
- Path related helpers and methods in Application (https://github.com/flarum/core/pull/2428).
- Backward compatibility layers from the frontend rewrite (https://github.com/flarum/core/pull/2428).
### Deprecated
- `CheckingForFlooding` (https://github.com/flarum/core/commit/8e25bcb68f86cc992c46dfa70368419fe9f936ac).
## [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 (https://github.com/flarum/core/pull/2407).
- Scripts from textformatter aren't executed (https://github.com/flarum/core/pull/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
@@ -301,7 +45,7 @@
- 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
- `Flarum\Event\ConfigureMiddleware` event
### Deprecated
- `Flarum\Event\AbstractConfigureRoutes` event class

View File

@@ -1,14 +1,12 @@
<p align="center"><img src="https://flarum.org/assets/img/logo.png"></p>
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
<p align="center">
<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>
<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>
</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:
@@ -34,3 +32,4 @@ If you discover a security vulnerability within Flarum, please send an e-mail to
## License
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).

View File

@@ -1,17 +1,32 @@
{
"name": "flarum/core",
"description": "Delightfully simple forum software.",
"keywords": [
"forum",
"discussion"
],
"keywords": ["forum", "discussion"],
"homepage": "https://flarum.org/",
"license": "MIT",
"authors": [
{
"name": "Flarum",
"email": "info@flarum.org",
"homepage": "https://flarum.org/team"
"name": "Franz Liedke",
"email": "franz@develophp.org"
},
{
"name": "Daniel Klabbers",
"email": "daniel@klabbers.email",
"homepage": "https://luceos.com"
},
{
"name": "David Sevilla Martin",
"email": "me+flarum@datitisev.me",
"homepage": "https://datitisev.me"
},
{
"name": "Clark Winkelmann",
"email": "clark.winkelmann@gmail.com",
"homepage": "https://clarkwinkelmann.com"
},
{
"name": "Matthew Kilgore",
"email": "matthew@kilgore.dev"
}
],
"support": {
@@ -20,38 +35,36 @@
"docs": "https://flarum.org/docs/"
},
"require": {
"php": ">=7.3",
"php": ">=7.2",
"axy/sourcemap": "^0.1.4",
"components/font-awesome": "^5.14.0",
"dflydev/fig-cookies": "^3.0.0",
"dflydev/fig-cookies": "^2.0.1",
"doctrine/dbal": "^2.7",
"dragonmantank/cron-expression": "^3.1.0",
"franzl/whoops-middleware": "^2.0.0",
"illuminate/bus": "^8.0",
"illuminate/cache": "^8.0",
"illuminate/config": "^8.0",
"illuminate/console": "^8.0",
"illuminate/container": "^8.0",
"illuminate/contracts": "^8.0",
"illuminate/database": "^8.0",
"illuminate/events": "^8.0",
"illuminate/filesystem": "^8.0",
"illuminate/hashing": "^8.0",
"illuminate/mail": "^8.0",
"illuminate/queue": "^8.0",
"illuminate/session": "^8.0",
"illuminate/support": "^8.0",
"illuminate/validation": "^8.0",
"illuminate/view": "^8.0",
"franzl/whoops-middleware": "^0.4.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": "^2.4.1",
"laminas/laminas-httphandlerrunner": "^1.2.0",
"laminas/laminas-stratigility": "^3.2.2",
"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": "^2.0.1",
"middlewares/base-path-router": "^2.0.1",
"middlewares/request-handler": "^2.0.1",
"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",
@@ -59,17 +72,17 @@
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^2.3.6",
"symfony/config": "^5.2.2",
"symfony/console": "^5.2.2",
"symfony/event-dispatcher": "^5.2.2",
"symfony/mime": "^5.2.0",
"symfony/translation": "^5.1.5",
"symfony/yaml": "^5.2.2",
"symfony/config": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"tobscure/json-api": "^0.3.0",
"wikimedia/less.php": "^3.0"
},
"require-dev": {
"flarum/testing": "^0.1.0-beta.16"
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0"
},
"autoload": {
"psr-4": {

View File

@@ -1,8 +0,0 @@
{
"files": [
{
"path": "./dist/*.js"
}
],
"defaultCompression": "gzip"
}

16
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

54
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

10836
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,40 +2,32 @@
"private": true,
"name": "@flarum/core",
"dependencies": {
"@babel/preset-typescript": "^7.10.1",
"@types/mithril": "^2.0.3",
"bootstrap": "^3.4.1",
"clsx": "^1.1.1",
"classnames": "^2.2.5",
"color-thief-browser": "^2.0.2",
"dayjs": "^1.10.4",
"expose-loader": "^2.0.0",
"jquery": "^3.6.0",
"dayjs": "^1.8.28",
"expose-loader": "^0.7.5",
"flarum-webpack-config": "0.1.0-beta.10",
"jquery": "^3.4.1",
"jquery.hotkeys": "^0.1.0",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.14",
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
"mithril": "^2.0.4",
"punycode": "^2.1.1",
"spin.js": "^3.1.0",
"textarea-caret": "^3.1.0"
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-merge": "^4.1.4"
},
"devDependencies": {
"@babel/preset-typescript": "^7.13.0",
"@types/jquery": "^3.5.5",
"@types/lodash-es": "^4.17.4",
"@types/mithril": "^2.0.7",
"@types/punycode": "^2.1.0",
"@types/textarea-caret": "^3.0.0",
"bundlewatch": "^0.3.2",
"cross-env": "^7.0.3",
"flarum-webpack-config": "0.1.0-beta.10",
"husky": "^4.3.8",
"prettier": "^2.2.1",
"webpack": "^5.0.0",
"webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^4.0.0",
"webpack-merge": "^4.0.0"
"husky": "^4.2.5",
"prettier": "2.0.2"
},
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"analyze": "cross-env ANALYZER=true npm run build",
"format": "prettier --write src",
"format-check": "prettier --check src"
},

34
js/shims.d.ts vendored
View File

@@ -1,5 +1,6 @@
// Mithril
import Mithril from 'mithril';
import * as Mithril from 'mithril';
import Stream from 'mithril/stream';
// Other third-party libs
import * as _dayjs from 'dayjs';
@@ -8,6 +9,21 @@ import * as _$ from 'jquery';
// Globals from flarum/core
import Application from './src/common/Application';
/**
* Helpers that flarum/core patches into Mithril
*/
interface m extends Mithril.Static {
prop: typeof Stream;
}
/**
* Export Mithril typings globally.
*
* This lets us use these typings without an extra import everywhere we use
* Mithril in a TypeScript file.
*/
export as namespace Mithril;
/**
* flarum/core exposes several extensions globally:
*
@@ -19,21 +35,9 @@ import Application from './src/common/Application';
* to (and should not) bundle these themselves.
*/
declare global {
// $ is already defined by `@types/jquery`
const m: Mithril.Static;
const $: typeof _$;
const m: m;
const dayjs: typeof _dayjs;
// Extend JQuery with our custom functions, defined with $.fn
interface JQuery {
/**
* Creates a tooltip on a jQuery element reference.
*
* Optionally accepts placement and delay options.
*
* Returns the same reference to allow for method chaining.
*/
tooltip: (tooltipOptions?: { placement?: 'top' | 'bottom' | 'left' | 'right'; delay?: number }) => JQuery;
}
}
/**

View File

@@ -4,16 +4,9 @@ import routes from './routes';
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 {
extensionData = new ExtensionData();
extensionCategories = {
feature: 30,
theme: 20,
language: 10,
};
extensionSettings = {};
history = {
canGoBack: () => true,
@@ -34,24 +27,27 @@ export default class AdminApplication extends Application {
* @inheritdoc
*/
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);
// 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.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]) {
this.extensionSettings[enabled]();
localStorage.removeItem('enabledExtension');
}
}
getRequiredPermissions(permission) {

View File

@@ -1,8 +0,0 @@
import Admin from './AdminApplication';
const app = new Admin();
// @ts-ignore
window.app = app;
export default app;

View File

@@ -1,24 +1,19 @@
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 AdminPage from './components/AdminPage';
import AppearancePage from './components/AppearancePage';
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 ExtensionPage from './components/ExtensionPage';
import ExtensionLinkButton from './components/ExtensionLinkButton';
import AddExtensionModal from './components/AddExtensionModal';
import ExtensionsPage from './components/ExtensionsPage';
import AdminLinkButton from './components/AdminLinkButton';
import PermissionGrid from './components/PermissionGrid';
import ExtensionPermissionGrid from './components/ExtensionPermissionGrid';
import MailPage from './components/MailPage';
import UploadImageButton from './components/UploadImageButton';
import LoadingModal from './components/LoadingModal';
@@ -28,7 +23,6 @@ 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';
@@ -36,24 +30,19 @@ 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/AdminPage': AdminPage,
'components/AppearancePage': AppearancePage,
'components/StatusWidget': StatusWidget,
'components/ExtensionsWidget': ExtensionsWidget,
'components/HeaderSecondary': HeaderSecondary,
'components/SettingsModal': SettingsModal,
'components/DashboardWidget': DashboardWidget,
'components/ExtensionPage': ExtensionPage,
'components/ExtensionLinkButton': ExtensionLinkButton,
'components/AddExtensionModal': AddExtensionModal,
'components/ExtensionsPage': ExtensionsPage,
'components/AdminLinkButton': AdminLinkButton,
'components/PermissionGrid': PermissionGrid,
'components/ExtensionPermissionGrid': ExtensionPermissionGrid,
'components/MailPage': MailPage,
'components/UploadImageButton': UploadImageButton,
'components/LoadingModal': LoadingModal,
@@ -63,7 +52,6 @@ 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,

View File

@@ -0,0 +1,32 @@
/*
* 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 Modal from '../../common/components/Modal';
export default class AddExtensionModal extends Modal {
className() {
return 'AddExtensionModal Modal--small';
}
title() {
return app.translator.trans('core.admin.add_extension.title');
}
content() {
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>
</div>
);
}
}

View File

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

@@ -0,0 +1,16 @@
/*
* 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 LinkButton from '../../common/components/LinkButton';
export default class AdminLinkButton extends LinkButton {
getButtonContent(children) {
return [...super.getButtonContent(children), <div className="AdminLinkButton-description">{this.attrs.description}</div>];
}
}

View File

@@ -1,151 +1,106 @@
import ExtensionLinkButton from './ExtensionLinkButton';
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import Component from '../../common/Component';
import LinkButton from '../../common/components/LinkButton';
import AdminLinkButton from './AdminLinkButton';
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 AdminNav-Main" buttonClassName="Button">
{this.items().toArray().concat(this.extensionItems().toArray())}
<SelectDropdown className="AdminNav App-titleControl" buttonClassName="Button">
{this.items().toArray()}
</SelectDropdown>
);
}
oncreate(vnode) {
super.oncreate(vnode);
this.scrollToActive();
}
onupdate() {
this.scrollToActive();
}
scrollToActive() {
const children = $('.Dropdown-menu').children('.active');
const nav = $('#admin-navigation');
const time = app.previous.type ? 250 : 0;
if (
children.length > 0 &&
(children[0].offsetTop > nav.scrollTop() + nav.outerHeight() || children[0].offsetTop + children[0].offsetHeight < nav.scrollTop())
) {
nav.animate(
{
scrollTop: children[0].offsetTop - nav.height() / 2,
},
time
);
}
}
/**
* Build an item list of main links to show in the admin navigation.
* Build an item list of links to show in the admin navigation.
*
* @return {ItemList}
*/
items() {
const items = new ItemList();
items.add('category-core', <h4 className="ExtensionListTitle">{app.translator.trans('core.admin.nav.categories.core')}</h4>);
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>
AdminLinkButton.component(
{
href: app.route('dashboard'),
icon: 'far fa-chart-bar',
description: app.translator.trans('core.admin.nav.dashboard_text'),
},
app.translator.trans('core.admin.nav.dashboard_button')
)
);
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>
AdminLinkButton.component(
{
href: app.route('basics'),
icon: 'fas fa-pencil-alt',
description: app.translator.trans('core.admin.nav.basics_text'),
},
app.translator.trans('core.admin.nav.basics_button')
)
);
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>
AdminLinkButton.component(
{
href: app.route('mail'),
icon: 'fas fa-envelope',
description: app.translator.trans('core.admin.nav.email_text'),
},
app.translator.trans('core.admin.nav.email_button')
)
);
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>
AdminLinkButton.component(
{
href: app.route('permissions'),
icon: 'fas fa-key',
description: app.translator.trans('core.admin.nav.permissions_text'),
},
app.translator.trans('core.admin.nav.permissions_button')
)
);
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>
AdminLinkButton.component(
{
href: app.route('appearance'),
icon: 'fas fa-paint-brush',
description: app.translator.trans('core.admin.nav.appearance_text'),
},
app.translator.trans('core.admin.nav.appearance_button')
)
);
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>
'extensions',
AdminLinkButton.component(
{
href: app.route('extensions'),
icon: 'fas fa-puzzle-piece',
description: app.translator.trans('core.admin.nav.extensions_text'),
},
app.translator.trans('core.admin.nav.extensions_button')
)
);
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-${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 || '';
const description = extension.description || '';
if (!query || title.toUpperCase().includes(query) || description.toUpperCase().includes(query)) {
items.add(
`extension-${extension.id}`,
<ExtensionLinkButton
href={app.route('extension', { id: extension.id })}
extensionId={extension.id}
className="ExtensionNavButton"
title={description}
>
{title}
</ExtensionLinkButton>,
categories[category]
);
}
});
});
return items;
}
}

View File

@@ -1,180 +0,0 @@
import Page from '../../common/components/Page';
import Button from '../../common/components/Button';
import Switch from '../../common/components/Switch';
import Select from '../../common/components/Select';
import classList from '../../common/utils/classList';
import Stream from '../../common/utils/Stream';
import saveSettings from '../utils/saveSettings';
import AdminHeader from './AdminHeader';
export default class AdminPage extends Page {
oninit(vnode) {
super.oninit(vnode);
this.settings = {};
this.loading = false;
}
view() {
const className = classList(['AdminPage', this.headerInfo().className]);
return (
<div className={className}>
{this.header()}
<div className="container">{this.content()}</div>
</div>
);
}
content() {
return '';
}
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>
);
}
header() {
const headerInfo = this.headerInfo();
return (
<AdminHeader icon={headerInfo.icon} description={headerInfo.description} className={headerInfo.className + '-header'}>
{headerInfo.title}
</AdminHeader>
);
}
headerInfo() {
return {
className: '',
icon: '',
title: '',
description: '',
};
}
/**
* buildSettingComponent 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. Any values inside the 'extra' object will be added
* to the component as an attribute.
*
* Alternatively, you can pass a callback that will be executed in ExtensionPage's
* context to include custom JSX elements.
*
* @example
*
* {
* setting: 'acme.checkbox',
* label: app.translator.trans('acme.admin.setting_label'),
* type: 'bool',
* help: app.translator.trans('acme.admin.setting_help'),
* className: 'Setting-item'
* }
*
* @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) {
if (typeof entry === 'function') {
return entry.call(this);
}
const { setting, help, ...componentAttrs } = entry;
delete componentAttrs.help;
const value = this.setting([setting])();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(componentAttrs.type)) {
return (
<div className="Form-group">
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...componentAttrs}>
{componentAttrs.label}
</Switch>
<div className="helpText">{help}</div>
</div>
);
} else if (['select', 'dropdown', 'selectdropdown'].includes(componentAttrs.type)) {
return (
<div className="Form-group">
<label>{componentAttrs.label}</label>
<div className="helpText">{help}</div>
<Select
value={value || componentAttrs.default}
options={componentAttrs.options}
buttonClassName="Button"
onchange={this.settings[setting]}
{...componentAttrs}
/>
</div>
);
} else {
componentAttrs.className = classList(['FormControl', componentAttrs.className]);
return (
<div className="Form-group">
{componentAttrs.label ? <label>{componentAttrs.label}</label> : ''}
<div className="helpText">{help}</div>
<input type={componentAttrs.type} bidi={this.setting(setting)} {...componentAttrs} />
</div>
);
}
}
onsaved() {
this.loading = false;
app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.settings.saved_message'));
}
setting(key, fallback = '') {
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
return this.settings[key];
}
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;
return saveSettings(this.dirty()).then(this.onsaved.bind(this));
}
}

View File

@@ -1,120 +1,133 @@
import Page from '../../common/components/Page';
import Button from '../../common/components/Button';
import Switch from '../../common/components/Switch';
import EditCustomCssModal from './EditCustomCssModal';
import EditCustomHeaderModal from './EditCustomHeaderModal';
import EditCustomFooterModal from './EditCustomFooterModal';
import UploadImageButton from './UploadImageButton';
import AdminPage from './AdminPage';
import saveSettings from '../utils/saveSettings';
import withAttr from '../../common/utils/withAttr';
export default class AppearancePage extends AdminPage {
headerInfo() {
return {
className: 'AppearancePage',
icon: 'fas fa-paint-brush',
title: app.translator.trans('core.admin.appearance.title'),
description: app.translator.trans('core.admin.appearance.description'),
};
export default class AppearancePage extends Page {
oninit(vnode) {
super.oninit(vnode);
this.primaryColor = m.stream(app.data.settings.theme_primary_color);
this.secondaryColor = m.stream(app.data.settings.theme_secondary_color);
this.darkMode = m.stream(app.data.settings.theme_dark_mode);
this.coloredHeader = m.stream(app.data.settings.theme_colored_header);
}
content() {
return [
<div className="Form">
<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>
view() {
return (
<div className="AppearancePage">
<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="AppearancePage-colors-input">
{this.buildSettingComponent({
type: 'text',
setting: 'theme_primary_color',
placeholder: '#aaaaaa',
})}
{this.buildSettingComponent({
type: 'text',
setting: 'theme_secondary_color',
placeholder: '#aaaaaa',
})}
</div>
<div className="AppearancePage-colors-input">
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.primaryColor} />
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.secondaryColor} />
</div>
{this.buildSettingComponent({
type: 'switch',
setting: 'theme_dark_mode',
label: app.translator.trans('core.admin.appearance.dark_mode_label'),
})}
{Switch.component(
{
state: this.darkMode(),
onchange: this.darkMode,
},
app.translator.trans('core.admin.appearance.dark_mode_label')
)}
{this.buildSettingComponent({
type: 'switch',
setting: 'theme_colored_header',
label: app.translator.trans('core.admin.appearance.colored_header_label'),
})}
{Switch.component(
{
state: this.coloredHeader(),
onchange: this.coloredHeader,
},
app.translator.trans('core.admin.appearance.colored_header_label')
)}
{this.submitButton()}
</fieldset>
</div>,
{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" />
</fieldset>,
<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" />
</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" />
</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" />
</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',
onclick: () => app.modal.show(EditCustomHeaderModal),
},
app.translator.trans('core.admin.appearance.edit_header_button')
)}
</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',
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',
onclick: () => app.modal.show(EditCustomFooterModal),
},
app.translator.trans('core.admin.appearance.edit_footer_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',
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',
onclick: () => app.modal.show(EditCustomCssModal),
},
app.translator.trans('core.admin.appearance.edit_css_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',
onclick: () => app.modal.show(EditCustomCssModal),
},
app.translator.trans('core.admin.appearance.edit_css_button')
)}
</fieldset>
</div>
</div>
);
}
onsaved() {
window.location.reload();
}
saveSettings(e) {
onsubmit(e) {
e.preventDefault();
const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
if (!hex.test(this.settings['theme_primary_color']()) || !hex.test(this.settings['theme_secondary_color']())) {
if (!hex.test(this.primaryColor()) || !hex.test(this.secondaryColor())) {
alert(app.translator.trans('core.admin.appearance.enter_hex_message'));
return;
}
super.saveSettings(e);
this.loading = true;
saveSettings({
theme_primary_color: this.primaryColor(),
theme_secondary_color: this.secondaryColor(),
theme_dark_mode: this.darkMode(),
theme_colored_header: this.coloredHeader(),
}).then(() => window.location.reload());
}
}

View File

@@ -1,11 +1,33 @@
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 saveSettings from '../utils/saveSettings';
import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import Switch from '../../common/components/Switch';
import withAttr from '../../common/utils/withAttr';
export default class BasicsPage extends AdminPage {
export default class BasicsPage extends Page {
oninit(vnode) {
super.oninit(vnode);
this.loading = false;
this.fields = [
'forum_title',
'forum_description',
'default_locale',
'show_language_selector',
'default_route',
'welcome_title',
'welcome_message',
'display_name_driver',
];
this.values = {};
const settings = app.data.settings;
this.fields.forEach((key) => (this.values[key] = m.stream(settings[key])));
this.localeOptions = {};
const locales = app.data.locales;
for (const i in locales) {
@@ -18,101 +40,125 @@ export default class BasicsPage extends AdminPage {
this.displayNameOptions[identifier] = identifier;
}, this);
this.slugDriverOptions = {};
Object.keys(app.data.slugDrivers).forEach((model) => {
this.slugDriverOptions[model] = {};
if (!this.values.display_name_driver() && displayNameDrivers.includes('username')) this.values.display_name_driver('username');
app.data.slugDrivers[model].forEach((option) => {
this.slugDriverOptions[model][option] = option;
});
});
if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1);
}
headerInfo() {
return {
className: 'BasicsPage',
icon: 'fas fa-pencil-alt',
title: app.translator.trans('core.admin.basics.title'),
description: app.translator.trans('core.admin.basics.description'),
};
}
view() {
return (
<div className="BasicsPage">
<div className="container">
<form onsubmit={this.onsubmit.bind(this)}>
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.forum_title_heading'),
},
[<input className="FormControl" bidi={this.values.forum_title} />]
)}
content() {
return [
<div className="Form">
{this.buildSettingComponent({
type: 'text',
setting: 'forum_title',
label: app.translator.trans('core.admin.basics.forum_title_heading'),
})}
{this.buildSettingComponent({
type: 'text',
setting: 'forum_description',
label: app.translator.trans('core.admin.basics.forum_description_heading'),
help: app.translator.trans('core.admin.basics.forum_description_text'),
})}
{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
? [
this.buildSettingComponent({
type: 'select',
setting: 'default_locale',
options: this.localeOptions,
label: app.translator.trans('core.admin.basics.default_language_heading'),
}),
this.buildSettingComponent({
type: 'switch',
setting: 'show_language_selector',
label: app.translator.trans('core.admin.basics.show_language_selector_label'),
}),
]
: ''}
{Object.keys(this.localeOptions).length > 1
? 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 className="BasicsPage-homePage Form-group" label={app.translator.trans('core.admin.basics.home_page_heading')}>
<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} bidi={this.setting('default_route')} />
{label}
</label>
))}
</FieldSet>
{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>
)),
]
)}
<div className="Form-group BasicsPage-welcomeBanner-input">
<label>{app.translator.trans('core.admin.basics.welcome_banner_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>
<input type="text" className="FormControl" bidi={this.setting('welcome_title')} />
<textarea className="FormControl" bidi={this.setting('welcome_message')} />
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
className: 'BasicsPage-welcomeBanner',
},
[
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>,
<div className="BasicsPage-welcomeBanner-input">
<input className="FormControl" bidi={this.values.welcome_title} />
<textarea className="FormControl" bidi={this.values.welcome_message} />
</div>,
]
)}
{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>
);
}
{Object.keys(this.displayNameOptions).length > 1
? this.buildSettingComponent({
type: 'select',
setting: 'display_name_driver',
options: this.displayNameOptions,
label: app.translator.trans('core.admin.basics.display_name_heading'),
help: app.translator.trans('core.admin.basics.display_name_text'),
})
: ''}
{Object.keys(this.slugDriverOptions).map((model) => {
const options = this.slugDriverOptions[model];
if (Object.keys(options).length > 1) {
return this.buildSettingComponent({
type: 'select',
setting: `slug_driver_${model}`,
options,
label: app.translator.trans('core.admin.basics.slug_driver_heading', { model }),
help: app.translator.trans('core.admin.basics.slug_driver_text', { model }),
});
}
})}
{this.submitButton()}
</div>,
];
changed() {
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
}
/**
@@ -132,4 +178,29 @@ export default class BasicsPage extends AdminPage {
return items;
}
onsubmit(e) {
e.preventDefault();
if (this.loading) return;
this.loading = true;
app.alerts.dismiss(this.successAlert);
const settings = {};
this.fields.forEach((key) => (settings[key] = this.values[key]()));
saveSettings(settings)
.then(() => {
this.successAlert = app.alerts.show(app.translator.trans('core.admin.basics.saved_message'), {
type: 'success',
});
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
}

View File

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

View File

@@ -4,7 +4,7 @@ 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';
import withAttr from '../../common/utils/withAttr';
/**
* The `EditGroupModal` component shows a modal dialog which allows the user
@@ -16,11 +16,11 @@ export default class EditGroupModal extends Modal {
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);
this.nameSingular = m.stream(this.group.nameSingular() || '');
this.namePlural = m.stream(this.group.namePlural() || '');
this.icon = m.stream(this.group.icon() || '');
this.color = m.stream(this.group.color() || '');
this.isHidden = m.stream(this.group.isHidden() || false);
}
className() {

View File

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

@@ -1,248 +0,0 @@
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
import LinkButton from '../../common/components/LinkButton';
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 LoadingModal from './LoadingModal';
import ExtensionPermissionGrid from './ExtensionPermissionGrid';
import isExtensionEnabled from '../utils/isExtensionEnabled';
import AdminPage from './AdminPage';
export default class ExtensionPage extends AdminPage {
oninit(vnode) {
super.oninit(vnode);
this.extension = app.data.extensions[this.attrs.id];
this.changingState = false;
this.infoFields = {
discuss: 'fas fa-comment-alt',
documentation: 'fas fa-book',
support: 'fas fa-life-ring',
website: 'fas fa-link',
donate: 'fas fa-donate',
source: 'fas fa-code',
};
if (!this.extension) {
return m.route.set(app.route('dashboard'));
}
}
className() {
if (!this.extension) return '';
return this.extension.id + '-Page';
}
view() {
if (!this.extension) return null;
return (
<div className={'ExtensionPage ' + this.className()}>
{this.header()}
{!this.isEnabled() ? (
<div className="container">
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h3>
</div>
) : (
<div className="ExtensionPage-body">{this.sections().toArray()}</div>
)}
</div>
);
}
header() {
const isEnabled = this.isEnabled();
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.changingState ? !isEnabled : isEnabled}
loading={this.changingState}
onchange={this.toggle.bind(this, this.extension.id)}
>
{isEnabled ? 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 })
) : (
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h3>
)}
</div>
</div>,
]);
return items;
}
content() {
const settings = app.extensionData.getSettings(this.extension.id);
return (
<div className="ExtensionPage-settings">
<div className="container">
{settings ? (
<div className="Form">
{settings.map(this.buildSettingComponent.bind(this))}
<div className="Form-group">{this.submitButton()}</div>
</div>
) : (
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3>
)}
</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();
const links = this.extension.links;
if (links.authors.length) {
let authors = [];
links.authors.map((author) => {
authors.push(
<Link href={author.link} external={true} target="_blank">
{author.name}
</Link>
);
});
items.add('authors', [icon('fas fa-user'), <span>{punctuateSeries(authors)}</span>]);
}
Object.keys(this.infoFields).map((field) => {
if (links[field]) {
items.add(
field,
<LinkButton href={links[field]} icon={this.infoFields[field]} external={true} target="_blank">
{app.translator.trans(`core.admin.extension.info_links.${field}`)}
</LinkButton>
);
}
});
return items;
}
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);
}
isEnabled() {
return isExtensionEnabled(this.extension.id);
}
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.
this.changingState = false;
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

@@ -1,53 +0,0 @@
import PermissionGrid from './PermissionGrid';
import Button from '../../common/components/Button';
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();
}
scopeControlItems() {
const items = new ItemList();
items.add(
'configureScopes',
<Button className="Button Button--text" onclick={() => m.route.set(app.route('permissions'))}>
{app.translator.trans('core.admin.extension.configure_scopes')}
</Button>
);
return items;
}
}

View File

@@ -0,0 +1,134 @@
import Page from '../../common/components/Page';
import Button from '../../common/components/Button';
import Dropdown from '../../common/components/Dropdown';
import AddExtensionModal from './AddExtensionModal';
import LoadingModal from './LoadingModal';
import ItemList from '../../common/utils/ItemList';
import icon from '../../common/helpers/icon';
export default class ExtensionsPage extends Page {
view() {
return (
<div className="ExtensionsPage">
<div className="ExtensionsPage-header">
<div className="container">
{Button.component(
{
icon: 'fas fa-plus',
className: 'Button Button--primary',
onclick: () => app.modal.show(AddExtensionModal),
},
app.translator.trans('core.admin.extensions.add_button')
)}
</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',
onclick: app.extensionSettings[name],
},
app.translator.trans('core.admin.extensions.settings_button')
)
);
}
if (!enabled) {
items.add(
'uninstall',
Button.component(
{
icon: 'far fa-trash-alt',
onclick: () => {
app
.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
method: 'DELETE',
})
.then(() => window.location.reload());
app.modal.show(LoadingModal);
},
},
app.translator.trans('core.admin.extensions.uninstall_button')
)
);
}
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',
body: { enabled: !enabled },
})
.then(() => {
if (!enabled) localStorage.setItem('enabledExtension', id);
window.location.reload();
});
app.modal.show(LoadingModal);
}
}

View File

@@ -1,51 +0,0 @@
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 {
oninit(vnode) {
super.oninit(vnode);
this.categorizedExtensions = getCategorizedExtensions();
}
className() {
return 'ExtensionsWidget';
}
content() {
const categories = app.extensionCategories;
return (
<div className="ExtensionsWidget-list">
{Object.keys(categories).map((category) => (this.categorizedExtensions[category] ? this.extensionCategory(category) : ''))}
</div>
);
}
extensionCategory(category) {
return (
<div className="ExtensionList-Category">
<h4 className="ExtensionList-Label">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>
<ul className="ExtensionList">{this.categorizedExtensions[category].map((extension) => this.extensionWidget(extension))}</ul>
</div>
);
}
extensionWidget(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>
);
}
}

View File

@@ -1,5 +1,4 @@
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';
@@ -20,13 +19,6 @@ 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,31 +1,32 @@
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 AdminPage from './AdminPage';
import saveSettings from '../utils/saveSettings';
import withAttr from '../../common/utils/withAttr';
export default class MailPage extends AdminPage {
export default class MailPage extends Page {
oninit(vnode) {
super.oninit(vnode);
this.saving = false;
this.sendingTest = false;
this.refresh();
}
headerInfo() {
return {
className: 'MailPage',
icon: 'fas fa-envelope',
title: app.translator.trans('core.admin.email.title'),
description: app.translator.trans('core.admin.email.description'),
};
}
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.stream(settings[key])));
app
.request({
method: 'GET',
@@ -36,78 +37,150 @@ export default class MailPage extends AdminPage {
this.status.sending = response['data']['attributes']['sending'];
this.status.errors = response['data']['attributes']['errors'];
for (const driver in this.driverFields) {
for (const field in this.driverFields[driver]) {
this.fields.push(field);
this.values[field] = m.stream(settings[field]);
}
}
this.loading = false;
m.redraw();
});
}
content() {
if (this.loading) {
return <LoadingIndicator />;
view() {
if (this.loading || this.saving) {
return (
<div className="MailPage">
<div className="container">
<LoadingIndicator />
</div>
</div>
);
}
const fields = this.driverFields[this.setting('mail_driver')()];
const fields = this.driverFields[this.values.mail_driver()];
const fieldKeys = Object.keys(fields);
return (
<div className="Form">
{this.buildSettingComponent({
type: 'text',
setting: 'mail_from',
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
})}
{this.buildSettingComponent({
type: 'select',
setting: 'mail_driver',
options: Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.email.driver_heading'),
className: 'MailPage-MailSettings',
})}
{this.status.sending ||
Alert.component(
{
dismissible: false,
},
app.translator.trans('core.admin.email.not_sending_message')
)}
<div className="MailPage">
<div className="container">
<form onsubmit={this.onsubmit.bind(this)}>
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
<div className="helpText">{app.translator.trans('core.admin.email.text')}</div>
{fieldKeys.length > 0 && (
<FieldSet label={app.translator.trans(`core.admin.email.${this.setting('mail_driver')()}_heading`)} className="MailPage-MailSettings">
<div className="MailPage-MailSettings-input">
{fieldKeys.map((field) => {
const fieldInfo = fields[field];
{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.from_label')}
<input className="FormControl" bidi={this.values.mail_from} />
</label>
</div>,
]
)}
return [
this.buildSettingComponent({
type: typeof this.setting(field)() === 'string' ? 'text' : 'select',
label: app.translator.trans(`core.admin.email.${field}_label`),
setting: field,
options: fieldInfo,
}),
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
];
})}
</div>
</FieldSet>
)}
{this.submitButton()}
{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.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 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.isChanged(),
onclick: () => this.sendTestEmail(),
},
app.translator.trans('core.admin.email.send_test_mail_button')
)}
</FieldSet>
{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')
),
]
)}
</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]);
}
sendTestEmail() {
if (this.saving || this.sendingTest) return;
@@ -121,7 +194,9 @@ export default class MailPage extends AdminPage {
})
.then((response) => {
this.sendingTest = false;
this.testEmailSuccessAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.email.send_test_mail_success'));
this.testEmailSuccessAlert = app.alerts.show(app.translator.trans('core.admin.email.send_test_mail_success'), {
type: 'success',
});
})
.catch((error) => {
this.sendingTest = false;
@@ -130,7 +205,28 @@ export default class MailPage extends AdminPage {
});
}
saveSettings(e) {
super.saveSettings(e).then(this.refresh());
onsubmit(e) {
e.preventDefault();
if (this.saving || this.sendingTest) return;
this.saving = true;
app.alerts.dismiss(this.successAlert);
const settings = {};
this.fields.forEach((key) => (settings[key] = this.values[key]()));
saveSettings(settings)
.then(() => {
this.successAlert = app.alerts.show(app.translator.trans('core.admin.basics.saved_message'), {
type: 'success',
});
})
.catch(() => {})
.then(() => {
this.saving = false;
this.refresh();
});
}
}

View File

@@ -6,6 +6,12 @@ import ItemList from '../../common/utils/ItemList';
import icon from '../../common/helpers/icon';
export default class PermissionGrid extends Component {
oninit(vnode) {
super.oninit(vnode);
this.permissions = this.permissionItems().toArray();
}
view() {
const scopes = this.scopeItems().toArray();
@@ -29,27 +35,25 @@ export default class PermissionGrid extends Component {
<th>{this.scopeControlItems().toArray()}</th>
</tr>
</thead>
{this.permissionItems()
.toArray()
.map((section) => (
<tbody>
<tr className="PermissionGrid-section">
<th>{section.label}</th>
{permissionCells(section)}
{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 />
</tr>
{section.children.map((child) => (
<tr className="PermissionGrid-child">
<th>
{icon(child.icon)}
{child.label}
</th>
{permissionCells(child)}
<td />
</tr>
))}
</tbody>
))}
))}
</tbody>
))}
</table>
);
}
@@ -154,8 +158,6 @@ export default class PermissionGrid extends Component {
permission: 'user.viewLastSeenAt',
});
items.merge(app.extensionData.getAllExtensionPermissions('view'));
return items;
}
@@ -196,8 +198,6 @@ export default class PermissionGrid extends Component {
90
);
items.merge(app.extensionData.getAllExtensionPermissions('start'));
return items;
}
@@ -238,8 +238,6 @@ export default class PermissionGrid extends Component {
90
);
items.merge(app.extensionData.getAllExtensionPermissions('reply'));
return items;
}
@@ -326,38 +324,16 @@ export default class PermissionGrid extends Component {
60
);
items.add(
'userEditCredentials',
{
icon: 'fas fa-user-cog',
label: app.translator.trans('core.admin.permissions.edit_users_credentials_label'),
permission: 'user.editCredentials',
},
60
);
items.add(
'userEditGroups',
{
icon: 'fas fa-users-cog',
label: app.translator.trans('core.admin.permissions.edit_users_groups_label'),
permission: 'user.editGroups',
},
60
);
items.add(
'userEdit',
{
icon: 'fas fa-address-card',
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;
}

View File

@@ -1,43 +1,40 @@
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 AdminPage from './AdminPage';
export default class PermissionsPage extends AdminPage {
headerInfo() {
return {
className: 'PermissionsPage',
icon: 'fas fa-key',
title: app.translator.trans('core.admin.permissions.title'),
description: app.translator.trans('core.admin.permissions.description'),
};
}
content() {
return [
<div className="PermissionsPage-groups">
{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,
})}
<span className="Group-name">{group.namePlural()}</span>
export default class PermissionsPage extends Page {
view() {
return (
<div className="PermissionsPage">
<div className="PermissionsPage-groups">
<div className="container">
{app.store
.all('groups')
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.map((group) => (
<button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}>
{GroupBadge.component({
group,
className: 'Group-icon',
label: null,
})}
<span className="Group-name">{group.namePlural()}</span>
</button>
))}
<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>
))}
<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>
<div className="PermissionsPage-permissions">{PermissionGrid.component()}</div>,
];
<div className="PermissionsPage-permissions">
<div className="container">{PermissionGrid.component()}</div>
</div>
</div>
);
}
}

View File

@@ -1,6 +1,5 @@
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 {
@@ -36,7 +35,7 @@ export default class SettingsModal extends Modal {
}
setting(key, fallback = '') {
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
this.settings[key] = this.settings[key] || m.stream(app.data.settings[key] || fallback);
return this.settings[key];
}

View File

@@ -1,13 +1,17 @@
import app from './app';
import AdminApplication from './AdminApplication';
const app = new AdminApplication();
// Backwards compatibility
window.app = app;
export { app };
// Export public API
// Export compat API
import compatObj from './compat';
import proxifyCompat from '../common/utils/proxifyCompat';
import compat from './compat';
compatObj.app = app;
compat.app = app;
export const compat = proxifyCompat(compatObj, 'admin');
export { compat };

View File

@@ -1,19 +0,0 @@
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,9 +2,8 @@ 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.
@@ -17,7 +16,7 @@ export default function (app) {
basics: { path: '/basics', component: BasicsPage },
permissions: { path: '/permissions', component: PermissionsPage },
appearance: { path: '/appearance', component: AppearancePage },
extensions: { path: '/extensions', component: ExtensionsPage },
mail: { path: '/mail', component: MailPage },
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },
};
}

View File

@@ -1,177 +0,0 @@
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
*
* It takes either a settings object or a callback.
*
* @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();
// Callbacks can be passed in instead of settings to display custom content.
// By default, they will be added with the `null` key, since they don't have a `.setting` attr.
// To support multiple such items for one extension, we assign a random ID.
// 36 is arbitrary length, but makes collisions very unlikely.
if (typeof content === 'function') {
content.setting = Math.random().toString(36);
}
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

@@ -1,25 +0,0 @@
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.feature = extensions.feature || [];
extensions.feature.push(extension);
}
});
return extensions;
}

View File

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

View File

@@ -159,8 +159,6 @@ export default class Application {
title = '';
titleCount = 0;
initialRoute;
load(payload) {
this.data = payload;
this.translator.locale = payload.locale;
@@ -176,8 +174,6 @@ export default class Application {
this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
this.mount();
this.initialRoute = window.location.href;
}
bootExtensions(extensions) {
@@ -202,19 +198,13 @@ export default class Application {
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. 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) => {
// down.
new ScrollListener((top) => {
const $app = $('#app');
const offset = $app.offset().top;
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
$('.App-header').toggleClass('navbar-fixed-top', top >= offset);
});
scrollListener.start();
scrollListener.update();
}).start();
$(() => {
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
@@ -230,8 +220,7 @@ export default class Application {
* @public
*/
preloadedApiDocument() {
// If the URL has changed, the preloaded Api document is invalid.
if (this.data.apiDocument && window.location.href === this.initialRoute) {
if (this.data.apiDocument) {
const results = this.store.pushPayload(this.data.apiDocument);
this.data.apiDocument = null;
@@ -275,7 +264,7 @@ export default class Application {
updateTitle() {
const count = this.titleCount ? `(${this.titleCount}) ` : '';
const pageTitleWithSeparator = this.title && m.route.get() !== this.forum.attribute('basePath') + '/' ? this.title + ' - ' : '';
const pageTitleWithSeparator = this.title && m.route.get() !== '/' ? this.title + ' - ' : '';
const title = this.forum.attribute('title');
document.title = count + pageTitleWithSeparator + title;
}
@@ -283,7 +272,7 @@ export default class Application {
/**
* Make an AJAX request, handling any low-level errors that may occur.
*
* @see https://mithril.js.org/request.html
* @see https://lhorie.github.io/mithril/mithril.request.html
* @param {Object} options
* @return {Promise}
* @public
@@ -415,7 +404,7 @@ export default class Application {
console.groupEnd();
}
this.requestErrorAlert = this.alerts.show(error.alert, error.alert.content);
this.requestErrorAlert = this.alerts.show(error.alert.content, error.alert);
}
return Promise.reject(error);

View File

@@ -1,6 +1,10 @@
import * as Mithril from 'mithril';
export interface ComponentAttrs extends Mithril.Attributes {}
export type ComponentAttrs = {
className?: string;
[key: string]: any;
};
/**
* The `Component` class defines a user interface 'building block'. A component
@@ -29,7 +33,7 @@ export interface ComponentAttrs extends Mithril.Attributes {}
*
* @see https://mithril.js.org/components.html
*/
export default abstract class Component<T extends ComponentAttrs = ComponentAttrs> implements Mithril.ClassComponent<T> {
export default abstract class Component<T extends ComponentAttrs = any> implements Mithril.ClassComponent<T> {
/**
* The root DOM element for the component.
*/
@@ -77,12 +81,12 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
* containing all of the `li` elements inside the DOM element of this
* component.
*
* @param [selector] a jQuery-compatible selector string
* @returns the jQuery object for the DOM node
* @param {String} [selector] a jQuery-compatible selector string
* @returns {jQuery} the jQuery object for the DOM node
* @final
*/
protected $(selector: string): JQuery {
const $element = $(this.element) as JQuery<HTMLElement>;
protected $(selector) {
const $element = $(this.element);
return selector ? $element.find(selector) : $element;
}
@@ -94,7 +98,7 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
*/
static component(attrs = {}, children = null): Mithril.Vnode {
const componentAttrs = Object.assign({}, attrs) as Record<string, unknown>;
const componentAttrs = Object.assign({}, attrs);
return m(this as any, componentAttrs, children);
}

View File

@@ -15,7 +15,7 @@ import * as Mithril from '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 {
export default abstract class Fragment implements Mithril.ClassComponent {
/**
* The root DOM element for the fragment.
*/
@@ -52,7 +52,7 @@ export default abstract class Fragment {
*
* @final
*/
public render(): Mithril.Vnode<Mithril.Attributes, this> {
public render(): Mithril.Vnode {
const vdom = this.view();
vdom.attrs = vdom.attrs || {};
@@ -61,14 +61,15 @@ export default abstract class Fragment {
vdom.attrs.oncreate = (vnode) => {
this.element = vnode.dom;
if (originalOnCreate) originalOnCreate.apply(this, [vnode]);
if (this.oncreate) this.oncreate.apply(this, vnode);
if (originalOnCreate) originalOnCreate.apply(this, vnode);
};
return vdom;
}
/**
* Creates a view out of virtual elements.
* @inheritdoc
*/
abstract view(): Mithril.Vnode<Mithril.Attributes, this>;
abstract view();
}

View File

@@ -1,7 +1,6 @@
import * as extend from './extend';
import Session from './Session';
import Store from './Store';
import BasicEditorDriver from './utils/BasicEditorDriver';
import evented from './utils/evented';
import liveHumanTimes from './utils/liveHumanTimes';
import ItemList from './utils/ItemList';
@@ -13,7 +12,6 @@ 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';
@@ -21,7 +19,6 @@ import ScrollListener from './utils/ScrollListener';
import stringToColor from './utils/stringToColor';
import subclassOf from './utils/subclassOf';
import patchMithril from './utils/patchMithril';
import proxifyCompat from './utils/proxifyCompat';
import classList from './utils/classList';
import extractText from './utils/extractText';
import formatNumber from './utils/formatNumber';
@@ -49,7 +46,6 @@ 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';
@@ -57,8 +53,6 @@ import ModalManager from './components/ModalManager';
import Button from './components/Button';
import Modal from './components/Modal';
import GroupBadge from './components/GroupBadge';
import TextEditor from './components/TextEditor';
import TextEditorButton from './components/TextEditorButton';
import Model from './Model';
import Application from './Application';
import fullTime from './helpers/fullTime';
@@ -71,13 +65,11 @@ 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,
'utils/BasicEditorDriver': BasicEditorDriver,
'utils/evented': evented,
'utils/liveHumanTimes': liveHumanTimes,
'utils/ItemList': ItemList,
@@ -93,11 +85,9 @@ export default {
'utils/extract': extract,
'utils/ScrollListener': ScrollListener,
'utils/stringToColor': stringToColor,
'utils/Stream': Stream,
'utils/subclassOf': subclassOf,
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
'utils/patchMithril': patchMithril,
'utils/proxifyCompat': proxifyCompat,
'utils/classList': classList,
'utils/extractText': extractText,
'utils/formatNumber': formatNumber,
@@ -126,7 +116,6 @@ export default {
'components/Select': Select,
'components/Navigation': Navigation,
'components/Alert': Alert,
'components/Link': Link,
'components/LinkButton': LinkButton,
'components/Checkbox': Checkbox,
'components/SelectDropdown': SelectDropdown,
@@ -134,8 +123,6 @@ export default {
'components/Button': Button,
'components/Modal': Modal,
'components/GroupBadge': GroupBadge,
'components/TextEditor': TextEditor,
'components/TextEditorButton': TextEditorButton,
Model: Model,
Application: Application,
'helpers/fullTime': fullTime,
@@ -147,5 +134,4 @@ export default {
'helpers/username': username,
'helpers/userOnline': userOnline,
'helpers/listItems': listItems,
'resolvers/DefaultResolver': DefaultResolver,
};

View File

@@ -1,33 +1,31 @@
import Component, { ComponentAttrs } from '../Component';
import Component 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.
*
* ### Attrs
*
* - `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 attrs will be assigned as attributes on the DOM element.
*/
export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<T> {
view(vnode: Mithril.Vnode) {
export default class Alert extends Component {
view(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;
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

View File

@@ -35,11 +35,6 @@ export default class Button extends Component {
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 && vnode.children) {
attrs.title = extractText(vnode.children);

View File

@@ -13,7 +13,6 @@ import listItems from '../helpers/listItems';
* - `icon` The name of an icon to show in the dropdown toggle button.
* - `caretIcon` The name of an icon to show on the right of the button.
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
* - `accessibleToggleLabel` The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'.
* - `onhide`
* - `onshow`
*
@@ -26,7 +25,6 @@ export default class Dropdown extends Component {
attrs.menuClassName = attrs.menuClassName || '';
attrs.label = attrs.label || '';
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label');
}
oninit(vnode) {
@@ -94,13 +92,7 @@ export default class Dropdown extends Component {
*/
getButton(children) {
return (
<button
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
aria-haspopup="menu"
aria-label={this.attrs.accessibleToggleLabel}
data-toggle="dropdown"
onclick={this.attrs.onclick}
>
<button className={'Dropdown-toggle ' + this.attrs.buttonClassName} data-toggle="dropdown" onclick={this.attrs.onclick}>
{this.getButtonContent(children)}
</button>
);

View File

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

View File

@@ -1,5 +1,4 @@
import Button from './Button';
import Link from './Link';
/**
* The `LinkButton` component defines a `Button` which links to a route.
@@ -12,20 +11,18 @@ import Link from './Link';
* 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 initAttrs(attrs) {
super.initAttrs(attrs);
attrs.active = this.isActive(attrs);
if (attrs.force === undefined) attrs.force = true;
}
view(vnode) {
const vdom = super.view(vnode);
vdom.tag = Link;
vdom.tag = m.route.Link;
vdom.attrs.active = String(vdom.attrs.active);
return vdom;

View File

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

View File

@@ -1,61 +0,0 @@
import Component, { ComponentAttrs } from '../Component';
import classList from '../utils/classList';
export interface LoadingIndicatorAttrs extends ComponentAttrs {
/**
* Custom classes fro the loading indicator's container.
*/
className?: string;
/**
* Custom classes for the loading indicator's container.
*/
containerClassName?: string;
/**
* Optional size to specify for the loading indicator.
*/
size?: 'large' | 'medium' | 'small';
/**
* Optional attributes to apply to the loading indicator's container.
*/
containerAttrs?: Partial<ComponentAttrs>;
}
/**
* The `LoadingIndicator` component displays a simple CSS-based loading spinner.
*
* To set a custom color, use the CSS `color` property.
*
* To increase spacing around the spinner, use the CSS `height` property on the
* spinner's **container**.
*
* To apply a custom size to the loading indicator, set the `--size` and
* `--thickness` custom properties on the loading indicator itself.
*
* If you really want to change how this looks as part of your custom theme,
* you can override the `border-radius` and `border` then set either a
* background image, or use `content: "\<glyph>"` (e.g. `content: "\f1ce"`)
* and `font-family: 'Font Awesome 5 Free'` to set an FA icon if you'd rather.
*
* ### Attrs
*
* - `containerClassName` Class name(s) to apply to the indicator's parent
* - `className` Class name(s) to apply to the indicator itself
* - `size` Size of the loading indicator
* - `containerAttrs` Optional attrs to be applied to the container DOM element
*
* All other attrs will be assigned as attributes on the DOM element.
*/
export default class LoadingIndicator extends Component<LoadingIndicatorAttrs> {
view() {
const { size, ...attrs } = this.attrs;
attrs.className = classList({ LoadingIndicator: true, [attrs.className || '']: true });
attrs.containerClassName = classList({ 'LoadingIndicator-container': true, [attrs.containerClassName || '']: true });
return (
<div {...attrs.containerAttrs} data-size={size} className={attrs.containerClassName}>
<div {...attrs}></div>
</div>
);
}
}

View File

@@ -24,20 +24,11 @@ export default class Modal extends Component {
oncreate(vnode) {
super.oncreate(vnode);
this.attrs.animateShow(() => this.onready());
this.attrs.onshow(() => 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));
}
onremove() {
this.attrs.onhide();
}
view() {
@@ -112,11 +103,13 @@ export default class Modal extends Component {
this.$('form').find('input, select, textarea').first().focus().select();
}
onhide() {}
/**
* Hide the modal.
*/
hide() {
this.attrs.state.close();
this.attrs.onhide();
}
/**

View File

@@ -11,14 +11,7 @@ export default class ModalManager extends Component {
return (
<div className="ModalManager modal fade">
{modal
? modal.componentClass.component({
...modal.attrs,
animateShow: this.animateShow.bind(this),
animateHide: this.animateHide.bind(this),
state: this.attrs.state,
})
: ''}
{modal ? modal.componentClass.component({ ...modal.attrs, onshow: this.animateShow.bind(this), onhide: this.animateHide.bind(this) }) : ''}
</div>
);
}
@@ -35,14 +28,6 @@ export default class ModalManager extends Component {
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.$()
.one('shown.bs.modal', readyCallback)
.modal({

View File

@@ -11,7 +11,9 @@ export default class Page extends Component {
super.oninit(vnode);
app.previous = app.current;
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
app.current = new PageState(this.constructor);
this.onNewRoute();
app.drawer.hide();
app.modal.close();
@@ -22,20 +24,16 @@ export default class Page extends Component {
* @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;
/**
* A collections of actions to run when the route changes.
* This is extracted here, and not hardcoded in oninit, as oninit is not called
* when a different route is handled by the same component, but we still need to
* adjust the current route name.
*/
onNewRoute() {
app.current.set('routeName', this.attrs.routeName);
}
oncreate(vnode) {
@@ -44,14 +42,6 @@ export default class Page extends Component {
if (this.bodyClass) {
$('#app').addClass(this.bodyClass);
}
if (this.scrollTopOnCreate) {
$(window).scrollTop(0);
}
if ('scrollRestoration' in history) {
history.scrollRestoration = this.useBrowserScrollRestoration ? 'auto' : 'manual';
}
}
onremove() {

View File

@@ -12,9 +12,6 @@ import icon from '../helpers/icon';
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);
}

View File

@@ -24,12 +24,7 @@ export default class SplitDropdown extends Dropdown {
return [
Button.component(buttonAttrs, firstChild.children),
<button
className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName}
aria-haspopup="menu"
aria-label={this.attrs.accessibleToggleLabel}
data-toggle="dropdown"
>
<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>,

View File

@@ -1,28 +1,26 @@
import * as Mithril from 'mithril';
import User from '../models/User';
/**
* The `avatar` helper displays a user's avatar.
*
* @param user
* @param attrs Attributes to apply to the avatar element
* @param {User} user
* @param {Object} attrs Attributes to apply to the avatar element
* @return {Object}
*/
export default function avatar(user: User, attrs: Object = {}): Mithril.Vnode {
export default function avatar(user, attrs = {}) {
attrs.className = 'Avatar ' + (attrs.className || '');
let content: string = '';
let content = '';
// If the `title` attribute is set to null or false, we don't want to give the
// avatar a title. On the other hand, if it hasn't been given at all, we can
// safely default it to the user's username.
const hasTitle: boolean | string = attrs.title === 'undefined' || attrs.title;
const hasTitle = attrs.title === 'undefined' || attrs.title;
if (!hasTitle) delete attrs.title;
// If a user has been passed, then we will set up an avatar using their
// uploaded image, or the first letter of their username if they haven't
// uploaded one.
if (user) {
const username: string = user.displayName() || '?';
const avatarUrl: string = user.avatarUrl();
const username = user.displayName() || '?';
const avatarUrl = user.avatarUrl();
if (hasTitle) attrs.title = attrs.title || username;

View File

@@ -1,11 +1,11 @@
import dayjs from 'dayjs';
import * as Mithril from 'mithril';
/**
* The `fullTime` helper displays a formatted time string wrapped in a <time>
* tag.
*
* @param {Date} time
* @return {Object}
*/
export default function fullTime(time: Date): Mithril.Vnode {
export default function fullTime(time) {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -1,16 +1,16 @@
import * as Mithril from 'mithril';
import { truncate } from '../utils/string';
/**
* The `highlight` helper searches for a word phrase in a string, and wraps
* matches with the <mark> tag.
*
* @param string The string to highlight.
* @param phrase The word or words to highlight.
* @param [length] The number of characters to truncate the string to.
* @param {String} string The string to highlight.
* @param {String|RegExp} phrase The word or words to highlight.
* @param {Integer} [length] The number of characters to truncate the string to.
* The string will be truncated surrounding the first match.
* @return {Object}
*/
export default function highlight(string: string, phrase: string | RegExp, length?: number): Mithril.Vnode<any, any> | string {
export default function highlight(string, phrase, length) {
if (!phrase && !length) return string;
// Convert the word phrase into a global regular expression (if it isn't

View File

@@ -1,13 +1,14 @@
import dayjs from 'dayjs';
import * as Mithril from 'mithril';
import humanTimeUtil from '../utils/humanTime';
/**
* The `humanTime` helper displays a time in a human-friendly time-ago format
* (e.g. '12 days ago'), wrapped in a <time> tag with other information about
* the time.
*
* @param {Date} time
* @return {Object}
*/
export default function humanTime(time: Date): Mithril.Vnode {
export default function humanTime(time) {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -0,0 +1,12 @@
/**
* The `icon` helper displays an icon.
*
* @param {String} fontClass The full icon class, prefix and the icons name.
* @param {Object} attrs Any other attributes to apply.
* @return {Object}
*/
export default function icon(fontClass, attrs = {}) {
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
return <i {...attrs} />;
}

View File

@@ -1,13 +0,0 @@
import * as Mithril from 'mithril';
/**
* The `icon` helper displays an icon.
*
* @param fontClass The full icon class, prefix and the icons name.
* @param attrs Any other attributes to apply.
*/
export default function icon(fontClass: string, attrs: Mithril.Attributes = {}): Mithril.Vnode {
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
return <i {...attrs} />;
}

View File

@@ -1,16 +1,15 @@
import * as Mithril from 'mithril';
import Separator from '../components/Separator';
import classList from '../utils/classList';
function isSeparator(item): boolean {
function isSeparator(item) {
return item.tag === Separator;
}
function withoutUnnecessarySeparators(items: Array<Mithril.Vnode>): Array<Mithril.Vnode> {
function withoutUnnecessarySeparators(items) {
const newItems = [];
let prevItem;
items.filter(Boolean).forEach((item: Mithril.Vnode, i: number) => {
items.filter(Boolean).forEach((item, i) => {
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
prevItem = item;
newItems.push(item);
@@ -23,11 +22,14 @@ function withoutUnnecessarySeparators(items: Array<Mithril.Vnode>): Array<Mithri
/**
* The `listItems` helper wraps a collection of components in <li> tags,
* stripping out any unnecessary `Separator` components.
*
* @param {*} items
* @return {Array}
*/
export default function listItems(items: Mithril.Vnode | Array<Mithril.Vnode>): Array<Mithril.Vnode> {
export default function listItems(items) {
if (!(items instanceof Array)) items = [items];
return withoutUnnecessarySeparators(items).map((item: Mithril.Vnode) => {
return withoutUnnecessarySeparators(items).map((item) => {
const isListItem = item.tag && item.tag.isListItem;
const active = item.tag && item.tag.isActive && item.tag.isActive(item.attrs);
const className = (item.attrs && item.attrs.itemClassName) || item.itemClassName;
@@ -38,7 +40,7 @@ export default function listItems(items: Mithril.Vnode | Array<Mithril.Vnode>):
item.key = item.attrs.key;
}
const node: Mithril.Vnode = isListItem ? (
const node = isListItem ? (
item
) : (
<li
@@ -49,6 +51,8 @@ export default function listItems(items: Mithril.Vnode | Array<Mithril.Vnode>):
</li>
);
node.state = node.state || {};
return node;
});
}

View File

@@ -1,11 +1,12 @@
import * as Mithril from 'mithril';
import User from '../models/User';
import icon from './icon';
/**
* The `useronline` helper displays a green circle if the user is online
*
* @param {User} user
* @return {Object}
*/
export default function userOnline(user: User): Mithril.Vnode {
export default function userOnline(user) {
if (user.lastSeenAt() && user.isOnline()) {
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
}

View File

@@ -1,11 +1,11 @@
import * as Mithril from 'mithril';
import User from '../models/User';
/**
* The `username` helper displays a user's username in a <span class="username">
* tag. If the user doesn't exist, the username will be displayed as [deleted].
*
* @param {User} user
* @return {Object}
*/
export default function username(user: User): Mithril.Vnode {
export default function username(user) {
const name = (user && user.displayName()) || app.translator.trans('core.lib.username.deleted_text');
return <span className="username">{name}</span>;

View File

@@ -1,8 +1,7 @@
// Expose jQuery, mithril and dayjs to the window browser object
import 'expose-loader?exposes=$,jQuery!jquery';
import 'expose-loader?exposes=m!mithril';
import 'expose-loader?exposes=dayjs!dayjs';
import 'expose-loader?$!expose-loader?jQuery!jquery';
import 'expose-loader?m!mithril';
import 'expose-loader?moment!expose-loader?dayjs!dayjs';
import 'expose-loader?m.bidi!m.attrs.bidi';
import 'bootstrap/js/affix';
import 'bootstrap/js/dropdown';
import 'bootstrap/js/modal';

View File

@@ -10,7 +10,6 @@ export default class User extends Model {}
Object.assign(User.prototype, {
username: Model.attribute('username'),
slug: Model.attribute('slug'),
displayName: Model.attribute('displayName'),
email: Model.attribute('email'),
isEmailConfirmed: Model.attribute('isEmailConfirmed'),
@@ -30,8 +29,6 @@ Object.assign(User.prototype, {
commentCount: Model.attribute('commentCount'),
canEdit: Model.attribute('canEdit'),
canEditCredentials: Model.attribute('canEditCredentials'),
canEditGroups: Model.attribute('canEditGroups'),
canDelete: Model.attribute('canDelete'),
avatarColor: null,

View File

@@ -1,41 +0,0 @@
import Mithril from 'mithril';
/**
* Generates a route resolver for a given component.
* In addition to regular route resolver functionality:
* - It provide the current route name as an attr
* - It sets a key on the component so a rerender will be triggered on route change.
*/
export default class DefaultResolver {
component: Mithril.Component;
routeName: string;
constructor(component, routeName) {
this.component = component;
this.routeName = routeName;
}
/**
* When a route change results in a changed key, a full page
* rerender occurs. This method can be overriden in subclasses
* to prevent rerenders on some route changes.
*/
makeKey() {
return this.routeName + JSON.stringify(m.route.param());
}
makeAttrs(vnode) {
return {
...vnode.attrs,
routeName: this.routeName,
};
}
onmatch(args, requestedPath, route) {
return this.component;
}
render(vnode) {
return [{ ...vnode, attrs: this.makeAttrs(vnode), key: this.makeKey() }];
}
}

View File

@@ -0,0 +1,50 @@
import Alert from '../components/Alert';
export default class AlertManagerState {
constructor() {
this.activeAlerts = {};
this.alertId = 0;
}
getActiveAlerts() {
return this.activeAlerts;
}
/**
* Show an Alert in the alerts area.
*/
show(children, attrs, componentClass = Alert) {
// Breaking Change Compliance Warning, Remove in Beta 15.
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
if (attrs === Alert || attrs instanceof Alert) {
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
}
// End Change Compliance Warning, Remove in Beta 15
this.activeAlerts[++this.alertId] = { children, attrs, componentClass };
m.redraw();
return this.alertId;
}
/**
* Dismiss an alert.
*/
dismiss(key) {
if (!key || !(key in this.activeAlerts)) return;
delete this.activeAlerts[key];
m.redraw();
}
/**
* Clear all alerts.
*
* @public
*/
clear() {
this.activeAlerts = {};
m.redraw();
}
}

View File

@@ -1,80 +0,0 @@
import Mithril from 'mithril';
import Alert, { AlertAttrs } from '../components/Alert';
/**
* Returned by `AlertManagerState.show`. Used to dismiss alerts.
*/
export type AlertIdentifier = number;
export interface AlertState {
componentClass: typeof Alert;
attrs: AlertAttrs;
children: Mithril.Children;
}
export default class AlertManagerState {
protected activeAlerts: { [id: number]: AlertState } = {};
protected alertId = 0;
getActiveAlerts() {
return this.activeAlerts;
}
/**
* Show an Alert in the alerts area.
*
* @returns The alert's ID, which can be used to dismiss the alert.
*/
show(children: Mithril.Children): AlertIdentifier;
show(attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
show(componentClass: Alert, attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
show(arg1: any, arg2?: any, arg3?: any) {
// Assigns variables as per the above signatures
let componentClass = Alert;
let attrs: AlertAttrs = {};
let children: Mithril.Children;
if (arguments.length == 1) {
children = arg1 as Mithril.Children;
} else if (arguments.length == 2) {
attrs = arg1 as AlertAttrs;
children = arg2 as Mithril.Children;
} else if (arguments.length == 3) {
componentClass = arg1 as typeof Alert;
attrs = arg2 as AlertAttrs;
children = arg3;
}
// Breaking Change Compliance Warning, Remove in Beta 15.
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
if (attrs === Alert || attrs instanceof Alert) {
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
}
// End Change Compliance Warning, Remove in Beta 15
this.activeAlerts[++this.alertId] = { children, attrs, componentClass };
m.redraw();
return this.alertId;
}
/**
* Dismiss an alert.
*/
dismiss(key: AlertIdentifier): void {
if (!key || !(key in this.activeAlerts)) return;
delete this.activeAlerts[key];
m.redraw();
}
/**
* Clear all alerts.
*/
clear(): void {
this.activeAlerts = {};
m.redraw();
}
}

View File

@@ -1,124 +0,0 @@
import getCaretCoordinates from 'textarea-caret';
import EditorDriverInterface, { EditorDriverParams } from './EditorDriverInterface';
export default class BasicEditorDriver implements EditorDriverInterface {
el: HTMLTextAreaElement;
constructor(dom: HTMLElement, params: EditorDriverParams) {
this.el = document.createElement('textarea');
this.build(dom, params);
}
build(dom: HTMLElement, params: EditorDriverParams) {
this.el.className = params.classNames.join(' ');
this.el.disabled = params.disabled;
this.el.placeholder = params.placeholder;
this.el.value = params.value;
const callInputListeners = (e) => {
params.inputListeners.forEach((listener) => {
listener();
});
e.redraw = false;
};
this.el.oninput = (e) => {
params.oninput(this.el.value);
callInputListeners(e);
};
this.el.onclick = callInputListeners;
this.el.onkeyup = callInputListeners;
this.el.addEventListener('keydown', function (e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
params.onsubmit();
}
});
dom.append(this.el);
}
protected setValue(value: string) {
$(this.el).val(value).trigger('input');
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
}
moveCursorTo(position: number) {
this.setSelectionRange(position, position);
}
getSelectionRange(): Array<number> {
return [this.el.selectionStart, this.el.selectionEnd];
}
getLastNChars(n: number): string {
const value = this.el.value;
return value.slice(Math.max(0, this.el.selectionStart - n), this.el.selectionStart);
}
insertAtCursor(text: string) {
this.insertAt(this.el.selectionStart, text);
}
insertAt(pos: number, text: string) {
this.insertBetween(pos, pos, text);
}
insertBetween(start: number, end: number, text: string) {
const value = this.el.value;
const before = value.slice(0, start);
const after = value.slice(end);
this.setValue(`${before}${text}${after}`);
// Move the textarea cursor to the end of the content we just inserted.
this.moveCursorTo(start + text.length);
}
replaceBeforeCursor(start: number, text: string) {
this.insertBetween(start, this.el.selectionStart, text);
}
protected setSelectionRange(start: number, end: number) {
this.el.setSelectionRange(start, end);
this.focus();
}
getCaretCoordinates(position: number) {
const relCoords = getCaretCoordinates(this.el, position);
return {
top: relCoords.top - this.el.scrollTop,
left: relCoords.left,
};
}
// DOM Interactions
/**
* Set the disabled status of the editor.
*/
disabled(disabled: boolean) {
this.el.disabled = disabled;
}
/**
* Focus on the editor.
*/
focus() {
this.el.focus();
}
/**
* Destroy the editor
*/
destroy() {
this.el.remove();
}
}

View File

@@ -31,24 +31,7 @@ export default class Drawer {
* @public
*/
hide() {
/**
* As part of hiding the drawer, this function also ensures that the drawer
* correctly animates out, while ensuring it is not part of the navigation
* tree while off-screen.
*
* More info: https://github.com/flarum/core/pull/2666#discussion_r595381014
*/
const $app = $('#app');
if (!$app.hasClass('drawerOpen')) return;
const $drawer = $('#drawer');
// Used to prevent `visibility: hidden` from breaking the exit animation
$drawer.css('visibility', 'visible').one('transitionend', () => $drawer.css('visibility', ''));
$app.removeClass('drawerOpen');
$('#app').removeClass('drawerOpen');
if (this.$backdrop) this.$backdrop.remove();
}

View File

@@ -1,105 +0,0 @@
export interface EditorDriverParams {
/**
* An array of HTML class names to apply to the editor's main DOM element.
*/
classNames: string[];
/**
* Whether the editor should be initially disabled.
*/
disabled: boolean;
/**
* An optional placeholder for the editor.
*/
placeholder: string;
/**
* An optional initial value for the editor.
*/
value: string;
/**
* This is separate from inputListeners since the full serialized content will be passed to it.
* It is considered private API, and should not be used/modified by extensions not implementing
* EditorDriverInterface.
*/
oninput: Function;
/**
* Each of these functions will be called on click, input, and keyup.
* No arguments will be passed.
*/
inputListeners: Function[];
/**
* This function will be called if submission is triggered programmatically via keybind.
* No arguments should be passed.
*/
onsubmit: Function;
}
export default interface EditorDriverInterface {
/**
* Focus the editor and place the cursor at the given position.
*/
moveCursorTo(position: number): void;
/**
* Get the selected range of the editor.
*/
getSelectionRange(): Array<number>;
/**
* Get the last N characters from the current "text block".
*
* A textarea-based driver would just return the last N characters,
* but more advanced implementations might restrict to the current block.
*
* This is useful for monitoring recent user input to trigger autocomplete.
*/
getLastNChars(n: number): string;
/**
* Insert content into the editor at the position of the cursor.
*/
insertAtCursor(text: string, escape: boolean): void;
/**
* Insert content into the editor at the given position.
*/
insertAt(pos: number, text: string, escape: boolean): void;
/**
* Insert content into the editor between the given positions.
*
* If the start and end positions are different, any text between them will be
* overwritten.
*/
insertBetween(start: number, end: number, text: string, escape: boolean): void;
/**
* Replace existing content from the start to the current cursor position.
*/
replaceBeforeCursor(start: number, text: string, escape: boolean): void;
/**
* Get left and top coordinates of the caret relative to the editor viewport.
*/
getCaretCoordinates(position: number): { left: number; top: number };
/**
* Set the disabled status of the editor.
*/
disabled(disabled: boolean): void;
/**
* Focus on the editor.
*/
focus(): void;
/**
* Destroy the editor
*/
destroy(): void;
}

View File

@@ -58,7 +58,7 @@ export default class ScrollListener {
*/
start() {
if (!this.active) {
window.addEventListener('scroll', (this.active = this.loop.bind(this)), { passive: true });
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
}
}

View File

@@ -1,3 +0,0 @@
import Stream from 'mithril/stream';
export default Stream;

View File

@@ -28,9 +28,6 @@ export default class SubtreeRetainer {
constructor(...callbacks) {
this.callbacks = callbacks;
this.data = {};
// Build the initial data, so it is available when calling
// needsRebuild from the onbeforeupdate hook.
this.needsRebuild();
}
/**
@@ -63,8 +60,6 @@ export default class SubtreeRetainer {
*/
check(...callbacks) {
this.callbacks = this.callbacks.concat(callbacks);
// Update the data cache when new checks are added.
this.needsRebuild();
}
/**

View File

@@ -0,0 +1,109 @@
/**
* A textarea wrapper with powerful helpers for text manipulation.
*
* This wraps a <textarea> DOM element and allows directly manipulating its text
* contents and cursor positions.
*
* I apologize for the pretentious name. :)
*/
export default class SuperTextarea {
/**
* @param {HTMLTextAreaElement} textarea
*/
constructor(textarea) {
this.el = textarea;
this.$ = $(textarea);
}
/**
* Set the value of the text editor.
*
* @param {String} value
*/
setValue(value) {
this.$.val(value).trigger('input');
}
/**
* Focus the textarea and place the cursor at the given index.
*
* @param {number} position
*/
moveCursorTo(position) {
this.setSelectionRange(position, position);
}
/**
* Get the selected range of the textarea.
*
* @return {Array}
*/
getSelectionRange() {
return [this.el.selectionStart, this.el.selectionEnd];
}
/**
* Insert content into the textarea at the position of the cursor.
*
* @param {String} text
*/
insertAtCursor(text) {
this.insertAt(this.el.selectionStart, text);
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
}
/**
* Insert content into the textarea at the given position.
*
* @param {number} pos
* @param {String} text
*/
insertAt(pos, text) {
this.insertBetween(pos, pos, text);
}
/**
* Insert content into the textarea between the given positions.
*
* If the start and end positions are different, any text between them will be
* overwritten.
*
* @param start
* @param end
* @param text
*/
insertBetween(start, end, text) {
const value = this.el.value;
const before = value.slice(0, start);
const after = value.slice(end);
this.setValue(`${before}${text}${after}`);
// Move the textarea cursor to the end of the content we just inserted.
this.moveCursorTo(start + text.length);
}
/**
* Replace existing content from the start to the current cursor position.
*
* @param start
* @param text
*/
replaceBeforeCursor(start, text) {
this.insertBetween(start, this.el.selectionStart, text);
}
/**
* Set the selected range of the textarea.
*
* @param {number} start
* @param {number} end
* @private
*/
setSelectionRange(start, end) {
this.el.setSelectionRange(start, end);
this.$.focus();
}
}

View File

@@ -1,50 +0,0 @@
function bidi(node, prop) {
var type = node.tag === 'select' ? (node.attrs.multi ? 'multi' : 'select') : node.attrs.type;
// Setup: bind listeners
if (type === 'multi') {
node.attrs.onchange = function () {
prop(
[].slice.call(this.selectedOptions, function (x) {
return x.value;
})
);
};
} else if (type === 'select') {
node.attrs.onchange = function (e) {
prop(this.selectedOptions[0].value);
};
} else if (type === 'checkbox') {
node.attrs.onchange = function (e) {
prop(this.checked);
};
} else {
node.attrs.onchange = node.attrs.oninput = function (e) {
prop(this.value);
};
}
if (node.tag === 'select') {
node.children.forEach(function (option) {
if (option.attrs.value === prop() || option.children[0] === prop()) {
option.attrs.selected = true;
}
});
} else if (type === 'checkbox') {
node.attrs.checked = prop();
} else if (type === 'radio') {
node.attrs.checked = prop() === node.attrs.value;
} else {
node.attrs.value = prop();
}
node.attrs.bidi = null;
return node;
}
bidi.view = function (ctrl, node, prop) {
return bidi(node, node.attrs.bidi);
};
export default bidi;

View File

@@ -0,0 +1,26 @@
/**
* The `classList` utility creates a list of class names by joining an object's
* keys, but only for values which are truthy.
*
* @example
* classList({ foo: true, bar: false, qux: 'qaz' });
* // "foo qux"
*
* @param {Object} classes
* @return {String}
*/
export default function classList(classes) {
let classNames;
if (classes instanceof Array) {
classNames = classes.filter((name) => name);
} else {
classNames = [];
for (const i in classes) {
if (classes[i]) classNames.push(i);
}
}
return classNames.join(' ');
}

View File

@@ -1,12 +0,0 @@
import clsx from 'clsx';
/**
* This util exposes `clsx` to core and extensions as a re-usable utility.
*
* For full documentation, see `clsx` on GitHub.
*
* @see https://github.com/lukeed/clsx
*/
const classList = clsx;
export default classList;

View File

@@ -1,6 +1,3 @@
import dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
/**
* The `humanTime` utility converts a date to a localized, human-readable time-
* ago string.

View File

@@ -1,9 +1,6 @@
import DefaultResolver from '../resolvers/DefaultResolver';
/**
* The `mapRoutes` utility converts a map of named application routes into a
* format that can be understood by Mithril, and wraps them in route resolvers
* to provide each route with the current route name.
* format that can be understood by Mithril.
*
* @see https://mithril.js.org/route.html#signature
* @param {Object} routes
@@ -13,17 +10,14 @@ import DefaultResolver from '../resolvers/DefaultResolver';
export default function mapRoutes(routes, basePath = '') {
const map = {};
for (const routeName in routes) {
const route = routes[routeName];
for (const key in routes) {
const route = routes[key];
if ('resolver' in route) {
map[basePath + route.path] = route.resolver;
} else if ('component' in route) {
const resolverClass = 'resolverClass' in route ? route.resolverClass : DefaultResolver;
map[basePath + route.path] = new resolverClass(route.component, routeName);
} else {
throw new Error(`Either a resolver or a component must be provided for the route [${routeName}]`);
}
map[basePath + route.path] = {
render() {
return m(route.component, { routeName: key });
},
};
}
return map;

View File

@@ -1,8 +1,39 @@
import bidi from './bidi';
import Stream from 'mithril/stream';
import extract from './extract';
export default function patchMithril(global) {
const defaultMithril = global.m;
/**
* 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.
*
* We also add the `force` attribute that adds a custom state key
* for when you want to force a complete refresh of the Page
*/
const defaultLinkView = defaultMithril.route.Link.view;
const modifiedLink = {
view: function (vnode) {
let { href, options = {} } = vnode.attrs;
if (href === m.route.get()) {
if (!('replace' in options)) options.replace = true;
}
if (extract(vnode.attrs, 'force')) {
if (!('state' in options)) options.state = {};
if (!('key' in options.state)) options.state.key = Date.now();
}
vnode.attrs.options = options;
return defaultLinkView(vnode);
},
};
const modifiedMithril = function (comp, ...args) {
const node = defaultMithril.apply(this, arguments);
@@ -10,7 +41,22 @@ export default function patchMithril(global) {
// Allows the use of the bidi attr.
if (node.attrs.bidi) {
bidi(node, node.attrs.bidi);
modifiedMithril.bidi(node, node.attrs.bidi);
}
// Allows us to use a "route" attr on links, which will automatically convert the link to one which
// supports linking to other pages in the SPA without refreshing the document.
if (node.attrs.route) {
node.attrs.href = node.attrs.route;
node.tag = modifiedLink;
// 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.
if (node.text) {
node.children = { tag: '#', children: node.text };
}
delete node.attrs.route;
}
return node;
@@ -18,5 +64,9 @@ export default function patchMithril(global) {
Object.keys(defaultMithril).forEach((key) => (modifiedMithril[key] = defaultMithril[key]));
modifiedMithril.stream = Stream;
modifiedMithril.route.Link = modifiedLink;
global.m = modifiedMithril;
}

View File

@@ -1,10 +0,0 @@
export default (compat: { [key: string]: any }, namespace: string) => {
// regex to replace common/ and NAMESPACE/ for core & core extensions
// e.g. admin/utils/extract --> utils/extract
// e.g. tags/common/utils/sortTags --> tags/utils/sortTags
const regex = new RegExp(`(\\w+\\/)?(${namespace}|common)\\/`);
return new Proxy(compat, {
get: (obj, prop: string) => obj[prop] || obj[prop.replace(regex, '$1')],
});
};

View File

@@ -16,7 +16,6 @@ import NotificationListState from './states/NotificationListState';
import GlobalSearchState from './states/GlobalSearchState';
import DiscussionListState from './states/DiscussionListState';
import ComposerState from './states/ComposerState';
import isSafariMobile from './utils/isSafariMobile';
export default class ForumApplication extends Application {
/**
@@ -91,6 +90,11 @@ export default class ForumApplication extends Application {
* @type {DiscussionListState}
*/
this.discussions = new DiscussionListState({}, this);
/**
* @deprecated beta 14, remove in beta 15.
*/
this.cache.discussionList = this.discussions;
}
/**
@@ -111,19 +115,17 @@ export default class ForumApplication extends Application {
this.routes[defaultAction].path = '/';
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
this.pane = new Pane(document.getElementById('app'));
m.route.prefix = '';
super.mount(this.forum.attribute('basePath'));
// We mount navigation and header components after the page, so components
// like the back button can access the updated state when rendering.
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('composer'), { view: () => Composer.component({ state: this.composer }) });
this.pane = new Pane(document.getElementById('app'));
m.route.prefix = '';
super.mount(this.forum.attribute('basePath'));
alertEmailConfirmation(this);
// Route the home link back home when clicked. We do not want it to register
@@ -139,12 +141,6 @@ export default class ForumApplication extends Application {
m.redraw();
}
});
if (isSafariMobile()) {
$(() => {
$('.App').addClass('mobile-safari');
});
}
}
/**

View File

@@ -1,8 +0,0 @@
import Forum from './ForumApplication';
const app = new Forum();
// @ts-ignore
window.app = app;
export default app;

View File

@@ -16,7 +16,6 @@ import PostStreamState from './states/PostStreamState';
import SearchState from './states/SearchState';
import AffixedSidebar from './components/AffixedSidebar';
import DiscussionPage from './components/DiscussionPage';
import DiscussionListPane from './components/DiscussionListPane';
import LogInModal from './components/LogInModal';
import ComposerBody from './components/ComposerBody';
import ForgotPasswordModal from './components/ForgotPasswordModal';
@@ -36,6 +35,8 @@ import HeaderSecondary from './components/HeaderSecondary';
import ComposerButton from './components/ComposerButton';
import DiscussionList from './components/DiscussionList';
import ReplyPlaceholder from './components/ReplyPlaceholder';
import TextEditor from './components/TextEditor';
import TextEditorButton from './components/TextEditorButton';
import AvatarEditor from './components/AvatarEditor';
import Post from './components/Post';
import SettingsPage from './components/SettingsPage';
@@ -70,8 +71,6 @@ import Search from './components/Search';
import DiscussionListItem from './components/DiscussionListItem';
import LoadingPost from './components/LoadingPost';
import PostsUserPage from './components/PostsUserPage';
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
import BasicEditorDriver from '../common/utils/BasicEditorDriver';
import routes from './routes';
import ForumApplication from './ForumApplication';
@@ -84,7 +83,6 @@ export default Object.assign(compat, {
'utils/alertEmailConfirmation': alertEmailConfirmation,
'utils/UserControls': UserControls,
'utils/Pane': Pane,
'utils/BasicEditorDriver': BasicEditorDriver,
'states/ComposerState': ComposerState,
'states/DiscussionListState': DiscussionListState,
'states/GlobalSearchState': GlobalSearchState,
@@ -93,7 +91,6 @@ export default Object.assign(compat, {
'states/SearchState': SearchState,
'components/AffixedSidebar': AffixedSidebar,
'components/DiscussionPage': DiscussionPage,
'components/DiscussionListPane': DiscussionListPane,
'components/LogInModal': LogInModal,
'components/ComposerBody': ComposerBody,
'components/ForgotPasswordModal': ForgotPasswordModal,
@@ -113,6 +110,8 @@ export default Object.assign(compat, {
'components/ComposerButton': ComposerButton,
'components/DiscussionList': DiscussionList,
'components/ReplyPlaceholder': ReplyPlaceholder,
'components/TextEditor': TextEditor,
'components/TextEditorButton': TextEditorButton,
'components/AvatarEditor': AvatarEditor,
'components/Post': Post,
'components/SettingsPage': SettingsPage,
@@ -147,7 +146,6 @@ export default Object.assign(compat, {
'components/DiscussionListItem': DiscussionListItem,
'components/LoadingPost': LoadingPost,
'components/PostsUserPage': PostsUserPage,
'resolvers/DiscussionPageResolver': DiscussionPageResolver,
routes: routes,
ForumApplication: ForumApplication,
});

View File

@@ -1,6 +1,5 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Stream from '../../common/utils/Stream';
/**
* The `ChangeEmailModal` component shows a modal dialog which allows the user
@@ -22,14 +21,14 @@ export default class ChangeEmailModal extends Modal {
*
* @type {function}
*/
this.email = Stream(app.session.user.email());
this.email = m.stream(app.session.user.email());
/**
* The value of the password input.
*
* @type {function}
*/
this.password = Stream('');
this.password = m.stream('');
}
className() {
@@ -106,8 +105,9 @@ export default class ChangeEmailModal extends Modal {
return;
}
const oldEmail = app.session.user.email();
this.loading = true;
this.alertAttrs = null;
app.session.user
.save(
@@ -117,9 +117,7 @@ export default class ChangeEmailModal extends Modal {
meta: { password: this.password() },
}
)
.then(() => {
this.success = true;
})
.then(() => (this.success = true))
.catch(() => {})
.then(this.loaded.bind(this));
}

View File

@@ -56,7 +56,9 @@ export default class CommentPost extends Post {
]);
}
refreshContent() {
onupdate(vnode) {
super.onupdate();
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
// If the post content has changed since the last render, we'll run through
@@ -64,28 +66,13 @@ export default class CommentPost extends Post {
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
if (this.contentHtml !== contentHtml) {
this.$('.Post-body script').each(function () {
const script = document.createElement('script');
script.textContent = this.textContent;
Array.from(this.attributes).forEach((attr) => script.setAttribute(attr.name, attr.value));
this.parentNode.replaceChild(script, this);
eval.call(window, $(this).text());
});
}
this.contentHtml = contentHtml;
}
oncreate(vnode) {
super.oncreate(vnode);
this.refreshContent();
}
onupdate(vnode) {
super.onupdate(vnode);
this.refreshContent();
}
isEditing() {
return app.composer.bodyMatches(EditPostComposer, { post: this.attrs.post });
}

View File

@@ -76,13 +76,13 @@ export default class Composer extends Component {
// Whenever any of the inputs inside the composer are have focus, we want to
// add a class to the composer to draw attention to it.
this.$().on('focus blur', ':input,.TextEditor-editorContainer', (e) => {
this.$().on('focus blur', ':input', (e) => {
this.active = e.type === 'focusin';
m.redraw();
});
// When the escape key is pressed on any inputs, close the composer.
this.$().on('keydown', ':input,.TextEditor-editorContainer', 'esc', () => this.state.close());
this.$().on('keydown', ':input', 'esc', () => this.close());
this.handlers = {};
@@ -157,7 +157,7 @@ export default class Composer extends Component {
* Draw focus to the first focusable content element (the text editor).
*/
focus() {
this.$('.Composer-content :input:enabled:visible, .TextEditor-editor').first().focus();
this.$('.Composer-content :input:enabled:visible:first').focus();
}
/**
@@ -199,7 +199,7 @@ export default class Composer extends Component {
*/
animatePositionChange() {
// When exiting full-screen mode: focus content
if (this.prevPosition === ComposerState.Position.FULLSCREEN && this.state.position === ComposerState.Position.NORMAL) {
if (this.prevPosition === ComposerState.Position.FULLSCREEN) {
this.focus();
return;
}
@@ -265,17 +265,7 @@ export default class Composer extends Component {
this.animateHeightChange().then(() => this.focus());
if (app.screen() === 'phone') {
// On safari fixed position doesn't properly work on mobile,
// So we use absolute and set the top value.
// https://github.com/flarum/core/issues/2652
// Due to another safari bug, `scrollTop` is unreliable when
// at the very bottom of the page AND opening the composer.
// So we fallback to a calculated version of scrollTop.
// https://github.com/flarum/core/issues/2683
const scrollElement = document.documentElement;
const topOfViewport = Math.min(scrollElement.scrollTop, scrollElement.scrollHeight - scrollElement.clientHeight);
this.$().css('top', $('.App').is('.mobile-safari') ? topOfViewport : 0);
this.$().css('top', $(window).scrollTop());
this.showBackdrop();
}
}

View File

@@ -1,7 +1,7 @@
import Component from '../../common/Component';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';
import TextEditor from '../../common/components/TextEditor';
import TextEditor from './TextEditor';
import avatar from '../../common/helpers/avatar';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
@@ -44,6 +44,12 @@ export default class ComposerBody extends Component {
}
this.composer.fields.content(this.attrs.originalContent || '');
/**
* @deprecated BC layer, remove in Beta 15.
*/
this.content = this.composer.fields.content;
this.editor = this.composer;
}
view() {

View File

@@ -1,6 +1,5 @@
import ComposerBody from './ComposerBody';
import extractText from '../../common/utils/extractText';
import Stream from '../../common/utils/Stream';
/**
* The `DiscussionComposer` component displays the composer content for starting
@@ -27,7 +26,7 @@ export default class DiscussionComposer extends ComposerBody {
oninit(vnode) {
super.oninit(vnode);
this.composer.fields.title = this.composer.fields.title || Stream('');
this.composer.fields.title = this.composer.fields.title || m.stream('');
/**
* The value of the title input.
@@ -100,7 +99,7 @@ export default class DiscussionComposer extends ComposerBody {
.save(data)
.then((discussion) => {
this.composer.hide();
app.discussions.refresh({ deferClear: true });
app.discussions.refresh();
m.route.set(app.route.discussion(discussion));
}, this.loaded.bind(this));
}

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