mirror of
https://github.com/flarum/core.git
synced 2025-08-15 04:44:08 +02:00
Compare commits
37 Commits
v0.1.0-bet
...
urlgenerat
Author | SHA1 | Date | |
---|---|---|---|
|
68cacd5404 | ||
|
345a8e5773 | ||
|
1cbaef579c | ||
|
8fb65e9de7 | ||
|
92a5b98b77 | ||
|
1eb316bf44 | ||
|
47e8990813 | ||
|
dbce6705fd | ||
|
9de6ebbe4f | ||
|
f63ca2be3c | ||
|
6a1017d4b1 | ||
|
696dc8ef6b | ||
|
a8fa57ad30 | ||
|
9ff02f8822 | ||
|
dde718e27f | ||
|
a155e41432 | ||
|
4574fa6290 | ||
|
e1c61a0e85 | ||
|
102e76b084 | ||
|
d09d4bc507 | ||
|
c3989cc952 | ||
|
9cb9097b24 | ||
|
571a835be0 | ||
|
0c95774333 | ||
|
67741c7a6f | ||
|
f5cfec15e3 | ||
|
47d2eee9ce | ||
|
c10cc92deb | ||
|
529d2edcaf | ||
|
f0e77a5789 | ||
|
87c258b2f8 | ||
|
cee87848fe | ||
|
5842dd1200 | ||
|
b311512502 | ||
|
0b2a5fa5b8 | ||
|
52e45aacad | ||
|
21c2a4b2a4 |
6
js/dist/admin.js
vendored
6
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
6
js/dist/forum.js
vendored
6
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -1,13 +1,29 @@
|
||||
import HeaderPrimary from './components/HeaderPrimary';
|
||||
import HeaderSecondary from './components/HeaderSecondary';
|
||||
import routes from './routes';
|
||||
import ExtensionPage from './components/ExtensionPage';
|
||||
import Application from '../common/Application';
|
||||
import Navigation from '../common/components/Navigation';
|
||||
import AdminNav from './components/AdminNav';
|
||||
import ExtensionData from './utils/ExtensionData';
|
||||
|
||||
export default class AdminApplication extends Application {
|
||||
// Deprecated as of beta 15
|
||||
extensionSettings = {};
|
||||
|
||||
extensionData = new ExtensionData();
|
||||
|
||||
extensionCategories = {
|
||||
discussion: 70,
|
||||
moderation: 60,
|
||||
feature: 50,
|
||||
formatting: 40,
|
||||
theme: 30,
|
||||
authentication: 20,
|
||||
language: 10,
|
||||
other: 0,
|
||||
};
|
||||
|
||||
history = {
|
||||
canGoBack: () => true,
|
||||
getPrevious: () => {},
|
||||
@@ -34,7 +50,13 @@ export default class AdminApplication extends Application {
|
||||
m.route.prefix = '#';
|
||||
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-primary'), HeaderPrimary);
|
||||
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
|
||||
// callback.
|
||||
const enabled = localStorage.getItem('enabledExtension');
|
||||
if (enabled && this.extensionSettings[enabled]) {
|
||||
if (enabled && this.extensionSettings[enabled] && typeof this.extensionSettings[enabled] === 'function') {
|
||||
this.extensionSettings[enabled]();
|
||||
localStorage.removeItem('enabledExtension');
|
||||
}
|
||||
|
@@ -1,17 +1,21 @@
|
||||
import compat from '../common/compat';
|
||||
|
||||
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 EditCustomFooterModal from './components/EditCustomFooterModal';
|
||||
import SessionDropdown from './components/SessionDropdown';
|
||||
import HeaderPrimary from './components/HeaderPrimary';
|
||||
import AppearancePage from './components/AppearancePage';
|
||||
import StatusWidget from './components/StatusWidget';
|
||||
import ExtensionsWidget from './components/ExtensionsWidget';
|
||||
import HeaderSecondary from './components/HeaderSecondary';
|
||||
import SettingsModal from './components/SettingsModal';
|
||||
import DashboardWidget from './components/DashboardWidget';
|
||||
import AddExtensionModal from './components/AddExtensionModal';
|
||||
import ExtensionsPage from './components/ExtensionsPage';
|
||||
import ExtensionPage from './components/ExtensionPage';
|
||||
import ExtensionLinkButton from './components/ExtensionLinkButton';
|
||||
import AdminLinkButton from './components/AdminLinkButton';
|
||||
import PermissionGrid from './components/PermissionGrid';
|
||||
import MailPage from './components/MailPage';
|
||||
@@ -23,6 +27,7 @@ import EditCustomHeaderModal from './components/EditCustomHeaderModal';
|
||||
import PermissionsPage from './components/PermissionsPage';
|
||||
import PermissionDropdown from './components/PermissionDropdown';
|
||||
import AdminNav from './components/AdminNav';
|
||||
import AdminHeader from './components/AdminHeader';
|
||||
import EditCustomCssModal from './components/EditCustomCssModal';
|
||||
import EditGroupModal from './components/EditGroupModal';
|
||||
import routes from './routes';
|
||||
@@ -30,17 +35,21 @@ import AdminApplication from './AdminApplication';
|
||||
|
||||
export default Object.assign(compat, {
|
||||
'utils/saveSettings': saveSettings,
|
||||
'utils/ExtensionData': ExtensionData,
|
||||
'utils/isExtensionEnabled': isExtensionEnabled,
|
||||
'utils/getCategorizedExtensions': getCategorizedExtensions,
|
||||
'components/SettingDropdown': SettingDropdown,
|
||||
'components/EditCustomFooterModal': EditCustomFooterModal,
|
||||
'components/SessionDropdown': SessionDropdown,
|
||||
'components/HeaderPrimary': HeaderPrimary,
|
||||
'components/AppearancePage': AppearancePage,
|
||||
'components/StatusWidget': StatusWidget,
|
||||
'components/ExtensionsWidget': ExtensionsWidget,
|
||||
'components/HeaderSecondary': HeaderSecondary,
|
||||
'components/SettingsModal': SettingsModal,
|
||||
'components/DashboardWidget': DashboardWidget,
|
||||
'components/AddExtensionModal': AddExtensionModal,
|
||||
'components/ExtensionsPage': ExtensionsPage,
|
||||
'components/ExtensionPage': ExtensionPage,
|
||||
'components/ExtensionLinkButton': ExtensionLinkButton,
|
||||
'components/AdminLinkButton': AdminLinkButton,
|
||||
'components/PermissionGrid': PermissionGrid,
|
||||
'components/MailPage': MailPage,
|
||||
@@ -52,6 +61,7 @@ export default Object.assign(compat, {
|
||||
'components/PermissionsPage': PermissionsPage,
|
||||
'components/PermissionDropdown': PermissionDropdown,
|
||||
'components/AdminNav': AdminNav,
|
||||
'components/AdminHeader': AdminHeader,
|
||||
'components/EditCustomCssModal': EditCustomCssModal,
|
||||
'components/EditGroupModal': EditGroupModal,
|
||||
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 @@
|
||||
/*
|
||||
* 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 ExtensionLinkButton from './ExtensionLinkButton';
|
||||
import Component from '../../common/Component';
|
||||
import AdminLinkButton from './AdminLinkButton';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
import getCategorizedExtensions from '../utils/getCategorizedExtensions';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
|
||||
export default class AdminNav extends Component {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.query = Stream('');
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<SelectDropdown className="AdminNav App-titleControl" buttonClassName="Button">
|
||||
{this.items().toArray()}
|
||||
<SelectDropdown className="AdminNav App-titleControl AdminNav-Main" buttonClassName="Button">
|
||||
{this.items().toArray().concat(this.extensionItems().toArray())}
|
||||
</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}
|
||||
*/
|
||||
@@ -31,76 +31,90 @@ export default class AdminNav extends Component {
|
||||
|
||||
items.add(
|
||||
'dashboard',
|
||||
AdminLinkButton.component(
|
||||
{
|
||||
href: app.route('dashboard'),
|
||||
icon: 'far fa-chart-bar',
|
||||
description: app.translator.trans('core.admin.nav.dashboard_text'),
|
||||
},
|
||||
app.translator.trans('core.admin.nav.dashboard_button')
|
||||
)
|
||||
<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')}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
items.add(
|
||||
'basics',
|
||||
AdminLinkButton.component(
|
||||
{
|
||||
href: app.route('basics'),
|
||||
icon: 'fas fa-pencil-alt',
|
||||
description: app.translator.trans('core.admin.nav.basics_text'),
|
||||
},
|
||||
app.translator.trans('core.admin.nav.basics_button')
|
||||
)
|
||||
<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')}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
items.add(
|
||||
'mail',
|
||||
AdminLinkButton.component(
|
||||
{
|
||||
href: app.route('mail'),
|
||||
icon: 'fas fa-envelope',
|
||||
description: app.translator.trans('core.admin.nav.email_text'),
|
||||
},
|
||||
app.translator.trans('core.admin.nav.email_button')
|
||||
)
|
||||
<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')}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
items.add(
|
||||
'permissions',
|
||||
AdminLinkButton.component(
|
||||
{
|
||||
href: app.route('permissions'),
|
||||
icon: 'fas fa-key',
|
||||
description: app.translator.trans('core.admin.nav.permissions_text'),
|
||||
},
|
||||
app.translator.trans('core.admin.nav.permissions_button')
|
||||
)
|
||||
<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')}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
items.add(
|
||||
'appearance',
|
||||
AdminLinkButton.component(
|
||||
{
|
||||
href: app.route('appearance'),
|
||||
icon: 'fas fa-paint-brush',
|
||||
description: app.translator.trans('core.admin.nav.appearance_text'),
|
||||
},
|
||||
app.translator.trans('core.admin.nav.appearance_button')
|
||||
)
|
||||
<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')}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
items.add(
|
||||
'extensions',
|
||||
AdminLinkButton.component(
|
||||
{
|
||||
href: app.route('extensions'),
|
||||
icon: 'fas fa-puzzle-piece',
|
||||
description: app.translator.trans('core.admin.nav.extensions_text'),
|
||||
},
|
||||
app.translator.trans('core.admin.nav.extensions_button')
|
||||
)
|
||||
'search',
|
||||
<div className="Search-input">
|
||||
<input
|
||||
className="FormControl SearchBar"
|
||||
bidi={this.query}
|
||||
type="search"
|
||||
placeholder={app.translator.trans('core.admin.nav.search_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 UploadImageButton from './UploadImageButton';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
import AdminHeader from './AdminHeader';
|
||||
|
||||
export default class AppearancePage extends Page {
|
||||
oninit(vnode) {
|
||||
@@ -21,6 +22,13 @@ export default class AppearancePage extends Page {
|
||||
view() {
|
||||
return (
|
||||
<div className="AppearancePage">
|
||||
<AdminHeader
|
||||
icon="fas fa-paint-brush"
|
||||
description={app.translator.trans('core.admin.appearance.description')}
|
||||
className="AppearancePage-header"
|
||||
>
|
||||
{app.translator.trans('core.admin.appearance.title')}
|
||||
</AdminHeader>
|
||||
<div className="container">
|
||||
<form onsubmit={this.onsubmit.bind(this)}>
|
||||
<fieldset className="AppearancePage-colors">
|
||||
|
@@ -7,6 +7,7 @@ import ItemList from '../../common/utils/ItemList';
|
||||
import Switch from '../../common/components/Switch';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import withAttr from '../../common/utils/withAttr';
|
||||
import AdminHeader from './AdminHeader';
|
||||
|
||||
export default class BasicsPage extends Page {
|
||||
oninit(vnode) {
|
||||
@@ -24,10 +25,6 @@ export default class BasicsPage extends Page {
|
||||
'welcome_message',
|
||||
'display_name_driver',
|
||||
];
|
||||
this.values = {};
|
||||
|
||||
const settings = app.data.settings;
|
||||
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
|
||||
|
||||
this.localeOptions = {};
|
||||
const locales = app.data.locales;
|
||||
@@ -41,14 +38,38 @@ export default class BasicsPage extends Page {
|
||||
this.displayNameOptions[identifier] = identifier;
|
||||
}, this);
|
||||
|
||||
this.slugDriverOptions = {};
|
||||
Object.keys(app.data.slugDrivers).forEach((model) => {
|
||||
this.fields.push(`slug_driver_${model}`);
|
||||
this.slugDriverOptions[model] = {};
|
||||
|
||||
app.data.slugDrivers[model].forEach((option) => {
|
||||
this.slugDriverOptions[model][option] = option;
|
||||
});
|
||||
});
|
||||
|
||||
this.values = {};
|
||||
|
||||
const settings = app.data.settings;
|
||||
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
|
||||
|
||||
if (!this.values.display_name_driver() && displayNameDrivers.includes('username')) this.values.display_name_driver('username');
|
||||
|
||||
Object.keys(app.data.slugDrivers).forEach((model) => {
|
||||
if (!this.values[`slug_driver_${model}`]() && 'default' in this.slugDriverOptions[model]) {
|
||||
this.values[`slug_driver_${model}`]('default');
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1);
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="BasicsPage">
|
||||
<AdminHeader icon="fas fa-pencil-alt" description={app.translator.trans('core.admin.basics.description')} className="BasicsPage-header">
|
||||
{app.translator.trans('core.admin.basics.title')}
|
||||
</AdminHeader>
|
||||
<div className="container">
|
||||
<form onsubmit={this.onsubmit.bind(this)}>
|
||||
{FieldSet.component(
|
||||
@@ -128,20 +149,30 @@ export default class BasicsPage extends Page {
|
||||
]
|
||||
)}
|
||||
|
||||
{Object.keys(this.displayNameOptions).length > 1
|
||||
? FieldSet.component(
|
||||
{
|
||||
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
||||
},
|
||||
[
|
||||
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>,
|
||||
Select.component({
|
||||
options: this.displayNameOptions,
|
||||
bidi: this.values.display_name_driver,
|
||||
}),
|
||||
]
|
||||
)
|
||||
: ''}
|
||||
{Object.keys(this.displayNameOptions).length > 1 ? (
|
||||
<FieldSet label={app.translator.trans('core.admin.basics.display_name_heading')}>
|
||||
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>
|
||||
<Select
|
||||
options={this.displayNameOptions}
|
||||
value={this.values.display_name_driver()}
|
||||
onchange={this.values.display_name_driver}
|
||||
></Select>
|
||||
</FieldSet>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
{Object.keys(this.slugDriverOptions).map((model) => {
|
||||
const options = this.slugDriverOptions[model];
|
||||
if (Object.keys(options).length > 1) {
|
||||
return (
|
||||
<FieldSet label={app.translator.trans('core.admin.basics.slug_driver_heading', { model })}>
|
||||
<div className="helpText">{app.translator.trans('core.admin.basics.slug_driver_text', { model })}</div>
|
||||
<Select options={options} value={this.values[`slug_driver_${model}`]()} onchange={this.values[`slug_driver_${model}`]}></Select>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{Button.component(
|
||||
{
|
||||
|
@@ -1,16 +1,29 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import StatusWidget from './StatusWidget';
|
||||
import ExtensionsWidget from './ExtensionsWidget';
|
||||
import AdminHeader from './AdminHeader';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
|
||||
export default class DashboardPage extends Page {
|
||||
view() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 LinkButton from '../../common/components/LinkButton';
|
||||
import SessionDropdown from './SessionDropdown';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
@@ -19,6 +20,13 @@ export default class HeaderSecondary extends Component {
|
||||
items() {
|
||||
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());
|
||||
|
||||
return items;
|
||||
|
@@ -6,6 +6,8 @@ import Select from '../../common/components/Select';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import AdminHeader from './AdminHeader';
|
||||
|
||||
export default class MailPage extends Page {
|
||||
oninit(vnode) {
|
||||
@@ -65,11 +67,11 @@ export default class MailPage extends Page {
|
||||
|
||||
return (
|
||||
<div className="MailPage">
|
||||
<AdminHeader icon="fas fa-envelope" description={app.translator.trans('core.admin.email.description')} className="MailPage-header">
|
||||
{app.translator.trans('core.admin.email.title')}
|
||||
</AdminHeader>
|
||||
<div className="container">
|
||||
<form onsubmit={this.onsubmit.bind(this)}>
|
||||
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
|
||||
<div className="helpText">{app.translator.trans('core.admin.email.text')}</div>
|
||||
|
||||
{FieldSet.component(
|
||||
{
|
||||
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';
|
||||
|
||||
export default class PermissionGrid extends Component {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.permissions = this.permissionItems().toArray();
|
||||
}
|
||||
|
||||
view() {
|
||||
const scopes = this.scopeItems().toArray();
|
||||
|
||||
@@ -35,25 +29,27 @@ export default class PermissionGrid extends Component {
|
||||
<th>{this.scopeControlItems().toArray()}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.permissions.map((section) => (
|
||||
<tbody>
|
||||
<tr className="PermissionGrid-section">
|
||||
<th>{section.label}</th>
|
||||
{permissionCells(section)}
|
||||
<td />
|
||||
</tr>
|
||||
{section.children.map((child) => (
|
||||
<tr className="PermissionGrid-child">
|
||||
<th>
|
||||
{icon(child.icon)}
|
||||
{child.label}
|
||||
</th>
|
||||
{permissionCells(child)}
|
||||
{this.permissionItems()
|
||||
.toArray()
|
||||
.map((section) => (
|
||||
<tbody>
|
||||
<tr className="PermissionGrid-section">
|
||||
<th>{section.label}</th>
|
||||
{permissionCells(section)}
|
||||
<td />
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
))}
|
||||
{section.children.map((child) => (
|
||||
<tr className="PermissionGrid-child">
|
||||
<th>
|
||||
{icon(child.icon)}
|
||||
{child.label}
|
||||
</th>
|
||||
{permissionCells(child)}
|
||||
<td />
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
))}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -158,6 +154,8 @@ export default class PermissionGrid extends Component {
|
||||
permission: 'user.viewLastSeenAt',
|
||||
});
|
||||
|
||||
items.merge(app.extensionData.getAllExtensionPermissions('view'));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -198,6 +196,8 @@ export default class PermissionGrid extends Component {
|
||||
90
|
||||
);
|
||||
|
||||
items.merge(app.extensionData.getAllExtensionPermissions('start'));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -238,6 +238,8 @@ export default class PermissionGrid extends Component {
|
||||
90
|
||||
);
|
||||
|
||||
items.merge(app.extensionData.getAllExtensionPermissions('reply'));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -334,6 +336,8 @@ export default class PermissionGrid extends Component {
|
||||
60
|
||||
);
|
||||
|
||||
items.merge(app.extensionData.getAllExtensionPermissions('moderate'));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
@@ -4,11 +4,15 @@ import EditGroupModal from './EditGroupModal';
|
||||
import Group from '../../common/models/Group';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import PermissionGrid from './PermissionGrid';
|
||||
import AdminHeader from './AdminHeader';
|
||||
|
||||
export default class PermissionsPage extends Page {
|
||||
view() {
|
||||
return (
|
||||
<div className="PermissionsPage">
|
||||
<AdminHeader icon="fas fa-key" description={app.translator.trans('core.admin.permissions.description')} className="PermissionsPage-header">
|
||||
{app.translator.trans('core.admin.permissions.title')}
|
||||
</AdminHeader>
|
||||
<div className="PermissionsPage-groups">
|
||||
<div className="container">
|
||||
{app.store
|
||||
|
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 PermissionsPage from './components/PermissionsPage';
|
||||
import AppearancePage from './components/AppearancePage';
|
||||
import ExtensionsPage from './components/ExtensionsPage';
|
||||
import MailPage from './components/MailPage';
|
||||
import ExtensionPage from './components/ExtensionPage';
|
||||
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
|
||||
|
||||
/**
|
||||
* The `routes` initializer defines the forum app's routes.
|
||||
@@ -16,7 +17,7 @@ export default function (app) {
|
||||
basics: { path: '/basics', component: BasicsPage },
|
||||
permissions: { path: '/permissions', component: PermissionsPage },
|
||||
appearance: { path: '/appearance', component: AppearancePage },
|
||||
extensions: { path: '/extensions', component: ExtensionsPage },
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 (!attrs.title && vnode.children) {
|
||||
attrs.title = extractText(vnode.children);
|
||||
|
@@ -12,6 +12,9 @@ import icon from '../helpers/icon';
|
||||
function isActive(vnode) {
|
||||
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) {
|
||||
tag.initAttrs(vnode.attrs);
|
||||
}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import dayjs from 'dayjs';
|
||||
import * as Mithril from 'mithril';
|
||||
|
||||
/**
|
||||
* The `fullTime` helper displays a formatted time string wrapped in a <time>
|
||||
* tag.
|
||||
*
|
||||
* @param {Date} time
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function fullTime(time) {
|
||||
export default function fullTime(time: Date): Mithril.Vnode {
|
||||
const d = dayjs(time);
|
||||
|
||||
const datetime = d.format();
|
@@ -1,14 +1,13 @@
|
||||
import dayjs from 'dayjs';
|
||||
import * as Mithril from 'mithril';
|
||||
import humanTimeUtil from '../utils/humanTime';
|
||||
|
||||
/**
|
||||
* The `humanTime` helper displays a time in a human-friendly time-ago format
|
||||
* (e.g. '12 days ago'), wrapped in a <time> tag with other information about
|
||||
* the time.
|
||||
*
|
||||
* @param {Date} time
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function humanTime(time) {
|
||||
export default function humanTime(time: Date): Mithril.Vnode {
|
||||
const d = dayjs(time);
|
||||
|
||||
const datetime = d.format();
|
@@ -10,6 +10,7 @@ export default class User extends Model {}
|
||||
|
||||
Object.assign(User.prototype, {
|
||||
username: Model.attribute('username'),
|
||||
slug: Model.attribute('slug'),
|
||||
displayName: Model.attribute('displayName'),
|
||||
email: Model.attribute('email'),
|
||||
isEmailConfirmed: Model.attribute('isEmailConfirmed'),
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/plugin/relativeTime';
|
||||
|
||||
/**
|
||||
* The `humanTime` utility converts a date to a localized, human-readable time-
|
||||
* ago string.
|
||||
|
@@ -100,7 +100,7 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
.save(data)
|
||||
.then((discussion) => {
|
||||
this.composer.hide();
|
||||
app.discussions.refresh();
|
||||
app.discussions.refresh({ deferClear: true });
|
||||
m.route.set(app.route.discussion(discussion));
|
||||
}, this.loaded.bind(this));
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import DiscussionControls from '../utils/DiscussionControls';
|
||||
import slidable from '../utils/slidable';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import classList from '../../common/utils/classList';
|
||||
import DiscussionPage from './DiscussionPage';
|
||||
|
||||
import { escapeRegExp } from 'lodash-es';
|
||||
/**
|
||||
@@ -156,9 +157,7 @@ export default class DiscussionListItem extends Component {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
active() {
|
||||
const idParam = m.route.param('id');
|
||||
|
||||
return idParam && idParam.split('-')[0] === this.attrs.discussion.id();
|
||||
return app.current.matches(DiscussionPage, { discussion: this.attrs.discussion });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -107,7 +107,7 @@ export default class DiscussionPage extends Page {
|
||||
} else {
|
||||
const params = this.requestParams();
|
||||
|
||||
app.store.find('discussions', m.route.param('id').split('-')[0], params).then(this.show.bind(this));
|
||||
app.store.find('discussions', m.route.param('id'), params).then(this.show.bind(this));
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
@@ -121,6 +121,7 @@ export default class DiscussionPage extends Page {
|
||||
*/
|
||||
requestParams() {
|
||||
return {
|
||||
bySlug: true,
|
||||
page: { near: this.near },
|
||||
};
|
||||
}
|
||||
|
@@ -287,7 +287,9 @@ export default class PostStream extends Component {
|
||||
* @return {Integer}
|
||||
*/
|
||||
getMarginTop() {
|
||||
return this.$() && $('#header').outerHeight() + parseInt(this.$().css('margin-top'), 10);
|
||||
const headerId = app.screen() === 'phone' ? '#app-navigation' : '#header';
|
||||
|
||||
return this.$() && $(headerId).outerHeight() + parseInt(this.$().css('margin-top'), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -21,6 +21,8 @@ import UsersSearchSource from './UsersSearchSource';
|
||||
* - state: SearchState instance.
|
||||
*/
|
||||
export default class Search extends Component {
|
||||
static MIN_SEARCH_LEN = 3;
|
||||
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
this.state = this.attrs.state;
|
||||
@@ -152,7 +154,7 @@ export default class Search extends Component {
|
||||
search.searchTimeout = setTimeout(() => {
|
||||
if (state.isCached(query)) return;
|
||||
|
||||
if (query.length >= 3) {
|
||||
if (query.length >= Search.MIN_SEARCH_LEN) {
|
||||
search.sources.map((source) => {
|
||||
if (!source.search) return;
|
||||
|
||||
|
@@ -102,7 +102,7 @@ export default class UserPage extends Page {
|
||||
});
|
||||
|
||||
if (!this.user) {
|
||||
app.store.find('users', username).then(this.show.bind(this));
|
||||
app.store.find('users', username, { bySlug: true }).then(this.show.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,6 @@
|
||||
import DefaultResolver from '../../common/resolvers/DefaultResolver';
|
||||
import DiscussionPage from '../components/DiscussionPage';
|
||||
|
||||
/**
|
||||
* This isn't exported as it is a temporary measure.
|
||||
* A more robust system will be implemented alongside UTF-8 support in beta 15.
|
||||
*/
|
||||
function getDiscussionIdFromSlug(slug: string | undefined) {
|
||||
if (!slug) return;
|
||||
return slug.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom route resolver for DiscussionPage that generates the same key to all posts
|
||||
* on the same discussion. It triggers a scroll when going from one post to another
|
||||
@@ -18,17 +9,32 @@ function getDiscussionIdFromSlug(slug: string | undefined) {
|
||||
export default class DiscussionPageResolver extends DefaultResolver {
|
||||
static scrollToPostNumber: string | null = null;
|
||||
|
||||
/**
|
||||
* Remove optional parts of a discussion's slug to keep the substring
|
||||
* that bijectively maps to a discussion object. By default this just
|
||||
* extracts the numerical ID from the slug. If a custom discussion
|
||||
* slugging driver is used, this may need to be overriden.
|
||||
* @param slug
|
||||
*/
|
||||
canonicalizeDiscussionSlug(slug: string | undefined) {
|
||||
if (!slug) return;
|
||||
return slug.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
makeKey() {
|
||||
const params = { ...m.route.param() };
|
||||
if ('near' in params) {
|
||||
delete params.near;
|
||||
}
|
||||
params.id = getDiscussionIdFromSlug(params.id);
|
||||
params.id = this.canonicalizeDiscussionSlug(params.id);
|
||||
return this.routeName.replace('.near', '') + JSON.stringify(params);
|
||||
}
|
||||
|
||||
onmatch(args, requestedPath, route) {
|
||||
if (app.current.matches(DiscussionPage) && getDiscussionIdFromSlug(args.id) === getDiscussionIdFromSlug(m.route.param('id'))) {
|
||||
if (app.current.matches(DiscussionPage) && this.canonicalizeDiscussionSlug(args.id) === this.canonicalizeDiscussionSlug(m.route.param('id'))) {
|
||||
// By default, the first post number of any discussion is 1
|
||||
DiscussionPageResolver.scrollToPostNumber = args.near || '1';
|
||||
}
|
||||
|
@@ -34,9 +34,8 @@ export default function (app) {
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.discussion = (discussion, near) => {
|
||||
const slug = discussion.slug();
|
||||
return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
|
||||
id: discussion.id() + (slug.trim() ? '-' + slug : ''),
|
||||
id: discussion.slug(),
|
||||
near: near && near !== 1 ? near : undefined,
|
||||
});
|
||||
};
|
||||
@@ -59,7 +58,7 @@ export default function (app) {
|
||||
*/
|
||||
app.route.user = (user) => {
|
||||
return app.route('user', {
|
||||
username: user.username(),
|
||||
username: user.slug(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
@import "common/common";
|
||||
|
||||
@import "admin/AdminHeader";
|
||||
@import "admin/AdminNav";
|
||||
@import "admin/DashboardPage";
|
||||
@import "admin/BasicsPage";
|
||||
@import "admin/PermissionsPage";
|
||||
@import "admin/EditGroupModal";
|
||||
@import "admin/ExtensionsPage";
|
||||
@import "admin/ExtensionPage";
|
||||
@import "admin/ExtensionWidget";
|
||||
@import "admin/AppearancePage";
|
||||
@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 {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.AdminLinkButton-description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.AdminContent {
|
||||
padding: 20px 0;
|
||||
}
|
||||
.App-content .sideNavOffset {
|
||||
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 {
|
||||
.App-nav {
|
||||
position: absolute;
|
||||
@@ -20,60 +88,84 @@
|
||||
width: @admin-pane-width;
|
||||
.box-shadow(0 6px 6px @shadow-color);
|
||||
background: @body-bg;
|
||||
border-top: 1px solid @control-bg;
|
||||
z-index: @zindex-pane;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
padding-bottom: 40px;
|
||||
|
||||
.affix & {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.App-content .sideNavOffset {
|
||||
margin-left: @admin-pane-width;
|
||||
}
|
||||
.App-nav .AdminNav {
|
||||
.Dropdown-menu > li {
|
||||
> a {
|
||||
padding: 15px 15px 15px 45px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
white-space: normal;
|
||||
.Dropdown-menu {
|
||||
.item-search {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
> a, > a:hover, &.active > a {
|
||||
color: @muted-color;
|
||||
}
|
||||
> a:hover {
|
||||
background: @control-bg;
|
||||
}
|
||||
&.active > a {
|
||||
background: @control-bg;
|
||||
font-weight: normal;
|
||||
|
||||
.Button-label, .Button-icon {
|
||||
> li {
|
||||
> a {
|
||||
padding: 10px 10px 10px 45px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
> a,
|
||||
> a:hover,
|
||||
&.active > a {
|
||||
color: @text-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
> a:hover {
|
||||
background: @control-bg;
|
||||
}
|
||||
&.active > a {
|
||||
background: @primary-color;
|
||||
font-weight: normal;
|
||||
|
||||
.Button-label,
|
||||
.Button-icon {
|
||||
color: @body-bg;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.Button-icon {
|
||||
float: left;
|
||||
font-size: 13px !important;
|
||||
margin-left: -25px !important;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
.Button-label {
|
||||
padding-left: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.Search-input,
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.Button-icon {
|
||||
float: left;
|
||||
margin-left: -30px;
|
||||
font-size: 14px;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
.Button-label {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
.AdminLinkButton-description {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -85,4 +177,33 @@
|
||||
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 {
|
||||
|
||||
@media @desktop-up {
|
||||
.container {
|
||||
max-width: 600px;
|
||||
padding: 30px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
.BasicsPage {
|
||||
padding: 20px 0;
|
||||
|
||||
@media @desktop-up {
|
||||
.container {
|
||||
|
@@ -1,18 +1,11 @@
|
||||
.DashboardPage {
|
||||
background: @control-bg;
|
||||
background: @body-bg;
|
||||
color: @control-color;
|
||||
min-height: 100vh;
|
||||
|
||||
@media @desktop-up {
|
||||
.container {
|
||||
padding: 30px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.DashboardWidget {
|
||||
background: @body-bg;
|
||||
.Widget {
|
||||
background: @control-bg;
|
||||
color: @text-color;
|
||||
border-radius: @border-radius;
|
||||
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 {
|
||||
padding: 20px 0;
|
||||
|
||||
@media @desktop-up {
|
||||
.container {
|
||||
|
@@ -1,6 +1,11 @@
|
||||
.PermissionsPage-groups {
|
||||
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 {
|
||||
width: 90px;
|
||||
|
@@ -10,29 +10,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Fix a solid white box to the top of the viewport. This toolbar's contents
|
||||
// will differ depending on the device: on phones it will be content
|
||||
// controls, whereas on desktops it will be the header. We will overlay
|
||||
// these things on top of it later.
|
||||
.App:before {
|
||||
content: " ";
|
||||
.header-background();
|
||||
border-bottom: 0;
|
||||
position: absolute;
|
||||
|
||||
.affix& {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.scrolled& {
|
||||
.box-shadow(0 2px 6px @shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
// PHONES: Somewhere on the page there will be a .App-backControl, a
|
||||
// .App-primaryControl, and a .App-titleControl. We will position these on the
|
||||
// left, right, and center of the header respectively.
|
||||
@media @phone {
|
||||
.App-navigation {
|
||||
.header-background();
|
||||
border-bottom: 0;
|
||||
position: absolute;
|
||||
|
||||
.affix & {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.scrolled & {
|
||||
.box-shadow(0 2px 6px @shadow-color);
|
||||
}
|
||||
}
|
||||
.App-primaryControl, .App-titleControl, .App-backControl {
|
||||
position: absolute !important;
|
||||
z-index: @zindex-header + 1;
|
||||
@@ -234,18 +228,18 @@
|
||||
display: none;
|
||||
}
|
||||
.App-header {
|
||||
.header-background();
|
||||
padding: 8px;
|
||||
height: @header-height;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: @zindex-header;
|
||||
|
||||
|
||||
.affix & {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.scrolled & {
|
||||
.box-shadow(0 2px 6px @shadow-color);
|
||||
}
|
||||
|
||||
& when (@config-colored-header = true) {
|
||||
.light-contents(@header-color, @header-control-bg, @header-control-color);
|
||||
}
|
||||
|
@@ -105,6 +105,10 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.off.Checkbox--switch .Checkbox-display {
|
||||
background: @muted-more-color;
|
||||
}
|
||||
}
|
||||
.Modal-footer {
|
||||
border: 0;
|
||||
|
@@ -54,9 +54,10 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
HttpMiddleware\StartSession::class,
|
||||
HttpMiddleware\RememberFromCookie::class,
|
||||
HttpMiddleware\AuthenticateWithSession::class,
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
HttpMiddleware\SetLocale::class,
|
||||
Middleware\RequireAdministrateAbility::class,
|
||||
'flarum.admin.route_resolver',
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
Middleware\RequireAdministrateAbility::class
|
||||
];
|
||||
});
|
||||
|
||||
@@ -68,6 +69,10 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.admin.route_resolver', function () {
|
||||
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.admin.routes'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.admin.handler', function () {
|
||||
$pipe = new MiddlewarePipe;
|
||||
|
||||
@@ -75,7 +80,7 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
$pipe->pipe($this->app->make($middleware));
|
||||
}
|
||||
|
||||
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.admin.routes')));
|
||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||
|
||||
return $pipe;
|
||||
});
|
||||
|
@@ -75,6 +75,9 @@ class AdminPayload
|
||||
$document->payload['extensions'] = $this->extensions->getExtensions()->toArray();
|
||||
|
||||
$document->payload['displayNameDrivers'] = array_keys($this->container->make('flarum.user.display_name.supported_drivers'));
|
||||
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
|
||||
return array_keys($resourceDrivers);
|
||||
}, $this->container->make('flarum.http.slugDrivers'));
|
||||
|
||||
$document->payload['phpVersion'] = PHP_VERSION;
|
||||
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;
|
||||
|
@@ -51,8 +51,9 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
HttpMiddleware\RememberFromCookie::class,
|
||||
HttpMiddleware\AuthenticateWithSession::class,
|
||||
HttpMiddleware\AuthenticateWithHeader::class,
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
HttpMiddleware\SetLocale::class,
|
||||
'flarum.api.route_resolver',
|
||||
HttpMiddleware\CheckCsrfToken::class
|
||||
];
|
||||
});
|
||||
|
||||
@@ -64,6 +65,10 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.api.route_resolver', function () {
|
||||
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.api.routes'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.handler', function () {
|
||||
$pipe = new MiddlewarePipe;
|
||||
|
||||
@@ -71,10 +76,16 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
$pipe->pipe($this->app->make($middleware));
|
||||
}
|
||||
|
||||
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.api.routes')));
|
||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||
|
||||
return $pipe;
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.notification_serializers', function () {
|
||||
return [
|
||||
'discussionRenamed' => BasicDiscussionSerializer::class
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +93,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->registerNotificationSerializers();
|
||||
$this->setNotificationSerializers();
|
||||
|
||||
AbstractSerializeController::setContainer($this->app);
|
||||
AbstractSerializeController::setEventDispatcher($events = $this->app->make('events'));
|
||||
@@ -94,13 +105,12 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
/**
|
||||
* Register notification serializers.
|
||||
*/
|
||||
protected function registerNotificationSerializers()
|
||||
protected function setNotificationSerializers()
|
||||
{
|
||||
$blueprints = [];
|
||||
$serializers = [
|
||||
'discussionRenamed' => BasicDiscussionSerializer::class
|
||||
];
|
||||
$serializers = $this->app->make('flarum.api.notification_serializers');
|
||||
|
||||
// Deprecated in beta 15, remove in beta 16
|
||||
$this->app->make('events')->dispatch(
|
||||
new ConfigureNotificationTypes($blueprints, $serializers)
|
||||
);
|
||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\DiscussionRepository;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
@@ -36,6 +37,11 @@ class ShowDiscussionController extends AbstractShowController
|
||||
*/
|
||||
public $serializer = DiscussionSerializer::class;
|
||||
|
||||
/**
|
||||
* @var SlugManager
|
||||
*/
|
||||
protected $slugManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -62,8 +68,9 @@ class ShowDiscussionController extends AbstractShowController
|
||||
* @param \Flarum\Discussion\DiscussionRepository $discussions
|
||||
* @param \Flarum\Post\PostRepository $posts
|
||||
*/
|
||||
public function __construct(DiscussionRepository $discussions, PostRepository $posts)
|
||||
public function __construct(SlugManager $slugManager, DiscussionRepository $discussions, PostRepository $posts)
|
||||
{
|
||||
$this->slugManager = $slugManager;
|
||||
$this->discussions = $discussions;
|
||||
$this->posts = $posts;
|
||||
}
|
||||
@@ -77,7 +84,11 @@ class ShowDiscussionController extends AbstractShowController
|
||||
$actor = $request->getAttribute('actor');
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
$discussion = $this->discussions->findOrFail($discussionId, $actor);
|
||||
if (Arr::get($request->getQueryParams(), 'bySlug', false)) {
|
||||
$discussion = $this->slugManager->forResource(Discussion::class)->fromSlug($discussionId, $actor);
|
||||
} else {
|
||||
$discussion = $this->discussions->findOrFail($discussionId, $actor);
|
||||
}
|
||||
|
||||
if (in_array('posts', $include)) {
|
||||
$postRelationships = $this->getPostRelationships($include);
|
||||
|
@@ -11,6 +11,8 @@ namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\User\User;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
@@ -29,15 +31,22 @@ class ShowUserController extends AbstractShowController
|
||||
public $include = ['groups'];
|
||||
|
||||
/**
|
||||
* @var \Flarum\User\UserRepository
|
||||
* @var SlugManager
|
||||
*/
|
||||
protected $slugManager;
|
||||
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* @param \Flarum\User\UserRepository $users
|
||||
* @param SlugManager $slugManager
|
||||
* @param UserRepository $users
|
||||
*/
|
||||
public function __construct(UserRepository $users)
|
||||
public function __construct(SlugManager $slugManager, UserRepository $users)
|
||||
{
|
||||
$this->slugManager = $slugManager;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
@@ -47,17 +56,18 @@ class ShowUserController extends AbstractShowController
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
if (! is_numeric($id)) {
|
||||
$id = $this->users->getIdForUsername($id);
|
||||
}
|
||||
|
||||
$actor = $request->getAttribute('actor');
|
||||
|
||||
if ($actor->id == $id) {
|
||||
if (Arr::get($request->getQueryParams(), 'bySlug', false)) {
|
||||
$user = $this->slugManager->forResource(User::class)->fromSlug($id, $actor);
|
||||
} else {
|
||||
$user = $this->users->findOrFail($id, $actor);
|
||||
}
|
||||
|
||||
if ($actor->id === $user->id) {
|
||||
$this->serializer = CurrentUserSerializer::class;
|
||||
}
|
||||
|
||||
return $this->users->findOrFail($id, $actor);
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Api\Serializer;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\SlugManager;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class BasicDiscussionSerializer extends AbstractSerializer
|
||||
@@ -19,6 +20,16 @@ class BasicDiscussionSerializer extends AbstractSerializer
|
||||
*/
|
||||
protected $type = 'discussions';
|
||||
|
||||
/**
|
||||
* @var SlugManager
|
||||
*/
|
||||
protected $slugManager;
|
||||
|
||||
public function __construct(SlugManager $slugManager)
|
||||
{
|
||||
$this->slugManager = $slugManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
@@ -35,7 +46,7 @@ class BasicDiscussionSerializer extends AbstractSerializer
|
||||
|
||||
return [
|
||||
'title' => $discussion->title,
|
||||
'slug' => $discussion->slug,
|
||||
'slug' => $this->slugManager->forResource(Discussion::class)->toSlug($discussion),
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Api\Serializer;
|
||||
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\User\User;
|
||||
use InvalidArgumentException;
|
||||
|
||||
@@ -19,6 +20,16 @@ class BasicUserSerializer extends AbstractSerializer
|
||||
*/
|
||||
protected $type = 'users';
|
||||
|
||||
/**
|
||||
* @var SlugManager
|
||||
*/
|
||||
protected $slugManager;
|
||||
|
||||
public function __construct(SlugManager $slugManager)
|
||||
{
|
||||
$this->slugManager = $slugManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
@@ -36,7 +47,8 @@ class BasicUserSerializer extends AbstractSerializer
|
||||
return [
|
||||
'username' => $user->username,
|
||||
'displayName' => $user->display_name,
|
||||
'avatarUrl' => $user->avatar_url
|
||||
'avatarUrl' => $user->avatar_url,
|
||||
'slug' => $this->slugManager->forResource(User::class)->toSlug($user)
|
||||
];
|
||||
}
|
||||
|
||||
|
42
src/Discussion/IdWithTransliteratedSlugDriver.php
Normal file
42
src/Discussion/IdWithTransliteratedSlugDriver.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Discussion;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Http\SlugDriverInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class IdWithTransliteratedSlugDriver implements SlugDriverInterface
|
||||
{
|
||||
/**
|
||||
* @var DiscussionRepository
|
||||
*/
|
||||
protected $discussions;
|
||||
|
||||
public function __construct(DiscussionRepository $discussions)
|
||||
{
|
||||
$this->discussions = $discussions;
|
||||
}
|
||||
|
||||
public function toSlug(AbstractModel $instance): string
|
||||
{
|
||||
return $instance->id.(trim($instance->slug) ? '-'.$instance->slug : '');
|
||||
}
|
||||
|
||||
public function fromSlug(string $slug, User $actor): AbstractModel
|
||||
{
|
||||
if (strpos($slug, '-')) {
|
||||
$slug_array = explode('-', $slug);
|
||||
$slug = $slug_array[0];
|
||||
}
|
||||
|
||||
return $this->discussions->findOrFail($slug, $actor);
|
||||
}
|
||||
}
|
@@ -13,6 +13,9 @@ use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class ConfigureNotificationTypes
|
||||
{
|
||||
/**
|
||||
|
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace Flarum\Event;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 15, remove in beta 16. Use the Post extender instead.
|
||||
*/
|
||||
class ConfigurePostTypes
|
||||
{
|
||||
private $models;
|
||||
|
@@ -14,11 +14,28 @@ use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Csrf implements ExtenderInterface
|
||||
{
|
||||
protected $csrfExemptPaths = [];
|
||||
protected $csrfExemptRoutes = [];
|
||||
|
||||
/**
|
||||
* Exempt a named route from CSRF checks.
|
||||
*
|
||||
* @param string $routeName
|
||||
*/
|
||||
public function exemptRoute(string $routeName)
|
||||
{
|
||||
$this->csrfExemptRoutes[] = $routeName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exempt a path from csrf checks. Wildcards are supported.
|
||||
*
|
||||
* @deprecated beta 15, remove beta 16. Exempt routes should be used instead.
|
||||
*/
|
||||
public function exemptPath(string $path)
|
||||
{
|
||||
$this->csrfExemptPaths[] = $path;
|
||||
$this->csrfExemptRoutes[] = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -26,7 +43,7 @@ class Csrf implements ExtenderInterface
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->extend('flarum.http.csrfExemptPaths', function ($existingExemptPaths) {
|
||||
return array_merge($existingExemptPaths, $this->csrfExemptPaths);
|
||||
return array_merge($existingExemptPaths, $this->csrfExemptRoutes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ class Event implements ExtenderInterface
|
||||
* - the class attribute of a class with a public `handle` method, which accepts an instance of the event as a parameter
|
||||
*
|
||||
* @param string $event
|
||||
* @param callable $listener
|
||||
* @param callable|string $listener
|
||||
*/
|
||||
public function listen(string $event, $listener)
|
||||
{
|
||||
|
@@ -10,38 +10,93 @@
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Formatter\Event\Configuring;
|
||||
use Flarum\Formatter\Formatter as ActualFormatter;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
|
||||
class Formatter implements ExtenderInterface, LifecycleInterface
|
||||
{
|
||||
private $callback;
|
||||
private $configurationCallbacks = [];
|
||||
private $parsingCallbacks = [];
|
||||
private $renderingCallbacks = [];
|
||||
|
||||
/**
|
||||
* Configure the formatter. This can be used to add support for custom markdown/bbcode/etc tags,
|
||||
* or otherwise change the formatter. Please see documentation for the s9e text formatter library for more
|
||||
* information on how to use this.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or invokable class, and should accept:
|
||||
* - \s9e\TextFormatter\Configurator $configurator
|
||||
*/
|
||||
public function configure($callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
$this->configurationCallbacks[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the system for parsing. This can be used to modify the text that will be parsed, or to modify the parser.
|
||||
* Please note that the text to be parsed must be returned, regardless of whether it's changed.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or invokable class, and should accept:
|
||||
* - \s9e\TextFormatter\Parser $parser
|
||||
* - mixed $context
|
||||
* - string $text: The text to be parsed.
|
||||
*
|
||||
* The callback should return:
|
||||
* - string $text: The text to be parsed.
|
||||
*/
|
||||
public function parse($callback)
|
||||
{
|
||||
$this->parsingCallbacks[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the system for rendering. This can be used to modify the xml that will be rendered, or to modify the renderer.
|
||||
* Please note that the xml to be rendered must be returned, regardless of whether it's changed.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or invokable class, and should accept:
|
||||
* - \s9e\TextFormatter\Rendered $renderer
|
||||
* - mixed $context
|
||||
* - string $xml: The xml to be rendered.
|
||||
* - ServerRequestInterface $request
|
||||
*
|
||||
* The callback should return:
|
||||
* - string $xml: The xml to be rendered.
|
||||
*/
|
||||
public function render($callback)
|
||||
{
|
||||
$this->renderingCallbacks[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$events = $container->make(Dispatcher::class);
|
||||
|
||||
$events->listen(
|
||||
Configuring::class,
|
||||
function (Configuring $event) use ($container) {
|
||||
if (is_string($this->callback)) {
|
||||
$callback = $container->make($this->callback);
|
||||
} else {
|
||||
$callback = $this->callback;
|
||||
}
|
||||
|
||||
$callback($event->configurator);
|
||||
$container->extend('flarum.formatter', function ($formatter, $container) {
|
||||
foreach ($this->configurationCallbacks as $callback) {
|
||||
$formatter->addConfigurationCallback(ContainerUtil::wrapCallback($callback, $container));
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($this->parsingCallbacks as $callback) {
|
||||
$formatter->addParsingCallback(ContainerUtil::wrapCallback($callback, $container));
|
||||
}
|
||||
|
||||
foreach ($this->renderingCallbacks as $callback) {
|
||||
$formatter->addRenderingCallback(ContainerUtil::wrapCallback($callback, $container));
|
||||
}
|
||||
|
||||
return $formatter;
|
||||
});
|
||||
}
|
||||
|
||||
public function onEnable(Container $container, Extension $extension)
|
||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Extend;
|
||||
use Flarum\Extension\Event\Disabled;
|
||||
use Flarum\Extension\Event\Enabled;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Flarum\Foundation\Event\ClearingCache;
|
||||
use Flarum\Frontend\Assets;
|
||||
use Flarum\Frontend\Compiler\Source\SourceCollector;
|
||||
@@ -171,11 +172,7 @@ class Frontend implements ExtenderInterface
|
||||
"flarum.frontend.$this->frontend",
|
||||
function (ActualFrontend $frontend, Container $container) {
|
||||
foreach ($this->content as $content) {
|
||||
if (is_string($content)) {
|
||||
$content = $container->make($content);
|
||||
}
|
||||
|
||||
$frontend->content($content);
|
||||
$frontend->content(ContainerUtil::wrapCallback($content, $container));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -11,12 +11,14 @@ namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Model implements ExtenderInterface
|
||||
{
|
||||
private $modelClass;
|
||||
private $customRelations = [];
|
||||
|
||||
/**
|
||||
* @param string $modelClass The ::class attribute of the model you are modifying.
|
||||
@@ -48,7 +50,9 @@ class Model implements ExtenderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a default value for a given attribute, which can be an explicit value, or a closure.
|
||||
* Add a default value for a given attribute, which can be an explicit value, a closure,
|
||||
* or an instance of an invokable class. Unlike with some other extenders,
|
||||
* it CANNOT be the `::class` attribute of an invokable class.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
@@ -157,7 +161,7 @@ class Model implements ExtenderInterface
|
||||
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
|
||||
* but has to be unique from other relation names for this model, and should
|
||||
* work as the name of a method.
|
||||
* @param callable $callable
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callable can be a closure or invokable class, and should accept:
|
||||
* - $instance: An instance of this model.
|
||||
@@ -168,15 +172,17 @@ class Model implements ExtenderInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function relationship(string $name, callable $callable)
|
||||
public function relationship(string $name, $callback)
|
||||
{
|
||||
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", $callable);
|
||||
$this->customRelations[$name] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
// Nothing needed here.
|
||||
foreach ($this->customRelations as $name => $callback) {
|
||||
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", ContainerUtil::wrapCallback($callback, $container));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
84
src/Extend/ModelUrl.php
Normal file
84
src/Extend/ModelUrl.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ModelUrl implements ExtenderInterface
|
||||
{
|
||||
private $modelClass;
|
||||
private $slugDrivers = [];
|
||||
private $urlGenerator;
|
||||
|
||||
/**
|
||||
* @param string $modelClass The ::class attribute of the model you are modifying.
|
||||
* This model should extend from \Flarum\Database\AbstractModel.
|
||||
*/
|
||||
public function __construct(string $modelClass)
|
||||
{
|
||||
$this->modelClass = $modelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a slug driver.
|
||||
*
|
||||
* @param string $identifier Identifier for slug driver.
|
||||
* @param string $driver ::class attribute of driver class, which must implement Flarum\Http\SlugDriverInterface
|
||||
* @return self
|
||||
*/
|
||||
public function addSlugDriver(string $identifier, string $driver)
|
||||
{
|
||||
$this->slugDrivers[$identifier] = $driver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the url generator for this resource.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
* @return self
|
||||
*
|
||||
* The callable can be a closure or invokable class, and should accept:
|
||||
* - \Flarum\Http\UrlGenerator $urlGenerator: an instance of the URL generator.
|
||||
* - \Flarum\Database\AbstractModel $instance: The model instance for which the url is being generated
|
||||
* - ...$args: Any additional optional arguments
|
||||
*
|
||||
* The callable should return:
|
||||
* - string $url: A valid URL pointing to the resource instance
|
||||
*/
|
||||
public function setUrlGenerator($callback)
|
||||
{
|
||||
$this->urlGenerator = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
if ($this->urlGenerator) {
|
||||
$container->extend('flarum.http.resourceUrlGenerators', function ($existingUrlGenerators) use ($container) {
|
||||
$existingUrlGenerators[$this->modelClass] = ContainerUtil::wrapCallback($this->urlGenerator, $container);
|
||||
|
||||
return $existingUrlGenerators;
|
||||
});
|
||||
}
|
||||
if ($this->slugDrivers) {
|
||||
$container->extend('flarum.http.slugDrivers', function ($existingDrivers) {
|
||||
$existingDrivers[$this->modelClass] = array_merge(Arr::get($existingDrivers, $this->modelClass, []), $this->slugDrivers);
|
||||
|
||||
return $existingDrivers;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
78
src/Extend/Notification.php
Normal file
78
src/Extend/Notification.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Notification implements ExtenderInterface
|
||||
{
|
||||
private $blueprints = [];
|
||||
private $serializers = [];
|
||||
private $drivers = [];
|
||||
private $typesEnabledByDefault = [];
|
||||
|
||||
/**
|
||||
* @param string $blueprint The ::class attribute of the blueprint class.
|
||||
* This blueprint should implement \Flarum\Notification\Blueprint\BlueprintInterface.
|
||||
* @param string $serializer The ::class attribute of the serializer class.
|
||||
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
|
||||
* @param string[] $driversEnabledByDefault The names of the drivers enabled by default for this notification type.
|
||||
* (example: alert, email).
|
||||
* @return self
|
||||
*/
|
||||
public function type(string $blueprint, string $serializer, array $driversEnabledByDefault = [])
|
||||
{
|
||||
$this->blueprints[$blueprint] = $driversEnabledByDefault;
|
||||
$this->serializers[$blueprint::getType()] = $serializer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $driverName The name of the notification driver.
|
||||
* @param string $driver The ::class attribute of the driver class.
|
||||
* This driver should implement \Flarum\Notification\Driver\NotificationDriverInterface.
|
||||
* @param string[] $typesEnabledByDefault The names of blueprint classes of types enabled by default for this driver.
|
||||
* @return self
|
||||
*/
|
||||
public function driver(string $driverName, string $driver, array $typesEnabledByDefault = [])
|
||||
{
|
||||
$this->drivers[$driverName] = $driver;
|
||||
$this->typesEnabledByDefault[$driverName] = $typesEnabledByDefault;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->extend('flarum.notification.blueprints', function ($existingBlueprints) {
|
||||
$existingBlueprints = array_merge($existingBlueprints, $this->blueprints);
|
||||
|
||||
foreach ($this->typesEnabledByDefault as $driverName => $typesEnabledByDefault) {
|
||||
foreach ($typesEnabledByDefault as $blueprintClass) {
|
||||
if (isset($existingBlueprints[$blueprintClass]) && (! in_array($driverName, $existingBlueprints[$blueprintClass]))) {
|
||||
$existingBlueprints[$blueprintClass][] = $driverName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $existingBlueprints;
|
||||
});
|
||||
|
||||
$container->extend('flarum.api.notification_serializers', function ($existingSerializers) {
|
||||
return array_merge($existingSerializers, $this->serializers);
|
||||
});
|
||||
|
||||
$container->extend('flarum.notification.drivers', function ($existingDrivers) {
|
||||
return array_merge($existingDrivers, $this->drivers);
|
||||
});
|
||||
}
|
||||
}
|
39
src/Extend/Post.php
Normal file
39
src/Extend/Post.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Post\Post as PostModel;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Post implements ExtenderInterface
|
||||
{
|
||||
private $postTypes = [];
|
||||
|
||||
/**
|
||||
* Register a new post type. This is generally done for custom 'event posts',
|
||||
* such as those that appear when a discussion is renamed.
|
||||
*
|
||||
* @param string $postType: The ::class attribute of the custom Post type that is being added.
|
||||
*/
|
||||
public function type(string $postType)
|
||||
{
|
||||
$this->postTypes[] = $postType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
foreach ($this->postTypes as $postType) {
|
||||
PostModel::setModel($postType::$type, $postType);
|
||||
}
|
||||
}
|
||||
}
|
40
src/Extend/ServiceProvider.php
Normal file
40
src/Extend/ServiceProvider.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ServiceProvider implements ExtenderInterface
|
||||
{
|
||||
private $providers = [];
|
||||
|
||||
/**
|
||||
* Register a service provider.
|
||||
*
|
||||
* @param string $serviceProviderClass The ::class attribute of the service provider class.
|
||||
* @return self
|
||||
*/
|
||||
public function register(string $serviceProviderClass)
|
||||
{
|
||||
$this->providers[] = $serviceProviderClass;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$app = $container->make('flarum');
|
||||
|
||||
foreach ($this->providers as $provider) {
|
||||
$app->register($provider);
|
||||
}
|
||||
}
|
||||
}
|
@@ -35,7 +35,7 @@ class User implements ExtenderInterface
|
||||
* This can be used to give a user permissions for groups they aren't actually in, based on context.
|
||||
* It will not change the group badges displayed for the user.
|
||||
*
|
||||
* @param callable $callable
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callable can be a closure or invokable class, and should accept:
|
||||
* - \Flarum\User\User $user: the user in question.
|
||||
@@ -44,9 +44,9 @@ class User implements ExtenderInterface
|
||||
* The callable should return:
|
||||
* - array $groupIds: an array of ids for the groups the user belongs to.
|
||||
*/
|
||||
public function permissionGroups(callable $callable)
|
||||
public function permissionGroups($callback)
|
||||
{
|
||||
$this->groupProcessors[] = $callable;
|
||||
$this->groupProcessors[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
55
src/Extend/Validator.php
Normal file
55
src/Extend/Validator.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Validator implements ExtenderInterface
|
||||
{
|
||||
private $configurationCallbacks = [];
|
||||
private $validator;
|
||||
|
||||
/**
|
||||
* @param string $validatorClass: The ::class attribute of the validator you are modifying.
|
||||
* The validator should inherit from \Flarum\Foundation\AbstractValidator.
|
||||
*/
|
||||
public function __construct($validatorClass)
|
||||
{
|
||||
$this->validator = $validatorClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator. This is often used to adjust validation rules, but can be
|
||||
* used to make other changes to the validator as well.
|
||||
*
|
||||
* @param callable $callable
|
||||
*
|
||||
* The callable can be a closure or invokable class, and should accept:
|
||||
* - \Flarum\Foundation\AbstractValidator $flarumValidator: The Flarum validator wrapper
|
||||
* - \Illuminate\Validation\Validator $validator: The Laravel validator instance
|
||||
*/
|
||||
public function configure($callback)
|
||||
{
|
||||
$this->configurationCallbacks[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->resolving($this->validator, function ($validator, $container) {
|
||||
foreach ($this->configurationCallbacks as $callback) {
|
||||
$validator->addConfiguration(ContainerUtil::wrapCallback($callback, $container));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -11,6 +11,9 @@ namespace Flarum\Formatter\Event;
|
||||
|
||||
use s9e\TextFormatter\Configurator;
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
|
||||
*/
|
||||
class Configuring
|
||||
{
|
||||
/**
|
||||
|
@@ -11,6 +11,9 @@ namespace Flarum\Formatter\Event;
|
||||
|
||||
use s9e\TextFormatter\Parser;
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
|
||||
*/
|
||||
class Parsing
|
||||
{
|
||||
/**
|
||||
|
@@ -12,6 +12,9 @@ namespace Flarum\Formatter\Event;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use s9e\TextFormatter\Renderer;
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
|
||||
*/
|
||||
class Rendering
|
||||
{
|
||||
/**
|
||||
|
@@ -20,6 +20,12 @@ use s9e\TextFormatter\Unparser;
|
||||
|
||||
class Formatter
|
||||
{
|
||||
protected $configurationCallbacks = [];
|
||||
|
||||
protected $parsingCallbacks = [];
|
||||
|
||||
protected $renderingCallbacks = [];
|
||||
|
||||
/**
|
||||
* @var Repository
|
||||
*/
|
||||
@@ -47,6 +53,21 @@ class Formatter
|
||||
$this->cacheDir = $cacheDir;
|
||||
}
|
||||
|
||||
public function addConfigurationCallback($callback)
|
||||
{
|
||||
$this->configurationCallbacks[] = $callback;
|
||||
}
|
||||
|
||||
public function addParsingCallback($callback)
|
||||
{
|
||||
$this->parsingCallbacks[] = $callback;
|
||||
}
|
||||
|
||||
public function addRenderingCallback($callback)
|
||||
{
|
||||
$this->renderingCallbacks[] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse text.
|
||||
*
|
||||
@@ -58,8 +79,13 @@ class Formatter
|
||||
{
|
||||
$parser = $this->getParser($context);
|
||||
|
||||
// Deprecated in beta 15, remove in beta 16
|
||||
$this->events->dispatch(new Parsing($parser, $context, $text));
|
||||
|
||||
foreach ($this->parsingCallbacks as $callback) {
|
||||
$text = $callback($parser, $context, $text);
|
||||
}
|
||||
|
||||
return $parser->parse($text);
|
||||
}
|
||||
|
||||
@@ -75,8 +101,13 @@ class Formatter
|
||||
{
|
||||
$renderer = $this->getRenderer();
|
||||
|
||||
// Deprecated in beta 15, remove in beta 16
|
||||
$this->events->dispatch(new Rendering($renderer, $context, $xml, $request));
|
||||
|
||||
foreach ($this->renderingCallbacks as $callback) {
|
||||
$xml = $callback($renderer, $context, $xml, $request);
|
||||
}
|
||||
|
||||
return $renderer->render($xml);
|
||||
}
|
||||
|
||||
@@ -122,8 +153,13 @@ class Formatter
|
||||
$configurator->Autolink;
|
||||
$configurator->tags->onDuplicate('replace');
|
||||
|
||||
// Deprecated in beta 15, remove in beta 16
|
||||
$this->events->dispatch(new Configuring($configurator));
|
||||
|
||||
foreach ($this->configurationCallbacks as $callback) {
|
||||
$callback($configurator);
|
||||
}
|
||||
|
||||
$this->configureExternalLinks($configurator);
|
||||
|
||||
return $configurator;
|
||||
|
@@ -74,9 +74,7 @@ class Discussion
|
||||
unset($newQueryParams['id']);
|
||||
$queryString = http_build_query($newQueryParams);
|
||||
|
||||
$idWithSlug = $apiDocument->data->id.(trim($apiDocument->data->attributes->slug) ? '-'.$apiDocument->data->attributes->slug : '');
|
||||
|
||||
return $this->url->to('forum')->route('discussion', ['id' => $idWithSlug]).
|
||||
return $this->url->to('forum')->route('discussion', ['id' => $apiDocument->data->attributes->slug]).
|
||||
($queryString ? '?'.$queryString : '');
|
||||
};
|
||||
|
||||
@@ -106,6 +104,7 @@ class Discussion
|
||||
*/
|
||||
protected function getApiDocument(User $actor, array $params)
|
||||
{
|
||||
$params['bySlug'] = true;
|
||||
$response = $this->api->send('Flarum\Api\Controller\ShowDiscussionController', $actor, $params);
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
|
@@ -54,7 +54,7 @@ class User
|
||||
$user = $apiDocument->data->attributes;
|
||||
|
||||
$document->title = $user->displayName;
|
||||
$document->canonicalUrl = $this->url->to('forum')->route('user', ['username' => $user->username]);
|
||||
$document->canonicalUrl = $this->url->to('forum')->route('user', ['username' => $user->slug]);
|
||||
$document->payload['apiDocument'] = $apiDocument;
|
||||
|
||||
return $document;
|
||||
@@ -70,6 +70,7 @@ class User
|
||||
*/
|
||||
protected function getApiDocument(FlarumUser $actor, array $params)
|
||||
{
|
||||
$params['bySlug'] = true;
|
||||
$response = $this->api->send(ShowUserController::class, $actor, $params);
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
|
@@ -64,8 +64,9 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
HttpMiddleware\StartSession::class,
|
||||
HttpMiddleware\RememberFromCookie::class,
|
||||
HttpMiddleware\AuthenticateWithSession::class,
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
HttpMiddleware\SetLocale::class,
|
||||
'flarum.forum.route_resolver',
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
HttpMiddleware\ShareErrorsFromSession::class
|
||||
];
|
||||
});
|
||||
@@ -78,6 +79,10 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.forum.route_resolver', function () {
|
||||
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.forum.routes'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.forum.handler', function () {
|
||||
$pipe = new MiddlewarePipe;
|
||||
|
||||
@@ -85,7 +90,7 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
$pipe->pipe($this->app->make($middleware));
|
||||
}
|
||||
|
||||
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.forum.routes')));
|
||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||
|
||||
return $pipe;
|
||||
});
|
||||
@@ -198,8 +203,8 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
$factory = $this->app->make(RouteHandlerFactory::class);
|
||||
$defaultRoute = $this->app->make('flarum.settings')->get('default_route');
|
||||
|
||||
if (isset($routes->getRouteData()[0]['GET'][$defaultRoute])) {
|
||||
$toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute];
|
||||
if (isset($routes->getRouteData()[0]['GET'][$defaultRoute]['handler'])) {
|
||||
$toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute]['handler'];
|
||||
} else {
|
||||
$toDefaultController = $factory->toForum(Content\Index::class);
|
||||
}
|
||||
|
@@ -18,6 +18,16 @@ use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractValidator
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $configuration = [];
|
||||
|
||||
public function addConfiguration($callable)
|
||||
{
|
||||
$this->configuration[] = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@@ -92,10 +102,17 @@ abstract class AbstractValidator
|
||||
|
||||
$validator = $this->validator->make($attributes, $rules, $this->getMessages());
|
||||
|
||||
/**
|
||||
* @deprecated in beta 15, removed in beta 16.
|
||||
*/
|
||||
$this->events->dispatch(
|
||||
new Validating($this, $validator)
|
||||
);
|
||||
|
||||
foreach ($this->configuration as $callable) {
|
||||
$callable($this, $validator);
|
||||
}
|
||||
|
||||
return $validator;
|
||||
}
|
||||
}
|
||||
|
36
src/Foundation/ContainerUtil.php
Normal file
36
src/Foundation/ContainerUtil.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Foundation;
|
||||
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ContainerUtil
|
||||
{
|
||||
/**
|
||||
* Wraps a callback so that string-based invokable classes get resolved only when actually used.
|
||||
*
|
||||
* @internal Backwards compatability not guaranteed.
|
||||
*
|
||||
* @param callable|string $callback: A callable, or a ::class attribute of an invokable class
|
||||
* @param Container $container
|
||||
*/
|
||||
public static function wrapCallback($callback, Container $container)
|
||||
{
|
||||
if (is_string($callback)) {
|
||||
$callback = function () use ($container, $callback) {
|
||||
$callback = $container->make($callback);
|
||||
|
||||
return call_user_func_array($callback, func_get_args());
|
||||
};
|
||||
}
|
||||
|
||||
return $callback;
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ use Flarum\Foundation\AbstractValidator;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
/**
|
||||
* @deprecated in Beta 15, remove in beta 16. Use the Validator extender instead.
|
||||
* The `Validating` event is called when a validator instance for a
|
||||
* model is being built. This event can be used to add custom rules/extensions
|
||||
* to the validator for when validation takes place.
|
||||
|
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace Flarum\Foundation;
|
||||
|
||||
use Flarum\Http\Middleware\DispatchRoute;
|
||||
use Flarum\Http\Middleware as HttpMiddleware;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
@@ -85,8 +85,9 @@ class InstalledApp implements AppInterface
|
||||
$pipe = new MiddlewarePipe;
|
||||
$pipe->pipe(new BasePath($this->basePath()));
|
||||
$pipe->pipe(
|
||||
new DispatchRoute($this->container->make('flarum.update.routes'))
|
||||
new HttpMiddleware\ResolveRoute($this->container->make('flarum.update.routes'))
|
||||
);
|
||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||
|
||||
return $pipe;
|
||||
}
|
||||
|
@@ -9,7 +9,14 @@
|
||||
|
||||
namespace Flarum\Http;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\IdWithTransliteratedSlugDriver;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
use Flarum\User\UsernameSlugDriver;
|
||||
|
||||
class HttpServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@@ -19,11 +26,71 @@ class HttpServiceProvider extends AbstractServiceProvider
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('flarum.http.csrfExemptPaths', function () {
|
||||
return ['/api/token'];
|
||||
return ['token'];
|
||||
});
|
||||
|
||||
$this->app->bind(Middleware\CheckCsrfToken::class, function ($app) {
|
||||
return new Middleware\CheckCsrfToken($app->make('flarum.http.csrfExemptPaths'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.http.slugDrivers', function () {
|
||||
return [
|
||||
Discussion::class => [
|
||||
'default' => IdWithTransliteratedSlugDriver::class
|
||||
],
|
||||
User::class => [
|
||||
'default' => UsernameSlugDriver::class
|
||||
],
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.http.selectedSlugDrivers', function () {
|
||||
$settings = $this->app->make(SettingsRepositoryInterface::class);
|
||||
|
||||
$compiledDrivers = [];
|
||||
|
||||
foreach ($this->app->make('flarum.http.slugDrivers') as $resourceClass => $resourceDrivers) {
|
||||
$driverKey = $settings->get("slug_driver_$resourceClass", 'default');
|
||||
|
||||
$driverClass = array_key_exists($driverKey, $resourceDrivers) ? $resourceDrivers[$driverKey] : $resourceDrivers['default'];
|
||||
|
||||
$compiledDrivers[$resourceClass] = $this->app->make($driverClass);
|
||||
}
|
||||
|
||||
return $compiledDrivers;
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.http.resourceUrlGenerators', function () {
|
||||
$slugManager = $this->app->make(SlugManager::class);
|
||||
|
||||
return [
|
||||
Discussion::class => function (UrlGenerator $urlGenerator, Discussion $discussion) use ($slugManager) {
|
||||
return $urlGenerator->to('forum')->route('discussion', [
|
||||
'id' => $slugManager->forResource(Discussion::class)->toSlug($discussion)
|
||||
]);
|
||||
},
|
||||
Post::class => function (UrlGenerator $urlGenerator, Post $post) use ($slugManager) {
|
||||
return $urlGenerator->to('forum')->route('post', [
|
||||
'id' => $slugManager->forResource(Discussion::class)->toSlug($post->discussion),
|
||||
'near' => $post->id,
|
||||
]);
|
||||
},
|
||||
User::class => function (UrlGenerator $urlGenerator, User $user) use ($slugManager) {
|
||||
return $urlGenerator->to('forum')->route('user', [
|
||||
'username' => $slugManager->forResource(User::class)->toSlug($user)
|
||||
]);
|
||||
},
|
||||
];
|
||||
});
|
||||
$this->app->bind(SlugManager::class, function () {
|
||||
return new SlugManager($this->app->make('flarum.http.selectedSlugDrivers'));
|
||||
});
|
||||
|
||||
$this->app->bind(UrlGenerator::class, function () {
|
||||
return new UrlGenerator(
|
||||
$this->app->make(Application::class),
|
||||
$this->app->make('flarum.http.resourceUrlGenerators')
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -28,7 +28,10 @@ class CheckCsrfToken implements Middleware
|
||||
{
|
||||
$path = $request->getAttribute('originalUri')->getPath();
|
||||
foreach ($this->exemptRoutes as $exemptRoute) {
|
||||
if (fnmatch($exemptRoute, $path)) {
|
||||
/**
|
||||
* @deprecated path match should be removed in beta 16, only route name match should be supported.
|
||||
*/
|
||||
if ($exemptRoute === $request->getAttribute('routeName') || fnmatch($exemptRoute, $path)) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
29
src/Http/Middleware/ExecuteRoute.php
Normal file
29
src/Http/Middleware/ExecuteRoute.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Http\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||
use Psr\Http\Server\RequestHandlerInterface as Handler;
|
||||
|
||||
class ExecuteRoute implements Middleware
|
||||
{
|
||||
/**
|
||||
* Executes the route handler resolved in ResolveRoute.
|
||||
*/
|
||||
public function process(Request $request, Handler $handler): Response
|
||||
{
|
||||
$handler = $request->getAttribute('routeHandler');
|
||||
$parameters = $request->getAttribute('routeParameters');
|
||||
|
||||
return $handler($request, $parameters);
|
||||
}
|
||||
}
|
@@ -18,7 +18,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||
use Psr\Http\Server\RequestHandlerInterface as Handler;
|
||||
|
||||
class DispatchRoute implements Middleware
|
||||
class ResolveRoute implements Middleware
|
||||
{
|
||||
/**
|
||||
* @var RouteCollection
|
||||
@@ -41,7 +41,7 @@ class DispatchRoute implements Middleware
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given request to our route collection.
|
||||
* Resolve the given request from our route collection.
|
||||
*
|
||||
* @throws MethodNotAllowedException
|
||||
* @throws RouteNotFoundException
|
||||
@@ -59,10 +59,12 @@ class DispatchRoute implements Middleware
|
||||
case Dispatcher::METHOD_NOT_ALLOWED:
|
||||
throw new MethodNotAllowedException($method);
|
||||
case Dispatcher::FOUND:
|
||||
$handler = $routeInfo[1];
|
||||
$parameters = $routeInfo[2];
|
||||
$request = $request
|
||||
->withAttribute('routeName', $routeInfo[1]['name'])
|
||||
->withAttribute('routeHandler', $routeInfo[1]['handler'])
|
||||
->withAttribute('routeParameters', $routeInfo[2]);
|
||||
|
||||
return $handler($request, $parameters);
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
@@ -66,7 +66,7 @@ class RouteCollection
|
||||
$routeDatas = $this->routeParser->parse($path);
|
||||
|
||||
foreach ($routeDatas as $routeData) {
|
||||
$this->dataGenerator->addRoute($method, $routeData, $handler);
|
||||
$this->dataGenerator->addRoute($method, $routeData, ['name' => $name, 'handler' => $handler]);
|
||||
}
|
||||
|
||||
$this->reverse[$name] = $routeDatas;
|
||||
|
20
src/Http/SlugDriverInterface.php
Normal file
20
src/Http/SlugDriverInterface.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Http;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\User\User;
|
||||
|
||||
interface SlugDriverInterface
|
||||
{
|
||||
public function toSlug(AbstractModel $instance): string;
|
||||
|
||||
public function fromSlug(string $slug, User $actor): AbstractModel;
|
||||
}
|
27
src/Http/SlugManager.php
Normal file
27
src/Http/SlugManager.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Http;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class SlugManager
|
||||
{
|
||||
protected $drivers = [];
|
||||
|
||||
public function __construct(array $drivers)
|
||||
{
|
||||
$this->drivers = $drivers;
|
||||
}
|
||||
|
||||
public function forResource(string $resourceName): SlugDriverInterface
|
||||
{
|
||||
return Arr::get($this->drivers, $resourceName, null);
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Http;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Foundation\Application;
|
||||
|
||||
class UrlGenerator
|
||||
@@ -23,12 +24,18 @@ class UrlGenerator
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $resourceUrlGenerators;
|
||||
|
||||
/**
|
||||
* @param Application $app
|
||||
*/
|
||||
public function __construct(Application $app)
|
||||
public function __construct(Application $app, array $resourceUrlGenerators)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->resourceUrlGenerators = $resourceUrlGenerators;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,4 +66,22 @@ class UrlGenerator
|
||||
{
|
||||
return $this->routes[$collection];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL to an instance of a resource.
|
||||
*
|
||||
* @param AbstractModel $instance
|
||||
* @param $args
|
||||
* @return string
|
||||
*/
|
||||
public function toResource(AbstractModel $instance, ...$args): string
|
||||
{
|
||||
foreach (array_merge(array_reverse(class_parents($instance)), [get_class($instance)]) as $class) {
|
||||
$callback = $this->resourceUrlGenerators[$class];
|
||||
|
||||
if ($callback) {
|
||||
return $callback($instance, ...$args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,9 +13,7 @@ use Flarum\Foundation\AppInterface;
|
||||
use Flarum\Foundation\ErrorHandling\Registry;
|
||||
use Flarum\Foundation\ErrorHandling\Reporter;
|
||||
use Flarum\Foundation\ErrorHandling\WhoopsFormatter;
|
||||
use Flarum\Http\Middleware\DispatchRoute;
|
||||
use Flarum\Http\Middleware\HandleErrors;
|
||||
use Flarum\Http\Middleware\StartSession;
|
||||
use Flarum\Http\Middleware as HttpMiddleware;
|
||||
use Flarum\Install\Console\InstallCommand;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Laminas\Stratigility\MiddlewarePipe;
|
||||
@@ -38,15 +36,16 @@ class Installer implements AppInterface
|
||||
public function getRequestHandler()
|
||||
{
|
||||
$pipe = new MiddlewarePipe;
|
||||
$pipe->pipe(new HandleErrors(
|
||||
$pipe->pipe(new HttpMiddleware\HandleErrors(
|
||||
$this->container->make(Registry::class),
|
||||
$this->container->make(WhoopsFormatter::class),
|
||||
$this->container->tagged(Reporter::class)
|
||||
));
|
||||
$pipe->pipe($this->container->make(StartSession::class));
|
||||
$pipe->pipe($this->container->make(HttpMiddleware\StartSession::class));
|
||||
$pipe->pipe(
|
||||
new DispatchRoute($this->container->make('flarum.install.routes'))
|
||||
new HttpMiddleware\ResolveRoute($this->container->make('flarum.install.routes'))
|
||||
);
|
||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||
|
||||
return $pipe;
|
||||
}
|
||||
|
@@ -53,7 +53,7 @@ class WritablePaths implements PrerequisiteInterface
|
||||
})->map(function ($path, $index) {
|
||||
return [
|
||||
'message' => 'The '.$this->getAbsolutePath($path).' directory is not writable.',
|
||||
'detail' => 'Please chmod this directory'.(in_array($index, $this->wildcards) ? ' and its contents' : '').' to 0775.'
|
||||
'detail' => 'Please make sure your web server/PHP user has write access to this directory'.(in_array($index, $this->wildcards) ? ' and its contents' : '').'. Read the <a href="https://docs.flarum.org/install.html#folder-ownership">installation documentation</a> for a detailed explanation and steps to resolve this error.'
|
||||
];
|
||||
});
|
||||
}
|
||||
|
50
src/Notification/Driver/AlertNotificationDriver.php
Normal file
50
src/Notification/Driver/AlertNotificationDriver.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Notification\Driver;
|
||||
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\Notification\Job\SendNotificationsJob;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
|
||||
class AlertNotificationDriver implements NotificationDriverInterface
|
||||
{
|
||||
/**
|
||||
* @var Queue
|
||||
*/
|
||||
private $queue;
|
||||
|
||||
public function __construct(Queue $queue)
|
||||
{
|
||||
$this->queue = $queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function send(BlueprintInterface $blueprint, array $users): void
|
||||
{
|
||||
if (count($users)) {
|
||||
$this->queue->push(new SendNotificationsJob($blueprint, $users));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
|
||||
{
|
||||
User::addPreference(
|
||||
User::getNotificationPreferenceKey($blueprintClass::getType(), 'alert'),
|
||||
'boolval',
|
||||
in_array('alert', $driversEnabledByDefault)
|
||||
);
|
||||
}
|
||||
}
|
69
src/Notification/Driver/EmailNotificationDriver.php
Normal file
69
src/Notification/Driver/EmailNotificationDriver.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Notification\Driver;
|
||||
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\Notification\Job\SendEmailNotificationJob;
|
||||
use Flarum\Notification\MailableInterface;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
use ReflectionClass;
|
||||
|
||||
class EmailNotificationDriver implements NotificationDriverInterface
|
||||
{
|
||||
/**
|
||||
* @var Queue
|
||||
*/
|
||||
private $queue;
|
||||
|
||||
public function __construct(Queue $queue)
|
||||
{
|
||||
$this->queue = $queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function send(BlueprintInterface $blueprint, array $users): void
|
||||
{
|
||||
if ($blueprint instanceof MailableInterface) {
|
||||
$this->mailNotifications($blueprint, $users);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mail a notification to a list of users.
|
||||
*
|
||||
* @param MailableInterface $blueprint
|
||||
* @param User[] $recipients
|
||||
*/
|
||||
protected function mailNotifications(MailableInterface $blueprint, array $recipients)
|
||||
{
|
||||
foreach ($recipients as $user) {
|
||||
if ($user->shouldEmail($blueprint::getType())) {
|
||||
$this->queue->push(new SendEmailNotificationJob($blueprint, $user));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
|
||||
{
|
||||
if ((new ReflectionClass($blueprintClass))->implementsInterface(MailableInterface::class)) {
|
||||
User::addPreference(
|
||||
User::getNotificationPreferenceKey($blueprintClass::getType(), 'email'),
|
||||
'boolval',
|
||||
in_array('email', $driversEnabledByDefault)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
34
src/Notification/Driver/NotificationDriverInterface.php
Normal file
34
src/Notification/Driver/NotificationDriverInterface.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Notification\Driver;
|
||||
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
interface NotificationDriverInterface
|
||||
{
|
||||
/**
|
||||
* Conditionally sends a notification to users, generally using a queue.
|
||||
*
|
||||
* @param BlueprintInterface $blueprint
|
||||
* @param User[] $users
|
||||
* @return void
|
||||
*/
|
||||
public function send(BlueprintInterface $blueprint, array $users): void;
|
||||
|
||||
/**
|
||||
* Logic for registering a notification type, generally used for adding a user preference.
|
||||
*
|
||||
* @param string $blueprintClass
|
||||
* @param array $driversEnabledByDefault
|
||||
* @return void
|
||||
*/
|
||||
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void;
|
||||
}
|
@@ -11,6 +11,9 @@ namespace Flarum\Notification\Event;
|
||||
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class Sending
|
||||
{
|
||||
/**
|
||||
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Notification\Job;
|
||||
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\Notification\Event\Sending;
|
||||
use Flarum\Notification\Notification;
|
||||
use Flarum\Queue\AbstractJob;
|
||||
use Flarum\User\User;
|
||||
@@ -35,8 +34,6 @@ class SendNotificationsJob extends AbstractJob
|
||||
|
||||
public function handle()
|
||||
{
|
||||
event(new Sending($this->blueprint, $this->recipients));
|
||||
|
||||
Notification::notify($this->recipients, $this->blueprint);
|
||||
}
|
||||
}
|
||||
|
@@ -12,51 +12,76 @@ namespace Flarum\Notification;
|
||||
use Flarum\Event\ConfigureNotificationTypes;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Notification\Blueprint\DiscussionRenamedBlueprint;
|
||||
use Flarum\User\User;
|
||||
use ReflectionClass;
|
||||
|
||||
class NotificationServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('flarum.notification.drivers', function () {
|
||||
return [
|
||||
'alert' => Driver\AlertNotificationDriver::class,
|
||||
'email' => Driver\EmailNotificationDriver::class,
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.notification.blueprints', function () {
|
||||
return [
|
||||
DiscussionRenamedBlueprint::class => ['alert']
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->registerNotificationTypes();
|
||||
$this->setNotificationDrivers();
|
||||
$this->setNotificationTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register notification drivers.
|
||||
*/
|
||||
protected function setNotificationDrivers()
|
||||
{
|
||||
foreach ($this->app->make('flarum.notification.drivers') as $driverName => $driver) {
|
||||
NotificationSyncer::addNotificationDriver($driverName, $this->app->make($driver));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register notification types.
|
||||
*/
|
||||
public function registerNotificationTypes()
|
||||
protected function setNotificationTypes()
|
||||
{
|
||||
$blueprints = [
|
||||
DiscussionRenamedBlueprint::class => ['alert']
|
||||
];
|
||||
$blueprints = $this->app->make('flarum.notification.blueprints');
|
||||
|
||||
// Deprecated in beta 15, remove in beta 16
|
||||
$this->app->make('events')->dispatch(
|
||||
new ConfigureNotificationTypes($blueprints)
|
||||
);
|
||||
|
||||
foreach ($blueprints as $blueprint => $enabled) {
|
||||
Notification::setSubjectModel(
|
||||
$type = $blueprint::getType(),
|
||||
$blueprint::getSubjectModel()
|
||||
);
|
||||
foreach ($blueprints as $blueprint => $driversEnabledByDefault) {
|
||||
$this->addType($blueprint, $driversEnabledByDefault);
|
||||
}
|
||||
}
|
||||
|
||||
User::addPreference(
|
||||
User::getNotificationPreferenceKey($type, 'alert'),
|
||||
'boolval',
|
||||
in_array('alert', $enabled)
|
||||
);
|
||||
protected function addType(string $blueprint, array $driversEnabledByDefault)
|
||||
{
|
||||
Notification::setSubjectModel(
|
||||
$type = $blueprint::getType(),
|
||||
$blueprint::getSubjectModel()
|
||||
);
|
||||
|
||||
if ((new ReflectionClass($blueprint))->implementsInterface(MailableInterface::class)) {
|
||||
User::addPreference(
|
||||
User::getNotificationPreferenceKey($type, 'email'),
|
||||
'boolval',
|
||||
in_array('email', $enabled)
|
||||
);
|
||||
}
|
||||
foreach (NotificationSyncer::getNotificationDrivers() as $driverName => $driver) {
|
||||
$driver->registerType(
|
||||
$blueprint,
|
||||
$driversEnabledByDefault
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user