feat: advanced maintenance modes (#3977)

* feat: low maintenance mode (maintenance with admin access) (#3975)
* feat: low maintenance mode (maintenance with admin access)
* Apply fixes from StyleCI
* chore: only required when basic
* chore: more concise code
* chore(review): enum
* feat: enable through settings
* Apply fixes from StyleCI
* core: typing
* feat: safe mode (#3978)
* feat: safe mode
* feat: add extension page warning
* feat: `safe_mode_extensions`
* Apply fixes from StyleCI
This commit is contained in:
Sami Mazouz
2024-05-03 14:05:58 +01:00
committed by GitHub
parent 2b917372a7
commit b8e17182e9
96 changed files with 5801 additions and 342 deletions

View File

@ -1,63 +0,0 @@
/*!
* quantize.js Copyright 2008 Nick Rabinowitz.
* Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
*/
/*!
* Sizzle CSS Selector Engine v2.3.6
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2021-02-16
*/
/*!
* Block below copied from Protovis: http://mbostock.github.com/protovis/
* Copyright 2010 Stanford Visualization Group
* Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
*/
/*!
* Color Thief v2.0
* by Lokesh Dhakar - http://www.lokeshdhakar.com
*
* Thanks
* ------
* Nick Rabinowitz - For creating quantize.js.
* John Schulz - For clean up and optimization. @JFSIII
* Nathan Spady - For adding drag and drop support to the demo page.
*
* License
* -------
* Copyright 2011, 2015 Lokesh Dhakar
* Released under the MIT license
* https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE
*
*/
/*!
* jQuery JavaScript Library v3.6.1
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2022-08-26T17:52Z
*/
/*!
* focus-trap 6.9.4
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
*/
/*!
* tabbable 5.3.3
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
*/

View File

@ -1,2 +1,201 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[841],{4292:(s,t,i)=>{i.r(t),i.d(t,{default:()=>p});var e=i(7905),r=i(7465),a=i(899),n=i(8312),o=i(6697),d=i(7645),l=i(1552),u=i(4041),h=i(6458),c=i(6352);class p extends a.Z{constructor(){super(...arguments),(0,e.Z)(this,"username",void 0),(0,e.Z)(this,"email",void 0),(0,e.Z)(this,"isEmailConfirmed",void 0),(0,e.Z)(this,"setPassword",void 0),(0,e.Z)(this,"password",void 0),(0,e.Z)(this,"groups",{})}oninit(s){super.oninit(s);const t=this.attrs.user;this.username=(0,h.Z)(t.username()||""),this.email=(0,h.Z)(t.email()||""),this.isEmailConfirmed=(0,h.Z)(t.isEmailConfirmed()||!1),this.setPassword=(0,h.Z)(!1),this.password=(0,h.Z)(t.password()||"");const i=t.groups()||[];r.Z.store.all("groups").filter((s=>![d.Z.GUEST_ID,d.Z.MEMBER_ID].includes(s.id()))).forEach((s=>this.groups[s.id()]=(0,h.Z)(i.includes(s))))}className(){return"EditUserModal Modal--small"}title(){return r.Z.translator.trans("core.lib.edit_user.title")}content(){const s=this.fields().toArray();return m("div",{className:"Modal-body"},s.length>1?m(c.Z,null,this.fields().toArray()):r.Z.translator.trans("core.lib.edit_user.nothing_available"))}fields(){const s=new u.Z;return this.attrs.user.canEditCredentials()&&(s.add("username",m("div",{className:"Form-group"},m("label",null,r.Z.translator.trans("core.lib.edit_user.username_heading")),m("input",{className:"FormControl",placeholder:(0,l.Z)(r.Z.translator.trans("core.lib.edit_user.username_label")),bidi:this.username,disabled:this.nonAdminEditingAdmin()})),40),r.Z.session.user!==this.attrs.user&&(s.add("email",m("div",{className:"Form-group"},m("label",null,r.Z.translator.trans("core.lib.edit_user.email_heading")),m("input",{className:"FormControl",placeholder:(0,l.Z)(r.Z.translator.trans("core.lib.edit_user.email_label")),bidi:this.email,disabled:this.nonAdminEditingAdmin()}),!this.isEmailConfirmed()&&this.userIsAdmin(r.Z.session.user)&&m(n.Z,{className:"Button Button--block",loading:this.loading,onclick:this.activate.bind(this)},r.Z.translator.trans("core.lib.edit_user.activate_button"))),30),s.add("password",m("div",{className:"Form-group"},m("label",null,r.Z.translator.trans("core.lib.edit_user.password_heading")),m("div",null,m("label",{className:"checkbox"},m("input",{type:"checkbox",onchange:s=>{const t=s.target;this.setPassword(t.checked),m.redraw.sync(),t.checked&&this.$("[name=password]").select(),s.redraw=!1},disabled:this.nonAdminEditingAdmin()}),r.Z.translator.trans("core.lib.edit_user.set_password_label"))),this.setPassword()&&m("input",{className:"FormControl",type:"password",name:"password",placeholder:(0,l.Z)(r.Z.translator.trans("core.lib.edit_user.password_label")),bidi:this.password,disabled:this.nonAdminEditingAdmin()})),20))),this.attrs.user.canEditGroups()&&s.add("groups",m("div",{className:"Form-group EditUserModal-groups"},m("label",null,r.Z.translator.trans("core.lib.edit_user.groups_heading")),m("div",null,Object.keys(this.groups).map((s=>r.Z.store.getById("groups",s))).filter(Boolean).map((s=>s&&m("label",{className:"checkbox"},m("input",{type:"checkbox",bidi:this.groups[s.id()],disabled:s.id()===d.Z.ADMINISTRATOR_ID&&(this.attrs.user===r.Z.session.user||!this.userIsAdmin(r.Z.session.user))}),m(o.Z,{group:s,label:null})," ",s.nameSingular()))))),10),s.add("submit",m("div",{className:"Form-group Form-controls"},m(n.Z,{className:"Button Button--primary",type:"submit",loading:this.loading},r.Z.translator.trans("core.lib.edit_user.submit_button"))),-10),s}activate(){this.loading=!0;const s={username:this.username(),isEmailConfirmed:!0};this.attrs.user.save(s,{errorHandler:this.onerror.bind(this)}).then((()=>{this.isEmailConfirmed(!0),this.loading=!1,m.redraw()})).catch((()=>{this.loading=!1,m.redraw()}))}data(){const s={},t={};return this.attrs.user.canEditCredentials()&&!this.nonAdminEditingAdmin()&&(s.username=this.username(),r.Z.session.user!==this.attrs.user&&(s.email=this.email()),this.setPassword()&&(s.password=this.password())),this.attrs.user.canEditGroups()&&(t.groups=Object.keys(this.groups).filter((s=>this.groups[s]())).map((s=>r.Z.store.getById("groups",s))).filter((s=>s instanceof d.Z))),s.relationships=t,s}onsubmit(s){s.preventDefault(),this.loading=!0,this.attrs.user.save(this.data(),{errorHandler:this.onerror.bind(this)}).then(this.hide.bind(this)).catch((()=>{this.loading=!1,m.redraw()}))}nonAdminEditingAdmin(){return this.userIsAdmin(this.attrs.user)&&!this.userIsAdmin(r.Z.session.user)}userIsAdmin(s){return!!((null==s?void 0:s.groups())||[]).some((s=>(null==s?void 0:s.id())===d.Z.ADMINISTRATOR_ID))}}flarum.reg.add("core","common/components/EditUserModal",p)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["common/components/EditUserModal"],{
/***/ "./src/common/components/EditUserModal.tsx":
/*!*************************************************!*\
!*** ./src/common/components/EditUserModal.tsx ***!
\*************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ EditUserModal)
/* harmony export */ });
/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js");
/* harmony import */ var _common_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/app */ "./src/common/app.ts");
/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx");
/* harmony import */ var _Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./Button */ "./src/common/components/Button.tsx");
/* harmony import */ var _GroupBadge__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./GroupBadge */ "./src/common/components/GroupBadge.tsx");
/* harmony import */ var _models_Group__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../models/Group */ "./src/common/models/Group.ts");
/* harmony import */ var _utils_extractText__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../utils/extractText */ "./src/common/utils/extractText.ts");
/* harmony import */ var _utils_ItemList__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../utils/ItemList */ "./src/common/utils/ItemList.ts");
/* harmony import */ var _utils_Stream__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../utils/Stream */ "./src/common/utils/Stream.ts");
/* harmony import */ var _Form__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./Form */ "./src/common/components/Form.tsx");
class EditUserModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] {
constructor() {
super(...arguments);
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "username", void 0);
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "email", void 0);
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "isEmailConfirmed", void 0);
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "setPassword", void 0);
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "password", void 0);
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "groups", {});
}
oninit(vnode) {
super.oninit(vnode);
const user = this.attrs.user;
this.username = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(user.username() || '');
this.email = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(user.email() || '');
this.isEmailConfirmed = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(user.isEmailConfirmed() || false);
this.setPassword = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(false);
this.password = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(user.password() || '');
const userGroups = user.groups() || [];
_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].store.all('groups').filter(group => ![_models_Group__WEBPACK_IMPORTED_MODULE_5__["default"].GUEST_ID, _models_Group__WEBPACK_IMPORTED_MODULE_5__["default"].MEMBER_ID].includes(group.id())).forEach(group => this.groups[group.id()] = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(userGroups.includes(group)));
}
className() {
return 'EditUserModal Modal--small';
}
title() {
return _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.title');
}
content() {
const fields = this.fields().toArray();
return m("div", {
className: "Modal-body"
}, fields.length > 1 ? m(_Form__WEBPACK_IMPORTED_MODULE_9__["default"], null, this.fields().toArray()) : _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.nothing_available'));
}
fields() {
const items = new _utils_ItemList__WEBPACK_IMPORTED_MODULE_7__["default"]();
if (this.attrs.user.canEditCredentials()) {
items.add('username', m("div", {
className: "Form-group"
}, m("label", null, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.username_heading')), m("input", {
className: "FormControl",
placeholder: (0,_utils_extractText__WEBPACK_IMPORTED_MODULE_6__["default"])(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.username_label')),
bidi: this.username,
disabled: this.nonAdminEditingAdmin()
})), 40);
if (_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user !== this.attrs.user) {
items.add('email', m("div", {
className: "Form-group"
}, m("label", null, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.email_heading')), m("input", {
className: "FormControl",
placeholder: (0,_utils_extractText__WEBPACK_IMPORTED_MODULE_6__["default"])(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.email_label')),
bidi: this.email,
disabled: this.nonAdminEditingAdmin()
}), !this.isEmailConfirmed() && this.userIsAdmin(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user) && m(_Button__WEBPACK_IMPORTED_MODULE_3__["default"], {
className: "Button Button--block",
loading: this.loading,
onclick: this.activate.bind(this)
}, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.activate_button'))), 30);
items.add('password', m("div", {
className: "Form-group"
}, m("label", null, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.password_heading')), m("div", null, m("label", {
className: "checkbox"
}, m("input", {
type: "checkbox",
onchange: e => {
const target = e.target;
this.setPassword(target.checked);
m.redraw.sync();
if (target.checked) this.$('[name=password]').select();
e.redraw = false;
},
disabled: this.nonAdminEditingAdmin()
}), _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.set_password_label'))), this.setPassword() && m("input", {
className: "FormControl",
type: "password",
name: "password",
placeholder: (0,_utils_extractText__WEBPACK_IMPORTED_MODULE_6__["default"])(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.password_label')),
bidi: this.password,
disabled: this.nonAdminEditingAdmin()
})), 20);
}
}
if (this.attrs.user.canEditGroups()) {
items.add('groups', m("div", {
className: "Form-group EditUserModal-groups"
}, m("label", null, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.groups_heading')), m("div", null, Object.keys(this.groups).map(id => _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].store.getById('groups', id)).filter(Boolean).map(group =>
// Necessary because filter(Boolean) doesn't narrow out falsy values.
group && m("label", {
className: "checkbox"
}, m("input", {
type: "checkbox",
bidi: this.groups[group.id()],
disabled: group.id() === _models_Group__WEBPACK_IMPORTED_MODULE_5__["default"].ADMINISTRATOR_ID && (this.attrs.user === _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user || !this.userIsAdmin(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user))
}), m(_GroupBadge__WEBPACK_IMPORTED_MODULE_4__["default"], {
group: group,
label: null
}), " ", group.nameSingular())))), 10);
}
items.add('submit', m("div", {
className: "Form-group Form-controls"
}, m(_Button__WEBPACK_IMPORTED_MODULE_3__["default"], {
className: "Button Button--primary",
type: "submit",
loading: this.loading
}, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.submit_button'))), -10);
return items;
}
activate() {
this.loading = true;
const data = {
username: this.username(),
isEmailConfirmed: true
};
this.attrs.user.save(data, {
errorHandler: this.onerror.bind(this)
}).then(() => {
this.isEmailConfirmed(true);
this.loading = false;
m.redraw();
}).catch(() => {
this.loading = false;
m.redraw();
});
}
data() {
const data = {};
const relationships = {};
if (this.attrs.user.canEditCredentials() && !this.nonAdminEditingAdmin()) {
data.username = this.username();
if (_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user !== this.attrs.user) {
data.email = this.email();
}
if (this.setPassword()) {
data.password = this.password();
}
}
if (this.attrs.user.canEditGroups()) {
relationships.groups = Object.keys(this.groups).filter(id => this.groups[id]()).map(id => _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].store.getById('groups', id)).filter(g => g instanceof _models_Group__WEBPACK_IMPORTED_MODULE_5__["default"]);
}
data.relationships = relationships;
return data;
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
this.attrs.user.save(this.data(), {
errorHandler: this.onerror.bind(this)
}).then(this.hide.bind(this)).catch(() => {
this.loading = false;
m.redraw();
});
}
nonAdminEditingAdmin() {
return this.userIsAdmin(this.attrs.user) && !this.userIsAdmin(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user);
}
/**
* @internal
*/
userIsAdmin(user) {
return !!((user == null ? void 0 : user.groups()) || []).some(g => (g == null ? void 0 : g.id()) === _models_Group__WEBPACK_IMPORTED_MODULE_5__["default"].ADMINISTRATOR_ID);
}
}
flarum.reg.add('core', 'common/components/EditUserModal', EditUserModal);
/***/ })
}]);
//# sourceMappingURL=EditUserModal.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,63 +0,0 @@
/*!
* quantize.js Copyright 2008 Nick Rabinowitz.
* Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
*/
/*!
* Sizzle CSS Selector Engine v2.3.6
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2021-02-16
*/
/*!
* Block below copied from Protovis: http://mbostock.github.com/protovis/
* Copyright 2010 Stanford Visualization Group
* Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
*/
/*!
* Color Thief v2.0
* by Lokesh Dhakar - http://www.lokeshdhakar.com
*
* Thanks
* ------
* Nick Rabinowitz - For creating quantize.js.
* John Schulz - For clean up and optimization. @JFSIII
* Nathan Spady - For adding drag and drop support to the demo page.
*
* License
* -------
* Copyright 2011, 2015 Lokesh Dhakar
* Released under the MIT license
* https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE
*
*/
/*!
* jQuery JavaScript Library v3.6.1
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2022-08-26T17:52Z
*/
/*!
* focus-trap 6.9.4
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
*/
/*!
* tabbable 5.3.3
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,288 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[437],{2140:(s,e,t)=>{t.d(e,{Z:()=>h});var o=t(2190),i=t(5226);class r extends o.Z{handler(){return this.attrs.when()||void 0}oncreate(s){super.oncreate(s),this.boundHandler=this.handler.bind(this),$(window).on("beforeunload",this.boundHandler)}onremove(s){super.onremove(s),$(window).off("beforeunload",this.boundHandler)}view(s){return m("[",null,s.children)}}flarum.reg.add("core","common/components/ConfirmDocumentUnload",r);var n=t(4944),a=t(1268),d=t(4041),l=t(3344),c=t(7323);class h extends o.Z{oninit(s){super.oninit(s),this.composer=this.attrs.composer,this.loading=!1,this.attrs.confirmExit&&this.composer.preventClosingWhen((()=>this.hasChanges()),this.attrs.confirmExit),this.composer.fields.content(this.attrs.originalContent||"")}view(){var s;return m(r,{when:this.hasChanges.bind(this)},m("div",{className:(0,l.Z)("ComposerBody",this.attrs.className)},m(c.Z,{user:this.attrs.user,className:"ComposerBody-avatar"}),m("div",{className:"ComposerBody-content"},m("ul",{className:"ComposerBody-header"},(0,a.Z)(this.headerItems().toArray())),m("div",{className:"ComposerBody-editor"},m(n.Z,{submitLabel:this.attrs.submitLabel,placeholder:this.attrs.placeholder,disabled:this.loading||this.attrs.disabled,composer:this.composer,preview:null==(s=this.jumpToPreview)?void 0:s.bind(this),onchange:this.composer.fields.content,onsubmit:this.onsubmit.bind(this),value:this.composer.fields.content()}))),m(i.Z,{display:"unset",containerClassName:(0,l.Z)("ComposerBody-loading",this.loading&&"active"),size:"large"})))}hasChanges(){const s=this.composer.fields.content();return s&&s!==this.attrs.originalContent}headerItems(){return new d.Z}onsubmit(){}loaded(){this.loading=!1,m.redraw()}}flarum.reg.add("core","forum/components/ComposerBody",h)},3829:(s,e,t)=>{t.r(e),t.d(e,{default:()=>a});var o=t(6789),i=t(2140),r=t(1552),n=t(6458);class a extends i.Z{static initAttrs(s){super.initAttrs(s),s.placeholder=s.placeholder||(0,r.Z)(o.Z.translator.trans("core.forum.composer_discussion.body_placeholder")),s.submitLabel=s.submitLabel||o.Z.translator.trans("core.forum.composer_discussion.submit_button"),s.confirmExit=s.confirmExit||(0,r.Z)(o.Z.translator.trans("core.forum.composer_discussion.discard_confirmation")),s.titlePlaceholder=s.titlePlaceholder||(0,r.Z)(o.Z.translator.trans("core.forum.composer_discussion.title_placeholder")),s.className="ComposerBody--discussion"}oninit(s){super.oninit(s),this.composer.fields.title=this.composer.fields.title||(0,n.Z)(""),this.title=this.composer.fields.title}headerItems(){const s=super.headerItems();return s.add("title",m("h3",null,o.Z.translator.trans("core.forum.composer_discussion.title")),100),s.add("discussionTitle",m("h3",null,m("input",{className:"FormControl",bidi:this.title,placeholder:this.attrs.titlePlaceholder,disabled:!!this.attrs.disabled,onkeydown:this.onkeydown.bind(this)}))),s}onkeydown(s){13===s.which&&(s.preventDefault(),this.composer.editor.moveCursorTo(0)),s.redraw=!1}hasChanges(){return this.title()||this.composer.fields.content()}data(){return{title:this.title(),content:this.composer.fields.content()}}onsubmit(){this.loading=!0;const s=this.data();o.Z.store.createRecord("discussions").save(s).then((s=>{this.composer.hide(),o.Z.discussions.refresh(),m.route.set(o.Z.route.discussion(s))}),this.loaded.bind(this))}}flarum.reg.add("core","forum/components/DiscussionComposer",a)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/DiscussionComposer"],{
/***/ "./src/common/components/ConfirmDocumentUnload.js":
/*!********************************************************!*\
!*** ./src/common/components/ConfirmDocumentUnload.js ***!
\********************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ ConfirmDocumentUnload)
/* harmony export */ });
/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ "./src/common/Component.ts");
/**
* The `ConfirmDocumentUnload` component can be used to register a global
* event handler that prevents closing the browser window/tab based on the
* return value of a given callback prop.
*
* ### Attrs
*
* - `when` - a callback returning true when the browser should prompt for
* confirmation before closing the window/tab
*/
class ConfirmDocumentUnload extends _Component__WEBPACK_IMPORTED_MODULE_0__["default"] {
handler() {
return this.attrs.when() || undefined;
}
oncreate(vnode) {
super.oncreate(vnode);
this.boundHandler = this.handler.bind(this);
$(window).on('beforeunload', this.boundHandler);
}
onremove(vnode) {
super.onremove(vnode);
$(window).off('beforeunload', this.boundHandler);
}
view(vnode) {
return m('[', null, vnode.children);
}
}
flarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);
/***/ }),
/***/ "./src/forum/components/ComposerBody.js":
/*!**********************************************!*\
!*** ./src/forum/components/ComposerBody.js ***!
\**********************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ ComposerBody)
/* harmony export */ });
/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts");
/* harmony import */ var _common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/LoadingIndicator */ "./src/common/components/LoadingIndicator.tsx");
/* harmony import */ var _common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/ConfirmDocumentUnload */ "./src/common/components/ConfirmDocumentUnload.js");
/* harmony import */ var _common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/TextEditor */ "./src/common/components/TextEditor.js");
/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx");
/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts");
/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts");
/* harmony import */ var _common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Avatar */ "./src/common/components/Avatar.tsx");
/**
* The `ComposerBody` component handles the body, or the content, of the
* composer. Subclasses should implement the `onsubmit` method and override
* `headerTimes`.
*
* ### Attrs
*
* - `composer`
* - `originalContent`
* - `submitLabel`
* - `placeholder`
* - `user`
* - `confirmExit`
* - `disabled`
*
* @abstract
*/
class ComposerBody extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] {
oninit(vnode) {
super.oninit(vnode);
this.composer = this.attrs.composer;
/**
* Whether or not the component is loading.
*
* @type {Boolean}
*/
this.loading = false;
// Let the composer state know to ask for confirmation under certain
// circumstances, if the body supports / requires it and has a corresponding
// confirmation question to ask.
if (this.attrs.confirmExit) {
this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);
}
this.composer.fields.content(this.attrs.originalContent || '');
}
view() {
var _this$jumpToPreview;
return m(_common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__["default"], {
when: this.hasChanges.bind(this)
}, m("div", {
className: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody', this.attrs.className)
}, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__["default"], {
user: this.attrs.user,
className: "ComposerBody-avatar"
}), m("div", {
className: "ComposerBody-content"
}, m("ul", {
className: "ComposerBody-header"
}, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__["default"])(this.headerItems().toArray())), m("div", {
className: "ComposerBody-editor"
}, m(_common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__["default"], {
submitLabel: this.attrs.submitLabel,
placeholder: this.attrs.placeholder,
disabled: this.loading || this.attrs.disabled,
composer: this.composer,
preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),
onchange: this.composer.fields.content,
onsubmit: this.onsubmit.bind(this),
value: this.composer.fields.content()
}))), m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__["default"], {
display: "unset",
containerClassName: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody-loading', this.loading && 'active'),
size: "large"
})));
}
/**
* Check if there is any unsaved data.
*
* @return {boolean}
*/
hasChanges() {
const content = this.composer.fields.content();
return content && content !== this.attrs.originalContent;
}
/**
* Build an item list for the composer's header.
*
* @return {ItemList<import('mithril').Children>}
*/
headerItems() {
return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"]();
}
/**
* Handle the submit event of the text editor.
*
* @abstract
*/
onsubmit() {}
/**
* Stop loading.
*/
loaded() {
this.loading = false;
m.redraw();
}
}
flarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);
/***/ }),
/***/ "./src/forum/components/DiscussionComposer.js":
/*!****************************************************!*\
!*** ./src/forum/components/DiscussionComposer.js ***!
\****************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ DiscussionComposer)
/* harmony export */ });
/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts");
/* harmony import */ var _ComposerBody__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ComposerBody */ "./src/forum/components/ComposerBody.js");
/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts");
/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts");
/**
* The `DiscussionComposer` component displays the composer content for starting
* a new discussion. It adds a text field as a header control so the user can
* enter the title of their discussion. It also overrides the `submit` and
* `willExit` actions to account for the title.
*
* ### Attrs
*
* - All of the attrs for ComposerBody
* - `titlePlaceholder`
*/
class DiscussionComposer extends _ComposerBody__WEBPACK_IMPORTED_MODULE_1__["default"] {
static initAttrs(attrs) {
super.initAttrs(attrs);
attrs.placeholder = attrs.placeholder || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_2__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.body_placeholder'));
attrs.submitLabel = attrs.submitLabel || _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.submit_button');
attrs.confirmExit = attrs.confirmExit || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_2__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.discard_confirmation'));
attrs.titlePlaceholder = attrs.titlePlaceholder || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_2__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.title_placeholder'));
attrs.className = 'ComposerBody--discussion';
}
oninit(vnode) {
super.oninit(vnode);
this.composer.fields.title = this.composer.fields.title || (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_3__["default"])('');
/**
* The value of the title input.
*
* @type {Function}
*/
this.title = this.composer.fields.title;
}
headerItems() {
const items = super.headerItems();
items.add('title', m("h3", null, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.title')), 100);
items.add('discussionTitle', m("h3", null, m("input", {
className: "FormControl",
bidi: this.title,
placeholder: this.attrs.titlePlaceholder,
disabled: !!this.attrs.disabled,
onkeydown: this.onkeydown.bind(this)
})));
return items;
}
/**
* Handle the title input's keydown event. When the return key is pressed,
* move the focus to the start of the text editor.
*
* @param {KeyboardEvent} e
*/
onkeydown(e) {
if (e.which === 13) {
// Return
e.preventDefault();
this.composer.editor.moveCursorTo(0);
}
e.redraw = false;
}
hasChanges() {
return this.title() || this.composer.fields.content();
}
/**
* Get the data to submit to the server when the discussion is saved.
*
* @return {Record<string, unknown>}
*/
data() {
return {
title: this.title(),
content: this.composer.fields.content()
};
}
onsubmit() {
this.loading = true;
const data = this.data();
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].store.createRecord('discussions').save(data).then(discussion => {
this.composer.hide();
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].discussions.refresh();
m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.discussion(discussion));
}, this.loaded.bind(this));
}
}
flarum.reg.add('core', 'forum/components/DiscussionComposer', DiscussionComposer);
/***/ })
}]);
//# sourceMappingURL=DiscussionComposer.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,52 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[799],{6466:(s,e,r)=>{r.r(e),r.d(e,{default:()=>u});var t=r(3390),a=r(8421),n=r(1654);class u extends t.Z{oninit(s){super.oninit(s),this.loadUser(m.route.param("username"))}show(s){super.show(s),this.state=new n.Z({filter:{author:s.username()},sort:"newest"}),this.state.refresh()}content(){return m("div",{className:"DiscussionsUserPage"},m(a.Z,{state:this.state}))}}flarum.reg.add("core","forum/components/DiscussionsUserPage",u)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/DiscussionsUserPage"],{
/***/ "./src/forum/components/DiscussionsUserPage.tsx":
/*!******************************************************!*\
!*** ./src/forum/components/DiscussionsUserPage.tsx ***!
\******************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ DiscussionsUserPage)
/* harmony export */ });
/* harmony import */ var _UserPage__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./UserPage */ "./src/forum/components/UserPage.tsx");
/* harmony import */ var _DiscussionList__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./DiscussionList */ "./src/forum/components/DiscussionList.js");
/* harmony import */ var _states_DiscussionListState__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../states/DiscussionListState */ "./src/forum/states/DiscussionListState.ts");
/**
* The `DiscussionsUserPage` component shows a discussion list inside of a user
* page.
*/
class DiscussionsUserPage extends _UserPage__WEBPACK_IMPORTED_MODULE_0__["default"] {
oninit(vnode) {
super.oninit(vnode);
this.loadUser(m.route.param('username'));
}
show(user) {
super.show(user);
this.state = new _states_DiscussionListState__WEBPACK_IMPORTED_MODULE_2__["default"]({
filter: {
author: user.username()
},
sort: 'newest'
});
this.state.refresh();
}
content() {
return m("div", {
className: "DiscussionsUserPage"
}, m(_DiscussionList__WEBPACK_IMPORTED_MODULE_1__["default"], {
state: this.state
}));
}
}
flarum.reg.add('core', 'forum/components/DiscussionsUserPage', DiscussionsUserPage);
/***/ })
}]);
//# sourceMappingURL=DiscussionsUserPage.js.map

