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

Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Skvortsov
b17f26e6a8 feat: calculate post numbers via subquery
On heavily volatile communities running on distributed systems, if two replies are posted at approximately the same time, each node will try to use the same post number, which results in a failed integrity constraint.

Potential options for fixing this were discussed in https://github.com/flarum/framework/issues/3350. We settled on calculating the post number via subquery during post instance creation, which should solve this issue since DB transactions are atomic.

To facilicate this, we needed to implement support for DB attributes that are calculated via subquery at insert, but otherwise stored and accessed normally. This PR adds a `InsertViaSubqueryTrait` which adds exactly this feature.
2022-03-23 15:29:55 -04:00
108 changed files with 206 additions and 392 deletions

View File

@@ -13,9 +13,6 @@ export default class FlagList extends Component {
this.state = this.attrs.state;
}
/**
* @return {import('mithril').Children}
*/
view() {
const flags = this.state.cache || [];

View File

@@ -24,9 +24,6 @@ export default class FlagPostModal extends Modal {
return app.translator.trans('flarum-flags.forum.flag_post.title');
}
/**
* @return {import('mithril').Children}
*/
content() {
if (this.success) {
return (

View File

@@ -11,9 +11,6 @@ export default class FlagsDropdown extends NotificationsDropdown {
super.initAttrs(attrs);
}
/**
* @return {import('mithril').Children}
*/
getMenu() {
return (
<div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>

View File

@@ -18,9 +18,6 @@ export default class FlagsPage extends Page {
this.bodyClass = 'App--flags';
}
/**
* @return {import('mithril').Children}
*/
view() {
return (
<div className="FlagsPage">

View File

@@ -64,7 +64,7 @@ export default class Updater<Attrs> extends Component<Attrs> {
super.oninit(vnode);
}
view(): Mithril.Children {
view() {
const extensions = this.getExtensionUpdates();
let coreUpdate: UpdatedPackage | undefined = this.getCoreUpdate();
let core: any;

View File

@@ -26,7 +26,7 @@ export default class WhyNotModal<Attrs extends WhyNotModalAttrs = WhyNotModalAtt
this.requestWhyNot();
}
content(): Mithril.Children {
content() {
return <div className="Modal-body">{this.loading ? <LoadingIndicator /> : <pre className="WhyNotModal-contents">{this.whyNot}</pre>}</div>;
}

View File

@@ -46,9 +46,6 @@ export default class StatisticsWidget extends DashboardWidget {
return 'StatisticsWidget';
}
/**
* @return {import('mithril').Children}
*/
content() {
const thisPeriod = this.periods[this.selectedPeriod];

View File

@@ -7,7 +7,7 @@ import { slug } from 'flarum/common/utils/string';
import Stream from 'flarum/common/utils/Stream';
import tagLabel from '../../common/helpers/tagLabel';
import Mithril from 'mithril';
import type Mithril from 'mithril';
import type Tag from '../../common/models/Tag';
import extractText from 'flarum/common/utils/extractText';
import { ModelIdentifier } from 'flarum/common/Model';
@@ -50,13 +50,13 @@ export default class EditTagModal extends Modal<EditTagModalAttrs> {
return 'EditTagModal Modal--small';
}
title(): Mithril.Children {
title() {
return this.name()
? tagLabel(app.store.createRecord<Tag>('tags', { attributes: this.submitData() }))
? tagLabel(app.store.createRecord('tags', { attributes: this.submitData() }))
: app.translator.trans('flarum-tags.admin.edit_tag.title');
}
content(): Mithril.Children {
content() {
return (
<div className="Modal-body">
<div className="Form">
@@ -67,7 +67,7 @@ export default class EditTagModal extends Modal<EditTagModalAttrs> {
}
fields() {
const items = new ItemList<Mithril.Children>();
const items = new ItemList();
items.add('name', <div className="Form-group">
<label>{app.translator.trans('flarum-tags.admin.edit_tag.name_label')}</label>

View File

@@ -51,9 +51,6 @@ export default class TagsPage extends ExtensionPage {
});
}
/**
* @return {import('mithril').Children}
*/
content() {
if (this.loading) {
return <LoadingIndicator />;
@@ -66,7 +63,7 @@ export default class TagsPage extends ExtensionPage {
const maxSecondaryTags = this.setting('flarum-tags.max_secondary_tags', 0);
const tags = sortTags(app.store.all('tags').filter(tag => !tag.parent()));
return (
<div className="TagsContent">
<div className="TagsContent-list">

View File

@@ -1,9 +1,6 @@
import { ComponentAttrs } from 'flarum/common/Component';
import classList from 'flarum/common/utils/classList';
import type Tag from '../models/Tag';
import type Mithril from 'mithril';
export default function tagIcon(tag: Tag, attrs: ComponentAttrs = {}, settings: {useColor?: boolean} = {}): Mithril.Children {
export default function tagIcon(tag, attrs = {}, settings = {}) {
const hasIcon = tag && tag.icon();
const { useColor = true } = settings;

View File

@@ -1,13 +1,8 @@
import app from 'flarum/common/app';
import extract from 'flarum/common/utils/extract';
import Link from 'flarum/common/components/Link';
import tagIcon from './tagIcon';
import type Tag from '../models/Tag';
import type { ComponentAttrs } from 'flarum/common/Component';
import type Mithril from 'mithril';
export default function tagLabel(tag?: Tag, attrs: ComponentAttrs = {}): Mithril.Children {
export default function tagLabel(tag, attrs = {}) {
attrs.style = attrs.style || {};
attrs.className = 'TagLabel ' + (attrs.className || '');
@@ -33,15 +28,11 @@ export default function tagLabel(tag?: Tag, attrs: ComponentAttrs = {}): Mithril
attrs.className += ' untagged';
}
const children = (
<span className="TagLabel-text">
return (
m((link ? Link : 'span'), attrs,
<span className="TagLabel-text">
{tag && tag.icon() && tagIcon(tag, {}, {useColor: false})} {tagText}
</span>
)
);
if (link) {
return <Link {...attrs}>{children}</Link>
}
return <span {...attrs}>{children}</span>;
}

View File

@@ -1,18 +1,15 @@
import extract from 'flarum/common/utils/extract';
import tagLabel from './tagLabel';
import sortTags from '../utils/sortTags';
import type Tag from '../models/Tag';
import type { ComponentAttrs } from 'flarum/common/Component';
import type Mithril from 'mithril';
export default function tagsLabel(tags: Tag[], attrs: ComponentAttrs = {}): Mithril.Children {
export default function tagsLabel(tags, attrs = {}) {
const children = [];
const link = extract(attrs, 'link');
attrs.className = 'TagsLabel ' + (attrs.className || '');
if (tags) {
sortTags(tags).forEach((tag: Tag) => {
sortTags(tags).forEach(tag => {
if (tag || tags.length === 1) {
children.push(tagLabel(tag, {link}));
}

View File

@@ -139,9 +139,6 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
return '';
}
/**
* @return {import('mithril').Children}
*/
content() {
if (this.tagsLoading || !this.tags) {
return <LoadingIndicator />;

View File

@@ -2,9 +2,6 @@ import Component from 'flarum/common/Component';
import tagIcon from '../../common/helpers/tagIcon';
export default class TagHero extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
const tag = this.attrs.model;
const color = tag.color();

View File

@@ -4,9 +4,6 @@ import classList from 'flarum/common/utils/classList';
import tagIcon from '../../common/helpers/tagIcon';
export default class TagLinkButton extends LinkButton {
/**
* @return {import('mithril').Children}
*/
view(vnode) {
const tag = this.attrs.model;
const active = this.constructor.isActive(this.attrs);

View File

@@ -35,9 +35,6 @@ export default class TagsPage extends Page {
});
}
/**
* @return {import('mithril').Children}
*/
view() {
if (this.loading) {
return <LoadingIndicator />;

View File

@@ -6,9 +6,6 @@ import classList from 'flarum/common/utils/classList';
* @TODO move to core
*/
export default class ToggleButton extends Component {
/**
* @return {import('mithril').Children}
*/
view(vnode) {
const { className, isToggled, ...attrs } = this.attrs;
const icon = isToggled ? 'far fa-check-circle' : 'far fa-circle';

View File

@@ -1,6 +1,4 @@
declare module '@askvortsov/rich-icu-message-formatter' {
import Mithril from 'mithril';
type IValues = Record<string, any>;
type ITypeHandler = (
@@ -12,13 +10,16 @@ declare module '@askvortsov/rich-icu-message-formatter' {
) => string;
type IRichHandler = (tag: any, values: IValues, contents: string) => any;
type ValueOrArray<T> = T | ValueOrArray<T>[];
type NestedStringArray = ValueOrArray<string>;
export class RichMessageFormatter {
locale: string | null;
constructor(locale: string | null, typeHandlers: Record<string, ITypeHandler>, richHandler: IRichHandler);
format(message: string, values: IValues): string;
process(message: string, values: IValues): Mithril.Children;
rich(message: string, values: IValues): Mithril.Children;
process(message: string, values: IValues): NestedStringArray;
rich(message: string, values: IValues): NestedStringArray;
}
export function mithrilRichHandler(tag: any, values: IValues, contents: string): any;

View File

@@ -1,15 +1,9 @@
import Component, { ComponentAttrs } from '../../common/Component';
import Component from '../../common/Component';
import classList from '../../common/utils/classList';
import icon from '../../common/helpers/icon';
import type Mithril from 'mithril';
export interface IAdminHeaderAttrs extends ComponentAttrs {
icon: string;
description: string;
}
export default class AdminHeader<CustomAttrs extends IAdminHeaderAttrs = IAdminHeaderAttrs> extends Component<CustomAttrs> {
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
export default class AdminHeader extends Component {
view(vnode) {
return [
<div className={classList(['AdminHeader', this.attrs.className])}>
<div className="container">

View File

@@ -14,9 +14,6 @@ export default class AdminNav extends Component {
this.query = Stream('');
}
/**
* @return {import('mithril').Children}
*/
view() {
return (
<SelectDropdown className="AdminNav App-titleControl AdminNav-Main" buttonClassName="Button">

View File

@@ -17,9 +17,6 @@ export default class AppearancePage extends AdminPage {
};
}
/**
* @return {import('mithril').Children}
*/
content() {
return [
<div className="Form">

View File

@@ -38,9 +38,6 @@ export default class BasicsPage extends AdminPage {
};
}
/**
* @return {import('mithril').Children}
*/
content() {
return [
<div className="Form">

View File

@@ -10,9 +10,6 @@ export default class EditCustomCssModal extends SettingsModal {
return app.translator.trans('core.admin.edit_css.title');
}
/**
* @return {import('mithril').Children}
*/
form() {
return [
<p>

View File

@@ -10,9 +10,6 @@ export default class EditCustomFooterModal extends SettingsModal {
return app.translator.trans('core.admin.edit_footer.title');
}
/**
* @return {import('mithril').Children}
*/
form() {
return [
<p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>,

View File

@@ -10,9 +10,6 @@ export default class EditCustomHeaderModal extends SettingsModal {
return app.translator.trans('core.admin.edit_header.title');
}
/**
* @return {import('mithril').Children}
*/
form() {
return [
<p>{app.translator.trans('core.admin.edit_header.customize_text')}</p>,

View File

@@ -41,9 +41,6 @@ export default class EditGroupModal extends Modal {
];
}
/**
* @return {import('mithril').Children}
*/
content() {
return (
<div className="Modal-body">

View File

@@ -54,7 +54,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
return this.extension.id + '-Page';
}
view(vnode: Mithril.VnodeDOM<Attrs, this>): Mithril.Children {
view(vnode: Mithril.VnodeDOM<Attrs, this>) {
if (!this.extension) return null;
return (
@@ -71,7 +71,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
);
}
header(): Mithril.Children {
header() {
const isEnabled = this.isEnabled();
return [
@@ -106,8 +106,8 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
];
}
sections(vnode: Mithril.VnodeDOM<Attrs, this>): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
sections(vnode: Mithril.VnodeDOM<Attrs, this>) {
const items = new ItemList();
items.add('content', this.content(vnode));
@@ -131,7 +131,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
return items;
}
content(vnode: Mithril.VnodeDOM<Attrs, this>): Mithril.Children {
content(vnode: Mithril.VnodeDOM<Attrs, this>) {
const settings = app.extensionData.getSettings(this.extension.id);
return (
@@ -150,7 +150,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
);
}
topItems(): ItemList<Mithril.Children> {
topItems() {
const items = new ItemList<Mithril.Children>();
items.add('version', <span className="ExtensionVersion">{this.extension.version}</span>);

View File

@@ -16,9 +16,6 @@ export default class ExtensionsWidget extends DashboardWidget {
return 'ExtensionsWidget';
}
/**
* @return {import('mithril').Children}
*/
content() {
const categories = app.extensionCategories;
@@ -29,9 +26,6 @@ export default class ExtensionsWidget extends DashboardWidget {
);
}
/**
* @return {import('mithril').Children}
*/
extensionCategory(category) {
return (
<div className="ExtensionList-Category">
@@ -41,9 +35,6 @@ export default class ExtensionsWidget extends DashboardWidget {
);
}
/**
* @return {import('mithril').Children}
*/
extensionWidget(extension) {
return (
<li className={'ExtensionListItem ' + (!isExtensionEnabled(extension.id) ? 'disabled' : '')}>

View File

@@ -7,9 +7,6 @@ import listItems from '../../common/helpers/listItems';
* default skin, these are shown just to the right of the forum title.
*/
export default class HeaderPrimary extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
}

View File

@@ -9,9 +9,6 @@ import listItems from '../../common/helpers/listItems';
* The `HeaderSecondary` component displays secondary header controls.
*/
export default class HeaderSecondary extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
}

View File

@@ -42,9 +42,6 @@ export default class MailPage extends AdminPage {
});
}
/**
* @return {import('mithril').Children}
*/
content() {
if (this.loading) {
return <LoadingIndicator />;

View File

@@ -33,7 +33,7 @@ export interface ScopeItem {
export interface IPermissionGridAttrs extends ComponentAttrs {}
export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = IPermissionGridAttrs> extends Component<CustomAttrs> {
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
const scopes = this.scopeItems().toArray();
const permissionCells = (permission: PermissionGridEntry | { children: PermissionGridEntry[] }) => {

View File

@@ -16,9 +16,6 @@ export default class PermissionsPage extends AdminPage {
};
}
/**
* @return {import('mithril').Children}
*/
content() {
return [
<div className="PermissionsPage-groups">

View File

@@ -38,7 +38,7 @@ export default class ReadmeModal<CustomAttrs extends IReadmeModalAttrs = IReadme
});
}
content(): Mithril.Children {
content() {
const text = app.translator.trans('core.admin.extension.readme.no_readme');
return (

View File

@@ -18,16 +18,10 @@ export default class SessionDropdown extends Dropdown {
attrs.menuClassName = 'Dropdown-menu--right';
}
/**
* @return {import('mithril').Children}
*/
view(vnode) {
return super.view({ ...vnode, children: this.items().toArray() });
}
/**
* @returns {import('mithril').Children}
*/
getButtonContent() {
const user = app.session.user;

View File

@@ -16,9 +16,6 @@ export default class SettingsModal extends Modal {
return '';
}
/**
* @return {import('mithril').Children}
*/
content() {
return (
<div className="Modal-body">
@@ -31,9 +28,6 @@ export default class SettingsModal extends Modal {
);
}
/**
* @return {import('mithril').Children}
*/
submitButton() {
return (
<Button type="submit" className="Button Button--primary" loading={this.loading} disabled={!this.changed()}>

View File

@@ -11,9 +11,6 @@ export default class StatusWidget extends DashboardWidget {
return 'StatusWidget';
}
/**
* @return {import('mithril').Children}
*/
content() {
return <ul>{listItems(this.items().toArray())}</ul>;
}

View File

@@ -4,9 +4,6 @@ import Button from '../../common/components/Button';
export default class UploadImageButton extends Button {
loading = false;
/**
* @return {import('mithril').Children}
*/
view(vnode) {
this.attrs.loading = this.loading;
this.attrs.className = (this.attrs.className || '') + ' Button';

View File

@@ -77,7 +77,7 @@ export default class UserListPage extends AdminPage {
/**
* Component to render.
*/
content(): Mithril.Children {
content() {
if (typeof this.pageData === 'undefined') {
this.loadPage(0);

View File

@@ -22,7 +22,7 @@ export interface AlertAttrs extends ComponentAttrs {
* some controls, and may be dismissible.
*/
export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<T> {
view(vnode: Mithril.VnodeDOM<T, this>): Mithril.Children {
view(vnode: Mithril.VnodeDOM<T, this>) {
const attrs = Object.assign({}, this.attrs);
const type = extract(attrs, 'type');

View File

@@ -0,0 +1,31 @@
import Component from '../Component';
/**
* The `AlertManager` component provides an area in which `Alert` components can
* be shown and dismissed.
*/
export default class AlertManager extends Component {
oninit(vnode) {
super.oninit(vnode);
this.state = this.attrs.state;
}
view() {
return (
<div class="AlertManager">
{Object.entries(this.state.getActiveAlerts()).map(([key, alert]) => {
const urgent = alert.attrs.type === 'error';
return (
<div class="AlertManager-alert" role="alert" aria-live={urgent ? 'assertive' : 'polite'}>
<alert.componentClass {...alert.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}>
{alert.children}
</alert.componentClass>
</div>
);
})}
</div>
);
}
}

View File

@@ -1,39 +0,0 @@
import type Mithril from 'mithril';
import Component, { ComponentAttrs } from '../Component';
import type AlertManagerState from '../states/AlertManagerState';
export interface IAlertManagerAttrs extends ComponentAttrs {
state: AlertManagerState;
}
/**
* The `AlertManager` component provides an area in which `Alert` components can
* be shown and dismissed.
*/
export default class AlertManager<CustomAttrs extends IAlertManagerAttrs = IAlertManagerAttrs> extends Component<CustomAttrs> {
alertsState!: AlertManagerState;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.alertsState = this.attrs.state;
}
view(): Mithril.Children {
return (
<div class="AlertManager">
{Object.entries(this.alertsState.getActiveAlerts()).map(([key, alert]) => {
const urgent = alert.attrs.type === 'error';
return (
<div class="AlertManager-alert" role="alert" aria-live={urgent ? 'assertive' : 'polite'}>
<alert.componentClass {...alert.attrs} ondismiss={this.alertsState.dismiss.bind(this.alertsState, parseInt(key))}>
{alert.children}
</alert.componentClass>
</div>
);
})}
</div>
);
}
}

View File

@@ -17,9 +17,6 @@ import classList from '../utils/classList';
* All other attrs will be assigned as attributes on the badge element.
*/
export default class Badge extends Component {
/**
* @returns {import('mithril').Children}
*/
view() {
const { type, icon: iconName, label, color, style = {}, ...attrs } = this.attrs;

View File

@@ -67,7 +67,7 @@ export interface IButtonAttrs extends ComponentAttrs {
* styles can be applied by providing `className="Button"` to the Button component.
*/
export default class Button<CustomAttrs extends IButtonAttrs = IButtonAttrs> extends Component<CustomAttrs> {
view(vnode: Mithril.VnodeDOM<CustomAttrs, this>): Mithril.Children {
view(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
let { type, title, 'aria-label': ariaLabel, icon: iconName, disabled, loading, className, class: _class, ...attrs } = this.attrs;
// If no `type` attr provided, set to "button"

View File

@@ -17,9 +17,6 @@ import withAttr from '../utils/withAttr';
* - `children` A text label to display next to the checkbox.
*/
export default class Checkbox extends Component {
/**
* @returns {import('mithril').Children}
*/
view(vnode) {
// Sometimes, false is stored in the DB as '0'. This is a temporary
// conversion layer until a more robust settings encoding is introduced

View File

@@ -5,7 +5,7 @@ import classList from '../utils/classList';
import icon from '../helpers/icon';
export default class ColorPreviewInput extends Component {
view(vnode: Mithril.Vnode<ComponentAttrs, this>): Mithril.Children {
view(vnode: Mithril.Vnode<ComponentAttrs, this>) {
const { className, id, ...attrs } = this.attrs;
attrs.type ||= 'text';

View File

@@ -36,9 +36,6 @@ export default class Dropdown extends Component {
this.showing = false;
}
/**
* @returns {import('mithril').Children}
*/
view(vnode) {
const items = vnode.children ? listItems(vnode.children) : [];
const renderItems = this.attrs.lazyDraw ? this.showing : true;
@@ -137,9 +134,6 @@ export default class Dropdown extends Component {
];
}
/**
* @returns {import('mithril').Children}
*/
getMenu(items) {
return <ul className={'Dropdown-menu dropdown-menu ' + this.attrs.menuClassName}>{items}</ul>;
}

View File

@@ -45,11 +45,11 @@ export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEd
return 'EditUserModal Modal--small';
}
title(): Mithril.Children {
title() {
return app.translator.trans('core.lib.edit_user.title');
}
content(): Mithril.Children {
content() {
const fields = this.fields().toArray();
return (
<div className="Modal-body">
@@ -58,8 +58,8 @@ export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEd
);
}
fields(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
fields() {
const items = new ItemList();
if (this.attrs.user.canEditCredentials()) {
items.add(

View File

@@ -11,9 +11,6 @@ import listItems from '../helpers/listItems';
* The children should be an array of items to show in the fieldset.
*/
export default class FieldSet extends Component {
/**
* @returns {import('mithril').Children}
*/
view(vnode) {
return (
<fieldset className={this.attrs.className}>

View File

@@ -10,9 +10,6 @@ import extract from '../utils/extract';
* `true` for the link to be external.
*/
export default class Link extends Component {
/**
* @returns {import('mithril').Children}
*/
view(vnode) {
let { options = {}, ...attrs } = vnode.attrs;

View File

@@ -1,7 +1,6 @@
import app from '../../common/app';
import Component, { ComponentAttrs } from '../Component';
import classList from '../utils/classList';
import type Mithril from 'mithril';
export interface LoadingIndicatorAttrs extends ComponentAttrs {
/**
@@ -56,7 +55,7 @@ export interface LoadingIndicatorAttrs extends ComponentAttrs {
* All other attrs will be assigned as attributes on the DOM element.
*/
export default class LoadingIndicator extends Component<LoadingIndicatorAttrs> {
view(): Mithril.Children {
view() {
const { display = 'block', size = 'medium', containerClassName, className, ...attrs } = this.attrs;
const completeClassName = classList('LoadingIndicator', className);

View File

@@ -81,7 +81,7 @@ export default abstract class Modal<ModalAttrs extends IInternalModalAttrs = IIn
/**
* @todo split into FormModal and Modal in 2.0
*/
view(): Mithril.Children {
view() {
if (this.alertAttrs) {
this.alertAttrs.dismissible = false;
}

View File

@@ -19,9 +19,6 @@ import LinkButton from './LinkButton';
* there is no more history to pop.
*/
export default class Navigation extends Component {
/**
* @returns {import('mithril').Children}
*/
view() {
const { history, pane } = app;

View File

@@ -0,0 +1,19 @@
import Component from '../Component';
/**
* The `Placeholder` component displays a muted text with some call to action,
* usually used as an empty state.
*
* ### Attrs
*
* - `text`
*/
export default class Placeholder extends Component {
view() {
return (
<div className="Placeholder">
<p>{this.attrs.text}</p>
</div>
);
}
}

View File

@@ -1,20 +0,0 @@
import type Mithril from 'mithril';
import Component, { ComponentAttrs } from '../Component';
interface IPlaceholderAttrs extends ComponentAttrs {
text: Mithril.Children;
}
/**
* The `Placeholder` component displays a muted text with some call to action,
* usually used as an empty state.
*/
export default class Placeholder<CustomAttrs extends IPlaceholderAttrs = IPlaceholderAttrs> extends Component<CustomAttrs> {
view(): Mithril.Children {
return (
<div className="Placeholder">
<p>{this.attrs.text}</p>
</div>
);
}
}

View File

@@ -1,4 +1,3 @@
import type Mithril from 'mithril';
import type RequestError from '../utils/RequestError';
import Modal, { IInternalModalAttrs } from './Modal';
@@ -16,7 +15,7 @@ export default class RequestErrorModal<CustomAttrs extends IRequestErrorModalAtt
return this.attrs.error.xhr ? `${this.attrs.error.xhr.status} ${this.attrs.error.xhr.statusText}` : '';
}
content(): Mithril.Children {
content() {
const { error, formattedError } = this.attrs;
let responseText;

View File

@@ -16,9 +16,6 @@ import classList from '../utils/classList';
* Other attributes are passed directly to the `<select>` element rendered to the DOM.
*/
export default class Select extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
const {
options,

View File

@@ -41,9 +41,6 @@ export default class SelectDropdown extends Dropdown {
attrs.className += ' Dropdown--select';
}
/**
* @return {import('mithril').Children}
*/
getButtonContent(children) {
const activeChild = children.find(isActive);
let label = (activeChild && activeChild.children) || this.attrs.defaultLabel;

View File

@@ -1,13 +1,14 @@
import type Mithril from 'mithril';
import Component from '../Component';
/**
* The `Separator` component defines a menu separator item.
*/
export default class Separator extends Component {
static isListItem = true;
view(): Mithril.Children {
class Separator extends Component {
view() {
return <li className="Dropdown-separator" />;
}
}
Separator.isListItem = true;
export default Separator;

View File

@@ -14,9 +14,6 @@ export default class SplitDropdown extends Dropdown {
attrs.menuClassName += ' Dropdown-menu--right';
}
/**
* @return {import('mithril').Children}
*/
getButton(children) {
// Make a copy of the attrs of the first child component. We will assign
// these attrs to a new button, so that it has exactly the same behaviour as

View File

@@ -38,9 +38,6 @@ export default class TextEditor extends Component {
this.disabled = !!this.attrs.disabled;
}
/**
* @return {import('mithril').Children}
*/
view() {
return (
<div className="TextEditor">

View File

@@ -12,9 +12,6 @@ import Tooltip from './Tooltip';
* - `title` - Tooltip for the button
*/
export default class TextEditorButton extends Button {
/**
* @return {import('mithril').Children}
*/
view(vnode) {
const originalView = super.view(vnode);

View File

@@ -13,7 +13,7 @@ export interface AlertState {
}
export default class AlertManagerState {
protected activeAlerts: { [id: AlertIdentifier]: AlertState } = {};
protected activeAlerts: { [id: number]: AlertState } = {};
protected alertId = 0;
getActiveAlerts() {

View File

@@ -36,9 +36,6 @@ export default class AvatarEditor extends Component {
this.isDraggedOver = false;
}
/**
* @return {import('mithril').Children}
*/
view() {
const user = this.attrs.user;

View File

@@ -41,9 +41,6 @@ export default class ChangeEmailModal extends Modal {
return app.translator.trans('core.forum.change_email.title');
}
/**
* @return {import('mithril').Children}
*/
content() {
if (this.success) {
return (

View File

@@ -15,9 +15,6 @@ export default class ChangePasswordModal extends Modal {
return app.translator.trans('core.forum.change_password.title');
}
/**
* @return {import('mithril').Children}
*/
content() {
return (
<div className="Modal-body">

View File

@@ -33,9 +33,6 @@ export default class Composer extends Component {
this.prevPosition = this.state.position;
}
/**
* @return {import('mithril').Children}
*/
view() {
const body = this.state.body;
const classes = {

View File

@@ -47,9 +47,6 @@ export default class ComposerBody extends Component {
this.composer.fields.content(this.attrs.originalContent || '');
}
/**
* @return {import('mithril').Children}
*/
view() {
return (
<ConfirmDocumentUnload when={this.hasChanges.bind(this)}>

View File

@@ -20,9 +20,6 @@ export default class ComposerPostPreview extends Component {
attrs.surround = attrs.surround || ((preview) => preview());
}
/**
* @return {import('mithril').Children}
*/
view() {
return <div className={this.attrs.className} />;
}

View File

@@ -10,9 +10,6 @@ import listItems from '../../common/helpers/listItems';
* - `discussion`
*/
export default class DiscussionHero extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
return (
<header className="Hero DiscussionHero">

View File

@@ -13,9 +13,6 @@ import Placeholder from '../../common/components/Placeholder';
* - `state` A DiscussionListState object that represents the discussion lists's state.
*/
export default class DiscussionList extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
/**
* @type {import('../states/DiscussionListState').default}

View File

@@ -62,7 +62,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
};
}
view(): Mithril.Children {
view() {
const discussion = this.attrs.discussion;
const user = discussion.user();
const isUnread = discussion.isUnread();
@@ -164,8 +164,10 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
/**
* Determine whether or not the number of replies should be shown instead of
* the number of unread posts.
*
* @return {boolean}
*/
showRepliesCount(): boolean {
showRepliesCount() {
return this.attrs.params.sort === 'replies';
}
@@ -208,7 +210,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
return items;
}
replyCountItem(): Mithril.Children {
replyCountItem() {
const discussion = this.attrs.discussion;
const showUnread = !this.showRepliesCount() && discussion.isUnread();

View File

@@ -17,9 +17,6 @@ const hotEdge = (e) => {
* - `state` A DiscussionListState object that represents the discussion lists's state.
*/
export default class DiscussionListPane extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
if (!this.attrs.state.hasItems()) {
return;

View File

@@ -79,7 +79,7 @@ export default class DiscussionPage<CustomAttrs extends IDiscussionPageAttrs = I
}
}
view(): Mithril.Children {
view() {
return (
<div className="DiscussionPage">
<DiscussionListPane state={app.discussions} />

View File

@@ -15,9 +15,6 @@ export default class DiscussionRenamedPost extends EventPost {
return 'fas fa-pencil-alt';
}
/**
* @return {import('mithril').Children}
*/
description(data) {
const renamed = app.translator.trans('core.forum.post_stream.discussion_renamed_text', data);
const oldName = app.translator.trans('core.forum.post_stream.discussion_renamed_old_tooltip', data);
@@ -25,9 +22,6 @@ export default class DiscussionRenamedPost extends EventPost {
return <span title={extractText(oldName)}>{renamed}</span>;
}
/**
* @return {Record<string, unknown>}
*/
descriptionData() {
const post = this.attrs.post;
const oldTitle = post.content()[0];

View File

@@ -24,9 +24,6 @@ export default class DiscussionsUserPage extends UserPage {
this.state.refresh();
}
/**
* @return {import('mithril').Children}
*/
content() {
return <div className="DiscussionsUserPage">{DiscussionList.component({ state: this.state })}</div>;
}

View File

@@ -36,7 +36,7 @@ export default class ForgotPasswordModal<CustomAttrs extends IForgotPasswordModa
return app.translator.trans('core.forum.forgot_password.title');
}
content(): Mithril.Children {
content() {
if (this.success) {
return (
<div className="Modal-body">

View File

@@ -7,9 +7,6 @@ import listItems from '../../common/helpers/listItems';
* default skin, these are shown just to the right of the forum title.
*/
export default class HeaderPrimary extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
}

View File

@@ -16,9 +16,6 @@ import Search from '../components/Search';
* right side of the header.
*/
export default class HeaderSecondary extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
}

View File

@@ -45,9 +45,6 @@ export default class IndexPage extends Page {
this.scrollTopOnCreate = false;
}
/**
* @return {import('mithril').Children}
*/
view() {
return (
<div className="IndexPage">

View File

@@ -1,4 +1,3 @@
import type Mithril from 'mithril';
import Component from '../../common/Component';
import avatar from '../../common/helpers/avatar';
@@ -7,7 +6,7 @@ import avatar from '../../common/helpers/avatar';
* indicating that the post is loading.
*/
export default class LoadingPost extends Component {
view(): Mithril.Children {
view() {
return (
<div className="Post CommentPost LoadingPost">
<header className="Post-header">

View File

@@ -1,4 +1,3 @@
import type Mithril from 'mithril';
import Component from '../../common/Component';
import ItemList from '../../common/utils/ItemList';
@@ -6,14 +5,16 @@ import ItemList from '../../common/utils/ItemList';
* The `LogInButtons` component displays a collection of social login buttons.
*/
export default class LogInButtons extends Component {
view(): Mithril.Children {
view() {
return <div className="LogInButtons">{this.items().toArray()}</div>;
}
/**
* Build a list of LogInButton components.
*
* @return {ItemList<import('mithril').Children>}
*/
items() {
return new ItemList<Mithril.Children>();
return new ItemList();
}
}

View File

@@ -42,20 +42,20 @@ export default class LogInModal<CustomAttrs extends ILoginModalAttrs = ILoginMod
return 'LogInModal Modal--small';
}
title(): Mithril.Children {
title() {
return app.translator.trans('core.forum.log_in.title');
}
content(): Mithril.Children {
content() {
return [<div className="Modal-body">{this.body()}</div>, <div className="Modal-footer">{this.footer()}</div>];
}
body(): Mithril.Children {
body() {
return [<LogInButtons />, <div className="Form Form--centered">{this.fields().toArray()}</div>];
}
fields() {
const items = new ItemList<Mithril.Children>();
const items = new ItemList();
const identificationLabel = extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'));
const passwordLabel = extractText(app.translator.trans('core.forum.log_in.password_placeholder'));
@@ -124,7 +124,7 @@ export default class LogInModal<CustomAttrs extends ILoginModalAttrs = ILoginMod
return items;
}
footer(): Mithril.Children {
footer() {
return [
<p className="LogInModal-forgotPassword">
<a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>

View File

@@ -18,7 +18,7 @@ export interface INotificationAttrs extends ComponentAttrs {
* Subclasses should implement the `icon`, `href`, and `content` methods.
*/
export default abstract class Notification<CustomAttrs extends INotificationAttrs = INotificationAttrs> extends Component<CustomAttrs> {
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
const notification = this.attrs.notification;
const href = this.href();

View File

@@ -38,9 +38,6 @@ export default class NotificationGrid extends Component {
this.types = this.notificationTypes().toArray();
}
/**
* @return {import('mithril').Children}
*/
view() {
const preferences = this.attrs.user.preferences();

View File

@@ -12,9 +12,6 @@ import ItemList from '../../common/utils/ItemList';
* notifications, grouped by discussion.
*/
export default class NotificationList extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
const state = this.attrs.state;

View File

@@ -18,9 +18,6 @@ export default class NotificationsDropdown extends Dropdown {
super.initAttrs(attrs);
}
/**
* @return {import('mithril').Children}
*/
getButton() {
const newNotifications = this.getNewCount();
const vdom = super.getButton();
@@ -33,9 +30,6 @@ export default class NotificationsDropdown extends Dropdown {
return vdom;
}
/**
* @return {import('mithril').Children}
*/
getButtonContent() {
const unread = this.getUnreadCount();
@@ -46,9 +40,6 @@ export default class NotificationsDropdown extends Dropdown {
];
}
/**
* @return {import('mithril').Children}
*/
getMenu() {
return (
<div className={classList('Dropdown-menu', this.attrs.menuClassName)} onclick={this.menuClick.bind(this)}>

View File

@@ -17,9 +17,6 @@ export default class NotificationsPage extends Page {
this.bodyClass = 'App--notifications';
}
/**
* @return {import('mithril').Children}
*/
view() {
return (
<div className="NotificationsPage">

View File

@@ -45,7 +45,7 @@ export default abstract class Post<CustomAttrs extends IPostAttrs = IPostAttrs>
);
}
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
const attrs = this.elementAttrs();
attrs.className = this.classes(attrs.className as string | undefined).join(' ');

View File

@@ -16,9 +16,6 @@ export default class PostEdited extends Component {
super.oninit(vnode);
}
/**
* @return {import('mithril').Children}
*/
view() {
const post = this.attrs.post;
const editedUser = post.editedUser();

View File

@@ -13,9 +13,6 @@ import fullTime from '../../common/helpers/fullTime';
* - `post`
*/
export default class PostMeta extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
const post = this.attrs.post;
const time = post.createdAt();

View File

@@ -14,9 +14,6 @@ import highlight from '../../common/helpers/highlight';
* - `post`
*/
export default class PostPreview extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
const post = this.attrs.post;
const user = post.user();

View File

@@ -26,9 +26,6 @@ export default class PostStream extends Component {
this.scrollListener = new ScrollListener(this.onscroll.bind(this));
}
/**
* @return {import('mithril').Children}
*/
view() {
let lastTime;

View File

@@ -23,9 +23,6 @@ export default class PostStreamScrubber extends Component {
this.scrollListener = new ScrollListener(this.updateScrubberValues.bind(this, { fromScroll: true, forceHeightChange: true }));
}
/**
* @return {import('mithril').Children}
*/
view() {
const count = this.stream.count();

View File

@@ -15,9 +15,6 @@ import listItems from '../../common/helpers/listItems';
* - `post`
*/
export default class PostUser extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
const post = this.attrs.post;
const user = post.user();

View File

@@ -45,9 +45,6 @@ export default class PostsUserPage extends UserPage {
this.loadUser(m.route.param('username'));
}
/**
* @return {import('mithril').Children}
*/
content() {
if (this.posts.length === 0 && !this.loading) {
return (

View File

@@ -19,16 +19,10 @@ export default class RenameDiscussionModal extends Modal {
return 'RenameDiscussionModal Modal--small';
}
/**
* @return {import('mithril').Children}
*/
title() {
return app.translator.trans('core.forum.rename_discussion.title');
}
/**
* @return {import('mithril').Children}
*/
content() {
return (
<div className="Modal-body">

View File

@@ -15,9 +15,6 @@ import listItems from '../../common/helpers/listItems';
* - `discussion`
*/
export default class ReplyPlaceholder extends Component {
/**
* @return {import('mithril').Children}
*/
view() {
if (app.composer.composingReplyTo(this.attrs.discussion)) {
return (

View File

@@ -117,7 +117,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
this.searchState = this.attrs.state;
}
view(): Mithril.Children {
view() {
const currentSearch = this.searchState.getInitialSearch();
// Initialize search sources in the view rather than the constructor so

View File

@@ -22,16 +22,10 @@ export default class SessionDropdown extends Dropdown {
attrs.accessibleToggleLabel = app.translator.trans('core.forum.header.session_dropdown_accessible_label');
}
/**
* @return {import('mithril').Children}
*/
view(vnode) {
return super.view({ ...vnode, children: this.items().toArray() });
}
/**
* @return {import('mithril').Children}
*/
getButtonContent() {
const user = app.session.user;

Some files were not shown because too many files have changed in this diff Show More