AdminUX Patch and Admin Page (#2593)

* AdminPage

* More fixes

* Settings Modal Drop

* Translation and docblock

* settingS

* Convert Fieldset to JSX

* info -> headerInfo, className

* Overflow fixes

* MailPage

* Admin Less

* Basics Page

* Changes

* Cleanup

* Permission Page

* Add padding
This commit is contained in:
Charlie 2021-02-18 15:45:43 -08:00 committed by GitHub
parent c4ebebe48e
commit 71ccdc00e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 563 additions and 767 deletions

View File

@ -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) {

View File

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

View File

@ -1,32 +0,0 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import Modal from '../../common/components/Modal';
export default class AddExtensionModal extends Modal {
className() {
return 'AddExtensionModal Modal--small';
}
title() {
return app.translator.trans('core.admin.add_extension.title');
}
content() {
return (
<div className="Modal-body">
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
<p>
{app.translator.trans('core.admin.add_extension.install_text', { a: <a href="https://discuss.flarum.org/t/extensions" target="_blank" /> })}
</p>
<p>{app.translator.trans('core.admin.add_extension.developer_text', { a: <a href="http://flarum.org/docs/extend" target="_blank" /> })}</p>
</div>
);
}
}

View File

@ -1,16 +0,0 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import LinkButton from '../../common/components/LinkButton';
export default class AdminLinkButton extends LinkButton {
getButtonContent(children) {
return [...super.getButtonContent(children), <div className="AdminLinkButton-description">{this.attrs.description}</div>];
}
}

View File

@ -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 (
<div className={className}>
{this.header()}
<div className="container">{this.content()}</div>
</div>
);
}
content() {
return '';
}
submitButton() {
return (
<Button onclick={this.saveSettings.bind(this)} className="Button Button--primary" loading={this.loading} disabled={!this.isChanged()}>
{app.translator.trans('core.admin.settings.submit_button')}
</Button>
);
}
header() {
const headerInfo = this.headerInfo();
return (
<AdminHeader icon={headerInfo.icon} description={headerInfo.description} className={headerInfo.className + '-header'}>
{headerInfo.title}
</AdminHeader>
);
}
headerInfo() {
return {
className: '',
icon: '',
title: '',
description: '',
};
}
/**
* buildSettingComponent takes a settings object and turns it into a component.
* Depending on the type of input, you can set the type to 'bool', 'select', or
* any standard <input> type. Any values inside the 'extra' object will be added
* to the component as an attribute.
*
* Alternatively, you can pass a callback that will be executed in ExtensionPage's
* context to include custom JSX elements.
*
* @example
*
* {
* setting: 'acme.checkbox',
* label: app.translator.trans('acme.admin.setting_label'),
* type: 'bool',
* help: app.translator.trans('acme.admin.setting_help'),
* className: 'Setting-item'
* }
*
* @example
*
* {
* setting: 'acme.select',
* label: app.translator.trans('acme.admin.setting_label'),
* type: 'select',
* options: {
* 'option1': 'Option 1 label',
* 'option2': 'Option 2 label',
* },
* default: 'option1',
* }
*
* @param setting
* @returns {JSX.Element}
*/
buildSettingComponent(entry) {
if (typeof entry === 'function') {
return entry.call(this);
}
const setting = entry.setting;
const help = entry.help;
delete entry.help;
const value = this.setting([setting])();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(entry.type)) {
return (
<div className="Form-group">
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...entry}>
{entry.label}
</Switch>
<div className="helpText">{help}</div>
</div>
);
} else if (['select', 'dropdown', 'selectdropdown'].includes(entry.type)) {
return (
<div className="Form-group">
<label>{entry.label}</label>
<div className="helpText">{help}</div>
<Select value={value || entry.default} options={entry.options} buttonClassName="Button" onchange={this.settings[setting]} {...entry} />
</div>
);
} else {
entry.className = classList(['FormControl', entry.className]);
return (
<div className="Form-group">
{entry.label ? <label>{entry.label}</label> : ''}
<div className="helpText">{help}</div>
<input type={entry.type} bidi={this.setting(setting)} {...entry} />
</div>
);
}
}
onsaved() {
this.loading = false;
app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.settings.saved_message'));
}
setting(key, fallback = '') {
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
return this.settings[key];
}
dirty() {
const dirty = {};
Object.keys(this.settings).forEach((key) => {
const value = this.settings[key]();
if (value !== app.data.settings[key]) {
dirty[key] = value;
}
});
return dirty;
}
isChanged() {
return Object.keys(this.dirty()).length;
}
saveSettings(e) {
e.preventDefault();
app.alerts.clear();
this.loading = true;
return saveSettings(this.dirty()).then(this.onsaved.bind(this));
}
}