View File

@ -1 +1 @@
{"version":3,"file":"forum/components/DiscussionsUserPage.js","mappings":"yKAOe,MAAMA,UAA4B,IAC/CC,OAAOC,GACLC,MAAMF,OAAOC,GACbE,KAAKC,SAASC,EAAEC,MAAMC,MAAM,YAC9B,CACAC,KAAKC,GACHP,MAAMM,KAAKC,GACXN,KAAKO,MAAQ,IAAI,IAAoB,CACnCC,OAAQ,CACNC,OAAQH,EAAKI,YAEfC,KAAM,WAERX,KAAKO,MAAMK,SACb,CACAC,UACE,OAAOX,EAAE,MAAO,CACdY,UAAW,uBACVZ,EAAE,IAAgB,CACnBK,MAAOP,KAAKO,QAEhB,EAEFQ,OAAOC,IAAIC,IAAI,OAAQ,uCAAwCrB,E","sources":["webpack://@flarum/core/./src/forum/components/DiscussionsUserPage.tsx"],"sourcesContent":["import UserPage from './UserPage';\nimport DiscussionList from './DiscussionList';\nimport DiscussionListState from '../states/DiscussionListState';\n/**\n * The `DiscussionsUserPage` component shows a discussion list inside of a user\n * page.\n */\nexport default class DiscussionsUserPage extends UserPage {\n oninit(vnode) {\n super.oninit(vnode);\n this.loadUser(m.route.param('username'));\n }\n show(user) {\n super.show(user);\n this.state = new DiscussionListState({\n filter: {\n author: user.username()\n },\n sort: 'newest'\n });\n this.state.refresh();\n }\n content() {\n return m(\"div\", {\n className: \"DiscussionsUserPage\"\n }, m(DiscussionList, {\n state: this.state\n }));\n }\n}\nflarum.reg.add('core', 'forum/components/DiscussionsUserPage', DiscussionsUserPage);"],"names":["DiscussionsUserPage","oninit","vnode","super","this","loadUser","m","route","param","show","user","state","filter","author","username","sort","refresh","content","className","flarum","reg","add"],"sourceRoot":""}
{"version":3,"file":"forum/components/DiscussionsUserPage.js","mappings":";;;;;;;;;;;;;;;;AAAkC;AACY;AACkB;AAChE;AACA;AACA;AACA;AACe,kCAAkC,iDAAQ;AACzD;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,mEAAmB;AACxC;AACA;AACA,OAAO;AACP;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA,KAAK,IAAI,uDAAc;AACvB;AACA,KAAK;AACL;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/DiscussionsUserPage.tsx"],"sourcesContent":["import UserPage from './UserPage';\nimport DiscussionList from './DiscussionList';\nimport DiscussionListState from '../states/DiscussionListState';\n/**\n * The `DiscussionsUserPage` component shows a discussion list inside of a user\n * page.\n */\nexport default class DiscussionsUserPage extends UserPage {\n oninit(vnode) {\n super.oninit(vnode);\n this.loadUser(m.route.param('username'));\n }\n show(user) {\n super.show(user);\n this.state = new DiscussionListState({\n filter: {\n author: user.username()\n },\n sort: 'newest'\n });\n this.state.refresh();\n }\n content() {\n return m(\"div\", {\n className: \"DiscussionsUserPage\"\n }, m(DiscussionList, {\n state: this.state\n }));\n }\n}\nflarum.reg.add('core', 'forum/components/DiscussionsUserPage', DiscussionsUserPage);"],"names":[],"sourceRoot":""}

View File

