mirror of
https://github.com/flarum/core.git
synced 2025-08-04 15:37:51 +02:00
Ultra-basic admin site (dashboard page only)
This commit is contained in:
committed by
David Sevilla Martín
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;
|
||||||
|
});
|
||||||
|
}
|
Reference in New Issue
Block a user