mirror of
https://github.com/flarum/framework.git
synced 2024-11-25 17:57:04 +08:00
Ultra-basic admin site (dashboard page only)
This commit is contained in:
parent
a7937edac7
commit
990cdbc571
62
js/src/admin/Admin.ts
Normal file
62
js/src/admin/Admin.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import HeaderPrimary from './components/HeaderPrimary';
|
||||
import HeaderSecondary from './components/HeaderSecondary';
|
||||
import routes from './routes';
|
||||
import Application from '../common/Application';
|
||||
import Navigation from '../common/components/Navigation';
|
||||
import AdminNav from './components/AdminNav';
|
||||
|
||||
export default class Admin extends Application {
|
||||
extensionSettings = {};
|
||||
|
||||
history = {
|
||||
canGoBack: () => true,
|
||||
getPrevious: () => {},
|
||||
backUrl: () => this.forum.attribute('baseUrl'),
|
||||
back: function () {
|
||||
window.location = this.backUrl();
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
routes(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
mount() {
|
||||
m.mount(document.getElementById('app-navigation'), new Navigation({ className: 'App-backControl', drawer: true }));
|
||||
m.mount(document.getElementById('header-navigation'), new Navigation());
|
||||
m.mount(document.getElementById('header-primary'), new HeaderPrimary());
|
||||
m.mount(document.getElementById('header-secondary'), new HeaderSecondary());
|
||||
m.mount(document.getElementById('admin-navigation'), new AdminNav());
|
||||
|
||||
super.mount();
|
||||
|
||||
// If an extension has just been enabled, then we will run its settings
|
||||
// callback.
|
||||
const enabled = localStorage.getItem('enabledExtension');
|
||||
if (enabled && this.extensionSettings[enabled]) {
|
||||
this.extensionSettings[enabled]();
|
||||
localStorage.removeItem('enabledExtension');
|
||||
}
|
||||
}
|
||||
|
||||
getRequiredPermissions(permission) {
|
||||
const required: string[] = [];
|
||||
|
||||
if (permission === 'startDiscussion' || permission.indexOf('discussion.') === 0) {
|
||||
required.push('viewDiscussions');
|
||||
}
|
||||
if (permission === 'discussion.delete') {
|
||||
required.push('discussion.hide');
|
||||
}
|
||||
if (permission === 'discussion.deletePosts') {
|
||||
required.push('discussion.hidePosts');
|
||||
}
|
||||
|
||||
return required;
|
||||
}
|
||||
}
|
8
js/src/admin/app.ts
Normal file
8
js/src/admin/app.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Admin from './Admin';
|
||||
|
||||
const app = new Admin();
|
||||
|
||||
// @ts-ignore
|
||||
window.app = app;
|
||||
|
||||
export default app;
|
7
js/src/admin/compat.ts
Normal file
7
js/src/admin/compat.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import compat from '../common/compat';
|
||||
|
||||
import Admin from './Admin';
|
||||
|
||||
export default Object.assign(compat, {
|
||||
Admin: Admin,
|
||||
}) as any;
|
24
js/src/admin/components/AdminLinkButton.tsx
Normal file
24
js/src/admin/components/AdminLinkButton.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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, { LinkButtonProps } from '../../common/components/LinkButton';
|
||||
|
||||
interface AdminLinkButtonProps extends LinkButtonProps {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default class AdminLinkButton extends LinkButton<AdminLinkButtonProps> {
|
||||
getButtonContent() {
|
||||
const content = super.getButtonContent(this.props.icon, this.props.loading, this.props.children);
|
||||
|
||||
content.push(<div className="AdminLinkButton-description">{this.props.description}</div>);
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
79
js/src/admin/components/AdminNav.tsx
Normal file
79
js/src/admin/components/AdminNav.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import Component from '../../common/Component';
|
||||
import AdminLinkButton from './AdminLinkButton';
|
||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
|
||||
export default class AdminNav<T> extends Component<T> {
|
||||
view() {
|
||||
return (
|
||||
<SelectDropdown className="AdminNav App-titleControl" buttonClassName="Button">
|
||||
{this.items().toArray()}
|
||||
</SelectDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list of links to show in the admin navigation.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add(
|
||||
'dashboard',
|
||||
AdminLinkButton.component({
|
||||
href: app.route('dashboard'),
|
||||
icon: 'far fa-chart-bar',
|
||||
children: app.translator.trans('core.admin.nav.dashboard_button'),
|
||||
description: app.translator.trans('core.admin.nav.dashboard_text'),
|
||||
})
|
||||
);
|
||||
|
||||
// items.add('basics', AdminLinkButton.component({
|
||||
// href: app.route('basics'),
|
||||
// icon: 'fas fa-pencil-alt',
|
||||
// children: app.translator.trans('core.admin.nav.basics_button'),
|
||||
// description: app.translator.trans('core.admin.nav.basics_text')
|
||||
// }));
|
||||
|
||||
// items.add('mail', AdminLinkButton.component({
|
||||
// href: app.route('mail'),
|
||||
// icon: 'fas fa-envelope',
|
||||
// children: app.translator.trans('core.admin.nav.email_button'),
|
||||
// description: app.translator.trans('core.admin.nav.email_text')
|
||||
// }));
|
||||
|
||||
// items.add('permissions', AdminLinkButton.component({
|
||||
// href: app.route('permissions'),
|
||||
// icon: 'fas fa-key',
|
||||
// children: app.translator.trans('core.admin.nav.permissions_button'),
|
||||
// description: app.translator.trans('core.admin.nav.permissions_text')
|
||||
// }));
|
||||
|
||||
// items.add('appearance', AdminLinkButton.component({
|
||||
// href: app.route('appearance'),
|
||||
// icon: 'fas fa-paint-brush',
|
||||
// children: app.translator.trans('core.admin.nav.appearance_button'),
|
||||
// description: app.translator.trans('core.admin.nav.appearance_text')
|
||||
// }));
|
||||
|
||||
// items.add('extensions', AdminLinkButton.component({
|
||||
// href: app.route('extensions'),
|
||||
// icon: 'fas fa-puzzle-piece',
|
||||
// children: app.translator.trans('core.admin.nav.extensions_button'),
|
||||
// description: app.translator.trans('core.admin.nav.extensions_text')
|
||||
// }));
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
16
js/src/admin/components/DashboardPage.tsx
Normal file
16
js/src/admin/components/DashboardPage.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Page from './Page';
|
||||
import StatusWidget from './StatusWidget';
|
||||
|
||||
export default class DashboardPage extends Page {
|
||||
view() {
|
||||
return (
|
||||
<div className="DashboardPage">
|
||||
<div className="container">{this.availableWidgets()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
availableWidgets() {
|
||||
return [<StatusWidget />];
|
||||
}
|
||||
}
|
34
js/src/admin/components/DashboardWidget.tsx
Normal file
34
js/src/admin/components/DashboardWidget.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import Component from '../../common/Component';
|
||||
|
||||
export default class Widget extends Component {
|
||||
view() {
|
||||
return <div className={'Widget ' + this.className()}>{this.content()}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name to apply to the widget.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
className() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of the widget.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
content() {
|
||||
return [];
|
||||
}
|
||||
}
|
29
js/src/admin/components/HeaderPrimary.tsx
Normal file
29
js/src/admin/components/HeaderPrimary.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Component from '../../common/Component';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `HeaderPrimary` component displays primary header controls. On the
|
||||
* default skin, these are shown just to the right of the forum title.
|
||||
*/
|
||||
export default class HeaderPrimary extends Component {
|
||||
view() {
|
||||
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
// Since this component is 'above' the content of the page (that is, it is a
|
||||
// part of the global UI that persists between routes), we will flag the DOM
|
||||
// to be retained across route changes.
|
||||
context.retain = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
return new ItemList();
|
||||
}
|
||||
}
|
33
js/src/admin/components/HeaderSecondary.tsx
Normal file
33
js/src/admin/components/HeaderSecondary.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import Component from '../../common/Component';
|
||||
import SessionDropdown from './SessionDropdown';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `HeaderSecondary` component displays secondary header controls.
|
||||
*/
|
||||
export default class HeaderSecondary extends Component {
|
||||
view() {
|
||||
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
// Since this component is 'above' the content of the page (that is, it is a
|
||||
// part of the global UI that persists between routes), we will flag the DOM
|
||||
// to be retained across route changes.
|
||||
context.retain = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('session', SessionDropdown.component());
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
19
js/src/admin/components/LoadingModal.tsx
Normal file
19
js/src/admin/components/LoadingModal.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Modal from '../../common/components/Modal';
|
||||
|
||||
export default class LoadingModal extends Modal {
|
||||
isDismissible() {
|
||||
return false;
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'LoadingModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.translator.trans('core.admin.loading.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return '';
|
||||
}
|
||||
}
|
32
js/src/admin/components/Page.tsx
Normal file
32
js/src/admin/components/Page.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Component from '../../common/Component';
|
||||
|
||||
/**
|
||||
* The `Page` component
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Page extends Component {
|
||||
init() {
|
||||
app.previous = app.current;
|
||||
app.current = this;
|
||||
|
||||
app.modal.close();
|
||||
|
||||
/**
|
||||
* A class name to apply to the body while the route is active.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.bodyClass = '';
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
if (isInitialized) return;
|
||||
|
||||
if (this.bodyClass) {
|
||||
$('#app').addClass(this.bodyClass);
|
||||
|
||||
context.onunload = () => $('#app').removeClass(this.bodyClass);
|
||||
}
|
||||
}
|
||||
}
|
52
js/src/admin/components/SessionDropdown.tsx
Normal file
52
js/src/admin/components/SessionDropdown.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import Button from '../../common/components/Button';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
|
||||
/**
|
||||
* The `SessionDropdown` component shows a button with the current user's
|
||||
* avatar/name, with a dropdown of session controls.
|
||||
*/
|
||||
export default class SessionDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = 'SessionDropdown';
|
||||
props.buttonClassName = 'Button Button--user Button--flat';
|
||||
props.menuClassName = 'Dropdown-menu--right';
|
||||
}
|
||||
|
||||
view() {
|
||||
this.props.children = this.items().toArray();
|
||||
|
||||
return super.view();
|
||||
}
|
||||
|
||||
getButtonContent() {
|
||||
const user = app.session.user;
|
||||
|
||||
return [avatar(user), ' ', <span className="Button-label">{username(user)}</span>];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the contents of the dropdown menu.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add(
|
||||
'logOut',
|
||||
Button.component({
|
||||
icon: 'fas fa-sign-out-alt',
|
||||
children: app.translator.trans('core.admin.header.log_out_button'),
|
||||
onclick: app.session.logout.bind(app.session),
|
||||
}),
|
||||
-100
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
25
js/src/admin/components/SettingDropdown.tsx
Normal file
25
js/src/admin/components/SettingDropdown.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
import Button from '../../common/components/Button';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
|
||||
export default class SettingDropdown extends SelectDropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = 'SettingDropdown';
|
||||
props.buttonClassName = 'Button Button--text';
|
||||
props.caretIcon = 'fas fa-caret-down';
|
||||
props.defaultLabel = 'Custom';
|
||||
|
||||
props.children = props.options.map(({ value, label }) => {
|
||||
const active = app.data.settings[props.key] === value;
|
||||
|
||||
return Button.component({
|
||||
children: label,
|
||||
icon: active ? 'fas fa-check' : true,
|
||||
onclick: saveSettings.bind(this, { [props.key]: value }),
|
||||
active,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
70
js/src/admin/components/SettingsModal.tsx
Normal file
70
js/src/admin/components/SettingsModal.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import Modal from '../../common/components/Modal';
|
||||
import Button from '../../common/components/Button';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
|
||||
export default class SettingsModal extends Modal {
|
||||
init() {
|
||||
this.settings = {};
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
form() {
|
||||
return '';
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form">
|
||||
{this.form()}
|
||||
|
||||
<div className="Form-group">{this.submitButton()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
submitButton() {
|
||||
return (
|
||||
<Button type="submit" className="Button Button--primary" loading={this.loading} disabled={!this.changed()}>
|
||||
{app.translator.trans('core.admin.settings.submit_button')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
setting(key, fallback = '') {
|
||||
this.settings[key] = this.settings[key] || m.prop(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;
|
||||
}
|
||||
|
||||
changed() {
|
||||
return Object.keys(this.dirty()).length;
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
saveSettings(this.dirty()).then(this.onsaved.bind(this), this.loaded.bind(this));
|
||||
}
|
||||
|
||||
onsaved() {
|
||||
this.hide();
|
||||
}
|
||||
}
|
56
js/src/admin/components/StatusWidget.tsx
Normal file
56
js/src/admin/components/StatusWidget.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 DashboardWidget from './DashboardWidget';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import Button from '../../common/components/Button';
|
||||
import LoadingModal from './LoadingModal';
|
||||
|
||||
export default class StatusWidget extends DashboardWidget {
|
||||
className() {
|
||||
return 'StatusWidget';
|
||||
}
|
||||
|
||||
content() {
|
||||
return <ul>{listItems(this.items().toArray())}</ul>;
|
||||
}
|
||||
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add(
|
||||
'tools',
|
||||
<Dropdown
|
||||
label={app.translator.trans('core.admin.dashboard.tools_button')}
|
||||
icon="fas fa-cog"
|
||||
buttonClassName="Button"
|
||||
menuClassName="Dropdown-menu--right"
|
||||
>
|
||||
<Button onclick={this.handleClearCache.bind(this)}>{app.translator.trans('core.admin.dashboard.clear_cache_button')}</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
items.add('version-flarum', [<strong>Flarum</strong>, <br />, app.forum.attribute('version')]);
|
||||
items.add('version-php', [<strong>PHP</strong>, <br />, app.data.phpVersion]);
|
||||
items.add('version-mysql', [<strong>MySQL</strong>, <br />, app.data.mysqlVersion]);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
handleClearCache(e) {
|
||||
app.modal.show(new LoadingModal());
|
||||
|
||||
app.request({
|
||||
method: 'DELETE',
|
||||
url: app.forum.attribute('apiUrl') + '/cache',
|
||||
}).then(() => window.location.reload());
|
||||
}
|
||||
}
|
97
js/src/admin/components/UploadImageButton.tsx
Normal file
97
js/src/admin/components/UploadImageButton.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import Button from '../../common/components/Button';
|
||||
|
||||
export default class UploadImageButton extends Button {
|
||||
init() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
view() {
|
||||
this.props.loading = this.loading;
|
||||
this.props.className = (this.props.className || '') + ' Button';
|
||||
|
||||
if (app.data.settings[this.props.name + '_path']) {
|
||||
this.props.onclick = this.remove.bind(this);
|
||||
this.props.children = app.translator.trans('core.admin.upload_image.remove_button');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<img src={app.forum.attribute(this.props.name + 'Url')} alt="" />
|
||||
</p>
|
||||
<p>{super.view()}</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
this.props.onclick = this.upload.bind(this);
|
||||
this.props.children = app.translator.trans('core.admin.upload_image.upload_button');
|
||||
}
|
||||
|
||||
return super.view();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to upload an image.
|
||||
*/
|
||||
upload() {
|
||||
if (this.loading) return;
|
||||
|
||||
const $input = $('<input type="file">');
|
||||
|
||||
$input
|
||||
.appendTo('body')
|
||||
.hide()
|
||||
.click()
|
||||
.on('change', (e) => {
|
||||
const data = new FormData();
|
||||
data.append(this.props.name, $(e.target)[0].files[0]);
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: this.resourceUrl(),
|
||||
serialize: (raw) => raw,
|
||||
data,
|
||||
}).then(this.success.bind(this), this.failure.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the logo.
|
||||
*/
|
||||
remove() {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'DELETE',
|
||||
url: this.resourceUrl(),
|
||||
}).then(this.success.bind(this), this.failure.bind(this));
|
||||
}
|
||||
|
||||
resourceUrl() {
|
||||
return app.forum.attribute('apiUrl') + '/' + this.props.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* After a successful upload/removal, reload the page.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
success(response) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* If upload/removal fails, stop loading.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
failure(response) {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
}
|
34
js/src/admin/components/Widget.tsx
Normal file
34
js/src/admin/components/Widget.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import Component from '../../common/Component';
|
||||
|
||||
export default class DashboardWidget extends Component {
|
||||
view() {
|
||||
return <div className={'DashboardWidget ' + this.className()}>{this.content()}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name to apply to the widget.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
className() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of the widget.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
content() {
|
||||
return [];
|
||||
}
|
||||
}
|
10
js/src/admin/index.ts
Normal file
10
js/src/admin/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import app from './app';
|
||||
|
||||
export { app };
|
||||
|
||||
// Export compat API
|
||||
import compat from './compat';
|
||||
|
||||
compat.app = app;
|
||||
|
||||
export { compat };
|
7
js/src/admin/routes.ts
Normal file
7
js/src/admin/routes.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import DashboardPage from './components/DashboardPage';
|
||||
|
||||
export default (app) => {
|
||||
app.routes = {
|
||||
dashboard: { path: '/', component: DashboardPage },
|
||||
};
|
||||
};
|
16
js/src/admin/utils/saveSettings.ts
Normal file
16
js/src/admin/utils/saveSettings.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export default function saveSettings(settings) {
|
||||
const oldSettings = JSON.parse(JSON.stringify(app.data.settings));
|
||||
|
||||
Object.assign(app.data.settings, settings);
|
||||
|
||||
return app
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/settings',
|
||||
data: settings,
|
||||
})
|
||||
.catch((error) => {
|
||||
app.data.settings = oldSettings;
|
||||
throw error;
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user