@ -1,2 +1,293 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[293],{2140:(t,s,e)=>{e.d(s,{Z:()=>u});var o=e(2190),r=e(5226);class i extends o.Z{handler(){return this.attrs.when()||void 0}oncreate(t){super.oncreate(t),this.boundHandler=this.handler.bind(this),$(window).on("beforeunload",this.boundHandler)}onremove(t){super.onremove(t),$(window).off("beforeunload",this.boundHandler)}view(t){return m("[",null,t.children)}}flarum.reg.add("core","common/components/ConfirmDocumentUnload",i);var n=e(4944),a=e(1268),d=e(4041),c=e(3344),l=e(7323);class u extends o.Z{oninit(t){super.oninit(t),this.composer=this.attrs.composer,this.loading=!1,this.attrs.confirmExit&&this.composer.preventClosingWhen((()=>this.hasChanges()),this.attrs.confirmExit),this.composer.fields.content(this.attrs.originalContent||"")}view(){var t;return m(i,{when:this.hasChanges.bind(this)},m("div",{className:(0,c.Z)("ComposerBody",this.attrs.className)},m(l.Z,{user:this.attrs.user,className:"ComposerBody-avatar"}),m("div",{className:"ComposerBody-content"},m("ul",{className:"ComposerBody-header"},(0,a.Z)(this.headerItems().toArray())),m("div",{className:"ComposerBody-editor"},m(n.Z,{submitLabel:this.attrs.submitLabel,placeholder:this.attrs.placeholder,disabled:this.loading||this.attrs.disabled,composer:this.composer,preview:null==(t=this.jumpToPreview)?void 0:t.bind(this),onchange:this.composer.fields.content,onsubmit:this.onsubmit.bind(this),value:this.composer.fields.content()}))),m(r.Z,{display:"unset",containerClassName:(0,c.Z)("ComposerBody-loading",this.loading&&"active"),size:"large"})))}hasChanges(){const t=this.composer.fields.content();return t&&t!==this.attrs.originalContent}headerItems(){return new d.Z}onsubmit(){}loaded(){this.loading=!1,m.redraw()}}flarum.reg.add("core","forum/components/ComposerBody",u)},500:(t,s,e)=>{e.r(s),e.d(s,{default:()=>c});var o=e(6789),r=e(2140),i=e(8312),n=e(6597),a=e(9133);function d(t){o.Z.composer.isFullScreen()&&(o.Z.composer.minimize(),t.stopPropagation())}class c extends r.Z{static initAttrs(t){super.initAttrs(t),t.submitLabel=t.submitLabel||o.Z.translator.trans("core.forum.composer_edit.submit_button"),t.confirmExit=t.confirmExit||o.Z.translator.trans("core.forum.composer_edit.discard_confirmation"),t.originalContent=t.originalContent||t.post.content(),t.user=t.user||t.post.user(),t.post.editedContent=t.originalContent}headerItems(){const t=super.headerItems(),s=this.attrs.post;return t.add("title",m("h3",null,m(a.Z,{name:"fas fa-pencil-alt"})," ",m(n.Z,{href:o.Z.route.discussion(s.discussion(),s.number()),onclick:d},o.Z.translator.trans("core.forum.composer_edit.post_link",{number:s.number(),discussion:s.discussion().title()})))),t}jumpToPreview(t){d(t),m.route.set(o.Z.route.post(this.attrs.post))}data(){return{content:this.composer.fields.content()}}onsubmit(){const t=this.attrs.post.discussion();this.loading=!0;const s=this.data();this.attrs.post.save(s).then((s=>{if(o.Z.viewingDiscussion(t))o.Z.current.get("stream").goToNumber(s.number());else{const t=o.Z.alerts.show({type:"success",controls:[m(i.Z,{className:"Button Button--link",onclick:()=>{m.route.set(o.Z.route.post(s)),o.Z.alerts.dismiss(t)}},o.Z.translator.trans("core.forum.composer_edit.view_button"))]},o.Z.translator.trans("core.forum.composer_edit.edited_message"))}this.composer.hide()}),this.loaded.bind(this))}}flarum.reg.add("core","forum/components/EditPostComposer",c)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/EditPostComposer"],{
/***/ "./src/common/components/ConfirmDocumentUnload.js":
/*!********************************************************!*\
!*** ./src/common/components/ConfirmDocumentUnload.js ***!
\********************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ ConfirmDocumentUnload)
/* harmony export */ });
/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ "./src/common/Component.ts");
/**
* The `ConfirmDocumentUnload` component can be used to register a global
* event handler that prevents closing the browser window/tab based on the
* return value of a given callback prop.
*
* ### Attrs
*
* - `when` - a callback returning true when the browser should prompt for
* confirmation before closing the window/tab
*/
class ConfirmDocumentUnload extends _Component__WEBPACK_IMPORTED_MODULE_0__["default"] {
handler() {
return this.attrs.when() || undefined;
}
oncreate(vnode) {
super.oncreate(vnode);
this.boundHandler = this.handler.bind(this);
$(window).on('beforeunload', this.boundHandler);
}
onremove(vnode) {
super.onremove(vnode);
$(window).off('beforeunload', this.boundHandler);
}
view(vnode) {
return m('[', null, vnode.children);
}
}
flarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);
/***/ }),
/***/ "./src/forum/components/ComposerBody.js":
/*!**********************************************!*\
!*** ./src/forum/components/ComposerBody.js ***!
\**********************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ ComposerBody)
/* harmony export */ });
/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts");
/* harmony import */ var _common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/LoadingIndicator */ "./src/common/components/LoadingIndicator.tsx");
/* harmony import */ var _common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/ConfirmDocumentUnload */ "./src/common/components/ConfirmDocumentUnload.js");
/* harmony import */ var _common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/TextEditor */ "./src/common/components/TextEditor.js");
/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx");
/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts");
/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts");
/* harmony import */ var _common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Avatar */ "./src/common/components/Avatar.tsx");
/**
* The `ComposerBody` component handles the body, or the content, of the
* composer. Subclasses should implement the `onsubmit` method and override
* `headerTimes`.
*
* ### Attrs
*
* - `composer`
* - `originalContent`
* - `submitLabel`
* - `placeholder`
* - `user`
* - `confirmExit`
* - `disabled`
*
* @abstract
*/
class ComposerBody extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] {
oninit(vnode) {
super.oninit(vnode);
this.composer = this.attrs.composer;
/**
* Whether or not the component is loading.
*
* @type {Boolean}
*/
this.loading = false;
// Let the composer state know to ask for confirmation under certain
// circumstances, if the body supports / requires it and has a corresponding
// confirmation question to ask.
if (this.attrs.confirmExit) {
this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);
}
this.composer.fields.content(this.attrs.originalContent || '');
}
view() {
var _this$jumpToPreview;
return m(_common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__["default"], {
when: this.hasChanges.bind(this)
}, m("div", {
className: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody', this.attrs.className)
}, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__["default"], {
user: this.attrs.user,
className: "ComposerBody-avatar"
}), m("div", {
className: "ComposerBody-content"
}, m("ul", {
className: "ComposerBody-header"
}, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__["default"])(this.headerItems().toArray())), m("div", {
className: "ComposerBody-editor"
}, m(_common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__["default"], {
submitLabel: this.attrs.submitLabel,
placeholder: this.attrs.placeholder,
disabled: this.loading || this.attrs.disabled,
composer: this.composer,
preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),
onchange: this.composer.fields.content,
onsubmit: this.onsubmit.bind(this),
value: this.composer.fields.content()
}))), m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__["default"], {
display: "unset",
containerClassName: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody-loading', this.loading && 'active'),
size: "large"
})));
}
/**
* Check if there is any unsaved data.
*
* @return {boolean}
*/
hasChanges() {
const content = this.composer.fields.content();
return content && content !== this.attrs.originalContent;
}
/**
* Build an item list for the composer's header.
*
* @return {ItemList<import('mithril').Children>}
*/
headerItems() {
return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"]();
}
/**
* Handle the submit event of the text editor.
*
* @abstract
*/
onsubmit() {}
/**
* Stop loading.
*/
loaded() {
this.loading = false;
m.redraw();
}
}
flarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);
/***/ }),
/***/ "./src/forum/components/EditPostComposer.js":
/*!**************************************************!*\
!*** ./src/forum/components/EditPostComposer.js ***!
\**************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ EditPostComposer)
/* harmony export */ });
/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts");
/* harmony import */ var _ComposerBody__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ComposerBody */ "./src/forum/components/ComposerBody.js");
/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx");
/* harmony import */ var _common_components_Link__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Link */ "./src/common/components/Link.js");
/* harmony import */ var _common_components_Icon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/components/Icon */ "./src/common/components/Icon.tsx");
function minimizeComposerIfFullScreen(e) {
if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.isFullScreen()) {
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.minimize();
e.stopPropagation();
}
}
/**
* The `EditPostComposer` component displays the composer content for editing a
* post. It sets the initial content to the content of the post that is being
* edited, and adds a header control to indicate which post is being edited.
*
* ### Attrs
*
* - All of the attrs for ComposerBody
* - `post`
*/
class EditPostComposer extends _ComposerBody__WEBPACK_IMPORTED_MODULE_1__["default"] {
static initAttrs(attrs) {
super.initAttrs(attrs);
attrs.submitLabel = attrs.submitLabel || _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.submit_button');
attrs.confirmExit = attrs.confirmExit || _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.discard_confirmation');
attrs.originalContent = attrs.originalContent || attrs.post.content();
attrs.user = attrs.user || attrs.post.user();
attrs.post.editedContent = attrs.originalContent;
}
headerItems() {
const items = super.headerItems();
const post = this.attrs.post;
items.add('title', m("h3", null, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_4__["default"], {
name: 'fas fa-pencil-alt'
}), ' ', m(_common_components_Link__WEBPACK_IMPORTED_MODULE_3__["default"], {
href: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.discussion(post.discussion(), post.number()),
onclick: minimizeComposerIfFullScreen
}, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.post_link', {
number: post.number(),
discussion: post.discussion().title()
}))));
return items;
}
/**
* Jump to the preview when triggered by the text editor.
*/
jumpToPreview(e) {
minimizeComposerIfFullScreen(e);
m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.post(this.attrs.post));
}
/**
* Get the data to submit to the server when the post is saved.
*
* @return {Record<string, unknown>}
*/
data() {
return {
content: this.composer.fields.content()
};
}
onsubmit() {
const discussion = this.attrs.post.discussion();
this.loading = true;
const data = this.data();
this.attrs.post.save(data).then(post => {
// If we're currently viewing the discussion which this edit was made
// in, then we can scroll to the post.
if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].viewingDiscussion(discussion)) {
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].current.get('stream').goToNumber(post.number());
} else {
// Otherwise, we'll create an alert message to inform the user that
// their edit has been made, containing a button which will
// transition to their edited post when clicked.
const alert = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].alerts.show({
type: 'success',
controls: [m(_common_components_Button__WEBPACK_IMPORTED_MODULE_2__["default"], {
className: "Button Button--link",
onclick: () => {
m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.post(post));
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].alerts.dismiss(alert);
}
}, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.view_button'))]
}, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.edited_message'));
}
this.composer.hide();
}, this.loaded.bind(this));
}
}
flarum.reg.add('core', 'forum/components/EditPostComposer', EditPostComposer);
/***/ })
}]);
//# sourceMappingURL=EditPostComposer.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,129 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[502],{1839:(r,t,s)=>{s.r(t),s.d(t,{default:()=>c});var o=s(7905),a=s(6789),e=s(899),l=s(8312),i=s(1552),n=s(6458),d=s(4041),u=s(6352);class c extends e.Z{constructor(){super(...arguments),(0,o.Z)(this,"email",void 0),(0,o.Z)(this,"success",!1)}oninit(r){super.oninit(r),this.email=(0,n.Z)(this.attrs.email||"")}className(){return"ForgotPasswordModal Modal--small"}title(){return a.Z.translator.trans("core.forum.forgot_password.title")}content(){return this.success?m("div",{className:"Modal-body"},m(u.Z,{className:"Form--centered"},m("p",{className:"helpText"},a.Z.translator.trans("core.forum.forgot_password.email_sent_message")),m("div",{className:"Form-group Form-controls"},m(l.Z,{className:"Button Button--primary Button--block",onclick:this.hide.bind(this)},a.Z.translator.trans("core.forum.forgot_password.dismiss_button"))))):m("div",{className:"Modal-body"},m(u.Z,{className:"Form--centered",description:a.Z.translator.trans("core.forum.forgot_password.text")},this.fields().toArray()))}fields(){const r=new d.Z,t=(0,i.Z)(a.Z.translator.trans("core.forum.forgot_password.email_placeholder"));return r.add("email",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"email",type:"email",placeholder:t,"aria-label":t,bidi:this.email,disabled:this.loading})),50),r.add("submit",m("div",{className:"Form-group Form-controls"},m(l.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},a.Z.translator.trans("core.forum.forgot_password.submit_button"))),-10),r}onsubmit(r){r.preventDefault(),this.loading=!0,a.Z.request({method:"POST",url:a.Z.forum.attribute("apiUrl")+"/forgot",body:this.requestParams(),errorHandler:this.onerror.bind(this)}).then((()=>{this.success=!0,this.alertAttrs=null})).catch((()=>{})).then(this.loaded.bind(this))}requestParams(){return{email:this.email()}}onerror(r){404===r.status&&r.alert&&(r.alert.content=a.Z.translator.trans("core.forum.forgot_password.not_found_message")),super.onerror(r)}}flarum.reg.add("core","forum/components/ForgotPasswordModal",c)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/ForgotPasswordModal"],{
/***/ "./src/forum/components/ForgotPasswordModal.tsx":
/*!******************************************************!*\
!*** ./src/forum/components/ForgotPasswordModal.tsx ***!
\******************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ ForgotPasswordModal)
/* harmony export */ });
/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js");
/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts");
/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx");
/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx");
/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts");
/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts");
/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts");
/* harmony import */ var _common_components_Form__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Form */ "./src/common/components/Form.tsx");
/**
* The `ForgotPasswordModal` component displays a modal which allows the user to
* enter their email address and request a link to reset their password.
*/
class ForgotPasswordModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] {
constructor() {
super(...arguments);
/**
* The value of the email input.
*/
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "email", void 0);
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "success", false);
}
oninit(vnode) {
super.oninit(vnode);
this.email = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_5__["default"])(this.attrs.email || '');
}
className() {
return 'ForgotPasswordModal Modal--small';
}
title() {
return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.title');
}
content() {
if (this.success) {
return m("div", {
className: "Modal-body"
}, m(_common_components_Form__WEBPACK_IMPORTED_MODULE_7__["default"], {
className: "Form--centered"
}, m("p", {
className: "helpText"
}, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.email_sent_message')), m("div", {
className: "Form-group Form-controls"
}, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], {
className: "Button Button--primary Button--block",
onclick: this.hide.bind(this)
}, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.dismiss_button')))));
}
return m("div", {
className: "Modal-body"
}, m(_common_components_Form__WEBPACK_IMPORTED_MODULE_7__["default"], {
className: "Form--centered",
description: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.text')
}, this.fields().toArray()));
}
fields() {
const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__["default"]();
const emailLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.email_placeholder'));
items.add('email', m("div", {
className: "Form-group"
}, m("input", {
className: "FormControl",
name: "email",
type: "email",
placeholder: emailLabel,
"aria-label": emailLabel,
bidi: this.email,
disabled: this.loading
})), 50);
items.add('submit', m("div", {
className: "Form-group Form-controls"
}, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], {
className: "Button Button--primary Button--block",
type: "submit",
loading: this.loading
}, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.submit_button'))), -10);
return items;
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].request({
method: 'POST',
url: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('apiUrl') + '/forgot',
body: this.requestParams(),
errorHandler: this.onerror.bind(this)
}).then(() => {
this.success = true;
this.alertAttrs = null;
}).catch(() => {}).then(this.loaded.bind(this));
}
requestParams() {
const data = {
email: this.email()
};
return data;
}
onerror(error) {
if (error.status === 404 && error.alert) {
error.alert.content = _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.not_found_message');
}
super.onerror(error);
}
}
flarum.reg.add('core', 'forum/components/ForgotPasswordModal', ForgotPasswordModal);
/***/ })
}]);
//# sourceMappingURL=ForgotPasswordModal.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,222 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[460],{5049:(o,r,t)=>{t.r(r),t.d(r,{default:()=>u});var s=t(7905),e=t(6789),i=t(899),a=t(8312),n=t(6403),l=t(1552),d=t(4041),c=t(6458);class u extends i.Z{constructor(){super(...arguments),(0,s.Z)(this,"identification",void 0),(0,s.Z)(this,"password",void 0),(0,s.Z)(this,"remember",void 0)}oninit(o){super.oninit(o),this.identification=(0,c.Z)(this.attrs.identification||""),this.password=(0,c.Z)(this.attrs.password||""),this.remember=(0,c.Z)(!!this.attrs.remember)}className(){return"LogInModal Modal--small"}title(){return e.Z.translator.trans("core.forum.log_in.title")}content(){return[m("div",{className:"Modal-body"},this.body()),m("div",{className:"Modal-footer"},this.footer())]}body(){return[m(n.Z,null),m("div",{className:"Form Form--centered"},this.fields().toArray())]}fields(){const o=new d.Z,r=(0,l.Z)(e.Z.translator.trans("core.forum.log_in.username_or_email_placeholder")),t=(0,l.Z)(e.Z.translator.trans("core.forum.log_in.password_placeholder"));return o.add("identification",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"identification",type:"text",placeholder:r,"aria-label":r,bidi:this.identification,disabled:this.loading})),30),o.add("password",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"password",type:"password",autocomplete:"current-password",placeholder:t,"aria-label":t,bidi:this.password,disabled:this.loading})),20),o.add("remember",m("div",{className:"Form-group"},m("div",null,m("label",{className:"checkbox"},m("input",{type:"checkbox",bidi:this.remember,disabled:this.loading}),e.Z.translator.trans("core.forum.log_in.remember_me_label")))),10),o.add("submit",m("div",{className:"Form-group"},m(a.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},e.Z.translator.trans("core.forum.log_in.submit_button"))),-10),o}footer(){return m("[",null,m("p",{className:"LogInModal-forgotPassword"},m("a",{onclick:this.forgotPassword.bind(this)},e.Z.translator.trans("core.forum.log_in.forgot_password_link"))),e.Z.forum.attribute("allowSignUp")&&m("p",{className:"LogInModal-signUp"},e.Z.translator.trans("core.forum.log_in.sign_up_text",{a:m("a",{onclick:this.signUp.bind(this)})})))}forgotPassword(){const o=this.identification(),r=o.includes("@")?{email:o}:void 0;e.Z.modal.show((()=>t.e(502).then(t.bind(t,1839))),r)}signUp(){const o=this.identification(),r={[o.includes("@")?"email":"username"]:o};e.Z.modal.show((()=>t.e(395).then(t.bind(t,8686))),r)}onready(){this.$("[name="+(this.identification()?"password":"identification")+"]").trigger("select")}onsubmit(o){o.preventDefault(),this.loading=!0,e.Z.session.login(this.loginParams(),{errorHandler:this.onerror.bind(this)}).then((()=>window.location.reload()),this.loaded.bind(this))}loginParams(){return{identification:this.identification(),password:this.password(),remember:this.remember()}}onerror(o){401===o.status&&o.alert&&(o.alert.content=e.Z.translator.trans("core.forum.log_in.invalid_login_message"),this.password("")),super.onerror(o)}}flarum.reg.add("core","forum/components/LogInModal",u),flarum.reg.addChunkModule("502","1839","core","forum/components/ForgotPasswordModal"),flarum.reg.addChunkModule("395","8686","core","forum/components/SignUpModal")},6403:(o,r,t)=>{t.d(r,{Z:()=>i});var s=t(2190),e=t(4041);class i extends s.Z{view(){return m("div",{className:"LogInButtons"},this.items().toArray())}items(){return new e.Z}}flarum.reg.add("core","forum/components/LogInButtons",i)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/LogInModal"],{
/***/ "./src/forum/components/LogInModal.tsx":
/*!*********************************************!*\
!*** ./src/forum/components/LogInModal.tsx ***!
\*********************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ LogInModal)
/* harmony export */ });
/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js");
/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts");
/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx");
/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx");
/* harmony import */ var _LogInButtons__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./LogInButtons */ "./src/forum/components/LogInButtons.js");
/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts");
/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts");
/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts");
class LogInModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] {
constructor() {
super(...arguments);
/**
* The value of the identification input.
*/
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "identification", void 0);
/**
* The value of the password input.
*/
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "password", void 0);
/**
* The value of the remember me input.
*/
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "remember", void 0);
}
oninit(vnode) {
super.oninit(vnode);
this.identification = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.identification || '');
this.password = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.password || '');
this.remember = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(!!this.attrs.remember);
}
className() {
return 'LogInModal Modal--small';
}
title() {
return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.title');
}
content() {
return [m("div", {
className: "Modal-body"
}, this.body()), m("div", {
className: "Modal-footer"
}, this.footer())];
}
body() {
return [m(_LogInButtons__WEBPACK_IMPORTED_MODULE_4__["default"], null), m("div", {
className: "Form Form--centered"
}, this.fields().toArray())];
}
fields() {
const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__["default"]();
const identificationLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.username_or_email_placeholder'));
const passwordLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.password_placeholder'));
items.add('identification', m("div", {
className: "Form-group"
}, m("input", {
className: "FormControl",
name: "identification",
type: "text",
placeholder: identificationLabel,
"aria-label": identificationLabel,
bidi: this.identification,
disabled: this.loading
})), 30);
items.add('password', m("div", {
className: "Form-group"
}, m("input", {
className: "FormControl",
name: "password",
type: "password",
autocomplete: "current-password",
placeholder: passwordLabel,
"aria-label": passwordLabel,
bidi: this.password,
disabled: this.loading
})), 20);
items.add('remember', m("div", {
className: "Form-group"
}, m("div", null, m("label", {
className: "checkbox"
}, m("input", {
type: "checkbox",
bidi: this.remember,
disabled: this.loading
}), _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.remember_me_label')))), 10);
items.add('submit', m("div", {
className: "Form-group"
}, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], {
className: "Button Button--primary Button--block",
type: "submit",
loading: this.loading
}, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.submit_button'))), -10);
return items;
}
footer() {
return m('[', null, m("p", {
className: "LogInModal-forgotPassword"
}, m("a", {
onclick: this.forgotPassword.bind(this)
}, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.forgot_password_link'))), _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('allowSignUp') && m("p", {
className: "LogInModal-signUp"
}, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.sign_up_text', {
a: m("a", {
onclick: this.signUp.bind(this)
})
})));
}
/**
* Open the forgot password modal, prefilling it with an email if the user has
* entered one.
*/
forgotPassword() {
const email = this.identification();
const attrs = email.includes('@') ? {
email
} : undefined;
_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(() => __webpack_require__.e(/*! import() | forum/components/ForgotPasswordModal */ "forum/components/ForgotPasswordModal").then(__webpack_require__.bind(__webpack_require__, /*! ./ForgotPasswordModal */ "./src/forum/components/ForgotPasswordModal.tsx")), attrs);
}
/**
* Open the sign up modal, prefilling it with an email/username/password if
* the user has entered one.
*/
signUp() {
const identification = this.identification();
const attrs = {
[identification.includes('@') ? 'email' : 'username']: identification
};
_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(() => __webpack_require__.e(/*! import() | forum/components/SignUpModal */ "forum/components/SignUpModal").then(__webpack_require__.bind(__webpack_require__, /*! ./SignUpModal */ "./src/forum/components/SignUpModal.tsx")), attrs);
}
onready() {
this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').trigger('select');
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.login(this.loginParams(), {
errorHandler: this.onerror.bind(this)
}).then(() => window.location.reload(), this.loaded.bind(this));
}
loginParams() {
const data = {
identification: this.identification(),
password: this.password(),
remember: this.remember()
};
return data;
}
onerror(error) {
if (error.status === 401 && error.alert) {
error.alert.content = _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.invalid_login_message');
this.password('');
}
super.onerror(error);
}
}
flarum.reg.add('core', 'forum/components/LogInModal', LogInModal);flarum.reg.addChunkModule('forum/components/ForgotPasswordModal', './src/forum/components/ForgotPasswordModal.tsx', 'core', 'forum/components/ForgotPasswordModal');
flarum.reg.addChunkModule('forum/components/SignUpModal', './src/forum/components/SignUpModal.tsx', 'core', 'forum/components/SignUpModal');
/***/ }),
/***/ "./src/forum/components/LogInButtons.js":
/*!**********************************************!*\
!*** ./src/forum/components/LogInButtons.js ***!
\**********************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ LogInButtons)
/* harmony export */ });
/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts");
/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts");
/**
* The `LogInButtons` component displays a collection of social login buttons.
*/
class LogInButtons extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] {
view() {
return m("div", {
className: "LogInButtons"
}, this.items().toArray());
}
/**
* Build a list of LogInButton components.
*
* @return {ItemList<import('mithril').Children>}
*/
items() {
return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_1__["default"]();
}
}
flarum.reg.add('core', 'forum/components/LogInButtons', LogInButtons);
/***/ })
}]);
//# sourceMappingURL=LogInModal.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,47 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[744],{8246:(i,t,o)=>{o.r(t),o.d(t,{default:()=>r});var s=o(6789),a=o(4661),n=o(7297),e=o(1552);class r extends a.Z{oninit(i){super.oninit(i),s.Z.history.push("notifications",(0,e.Z)(s.Z.translator.trans("core.forum.notifications.title"))),s.Z.notifications.load(),this.bodyClass="App--notifications"}view(){return m("div",{className:"NotificationsPage"},m(n.Z,{state:s.Z.notifications}))}}flarum.reg.add("core","forum/components/NotificationsPage",r)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/NotificationsPage"],{
/***/ "./src/forum/components/NotificationsPage.tsx":
/*!****************************************************!*\
!*** ./src/forum/components/NotificationsPage.tsx ***!
\****************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ NotificationsPage)
/* harmony export */ });
/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts");
/* harmony import */ var _common_components_Page__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/Page */ "./src/common/components/Page.tsx");
/* harmony import */ var _NotificationList__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./NotificationList */ "./src/forum/components/NotificationList.js");
/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts");
/**
* The `NotificationsPage` component shows the notifications list. It is only
* used on mobile devices where the notifications dropdown is within the drawer.
*/
class NotificationsPage extends _common_components_Page__WEBPACK_IMPORTED_MODULE_1__["default"] {
oninit(vnode) {
super.oninit(vnode);
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].history.push('notifications', (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_3__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.notifications.title')));
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].notifications.load();
this.bodyClass = 'App--notifications';
}
view() {
return m("div", {
className: "NotificationsPage"
}, m(_NotificationList__WEBPACK_IMPORTED_MODULE_2__["default"], {
state: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].notifications
}));
}
}
flarum.reg.add('core', 'forum/components/NotificationsPage', NotificationsPage);
/***/ })
}]);
//# sourceMappingURL=NotificationsPage.js.map