View File

@ -1,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 (
<div className="AppearancePage">
<AdminHeader
icon="fas fa-paint-brush"
description={app.translator.trans('core.admin.appearance.description')}
className="AppearancePage-header"
>
{app.translator.trans('core.admin.appearance.title')}
</AdminHeader>
<div className="container">
<form onsubmit={this.onsubmit.bind(this)}>
<fieldset className="AppearancePage-colors">
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>
content() {
return [
<div className="Form">
<fieldset className="AppearancePage-colors">
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>
<div className="AppearancePage-colors-input">
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.primaryColor} />
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.secondaryColor} />
</div>
<div className="AppearancePage-colors-input">
{this.buildSettingComponent({
type: 'text',
setting: 'theme_primary_color',
placeholder: '#aaaaaa',
})}
{this.buildSettingComponent({
type: 'text',
setting: 'theme_secondary_color',
placeholder: '#aaaaaa',
})}
</div>
{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')
)}
</fieldset>
</form>
{this.submitButton()}
</fieldset>
</div>,
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
<UploadImageButton name="logo" />
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
<UploadImageButton name="logo" />
</fieldset>,
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
<UploadImageButton name="favicon" />
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
<UploadImageButton name="favicon" />
</fieldset>,
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomHeaderModal),
},
app.translator.trans('core.admin.appearance.edit_header_button')
)}
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomHeaderModal),
},
app.translator.trans('core.admin.appearance.edit_header_button')
)}
</fieldset>,
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomFooterModal),
},
app.translator.trans('core.admin.appearance.edit_footer_button')
)}
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomFooterModal),
},
app.translator.trans('core.admin.appearance.edit_footer_button')
)}
</fieldset>,
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomCssModal),
},
app.translator.trans('core.admin.appearance.edit_css_button')
)}
</fieldset>
</div>
</div>
);
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomCssModal),
},
app.translator.trans('core.admin.appearance.edit_css_button')
)}
</fieldset>,
];
}
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);
}
}

View File

