From 71f3379fcc757a99be0a345cc1085d346544cd9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Sevilla=20Mart=C3=ADn?= <6401250+datitisev@users.noreply.github.com> Date: Wed, 23 Sep 2020 22:40:37 -0400 Subject: [PATCH] Mithril 2 update (#2255) * Update frontend to Mithril 2 - Update Mithril version to v2.0.4 - Add Typescript typings for Mithril - Rename "props" to "attrs"; "initProps" to "initAttrs"; "m.prop" to "m.stream"; "m.withAttr" to "utils/withAttr". - Use Mithril 2's new lifecycle hooks - SubtreeRetainer has been rewritten to be more useful for the new system - Utils for forcing page re-initializations have been added (force attr in links, setRouteWithForcedRefresh util) - Other mechanical changes, following the upgrade guide - Remove some of the custom stuff in our Component base class - Introduce "fragments" for non-components that control their own DOM - Remove Mithril patches, introduce a few new ones (route attrs in ; - Redesign AlertManagerState `show` with 3 overloads: `show(children)`, `show(attrs, children)`, `show(componentClass, attrs, children)` - The `affixedSidebar` util has been replaced with an `AffixedSidebar` component Challenges: - `children` and `tag` are now reserved, and can not be used as attr names - Behavior of links to current page changed in Mithril. If moving to a page that is handled by the same component, the page component WILL NOT be re-initialized by default. Additional code to keep track of the current url is needed (See IndexPage, DiscussionPage, and UserPage for examples) - Native Promise rejections are shown on console when not handled - Instances of components can no longer be stored. The state pattern should be used instead. Refs #1821. Co-authored-by: Alexander Skvortsov Co-authored-by: Matthew Kilgore Co-authored-by: Franz Liedke --- js/package-lock.json | 11 +- js/package.json | 3 +- js/shims.d.ts | 48 ++++ js/src/admin/AdminApplication.js | 17 +- js/src/admin/components/AdminLinkButton.js | 8 +- js/src/admin/components/AdminNav.js | 84 ++++--- js/src/admin/components/AppearancePage.js | 103 ++++---- js/src/admin/components/BasicsPage.js | 133 ++++++----- js/src/admin/components/EditGroupModal.js | 61 +++-- js/src/admin/components/ExtensionsPage.js | 54 +++-- js/src/admin/components/HeaderSecondary.js | 7 - js/src/admin/components/MailPage.js | 117 +++++----- js/src/admin/components/PermissionDropdown.js | 104 +++++---- js/src/admin/components/PermissionGrid.js | 4 +- js/src/admin/components/SessionDropdown.js | 28 +-- js/src/admin/components/SettingDropdown.js | 35 +-- js/src/admin/components/SettingsModal.js | 6 +- js/src/admin/components/UploadImageButton.js | 32 ++- js/src/admin/routes.js | 12 +- js/src/admin/utils/saveSettings.js | 2 +- js/src/common/Application.js | 52 +++-- js/src/common/Component.js | 221 ------------------ js/src/common/Component.ts | 136 +++++++++++ js/src/common/Fragment.ts | 74 ++++++ js/src/common/Model.js | 12 +- js/src/common/Session.js | 8 +- js/src/common/Store.js | 6 +- js/src/common/Translator.js | 17 +- js/src/common/compat.js | 6 + js/src/common/components/Alert.js | 12 +- js/src/common/components/AlertManager.js | 17 +- js/src/common/components/Badge.js | 12 +- js/src/common/components/Button.js | 34 +-- js/src/common/components/Checkbox.js | 27 ++- .../components/ConfirmDocumentUnload.js | 29 +-- js/src/common/components/Dropdown.js | 56 ++--- js/src/common/components/FieldSet.js | 8 +- js/src/common/components/GroupBadge.js | 16 +- js/src/common/components/LinkButton.js | 26 ++- js/src/common/components/LoadingIndicator.js | 15 +- js/src/common/components/Modal.js | 30 ++- js/src/common/components/ModalManager.js | 19 +- js/src/common/components/Navigation.js | 14 +- js/src/common/components/Page.js | 26 ++- js/src/common/components/Placeholder.js | 4 +- js/src/common/components/RequestErrorModal.js | 6 +- js/src/common/components/Select.js | 7 +- js/src/common/components/SelectDropdown.js | 36 ++- js/src/common/components/SplitDropdown.js | 30 +-- js/src/common/components/Switch.js | 8 +- js/src/common/helpers/listItems.js | 20 +- js/src/common/states/AlertManagerState.js | 18 +- js/src/common/states/ModalManagerState.js | 4 +- js/src/common/utils/SubtreeRetainer.js | 33 +-- js/src/common/utils/extractText.js | 2 +- js/src/common/utils/mapRoutes.js | 10 +- js/src/common/utils/patchMithril.js | 73 ++++-- .../common/utils/setRouteWithForcedRefresh.ts | 15 ++ js/src/common/utils/withAttr.ts | 15 ++ js/src/forum/ForumApplication.js | 16 +- js/src/forum/compat.js | 6 +- js/src/forum/components/AffixedSidebar.js | 51 ++++ js/src/forum/components/AvatarEditor.js | 50 ++-- js/src/forum/components/ChangeEmailModal.js | 24 +- .../forum/components/ChangePasswordModal.js | 16 +- js/src/forum/components/CommentPost.js | 72 ++---- js/src/forum/components/Composer.js | 43 ++-- js/src/forum/components/ComposerBody.js | 26 ++- js/src/forum/components/ComposerButton.js | 6 +- .../forum/components/ComposerPostPreview.js | 54 +++++ js/src/forum/components/DiscussionComposer.js | 41 ++-- js/src/forum/components/DiscussionHero.js | 4 +- js/src/forum/components/DiscussionList.js | 20 +- js/src/forum/components/DiscussionListItem.js | 72 +++--- js/src/forum/components/DiscussionListPane.js | 67 ++++++ js/src/forum/components/DiscussionPage.js | 132 ++++------- .../DiscussionRenamedNotification.js | 8 +- .../forum/components/DiscussionRenamedPost.js | 6 +- .../components/DiscussionsSearchSource.js | 14 +- .../forum/components/DiscussionsUserPage.js | 4 +- js/src/forum/components/EditPostComposer.js | 62 +++-- js/src/forum/components/EditUserModal.js | 60 ++--- js/src/forum/components/EventPost.js | 14 +- .../forum/components/ForgotPasswordModal.js | 30 +-- js/src/forum/components/HeaderPrimary.js | 7 - js/src/forum/components/HeaderSecondary.js | 67 +++--- js/src/forum/components/IndexPage.js | 148 +++++++----- js/src/forum/components/LogInButton.js | 12 +- js/src/forum/components/LogInModal.js | 38 +-- js/src/forum/components/Notification.js | 19 +- js/src/forum/components/NotificationGrid.js | 30 +-- js/src/forum/components/NotificationList.js | 51 ++-- .../forum/components/NotificationsDropdown.js | 28 +-- js/src/forum/components/NotificationsPage.js | 4 +- js/src/forum/components/Post.js | 89 +++---- js/src/forum/components/PostEdited.js | 20 +- js/src/forum/components/PostMeta.js | 8 +- js/src/forum/components/PostPreview.js | 8 +- js/src/forum/components/PostStream.js | 34 +-- js/src/forum/components/PostStreamScrubber.js | 47 ++-- js/src/forum/components/PostUser.js | 16 +- js/src/forum/components/PostsUserPage.js | 25 +- .../forum/components/RenameDiscussionModal.js | 24 +- js/src/forum/components/ReplyComposer.js | 58 +++-- js/src/forum/components/ReplyPlaceholder.js | 40 +--- js/src/forum/components/Search.js | 20 +- js/src/forum/components/SessionDropdown.js | 68 +++--- js/src/forum/components/SettingsPage.js | 92 +++----- js/src/forum/components/SignUpModal.js | 41 ++-- js/src/forum/components/TerminalPost.js | 6 +- js/src/forum/components/TextEditor.js | 63 +++-- js/src/forum/components/TextEditorButton.js | 12 +- js/src/forum/components/UserCard.js | 30 +-- js/src/forum/components/UserPage.js | 73 +++--- js/src/forum/components/UsersSearchSource.js | 7 +- js/src/forum/components/WelcomeHero.js | 4 +- js/src/forum/routes.js | 17 +- js/src/forum/states/ComposerState.js | 6 +- js/src/forum/states/GlobalSearchState.js | 7 +- js/src/forum/states/PostStreamState.js | 10 +- js/src/forum/utils/DiscussionControls.js | 122 +++++----- js/src/forum/utils/History.js | 8 +- js/src/forum/utils/PostControls.js | 60 ++--- js/src/forum/utils/UserControls.js | 23 +- js/src/forum/utils/affixSidebar.js | 39 ---- js/src/forum/utils/alertEmailConfirmation.js | 62 ++--- js/tsconfig.json | 21 ++ 127 files changed, 2411 insertions(+), 2074 deletions(-) create mode 100644 js/shims.d.ts delete mode 100644 js/src/common/Component.js create mode 100644 js/src/common/Component.ts create mode 100644 js/src/common/Fragment.ts create mode 100644 js/src/common/utils/setRouteWithForcedRefresh.ts create mode 100644 js/src/common/utils/withAttr.ts create mode 100644 js/src/forum/components/AffixedSidebar.js create mode 100644 js/src/forum/components/ComposerPostPreview.js create mode 100644 js/src/forum/components/DiscussionListPane.js delete mode 100644 js/src/forum/utils/affixSidebar.js create mode 100644 js/tsconfig.json diff --git a/js/package-lock.json b/js/package-lock.json index 9e788e9d1..a39b216f4 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1075,6 +1075,11 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/mithril": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.0.3.tgz", + "integrity": "sha512-cZHOdO2IiXYeyjeDYdbOisSdfaJRzfmRo3zVzgu33IWTMA0KEQObp9fdvqcuYdPz93iJ1yCl19GcEjo/9yv+yA==" + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -3807,9 +3812,9 @@ } }, "mithril": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/mithril/-/mithril-0.2.8.tgz", - "integrity": "sha512-9XuGnVmS2OyFexUuP/CcJFFJjHLM+RGYBxyVRNyQ6khbMfDJIF/xyZ4zq18ZRfPagpFmWUFpjHd5ZqPULGZyNg==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mithril/-/mithril-2.0.4.tgz", + "integrity": "sha512-mgw+DMZlhMS4PpprF6dl7ZoeZq5GGcAuWnrg5e12MvaGauc4jzWsDZtVGRCktsiQczOEUr2K5teKbE5k44RlOg==" }, "mixin-deep": { "version": "1.3.2", diff --git a/js/package.json b/js/package.json index 96a521dc9..f4c0af8a2 100644 --- a/js/package.json +++ b/js/package.json @@ -3,6 +3,7 @@ "name": "@flarum/core", "dependencies": { "@babel/preset-typescript": "^7.10.1", + "@types/mithril": "^2.0.3", "bootstrap": "^3.4.1", "classnames": "^2.2.5", "color-thief-browser": "^2.0.2", @@ -13,7 +14,7 @@ "jquery.hotkeys": "^0.1.0", "lodash-es": "^4.17.14", "m.attrs.bidi": "github:tobscure/m.attrs.bidi", - "mithril": "^0.2.8", + "mithril": "^2.0.4", "punycode": "^2.1.1", "spin.js": "^3.1.0", "webpack": "^4.43.0", diff --git a/js/shims.d.ts b/js/shims.d.ts new file mode 100644 index 000000000..a2d31e316 --- /dev/null +++ b/js/shims.d.ts @@ -0,0 +1,48 @@ +// Mithril +import * as Mithril from 'mithril'; +import Stream from 'mithril/stream'; + +// Other third-party libs +import * as _dayjs from 'dayjs'; +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: + * + * - jQuery for convenient DOM manipulation + * - Mithril for VDOM and components + * - dayjs for date/time operations + * + * Since these are already part of the global namespace, extensions won't need + * to (and should not) bundle these themselves. + */ +declare global { + const $: typeof _$; + const m: m; + const dayjs: typeof _dayjs; +} + +/** + * All global variables owned by flarum/core. + */ +declare global { + const app: Application; +} diff --git a/js/src/admin/AdminApplication.js b/js/src/admin/AdminApplication.js index e0d9464e4..03669d6e0 100644 --- a/js/src/admin/AdminApplication.js +++ b/js/src/admin/AdminApplication.js @@ -27,13 +27,18 @@ export default class AdminApplication extends Application { * @inheritdoc */ mount() { - m.mount(document.getElementById('app-navigation'), Navigation.component({ className: 'App-backControl', drawer: true })); - m.mount(document.getElementById('header-navigation'), Navigation.component()); - m.mount(document.getElementById('header-primary'), HeaderPrimary.component()); - m.mount(document.getElementById('header-secondary'), HeaderSecondary.component()); - m.mount(document.getElementById('admin-navigation'), AdminNav.component()); + 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 = '#'; - m.route.mode = 'hash'; super.mount(); // If an extension has just been enabled, then we will run its settings diff --git a/js/src/admin/components/AdminLinkButton.js b/js/src/admin/components/AdminLinkButton.js index 2602bc273..ff98f7130 100644 --- a/js/src/admin/components/AdminLinkButton.js +++ b/js/src/admin/components/AdminLinkButton.js @@ -10,11 +10,7 @@ import LinkButton from '../../common/components/LinkButton'; export default class AdminLinkButton extends LinkButton { - getButtonContent() { - const content = super.getButtonContent(); - - content.push(
{this.props.description}
); - - return content; + getButtonContent(children) { + return [...super.getButtonContent(children),
{this.attrs.description}
]; } } diff --git a/js/src/admin/components/AdminNav.js b/js/src/admin/components/AdminNav.js index f96a4e162..f41d2a1a6 100644 --- a/js/src/admin/components/AdminNav.js +++ b/js/src/admin/components/AdminNav.js @@ -31,62 +31,74 @@ export default class AdminNav extends Component { items.add( 'dashboard', - AdminLinkButton.component({ - href: app.route('dashboard'), - icon: 'far fa-chart-bar', - children: app.translator.trans('core.admin.nav.dashboard_button'), - description: app.translator.trans('core.admin.nav.dashboard_text'), - }) + 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', - AdminLinkButton.component({ - href: app.route('basics'), - icon: 'fas fa-pencil-alt', - children: app.translator.trans('core.admin.nav.basics_button'), - description: app.translator.trans('core.admin.nav.basics_text'), - }) + 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', - AdminLinkButton.component({ - href: app.route('mail'), - icon: 'fas fa-envelope', - children: app.translator.trans('core.admin.nav.email_button'), - description: app.translator.trans('core.admin.nav.email_text'), - }) + 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', - AdminLinkButton.component({ - href: app.route('permissions'), - icon: 'fas fa-key', - children: app.translator.trans('core.admin.nav.permissions_button'), - description: app.translator.trans('core.admin.nav.permissions_text'), - }) + 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', - AdminLinkButton.component({ - href: app.route('appearance'), - icon: 'fas fa-paint-brush', - children: app.translator.trans('core.admin.nav.appearance_button'), - description: app.translator.trans('core.admin.nav.appearance_text'), - }) + 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( 'extensions', - AdminLinkButton.component({ - href: app.route('extensions'), - icon: 'fas fa-puzzle-piece', - children: app.translator.trans('core.admin.nav.extensions_button'), - description: app.translator.trans('core.admin.nav.extensions_text'), - }) + 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; diff --git a/js/src/admin/components/AppearancePage.js b/js/src/admin/components/AppearancePage.js index 809bd6826..f77d50d98 100644 --- a/js/src/admin/components/AppearancePage.js +++ b/js/src/admin/components/AppearancePage.js @@ -6,15 +6,16 @@ import EditCustomHeaderModal from './EditCustomHeaderModal'; import EditCustomFooterModal from './EditCustomFooterModal'; import UploadImageButton from './UploadImageButton'; import saveSettings from '../utils/saveSettings'; +import withAttr from '../../common/utils/withAttr'; export default class AppearancePage extends Page { - init() { - super.init(); + oninit(vnode) { + super.oninit(vnode); - this.primaryColor = m.prop(app.data.settings.theme_primary_color); - this.secondaryColor = m.prop(app.data.settings.theme_secondary_color); - this.darkMode = m.prop(app.data.settings.theme_dark_mode); - this.coloredHeader = m.prop(app.data.settings.theme_colored_header); + 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); } view() { @@ -27,40 +28,34 @@ export default class AppearancePage extends Page {
{app.translator.trans('core.admin.appearance.colors_text')}
- - + +
- {Switch.component({ - state: this.darkMode(), - children: app.translator.trans('core.admin.appearance.dark_mode_label'), - onchange: this.darkMode, - })} + {Switch.component( + { + state: this.darkMode(), + onchange: this.darkMode, + }, + app.translator.trans('core.admin.appearance.dark_mode_label') + )} - {Switch.component({ - state: this.coloredHeader(), - children: app.translator.trans('core.admin.appearance.colored_header_label'), - onchange: this.coloredHeader, - })} + {Switch.component( + { + state: this.coloredHeader(), + onchange: this.coloredHeader, + }, + app.translator.trans('core.admin.appearance.colored_header_label') + )} - {Button.component({ - className: 'Button Button--primary', - type: 'submit', - children: app.translator.trans('core.admin.appearance.submit_button'), - loading: this.loading, - })} + {Button.component( + { + className: 'Button Button--primary', + type: 'submit', + loading: this.loading, + }, + app.translator.trans('core.admin.appearance.submit_button') + )} @@ -79,31 +74,37 @@ export default class AppearancePage extends Page {
{app.translator.trans('core.admin.appearance.custom_header_heading')}
{app.translator.trans('core.admin.appearance.custom_header_text')}
- {Button.component({ - className: 'Button', - children: app.translator.trans('core.admin.appearance.edit_header_button'), - onclick: () => app.modal.show(EditCustomHeaderModal), - })} + {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomHeaderModal), + }, + app.translator.trans('core.admin.appearance.edit_header_button') + )}
{app.translator.trans('core.admin.appearance.custom_footer_heading')}
{app.translator.trans('core.admin.appearance.custom_footer_text')}
- {Button.component({ - className: 'Button', - children: app.translator.trans('core.admin.appearance.edit_footer_button'), - onclick: () => app.modal.show(EditCustomFooterModal), - })} + {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomFooterModal), + }, + app.translator.trans('core.admin.appearance.edit_footer_button') + )}
{app.translator.trans('core.admin.appearance.custom_styles_heading')}
{app.translator.trans('core.admin.appearance.custom_styles_text')}
- {Button.component({ - className: 'Button', - children: app.translator.trans('core.admin.appearance.edit_css_button'), - onclick: () => app.modal.show(EditCustomCssModal), - })} + {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomCssModal), + }, + app.translator.trans('core.admin.appearance.edit_css_button') + )}
diff --git a/js/src/admin/components/BasicsPage.js b/js/src/admin/components/BasicsPage.js index 68f9ba142..9b5a7dc7f 100644 --- a/js/src/admin/components/BasicsPage.js +++ b/js/src/admin/components/BasicsPage.js @@ -5,10 +5,11 @@ import Button from '../../common/components/Button'; import saveSettings from '../utils/saveSettings'; import ItemList from '../../common/utils/ItemList'; import Switch from '../../common/components/Switch'; +import withAttr from '../../common/utils/withAttr'; export default class BasicsPage extends Page { - init() { - super.init(); + oninit(vnode) { + super.oninit(vnode); this.loading = false; @@ -25,7 +26,7 @@ export default class BasicsPage extends Page { this.values = {}; const settings = app.data.settings; - this.fields.forEach((key) => (this.values[key] = m.prop(settings[key]))); + this.fields.forEach((key) => (this.values[key] = m.stream(settings[key]))); this.localeOptions = {}; const locales = app.data.locales; @@ -49,45 +50,51 @@ export default class BasicsPage extends Page {
- {FieldSet.component({ - label: app.translator.trans('core.admin.basics.forum_title_heading'), - children: [], - })} + {FieldSet.component( + { + label: app.translator.trans('core.admin.basics.forum_title_heading'), + }, + [] + )} - {FieldSet.component({ - label: app.translator.trans('core.admin.basics.forum_description_heading'), - children: [ + {FieldSet.component( + { + label: app.translator.trans('core.admin.basics.forum_description_heading'), + }, + [
{app.translator.trans('core.admin.basics.forum_description_text')}
, -