View File

@ -1 +1 @@
{"version":3,"file":"forum/components/NotificationsPage.js","mappings":"mLASe,MAAMA,UAA0B,IAC7CC,OAAOC,GACLC,MAAMF,OAAOC,GACb,iBAAiB,iBAAiB,OAAY,qBAAqB,oCACnE,yBACAE,KAAKC,UAAY,oBACnB,CACAC,OACE,OAAOC,EAAE,MAAO,CACdC,UAAW,qBACVD,EAAE,IAAkB,CACrBE,MAAO,oBAEX,EAEFC,OAAOC,IAAIC,IAAI,OAAQ,qCAAsCZ,E","sources":["webpack://@flarum/core/./src/forum/components/NotificationsPage.tsx"],"sourcesContent":["import app from '../../forum/app';\nimport Page from '../../common/components/Page';\nimport NotificationList from './NotificationList';\nimport extractText from '../../common/utils/extractText';\n\n/**\n * The `NotificationsPage` component shows the notifications list. It is only\n * used on mobile devices where the notifications dropdown is within the drawer.\n */\nexport default class NotificationsPage extends Page {\n oninit(vnode) {\n super.oninit(vnode);\n app.history.push('notifications', extractText(app.translator.trans('core.forum.notifications.title')));\n app.notifications.load();\n this.bodyClass = 'App--notifications';\n }\n view() {\n return m(\"div\", {\n className: \"NotificationsPage\"\n }, m(NotificationList, {\n state: app.notifications\n }));\n }\n}\nflarum.reg.add('core', 'forum/components/NotificationsPage', NotificationsPage);"],"names":["NotificationsPage","oninit","vnode","super","this","bodyClass","view","m","className","state","flarum","reg","add"],"sourceRoot":""}
{"version":3,"file":"forum/components/NotificationsPage.js","mappings":";;;;;;;;;;;;;;;;;AAAkC;AACc;AACE;AACO;;AAEzD;AACA;AACA;AACA;AACe,gCAAgC,+DAAI;AACnD;AACA;AACA,IAAI,+DAAgB,kBAAkB,qEAAW,CAAC,mEAAoB;AACtE,IAAI,qEAAsB;AAC1B;AACA;AACA;AACA;AACA;AACA,KAAK,IAAI,yDAAgB;AACzB,aAAa,gEAAiB;AAC9B,KAAK;AACL;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/NotificationsPage.tsx"],"sourcesContent":["import app from '../../forum/app';\nimport Page from '../../common/components/Page';\nimport NotificationList from './NotificationList';\nimport extractText from '../../common/utils/extractText';\n\n/**\n * The `NotificationsPage` component shows the notifications list. It is only\n * used on mobile devices where the notifications dropdown is within the drawer.\n */\nexport default class NotificationsPage extends Page {\n oninit(vnode) {\n super.oninit(vnode);\n app.history.push('notifications', extractText(app.translator.trans('core.forum.notifications.title')));\n app.notifications.load();\n this.bodyClass = 'App--notifications';\n }\n view() {\n return m(\"div\", {\n className: \"NotificationsPage\"\n }, m(NotificationList, {\n state: app.notifications\n }));\n }\n}\nflarum.reg.add('core', 'forum/components/NotificationsPage', NotificationsPage);"],"names":[],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,296 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[630],{2140:(s,e,t)=>{t.d(e,{Z:()=>h});var o=t(2190),r=t(5226);class i extends o.Z{handler(){return this.attrs.when()||void 0}oncreate(s){super.oncreate(s),this.boundHandler=this.handler.bind(this),$(window).on("beforeunload",this.boundHandler)}onremove(s){super.onremove(s),$(window).off("beforeunload",this.boundHandler)}view(s){return m("[",null,s.children)}}flarum.reg.add("core","common/components/ConfirmDocumentUnload",i);var n=t(4944),a=t(1268),c=t(4041),l=t(3344),d=t(7323);class h extends o.Z{oninit(s){super.oninit(s),this.composer=this.attrs.composer,this.loading=!1,this.attrs.confirmExit&&this.composer.preventClosingWhen((()=>this.hasChanges()),this.attrs.confirmExit),this.composer.fields.content(this.attrs.originalContent||"")}view(){var s;return m(i,{when:this.hasChanges.bind(this)},m("div",{className:(0,l.Z)("ComposerBody",this.attrs.className)},m(d.Z,{user:this.attrs.user,className:"ComposerBody-avatar"}),m("div",{className:"ComposerBody-content"},m("ul",{className:"ComposerBody-header"},(0,a.Z)(this.headerItems().toArray())),m("div",{className:"ComposerBody-editor"},m(n.Z,{submitLabel:this.attrs.submitLabel,placeholder:this.attrs.placeholder,disabled:this.loading||this.attrs.disabled,composer:this.composer,preview:null==(s=this.jumpToPreview)?void 0:s.bind(this),onchange:this.composer.fields.content,onsubmit:this.onsubmit.bind(this),value:this.composer.fields.content()}))),m(r.Z,{display:"unset",containerClassName:(0,l.Z)("ComposerBody-loading",this.loading&&"active"),size:"large"})))}hasChanges(){const s=this.composer.fields.content();return s&&s!==this.attrs.originalContent}headerItems(){return new c.Z}onsubmit(){}loaded(){this.loading=!1,m.redraw()}}flarum.reg.add("core","forum/components/ComposerBody",h)},2925:(s,e,t)=>{t.r(e),t.d(e,{default:()=>d});var o=t(6789),r=t(2140),i=t(8312),n=t(6597),a=t(1552),c=t(9133);function l(s){o.Z.composer.isFullScreen()&&(o.Z.composer.minimize(),s.stopPropagation())}class d extends r.Z{static initAttrs(s){super.initAttrs(s),s.placeholder=s.placeholder||(0,a.Z)(o.Z.translator.trans("core.forum.composer_reply.body_placeholder")),s.submitLabel=s.submitLabel||o.Z.translator.trans("core.forum.composer_reply.submit_button"),s.confirmExit=s.confirmExit||(0,a.Z)(o.Z.translator.trans("core.forum.composer_reply.discard_confirmation"))}headerItems(){const s=super.headerItems(),e=this.attrs.discussion;return s.add("title",m("h3",null,m(c.Z,{name:"fas fa-reply"})," ",m(n.Z,{href:o.Z.route.discussion(e),onclick:l},e.title()))),s}jumpToPreview(s){l(s),m.route.set(o.Z.route.discussion(this.attrs.discussion,"reply"))}data(){return{content:this.composer.fields.content(),relationships:{discussion:this.attrs.discussion}}}onsubmit(){const s=this.attrs.discussion;this.loading=!0,m.redraw();const e=this.data();o.Z.store.createRecord("posts").save(e).then((e=>{if(o.Z.viewingDiscussion(s)){const s=o.Z.current.get("stream");s.update().then((()=>s.goToNumber(e.number())))}else{let s;const t=m(i.Z,{className:"Button Button--link",onclick:()=>{m.route.set(o.Z.route.post(e)),o.Z.alerts.dismiss(s)}},o.Z.translator.trans("core.forum.composer_reply.view_button"));s=o.Z.alerts.show({type:"success",controls:[t]},o.Z.translator.trans("core.forum.composer_reply.posted_message"))}this.composer.hide()}),this.loaded.bind(this))}}flarum.reg.add("core","forum/components/ReplyComposer",d)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/ReplyComposer"],{
/***/ "./src/common/components/ConfirmDocumentUnload.js":
/*!********************************************************!*\
!*** ./src/common/components/ConfirmDocumentUnload.js ***!
\********************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ ConfirmDocumentUnload)
/* harmony export */ });
/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ "./src/common/Component.ts");
/**
* The `ConfirmDocumentUnload` component can be used to register a global
* event handler that prevents closing the browser window/tab based on the
* return value of a given callback prop.
*
* ### Attrs
*
* - `when` - a callback returning true when the browser should prompt for
* confirmation before closing the window/tab
*/
class ConfirmDocumentUnload extends _Component__WEBPACK_IMPORTED_MODULE_0__["default"] {
handler() {
return this.attrs.when() || undefined;
}
oncreate(vnode) {
super.oncreate(vnode);
this.boundHandler = this.handler.bind(this);
$(window).on('beforeunload', this.boundHandler);
}
onremove(vnode) {
super.onremove(vnode);
$(window).off('beforeunload', this.boundHandler);
}
view(vnode) {
return m('[', null, vnode.children);
}
}
flarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);
/***/ }),
/***/ "./src/forum/components/ComposerBody.js":
/*!**********************************************!*\
!*** ./src/forum/components/ComposerBody.js ***!
\**********************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ ComposerBody)
/* harmony export */ });
/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts");
/* harmony import */ var _common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/LoadingIndicator */ "./src/common/components/LoadingIndicator.tsx");
/* harmony import */ var _common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/ConfirmDocumentUnload */ "./src/common/components/ConfirmDocumentUnload.js");
/* harmony import */ var _common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/TextEditor */ "./src/common/components/TextEditor.js");
/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx");
/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts");
/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts");
/* harmony import */ var _common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Avatar */ "./src/common/components/Avatar.tsx");
/**
* The `ComposerBody` component handles the body, or the content, of the
* composer. Subclasses should implement the `onsubmit` method and override
* `headerTimes`.
*
* ### Attrs
*
* - `composer`
* - `originalContent`
* - `submitLabel`
* - `placeholder`
* - `user`
* - `confirmExit`
* - `disabled`
*
* @abstract
*/
class ComposerBody extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] {
oninit(vnode) {
super.oninit(vnode);
this.composer = this.attrs.composer;
/**
* Whether or not the component is loading.
*
* @type {Boolean}
*/
this.loading = false;
// Let the composer state know to ask for confirmation under certain
// circumstances, if the body supports / requires it and has a corresponding
// confirmation question to ask.
if (this.attrs.confirmExit) {
this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);
}
this.composer.fields.content(this.attrs.originalContent || '');
}
view() {
var _this$jumpToPreview;
return m(_common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__["default"], {
when: this.hasChanges.bind(this)
}, m("div", {
className: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody', this.attrs.className)
}, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__["default"], {
user: this.attrs.user,
className: "ComposerBody-avatar"
}), m("div", {
className: "ComposerBody-content"
}, m("ul", {
className: "ComposerBody-header"
}, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__["default"])(this.headerItems().toArray())), m("div", {
className: "ComposerBody-editor"
}, m(_common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__["default"], {
submitLabel: this.attrs.submitLabel,
placeholder: this.attrs.placeholder,
disabled: this.loading || this.attrs.disabled,
composer: this.composer,
preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),
onchange: this.composer.fields.content,
onsubmit: this.onsubmit.bind(this),
value: this.composer.fields.content()
}))), m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__["default"], {
display: "unset",
containerClassName: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody-loading', this.loading && 'active'),
size: "large"
})));
}
/**
* Check if there is any unsaved data.
*
* @return {boolean}
*/
hasChanges() {
const content = this.composer.fields.content();
return content && content !== this.attrs.originalContent;
}
/**
* Build an item list for the composer's header.
*
* @return {ItemList<import('mithril').Children>}
*/
headerItems() {
return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"]();
}
/**
* Handle the submit event of the text editor.
*
* @abstract
*/
onsubmit() {}
/**
* Stop loading.
*/
loaded() {
this.loading = false;
m.redraw();
}
}
flarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);
/***/ }),
/***/ "./src/forum/components/ReplyComposer.js":
/*!***********************************************!*\
!*** ./src/forum/components/ReplyComposer.js ***!
\***********************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ ReplyComposer)
/* harmony export */ });
/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts");
/* harmony import */ var _ComposerBody__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ComposerBody */ "./src/forum/components/ComposerBody.js");
/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx");
/* harmony import */ var _common_components_Link__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Link */ "./src/common/components/Link.js");
/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts");
/* harmony import */ var _common_components_Icon__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/components/Icon */ "./src/common/components/Icon.tsx");
function minimizeComposerIfFullScreen(e) {
if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.isFullScreen()) {
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.minimize();
e.stopPropagation();
}
}
/**
* The `ReplyComposer` component displays the composer content for replying to a
* discussion.
*
* ### Attrs
*
* - All of the attrs of ComposerBody
* - `discussion`
*/
class ReplyComposer extends _ComposerBody__WEBPACK_IMPORTED_MODULE_1__["default"] {
static initAttrs(attrs) {
super.initAttrs(attrs);
attrs.placeholder = attrs.placeholder || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.body_placeholder'));
attrs.submitLabel = attrs.submitLabel || _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.submit_button');
attrs.confirmExit = attrs.confirmExit || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.discard_confirmation'));
}
headerItems() {
const items = super.headerItems();
const discussion = this.attrs.discussion;
items.add('title', m("h3", null, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_5__["default"], {
name: 'fas fa-reply'
}), ' ', m(_common_components_Link__WEBPACK_IMPORTED_MODULE_3__["default"], {
href: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.discussion(discussion),
onclick: minimizeComposerIfFullScreen
}, discussion.title())));
return items;
}
/**
* Jump to the preview when triggered by the text editor.
*/
jumpToPreview(e) {
minimizeComposerIfFullScreen(e);
m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.discussion(this.attrs.discussion, 'reply'));
}
/**
* Get the data to submit to the server when the reply is saved.
*
* @return {Record<string, unknown>}
*/
data() {
return {
content: this.composer.fields.content(),
relationships: {
discussion: this.attrs.discussion
}
};
}
onsubmit() {
const discussion = this.attrs.discussion;
this.loading = true;
m.redraw();
const data = this.data();
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].store.createRecord('posts').save(data).then(post => {
// If we're currently viewing the discussion which this reply was made
// in, then we can update the post stream and scroll to the post.
if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].viewingDiscussion(discussion)) {
const stream = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].current.get('stream');
stream.update().then(() => stream.goToNumber(post.number()));
} else {
// Otherwise, we'll create an alert message to inform the user that
// their reply has been posted, containing a button which will
// transition to their new post when clicked.
let alert;
const viewButton = m(_common_components_Button__WEBPACK_IMPORTED_MODULE_2__["default"], {
className: "Button Button--link",
onclick: () => {
m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.post(post));
_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].alerts.dismiss(alert);
}
}, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.view_button'));
alert = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].alerts.show({
type: 'success',
controls: [viewButton]
}, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.posted_message'));
}
this.composer.hide();
}, this.loaded.bind(this));
}
}
flarum.reg.add('core', 'forum/components/ReplyComposer', ReplyComposer);
/***/ })
}]);
//# sourceMappingURL=ReplyComposer.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,223 @@
"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[395],{8686:(s,t,e)=>{e.r(t),e.d(t,{default:()=>h});var a=e(7905),r=e(6789),o=e(899),i=e(8312),n=e(6403),l=e(1552),d=e(4041),u=e(6458);class h extends o.Z{constructor(){super(...arguments),(0,a.Z)(this,"username",void 0),(0,a.Z)(this,"email",void 0),(0,a.Z)(this,"password",void 0)}oninit(s){super.oninit(s),this.username=(0,u.Z)(this.attrs.username||""),this.email=(0,u.Z)(this.attrs.email||""),this.password=(0,u.Z)(this.attrs.password||"")}className(){return"Modal--small SignUpModal"}title(){return r.Z.translator.trans("core.forum.sign_up.title")}content(){return[m("div",{className:"Modal-body"},this.body()),m("div",{className:"Modal-footer"},this.footer())]}isProvided(s){var t,e;return null!=(t=null==(e=this.attrs.provided)?void 0:e.includes(s))&&t}body(){return[!this.attrs.token&&m(n.Z,null),m("div",{className:"Form Form--centered"},this.fields().toArray())]}fields(){const s=new d.Z,t=(0,l.Z)(r.Z.translator.trans("core.forum.sign_up.username_placeholder")),e=(0,l.Z)(r.Z.translator.trans("core.forum.sign_up.email_placeholder")),a=(0,l.Z)(r.Z.translator.trans("core.forum.sign_up.password_placeholder"));return s.add("username",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"username",type:"text",placeholder:t,"aria-label":t,bidi:this.username,disabled:this.loading||this.isProvided("username")})),30),s.add("email",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"email",type:"email",placeholder:e,"aria-label":e,bidi:this.email,disabled:this.loading||this.isProvided("email")})),20),this.attrs.token||s.add("password",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"password",type:"password",autocomplete:"new-password",placeholder:a,"aria-label":a,bidi:this.password,disabled:this.loading})),10),s.add("submit",m("div",{className:"Form-group"},m(i.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},r.Z.translator.trans("core.forum.sign_up.submit_button"))),-10),s}footer(){return[m("p",{className:"SignUpModal-logIn"},r.Z.translator.trans("core.forum.sign_up.log_in_text",{a:m("a",{onclick:this.logIn.bind(this)})}))]}logIn(){const s={identification:this.email()||this.username()};r.Z.modal.show((()=>e.e(460).then(e.bind(e,5049))),s)}onready(){this.attrs.username&&!this.attrs.email?this.$("[name=email]").select():this.$("[name=username]").select()}onsubmit(s){s.preventDefault(),this.loading=!0;const t=this.submitData();r.Z.request({url:r.Z.forum.attribute("baseUrl")+"/register",method:"POST",body:t,errorHandler:this.onerror.bind(this)}).then((()=>window.location.reload()),this.loaded.bind(this))}submitData(){const s=this.attrs.token?{token:this.attrs.token}:{password:this.password()};return{username:this.username(),email:this.email(),...s}}}flarum.reg.add("core","forum/components/SignUpModal",h),flarum.reg.addChunkModule("460","5049","core","forum/components/LogInModal")},6403:(s,t,e)=>{e.d(t,{Z:()=>o});var a=e(2190),r=e(4041);class o extends a.Z{view(){return m("div",{className:"LogInButtons"},this.items().toArray())}items(){return new r.Z}}flarum.reg.add("core","forum/components/LogInButtons",o)}}]);
"use strict";
(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/SignUpModal"],{
/***/ "./src/forum/components/SignUpModal.tsx":
/*!**********************************************!*\
!*** ./src/forum/components/SignUpModal.tsx ***!
\**********************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ SignUpModal)
/* harmony export */ });
/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js");
/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts");
/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx");
/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx");
/* harmony import */ var _LogInButtons__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./LogInButtons */ "./src/forum/components/LogInButtons.js");
/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts");
/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts");
/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts");
class SignUpModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] {
constructor() {
super(...arguments);
/**
* The value of the username input.
*/
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "username", void 0);
/**
* The value of the email input.
*/
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "email", void 0);
/**
* The value of the password input.
*/
(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "password", void 0);
}
oninit(vnode) {
super.oninit(vnode);
this.username = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.username || '');
this.email = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.email || '');
this.password = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.password || '');
}
className() {
return 'Modal--small SignUpModal';
}
title() {
return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.title');
}
content() {
return [m("div", {
className: "Modal-body"
}, this.body()), m("div", {
className: "Modal-footer"
}, this.footer())];
}
isProvided(field) {
var _this$attrs$provided$, _this$attrs$provided;
return (_this$attrs$provided$ = (_this$attrs$provided = this.attrs.provided) == null ? void 0 : _this$attrs$provided.includes(field)) != null ? _this$attrs$provided$ : false;
}
body() {
return [!this.attrs.token && m(_LogInButtons__WEBPACK_IMPORTED_MODULE_4__["default"], null), m("div", {
className: "Form Form--centered"
}, this.fields().toArray())];
}
fields() {
const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__["default"]();
const usernameLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.username_placeholder'));
const emailLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.email_placeholder'));
const passwordLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.password_placeholder'));
items.add('username', m("div", {
className: "Form-group"
}, m("input", {
className: "FormControl",
name: "username",
type: "text",
placeholder: usernameLabel,
"aria-label": usernameLabel,
bidi: this.username,
disabled: this.loading || this.isProvided('username')
})), 30);
items.add('email', m("div", {
className: "Form-group"
}, m("input", {
className: "FormControl",
name: "email",
type: "email",
placeholder: emailLabel,
"aria-label": emailLabel,
bidi: this.email,
disabled: this.loading || this.isProvided('email')
})), 20);
if (!this.attrs.token) {
items.add('password', m("div", {
className: "Form-group"
}, m("input", {
className: "FormControl",
name: "password",
type: "password",
autocomplete: "new-password",
placeholder: passwordLabel,
"aria-label": passwordLabel,
bidi: this.password,
disabled: this.loading
})), 10);
}
items.add('submit', m("div", {
className: "Form-group"
}, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], {
className: "Button Button--primary Button--block",
type: "submit",
loading: this.loading
}, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.submit_button'))), -10);
return items;
}
footer() {
return [m("p", {
className: "SignUpModal-logIn"
}, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.log_in_text', {
a: m("a", {
onclick: this.logIn.bind(this)
})
}))];
}
/**
* Open the log in modal, prefilling it with an email/username/password if
* the user has entered one.
*/
logIn() {
const attrs = {
identification: this.email() || this.username()
};
_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(() => __webpack_require__.e(/*! import() | forum/components/LogInModal */ "forum/components/LogInModal").then(__webpack_require__.bind(__webpack_require__, /*! ./LogInModal */ "./src/forum/components/LogInModal.tsx")), attrs);
}
onready() {
if (this.attrs.username && !this.attrs.email) {
this.$('[name=email]').select();
} else {
this.$('[name=username]').select();
}
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
const body = this.submitData();
_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].request({
url: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('baseUrl') + '/register',
method: 'POST',
body,
errorHandler: this.onerror.bind(this)
}).then(() => window.location.reload(), this.loaded.bind(this));
}
/**
* Get the data that should be submitted in the sign-up request.
*/
submitData() {
const authData = this.attrs.token ? {
token: this.attrs.token
} : {
password: this.password()
};
const data = {
username: this.username(),
email: this.email(),
...authData
};
return data;
}
}
flarum.reg.add('core', 'forum/components/SignUpModal', SignUpModal);flarum.reg.addChunkModule('forum/components/LogInModal', './src/forum/components/LogInModal.tsx', 'core', 'forum/components/LogInModal');
/***/ }),
/***/ "./src/forum/components/LogInButtons.js":
/*!**********************************************!*\
!*** ./src/forum/components/LogInButtons.js ***!
\**********************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ LogInButtons)
/* harmony export */ });
/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts");
/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts");
/**
* The `LogInButtons` component displays a collection of social login buttons.
*/
class LogInButtons extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] {
view() {
return m("div", {
className: "LogInButtons"
}, this.items().toArray());
}
/**
* Build a list of LogInButton components.
*
* @return {ItemList<import('mithril').Children>}
*/
items() {
return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_1__["default"]();
}
}
flarum.reg.add('core', 'forum/components/LogInButtons', LogInButtons);
/***/ })
}]);
//# sourceMappingURL=SignUpModal.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -34,6 +34,7 @@ export type Extension = {
title: string;
};
};
require?: Record<string, string>;
};
export interface AdminApplicationData extends ApplicationData {
@ -44,7 +45,9 @@ export interface AdminApplicationData extends ApplicationData {
slugDrivers: Record<string, string[]>;
searchDrivers: Record<string, string[]>;
permissions: Record<string, string[]>;
advancedPageEmpty: boolean;
maintenanceByConfig: boolean;
safeModeExtensions?: string[] | null;
safeModeExtensionsConfig?: string[] | null;
}
export default class AdminApplication extends Application {

View File

@ -112,7 +112,7 @@ export default class AdminNav extends Component {
50
);
if (app.data.settings.show_advanced_settings && !app.data.advancedPageEmpty) {
if (app.data.settings.show_advanced_settings) {
items.add(
'advanced',
<LinkButton href={app.route('advanced')} icon="fas fa-cog" title={app.translator.trans('core.admin.nav.advanced_title')}>

View File

@ -9,6 +9,7 @@ import saveSettings from '../utils/saveSettings';
import AdminHeader from './AdminHeader';
import FormGroup, { FieldComponentOptions } from '../../common/components/FormGroup';
import extractText from '../../common/utils/extractText';
import LoadingModal from './LoadingModal';
export interface AdminHeaderOptions {
title: Mithril.Children;
@ -24,6 +25,8 @@ export interface AdminHeaderOptions {
export type SettingsComponentOptions = FieldComponentOptions & {
setting: string;
json?: boolean;
refreshAfterSaving?: boolean;
};
/**
@ -38,6 +41,7 @@ export type SaveSubmitEvent = SubmitEvent & { redraw: boolean };
export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends Page<CustomAttrs> {
settings: MutableSettings = {};
refreshAfterSaving: string[] = [];
loading: boolean = false;
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
@ -137,9 +141,34 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
return entry.call(this);
}
const { setting, ...attrs } = entry;
const { setting, json, refreshAfterSaving, ...attrs } = entry;
return <FormGroup bidi={this.setting(setting)} {...attrs} />;
const originalBidi: (value?: string) => any = this.setting(setting);
let bidi: (value?: string) => any;
if (json) {
bidi = function (value?: string) {
if (arguments.length) {
originalBidi(JSON.stringify(value));
}
const v = originalBidi();
if (v) {
return JSON.parse(v);
}
return v;
};
} else {
bidi = originalBidi;
}
if (refreshAfterSaving) {
this.refreshAfterSaving.push(setting);
}
return <FormGroup stream={bidi} {...attrs} />;
}
/**
@ -194,7 +223,16 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
this.loading = true;
return saveSettings(this.dirty()).then(this.onsaved.bind(this));
const dirty = this.dirty();
return saveSettings(dirty)
.then(this.onsaved.bind(this))
.then(() => {
if (this.refreshAfterSaving.length && Object.keys(dirty).some((setting) => this.refreshAfterSaving.includes(setting))) {
app.modal.show(LoadingModal);
window.location.reload();
}
});
}
modelLocale(): Record<string, string> {

View File

@ -5,6 +5,9 @@ import type Mithril from 'mithril';
import Form from '../../common/components/Form';
import extractText from '../../common/utils/extractText';
import FormSectionGroup, { FormSection } from './FormSectionGroup';
import ItemList from '../../common/utils/ItemList';
import InfoTile from '../../common/components/InfoTile';
import { MaintenanceMode } from '../../common/Application';
export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends AdminPage<CustomAttrs> {
searchDriverOptions: Record<string, Record<string, string>> = {};
@ -35,8 +38,36 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
content() {
return [
<Form className="AdvancedPage-container">
<FormSectionGroup>
<FormSectionGroup>{this.sectionItems().toArray()}</FormSectionGroup>
<div className="Form-group Form-controls">{this.submitButton()}</div>
</Form>,
];
}
driverLocale(): Record<string, Record<string, string>> {
return {
search: {
default: extractText(app.translator.trans('core.admin.advanced.search.driver_options.default')),
},
};
}
sectionItems() {
const items = new ItemList<Mithril.Children>();
items.add('search', this.searchDrivers(), 100);
items.add('maintenance', this.maintenance(), 90);
return items;
}
searchDrivers() {
const hasOtherDrivers = Object.keys(this.searchDriverOptions).some((model) => Object.keys(this.searchDriverOptions[model]).length > 1);
return (
<FormSection label={app.translator.trans('core.admin.advanced.search.section_label')}>
{hasOtherDrivers ? (
<Form>
{Object.keys(this.searchDriverOptions).map((model) => {
const options = this.searchDriverOptions[model];
@ -55,19 +86,83 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
return null;
})}
</Form>
) : (
<InfoTile icon="fas fa-database" className="InfoTile--warning">
{app.translator.trans('core.admin.advanced.search.no_other_drivers')}
</InfoTile>
)}
</FormSection>
</FormSectionGroup>
<div className="Form-group Form-controls">{this.submitButton()}</div>
</Form>,
];
);
}
driverLocale(): Record<string, Record<string, string>> {
return {
search: {
default: extractText(app.translator.trans('core.admin.advanced.search.driver_options.default')),
maintenance() {
return (
<FormSection label={app.translator.trans('core.admin.advanced.maintenance.section_label')}>
<Form>
{this.buildSettingComponent({
type: 'select',
help: app.translator.trans('core.admin.advanced.maintenance.help'),
setting: 'maintenance_mode',
refreshAfterSaving: true,
options: {
[MaintenanceMode.NO_MAINTENANCE]: app.translator.trans('core.admin.advanced.maintenance.options.' + MaintenanceMode.NO_MAINTENANCE),
[MaintenanceMode.HIGH_MAINTENANCE]: {
label: app.translator.trans('core.admin.advanced.maintenance.options.' + MaintenanceMode.HIGH_MAINTENANCE),
disabled: true,
},
[MaintenanceMode.LOW_MAINTENANCE]: app.translator.trans('core.admin.advanced.maintenance.options.' + MaintenanceMode.LOW_MAINTENANCE),
[MaintenanceMode.SAFE_MODE]: app.translator.trans('core.admin.advanced.maintenance.options.' + MaintenanceMode.SAFE_MODE),
},
default: MaintenanceMode.NO_MAINTENANCE,
})}
{this.setting('maintenance_mode')() === MaintenanceMode.SAFE_MODE
? this.buildSettingComponent({
type: 'dropdown',
label: app.translator.trans('core.admin.advanced.maintenance.safe_mode_extensions'),
help: app.data.safeModeExtensionsConfig
? app.translator.trans('core.admin.advanced.maintenance.safe_mode_extensions_override_help', {
extensions: app.data.safeModeExtensionsConfig.map((id) => app.data.extensions[id].extra['flarum-extension'].title).join(', '),
})
: null,
setting: 'safe_mode_extensions',
json: true,
refreshAfterSaving: true,
multiple: true,
disabled: app.data.safeModeExtensionsConfig,
options: Object.entries(app.data.extensions).reduce((acc, [id, extension]) => {
const requiredExtensions = extension.require
? Object.entries(app.data.extensions).filter(([, e]) => extension.require![e.name])
: [];
// @ts-ignore
acc[id] = {
label: extension.extra['flarum-extension'].title,
disabled: (value: string[]) => {
let dependenciesMet = true;
if (extension.require) {
dependenciesMet = !requiredExtensions.length || requiredExtensions.every(([id]) => value.includes(id));
}
return !dependenciesMet;
},
tooltip: requiredExtensions.length
? `Requires: ${requiredExtensions.map(([, e]) => e.extra['flarum-extension'].title).join(', ')}`
: undefined,
};
return acc;
}, {}),
})
: null}
{app.data.maintenanceByConfig ? (
<div className="Form-group">
<label>{app.translator.trans('core.admin.advanced.maintenance.config_override.label')}</label>
<p className="helpText">{app.translator.trans('core.admin.advanced.maintenance.config_override.help')}</p>
<strong className="helpText">{app.translator.trans('core.admin.advanced.maintenance.options.' + app.data.maintenanceMode)}</strong>
</div>
) : null}
</Form>
</FormSection>
);
}
}

View File

@ -0,0 +1,18 @@
import Alert, { AlertAttrs } from '../../common/components/Alert';
import DashboardWidget, { type IDashboardWidgetAttrs } from './DashboardWidget';
import classList from '../../common/utils/classList';
import type Mithril from 'mithril';
export interface IAlertWidgetAttrs extends IDashboardWidgetAttrs {
alert: AlertAttrs;
}
export default class AlertWidget<CustomAttrs extends IAlertWidgetAttrs = IAlertWidgetAttrs> extends DashboardWidget<CustomAttrs> {
className() {
return classList('AlertWidget', this.attrs.className);
}
content(vnode: Mithril.Vnode<CustomAttrs, this>) {
return <Alert {...vnode.attrs.alert}>{vnode.children}</Alert>;
}
}

View File

@ -4,7 +4,8 @@ import ExtensionsWidget from './ExtensionsWidget';
import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import type { Children } from 'mithril';
import DebugWarningWidget from './DebugWarningWidget';
import AlertWidget from './AlertWidget';
import Link from '../../common/components/Link';
export default class DashboardPage extends AdminPage {
headerInfo() {
@ -23,8 +24,39 @@ export default class DashboardPage extends AdminPage {
availableWidgets(): ItemList<Children> {
const items = new ItemList<Children>();
if (app.data.maintenanceMode) {
items.add(
'maintenanceMode',
<AlertWidget
alert={{
type: 'error',
dismissible: false,
}}
>
{app.translator.trans('core.lib.notices.maintenance_mode_' + app.data.maintenanceMode)}
</AlertWidget>,
110
);
}
if (app.data.debugEnabled) {
items.add('debug-warning', <DebugWarningWidget />, 100);
items.add(
'debug-warning',
<AlertWidget
className="DebugWarningWidget"
alert={{
type: 'warning',
dismissible: false,
title: app.translator.trans('core.admin.debug-warning.label'),
icon: 'fas fa-exclamation-triangle',
}}
>
{app.translator.trans('core.admin.debug-warning.detail', {
link: <Link href="https://docs.flarum.org/troubleshoot/#step-0-activate-debug-mode" external={true} target="_blank" />,
})}
</AlertWidget>,
100
);
}
items.add('status', <StatusWidget />, 30);

View File

@ -1,18 +1,18 @@
import type { Children, Vnode } from 'mithril';
import type Mithril from 'mithril';
import Component, { ComponentAttrs } from '../../common/Component';
export interface IDashboardWidgetAttrs extends ComponentAttrs {}
export default class DashboardWidget<CustomAttrs extends IDashboardWidgetAttrs = IDashboardWidgetAttrs> extends Component<CustomAttrs> {
view(vnode: Vnode<CustomAttrs, this>): Children {
return <div className={'DashboardWidget Widget ' + this.className()}>{this.content()}</div>;
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
return <div className={'DashboardWidget Widget ' + this.className()}>{this.content(vnode)}</div>;
}
className() {
return '';
}
content(): Children {
content(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
return null;
}
}

View File

@ -1,20 +0,0 @@
import app from '../../admin/app';
import Alert from '../../common/components/Alert';
import Link from '../../common/components/Link';
import DashboardWidget from './DashboardWidget';
export default class DebugWarningWidget extends DashboardWidget {
className() {
return 'DebugWarningWidget';
}
content() {
return (
<Alert type="warning" dismissible={false} title={app.translator.trans('core.admin.debug-warning.label')} icon="fas fa-exclamation-triangle">
{app.translator.trans('core.admin.debug-warning.detail', {
link: <Link href="https://docs.flarum.org/troubleshoot/#step-0-activate-debug-mode" external={true} target="_blank" />,
})}
</Alert>
);
}
}

View File

@ -18,6 +18,8 @@ import type Mithril from 'mithril';
import extractText from '../../common/utils/extractText';
import Form from '../../common/components/Form';
import Icon from '../../common/components/Icon';
import { MaintenanceMode } from '../../common/Application';
import InfoTile from '../../common/components/InfoTile';
export interface ExtensionPageAttrs extends IPageAttrs {
id: string;
@ -61,17 +63,31 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
return (
<div className={'ExtensionPage ' + this.className()}>
{this.header()}
{!this.isEnabled() ? (
{app.data.maintenanceMode === MaintenanceMode.SAFE_MODE && !app.data.safeModeExtensions?.includes(this.extension.id) ? (
<div className="container">
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h3>
<div className="ExtensionPage-body">
<InfoTile icon="fas fa-exclamation-triangle" type="warning">
{app.translator.trans('core.admin.extension.safe_mode_warning')}
</InfoTile>
</div>
</div>
) : (
<div className="ExtensionPage-body">{this.sections(vnode).toArray()}</div>
this.body(vnode)
)}
</div>
);
}
body(vnode: Mithril.VnodeDOM<Attrs, this>) {
return this.isEnabled() ? (
<div className="ExtensionPage-body">{this.sections(vnode).toArray()}</div>
) : (
<div className="container">
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h3>
</div>
);
}
header() {
const isEnabled = this.isEnabled();

View File

@ -72,7 +72,6 @@ export default class StatusWidget extends DashboardWidget {
<Button onclick={this.handleClearCache.bind(this)}>{app.translator.trans('core.admin.dashboard.clear_cache_button')}</Button>
);
if (!app.data.advancedPageEmpty) {
items.add(
'toggleAdvancedPage',
<Button
@ -89,7 +88,6 @@ export default class StatusWidget extends DashboardWidget {
{app.translator.trans('core.admin.dashboard.toggle_advanced_page_button')}
</Button>
);
}
return items;
}

View File

@ -124,12 +124,20 @@ export interface RouteResolver<
render?(this: this, vnode: Mithril.Vnode<Attrs, Comp>): Mithril.Children;
}
export enum MaintenanceMode {
NO_MAINTENANCE = 'none',
HIGH_MAINTENANCE = 'high',
LOW_MAINTENANCE = 'low',
SAFE_MODE = 'safe',
}
export interface ApplicationData {
apiDocument: ApiPayload | null;
locale: string;
locales: Record<string, string>;
resources: SavedModelData[];
session: { userId: number; csrfToken: string };
maintenanceMode?: MaintenanceMode;
[key: string]: unknown;
}

View File

@ -9,6 +9,8 @@ import Icon from './Icon';
export interface IDropdownAttrs extends ComponentAttrs {
/** A class name to apply to the dropdown toggle button. */
buttonClassName?: string;
/** Additional attributes to apply to the dropdown toggle button. */
buttonAttrs?: Record<string, string>;
/** A class name to apply to the dropdown menu. */
menuClassName?: string;
/** The name of an icon to show in the dropdown toggle button. */
@ -132,6 +134,7 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
aria-label={this.attrs.accessibleToggleLabel}
data-toggle="dropdown"
onclick={this.attrs.onclick}
{...this.attrs.buttonAttrs}
>
{this.getButtonContent(children)}
</button>

View File

@ -10,6 +10,7 @@ import ItemList from '../utils/ItemList';
import type { IUploadImageButtonAttrs } from './UploadImageButton';
import type { ComponentAttrs } from '../Component';
import type Mithril from 'mithril';
import MultiSelect from './MultiSelect';
/**
* A type that matches any valid value for the `type` attribute on an HTML `<input>` element.
@ -81,8 +82,16 @@ export interface SelectFieldComponentOptions extends CommonFieldOptions {
/**
* Map of values to their labels
*/
options: { [value: string]: Mithril.Children };
options: {
[value: string]:
| Mithril.Children
| {
label: Mithril.Children;
disabled?: boolean;
};
};
default: string;
multiple?: boolean;
}
/**
@ -122,7 +131,7 @@ export type FieldComponentOptions =
export type IFormGroupAttrs = ComponentAttrs &
FieldComponentOptions & {
bidi?: Stream<any>;
stream?: Stream<any>;
};
/**
@ -157,12 +166,12 @@ export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupA
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
const customFieldComponents = this.customFieldComponents();
const { help, type, label, bidi, ...componentAttrs } = this.attrs;
const { help, type, label, stream, ...componentAttrs } = this.attrs;
// TypeScript being TypeScript
const attrs = componentAttrs as unknown as Omit<IFormGroupAttrs, 'bidi' | 'label' | 'help' | 'type'>;
const attrs = componentAttrs as unknown as Omit<IFormGroupAttrs, 'stream' | 'label' | 'help' | 'type'>;
const value = bidi ? bidi() : null;
const value = stream ? stream() : null;
const [inputId, helpTextId] = [generateElementId(), generateElementId()];
@ -175,29 +184,31 @@ export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupA
// TODO: Add aria-describedby for switch help text.
//? Requires changes to Checkbox component to allow providing attrs directly for the element(s).
<div className="Form-group">
<Switch state={!!value && value !== '0'} onchange={bidi} {...attrs}>
<Switch state={!!value && value !== '0'} onchange={stream} {...attrs}>
{label}
</Switch>
{help ? <div className="helpText">{help}</div> : null}
</div>
);
} else if ((SelectSettingTypes as readonly string[]).includes(type)) {
const { default: defaultValue, options, ...otherAttrs } = attrs;
const { default: defaultValue, options, multiple, ...otherAttrs } = attrs;
const Tag = multiple ? MultiSelect : Select;
settingElement = (
<Select id={inputId} aria-describedby={helpTextId} value={value || defaultValue} options={options} onchange={bidi} {...otherAttrs} />
<Tag id={inputId} aria-describedby={helpTextId} value={value || defaultValue} options={options} onchange={stream} {...otherAttrs} />
);
} else if (type === ImageUploadSettingType) {
const { value, ...otherAttrs } = attrs;
settingElement = <UploadImageButton value={bidi} {...otherAttrs} />;
settingElement = <UploadImageButton value={stream} {...otherAttrs} />;
} else if (customFieldComponents.has(type)) {
return customFieldComponents.get(type)(this.attrs);
} else {
attrs.className = classList('FormControl', attrs.className);
if ((TextareaSettingTypes as readonly string[]).includes(type)) {
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={bidi} {...attrs} />;
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={stream} {...attrs} />;
} else {
let Tag: VnodeElementTag = 'input';
@ -207,7 +218,7 @@ export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupA
attrs.type = type;
}
settingElement = <Tag id={inputId} aria-describedby={helpTextId} bidi={bidi} {...attrs} />;
settingElement = <Tag id={inputId} aria-describedby={helpTextId} bidi={stream} {...attrs} />;
}
}

View File

@ -0,0 +1,126 @@
import app from '../app';
import Component, { type ComponentAttrs } from '../Component';
import classList from '../utils/classList';
import Dropdown from './Dropdown';
import Mithril from 'mithril';
import Button from './Button';
import Tooltip from './Tooltip';
export type Option = {
label: string;
disabled?: boolean | ((value: string[]) => boolean);
tooltip?: string;
};
export interface IMultiSelectAttrs extends ComponentAttrs {
options: Record<string, string | Option>;
onchange?: (value: string[]) => void;
value?: string[];
disabled?: boolean;
wrapperAttrs?: Record<string, string>;
}
/**
* The `MultiSelect` component displays an input with selected elements.
* With a dropdown to select multiple options.
*
* - `options` A map of option values to labels.
* - `onchange` A callback to run when the selected value is changed.
* - `value` The value of the selected option.
* - `disabled` Disabled state for the input.
* - `wrapperAttrs` A map of attrs to be passed to the DOM element wrapping the input.
*
* Other attributes are passed directly to the input element rendered to the DOM.
*/
export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiSelectAttrs> extends Component<CustomAttrs> {
protected selected: string[] = [];
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.selected = this.attrs.value || [];
}
view() {
const {
options,
onchange,
disabled,
className,
class: _class,
// Destructure the `wrapperAttrs` object to extract the `className` for passing to `classList()`
// `= {}` prevents errors when `wrapperAttrs` is undefined
wrapperAttrs: { className: wrapperClassName, class: wrapperClass, ...wrapperAttrs } = {},
...domAttrs
} = this.attrs;
return (
<span className={classList('Select MultiSelect', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
<Dropdown
disabled={disabled}
buttonClassName="Button"
buttonAttrs={{ disabled }}
label={
Object.keys(options)
.filter((key) => this.selected.includes(key))
.map((key) => (typeof options[key] === 'string' ? options[key] : (options[key] as Option).label))
.join(', ') || app.translator.trans('core.lib.multi_select.placeholder')
}
>
{Object.keys(options).map((key) => {
const option = options[key];
const label = typeof option === 'string' ? option : option.label;
const tooltip = typeof option !== 'string' && option.tooltip;
let disabled = typeof option !== 'string' && option.disabled;
if (typeof disabled === 'function') {
disabled = disabled(this.selected);
}
const button = (
<Button
type="button"
className={classList('Dropdown-item', { disabled })}
onclick={this.toggle.bind(this, key)}
disabled={disabled}
icon={this.selected.includes(key) ? 'fas fa-check' : 'fas fa-empty'}
>
{label}
</Button>
);
if (tooltip) {
return <Tooltip text={tooltip}>{button}</Tooltip>;
}
return button;
})}
</Dropdown>
</span>
);
}
select(value: string) {
this.selected.push(value);
}
unselect(value: string) {
this.selected = this.selected.filter((v) => v !== value);
}
toggle(value: string, e: MouseEvent) {
e.stopPropagation();
if (this.selected.includes(value)) {
this.unselect(value);
} else {
this.select(value);
}
if (this.attrs.onchange) {
this.attrs.onchange(this.selected);
}
}
}

View File

@ -41,9 +41,25 @@ export default class Select extends Component {
disabled={disabled}
{...domAttrs}
>
{Object.keys(options).map((key) => (
<option value={key}>{options[key]}</option>
))}
{Object.keys(options).map((key) => {
const option = options[key];
let label;
let disabled = false;
if (typeof option === 'object' && option.label) {
label = option.label;
disabled = option.disabled ?? false;
} else {
label = option;
}
return (
<option value={key} disabled={disabled}>
{label}
</option>
);
})}
</select>
<Icon name="fas fa-sort" className="Select-caret" />
</span>

View File

@ -40,6 +40,15 @@ export default class Notices extends Component {
);
}
if (app.data.maintenanceMode) {
items.add(
'maintenanceMode',
<Alert type="error" dismissible={false} className="Alert--maintenanceMode" containerClassName="container">
{app.translator.trans('core.lib.notices.maintenance_mode_' + app.data.maintenanceMode)}
</Alert>
);
}
return items;
}

View File

@ -4,7 +4,7 @@
@import "admin/AdminNav";
@import "admin/CreateUserModal";
@import "admin/DashboardPage";
@import "admin/DebugWarningWidget";
@import "admin/AlertWidget";
@import "admin/FormSectionGroup";
@import "admin/BasicsPage";
@import "admin/PermissionsPage";

View File

@ -0,0 +1,3 @@
.AlertWidget {
padding: 0;
}

View File

@ -1,3 +0,0 @@
.DebugWarningWidget {
padding: 0;
}

View File

@ -130,6 +130,12 @@
padding: 5px 0;
}
}
&-body {
.InfoTile {
margin-top: 4rem
}
}
}
.ExtensionTitle {

View File

@ -16,6 +16,10 @@
border-radius: var(--border-radius);
flex: 1 1 160px;
gap: var(--gap);
&-body {
min-width: 0;
}
}
.FormSection > label {

View File

@ -34,6 +34,10 @@
border-bottom-left-radius: 0;
}
}
&, & > .Button {
max-width: 100%;
}
}
//
@ -214,6 +218,8 @@
}
.Button-label {
line-height: inherit;
overflow: hidden;
text-overflow: ellipsis;
}
.Button-icon {
line-height: inherit;

View File

@ -61,7 +61,7 @@
.FieldSet, .FieldSet-items, .Form, .Form-body {
> * {
min-width: 100%;
width: 100%;
margin-bottom: 0;
}
}

View File

@ -5,10 +5,16 @@
font-size: 1.1rem;
color: var(--control-color);
align-items: center;
justify-content: center;
text-align: center;
padding: 8px 0;
.icon {
color: var(--control-muted-color);
font-size: 2rem;
}
.FormSection & {
font-size: 0.94rem;
}
}

