diff --git a/framework/core/js/src/admin/AdminApplication.js b/framework/core/js/src/admin/AdminApplication.js index 153ec13a7..e074eab24 100644 --- a/framework/core/js/src/admin/AdminApplication.js +++ b/framework/core/js/src/admin/AdminApplication.js @@ -1,16 +1,12 @@ import HeaderPrimary from './components/HeaderPrimary'; import HeaderSecondary from './components/HeaderSecondary'; import routes from './routes'; -import ExtensionPage from './components/ExtensionPage'; import Application from '../common/Application'; import Navigation from '../common/components/Navigation'; import AdminNav from './components/AdminNav'; import ExtensionData from './utils/ExtensionData'; export default class AdminApplication extends Application { - // Deprecated as of beta 15 - extensionSettings = {}; - extensionData = new ExtensionData(); extensionCategories = { @@ -61,14 +57,6 @@ export default class AdminApplication extends Application { m.mount(document.getElementById('header-primary'), HeaderPrimary); m.mount(document.getElementById('header-secondary'), HeaderSecondary); m.mount(document.getElementById('admin-navigation'), AdminNav); - - // If an extension has just been enabled, then we will run its settings - // callback. - const enabled = localStorage.getItem('enabledExtension'); - if (enabled && this.extensionSettings[enabled] && typeof this.extensionSettings[enabled] === 'function') { - this.extensionSettings[enabled](); - localStorage.removeItem('enabledExtension'); - } } getRequiredPermissions(permission) { diff --git a/framework/core/js/src/admin/compat.js b/framework/core/js/src/admin/compat.js index fa3d3d8e2..a03f90430 100644 --- a/framework/core/js/src/admin/compat.js +++ b/framework/core/js/src/admin/compat.js @@ -8,6 +8,7 @@ import SettingDropdown from './components/SettingDropdown'; import EditCustomFooterModal from './components/EditCustomFooterModal'; import SessionDropdown from './components/SessionDropdown'; import HeaderPrimary from './components/HeaderPrimary'; +import AdminPage from './components/AdminPage'; import AppearancePage from './components/AppearancePage'; import StatusWidget from './components/StatusWidget'; import ExtensionsWidget from './components/ExtensionsWidget'; @@ -16,7 +17,6 @@ import SettingsModal from './components/SettingsModal'; import DashboardWidget from './components/DashboardWidget'; import ExtensionPage from './components/ExtensionPage'; import ExtensionLinkButton from './components/ExtensionLinkButton'; -import AdminLinkButton from './components/AdminLinkButton'; import PermissionGrid from './components/PermissionGrid'; import ExtensionPermissionGrid from './components/ExtensionPermissionGrid'; import MailPage from './components/MailPage'; @@ -43,6 +43,7 @@ export default Object.assign(compat, { 'components/EditCustomFooterModal': EditCustomFooterModal, 'components/SessionDropdown': SessionDropdown, 'components/HeaderPrimary': HeaderPrimary, + 'components/AdminPage': AdminPage, 'components/AppearancePage': AppearancePage, 'components/StatusWidget': StatusWidget, 'components/ExtensionsWidget': ExtensionsWidget, @@ -51,7 +52,6 @@ export default Object.assign(compat, { 'components/DashboardWidget': DashboardWidget, 'components/ExtensionPage': ExtensionPage, 'components/ExtensionLinkButton': ExtensionLinkButton, - 'components/AdminLinkButton': AdminLinkButton, 'components/PermissionGrid': PermissionGrid, 'components/ExtensionPermissionGrid': ExtensionPermissionGrid, 'components/MailPage': MailPage, diff --git a/framework/core/js/src/admin/components/AddExtensionModal.js b/framework/core/js/src/admin/components/AddExtensionModal.js deleted file mode 100644 index d3428c9ac..000000000 --- a/framework/core/js/src/admin/components/AddExtensionModal.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This file is part of Flarum. - * - * (c) Toby Zerner - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import Modal from '../../common/components/Modal'; - -export default class AddExtensionModal extends Modal { - className() { - return 'AddExtensionModal Modal--small'; - } - - title() { - return app.translator.trans('core.admin.add_extension.title'); - } - - content() { - return ( -
-

{app.translator.trans('core.admin.add_extension.temporary_text')}

-

- {app.translator.trans('core.admin.add_extension.install_text', { a: })} -

-

{app.translator.trans('core.admin.add_extension.developer_text', { a: })}

-
- ); - } -} diff --git a/framework/core/js/src/admin/components/AdminLinkButton.js b/framework/core/js/src/admin/components/AdminLinkButton.js deleted file mode 100644 index ff98f7130..000000000 --- a/framework/core/js/src/admin/components/AdminLinkButton.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * This file is part of Flarum. - * - * (c) Toby Zerner - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import LinkButton from '../../common/components/LinkButton'; - -export default class AdminLinkButton extends LinkButton { - getButtonContent(children) { - return [...super.getButtonContent(children),
{this.attrs.description}
]; - } -} diff --git a/framework/core/js/src/admin/components/AdminPage.js b/framework/core/js/src/admin/components/AdminPage.js new file mode 100644 index 000000000..4c009c7da --- /dev/null +++ b/framework/core/js/src/admin/components/AdminPage.js @@ -0,0 +1,174 @@ +import Page from '../../common/components/Page'; +import Button from '../../common/components/Button'; +import Switch from '../../common/components/Switch'; +import Select from '../../common/components/Select'; +import classList from '../../common/utils/classList'; +import Stream from '../../common/utils/Stream'; +import saveSettings from '../utils/saveSettings'; +import AdminHeader from './AdminHeader'; + +export default class AdminPage extends Page { + oninit(vnode) { + super.oninit(vnode); + + this.settings = {}; + + this.loading = false; + } + + view() { + const className = classList(['AdminPage', this.headerInfo().className]); + + return ( +
+ {this.header()} +
{this.content()}
+
+ ); + } + + content() { + return ''; + } + + submitButton() { + return ( + + ); + } + + header() { + const headerInfo = this.headerInfo(); + + return ( + + {headerInfo.title} + + ); + } + + headerInfo() { + return { + className: '', + icon: '', + title: '', + description: '', + }; + } + + /** + * buildSettingComponent takes a settings object and turns it into a component. + * Depending on the type of input, you can set the type to 'bool', 'select', or + * any standard type. Any values inside the 'extra' object will be added + * to the component as an attribute. + * + * Alternatively, you can pass a callback that will be executed in ExtensionPage's + * context to include custom JSX elements. + * + * @example + * + * { + * setting: 'acme.checkbox', + * label: app.translator.trans('acme.admin.setting_label'), + * type: 'bool', + * help: app.translator.trans('acme.admin.setting_help'), + * className: 'Setting-item' + * } + * + * @example + * + * { + * setting: 'acme.select', + * label: app.translator.trans('acme.admin.setting_label'), + * type: 'select', + * options: { + * 'option1': 'Option 1 label', + * 'option2': 'Option 2 label', + * }, + * default: 'option1', + * } + * + * @param setting + * @returns {JSX.Element} + */ + buildSettingComponent(entry) { + if (typeof entry === 'function') { + return entry.call(this); + } + + const setting = entry.setting; + const help = entry.help; + delete entry.help; + + const value = this.setting([setting])(); + if (['bool', 'checkbox', 'switch', 'boolean'].includes(entry.type)) { + return ( +
+ + {entry.label} + +
{help}
+
+ ); + } else if (['select', 'dropdown', 'selectdropdown'].includes(entry.type)) { + return ( +
+ +
{help}
+ +
+ ); + } + } + + onsaved() { + this.loading = false; + + app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.settings.saved_message')); + } + + setting(key, fallback = '') { + this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback); + + return this.settings[key]; + } + + dirty() { + const dirty = {}; + + Object.keys(this.settings).forEach((key) => { + const value = this.settings[key](); + + if (value !== app.data.settings[key]) { + dirty[key] = value; + } + }); + + return dirty; + } + + isChanged() { + return Object.keys(this.dirty()).length; + } + + saveSettings(e) { + e.preventDefault(); + + app.alerts.clear(); + + this.loading = true; + + return saveSettings(this.dirty()).then(this.onsaved.bind(this)); + } +} diff --git a/framework/core/js/src/admin/components/AppearancePage.js b/framework/core/js/src/admin/components/AppearancePage.js index 041d998c8..ad728e269 100644 --- a/framework/core/js/src/admin/components/AppearancePage.js +++ b/framework/core/js/src/admin/components/AppearancePage.js @@ -1,141 +1,120 @@ -import Page from '../../common/components/Page'; import Button from '../../common/components/Button'; -import Switch from '../../common/components/Switch'; -import Stream from '../../common/utils/Stream'; import EditCustomCssModal from './EditCustomCssModal'; import EditCustomHeaderModal from './EditCustomHeaderModal'; import EditCustomFooterModal from './EditCustomFooterModal'; import UploadImageButton from './UploadImageButton'; -import saveSettings from '../utils/saveSettings'; -import AdminHeader from './AdminHeader'; +import AdminPage from './AdminPage'; -export default class AppearancePage extends Page { - oninit(vnode) { - super.oninit(vnode); - - this.primaryColor = Stream(app.data.settings.theme_primary_color); - this.secondaryColor = Stream(app.data.settings.theme_secondary_color); - this.darkMode = Stream(app.data.settings.theme_dark_mode); - this.coloredHeader = Stream(app.data.settings.theme_colored_header); +export default class AppearancePage extends AdminPage { + headerInfo() { + return { + className: 'AppearancePage', + icon: 'fas fa-paint-brush', + title: app.translator.trans('core.admin.appearance.title'), + description: app.translator.trans('core.admin.appearance.description'), + }; } - view() { - return ( -
- - {app.translator.trans('core.admin.appearance.title')} - -
-
-
- {app.translator.trans('core.admin.appearance.colors_heading')} -
{app.translator.trans('core.admin.appearance.colors_text')}
+ content() { + return [ +
+
+ {app.translator.trans('core.admin.appearance.colors_heading')} +
{app.translator.trans('core.admin.appearance.colors_text')}
-
- - -
+
+ {this.buildSettingComponent({ + type: 'text', + setting: 'theme_primary_color', + placeholder: '#aaaaaa', + })} + {this.buildSettingComponent({ + type: 'text', + setting: 'theme_secondary_color', + placeholder: '#aaaaaa', + })} +
- {Switch.component( - { - state: this.darkMode(), - onchange: this.darkMode, - }, - app.translator.trans('core.admin.appearance.dark_mode_label') - )} + {this.buildSettingComponent({ + type: 'switch', + setting: 'theme_dark_mode', + label: app.translator.trans('core.admin.appearance.dark_mode_label'), + })} - {Switch.component( - { - state: this.coloredHeader(), - onchange: this.coloredHeader, - }, - app.translator.trans('core.admin.appearance.colored_header_label') - )} + {this.buildSettingComponent({ + type: 'switch', + setting: 'theme_colored_header', + label: app.translator.trans('core.admin.appearance.colored_header_label'), + })} - {Button.component( - { - className: 'Button Button--primary', - type: 'submit', - loading: this.loading, - }, - app.translator.trans('core.admin.appearance.submit_button') - )} -
- + {this.submitButton()} +
+
, -
- {app.translator.trans('core.admin.appearance.logo_heading')} -
{app.translator.trans('core.admin.appearance.logo_text')}
- -
+
+ {app.translator.trans('core.admin.appearance.logo_heading')} +
{app.translator.trans('core.admin.appearance.logo_text')}
+ +
, -
- {app.translator.trans('core.admin.appearance.favicon_heading')} -
{app.translator.trans('core.admin.appearance.favicon_text')}
- -
+
+ {app.translator.trans('core.admin.appearance.favicon_heading')} +
{app.translator.trans('core.admin.appearance.favicon_text')}
+ +
, -
- {app.translator.trans('core.admin.appearance.custom_header_heading')} -
{app.translator.trans('core.admin.appearance.custom_header_text')}
- {Button.component( - { - className: 'Button', - onclick: () => app.modal.show(EditCustomHeaderModal), - }, - app.translator.trans('core.admin.appearance.edit_header_button') - )} -
+
+ {app.translator.trans('core.admin.appearance.custom_header_heading')} +
{app.translator.trans('core.admin.appearance.custom_header_text')}
+ {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomHeaderModal), + }, + app.translator.trans('core.admin.appearance.edit_header_button') + )} +
, -
- {app.translator.trans('core.admin.appearance.custom_footer_heading')} -
{app.translator.trans('core.admin.appearance.custom_footer_text')}
- {Button.component( - { - className: 'Button', - onclick: () => app.modal.show(EditCustomFooterModal), - }, - app.translator.trans('core.admin.appearance.edit_footer_button') - )} -
+
+ {app.translator.trans('core.admin.appearance.custom_footer_heading')} +
{app.translator.trans('core.admin.appearance.custom_footer_text')}
+ {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomFooterModal), + }, + app.translator.trans('core.admin.appearance.edit_footer_button') + )} +
, -
- {app.translator.trans('core.admin.appearance.custom_styles_heading')} -
{app.translator.trans('core.admin.appearance.custom_styles_text')}
- {Button.component( - { - className: 'Button', - onclick: () => app.modal.show(EditCustomCssModal), - }, - app.translator.trans('core.admin.appearance.edit_css_button') - )} -
-
- - ); +
+ {app.translator.trans('core.admin.appearance.custom_styles_heading')} +
{app.translator.trans('core.admin.appearance.custom_styles_text')}
+ {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomCssModal), + }, + app.translator.trans('core.admin.appearance.edit_css_button') + )} +
, + ]; } - onsubmit(e) { + onsaved() { + window.location.reload(); + } + + saveSettings(e) { e.preventDefault(); const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i; - if (!hex.test(this.primaryColor()) || !hex.test(this.secondaryColor())) { + if (!hex.test(this.settings['theme_primary_color']()) || !hex.test(this.settings['theme_secondary_color']())) { alert(app.translator.trans('core.admin.appearance.enter_hex_message')); return; } - this.loading = true; - - saveSettings({ - theme_primary_color: this.primaryColor(), - theme_secondary_color: this.secondaryColor(), - theme_dark_mode: this.darkMode(), - theme_colored_header: this.coloredHeader(), - }).then(() => window.location.reload()); + super.saveSettings(e); } } diff --git a/framework/core/js/src/admin/components/BasicsPage.js b/framework/core/js/src/admin/components/BasicsPage.js index 7b452117d..a437b8a50 100644 --- a/framework/core/js/src/admin/components/BasicsPage.js +++ b/framework/core/js/src/admin/components/BasicsPage.js @@ -1,31 +1,11 @@ -import Page from '../../common/components/Page'; import FieldSet from '../../common/components/FieldSet'; -import Select from '../../common/components/Select'; -import Button from '../../common/components/Button'; -import saveSettings from '../utils/saveSettings'; import ItemList from '../../common/utils/ItemList'; -import Switch from '../../common/components/Switch'; -import Stream from '../../common/utils/Stream'; -import withAttr from '../../common/utils/withAttr'; -import AdminHeader from './AdminHeader'; +import AdminPage from './AdminPage'; -export default class BasicsPage extends Page { +export default class BasicsPage extends AdminPage { oninit(vnode) { super.oninit(vnode); - this.loading = false; - - this.fields = [ - 'forum_title', - 'forum_description', - 'default_locale', - 'show_language_selector', - 'default_route', - 'welcome_title', - 'welcome_message', - 'display_name_driver', - ]; - this.localeOptions = {}; const locales = app.data.locales; for (const i in locales) { @@ -40,157 +20,99 @@ export default class BasicsPage extends Page { this.slugDriverOptions = {}; Object.keys(app.data.slugDrivers).forEach((model) => { - this.fields.push(`slug_driver_${model}`); this.slugDriverOptions[model] = {}; app.data.slugDrivers[model].forEach((option) => { this.slugDriverOptions[model][option] = option; }); }); - - this.values = {}; - - const settings = app.data.settings; - this.fields.forEach((key) => (this.values[key] = Stream(settings[key]))); - - if (!this.values.display_name_driver() && displayNameDrivers.includes('username')) this.values.display_name_driver('username'); - - Object.keys(app.data.slugDrivers).forEach((model) => { - if (!this.values[`slug_driver_${model}`]() && 'default' in this.slugDriverOptions[model]) { - this.values[`slug_driver_${model}`]('default'); - } - }); - - if (this.values.show_language_selector() === undefined) this.values.show_language_selector(1); } - view() { - return ( -
- - {app.translator.trans('core.admin.basics.title')} - -
-
- {FieldSet.component( - { - label: app.translator.trans('core.admin.basics.forum_title_heading'), - }, - [] - )} + headerInfo() { + return { + className: 'BasicsPage', + icon: 'fas fa-pencil-alt', + title: app.translator.trans('core.admin.basics.title'), + description: app.translator.trans('core.admin.basics.description'), + }; + } - {FieldSet.component( - { - label: app.translator.trans('core.admin.basics.forum_description_heading'), - }, - [ -
{app.translator.trans('core.admin.basics.forum_description_text')}
, -