mirror of
https://github.com/flarum/core.git
synced 2025-07-23 09:41:26 +02:00
AdminUX Overhaul (#2409)
- Extensions now have their own pages - The API for extensions to register permissions and settings has been overhauled via the `flarum/admin/utils/ExtensionData` util - An extension grid has been added as a widget to the Dashboard page
This commit is contained in:
@@ -1,13 +1,29 @@
|
|||||||
import HeaderPrimary from './components/HeaderPrimary';
|
import HeaderPrimary from './components/HeaderPrimary';
|
||||||
import HeaderSecondary from './components/HeaderSecondary';
|
import HeaderSecondary from './components/HeaderSecondary';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
|
import ExtensionPage from './components/ExtensionPage';
|
||||||
import Application from '../common/Application';
|
import Application from '../common/Application';
|
||||||
import Navigation from '../common/components/Navigation';
|
import Navigation from '../common/components/Navigation';
|
||||||
import AdminNav from './components/AdminNav';
|
import AdminNav from './components/AdminNav';
|
||||||
|
import ExtensionData from './utils/ExtensionData';
|
||||||
|
|
||||||
export default class AdminApplication extends Application {
|
export default class AdminApplication extends Application {
|
||||||
|
// Deprecated as of beta 15
|
||||||
extensionSettings = {};
|
extensionSettings = {};
|
||||||
|
|
||||||
|
extensionData = new ExtensionData();
|
||||||
|
|
||||||
|
extensionCategories = {
|
||||||
|
discussion: 70,
|
||||||
|
moderation: 60,
|
||||||
|
feature: 50,
|
||||||
|
formatting: 40,
|
||||||
|
theme: 30,
|
||||||
|
authentication: 20,
|
||||||
|
language: 10,
|
||||||
|
other: 0,
|
||||||
|
};
|
||||||
|
|
||||||
history = {
|
history = {
|
||||||
canGoBack: () => true,
|
canGoBack: () => true,
|
||||||
getPrevious: () => {},
|
getPrevious: () => {},
|
||||||
@@ -34,7 +50,13 @@ export default class AdminApplication extends Application {
|
|||||||
m.route.prefix = '#';
|
m.route.prefix = '#';
|
||||||
super.mount();
|
super.mount();
|
||||||
|
|
||||||
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
|
m.mount(document.getElementById('app-navigation'), {
|
||||||
|
view: () =>
|
||||||
|
Navigation.component({
|
||||||
|
className: 'App-backControl',
|
||||||
|
drawer: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
m.mount(document.getElementById('header-navigation'), Navigation);
|
m.mount(document.getElementById('header-navigation'), Navigation);
|
||||||
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
||||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
||||||
@@ -43,7 +65,7 @@ export default class AdminApplication extends Application {
|
|||||||
// If an extension has just been enabled, then we will run its settings
|
// If an extension has just been enabled, then we will run its settings
|
||||||
// callback.
|
// callback.
|
||||||
const enabled = localStorage.getItem('enabledExtension');
|
const enabled = localStorage.getItem('enabledExtension');
|
||||||
if (enabled && this.extensionSettings[enabled]) {
|
if (enabled && this.extensionSettings[enabled] && typeof this.extensionSettings[enabled] === 'function') {
|
||||||
this.extensionSettings[enabled]();
|
this.extensionSettings[enabled]();
|
||||||
localStorage.removeItem('enabledExtension');
|
localStorage.removeItem('enabledExtension');
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,21 @@
|
|||||||
import compat from '../common/compat';
|
import compat from '../common/compat';
|
||||||
|
|
||||||
import saveSettings from './utils/saveSettings';
|
import saveSettings from './utils/saveSettings';
|
||||||
|
import ExtensionData from './utils/ExtensionData';
|
||||||
|
import isExtensionEnabled from './utils/isExtensionEnabled';
|
||||||
|
import getCategorizedExtensions from './utils/getCategorizedExtensions';
|
||||||
import SettingDropdown from './components/SettingDropdown';
|
import SettingDropdown from './components/SettingDropdown';
|
||||||
import EditCustomFooterModal from './components/EditCustomFooterModal';
|
import EditCustomFooterModal from './components/EditCustomFooterModal';
|
||||||
import SessionDropdown from './components/SessionDropdown';
|
import SessionDropdown from './components/SessionDropdown';
|
||||||
import HeaderPrimary from './components/HeaderPrimary';
|
import HeaderPrimary from './components/HeaderPrimary';
|
||||||
import AppearancePage from './components/AppearancePage';
|
import AppearancePage from './components/AppearancePage';
|
||||||
import StatusWidget from './components/StatusWidget';
|
import StatusWidget from './components/StatusWidget';
|
||||||
|
import ExtensionsWidget from './components/ExtensionsWidget';
|
||||||
import HeaderSecondary from './components/HeaderSecondary';
|
import HeaderSecondary from './components/HeaderSecondary';
|
||||||
import SettingsModal from './components/SettingsModal';
|
import SettingsModal from './components/SettingsModal';
|
||||||
import DashboardWidget from './components/DashboardWidget';
|
import DashboardWidget from './components/DashboardWidget';
|
||||||
import AddExtensionModal from './components/AddExtensionModal';
|
import ExtensionPage from './components/ExtensionPage';
|
||||||
import ExtensionsPage from './components/ExtensionsPage';
|
import ExtensionLinkButton from './components/ExtensionLinkButton';
|
||||||
import AdminLinkButton from './components/AdminLinkButton';
|
import AdminLinkButton from './components/AdminLinkButton';
|
||||||
import PermissionGrid from './components/PermissionGrid';
|
import PermissionGrid from './components/PermissionGrid';
|
||||||
import MailPage from './components/MailPage';
|
import MailPage from './components/MailPage';
|
||||||
@@ -23,6 +27,7 @@ import EditCustomHeaderModal from './components/EditCustomHeaderModal';
|
|||||||
import PermissionsPage from './components/PermissionsPage';
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
import PermissionDropdown from './components/PermissionDropdown';
|
import PermissionDropdown from './components/PermissionDropdown';
|
||||||
import AdminNav from './components/AdminNav';
|
import AdminNav from './components/AdminNav';
|
||||||
|
import AdminHeader from './components/AdminHeader';
|
||||||
import EditCustomCssModal from './components/EditCustomCssModal';
|
import EditCustomCssModal from './components/EditCustomCssModal';
|
||||||
import EditGroupModal from './components/EditGroupModal';
|
import EditGroupModal from './components/EditGroupModal';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
@@ -30,17 +35,21 @@ import AdminApplication from './AdminApplication';
|
|||||||
|
|
||||||
export default Object.assign(compat, {
|
export default Object.assign(compat, {
|
||||||
'utils/saveSettings': saveSettings,
|
'utils/saveSettings': saveSettings,
|
||||||
|
'utils/ExtensionData': ExtensionData,
|
||||||
|
'utils/isExtensionEnabled': isExtensionEnabled,
|
||||||
|
'utils/getCategorizedExtensions': getCategorizedExtensions,
|
||||||
'components/SettingDropdown': SettingDropdown,
|
'components/SettingDropdown': SettingDropdown,
|
||||||
'components/EditCustomFooterModal': EditCustomFooterModal,
|
'components/EditCustomFooterModal': EditCustomFooterModal,
|
||||||
'components/SessionDropdown': SessionDropdown,
|
'components/SessionDropdown': SessionDropdown,
|
||||||
'components/HeaderPrimary': HeaderPrimary,
|
'components/HeaderPrimary': HeaderPrimary,
|
||||||
'components/AppearancePage': AppearancePage,
|
'components/AppearancePage': AppearancePage,
|
||||||
'components/StatusWidget': StatusWidget,
|
'components/StatusWidget': StatusWidget,
|
||||||
|
'components/ExtensionsWidget': ExtensionsWidget,
|
||||||
'components/HeaderSecondary': HeaderSecondary,
|
'components/HeaderSecondary': HeaderSecondary,
|
||||||
'components/SettingsModal': SettingsModal,
|
'components/SettingsModal': SettingsModal,
|
||||||
'components/DashboardWidget': DashboardWidget,
|
'components/DashboardWidget': DashboardWidget,
|
||||||
'components/AddExtensionModal': AddExtensionModal,
|
'components/ExtensionPage': ExtensionPage,
|
||||||
'components/ExtensionsPage': ExtensionsPage,
|
'components/ExtensionLinkButton': ExtensionLinkButton,
|
||||||
'components/AdminLinkButton': AdminLinkButton,
|
'components/AdminLinkButton': AdminLinkButton,
|
||||||
'components/PermissionGrid': PermissionGrid,
|
'components/PermissionGrid': PermissionGrid,
|
||||||
'components/MailPage': MailPage,
|
'components/MailPage': MailPage,
|
||||||
@@ -52,6 +61,7 @@ export default Object.assign(compat, {
|
|||||||
'components/PermissionsPage': PermissionsPage,
|
'components/PermissionsPage': PermissionsPage,
|
||||||
'components/PermissionDropdown': PermissionDropdown,
|
'components/PermissionDropdown': PermissionDropdown,
|
||||||
'components/AdminNav': AdminNav,
|
'components/AdminNav': AdminNav,
|
||||||
|
'components/AdminHeader': AdminHeader,
|
||||||
'components/EditCustomCssModal': EditCustomCssModal,
|
'components/EditCustomCssModal': EditCustomCssModal,
|
||||||
'components/EditGroupModal': EditGroupModal,
|
'components/EditGroupModal': EditGroupModal,
|
||||||
routes: routes,
|
routes: routes,
|
||||||
|
19
js/src/admin/components/AdminHeader.js
Normal file
19
js/src/admin/components/AdminHeader.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Component from '../../common/Component';
|
||||||
|
import classList from '../../common/utils/classList';
|
||||||
|
import icon from '../../common/helpers/icon';
|
||||||
|
|
||||||
|
export default class AdminHeader extends Component {
|
||||||
|
view(vnode) {
|
||||||
|
return [
|
||||||
|
<div className={classList(['AdminHeader', this.attrs.className])}>
|
||||||
|
<div className="container">
|
||||||
|
<h2>
|
||||||
|
{icon(this.attrs.icon)}
|
||||||
|
{vnode.children}
|
||||||
|
</h2>
|
||||||
|
<div className="AdminHeader-description">{this.attrs.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -1,28 +1,28 @@
|
|||||||
/*
|
import ExtensionLinkButton from './ExtensionLinkButton';
|
||||||
* 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 Component from '../../common/Component';
|
||||||
import AdminLinkButton from './AdminLinkButton';
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||||
|
import getCategorizedExtensions from '../utils/getCategorizedExtensions';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import Stream from '../../common/utils/Stream';
|
||||||
|
|
||||||
export default class AdminNav extends Component {
|
export default class AdminNav extends Component {
|
||||||
|
oninit(vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
this.query = Stream('');
|
||||||
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<SelectDropdown className="AdminNav App-titleControl" buttonClassName="Button">
|
<SelectDropdown className="AdminNav App-titleControl AdminNav-Main" buttonClassName="Button">
|
||||||
{this.items().toArray()}
|
{this.items().toArray().concat(this.extensionItems().toArray())}
|
||||||
</SelectDropdown>
|
</SelectDropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an item list of links to show in the admin navigation.
|
* Build an item list of main links to show in the admin navigation.
|
||||||
*
|
*
|
||||||
* @return {ItemList}
|
* @return {ItemList}
|
||||||
*/
|
*/
|
||||||
@@ -31,76 +31,90 @@ export default class AdminNav extends Component {
|
|||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'dashboard',
|
'dashboard',
|
||||||
AdminLinkButton.component(
|
<LinkButton href={app.route('dashboard')} icon="far fa-chart-bar" title={app.translator.trans('core.admin.nav.dashboard_title')}>
|
||||||
{
|
{app.translator.trans('core.admin.nav.dashboard_button')}
|
||||||
href: app.route('dashboard'),
|
</LinkButton>
|
||||||
icon: 'far fa-chart-bar',
|
|
||||||
description: app.translator.trans('core.admin.nav.dashboard_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.dashboard_button')
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'basics',
|
'basics',
|
||||||
AdminLinkButton.component(
|
<LinkButton href={app.route('basics')} icon="fas fa-pencil-alt" title={app.translator.trans('core.admin.nav.basics_title')}>
|
||||||
{
|
{app.translator.trans('core.admin.nav.basics_button')}
|
||||||
href: app.route('basics'),
|
</LinkButton>
|
||||||
icon: 'fas fa-pencil-alt',
|
|
||||||
description: app.translator.trans('core.admin.nav.basics_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.basics_button')
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'mail',
|
'mail',
|
||||||
AdminLinkButton.component(
|
<LinkButton href={app.route('mail')} icon="fas fa-envelope" title={app.translator.trans('core.admin.nav.email_title')}>
|
||||||
{
|
{app.translator.trans('core.admin.nav.email_button')}
|
||||||
href: app.route('mail'),
|
</LinkButton>
|
||||||
icon: 'fas fa-envelope',
|
|
||||||
description: app.translator.trans('core.admin.nav.email_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.email_button')
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'permissions',
|
'permissions',
|
||||||
AdminLinkButton.component(
|
<LinkButton href={app.route('permissions')} icon="fas fa-key" title={app.translator.trans('core.admin.nav.permissions_title')}>
|
||||||
{
|
{app.translator.trans('core.admin.nav.permissions_button')}
|
||||||
href: app.route('permissions'),
|
</LinkButton>
|
||||||
icon: 'fas fa-key',
|
|
||||||
description: app.translator.trans('core.admin.nav.permissions_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.permissions_button')
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'appearance',
|
'appearance',
|
||||||
AdminLinkButton.component(
|
<LinkButton href={app.route('appearance')} icon="fas fa-paint-brush" title={app.translator.trans('core.admin.nav.appearance_title')}>
|
||||||
{
|
{app.translator.trans('core.admin.nav.appearance_button')}
|
||||||
href: app.route('appearance'),
|
</LinkButton>
|
||||||
icon: 'fas fa-paint-brush',
|
|
||||||
description: app.translator.trans('core.admin.nav.appearance_text'),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.nav.appearance_button')
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'extensions',
|
'search',
|
||||||
AdminLinkButton.component(
|
<div className="Search-input">
|
||||||
{
|
<input
|
||||||
href: app.route('extensions'),
|
className="FormControl SearchBar"
|
||||||
icon: 'fas fa-puzzle-piece',
|
bidi={this.query}
|
||||||
description: app.translator.trans('core.admin.nav.extensions_text'),
|
type="search"
|
||||||
},
|
placeholder={app.translator.trans('core.admin.nav.search_placeholder')}
|
||||||
app.translator.trans('core.admin.nav.extensions_button')
|
/>
|
||||||
)
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extensionItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
const categorizedExtensions = getCategorizedExtensions();
|
||||||
|
const categories = app.extensionCategories;
|
||||||
|
|
||||||
|
Object.keys(categorizedExtensions).map((category) => {
|
||||||
|
if (!this.query()) {
|
||||||
|
items.add(
|
||||||
|
category,
|
||||||
|
<h4 className="ExtensionListTitle">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>,
|
||||||
|
categories[category]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
categorizedExtensions[category].map((extension) => {
|
||||||
|
const query = this.query().toUpperCase();
|
||||||
|
const title = extension.extra['flarum-extension'].title;
|
||||||
|
|
||||||
|
if (!query || title.toUpperCase().includes(query) || extension.description.toUpperCase().includes(query)) {
|
||||||
|
items.add(
|
||||||
|
extension.id,
|
||||||
|
<ExtensionLinkButton
|
||||||
|
href={app.route('extension', { id: extension.id })}
|
||||||
|
extensionId={extension.id}
|
||||||
|
className="ExtensionNavButton"
|
||||||
|
title={extension.description}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</ExtensionLinkButton>,
|
||||||
|
categories[category]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import EditCustomHeaderModal from './EditCustomHeaderModal';
|
|||||||
import EditCustomFooterModal from './EditCustomFooterModal';
|
import EditCustomFooterModal from './EditCustomFooterModal';
|
||||||
import UploadImageButton from './UploadImageButton';
|
import UploadImageButton from './UploadImageButton';
|
||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
import AdminHeader from './AdminHeader';
|
||||||
|
|
||||||
export default class AppearancePage extends Page {
|
export default class AppearancePage extends Page {
|
||||||
oninit(vnode) {
|
oninit(vnode) {
|
||||||
@@ -21,6 +22,13 @@ export default class AppearancePage extends Page {
|
|||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="AppearancePage">
|
<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">
|
<div className="container">
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
<fieldset className="AppearancePage-colors">
|
<fieldset className="AppearancePage-colors">
|
||||||
|
@@ -7,6 +7,7 @@ import ItemList from '../../common/utils/ItemList';
|
|||||||
import Switch from '../../common/components/Switch';
|
import Switch from '../../common/components/Switch';
|
||||||
import Stream from '../../common/utils/Stream';
|
import Stream from '../../common/utils/Stream';
|
||||||
import withAttr from '../../common/utils/withAttr';
|
import withAttr from '../../common/utils/withAttr';
|
||||||
|
import AdminHeader from './AdminHeader';
|
||||||
|
|
||||||
export default class BasicsPage extends Page {
|
export default class BasicsPage extends Page {
|
||||||
oninit(vnode) {
|
oninit(vnode) {
|
||||||
@@ -49,6 +50,9 @@ export default class BasicsPage extends Page {
|
|||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="BasicsPage">
|
<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">
|
<div className="container">
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
{FieldSet.component(
|
{FieldSet.component(
|
||||||
|
@@ -1,16 +1,29 @@
|
|||||||
import Page from '../../common/components/Page';
|
import Page from '../../common/components/Page';
|
||||||
import StatusWidget from './StatusWidget';
|
import StatusWidget from './StatusWidget';
|
||||||
|
import ExtensionsWidget from './ExtensionsWidget';
|
||||||
|
import AdminHeader from './AdminHeader';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
|
||||||
export default class DashboardPage extends Page {
|
export default class DashboardPage extends Page {
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="DashboardPage">
|
<div className="DashboardPage">
|
||||||
<div className="container">{this.availableWidgets()}</div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
availableWidgets() {
|
availableWidgets() {
|
||||||
return [<StatusWidget />];
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('status', <StatusWidget />, 30);
|
||||||
|
|
||||||
|
items.add('extensions', <ExtensionsWidget />, 10);
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
js/src/admin/components/ExtensionLinkButton.js
Normal file
29
js/src/admin/components/ExtensionLinkButton.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import isExtensionEnabled from '../utils/isExtensionEnabled';
|
||||||
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
|
import icon from '../../common/helpers/icon';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
|
||||||
|
export default class ExtensionLinkButton extends LinkButton {
|
||||||
|
getButtonContent(children) {
|
||||||
|
const content = super.getButtonContent(children);
|
||||||
|
const extension = app.data.extensions[this.attrs.extensionId];
|
||||||
|
const statuses = this.statusItems(extension.id).toArray();
|
||||||
|
|
||||||
|
content.unshift(
|
||||||
|
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
||||||
|
{extension.icon ? icon(extension.icon.name) : ''}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
content.push(statuses);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusItems(name) {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('enabled', <span class={'ExtensionListItem-Dot ' + (isExtensionEnabled(name) ? 'enabled' : 'disabled')} />);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
376
js/src/admin/components/ExtensionPage.js
Normal file
376
js/src/admin/components/ExtensionPage.js
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
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 ExtensionData from '../utils/ExtensionData';
|
||||||
|
import isExtensionEnabled from '../utils/isExtensionEnabled';
|
||||||
|
|
||||||
|
export default class ExtensionPage extends Page {
|
||||||
|
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',
|
||||||
|
documentation: 'fas fa-book',
|
||||||
|
support: 'fas fa-life-ring',
|
||||||
|
website: 'fas fa-link',
|
||||||
|
donate: 'fas fa-donate',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
return this.extension.id + '-Page';
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className={'ExtensionPage ' + this.className()}>
|
||||||
|
{this.header()}
|
||||||
|
{!this.isEnabled() ? (
|
||||||
|
<div className="container">
|
||||||
|
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h2>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ExtensionPage-body">{this.sections().toArray()}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
header() {
|
||||||
|
return [
|
||||||
|
<div className="ExtensionPage-header">
|
||||||
|
<div className="container">
|
||||||
|
<div className="ExtensionTitle">
|
||||||
|
<span className="ExtensionIcon" style={this.extension.icon}>
|
||||||
|
{this.extension.icon ? icon(this.extension.icon.name) : ''}
|
||||||
|
</span>
|
||||||
|
<div className="ExtensionName">
|
||||||
|
<h2>{this.extension.extra['flarum-extension'].title}</h2>
|
||||||
|
</div>
|
||||||
|
<div className="ExtensionPage-headerTopItems">
|
||||||
|
<ul>{listItems(this.topItems().toArray())}</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="helpText">{this.extension.description}</div>
|
||||||
|
<div className="ExtensionPage-headerItems">
|
||||||
|
<Switch state={this.isEnabled()} onchange={this.toggle.bind(this, this.extension.id)}>
|
||||||
|
{this.isEnabled(this.extension.id)
|
||||||
|
? app.translator.trans('core.admin.extension.enabled')
|
||||||
|
: app.translator.trans('core.admin.extension.disabled')}
|
||||||
|
</Switch>
|
||||||
|
<aside className="ExtensionInfo">
|
||||||
|
<ul>{listItems(this.infoItems().toArray())}</ul>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
sections() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('content', this.content());
|
||||||
|
|
||||||
|
items.add('permissions', [
|
||||||
|
<div className="ExtensionPage-permissions">
|
||||||
|
<div className="ExtensionPage-permissions-header">
|
||||||
|
<div className="container">
|
||||||
|
<h2 className="ExtensionTitle">{app.translator.trans('core.admin.extension.permissions_title')}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="container">
|
||||||
|
{app.extensionData.extensionHasPermissions(this.extension.id) ? (
|
||||||
|
ExtensionPermissionGrid.component({ extensionId: this.extension.id })
|
||||||
|
) : (
|
||||||
|
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h2>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
const settings = app.extensionData.getSettings(this.extension.id);
|
||||||
|
|
||||||
|
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 ? (
|
||||||
|
<div className="Form">
|
||||||
|
{settings.map(this.buildSettingComponent.bind(this))}
|
||||||
|
<div className="Form-group">{this.submitButton()}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h2>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
topItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('version', <span className="ExtensionVersion">{this.extension.version}</span>);
|
||||||
|
|
||||||
|
if (!this.isEnabled()) {
|
||||||
|
const uninstall = () => {
|
||||||
|
if (confirm(app.translator.trans('core.admin.extension.confirm_uninstall'))) {
|
||||||
|
app
|
||||||
|
.request({
|
||||||
|
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
.then(() => window.location.reload());
|
||||||
|
|
||||||
|
app.modal.show(LoadingModal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
'uninstall',
|
||||||
|
<Button icon="fas fa-trash-alt" className="Button Button--primary" onclick={uninstall.bind(this)}>
|
||||||
|
{app.translator.trans('core.admin.extension.uninstall_button')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
infoItems() {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
if (this.extension.authors) {
|
||||||
|
let authors = [];
|
||||||
|
|
||||||
|
Object.keys(this.extension.authors).map((author, i) => {
|
||||||
|
const link = this.extension.authors[author].homepage
|
||||||
|
? this.extension.authors[author].homepage
|
||||||
|
: 'mailto:' + this.extension.authors[author].email;
|
||||||
|
|
||||||
|
authors.push(
|
||||||
|
<Link href={link} external={true} target="_blank">
|
||||||
|
{this.extension.authors[author].name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
items.add('authors', [icon('fas fa-user'), <span>{punctuateSeries(authors)}</span>]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoData = {};
|
||||||
|
|
||||||
|
if (this.extension.source || this.extension.support) {
|
||||||
|
infoData.source = {
|
||||||
|
icon: 'fas fa-code',
|
||||||
|
href: this.extension.source ? this.extension.source.url : this.extension.support.source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(this.infoFields).map((field) => {
|
||||||
|
const info = this.extension.extra['flarum-extension'].info;
|
||||||
|
|
||||||
|
if (info && info[field]) {
|
||||||
|
infoData[field] = {
|
||||||
|
icon: this.infoFields[field],
|
||||||
|
href: info[field],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(infoData).map(([field, value]) => {
|
||||||
|
items.add(
|
||||||
|
field,
|
||||||
|
<LinkButton href={value.href} icon={value.icon} external={true} target="_blank">
|
||||||
|
{app.translator.trans(`core.admin.extension.info_links.${field}`)}
|
||||||
|
</LinkButton>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @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) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
this.changingState = true;
|
||||||
|
|
||||||
|
app
|
||||||
|
.request({
|
||||||
|
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { enabled: !enabled },
|
||||||
|
errorHandler: this.onerror.bind(this),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (!enabled) localStorage.setItem('enabledExtension', this.extension.id);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
||||||
|
let isEnabled = isExtensionEnabled(this.extension.id);
|
||||||
|
|
||||||
|
return this.changingState ? !isEnabled : isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
onerror(e) {
|
||||||
|
// We need to give the modal animation time to start; if we close the modal too early,
|
||||||
|
// it breaks the bootstrap modal library.
|
||||||
|
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
|
||||||
|
setTimeout(() => {
|
||||||
|
app.modal.close();
|
||||||
|
}, 300); // Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
||||||
|
|
||||||
|
if (e.status !== 409) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = e.response.errors[0];
|
||||||
|
|
||||||
|
app.alerts.show(
|
||||||
|
{ type: 'error' },
|
||||||
|
app.translator.trans(`core.lib.error.${error.code}_message`, {
|
||||||
|
extension: error.extension,
|
||||||
|
extensions: error.extensions.join(', '),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
js/src/admin/components/ExtensionPermissionGrid.js
Normal file
39
js/src/admin/components/ExtensionPermissionGrid.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import PermissionGrid from './PermissionGrid';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
|
||||||
|
export default class ExtensionPermissionGrid extends PermissionGrid {
|
||||||
|
oninit(vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
this.extensionId = this.attrs.extensionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionItems() {
|
||||||
|
const permissionCategories = super.permissionItems();
|
||||||
|
|
||||||
|
permissionCategories.items = Object.entries(permissionCategories.items)
|
||||||
|
.filter(([category, info]) => info.content.children.length > 0)
|
||||||
|
.reduce((obj, [category, info]) => {
|
||||||
|
obj[category] = info;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return permissionCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewItems() {
|
||||||
|
return app.extensionData.getExtensionPermissions(this.extensionId, 'view') || new ItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
startItems() {
|
||||||
|
return app.extensionData.getExtensionPermissions(this.extensionId, 'start') || new ItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
replyItems() {
|
||||||
|
return app.extensionData.getExtensionPermissions(this.extensionId, 'reply') || new ItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
moderateItems() {
|
||||||
|
return app.extensionData.getExtensionPermissions(this.extensionId, 'moderate') || new ItemList();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,158 +0,0 @@
|
|||||||
import Page from '../../common/components/Page';
|
|
||||||
import Button from '../../common/components/Button';
|
|
||||||
import Dropdown from '../../common/components/Dropdown';
|
|
||||||
import AddExtensionModal from './AddExtensionModal';
|
|
||||||
import LoadingModal from './LoadingModal';
|
|
||||||
import ItemList from '../../common/utils/ItemList';
|
|
||||||
import icon from '../../common/helpers/icon';
|
|
||||||
|
|
||||||
export default class ExtensionsPage extends Page {
|
|
||||||
view() {
|
|
||||||
return (
|
|
||||||
<div className="ExtensionsPage">
|
|
||||||
<div className="ExtensionsPage-header">
|
|
||||||
<div className="container">
|
|
||||||
{Button.component(
|
|
||||||
{
|
|
||||||
icon: 'fas fa-plus',
|
|
||||||
className: 'Button Button--primary',
|
|
||||||
onclick: () => app.modal.show(AddExtensionModal),
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.extensions.add_button')
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ExtensionsPage-list">
|
|
||||||
<div className="container">
|
|
||||||
<ul className="ExtensionList">
|
|
||||||
{Object.keys(app.data.extensions).map((id) => {
|
|
||||||
const extension = app.data.extensions[id];
|
|
||||||
const controls = this.controlItems(extension.id).toArray();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
|
|
||||||
<div className="ExtensionListItem-content">
|
|
||||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
|
||||||
{extension.icon ? icon(extension.icon.name) : ''}
|
|
||||||
</span>
|
|
||||||
{controls.length ? (
|
|
||||||
<Dropdown
|
|
||||||
className="ExtensionListItem-controls"
|
|
||||||
buttonClassName="Button Button--icon Button--flat"
|
|
||||||
menuClassName="Dropdown-menu--right"
|
|
||||||
icon="fas fa-ellipsis-h"
|
|
||||||
>
|
|
||||||
{controls}
|
|
||||||
</Dropdown>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
<div className="ExtensionListItem-main">
|
|
||||||
<label className="ExtensionListItem-title">
|
|
||||||
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)} />{' '}
|
|
||||||
{extension.extra['flarum-extension'].title}
|
|
||||||
</label>
|
|
||||||
<div className="ExtensionListItem-version">{extension.version}</div>
|
|
||||||
<div className="ExtensionListItem-description">{extension.description}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
controlItems(name) {
|
|
||||||
const items = new ItemList();
|
|
||||||
const enabled = this.isEnabled(name);
|
|
||||||
|
|
||||||
if (app.extensionSettings[name]) {
|
|
||||||
items.add(
|
|
||||||
'settings',
|
|
||||||
Button.component(
|
|
||||||
{
|
|
||||||
icon: 'fas fa-cog',
|
|
||||||
onclick: app.extensionSettings[name],
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.extensions.settings_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
items.add(
|
|
||||||
'uninstall',
|
|
||||||
Button.component(
|
|
||||||
{
|
|
||||||
icon: 'far fa-trash-alt',
|
|
||||||
onclick: () => {
|
|
||||||
app
|
|
||||||
.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
.then(() => window.location.reload());
|
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
app.translator.trans('core.admin.extensions.uninstall_button')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled(name) {
|
|
||||||
const enabled = JSON.parse(app.data.settings.extensions_enabled);
|
|
||||||
|
|
||||||
return enabled.indexOf(name) !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle(id) {
|
|
||||||
const enabled = this.isEnabled(id);
|
|
||||||
|
|
||||||
app
|
|
||||||
.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
|
||||||
method: 'PATCH',
|
|
||||||
body: { enabled: !enabled },
|
|
||||||
errorHandler: this.onerror.bind(this),
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (!enabled) localStorage.setItem('enabledExtension', id);
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
|
||||||
}
|
|
||||||
|
|
||||||
onerror(e) {
|
|
||||||
// We need to give the modal animation time to start; if we close the modal too early,
|
|
||||||
// it breaks the bootstrap modal library.
|
|
||||||
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
|
|
||||||
setTimeout(() => {
|
|
||||||
app.modal.close();
|
|
||||||
}, 300); // Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
|
||||||
|
|
||||||
if (e.status !== 409) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = e.response.errors[0];
|
|
||||||
|
|
||||||
app.alerts.show(
|
|
||||||
{ type: 'error' },
|
|
||||||
app.translator.trans(`core.lib.error.${error.code}_message`, {
|
|
||||||
extension: error.extension,
|
|
||||||
extensions: error.extensions.join(', '),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
48
js/src/admin/components/ExtensionsWidget.js
Normal file
48
js/src/admin/components/ExtensionsWidget.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import DashboardWidget from './DashboardWidget';
|
||||||
|
import isExtensionEnabled from '../utils/isExtensionEnabled';
|
||||||
|
import getCategorizedExtensions from '../utils/getCategorizedExtensions';
|
||||||
|
import Link from '../../common/components/Link';
|
||||||
|
import icon from '../../common/helpers/icon';
|
||||||
|
|
||||||
|
export default class ExtensionsWidget extends DashboardWidget {
|
||||||
|
className() {
|
||||||
|
return 'ExtensionsWidget';
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
const categorizedExtensions = getCategorizedExtensions();
|
||||||
|
const categories = app.extensionCategories;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ExtensionsWidget-list">
|
||||||
|
<div className="container">
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
import Component from '../../common/Component';
|
import Component from '../../common/Component';
|
||||||
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
import SessionDropdown from './SessionDropdown';
|
import SessionDropdown from './SessionDropdown';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
@@ -19,6 +20,13 @@ export default class HeaderSecondary extends Component {
|
|||||||
items() {
|
items() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
'help',
|
||||||
|
<LinkButton href="https://docs.flarum.org/troubleshoot.html" icon="fas fa-question-circle" external={true} target="_blank">
|
||||||
|
{app.translator.trans('core.admin.header.get_help')}
|
||||||
|
</LinkButton>
|
||||||
|
);
|
||||||
|
|
||||||
items.add('session', SessionDropdown.component());
|
items.add('session', SessionDropdown.component());
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
@@ -6,6 +6,8 @@ import Select from '../../common/components/Select';
|
|||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
import Stream from '../../common/utils/Stream';
|
import Stream from '../../common/utils/Stream';
|
||||||
|
import icon from '../../common/helpers/icon';
|
||||||
|
import AdminHeader from './AdminHeader';
|
||||||
|
|
||||||
export default class MailPage extends Page {
|
export default class MailPage extends Page {
|
||||||
oninit(vnode) {
|
oninit(vnode) {
|
||||||
@@ -65,11 +67,11 @@ export default class MailPage extends Page {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="MailPage">
|
<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">
|
<div className="container">
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
|
|
||||||
<div className="helpText">{app.translator.trans('core.admin.email.text')}</div>
|
|
||||||
|
|
||||||
{FieldSet.component(
|
{FieldSet.component(
|
||||||
{
|
{
|
||||||
label: app.translator.trans('core.admin.email.addresses_heading'),
|
label: app.translator.trans('core.admin.email.addresses_heading'),
|
||||||
|
@@ -6,12 +6,6 @@ import ItemList from '../../common/utils/ItemList';
|
|||||||
import icon from '../../common/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
|
|
||||||
export default class PermissionGrid extends Component {
|
export default class PermissionGrid extends Component {
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.permissions = this.permissionItems().toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
const scopes = this.scopeItems().toArray();
|
const scopes = this.scopeItems().toArray();
|
||||||
|
|
||||||
@@ -35,7 +29,9 @@ export default class PermissionGrid extends Component {
|
|||||||
<th>{this.scopeControlItems().toArray()}</th>
|
<th>{this.scopeControlItems().toArray()}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{this.permissions.map((section) => (
|
{this.permissionItems()
|
||||||
|
.toArray()
|
||||||
|
.map((section) => (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className="PermissionGrid-section">
|
<tr className="PermissionGrid-section">
|
||||||
<th>{section.label}</th>
|
<th>{section.label}</th>
|
||||||
@@ -158,6 +154,8 @@ export default class PermissionGrid extends Component {
|
|||||||
permission: 'user.viewLastSeenAt',
|
permission: 'user.viewLastSeenAt',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
items.merge(app.extensionData.getAllExtensionPermissions('view'));
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +196,8 @@ export default class PermissionGrid extends Component {
|
|||||||
90
|
90
|
||||||
);
|
);
|
||||||
|
|
||||||
|
items.merge(app.extensionData.getAllExtensionPermissions('start'));
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +238,8 @@ export default class PermissionGrid extends Component {
|
|||||||
90
|
90
|
||||||
);
|
);
|
||||||
|
|
||||||
|
items.merge(app.extensionData.getAllExtensionPermissions('reply'));
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +336,8 @@ export default class PermissionGrid extends Component {
|
|||||||
60
|
60
|
||||||
);
|
);
|
||||||
|
|
||||||
|
items.merge(app.extensionData.getAllExtensionPermissions('moderate'));
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,11 +4,15 @@ import EditGroupModal from './EditGroupModal';
|
|||||||
import Group from '../../common/models/Group';
|
import Group from '../../common/models/Group';
|
||||||
import icon from '../../common/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
import PermissionGrid from './PermissionGrid';
|
import PermissionGrid from './PermissionGrid';
|
||||||
|
import AdminHeader from './AdminHeader';
|
||||||
|
|
||||||
export default class PermissionsPage extends Page {
|
export default class PermissionsPage extends Page {
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="PermissionsPage">
|
<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="PermissionsPage-groups">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{app.store
|
{app.store
|
||||||
|
19
js/src/admin/resolvers/ExtensionPageResolver.ts
Normal file
19
js/src/admin/resolvers/ExtensionPageResolver.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import DefaultResolver from '../../common/resolvers/DefaultResolver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom route resolver for ExtensionPage that generates handles routes
|
||||||
|
* to default extension pages or a page provided by an extension.
|
||||||
|
*/
|
||||||
|
export default class ExtensionPageResolver extends DefaultResolver {
|
||||||
|
static extension: string | null = null;
|
||||||
|
|
||||||
|
onmatch(args, requestedPath, route) {
|
||||||
|
const extensionPage = app.extensionData.getPage(args.id);
|
||||||
|
|
||||||
|
if (extensionPage) {
|
||||||
|
return extensionPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onmatch(args, requestedPath, route);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,8 +2,9 @@ import DashboardPage from './components/DashboardPage';
|
|||||||
import BasicsPage from './components/BasicsPage';
|
import BasicsPage from './components/BasicsPage';
|
||||||
import PermissionsPage from './components/PermissionsPage';
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
import AppearancePage from './components/AppearancePage';
|
import AppearancePage from './components/AppearancePage';
|
||||||
import ExtensionsPage from './components/ExtensionsPage';
|
|
||||||
import MailPage from './components/MailPage';
|
import MailPage from './components/MailPage';
|
||||||
|
import ExtensionPage from './components/ExtensionPage';
|
||||||
|
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `routes` initializer defines the forum app's routes.
|
* The `routes` initializer defines the forum app's routes.
|
||||||
@@ -16,7 +17,7 @@ export default function (app) {
|
|||||||
basics: { path: '/basics', component: BasicsPage },
|
basics: { path: '/basics', component: BasicsPage },
|
||||||
permissions: { path: '/permissions', component: PermissionsPage },
|
permissions: { path: '/permissions', component: PermissionsPage },
|
||||||
appearance: { path: '/appearance', component: AppearancePage },
|
appearance: { path: '/appearance', component: AppearancePage },
|
||||||
extensions: { path: '/extensions', component: ExtensionsPage },
|
|
||||||
mail: { path: '/mail', component: MailPage },
|
mail: { path: '/mail', component: MailPage },
|
||||||
|
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
167
js/src/admin/utils/ExtensionData.js
Normal file
167
js/src/admin/utils/ExtensionData.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
|
||||||
|
export default class ExtensionData {
|
||||||
|
constructor() {
|
||||||
|
this.data = {};
|
||||||
|
this.currentExtension = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function simply takes the extension id
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* app.extensionData.load('flarum-tags')
|
||||||
|
*
|
||||||
|
* flarum/flags -> flarum-flags | acme/extension -> acme-extension
|
||||||
|
*
|
||||||
|
* @param extension
|
||||||
|
*/
|
||||||
|
for(extension) {
|
||||||
|
this.currentExtension = extension;
|
||||||
|
this.data[extension] = this.data[extension] || {};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function registers your settings with Flarum
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* .registerSetting({
|
||||||
|
* setting: 'flarum-flags.guidelines_url',
|
||||||
|
* type: 'text', // This will be inputted into the input tag for the setting (text/number/etc)
|
||||||
|
* label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label')
|
||||||
|
* }, 15) // priority is optional (ItemList)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param content
|
||||||
|
* @param priority
|
||||||
|
* @returns {ExtensionData}
|
||||||
|
*/
|
||||||
|
registerSetting(content, priority = 0) {
|
||||||
|
this.data[this.currentExtension].settings = this.data[this.currentExtension].settings || new ItemList();
|
||||||
|
|
||||||
|
this.data[this.currentExtension].settings.add(content.setting, content, priority);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function registers your permission with Flarum
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* .registerPermission('permissions', {
|
||||||
|
* icon: 'fas fa-flag',
|
||||||
|
* label: app.translator.trans('flarum-flags.admin.permissions.view_flags_label'),
|
||||||
|
* permission: 'discussion.viewFlags'
|
||||||
|
* }, 'moderate', 65)
|
||||||
|
*
|
||||||
|
* @param content
|
||||||
|
* @param permissionType
|
||||||
|
* @param priority
|
||||||
|
* @returns {ExtensionData}
|
||||||
|
*/
|
||||||
|
registerPermission(content, permissionType = null, priority = 0) {
|
||||||
|
this.data[this.currentExtension].permissions = this.data[this.currentExtension].permissions || {};
|
||||||
|
|
||||||
|
if (!this.data[this.currentExtension].permissions[permissionType]) {
|
||||||
|
this.data[this.currentExtension].permissions[permissionType] = new ItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data[this.currentExtension].permissions[permissionType].add(content.permission, content, priority);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the default extension page with a custom component.
|
||||||
|
* This component would typically extend ExtensionPage
|
||||||
|
*
|
||||||
|
* @param component
|
||||||
|
* @returns {ExtensionData}
|
||||||
|
*/
|
||||||
|
registerPage(component) {
|
||||||
|
this.data[this.currentExtension].page = component;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an extension's registered settings
|
||||||
|
*
|
||||||
|
* @param extensionId
|
||||||
|
* @returns {boolean|*}
|
||||||
|
*/
|
||||||
|
getSettings(extensionId) {
|
||||||
|
if (this.data[extensionId] && this.data[extensionId].settings) {
|
||||||
|
return this.data[extensionId].settings.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Get an ItemList of all extensions' registered permissions
|
||||||
|
*
|
||||||
|
* @param extension
|
||||||
|
* @param type
|
||||||
|
* @returns {ItemList}
|
||||||
|
*/
|
||||||
|
getAllExtensionPermissions(type) {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
Object.keys(this.data).map((extension) => {
|
||||||
|
if (this.extensionHasPermissions(extension) && this.data[extension].permissions[type]) {
|
||||||
|
items.merge(this.data[extension].permissions[type]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a singular extension's registered permissions
|
||||||
|
*
|
||||||
|
* @param extension
|
||||||
|
* @param type
|
||||||
|
* @returns {boolean|*}
|
||||||
|
*/
|
||||||
|
getExtensionPermissions(extension, type) {
|
||||||
|
if (this.extensionHasPermissions(extension) && this.data[extension].permissions[type]) {
|
||||||
|
return this.data[extension].permissions[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a given extension has registered permissions.
|
||||||
|
*
|
||||||
|
* @param extension
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
extensionHasPermissions(extension) {
|
||||||
|
if (this.data[extension] && this.data[extension].permissions) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an extension's custom page component if it exists.
|
||||||
|
*
|
||||||
|
* @param extension
|
||||||
|
* @returns {boolean|*}
|
||||||
|
*/
|
||||||
|
getPage(extension) {
|
||||||
|
if (this.data[extension]) {
|
||||||
|
return this.data[extension].page;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
25
js/src/admin/utils/getCategorizedExtensions.js
Normal file
25
js/src/admin/utils/getCategorizedExtensions.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default function getCategorizedExtensions() {
|
||||||
|
let extensions = {};
|
||||||
|
|
||||||
|
Object.keys(app.data.extensions).map((id) => {
|
||||||
|
const extension = app.data.extensions[id];
|
||||||
|
let category = extension.extra['flarum-extension'].category;
|
||||||
|
|
||||||
|
// Wrap languages packs into new system
|
||||||
|
if (extension.extra['flarum-locale']) {
|
||||||
|
category = 'language';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category in app.extensionCategories) {
|
||||||
|
extensions[category] = extensions[category] || [];
|
||||||
|
|
||||||
|
extensions[category].push(extension);
|
||||||
|
} else {
|
||||||
|
extensions.other = extensions.other || [];
|
||||||
|
|
||||||
|
extensions.other.push(extension);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return extensions;
|
||||||
|
}
|
5
js/src/admin/utils/isExtensionEnabled.js
Normal file
5
js/src/admin/utils/isExtensionEnabled.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default function isExtensionEnabled(name) {
|
||||||
|
const enabled = JSON.parse(app.data.settings.extensions_enabled);
|
||||||
|
|
||||||
|
return enabled.includes(name);
|
||||||
|
}
|
@@ -35,6 +35,11 @@ export default class Button extends Component {
|
|||||||
attrs['aria-label'] = attrs.title;
|
attrs['aria-label'] = attrs.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If given a translation object, extract the text.
|
||||||
|
if (typeof attrs.title === 'object') {
|
||||||
|
attrs.title = extractText(attrs.title);
|
||||||
|
}
|
||||||
|
|
||||||
// If nothing else is provided, we use the textual button content as tooltip
|
// If nothing else is provided, we use the textual button content as tooltip
|
||||||
if (!attrs.title && vnode.children) {
|
if (!attrs.title && vnode.children) {
|
||||||
attrs.title = extractText(vnode.children);
|
attrs.title = extractText(vnode.children);
|
||||||
|
@@ -12,6 +12,9 @@ import icon from '../helpers/icon';
|
|||||||
function isActive(vnode) {
|
function isActive(vnode) {
|
||||||
const tag = vnode.tag;
|
const tag = vnode.tag;
|
||||||
|
|
||||||
|
// Allow non-selectable dividers/headers to be added.
|
||||||
|
if (typeof tag === 'string' && tag !== 'a' && tag !== 'button') return false;
|
||||||
|
|
||||||
if ('initAttrs' in tag) {
|
if ('initAttrs' in tag) {
|
||||||
tag.initAttrs(vnode.attrs);
|
tag.initAttrs(vnode.attrs);
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
@import "common/common";
|
@import "common/common";
|
||||||
|
|
||||||
|
@import "admin/AdminHeader";
|
||||||
@import "admin/AdminNav";
|
@import "admin/AdminNav";
|
||||||
@import "admin/DashboardPage";
|
@import "admin/DashboardPage";
|
||||||
@import "admin/BasicsPage";
|
@import "admin/BasicsPage";
|
||||||
@import "admin/PermissionsPage";
|
@import "admin/PermissionsPage";
|
||||||
@import "admin/EditGroupModal";
|
@import "admin/EditGroupModal";
|
||||||
@import "admin/ExtensionsPage";
|
@import "admin/ExtensionPage";
|
||||||
|
@import "admin/ExtensionWidget";
|
||||||
@import "admin/AppearancePage";
|
@import "admin/AppearancePage";
|
||||||
@import "admin/MailPage";
|
@import "admin/MailPage";
|
||||||
|
19
less/admin/AdminHeader.less
Normal file
19
less/admin/AdminHeader.less
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.AdminHeader {
|
||||||
|
background: @control-bg;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: @muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminHeader-description {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,17 +1,85 @@
|
|||||||
@admin-pane-width: 300px;
|
@admin-pane-width: 250px;
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
.AdminLinkButton-description {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.AdminContent {
|
.AdminContent {
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
.App-content .sideNavOffset {
|
.App-content .sideNavOffset {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Header-controls {
|
||||||
|
> li {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @phone {
|
||||||
|
.Dropdown-menu {
|
||||||
|
height: 70vh;
|
||||||
|
|
||||||
|
.item-search {
|
||||||
|
margin: 10px;
|
||||||
|
|
||||||
|
.SearchBar {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionNavButton {
|
||||||
|
.Button-label {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
.ExtensionIcon {
|
||||||
|
margin: 0 0 0 -4px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @tablet {
|
||||||
|
.item-search{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionItem, .item-search {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionListTitle {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @phone, @tablet {
|
||||||
|
.App-nav .AdminNav {
|
||||||
|
.Dropdown-menu {
|
||||||
|
> li {
|
||||||
|
.ExtensionListTitle {
|
||||||
|
color: @muted-color;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 25px 0 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionIcon {
|
||||||
|
margin: -2px -29px;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media @desktop, @desktop-hd {
|
@media @desktop, @desktop-hd {
|
||||||
.App-nav {
|
.App-nav {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -20,60 +88,84 @@
|
|||||||
width: @admin-pane-width;
|
width: @admin-pane-width;
|
||||||
.box-shadow(0 6px 6px @shadow-color);
|
.box-shadow(0 6px 6px @shadow-color);
|
||||||
background: @body-bg;
|
background: @body-bg;
|
||||||
border-top: 1px solid @control-bg;
|
|
||||||
z-index: @zindex-pane;
|
z-index: @zindex-pane;
|
||||||
overflow: auto;
|
overflow-y: scroll;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
|
||||||
.affix & {
|
.affix & {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.App-content .sideNavOffset {
|
.App-content .sideNavOffset {
|
||||||
margin-left: @admin-pane-width;
|
margin-left: @admin-pane-width;
|
||||||
}
|
}
|
||||||
.App-nav .AdminNav {
|
.App-nav .AdminNav {
|
||||||
.Dropdown-menu > li {
|
.Dropdown-menu {
|
||||||
|
.item-search {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> li {
|
||||||
> a {
|
> a {
|
||||||
padding: 15px 15px 15px 45px;
|
padding: 10px 10px 10px 45px;
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
white-space: normal;
|
|
||||||
}
|
}
|
||||||
> a, > a:hover, &.active > a {
|
> a,
|
||||||
color: @muted-color;
|
> a:hover,
|
||||||
|
&.active > a {
|
||||||
|
color: @text-color;
|
||||||
}
|
}
|
||||||
> a:hover {
|
> a:hover {
|
||||||
background: @control-bg;
|
background: @control-bg;
|
||||||
}
|
}
|
||||||
&.active > a {
|
&.active > a {
|
||||||
background: @control-bg;
|
background: @primary-color;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
||||||
.Button-label, .Button-icon {
|
.Button-label,
|
||||||
color: @text-color;
|
.Button-icon {
|
||||||
|
color: @body-bg;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.Button-icon {
|
.Button-icon {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: -30px;
|
font-size: 13px !important;
|
||||||
font-size: 14px;
|
margin-left: -25px !important;
|
||||||
margin-top: 4px !important;
|
margin-top: 4px !important;
|
||||||
}
|
}
|
||||||
.Button-label {
|
.Button-label {
|
||||||
display: block;
|
padding-left: 5px;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 0 0 5px;
|
|
||||||
}
|
}
|
||||||
.AdminLinkButton-description {
|
|
||||||
display: block;
|
.Search-input,
|
||||||
font-size: 12px;
|
.SearchBar {
|
||||||
|
max-width: 215px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionListTitle {
|
||||||
|
color: @muted-color;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 25px 0 15px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionIcon {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-left: -29px;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -85,4 +177,33 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionListItem-Dot {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
right: 13px;
|
||||||
|
margin: 6px 5px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionNavButton {
|
||||||
|
.Button-label {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: calc(100% - 18px);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionListItem-Dot.enabled {
|
||||||
|
background-color: #2ECC40;
|
||||||
|
}
|
||||||
|
.ExtensionListItem-Dot.disabled {
|
||||||
|
background-color: #FF4136;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
.AppearancePage {
|
.AppearancePage {
|
||||||
|
|
||||||
@media @desktop-up {
|
@media @desktop-up {
|
||||||
.container {
|
.container {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
padding: 30px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
.BasicsPage {
|
.BasicsPage {
|
||||||
padding: 20px 0;
|
|
||||||
|
|
||||||
@media @desktop-up {
|
@media @desktop-up {
|
||||||
.container {
|
.container {
|
||||||
|
@@ -1,18 +1,11 @@
|
|||||||
.DashboardPage {
|
.DashboardPage {
|
||||||
background: @control-bg;
|
background: @body-bg;
|
||||||
color: @control-color;
|
color: @control-color;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
@media @desktop-up {
|
|
||||||
.container {
|
|
||||||
padding: 30px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.DashboardWidget {
|
.Widget {
|
||||||
background: @body-bg;
|
background: @control-bg;
|
||||||
color: @text-color;
|
color: @text-color;
|
||||||
border-radius: @border-radius;
|
border-radius: @border-radius;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
153
less/admin/ExtensionPage.less
Normal file
153
less/admin/ExtensionPage.less
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
.ExtensionPage {
|
||||||
|
min-height: 110vh;
|
||||||
|
|
||||||
|
.ExtensionPage-header {
|
||||||
|
.ExtensionTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 20px 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionPage-header,
|
||||||
|
.ExtensionPage-permissions-header {
|
||||||
|
background: @control-bg;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: @muted-color;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 13px;
|
||||||
|
color: @muted-color;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button-icon {
|
||||||
|
display: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
display: inline;
|
||||||
|
color: @muted-color;
|
||||||
|
margin-left: 13px;
|
||||||
|
|
||||||
|
|
||||||
|
> a {
|
||||||
|
color: @muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionPage-headerItems {
|
||||||
|
padding: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.Checkbox {
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Checkbox.off {
|
||||||
|
.Checkbox-display {
|
||||||
|
background: @muted-more-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionInfo {
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
.item-authors {
|
||||||
|
a {
|
||||||
|
color: @muted-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ExtensionName {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionIcon {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-left: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionPage-headerTopItems {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: @screen-phone-max) {
|
||||||
|
.ExtensionPage-headerTopItems {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-website, .item-source, .item-documentation {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ExtensionPage-settings {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionPage-subHeader {
|
||||||
|
color: @muted-color;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ExtensionPage-permissions {
|
||||||
|
|
||||||
|
@media @phone {
|
||||||
|
> .container {
|
||||||
|
overflow-x: scroll;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionPage-permissions-header {
|
||||||
|
margin: 20px 0 20px;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
less/admin/ExtensionWidget.less
Normal file
93
less/admin/ExtensionWidget.less
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
.ExtensionsWidget {
|
||||||
|
background-color: @body-bg;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionsWidget-list {
|
||||||
|
> .container {
|
||||||
|
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;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: @muted-color;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionList {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 10px;
|
||||||
|
grid-template-columns: repeat(auto-fit, 90px);
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
text-align: left;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionListItem.disabled {
|
||||||
|
.ExtensionListItem-title {
|
||||||
|
opacity: 0.5;
|
||||||
|
color: @muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionListItem-icon {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionListItem {
|
||||||
|
transition: .15s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionListItem-title {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
color: @text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionIcon {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
background: @control-bg;
|
||||||
|
color: @control-color;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 45px;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
@@ -1,115 +0,0 @@
|
|||||||
@extension-list-column-width: 410px;
|
|
||||||
|
|
||||||
.ExtensionsPage-header {
|
|
||||||
padding: 20px 0;
|
|
||||||
background: @control-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ExtensionsPage-list {
|
|
||||||
padding: 30px 0;
|
|
||||||
}
|
|
||||||
.ExtensionGroup {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: @muted-color;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 0 0 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ExtensionList {
|
|
||||||
columns: 3;
|
|
||||||
column-width: @extension-list-column-width;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
.clearfix();
|
|
||||||
|
|
||||||
> li {
|
|
||||||
-webkit-column-break-inside: avoid;
|
|
||||||
break-inside: avoid-column;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
text-align: left;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background .2s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ExtensionListItem.disabled {
|
|
||||||
.ExtensionListItem-title {
|
|
||||||
opacity: 0.5;
|
|
||||||
color: @muted-color;
|
|
||||||
}
|
|
||||||
.ExtensionListItem-icon {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ExtensionListItem {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.ExtensionListItem:hover {
|
|
||||||
background: @control-bg;
|
|
||||||
}
|
|
||||||
.ExtensionListItem-content {
|
|
||||||
padding: 0 50px;
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
.ExtensionListItem-main {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.ExtensionListItem-title {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
.ExtensionListItem-version {
|
|
||||||
color: @muted-more-color;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: normal;
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
.ExtensionListItem-controls {
|
|
||||||
float: right;
|
|
||||||
display: none;
|
|
||||||
margin-right: -50px;
|
|
||||||
margin-top: 1px;
|
|
||||||
|
|
||||||
.ExtensionListItem:hover &, &.open {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ExtensionListItem-description {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: normal;
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ExtensionIcon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
background: @control-bg;
|
|
||||||
color: @control-color;
|
|
||||||
border-radius: 6px;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 40px;
|
|
||||||
text-align: center;
|
|
||||||
margin-left: -50px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: @extension-list-column-width) {
|
|
||||||
.ExtensionListItem-description {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.ExtensionListItem-version {
|
|
||||||
display: block;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,4 @@
|
|||||||
.MailPage {
|
.MailPage {
|
||||||
padding: 20px 0;
|
|
||||||
|
|
||||||
@media @desktop-up {
|
@media @desktop-up {
|
||||||
.container {
|
.container {
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
.PermissionsPage-groups {
|
.PermissionsPage-groups {
|
||||||
background: @control-bg;
|
background: @control-bg;
|
||||||
padding: 20px 0;
|
border-radius: @border-radius;
|
||||||
|
max-width: calc(~'100% - 60px');
|
||||||
|
display: block;
|
||||||
|
margin-left: 30px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 8px 0 8px;
|
||||||
}
|
}
|
||||||
.Group {
|
.Group {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
|
Reference in New Issue
Block a user