View File

@ -10,6 +10,10 @@
padding-right: 30px;
cursor: pointer;
line-height: 1;
.FormSection & {
width: 100%;
}
}
}
.Select-caret {
@ -19,3 +23,7 @@
text-align: center;
width: 1.25em;
}
.MultiSelect {
max-width: 100%;
}

View File

@ -10,12 +10,26 @@ core:
# These translations are used in the Advanced page.
advanced:
description: "Configure advanced settings for your forum."
maintenance:
config_override:
label: Your <code>config.php</code> file is overriding these settings.
help: You can still change these settings here, but they will not take effect until you set <code>offline</code> to <code>0</code> in your <code>config.php</code> file.
help: Put your forum in maintenance mode to prevent users from accessing it.
options:
none: No maintenance.
high: High maintenance mode. No one can access the forum (can only be enabled through config.php)
low: Low maintenance mode. Admins can access the forum.
safe: Safe mode. No extensions are booted and only admins can access the forum.
safe_mode_extensions: Extensions allowed to boot during safe mode
safe_mode_extensions_override_help: "This setting is overridden by the <code>safe_mode_extensions</code> key in your <code>config.php</code> file. (<b>{extensions}</b>)"
section_label: Maintenance
search:
section_label: Search Drivers
driver_heading: "Search Driver: {model}"
driver_text: Select a driver to be used for searching this model.
driver_options:
default: Default database search
no_other_drivers: No search drivers are available yet. Install a search driver extension to be able to configure it.
title: Advanced
# These translations are used in the Appearance page.
@ -194,6 +208,7 @@ core:
button_label: README
no_readme: This extension does not appear to have a README file
title: "{extName} documentation"
safe_mode_warning: Safe mode is currently enabled. Extensions are not booted and their settings are therefore inaccessible.
# These translations are used in the secondary header.
header:
@ -463,13 +478,13 @@ core:
# These translations are used in the Log In modal dialog.
log_in:
forgot_password_link: "Forgot password?"
invalid_login_message: Your login details were incorrect.
invalid_login_message: => core.ref.invalid_login_message
password_placeholder: => core.ref.password
remember_me_label: Remember Me
remember_me_label: => core.ref.remember_me_label
sign_up_text: "Don't have an account? <a>Sign Up</a>"
submit_button: => core.ref.log_in
title: => core.ref.log_in
username_or_email_placeholder: Username or Email
username_or_email_placeholder: => core.ref.username_or_email_placeholder
# These translations are used by the Notifications dropdown, a.k.a. "the bell".
notifications:
@ -689,10 +704,19 @@ core:
modal:
close: Close
# These translations are used in multi-select components.
multi_select:
placeholder: Select multiple options
# These translations are used in the navigation header.
nav:
drawer_button: Open Navigation Drawer
# These translations are used in forum & admin notices.
notices:
maintenance_mode_low: Down for maintenance. Only administrators can access the forum.
maintenance_mode_safe: Down for maintenance with safe mode. Only administrators can access the forum and no extensions are booted.
# These translations are used as suffixes when abbreviating numbers.
number_suffix:
kilo_text: K
@ -776,6 +800,9 @@ core:
csrf_token_mismatch: You have been inactive for too long.
csrf_token_mismatch_return_link: Go back, to try again
invalid_confirmation_token: This confirmation link has already been used or is invalid.
maintenance_mode_link: Administrator login
maintenance_mode_message: This forum is currently down for maintenance. Please check back later.
maintenance_mode_title: Maintenance
not_authenticated: You do not have permission to access this page. Try again after logging in.
not_found: The page you requested could not be found.
not_found_return_link: "Return to {forum}"
@ -794,6 +821,15 @@ core:
log_out_confirmation: "Are you sure you want to log out of {forum}?"
title: => core.ref.log_out
# Translations in this namespace are displayed on the login interface.
log_in:
invalid_login_message: => core.ref.invalid_login_message
username_or_email_placeholder: => core.ref.username_or_email_placeholder
password_placeholder: => core.ref.password
remember_me_label: => core.ref.remember_me_label
submit_button: => core.ref.log_in
title: => core.ref.log_in
# Translations in this namespace are displayed by the Reset Password interface.
reset_password:
confirm_password_label: Confirm New Password
@ -911,6 +947,7 @@ core:
generic_confirmation_message: "Are you sure you want to proceed? This action cannot be undone."
icon: Icon
icon_text: "Enter the name of any <a>FontAwesome</a> icon class, <em>including</em> the <code>fas fa-</code> prefix."
invalid_login_message: Your login details were incorrect.
load_more: Load More
loading: Loading...
log_in: Log In
@ -924,6 +961,7 @@ core:
password: Password
posts: Posts # Referenced by flarum-statistics.yml
previous_page: Previous Page
remember_me_label: Remember Me
remove: Remove
rename: Rename
reply: Reply # Referenced by flarum-mentions.yml
@ -937,6 +975,7 @@ core:
some_others: "{count, plural, one {# other} other {# others}}" # Referenced by flarum-likes.yml, flarum-mentions.yml
start_a_discussion: Start a Discussion
username: Username
username_or_email_placeholder: Username or Email
users: Users # Referenced by flarum-statistics.yml
view: View
write_a_reply: Write a Reply...