@ -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 (
<div className="BasicsPage">
<AdminHeader icon="fas fa-pencil-alt" description={app.translator.trans('core.admin.basics.description')} className="BasicsPage-header">
{app.translator.trans('core.admin.basics.title')}
</AdminHeader>
<div className="container">
<form onsubmit={this.onsubmit.bind(this)}>
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.forum_title_heading'),
},
[<input className="FormControl" bidi={this.values.forum_title} />]
)}
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'),
},
[
<div className="helpText">{app.translator.trans('core.admin.basics.forum_description_text')}</div>,
<textarea className="FormControl" bidi={this.values.forum_description} />,
]
)}
content() {
return [
<div className="Form">
{this.buildSettingComponent({
type: 'text',
setting: 'forum_title',
label: app.translator.trans('core.admin.basics.forum_title_heading'),
})}
{this.buildSettingComponent({
type: 'text',
setting: 'forum_description',
label: app.translator.trans('core.admin.basics.forum_description_heading'),
help: app.translator.trans('core.admin.basics.forum_description_text'),
})}
{Object.keys(this.localeOptions).length > 1
? FieldSet.component(
{
label: app.translator.trans('core.admin.basics.default_language_heading'),
},
[
Select.component({
options: this.localeOptions,
value: this.values.default_locale(),
onchange: this.values.default_locale,
}),
Switch.component(
{
state: this.values.show_language_selector(),
onchange: this.values.show_language_selector,
},
app.translator.trans('core.admin.basics.show_language_selector_label')
),
]
)
: ''}
{Object.keys(this.localeOptions).length > 1
? [
this.buildSettingComponent({
type: 'select',
setting: 'default_locale',
options: this.localeOptions,
label: app.translator.trans('core.admin.basics.default_language_heading'),
}),
this.buildSettingComponent({
type: 'switch',
setting: 'show_language_selector',
label: app.translator.trans('core.admin.basics.show_language_selector_label'),
}),
]
: ''}
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.home_page_heading'),
className: 'BasicsPage-homePage',
},
[
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>,
this.homePageItems()
.toArray()
.map(({ path, label }) => (
<label className="checkbox">
<input
type="radio"
name="homePage"
value={path}
checked={this.values.default_route() === path}
onclick={withAttr('value', this.values.default_route)}
/>
{label}
</label>
)),
]
)}
<FieldSet className="BasicsPage-homePage Form-group" label={app.translator.trans('core.admin.basics.home_page_heading')}>
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>
{this.homePageItems()
.toArray()
.map(({ path, label }) => (
<label className="checkbox">
<input type="radio" name="homePage" value={path} bidi={this.setting('default_route')} />
{label}
</label>
))}
</FieldSet>
{FieldSet.component(
{
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
className: 'BasicsPage-welcomeBanner',
},
[
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>,
<div className="BasicsPage-welcomeBanner-input">
<input className="FormControl" bidi={this.values.welcome_title} />
<textarea className="FormControl" bidi={this.values.welcome_message} />
</div>,
]
)}
{Object.keys(this.displayNameOptions).length > 1 ? (
<FieldSet label={app.translator.trans('core.admin.basics.display_name_heading')}>
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>
<Select
options={this.displayNameOptions}
value={this.values.display_name_driver()}
onchange={this.values.display_name_driver}
></Select>
</FieldSet>
) : (
''
)}
{Object.keys(this.slugDriverOptions).map((model) => {
const options = this.slugDriverOptions[model];
if (Object.keys(options).length > 1) {
return (
<FieldSet label={app.translator.trans('core.admin.basics.slug_driver_heading', { model })}>
<div className="helpText">{app.translator.trans('core.admin.basics.slug_driver_text', { model })}</div>
<Select options={options} value={this.values[`slug_driver_${model}`]()} onchange={this.values[`slug_driver_${model}`]}></Select>
</FieldSet>
);
}
})}
{Button.component(
{
type: 'submit',
className: 'Button Button--primary',
loading: this.loading,
disabled: !this.changed(),
},
app.translator.trans('core.admin.basics.submit_button')
)}
</form>
<div className="Form-group BasicsPage-welcomeBanner-input">
<label>{app.translator.trans('core.admin.basics.welcome_banner_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>
<input type="text" className="FormControl" bidi={this.setting('welcome_title')} />
<textarea className="FormControl" bidi={this.setting('welcome_message')} />
</div>
</div>
);
}
changed() {
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
{Object.keys(this.displayNameOptions).length > 1
? this.buildSettingComponent({
type: 'select',
setting: 'display_name_driver',
options: this.displayNameOptions,
label: app.translator.trans('core.admin.basics.display_name_heading'),
help: app.translator.trans('core.admin.basics.display_name_text'),
})
: ''}
{Object.keys(this.slugDriverOptions).map((model) => {
const options = this.slugDriverOptions[model];
if (Object.keys(options).length > 1) {
return this.buildSettingComponent({
type: 'select',
setting: `slug_driver_${model}`,
options,
label: app.translator.trans('core.admin.basics.slug_driver_heading', { model }),
help: app.translator.trans('core.admin.basics.slug_driver_text', { model }),
});
}
})}
{this.submitButton()}
</div>,
];
}
/**
@ -210,27 +132,4 @@ export default class BasicsPage extends Page {
return items;
}
onsubmit(e) {
e.preventDefault();
if (this.loading) return;
this.loading = true;
app.alerts.dismiss(this.successAlert);
const settings = {};
this.fields.forEach((key) => (settings[key] = this.values[key]()));
saveSettings(settings)
.then(() => {
this.successAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
}

View File

@ -1,20 +1,20 @@
import Page from '../../common/components/Page';
import StatusWidget from './StatusWidget';
import ExtensionsWidget from './ExtensionsWidget';
import AdminHeader from './AdminHeader';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
import AdminPage from './AdminPage';
export default class DashboardPage extends Page {
view() {
return (
<div className="DashboardPage">
<AdminHeader icon="fas fa-chart-bar" description={app.translator.trans('core.admin.dashboard.description')} className="DashboardPage-header">
{app.translator.trans('core.admin.dashboard.title')}
</AdminHeader>
<div className="container">{this.availableWidgets().toArray()}</div>
</div>
);
export default class DashboardPage extends AdminPage {
headerInfo() {
return {
className: 'DashboardPage',
icon: 'fas fa-chart-bar',
title: app.translator.trans('core.admin.dashboard.title'),
description: app.translator.trans('core.admin.dashboard.description'),
};
}
content() {
return this.availableWidgets().toArray();
}
availableWidgets() {

View File

@ -1,27 +1,22 @@
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
import LinkButton from '../../common/components/LinkButton';
import Page from '../../common/components/Page';
import Select from '../../common/components/Select';
import Switch from '../../common/components/Switch';
import icon from '../../common/helpers/icon';
import punctuateSeries from '../../common/helpers/punctuateSeries';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
import LoadingModal from './LoadingModal';
import ExtensionPermissionGrid from './ExtensionPermissionGrid';
import saveSettings from '../utils/saveSettings';
import isExtensionEnabled from '../utils/isExtensionEnabled';
import AdminPage from './AdminPage';
export default class ExtensionPage extends Page {
export default class ExtensionPage extends AdminPage {
oninit(vnode) {
super.oninit(vnode);
this.loading = false;
this.extension = app.data.extensions[this.attrs.id];
this.changingState = false;
this.settings = {};
this.infoFields = {
discuss: 'fas fa-comment-alt',
@ -35,12 +30,6 @@ export default class ExtensionPage extends Page {
if (!this.extension) {
return m.route.set(app.route('dashboard'));
}
// Backwards compatibility layer will be removed in
// Beta 16
if (app.extensionSettings[this.extension.id]) {
app.extensionData[this.extension.id] = app.extensionSettings[this.extension.id];
}
}
className() {
@ -132,11 +121,7 @@ export default class ExtensionPage extends Page {
return (
<div className="ExtensionPage-settings">
<div className="container">
{typeof app.extensionData[this.extension.id] === 'function' ? (
<Button onclick={app.extensionData[this.extension.id].bind(this)} className="Button Button--primary">
{app.translator.trans('core.admin.extension.open_modal')}
</Button>
) : settings ? (
{settings ? (
<div className="Form">
{settings.map(this.buildSettingComponent.bind(this))}
<div className="Form-group">{this.submitButton()}</div>
@ -212,78 +197,6 @@ export default class ExtensionPage extends Page {
return items;
}
submitButton() {
return (
<Button onclick={this.saveSettings.bind(this)} className="Button Button--primary" loading={this.loading} disabled={!this.isChanged()}>
{app.translator.trans('core.admin.settings.submit_button')}
</Button>
);
}
/**
* getSetting takes a settings object and turns it into a component.
* Depending on the type of input, you can set the type to 'bool', 'select', or
* any standard <input> type.
*
* 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'
* }
*
* @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 value = this.setting([setting])();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(entry.type)) {
return (
<div className="Form-group">
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]}>
{entry.label}
</Switch>
</div>
);
} else if (['select', 'dropdown', 'selectdropdown'].includes(entry.type)) {
return (
<div className="Form-group">
<label>{entry.label}</label>
<Select value={value || entry.default} options={entry.options} buttonClassName="Button" onchange={this.settings[setting]} />
</div>
);
} else {
return (
<div className="Form-group">
<label>{entry.label}</label>
<input type={entry.type} className="FormControl" bidi={this.setting(setting)} />
</div>
);
}
}
toggle() {
const enabled = this.isEnabled();
@ -304,46 +217,6 @@ export default class ExtensionPage extends Page {
app.modal.show(LoadingModal);
}
dirty() {
const dirty = {};
Object.keys(this.settings).forEach((key) => {
const value = this.settings[key]();
if (value !== app.data.settings[key]) {
dirty[key] = value;
}
});
return dirty;
}
isChanged() {
return Object.keys(this.dirty()).length;
}
saveSettings(e) {
e.preventDefault();
app.alerts.clear();
this.loading = true;
saveSettings(this.dirty()).then(this.onsaved.bind(this));
}
onsaved() {
this.loading = false;
app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.extension.saved_message'));
}
setting(key, fallback = '') {
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
return this.settings[key];
}
isEnabled() {
return isExtensionEnabled(this.extension.id);
}

View File

@ -1,4 +1,5 @@
import PermissionGrid from './PermissionGrid';
import Button from '../../common/components/Button';
import ItemList from '../../common/utils/ItemList';
export default class ExtensionPermissionGrid extends PermissionGrid {
@ -36,4 +37,17 @@ export default class ExtensionPermissionGrid extends PermissionGrid {
moderateItems() {
return app.extensionData.getExtensionPermissions(this.extensionId, 'moderate') || new ItemList();
}
scopeControlItems() {
const items = new ItemList();
items.add(
'configureScopes',
<Button className="Button Button--text" onclick={() => m.route.set(app.route('permissions'))}>
{app.translator.trans('core.admin.extension.configure_scopes')}
</Button>
);
return items;
}
}

View File

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

View File

@ -1,34 +1,31 @@
import Page from '../../common/components/Page';
import FieldSet from '../../common/components/FieldSet';
import Button from '../../common/components/Button';
import Alert from '../../common/components/Alert';
import Select from '../../common/components/Select';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import saveSettings from '../utils/saveSettings';
import Stream from '../../common/utils/Stream';
import icon from '../../common/helpers/icon';
import AdminHeader from './AdminHeader';
import AdminPage from './AdminPage';
export default class MailPage extends Page {
export default class MailPage extends AdminPage {
oninit(vnode) {
super.oninit(vnode);
this.saving = false;
this.sendingTest = false;
this.refresh();
}
headerInfo() {
return {
className: 'MailPage',
icon: 'fas fa-envelope',
title: app.translator.trans('core.admin.email.title'),
description: app.translator.trans('core.admin.email.description'),
};
}
refresh() {
this.loading = true;
this.driverFields = {};
this.fields = ['mail_driver', 'mail_from'];
this.values = {};
this.status = { sending: false, errors: {} };
const settings = app.data.settings;
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
app
.request({
method: 'GET',
@ -39,150 +36,78 @@ export default class MailPage extends Page {
this.status.sending = response['data']['attributes']['sending'];
this.status.errors = response['data']['attributes']['errors'];
for (const driver in this.driverFields) {
for (const field in this.driverFields[driver]) {
this.fields.push(field);
this.values[field] = Stream(settings[field]);
}
}
this.loading = false;
m.redraw();
});
}
view() {
if (this.loading || this.saving) {
return (
<div className="MailPage">
<div className="container">
<LoadingIndicator />
</div>
</div>
);
content() {
if (this.loading) {
return <LoadingIndicator />;
}
const fields = this.driverFields[this.values.mail_driver()];
const fields = this.driverFields[this.setting('mail_driver')()];
const fieldKeys = Object.keys(fields);
return (
<div className="MailPage">
<AdminHeader icon="fas fa-envelope" description={app.translator.trans('core.admin.email.description')} className="MailPage-header">
{app.translator.trans('core.admin.email.title')}
</AdminHeader>
<div className="container">
<form onsubmit={this.onsubmit.bind(this)}>
{FieldSet.component(
{
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
},
[
<div className="MailPage-MailSettings-input">
<label>
{app.translator.trans('core.admin.email.from_label')}
<input className="FormControl" bidi={this.values.mail_from} />
</label>
</div>,
]
)}
<div className="Form">
{this.buildSettingComponent({
type: 'text',
setting: 'mail_from',
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
})}
{this.buildSettingComponent({
type: 'select',
setting: 'mail_driver',
options: Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.email.driver_heading'),
className: 'MailPage-MailSettings',
})}
{this.status.sending ||
Alert.component(
{
dismissible: false,
},
app.translator.trans('core.admin.email.not_sending_message')
)}
{FieldSet.component(
{
label: app.translator.trans('core.admin.email.driver_heading'),
className: 'MailPage-MailSettings',
},
[
<div className="MailPage-MailSettings-input">
<label>
{app.translator.trans('core.admin.email.driver_label')}
<Select
value={this.values.mail_driver()}
options={Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {})}
onchange={this.values.mail_driver}
/>
</label>
</div>,
]
)}
{fieldKeys.length > 0 && (
<FieldSet label={app.translator.trans(`core.admin.email.${this.setting('mail_driver')()}_heading`)} className="MailPage-MailSettings">
<div className="MailPage-MailSettings-input">
{fieldKeys.map((field) => {
const fieldInfo = fields[field];
{this.status.sending ||
Alert.component(
{
dismissible: false,
},
app.translator.trans('core.admin.email.not_sending_message')
)}
return [
this.buildSettingComponent({
type: typeof this.setting(field)() === 'string' ? 'text' : 'select',
label: app.translator.trans(`core.admin.email.${field}_label`),
setting: field,
options: fieldInfo,
}),
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
];
})}
</div>
</FieldSet>
)}
{this.submitButton()}
{fieldKeys.length > 0 &&
FieldSet.component(
{
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
className: 'MailPage-MailSettings',
},
[
<div className="MailPage-MailSettings-input">
{fieldKeys.map((field) => [
<label>
{app.translator.trans(`core.admin.email.${field}_label`)}
{this.renderField(field)}
</label>,
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
])}
</div>,
]
)}
<FieldSet>
{Button.component(
{
type: 'submit',
className: 'Button Button--primary',
disabled: !this.changed(),
},
app.translator.trans('core.admin.email.submit_button')
)}
</FieldSet>
{FieldSet.component(
{
label: app.translator.trans('core.admin.email.send_test_mail_heading'),
className: 'MailPage-MailSettings',
},
[
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user.email() })}</div>,
Button.component(
{
className: 'Button Button--primary',
disabled: this.sendingTest || this.changed(),
onclick: () => this.sendTestEmail(),
},
app.translator.trans('core.admin.email.send_test_mail_button')
),
]
)}
</form>
</div>
<FieldSet label={app.translator.trans('core.admin.email.send_test_mail_heading')} className="MailPage-MailSettings">
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user.email() })}</div>
{Button.component(
{
className: 'Button Button--primary',
disabled: this.sendingTest || this.isChanged(),
onclick: () => this.sendTestEmail(),
},
app.translator.trans('core.admin.email.send_test_mail_button')
)}
</FieldSet>
</div>
);
}
renderField(name) {
const driver = this.values.mail_driver();
const field = this.driverFields[driver][name];
const prop = this.values[name];
if (typeof field === 'string') {
return <input className="FormControl" bidi={prop} />;
} else {
return <Select value={prop()} options={field} onchange={prop} />;
}
}
changed() {
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
}
sendTestEmail() {
if (this.saving || this.sendingTest) return;
@ -205,26 +130,7 @@ export default class MailPage extends Page {
});
}
onsubmit(e) {
e.preventDefault();
if (this.saving || this.sendingTest) return;
this.saving = true;
app.alerts.dismiss(this.successAlert);
const settings = {};
this.fields.forEach((key) => (settings[key] = this.values[key]()));
saveSettings(settings)
.then(() => {
this.successAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
})
.catch(() => {})
.then(() => {
this.saving = false;
this.refresh();
});
saveSettings(e) {
super.saveSettings(e).then(this.refresh());
}
}

View File

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

View File

@ -206,7 +206,7 @@
.ExtensionNavButton {
.Button-label {
display: inline-block;
max-width: calc(100% - 18px);
max-width: ~"calc(100% - 18px)";
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;

View File

@ -1,4 +1,5 @@
.AppearancePage {
padding-bottom: 30px;
@media @desktop-up {
.container {
@ -14,8 +15,16 @@
.AppearancePage-colors-input {
overflow: hidden;
.Form-group {
display: inline-block;
}
.Form-group:last-child {
margin-bottom: 24px !important;
margin-left: 10px;
}
input {
width: 49%;
float: left;
&:first-child {
@ -23,7 +32,7 @@
}
}
}
.AppearancePage-colors-input,
.AppearancePage-colors .Checkbox {
margin-bottom: 15px;
}

View File

@ -1,4 +1,5 @@
.BasicsPage {
padding-bottom: 30px;
@media @desktop-up {
.container {
@ -7,26 +8,27 @@
}
}
fieldset {
.Form-group {
margin-bottom: 20px;
}
> ul {
list-style: none;
margin: 0;
padding: 0;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
}
.BasicsPage-welcomeBanner-input {
:first-child {
input {
margin-bottom: 1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
font-weight: bold;
}
:last-child {
textarea {
border-top-right-radius: 0;
border-top-left-radius: 0;
margin-bottom: 10px;
}
}

View File

@ -2,6 +2,7 @@
background: @body-bg;
color: @control-color;
min-height: 100vh;
padding-bottom: 30px;
}
.Widget {

View File

@ -1,5 +1,4 @@
.ExtensionPage {
min-height: 110vh;
.ExtensionPage-header {
.ExtensionTitle {
@ -142,11 +141,13 @@
.ExtensionPage-permissions {
@media @phone {
> .container {
overflow-x: scroll;
padding-bottom: 20px;
}
.PermissionGrid-removeScope {
display: none;
}
> .container {
overflow-x: auto;
padding-bottom: 25vh;
}
.ExtensionPage-permissions-header {

View File

@ -7,18 +7,6 @@
padding: 0;
background-color: @body-bg;
.ExtensionList-Category {
background: @control-bg;
padding: 20px 0 20px 20px;
margin-bottom: 20px;
border-radius: @border-radius;
.ExtensionList-Label {
margin-top: 0;
color: @muted-color;
}
}
.ExtensionGroup {
margin-bottom: 20px;
@ -46,6 +34,18 @@
}
}
.ExtensionList-Category {
background: @control-bg;
padding: 20px 0 20px 20px;
margin-bottom: 20px;
border-radius: @border-radius;
}
.ExtensionList-Label {
margin-top: 0;
color: @muted-color;
}
.ExtensionListItem.disabled {
.ExtensionListItem-title {
opacity: 0.5;
@ -64,18 +64,18 @@
transform: scale(1.05);
}
.ExtensionListItem-title {
display: block;
text-align: center;
margin-top: 5px;
color: @text-color;
}
a:hover {
text-decoration: none;
}
}
.ExtensionListItem-title {
display: block;
text-align: center;
margin-top: 5px;
color: @text-color;
}
.ExtensionIcon {
width: 90px;
height: 90px;

View File

@ -1,4 +1,5 @@
.MailPage {
padding-bottom: 30px;
@media @desktop-up {
.container {
@ -7,11 +8,11 @@
}
}
fieldset, .Alert {
button, .Alert {
margin-bottom: 20px;
}
fieldset > ul {
ul {
list-style: none;
margin: 0;
padding: 0;

View File

@ -1,15 +1,9 @@
.PermissionsPage-groups {
background: @control-bg;
border-radius: @border-radius;
max-width: calc(~'100% - 60px');
display: block;
margin-left: 30px;
overflow-x: auto;
padding: 10px 0 8px;
> .container {
padding: 0 10px;
}
padding: 10px;
}
.Group {
width: 90px;
@ -46,7 +40,6 @@
.PermissionGrid {
white-space: nowrap;
padding-left: 0 12px;
td, th {
padding: 5px;