1
0
mirror of https://github.com/flarum/core.git synced 2025-08-15 04:44:08 +02:00

Compare commits

...

42 Commits

Author SHA1 Message Date
David Wheatley
55050914f3 Use FA icons in info box 2021-08-11 21:49:35 +02:00
David Wheatley
961390da46 Add frontend code 2021-08-11 21:42:25 +02:00
Daniel Klabbers
60300939bc wip 2021-08-11 16:52:25 +02:00
Daniel Klabbers
c1754af74a try to patch up the advanced page 2021-08-11 16:52:25 +02:00
Daniel Klabbers
731fae666f wip 2021-08-11 16:51:48 +02:00
Daniel Klabbers
6006ad00a2 wip 2021-08-11 16:51:48 +02:00
Daniel Klabbers
7cd67720d3 wip 2021-08-11 16:51:48 +02:00
luceos
65a5ed4e86 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-08-11 16:51:48 +02:00
Daniel Klabbers
72780f514f further additions for queue handling with db and a advanced pane 2021-08-11 16:51:48 +02:00
Daniel Klabbers
93dbd4ec86 rename vars in use 2021-08-11 16:51:48 +02:00
luceos
df2c4323ff Apply fixes from StyleCI
[ci skip] [skip ci]
2021-08-11 16:48:06 +02:00
Daniel Klabbers
06e5922be5 wip 2021-08-11 16:48:06 +02:00
flarum-bot
2dd9e17568 Bundled output for commit 13d302b650
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-08-10 13:54:32 +00:00
Ornanovitch
13d302b650 make user.editGroups depending on viewHiddenGroups (#2880)
should resolve #2610
2021-08-10 14:52:34 +01:00
luceos
9490b3dc32 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-07-31 12:34:23 +02:00
Daniel Klabbers
a26f279e0f use construct binding for ioc dependencies 2021-07-31 12:34:23 +02:00
luceos
ef3d4ca018 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-07-31 12:34:23 +02:00
Daniel Klabbers
c449ea211a added mysql version, queue and mail driver 2021-07-31 12:34:23 +02:00
David Wheatley
ce824b0ccf Use organization Prettier config (#2967)
* Use organization Prettier config

* Bump version to 1.0.0

* Update workflow

* Use npm ci and package.json script
2021-07-30 12:18:20 +01:00
flarum-bot
34803f4b43 Bundled output for commit 81e6b17f83
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-07-26 11:04:48 +00:00
SychO9
81e6b17f83 npm run format 2021-07-26 13:03:09 +02:00
David Wheatley
f949b0a28e Remove class from text input 2021-07-26 13:03:09 +02:00
David Wheatley
66064ca9be Remove class from Mail Select setting component 2021-07-26 13:03:09 +02:00
David Wheatley
f9fc78a10d Prevent class attrs overriding default Select classes 2021-07-26 13:03:09 +02:00
David
e195ca27a8 Fix Select-based setting breaking admin pages 2021-07-26 13:03:09 +02:00
Daniël Klabbers
61624d1533 Update composer.json 2021-07-26 13:02:33 +02:00
Sami Mazouz
d31690e7f5 Avoid intervention/image 2.6.0 2021-07-26 13:02:33 +02:00
Sami Mazouz
2bed1d8038 Revert "Avoid using intervention/image 2.6.0"
This reverts commit 8a7fd66919.
2021-07-26 13:02:33 +02:00
Sami Mazouz
0ce6a1ea9a Revert "Use wildcard for constraint instead"
This reverts commit 4bcfc5078c.
2021-07-26 13:02:33 +02:00
Sami Mazouz
4bcfc5078c Use wildcard for constraint instead
Co-authored-by: Daniël Klabbers <luceos@users.noreply.github.com>
2021-07-21 10:41:53 +02:00
SychO9
8a7fd66919 Avoid using intervention/image 2.6.0 2021-07-21 10:41:53 +02:00
flarum-bot
ac0e98e721 Bundled output for commit 5a1948c4fc
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-07-14 14:15:44 +00:00
David Wheatley
5a1948c4fc Add fix for broken type hinting on class components (#2962) 2021-07-14 15:13:57 +01:00
flarum-bot
9ff1a42396 Bundled output for commit 3130e3de5e
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-07-13 12:44:21 +00:00
David Wheatley
3130e3de5e Allow extra attrs provided to <Select> to be passed through to the DOM element (#2959)
* Allow extra attrs provided to `<Select>` to be passed through to the DOM element

* Allow direct passing attrs to the Select wrapper

* Format
2021-07-13 13:42:46 +01:00
David Wheatley
da20d75e3c Hide post footer when empty (#2926)
* Add `Post-footer--empty` class if the post footer contains no items

* Hide post footer when it has class `Post-footer--empty`

* Swap to `:empty` pseudoselector

* Prefer ternary operator

* Fix typo
2021-07-13 13:42:19 +01:00
Daniel Klabbers
7a0df21c5a prevent a couple of cycles by not resolving the excluded middleware on each middleware items 2021-07-13 00:44:27 +02:00
Daniel Klabbers
7d4d3d977b fixes internal clients use of session
With remember from cookie, in certain edge cases, the middleware would
try to load a session which hasn't been instantiated as this middleware
is excluded for the client. Excluding the remember from cookie
middleware will resolve this as authentication is done using the
RequestUtil and ActorReference regardlessly.
2021-07-13 00:32:13 +02:00
David Wheatley
408bb38cc0 Update code block styling to match HLJS 11's new styles (#2909) 2021-07-09 10:04:12 +01:00
flarum-bot
b7cb1e8d36 Bundled output for commit 42dabea81f
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-07-05 15:37:15 +00:00
Lucas Henrique
42dabea81f Move Day.js plugin types import to global typings (#2954) 2021-07-05 16:35:37 +01:00
Daniel Klabbers
a077ae9ca3 set version constant for 1.0.5-dev 2021-06-28 12:26:15 +02:00
38 changed files with 610 additions and 76 deletions

View File

@@ -23,6 +23,10 @@ jobs:
with:
node-version: "14"
- name: Check JS formatting
run: npx prettier --check src
- name: Install JS dependencies
run: npm ci
working-directory: ./js
- name: Check JS formatting
run: npm run format-check
working-directory: ./js

View File

@@ -59,7 +59,7 @@
"illuminate/support": "^8.0",
"illuminate/validation": "^8.0",
"illuminate/view": "^8.0",
"intervention/image": "^2.5.0",
"intervention/image": "2.5.* || ^2.6.1",
"laminas/laminas-diactoros": "^2.4.1",
"laminas/laminas-httphandlerrunner": "^1.2.0",
"laminas/laminas-stratigility": "^3.2.2",

View File

@@ -1,6 +0,0 @@
{
"printWidth": 150,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -3,6 +3,7 @@ import Mithril from 'mithril';
// Other third-party libs
import * as _dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
import * as _$ from 'jquery';
// Globals from flarum/core
@@ -29,6 +30,19 @@ declare global {
interface JQuery {
tooltip: TooltipJQueryFunction;
}
/**
* For more info, see: https://www.typescriptlang.org/docs/handbook/jsx.html#attribute-type-checking
*
* In a nutshell, we need to add `ElementAttributesProperty` to tell Typescript
* what property on component classes to look at for attribute typings. For our
* Component class, this would be `attrs` (e.g. `this.attrs...`)
*/
namespace JSX {
interface ElementAttributesProperty {
attrs: Record<string, unknown>;
}
}
}
/**

View File

@@ -3,6 +3,7 @@ import Mithril from 'mithril';
// Other third-party libs
import * as _dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
import * as _$ from 'jquery';
// Globals from flarum/core
@@ -29,6 +30,19 @@ declare global {
interface JQuery {
tooltip: TooltipJQueryFunction;
}
/**
* For more info, see: https://www.typescriptlang.org/docs/handbook/jsx.html#attribute-type-checking
*
* In a nutshell, we need to add `ElementAttributesProperty` to tell Typescript
* what property on component classes to look at for attribute typings. For our
* Component class, this would be `attrs` (e.g. `this.attrs...`)
*/
namespace JSX {
interface ElementAttributesProperty {
attrs: Record<string, unknown>;
}
}
}
/**

View File

@@ -6,6 +6,9 @@
* - `onchange` A callback to run when the selected value is changed.
* - `value` The value of the selected option.
* - `disabled` Disabled state for the input.
* - `wrapperAttrs` A map of attrs to be passed to the DOM element wrapping the `<select>`
*
* Other attributes are passed directly to the `<select>` element rendered to the DOM.
*/
export default class Select extends Component<import("../Component").ComponentAttrs> {
constructor();

View File

@@ -1,4 +1,3 @@
import 'dayjs/plugin/relativeTime';
/**
* The `humanTime` utility converts a date to a localized, human-readable time-
* ago string.

6
js/dist/admin.js generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/admin.js.map generated vendored

File diff suppressed because one or more lines are too long

6
js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/forum.js.map generated vendored

File diff suppressed because one or more lines are too long

13
js/package-lock.json generated
View File

@@ -22,6 +22,7 @@
},
"devDependencies": {
"@babel/preset-typescript": "^7.13.0",
"@flarum/prettier-config": "^1.0.0",
"@types/jquery": "^3.5.5",
"@types/mithril": "^2.0.7",
"@types/punycode": "^2.1.0",
@@ -1477,6 +1478,12 @@
"to-fast-properties": "^2.0.0"
}
},
"node_modules/@flarum/prettier-config": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@flarum/prettier-config/-/prettier-config-1.0.0.tgz",
"integrity": "sha512-3/AcliIi5jPt4i7COb5hsLv6hm4EeXT9yI9I2EuEvhPi2QR+O9Y/8wrqRuO5mDkRzCIhUY+mjIL/f9770Zwfqg==",
"dev": true
},
"node_modules/@polka/url": {
"version": "1.0.0-next.12",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
@@ -8870,6 +8877,12 @@
"to-fast-properties": "^2.0.0"
}
},
"@flarum/prettier-config": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@flarum/prettier-config/-/prettier-config-1.0.0.tgz",
"integrity": "sha512-3/AcliIi5jPt4i7COb5hsLv6hm4EeXT9yI9I2EuEvhPi2QR+O9Y/8wrqRuO5mDkRzCIhUY+mjIL/f9770Zwfqg==",
"dev": true
},
"@polka/url": {
"version": "1.0.0-next.12",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",

View File

@@ -1,6 +1,7 @@
{
"private": true,
"name": "@flarum/core",
"prettier": "@flarum/prettier-config",
"dependencies": {
"@askvortsov/rich-icu-message-formatter": "^0.1.0",
"@ultraq/icu-message-formatter": "^0.10.1",
@@ -18,6 +19,7 @@
},
"devDependencies": {
"@babel/preset-typescript": "^7.13.0",
"@flarum/prettier-config": "^1.0.0",
"@types/jquery": "^3.5.5",
"@types/mithril": "^2.0.7",
"@types/punycode": "^2.1.0",

View File

@@ -66,6 +66,9 @@ export default class AdminApplication extends Application {
if (permission === 'discussion.deletePosts') {
required.push('discussion.hidePosts');
}
if (permission === 'user.editGroups') {
required.push('viewHiddenGroups');
}
return required;
}

View File

@@ -34,6 +34,7 @@ import EditCustomCssModal from './components/EditCustomCssModal';
import EditGroupModal from './components/EditGroupModal';
import routes from './routes';
import AdminApplication from './AdminApplication';
import AdvancedPage from './components/AdvancedPage';
export default Object.assign(compat, {
'utils/saveSettings': saveSettings,
@@ -68,6 +69,7 @@ export default Object.assign(compat, {
'components/AdminHeader': AdminHeader,
'components/EditCustomCssModal': EditCustomCssModal,
'components/EditGroupModal': EditGroupModal,
'components/AdvancedPage': AdvancedPage,
routes: routes,
AdminApplication: AdminApplication,
});

View File

@@ -75,6 +75,16 @@ export default class AdminNav extends Component {
</LinkButton>
);
// We only display the advanced pane when a certain threshold is reached or it is manually activated.
if (app.data.settings['advanced_settings_pane_enabled']) {
items.add(
'advanced',
<LinkButton href={app.route('advanced')} icon="fas fa-rocket" title={app.translator.trans('core.admin.nav.advanced_title')}>
{app.translator.trans('core.admin.nav.advanced_button')}
</LinkButton>
);
}
items.add(
'mail',
<LinkButton href={app.route('mail')} icon="fas fa-envelope" title={app.translator.trans('core.admin.nav.email_title')}>

View File

@@ -98,39 +98,37 @@ export default class AdminPage extends Page {
return entry.call(this);
}
const { setting, help, ...componentAttrs } = entry;
const { setting, help, type, label, ...componentAttrs } = entry;
const value = this.setting([setting])();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(componentAttrs.type)) {
const value = this.setting(setting)();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(type)) {
return (
<div className="Form-group">
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...componentAttrs}>
{componentAttrs.label}
{label}
</Switch>
<div className="helpText">{help}</div>
</div>
);
} else if (['select', 'dropdown', 'selectdropdown'].includes(componentAttrs.type)) {
} else if (['select', 'dropdown', 'selectdropdown'].includes(type)) {
const { default: defaultValue, options } = componentAttrs;
return (
<div className="Form-group">
<label>{componentAttrs.label}</label>
<label>{label}</label>
<div className="helpText">{help}</div>
<Select
value={value || componentAttrs.default}
options={componentAttrs.options}
buttonClassName="Button"
onchange={this.settings[setting]}
{...componentAttrs}
/>
<Select value={value || defaultValue} options={options} onchange={this.settings[setting]} {...componentAttrs} />
</div>
);
} else {
componentAttrs.className = classList(['FormControl', componentAttrs.className]);
return (
<div className="Form-group">
{componentAttrs.label ? <label>{componentAttrs.label}</label> : ''}
{label ? <label>{label}</label> : ''}
<div className="helpText">{help}</div>
<input type={componentAttrs.type} bidi={this.setting(setting)} {...componentAttrs} />
<input type={type} bidi={this.setting(setting)} {...componentAttrs} />
</div>
);
}

View File

@@ -0,0 +1,40 @@
import FieldSet from '../../common/components/FieldSet';
import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import Alert from '../../common/components/Alert';
export default class AdvancedPage extends AdminPage {
oninit(vnode) {
super.oninit(vnode);
this.queueDrivers = {};
app.data.queueDrivers.forEach((driver) => {
this.queueDrivers[driver] = app.translator.trans('core.admin.queue.' + driver);
});
}
headerInfo() {
return {
className: 'AdvancedPage',
icon: 'fas fa-rocket',
title: app.translator.trans('core.admin.advanced.title'),
description: app.translator.trans('core.admin.advanced.description'),
};
}
content() {
return [
<div className="Form">
{this.buildSettingComponent({
type: 'select',
setting: 'queue_driver',
options: Object.keys(this.queueDrivers).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.queue.driver_heading'),
className: 'AdvancedPage-QueueSettings',
})}
{this.submitButton()}
</div>,
];
}
}

View File

@@ -0,0 +1,158 @@
import Mithril from 'mithril';
import Link from '../../common/components/Link';
import classList from '../../common/utils/classList';
import ItemList from '../../common/utils/ItemList';
import app from '../app';
import AdminPage from './AdminPage';
export interface IAdvancedPageAttrs extends Mithril.Attributes {}
export interface ICreateDriverComponentOptions<Options extends string[]> {
/**
* The default driver value.
*
* This will appear selected if the driver is not specified.
*/
defaultValue: Options[number];
/**
* Custom class to apply to the `<select>` component.
*
* This is applied in addition to the default `AdvancedPage-driverSelect` class.
*/
className: string;
}
export default class AdvancedPage extends AdminPage {
oninit(vnode: Mithril.Vnode<IAdvancedPageAttrs, this>) {
super.oninit(vnode);
}
headerInfo() {
return {
className: 'AdvancedPage',
icon: 'fas fa-rocket',
title: app.translator.trans('core.admin.advanced.title'),
description: app.translator.trans('core.admin.advanced.description'),
};
}
content() {
return (
<>
<form class="Form">{this.items().toArray()}</form>
</>
);
}
items(): ItemList {
const items = new ItemList();
if (!app.data.settings.advanced_settings_pane_enabled) {
items.add(
'page_not_enabled',
// TODO: Add link to docs page
<p class="AdvancedPage-notEnabledWarning">
{app.translator.trans('core.admin.advanced.not_enabled_warning', {
a: <Link external href="https://docs.flarum.org/" />,
icon: <span aria-label={app.translator.trans('core.admin.advanced.warning_icon_accessible_label')} class="fas fa-exclamation-triangle" />,
})}
</p>,
110
);
} else {
items.add(
'large_community_text',
// TODO: Add link to docs page
<p class="AdvancedPage-congratsText">
{app.translator.trans('core.admin.advanced.large_community_note', {
a: <Link external href="https://docs.flarum.org/" />,
icon: <span aria-label={app.translator.trans('core.admin.advanced.info_icon_accessible_label')} class="fas fa-info-circle" />,
})}
</p>,
110
);
}
items.add(
'drivers',
<fieldset class="Form-group AdvancedPage-category">
<legend>{app.translator.trans('core.admin.advanced.drivers.legend')}</legend>
{this.drivers().toArray()}
</fieldset>,
90
);
items.add('save', this.submitButton(), -10);
return items;
}
drivers(): ItemList {
const items = new ItemList();
items.add(
'queueDriver',
this.createDriverComponent('queue_driver', 'core.admin.advanced.drivers.queue', app.data.queueDrivers, {
className: 'AdvancedPage-queueDriver',
defaultValue: 'database',
}),
100
);
return items;
}
/**
* Build a form component for a given driver.
*
* Requires the follow translations under the given prefix:
* - `driver_heading` (shown as legend for the form group)
* - `driver_label` (shown as the label for the select box)
* - `names.{driver_id}` (shown as the options for the select box)
*
* @param settingKey The setting key for the driver.
* @param driverTranslatorPrefix The prefix used for translations.
* @param driverOptions An array of possible driver values.
* @param options Optional settings for the component.
*
* @example <caption>Queue driver</caption>
* this.createDriverComponent(
* 'queue_driver',
* 'core.admin.advanced.drivers.queue',
* [ 'database', 'sync' ],
* },
* {
* defaultValue: 'database',
* },
* );
*/
createDriverComponent<Options extends string[]>(
settingKey: string,
driverTranslatorPrefix: string,
driverOptions: Options,
options: Partial<ICreateDriverComponentOptions<Options>> = {}
): JSX.Element {
return (
<fieldset class="Form-group">
<legend>{app.translator.trans(`${driverTranslatorPrefix}.driver_heading`)}</legend>
{this.buildSettingComponent({
type: 'select',
setting: settingKey,
options: driverOptions.reduce(
(acc, value) => ({
...acc,
[value]: app.translator.trans(`${driverTranslatorPrefix}.names.${value}`),
}),
{} as Record<Options[number], ReturnType<typeof app.translator.trans>>
),
default: options.defaultValue,
label: app.translator.trans(`${driverTranslatorPrefix}.driver_label`),
className: classList('AdvancedPage-driverSelect', options.className),
})}
</fieldset>
);
}
}

View File

@@ -55,14 +55,12 @@ export default class MailPage extends AdminPage {
type: 'text',
setting: 'mail_from',
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
})}
{this.buildSettingComponent({
type: 'select',
setting: 'mail_driver',
options: Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.email.driver_heading'),
className: 'MailPage-MailSettings',
})}
{this.status.sending ||
Alert.component(

View File

@@ -1,4 +1,5 @@
import DashboardPage from './components/DashboardPage';
import AdvancedPage from './components/AdvancedPage';
import BasicsPage from './components/BasicsPage';
import PermissionsPage from './components/PermissionsPage';
import AppearancePage from './components/AppearancePage';
@@ -8,16 +9,18 @@ import ExtensionPage from './components/ExtensionPage';
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
/**
* The `routes` initializer defines the forum app's routes.
* The `routes` initializer defines the admin app's routes.
*
* @param {App} app
* @param {import('./app').default} app
*/
export default function (app) {
app.routes = {
dashboard: { path: '/', component: DashboardPage },
basics: { path: '/basics', component: BasicsPage },
advanced: { path: '/advanced', component: AdvancedPage },
permissions: { path: '/permissions', component: PermissionsPage },
appearance: { path: '/appearance', component: AppearancePage },
advanced: { path: '/advanced', component: AdvancedPage },
mail: { path: '/mail', component: MailPage },
users: { path: '/users', component: UserListPage },
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },

View File

@@ -1,6 +1,7 @@
import Component from '../Component';
import icon from '../helpers/icon';
import withAttr from '../utils/withAttr';
import classList from '../utils/classList';
/**
* The `Select` component displays a <select> input, surrounded with some extra
@@ -10,18 +11,35 @@ import withAttr from '../utils/withAttr';
* - `onchange` A callback to run when the selected value is changed.
* - `value` The value of the selected option.
* - `disabled` Disabled state for the input.
* - `wrapperAttrs` A map of attrs to be passed to the DOM element wrapping the `<select>`
*
* Other attributes are passed directly to the `<select>` element rendered to the DOM.
*/
export default class Select extends Component {
view() {
const { options, onchange, value, disabled } = this.attrs;
const {
options,
onchange,
value,
disabled,
className,
class: _class,
// Destructure the `wrapperAttrs` object to extract the `className` for passing to `classList()`
// `= {}` prevents errors when `wrapperAttrs` is undefined
wrapperAttrs: { className: wrapperClassName, class: wrapperClass, ...wrapperAttrs } = {},
...domAttrs
} = this.attrs;
return (
<span className="Select">
<span className={classList('Select', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
<select
className="Select-input FormControl"
className={classList('Select-input FormControl', className, _class)}
onchange={onchange ? withAttr('value', onchange.bind(this)) : undefined}
value={value}
disabled={disabled}
{...domAttrs}
>
{Object.keys(options).map((key) => (
<option value={key}>{options[key]}</option>

View File

@@ -1,5 +1,4 @@
import dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
/**
* The `humanTime` utility converts a date to a localized, human-readable time-

View File

@@ -44,6 +44,7 @@ export default class Post extends Component {
attrs.className = this.classes(attrs.className).join(' ');
const controls = PostControls.controls(this.attrs.post, this).toArray();
const footerItems = this.footerItems().toArray();
return (
<article {...attrs}>
@@ -71,9 +72,7 @@ export default class Post extends Component {
)}
</ul>
</aside>
<footer className="Post-footer">
<ul>{listItems(this.footerItems().toArray())}</ul>
</footer>
<footer className="Post-footer">{footerItems.length ? <ul>{listItems(footerItems)}</ul> : null}</footer>
</div>
</article>
);

View File

@@ -11,3 +11,4 @@
@import "admin/AppearancePage";
@import "admin/MailPage";
@import "admin/UsersListPage.less";
@import "admin/AdvancedPage.less";

View File

@@ -0,0 +1,33 @@
.AdvancedPage {
&-notEnabledWarning,
&-congratsText {
padding: 8px 8px 8px 12px;
max-width: 600px;
}
&-notEnabledWarning {
border-left: 8px solid @error-color;
background: fade(@error-color, 5%);
}
&-congratsText {
border-left: 8px solid @control-color;
}
&-category {
&:first-of-type {
margin-top: 16px;
}
> legend {
font-size: 1.25rem;
}
fieldset {
> legend {
font-size: 1.1rem;
margin-bottom: 4px;
}
}
}
}

View File

@@ -131,7 +131,7 @@
// Code blocks
pre {
border: 0;
padding: 15px;
padding: 0;
background: @code-bg;
color: #666;
font-size: 90%;
@@ -139,12 +139,14 @@
overflow-wrap: normal;
code {
padding: 0;
padding: 1em;
background: none;
color: inherit;
line-height: inherit;
font-size: 100%;
border-radius: 0;
display: block;
overflow-x: auto;
}
}
h1, h2, h3, h4, h5, h6 {
@@ -245,9 +247,6 @@
a {
font-weight: bold;
}
.Post-footer {
margin-bottom: 0;
}
}
.EventPost-info {
font-size: 14px;
@@ -285,6 +284,10 @@
font-size: 14px;
margin-right: 5px;
}
&:empty {
display: none;
}
}
.Post-actions {
margin-top: -5px;

View File

@@ -6,6 +6,35 @@ core:
# Translations in this namespace are used by the admin interface.
admin:
advanced:
description: Settings relating to Flarum's scalability.
drivers:
legend: Drivers
queue:
driver_heading: Queue Driver
driver_label: Choose a Queue Driver
names:
database: Database
sync: Sync
info_icon_accessible_label: information symbol
# Shown on the page when it's meant to be enabled
large_community_note: |
<icon></icon> This page is intended for very active communities, like yours! Great job! Modifying
some of these settings may require advanced setup or create extra load for your
installation. <a>Learn more about these settings.</a>
# Shown on the page when it's hidden and was directly navigated to
not_enabled_warning: |
<icon></icon> This page is intended for very active communities. This page is hidden for your
forum because you don't meet the recommended criteria for modifying these settings.
Doing so may require advanced setup or create extra load for your installation.
<a>Learn more about these settings.</a>
title: Advanced settings
warning_icon_accessible_label: warning symbol
# These translations are used in the Appearance page.
appearance:
@@ -143,6 +172,8 @@ core:
# These translations are used in the navigation bar.
nav:
advanced_button: => core.admin.advanced.title
advanced_title: => core.admin.advanced.description
appearance_button: => core.admin.appearance.title
appearance_title: => core.admin.appearance.description
basics_button: => core.admin.basics.title

View File

@@ -0,0 +1,24 @@
<?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.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'queue_failed_jobs',
function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection')->nullable();
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
}
);

View File

@@ -0,0 +1,24 @@
<?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.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'queue_jobs',
function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
}
);

View File

@@ -79,6 +79,7 @@ class AdminPayload
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
return array_keys($resourceDrivers);
}, $this->container->make('flarum.http.slugDrivers'));
$document->payload['queueDrivers'] = array_keys($this->container->make('flarum.queue.supported_drivers'));
$document->payload['phpVersion'] = PHP_VERSION;
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;

View File

@@ -111,15 +111,18 @@ class ApiServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\AuthenticateWithHeader::class,
HttpMiddleware\CheckCsrfToken::class
HttpMiddleware\CheckCsrfToken::class,
// HttpMiddleware\RememberFromCookie::class,
];
});
$this->container->singleton(Client::class, function ($container) {
$pipe = new MiddlewarePipe;
$middlewareStack = array_filter($container->make('flarum.api.middleware'), function ($middlewareClass) use ($container) {
return ! in_array($middlewareClass, $container->make('flarum.api_client.exclude_middleware'));
$exclude = $container->make('flarum.api_client.exclude_middleware');
$middlewareStack = array_filter($container->make('flarum.api.middleware'), function ($middlewareClass) use ($exclude) {
return ! in_array($middlewareClass, $exclude);
});
foreach ($middlewareStack as $middleware) {

View File

@@ -14,6 +14,7 @@ use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\DatabaseTransactionsManager;
class DatabaseServiceProvider extends AbstractServiceProvider
{
@@ -63,6 +64,10 @@ class DatabaseServiceProvider extends AbstractServiceProvider
$this->container->singleton('flarum.database.model_private_checkers', function () {
return [];
});
$this->container->singleton('db.transactions', function () {
return new DatabaseTransactionsManager;
});
}
public function boot(Container $container)

View File

@@ -21,7 +21,7 @@ class Application
*
* @var string
*/
const VERSION = '1.0.4';
const VERSION = '1.0.5-dev';
/**
* The IoC container for the Flarum application.

View File

@@ -13,6 +13,11 @@ use Flarum\Console\AbstractCommand;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\Application;
use Flarum\Foundation\Config;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Str;
use PDO;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableStyle;
@@ -29,13 +34,31 @@ class InfoCommand extends AbstractCommand
protected $config;
/**
* @param ExtensionManager $extensions
* @param Config config
* @var SettingsRepositoryInterface
*/
public function __construct(ExtensionManager $extensions, Config $config)
{
protected $settings;
/**
* @var ConnectionInterface
*/
protected $db;
/**
* @var Queue
*/
private $queue;
public function __construct(
ExtensionManager $extensions,
Config $config,
SettingsRepositoryInterface $settings,
ConnectionInterface $db,
Queue $queue
) {
$this->extensions = $extensions;
$this->config = $config;
$this->settings = $settings;
$this->db = $db;
$this->queue = $queue;
parent::__construct();
}
@@ -59,6 +82,7 @@ class InfoCommand extends AbstractCommand
$this->output->writeln("<info>Flarum core $coreVersion</info>");
$this->output->writeln('<info>PHP version:</info> '.PHP_VERSION);
$this->output->writeln('<info>MySQL version:</info> '.$this->identifyDatabaseVersion());
$phpExtensions = implode(', ', get_loaded_extensions());
$this->output->writeln("<info>Loaded extensions:</info> $phpExtensions");
@@ -67,9 +91,12 @@ class InfoCommand extends AbstractCommand
$this->output->writeln('<info>Base URL:</info> '.$this->config->url());
$this->output->writeln('<info>Installation path:</info> '.getcwd());
$this->output->writeln('<info>Debug mode:</info> '.($this->config->inDebugMode() ? 'ON' : 'off'));
$this->output->writeln('<info>Queue driver:</info> '.$this->identifyQueueDriver());
$this->output->writeln('<info>Mail driver:</info> '.$this->settings->get('mail_driver', 'unknown'));
$this->output->writeln('<info>Debug mode:</info> '.($this->config->inDebugMode() ? '<error>ON</error>' : 'off'));
if ($this->config->inDebugMode()) {
$this->output->writeln('');
$this->error(
"Don't forget to turn off debug mode! It should never be turned on in a production system."
);
@@ -102,12 +129,8 @@ class InfoCommand extends AbstractCommand
*
* If the package seems to be a Git version, we extract the currently
* checked out commit using the command line.
*
* @param string $path
* @param string $fallback
* @return string
*/
private function findPackageVersion($path, $fallback = null)
private function findPackageVersion(string $path, string $fallback = null): ?string
{
if (file_exists("$path/.git")) {
$cwd = getcwd();
@@ -126,4 +149,23 @@ class InfoCommand extends AbstractCommand
return $fallback;
}
private function identifyQueueDriver(): string
{
// Get class name
$queue = get_class($this->queue);
// Drop the namespace
$queue = Str::afterLast($queue, '\\');
// Lowercase the class name
$queue = strtolower($queue);
// Drop everything like queue SyncQueue, RedisQueue
$queue = str_replace('queue', null, $queue);
return $queue;
}
private function identifyDatabaseVersion(): string
{
return $this->db->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
}
}

View File

@@ -131,6 +131,7 @@ class InstalledSite implements SiteInterface
$laravel->register(NotificationServiceProvider::class);
$laravel->register(PostServiceProvider::class);
$laravel->register(QueueServiceProvider::class);
$laravel->register(ScalabilityServiceProvider::class);
$laravel->register(SearchServiceProvider::class);
$laravel->register(SessionServiceProvider::class);
$laravel->register(SettingsServiceProvider::class);

View File

@@ -0,0 +1,62 @@
<?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 Flarum\Settings\Event\Deserializing;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Queue\SyncQueue;
class ScalabilityServiceProvider extends AbstractServiceProvider
{
public function boot(Dispatcher $events, Queue $queue)
{
if ($queue instanceof SyncQueue) {
$events->listen(JobProcessing::class, [$this, 'trackQueueLoad']);
}
$events->listen(Deserializing::class, [$this, 'recommendations']);
}
public function trackQueueLoad(JobProcessing $event)
{
/** @var Repository $cache */
$cache = resolve('cache.store');
// Retrieve existing queue load.
$count = (int) $cache->get('flarum.scalability.queue-load', 0);
$count++;
// Store the queue load, but only for one minute.
$cache->set('flarum.scalability.queue-load', $count, 60);
// If within that minute 10 queue tasks were fired, we need to suggest an alternative driver.
if ($count > 10) {
/** @var SettingsRepositoryInterface $settings */
$settings = resolve(SettingsRepositoryInterface::class);
$settings->set('flarum.scalability.queue-recommended', true);
}
}
public function recommendations(Deserializing $event)
{
/** @var Config $config */
$config = resolve(Config::class);
// Toggles the advanced pane for admins.
$event->settings['advanced_settings_pane_enabled'] = $event->settings['flarum.scalability.queue-recommended']
?? $config->offsetGet('advancedSettings')
?? false;
}
}

View File

@@ -14,6 +14,7 @@ use Flarum\Foundation\Config;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\Paths;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Cache\Factory as CacheFactory;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandling;
@@ -22,11 +23,13 @@ use Illuminate\Contracts\Queue\Factory;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Connectors\ConnectorInterface;
use Illuminate\Queue\Console as Commands;
use Illuminate\Queue\DatabaseQueue;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Failed\NullFailedJobProvider;
use Illuminate\Queue\Failed\DatabaseFailedJobProvider;
use Illuminate\Queue\Listener as QueueListener;
use Illuminate\Queue\SyncQueue;
use Illuminate\Queue\Worker;
use Illuminate\Support\Arr;
class QueueServiceProvider extends AbstractServiceProvider
{
@@ -42,6 +45,34 @@ class QueueServiceProvider extends AbstractServiceProvider
public function register()
{
$this->container->singleton('flarum.queue.supported_drivers', function () {
return [
'sync' => SyncQueue::class,
'database' => DatabaseQueue::class,
];
});
$this->container->singleton('flarum.queue.connection', function (Container $container) {
/** @var array $drivers */
$drivers = $container->make('flarum.queue.supported_drivers');
/** @var SettingsRepositoryInterface $settings */
$settings = $container->make(SettingsRepositoryInterface::class);
$driverName = $settings->get('queue_driver', 'sync');
$driverClass = Arr::get($drivers, $driverName);
/** @var Queue $driver */
$driver = $container->make($driverClass);
// This method only exists on the Laravel abstract Queue implementation, not the contract,
// for simplicity we will try to inject the container if the method is available on the driver.
if (method_exists($driver, 'setContainer')) {
$driver->setContainer($container);
}
return $driver;
});
// Register a simple connection factory that always returns the same
// connection, as that is enough for our purposes.
$this->container->singleton(Factory::class, function (Container $container) {
@@ -50,15 +81,6 @@ class QueueServiceProvider extends AbstractServiceProvider
});
});
// Extensions can override this binding if they want to make Flarum use
// a different queuing backend.
$this->container->singleton('flarum.queue.connection', function (Container $container) {
$queue = new SyncQueue;
$queue->setContainer($container);
return $queue;
});
$this->container->singleton(ExceptionHandling::class, function (Container $container) {
return new ExceptionHandler($container['log']);
});
@@ -78,7 +100,7 @@ class QueueServiceProvider extends AbstractServiceProvider
});
// Override the Laravel native Listener, so that we can ignore the environment
// option and force the binary to flarum.
// option and force the binary to Flarum.
$this->container->singleton(QueueListener::class, function (Container $container) {
return new Listener($container->make(Paths::class)->base);
});
@@ -110,10 +132,21 @@ class QueueServiceProvider extends AbstractServiceProvider
};
});
$this->container->singleton('queue.failer', function () {
return new NullFailedJobProvider();
$this->container->singleton('queue.failer', function (Container $container) {
/** @var Config $config */
$config = $container->make(Config::class);
return new DatabaseFailedJobProvider(
$container->make('db'),
$config->offsetGet('database.database'),
'queue_failed_jobs'
);
});
$this->container->when(DatabaseQueue::class)
->needs('$table')
->give('queue_jobs');
$this->container->alias('flarum.queue.connection', Queue::class);
$this->container->alias(ConnectorInterface::class, 'queue.connection');