View File

@ -19,6 +19,7 @@ use Flarum\Foundation\ErrorHandling\WhoopsFormatter;
use Flarum\Foundation\Event\ClearingCache;
use Flarum\Frontend\AddLocaleAssets;
use Flarum\Frontend\AddTranslations;
use Flarum\Frontend\AssetManager;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Frontend\RecompileFrontendAssets;
use Flarum\Http\Middleware as HttpMiddleware;
@ -26,7 +27,10 @@ use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\LocaleManager;
use Flarum\Settings\Event\Saved;
use Flarum\Settings\Event\Saving;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Laminas\Stratigility\MiddlewarePipe;
class AdminServiceProvider extends AbstractServiceProvider
@ -104,6 +108,10 @@ class AdminServiceProvider extends AbstractServiceProvider
return $assets;
});
$this->container->afterResolving(AssetManager::class, function (AssetManager $assets) {
$assets->register('admin', 'flarum.assets.admin');
});
$this->container->bind('flarum.frontend.admin', function (Container $container) {
/** @var \Flarum\Frontend\Frontend $frontend */
$frontend = $container->make('flarum.frontend.factory')('admin');
@ -114,22 +122,34 @@ class AdminServiceProvider extends AbstractServiceProvider
});
}
public function boot(): void
public function boot(Container $container, Dispatcher $events): void
{
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin');
$events = $this->container->make('events');
$events->listen(
[Enabled::class, Disabled::class, ClearingCache::class],
function () {
function () use ($container) {
$recompile = new RecompileFrontendAssets(
$this->container->make('flarum.assets.admin'),
$this->container->make(LocaleManager::class)
$container->make('flarum.assets.admin'),
$container->make(LocaleManager::class)
);
$recompile->flush();
}
);
$events->listen(
[Saved::class, Saving::class],
function (Saved|Saving $event) use ($container) {
/** @var WhenSavingSettings $listener */
$listener = $container->make(WhenSavingSettings::class);
if ($event instanceof Saving) {
$listener->beforeSave($event);
} else {
$listener->afterSave($event);
}
}
);
}
protected function populateRoutes(RouteCollection $routes): void

View File

@ -13,6 +13,7 @@ use Flarum\Database\AbstractModel;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\ApplicationInfoProvider;
use Flarum\Foundation\Config;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Frontend\Document;
use Flarum\Group\Permission;
use Flarum\Search\AbstractDriver;
@ -35,7 +36,8 @@ class AdminPayload
protected ConnectionInterface $db,
protected Dispatcher $events,
protected Config $config,
protected ApplicationInfoProvider $appInfo
protected ApplicationInfoProvider $appInfo,
protected MaintenanceMode $maintenance
) {
}
@ -57,8 +59,6 @@ class AdminPayload
}, $this->container->make('flarum.http.slugDrivers'));
$document->payload['searchDrivers'] = $this->getSearchDrivers();
$document->payload['advancedPageEmpty'] = $this->checkAdvancedPageEmpty();
$document->payload['phpVersion'] = $this->appInfo->identifyPHPVersion();
$document->payload['mysqlVersion'] = $this->appInfo->identifyDatabaseVersion();
$document->payload['debugEnabled'] = Arr::get($this->config, 'debug');
@ -82,6 +82,10 @@ class AdminPayload
'total' => User::query()->count()
]
];
$document->payload['maintenanceByConfig'] = $this->maintenance->configOverride();
$document->payload['safeModeExtensions'] = $this->maintenance->safeModeExtensions();
$document->payload['safeModeExtensionsConfig'] = $this->config->safeModeExtensions();
}
protected function getSearchDrivers(): array
@ -98,9 +102,4 @@ class AdminPayload
return $searchDriversPerModel;
}
protected function checkAdvancedPageEmpty(): bool
{
return count($this->container->make('flarum.search.drivers')) === 1;
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Admin;
use Flarum\Extension\Extension;
use Flarum\Extension\ExtensionManager;
use Flarum\Frontend\AssetManager;
use Flarum\Locale\LocaleManager;
use Flarum\Settings\Event\Saved;
use Flarum\Settings\Event\Saving;
class WhenSavingSettings
{
/**
* Settings that should trigger JS cache clear when saved.
*
* @var string[]
*/
protected array $resetJsCacheFor = ['maintenance_mode', 'safe_mode_extensions'];
public function __construct(
protected AssetManager $assets,
protected LocaleManager $locales,
protected ExtensionManager $extensions,
) {
}
public function beforeSave(Saving $event): void
{
if (array_key_exists('safe_mode_extensions', $event->settings)) {
$safeModeExtensions = json_decode($event->settings['safe_mode_extensions'] ?? '[]', true);
$extensions = $this->extensions->getExtensions()->filter(function ($extension) use ($safeModeExtensions) {
return in_array($extension->getId(), $safeModeExtensions);
});
$sorted = array_map(fn (Extension $e) => $e->getId(), $this->extensions->sortDependencies($extensions->all()));
$event->settings['safe_mode_extensions'] = json_encode(array_values($sorted));
}
}
public function afterSave(Saved $event): void
{
$this->resetCache($event);
}
protected function resetCache(Saved $event): void
{
if (! $this->hasDirtySettings($event)) {
return;
}
$this->assets->flushJs();
}
public function resetJsCacheFor(string|array $setting): void
{
$this->resetJsCacheFor = array_merge($this->resetJsCacheFor, (array) $setting);
}
protected function hasDirtySettings(Saved $event): bool
{
return array_intersect(array_keys($event->settings), $this->resetJsCacheFor) !== [];
}
}

View File

@ -17,6 +17,7 @@ use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
@ -65,6 +66,7 @@ class ApiServiceProvider extends AbstractServiceProvider
HttpMiddleware\AuthenticateWithHeader::class,
HttpMiddleware\SetLocale::class,
'flarum.api.route_resolver',
'flarum.api.check_for_maintenance',
HttpMiddleware\CheckCsrfToken::class,
Middleware\ThrottleApi::class
];
@ -82,6 +84,17 @@ class ApiServiceProvider extends AbstractServiceProvider
return new HttpMiddleware\ResolveRoute($container->make('flarum.api.routes'));
});
$this->container->bind('flarum.api.check_for_maintenance', function (Container $container) {
return new HttpMiddleware\CheckForMaintenanceMode(
$container->make(MaintenanceMode::class),
$container->make('flarum.api.maintenance_route_exclusions')
);
});
$this->container->singleton('flarum.api.maintenance_route_exclusions', function () {
return [];
});
$this->container->singleton('flarum.api.handler', function (Container $container) {
$pipe = new MiddlewarePipe;
@ -108,6 +121,7 @@ class ApiServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\AuthenticateWithHeader::class,
'flarum.api.check_for_maintenance',
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\RememberFromCookie::class,
];

View File

@ -29,7 +29,7 @@ class Schedule extends LaravelSchedule
public function isDownForMaintenance(): bool
{
return $this->config->inMaintenanceMode();
return $this->config->inHighMaintenanceMode();
}
public function environment(): string

View File

@ -46,8 +46,6 @@ class UserState extends AbstractModel
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = ['last_read_post_number'];

View File

@ -9,6 +9,7 @@
namespace Flarum\Extend;
use Flarum\Admin\WhenSavingSettings;
use Flarum\Api\Serializer\AbstractSerializer;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Extension\Extension;
@ -22,6 +23,7 @@ class Settings implements ExtenderInterface
private array $settings = [];
private array $defaults = [];
private array $lessConfigs = [];
private array $resetJsCacheFor = [];
/**
* Serialize a setting value to the ForumSerializer attributes.
@ -82,6 +84,19 @@ class Settings implements ExtenderInterface
return $this;
}
/**
* Register a setting that should trigger JS cache clear when saved.
*
* @param string $setting: The key of the setting.
* @return self
*/
public function resetJsCacheFor(string $setting): self
{
$this->resetJsCacheFor[] = $setting;
return $this;
}
public function extend(Container $container, Extension $extension = null): void
{
if (! empty($this->defaults)) {
@ -134,5 +149,11 @@ class Settings implements ExtenderInterface
return array_merge($existingConfig, $config);
});
}
if (! empty($this->resetJsCacheFor)) {
$container->afterResolving(WhenSavingSettings::class, function (WhenSavingSettings $whenSavingSettings) {
$whenSavingSettings->resetJsCacheFor($this->resetJsCacheFor);
});
}
}
}

View File

@ -16,6 +16,7 @@ use Flarum\Extension\Event\Enabled;
use Flarum\Extension\Event\Enabling;
use Flarum\Extension\Event\Uninstalled;
use Flarum\Extension\Exception\CircularDependenciesException;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Foundation\Paths;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
@ -37,7 +38,8 @@ class ExtensionManager
protected Container $container,
protected Migrator $migrator,
protected Dispatcher $dispatcher,
protected Filesystem $filesystem
protected Filesystem $filesystem,
protected MaintenanceMode $maintenance,
) {
}
@ -302,7 +304,19 @@ class ExtensionManager
*/
public function extend(Container $container): void
{
foreach ($this->getEnabledExtensions() as $extension) {
$extensions = $this->getEnabledExtensions();
if ($this->maintenance->inSafeMode()) {
$safeModeExtensions = $this->maintenance->safeModeExtensions();
$extensions = array_filter($extensions, function (Extension $extension) use ($safeModeExtensions) {
return in_array($extension->getId(), $safeModeExtensions, true);
});
$extensions = $this->sortDependencies($extensions);
}
foreach ($extensions as $extension) {
$extension->extend($container);
}
}
@ -325,7 +339,21 @@ class ExtensionManager
*/
protected function setEnabledExtensions(array $enabledExtensions): void
{
$resolved = static::resolveExtensionOrder($enabledExtensions);
$this->config->set('extensions_enabled', json_encode(array_map(function (Extension $extension) {
return $extension->getId();
}, $this->sortDependencies($enabledExtensions))));
}
/**
* Apply a topological sort to the extensions to ensure that they are in the correct order.
*
* @param Extension[] $extensions
* @return Extension[]
* @throws CircularDependenciesException
*/
public function sortDependencies(array $extensions): array
{
$resolved = static::resolveExtensionOrder($extensions);
if (! empty($resolved['circularDependencies'])) {
throw new Exception\CircularDependenciesException(
@ -333,13 +361,7 @@ class ExtensionManager
);
}
$sortedEnabled = $resolved['valid'];
$sortedEnabledIds = array_map(function (Extension $extension) {
return $extension->getId();
}, $sortedEnabled);
$this->config->set('extensions_enabled', json_encode($sortedEnabledIds));
return $resolved['valid'];
}
public function isEnabled(string $extension): bool

View File

@ -11,6 +11,7 @@ namespace Flarum\Extension;
use Flarum\Extension\Event\Disabling;
use Flarum\Foundation\AbstractServiceProvider;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
class ExtensionServiceProvider extends AbstractServiceProvider
@ -24,8 +25,11 @@ class ExtensionServiceProvider extends AbstractServiceProvider
// listener on the app rather than in the service provider's boot method
// below, so that extensions have a chance to register things on the
// container before the core boots up (and starts resolving services).
$this->container['flarum']->booting(function () {
$this->container->make('flarum.extensions')->extend($this->container);
$this->container['flarum']->booting(function (Container $container) {
/** @var ExtensionManager $manager */
$manager = $container->make('flarum.extensions');
$manager->extend($container);
});
}

View File

@ -14,11 +14,16 @@ use Flarum\Forum\LogInValidator;
use Flarum\Http\AccessToken;
use Flarum\Http\RememberAccessToken;
use Flarum\Http\Rememberer;
use Flarum\Http\RequestUtil;
use Flarum\Http\SessionAuthenticator;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface;
use Flarum\User\Event\LoggedIn;
use Flarum\User\UserRepository;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
use Illuminate\Support\MessageBag;
use Laminas\Diactoros\Response\RedirectResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
@ -31,7 +36,9 @@ class LogInController implements RequestHandlerInterface
protected SessionAuthenticator $authenticator,
protected Dispatcher $events,
protected Rememberer $rememberer,
protected LogInValidator $validator
protected LogInValidator $validator,
protected UrlGenerator $url,
protected TranslatorInterface $translator
) {
}
@ -39,8 +46,22 @@ class LogInController implements RequestHandlerInterface
{
$body = $request->getParsedBody();
$params = Arr::only($body, ['identification', 'password', 'remember']);
$isHtmlRequest = RequestUtil::isHtmlRequest($request);
$errors = null;
if ($isHtmlRequest) {
$validator = $this->validator->basic()->prepare($body)->validator();
if (! $validator->passes()) {
$errors = $validator->errors();
$request->getAttribute('session')->put('errors', $errors);
return new RedirectResponse($this->url->to('forum')->route('maintenance.login'));
}
} else {
$this->validator->assertValid($body);
}
$response = $this->apiClient->withParentRequest($request)->withBody($params)->post('/token');
@ -59,6 +80,15 @@ class LogInController implements RequestHandlerInterface
}
}
if ($isHtmlRequest) {
if ($response->getStatusCode() === 401) {
$errors = new MessageBag(['identification' => $this->translator->trans('core.views.log_in.invalid_login_message')]);
$request->getAttribute('session')->put('errors', $errors);
}
return new RedirectResponse($this->url->to('forum')->route('maintenance.login'));
}
return $response;
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Forum\Controller;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Http\Controller\AbstractHtmlController;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Laminas\Diactoros\Response\RedirectResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Maintenance login view.
*/
class LogInViewController extends AbstractHtmlController
{
public function __construct(
protected Factory $view,
protected UrlGenerator $url,
protected MaintenanceMode $maintenance
) {
}
public function handle(Request $request): ResponseInterface
{
$actor = RequestUtil::getActor($request);
if (! $actor->isGuest() || ! $this->maintenance->inMaintenanceMode()) {
return new RedirectResponse($this->url->to('forum')->base());
}
return parent::handle($request);
}
public function render(Request $request): View
{
return $this->view
->make('flarum.forum::log-in')
->with('csrfToken', $request->getAttribute('session')->token());
}
}

View File

@ -18,8 +18,10 @@ use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\ErrorHandling\ViewFormatter;
use Flarum\Foundation\ErrorHandling\WhoopsFormatter;
use Flarum\Foundation\Event\ClearingCache;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Frontend\AddLocaleAssets;
use Flarum\Frontend\AddTranslations;
use Flarum\Frontend\AssetManager;
use Flarum\Frontend\Assets;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Frontend\Frontend;
@ -68,6 +70,7 @@ class ForumServiceProvider extends AbstractServiceProvider
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\SetLocale::class,
'flarum.forum.route_resolver',
'flarum.forum.check_for_maintenance',
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\ShareErrorsFromSession::class,
HttpMiddleware\FlarumPromotionHeader::class,
@ -88,6 +91,17 @@ class ForumServiceProvider extends AbstractServiceProvider
return new HttpMiddleware\ResolveRoute($container->make('flarum.forum.routes'));
});
$this->container->bind('flarum.forum.check_for_maintenance', function (Container $container) {
return new HttpMiddleware\CheckForMaintenanceMode(
$container->make(MaintenanceMode::class),
$container->make('flarum.forum.maintenance_route_exclusions')
);
});
$this->container->singleton('flarum.forum.maintenance_route_exclusions', function () {
return ['login', 'maintenance.login'];
});
$this->container->singleton('flarum.forum.handler', function (Container $container) {
$pipe = new MiddlewarePipe;
@ -128,6 +142,10 @@ class ForumServiceProvider extends AbstractServiceProvider
return $assets;
});
$this->container->afterResolving(AssetManager::class, function (AssetManager $assets) {
$assets->register('forum', 'flarum.assets.forum');
});
$this->container->bind('flarum.frontend.forum', function (Container $container, array $parameters = []) {
/** @var Frontend $frontend */
$frontend = $container->make('flarum.frontend.factory')('forum');

View File

@ -13,5 +13,16 @@ use Flarum\Foundation\AbstractValidator;
class LogInValidator extends AbstractValidator
{
public bool $basic = false;
protected array $rules = [];
public function basic(): static
{
$this->rules['identification'] = 'required';
$this->rules['password'] = 'required';
$this->basic = true;
return $this;
}
}

View File

@ -67,6 +67,12 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\GlobalLogOutController::class)
);
$map->get(
'/maintenance/login',
'maintenance.login',
$route->toController(Controller\LogInViewController::class)
);
$map->post(
'/login',
'login',

View File

@ -27,6 +27,8 @@ abstract class AbstractValidator
*/
protected array $rules = [];
protected ?Validator $laravelValidator = null;
public function __construct(
protected Factory $validator,
protected TranslatorInterface $translator
@ -40,6 +42,8 @@ abstract class AbstractValidator
/**
* Throw an exception if a model is not valid.
*
* @throws ValidationException
*/
public function assertValid(array $attributes): void
{
@ -50,6 +54,18 @@ abstract class AbstractValidator
}
}
public function prepare(array $attributes): static
{
$this->laravelValidator ??= $this->makeValidator($attributes);
return $this;
}
public function validator(): Validator
{
return $this->laravelValidator;
}
protected function getRules(): array
{
return $this->rules;

View File

@ -45,9 +45,14 @@ class Application extends IlluminateContainer implements LaravelApplication
$this->registerCoreContainerAliases();
}
public function getConfig(): Config
{
return $this->make(Config::class);
}
public function config(string $key, mixed $default = null): mixed
{
$config = $this->make('flarum.config');
$config = $this->getConfig();
return $config[$key] ?? $default;
}
@ -211,6 +216,7 @@ class Application extends IlluminateContainer implements LaravelApplication
'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class],
'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class],
'flarum.assets' => [\Flarum\Frontend\AssetManager::class],
'hash' => [\Illuminate\Contracts\Hashing\Hasher::class],
'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class],
'validator' => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],

View File

@ -139,13 +139,9 @@ trait InteractsWithLaravel
return null; // @phpstan-ignore-line
}
/**
* @deprecated Not actually used/has no meaning in Flarum.
*/
public function isDownForMaintenance(): bool
{
// TODO: Implement isDownForMaintenance() method.
return false;
return $this->getConfig()->inHighMaintenanceMode();
}
/**

View File

@ -36,7 +36,36 @@ class Config implements ArrayAccess
public function inMaintenanceMode(): bool
{
return $this->data['offline'] ?? false;
return $this->inHighMaintenanceMode() || $this->inLowMaintenanceMode() || $this->inSafeMode();
}
public function inHighMaintenanceMode(): bool
{
return $this->maintenanceMode() === MaintenanceMode::HIGH;
}
public function inLowMaintenanceMode(): bool
{
return $this->maintenanceMode() === MaintenanceMode::LOW;
}
public function inSafeMode(): bool
{
return $this->maintenanceMode() === MaintenanceMode::SAFE;
}
public function maintenanceMode(): string
{
return match ($mode = $this->data['offline'] ?? MaintenanceMode::NONE) {
true => MaintenanceMode::HIGH,
false => MaintenanceMode::NONE,
default => $mode,
};
}
public function safeModeExtensions(): ?array
{
return $this->data['safe_mode_extensions'];
}
private function requireKeys(mixed ...$keys): void

View File

@ -29,7 +29,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
*/
class ViewFormatter implements HttpFormatter
{
const ERRORS_WITH_VIEWS = ['csrf_token_mismatch', 'not_found'];
const ERRORS_WITH_VIEWS = ['csrf_token_mismatch', 'not_found', 'maintenance'];
public function __construct(
protected ViewFactory $view,

View File

@ -24,8 +24,17 @@ use Psr\Http\Message\ServerRequestInterface as Request;
*/
class WhoopsFormatter implements HttpFormatter
{
public function __construct(
protected ViewFormatter $viewFormatter
) {
}
public function format(HandledError $error, Request $request): Response
{
if (! $error->shouldBeReported()) {
return $this->viewFormatter->format($error, $request);
}
return WhoopsRunner::handle($error->getException(), $request)
->withStatus($error->getStatusCode());
}

View File

@ -44,6 +44,9 @@ class ErrorServiceProvider extends AbstractServiceProvider
// 429 Too Many Requests
'too_many_requests' => 429,
// 503 Service Unavailable
'maintenance' => 503,
];
});

View File

@ -9,7 +9,7 @@
namespace Flarum\Foundation;
use Illuminate\Support\Str;
use Flarum\Http\RequestUtil;
use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@ -17,7 +17,7 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Tobscure\JsonApi\Document;
class MaintenanceModeHandler implements RequestHandlerInterface
class HighMaintenanceModeHandler implements RequestHandlerInterface
{
const MESSAGE = 'Currently down for maintenance. Please come back later.';
@ -27,7 +27,7 @@ class MaintenanceModeHandler implements RequestHandlerInterface
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Special handling for API requests: they get a proper API response
if ($this->isApiRequest($request)) {
if (RequestUtil::isApiRequest($request)) {
return $this->apiResponse();
}
@ -35,14 +35,6 @@ class MaintenanceModeHandler implements RequestHandlerInterface
return new HtmlResponse(self::MESSAGE, 503);
}
private function isApiRequest(ServerRequestInterface $request): bool
{
return Str::contains(
$request->getHeaderLine('Accept'),
'application/vnd.api+json'
);
}
private function apiResponse(): ResponseInterface
{
return new JsonResponse(

View File

@ -35,7 +35,7 @@ class InstalledApp implements AppInterface
public function getRequestHandler(): RequestHandlerInterface
{
if ($this->config->inMaintenanceMode()) {
if ($this->config->inHighMaintenanceMode()) {
return $this->container->make('flarum.maintenance.handler');
} elseif ($this->needsUpdate()) {
return $this->getUpdaterHandler();

View File

@ -96,7 +96,7 @@ class InstalledSite implements SiteInterface
$app->alias('flarum.config', Config::class);
$app->instance('flarum.debug', $this->config->inDebugMode());
$app->instance('config', $this->getIlluminateConfig());
$app->instance('flarum.maintenance.handler', new MaintenanceModeHandler);
$app->instance('flarum.maintenance.handler', new HighMaintenanceModeHandler);
$this->registerLogger($app);
$this->registerCache($app);

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Foundation;
use Flarum\Settings\SettingsRepositoryInterface;
class MaintenanceMode
{
public const NONE = 'none';
public const HIGH = 'high';
public const LOW = 'low';
public const SAFE = 'safe';
public function __construct(
protected readonly Config $config,
protected readonly SettingsRepositoryInterface $settings
) {
}
public function inMaintenanceMode(): bool
{
return $this->inHighMaintenanceMode() || $this->inLowMaintenanceMode() || $this->inSafeMode();
}
public function inHighMaintenanceMode(): bool
{
return $this->mode() === self::HIGH;
}
public function inLowMaintenanceMode(): bool
{
return $this->mode() === self::LOW;
}
public function inSafeMode(): bool
{
return $this->mode() === self::SAFE;
}
public function mode(): string
{
$mode = $this->config->maintenanceMode();
if ($mode === self::NONE) {
$mode = strval($this->settings->get('maintenance_mode', self::NONE));
// Cannot set high maintenance mode from the settings.
if ($mode === self::HIGH) {
$mode = self::NONE;
}
}
return $mode;
}
/** @return string[] */
public function safeModeExtensions(): array
{
$extensions = $this->config->safeModeExtensions();
if ($extensions === null) {
$extensions = json_decode($this->settings->get('safe_mode_extensions', '[]'), true);
}
return $extensions;
}
public function configOverride(): bool
{
return $this->config->maintenanceMode() !== self::NONE;
}
}

View File

@ -0,0 +1,63 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Locale\LocaleManager;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
class AssetManager
{
/**
* @var array<string, string>
*/
protected array $assets = [];
public function __construct(
protected readonly Container $container,
protected readonly LocaleManager $locales
) {
}
public function frontend(string $frontend): Assets
{
if (! in_array($frontend, $this->assets)) {
throw new InvalidArgumentException("Unknown frontend: $frontend");
}
return $this->container->make($this->assets[$frontend]);
}
/**
* @return array<Assets>
* @throws BindingResolutionException
*/
public function all(): array
{
return array_map(fn (string $abstract) => $this->container->make($abstract), $this->assets);
}
public function register(string $frontend, string $abstract): void
{
$this->assets[$frontend] = $abstract;
}
public function flushJs(): void
{
foreach ($this->all() as $assets) {
$assets->makeJs()->flush();
foreach ($this->locales->getLocales() as $locale => $name) {
$assets->makeLocaleJs($locale)->flush();
}
}
}
}

View File

@ -9,6 +9,7 @@
namespace Flarum\Frontend\Content;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Frontend\Document;
use Flarum\Http\RequestUtil;
use Flarum\Locale\LocaleManager;
@ -17,7 +18,8 @@ use Psr\Http\Message\ServerRequestInterface as Request;
class CorePayload
{
public function __construct(
private readonly LocaleManager $locales
private readonly LocaleManager $locales,
private readonly MaintenanceMode $maintenance,
) {
}
@ -33,15 +35,21 @@ class CorePayload
{
$data = $this->getDataFromApiDocument($document->getForumApiDocument());
return [
$payload = [
'resources' => $data,
'session' => [
'userId' => RequestUtil::getActor($request)->id,
'csrfToken' => $request->getAttribute('session')->token()
],
'locales' => $this->locales->getLocales(),
'locale' => $request->getAttribute('locale')
'locale' => $request->getAttribute('locale'),
];
if ($this->maintenance->inMaintenanceMode()) {
$payload['maintenanceMode'] = $this->maintenance->mode();
}
return $payload;
}
private function getDataFromApiDocument(array $apiDocument): array

View File

@ -29,6 +29,10 @@ class FrontendServiceProvider extends AbstractServiceProvider
{
public function register(): void
{
$this->container->singleton('flarum.assets', function (Container $container) {
return new AssetManager($container, $container->make(LocaleManager::class));
});
$this->container->singleton('flarum.assets.factory', function (Container $container) {
return function (string $name) use ($container) {
$paths = $container[Paths::class];
@ -180,6 +184,10 @@ class FrontendServiceProvider extends AbstractServiceProvider
return $assets;
});
$this->container->afterResolving(AssetManager::class, function (AssetManager $assets) {
$assets->register('common', 'flarum.assets.common');
});
}
public function boot(Container $container, Dispatcher $events, ViewFactory $views): void

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Http\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class MaintenanceModeException extends Exception implements KnownError
{
public function getType(): string
{
return 'maintenance';
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Http\Middleware;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Http\Exception\MaintenanceModeException;
use Flarum\Http\RequestUtil;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class CheckForMaintenanceMode implements MiddlewareInterface
{
public function __construct(
private readonly MaintenanceMode $maintenance,
private readonly array $exemptRoutes,
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$actor = RequestUtil::getActor($request);
$isRouteExcluded = in_array($request->getAttribute('routeName'), $this->exemptRoutes, true);
if ($this->maintenance->inMaintenanceMode() && ! $actor->isAdmin() && ! $isRouteExcluded) {
throw new MaintenanceModeException('The forum is currently in maintenance mode.');
}
return $handler->handle($request);
}
}

View File

@ -10,10 +10,27 @@
namespace Flarum\Http;
use Flarum\User\User;
use Illuminate\Support\Str;
use Psr\Http\Message\ServerRequestInterface as Request;
class RequestUtil
{
public static function isApiRequest(Request $request): bool
{
return Str::contains(
$request->getHeaderLine('Accept'),
'application/vnd.api+json'
);
}
public static function isHtmlRequest(Request $request): bool
{
return Str::contains(
$request->getHeaderLine('Accept'),
'text/html'
);
}
public static function getActor(Request $request): User
{
return $request->getAttribute('actorReference')->getActor();

View File

@ -1,27 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Queue\Console;
use Flarum\Foundation\Config;
class WorkCommand extends \Illuminate\Queue\Console\WorkCommand
{
protected function downForMaintenance()
{
if ($this->option('force')) {
return false;
}
/** @var Config $config */
$config = $this->laravel->make(Config::class);
return $config->inMaintenanceMode();
}
}

View File

@ -39,7 +39,7 @@ class QueueServiceProvider extends AbstractServiceProvider
Commands\ListFailedCommand::class,
Commands\RestartCommand::class,
Commands\RetryCommand::class,
Console\WorkCommand::class,
Commands\WorkCommand::class,
];
public function register(): void
@ -74,7 +74,7 @@ class QueueServiceProvider extends AbstractServiceProvider
$container['events'],
$container[ExceptionHandling::class],
function () use ($config) {
return $config->inMaintenanceMode();
return $config->inHighMaintenanceMode();
}
);

View File

@ -22,8 +22,6 @@ use Symfony\Component\Mime\MimeTypes;
class AvatarValidator extends AbstractValidator
{
protected Validator $laravelValidator;
public function __construct(
Factory $validator,
TranslatorInterface $translator,

View File

@ -10,6 +10,7 @@
namespace Flarum\Tests\unit\Foundation;
use Flarum\Foundation\Config;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Testing\unit\TestCase;
use InvalidArgumentException;
use RuntimeException;
@ -73,14 +74,32 @@ class ConfigTest extends TestCase
'offline' => false,
]);
$this->assertFalse($config->inMaintenanceMode());
$this->assertFalse($config->inHighMaintenanceMode());
$config = new Config([
'url' => 'https://flarum.localhost',
'offline' => true,
]);
$this->assertTrue($config->inMaintenanceMode());
$this->assertTrue($config->inHighMaintenanceMode());
$config = new Config([
'url' => 'https://flarum.localhost',
'offline' => MaintenanceMode::LOW,
]);
$this->assertFalse($config->inSafeMode());
$this->assertTrue($config->inLowMaintenanceMode());
$this->assertFalse($config->inHighMaintenanceMode());
$config = new Config([
'url' => 'https://flarum.localhost',
'offline' => MaintenanceMode::SAFE,
]);
$this->assertTrue($config->inSafeMode());
$this->assertFalse($config->inLowMaintenanceMode());
$this->assertFalse($config->inHighMaintenanceMode());
}
/** @test */
@ -90,7 +109,7 @@ class ConfigTest extends TestCase
'url' => 'https://flarum.localhost',
]);
$this->assertFalse($config->inMaintenanceMode());
$this->assertFalse($config->inHighMaintenanceMode());
}
/** @test */

View File

@ -0,0 +1,14 @@
@extends('flarum.forum::layouts.basic')
@section('title', $translator->trans('core.views.error.maintenance_mode_title'))
@section('content')
<p>
{{ $translator->trans('core.views.error.maintenance_mode_message') }}
</p>
<p>
<a href="{{ $url->to('forum')->route('maintenance.login') }}">
{{ $translator->trans('core.views.error.maintenance_mode_link') }}
</a>
</p>
@endsection

View File

@ -0,0 +1,38 @@
@extends('flarum.forum::layouts.basic')
@section('title', $translator->trans('core.views.log_in.title'))
@section('content')
@if ($errors->any())
<div class="errors">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form class="form" method="POST" action="{{ $url->to('forum')->route('login') }}">
<input type="hidden" name="csrfToken" value="{{ $csrfToken }}">
<p class="form-group">
<input type="text" class="form-control" name="identification" placeholder="{{ $translator->trans('core.views.log_in.username_or_email_placeholder') }}" aria-label="{{ $translator->trans('core.views.log_in.username_or_email_placeholder') }}">
</p>
<p class="form-group">
<input type="password" class="form-control" name="password" autocomplete="current-password" placeholder="{{ $translator->trans('core.views.log_in.password_placeholder') }}" aria-label="{{ $translator->trans('core.views.log_in.password_placeholder') }}">
</p>
<p class="form-group">
<label>
<input type="checkbox" name="remember" value="1" tabindex="1">
<span>{{ $translator->trans('core.views.log_in.remember_me_label') }}</span>
</label>
</p>
<p class="form-group">
<button type="submit" class="button">{{ $translator->trans('core.views.log_in.submit_button') }}</button>
</p>
</form>
@endsection

View File

@ -12,6 +12,7 @@ namespace Flarum\Testing\integration\Extension;
use Flarum\Database\Migrator;
use Flarum\Extension\Extension;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\MaintenanceMode;
use Flarum\Foundation\Paths;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
@ -42,9 +43,10 @@ class ExtensionManagerIncludeCurrent extends ExtensionManager
Migrator $migrator,
Dispatcher $dispatcher,
Filesystem $filesystem,
MaintenanceMode $maintenance,
array $enabledIds
) {
parent::__construct($config, $paths, $container, $migrator, $dispatcher, $filesystem);
parent::__construct($config, $paths, $container, $migrator, $dispatcher, $filesystem, $maintenance);
$this->enabledIds = $enabledIds;
}