mirror of
https://github.com/flarum/core.git
synced 2025-07-16 06:16:23 +02:00
Merge pull request #3196 from flarum/as/finish-typing
Finish typing, enable error on TypeScript check failure
This commit is contained in:
2
framework/core/.github/workflows/js.yml
vendored
2
framework/core/.github/workflows/js.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
|||||||
working-directory: ./js
|
working-directory: ./js
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: yarn run check-typings || true # REMOVE THIS ONCE TYPE SAFETY REACHED
|
run: yarn run check-typings
|
||||||
working-directory: ./js
|
working-directory: ./js
|
||||||
|
|
||||||
type-coverage:
|
type-coverage:
|
||||||
|
15
framework/core/js/src/@types/global.d.ts
vendored
15
framework/core/js/src/@types/global.d.ts
vendored
@ -21,7 +21,20 @@ declare type KeysOfType<Type extends object, Match> = {
|
|||||||
*/
|
*/
|
||||||
declare type KeyOfType<Type extends object, Match> = KeysOfType<Type, Match>[keyof Type];
|
declare type KeyOfType<Type extends object, Match> = KeysOfType<Type, Match>[keyof Type];
|
||||||
|
|
||||||
declare type VnodeElementTag<Attrs = Record<string, unknown>, State = Record<string, unknown>> = string | ComponentTypes<Attrs, State>;
|
type Component<A> = import('mithril').Component<A>;
|
||||||
|
|
||||||
|
declare type ComponentClass<Attrs = Record<string, unknown>, C extends Component<Attrs> = Component<Attrs>> = {
|
||||||
|
new (...args: any[]): Component<Attrs>;
|
||||||
|
prototype: C;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfortunately, TypeScript only supports strings and classes for JSX tags.
|
||||||
|
* Therefore, our type definition should only allow for those two types.
|
||||||
|
*
|
||||||
|
* @see https://github.com/microsoft/TypeScript/issues/14789#issuecomment-412247771
|
||||||
|
*/
|
||||||
|
declare type VnodeElementTag<Attrs = Record<string, unknown>, C extends Component<Attrs> = Component<Attrs>> = string | ComponentClass<Attrs, C>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Please import `app` from a namespace instead of using it as a global variable.
|
* @deprecated Please import `app` from a namespace instead of using it as a global variable.
|
||||||
|
@ -13,8 +13,8 @@ import generateElementId from '../utils/generateElementId';
|
|||||||
import ColorPreviewInput from '../../common/components/ColorPreviewInput';
|
import ColorPreviewInput from '../../common/components/ColorPreviewInput';
|
||||||
|
|
||||||
export interface AdminHeaderOptions {
|
export interface AdminHeaderOptions {
|
||||||
title: string;
|
title: Mithril.Children;
|
||||||
description: string;
|
description: Mithril.Children;
|
||||||
icon: string;
|
icon: string;
|
||||||
/**
|
/**
|
||||||
* Will be used as the class for the AdminPage.
|
* Will be used as the class for the AdminPage.
|
||||||
|
@ -16,6 +16,7 @@ import RequestError from '../../common/utils/RequestError';
|
|||||||
import { Extension } from '../AdminApplication';
|
import { Extension } from '../AdminApplication';
|
||||||
import { IPageAttrs } from '../../common/components/Page';
|
import { IPageAttrs } from '../../common/components/Page';
|
||||||
import type Mithril from 'mithril';
|
import type Mithril from 'mithril';
|
||||||
|
import extractText from '../../common/utils/extractText';
|
||||||
|
|
||||||
export interface ExtensionPageAttrs extends IPageAttrs {
|
export interface ExtensionPageAttrs extends IPageAttrs {
|
||||||
id: string;
|
id: string;
|
||||||
@ -156,7 +157,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
|
|||||||
|
|
||||||
if (!this.isEnabled()) {
|
if (!this.isEnabled()) {
|
||||||
const purge = () => {
|
const purge = () => {
|
||||||
if (confirm(app.translator.trans('core.admin.extension.confirm_purge'))) {
|
if (confirm(extractText(app.translator.trans('core.admin.extension.confirm_purge')))) {
|
||||||
app
|
app
|
||||||
.request({
|
.request({
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
|
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import app from '../../admin/app';
|
import app from '../../admin/app';
|
||||||
import Modal from '../../common/components/Modal';
|
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
|
||||||
|
|
||||||
export default class LoadingModal<ModalAttrs = {}> extends Modal<ModalAttrs> {
|
export interface ILoadingModalAttrs extends IInternalModalAttrs {}
|
||||||
|
|
||||||
|
export default class LoadingModal<ModalAttrs extends ILoadingModalAttrs = ILoadingModalAttrs> extends Modal<ModalAttrs> {
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
import app from '../../admin/app';
|
import app from '../../admin/app';
|
||||||
import Modal from '../../common/components/Modal';
|
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
import Placeholder from '../../common/components/Placeholder';
|
import Placeholder from '../../common/components/Placeholder';
|
||||||
import ExtensionReadme from '../models/ExtensionReadme';
|
import ExtensionReadme from '../models/ExtensionReadme';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
import type { Extension } from '../AdminApplication';
|
||||||
|
|
||||||
export default class ReadmeModal extends Modal {
|
export interface IReadmeModalAttrs extends IInternalModalAttrs {
|
||||||
oninit(vnode) {
|
extension: Extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ReadmeModal<CustomAttrs extends IReadmeModalAttrs = IReadmeModalAttrs> extends Modal<CustomAttrs> {
|
||||||
|
protected name!: string;
|
||||||
|
protected extName!: string;
|
||||||
|
protected readme!: ExtensionReadme;
|
||||||
|
|
||||||
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
app.store.models['extension-readmes'] = ExtensionReadme;
|
app.store.models['extension-readmes'] = ExtensionReadme;
|
@ -21,7 +21,7 @@ type ColumnData = {
|
|||||||
/**
|
/**
|
||||||
* Column title
|
* Column title
|
||||||
*/
|
*/
|
||||||
name: String;
|
name: Mithril.Children;
|
||||||
/**
|
/**
|
||||||
* Component(s) to show for this column.
|
* Component(s) to show for this column.
|
||||||
*/
|
*/
|
||||||
|
@ -32,11 +32,11 @@ export interface SavedModelData {
|
|||||||
|
|
||||||
export type ModelData = UnsavedModelData | SavedModelData;
|
export type ModelData = UnsavedModelData | SavedModelData;
|
||||||
|
|
||||||
interface SaveRelationships {
|
export interface SaveRelationships {
|
||||||
[relationship: string]: Model | Model[];
|
[relationship: string]: Model | Model[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SaveAttributes {
|
export interface SaveAttributes {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
relationships?: SaveRelationships;
|
relationships?: SaveRelationships;
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,8 @@ export type LoginParams = {
|
|||||||
* The username/email
|
* The username/email
|
||||||
*/
|
*/
|
||||||
identification: string;
|
identification: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Password
|
|
||||||
*/
|
|
||||||
password: string;
|
password: string;
|
||||||
|
remember: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
import app from '../../common/app';
|
import app from '../../common/app';
|
||||||
import Modal from './Modal';
|
import Modal, { IInternalModalAttrs } from './Modal';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import GroupBadge from './GroupBadge';
|
import GroupBadge from './GroupBadge';
|
||||||
import Group from '../models/Group';
|
import Group from '../models/Group';
|
||||||
import extractText from '../utils/extractText';
|
import extractText from '../utils/extractText';
|
||||||
import ItemList from '../utils/ItemList';
|
import ItemList from '../utils/ItemList';
|
||||||
import Stream from '../utils/Stream';
|
import Stream from '../utils/Stream';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
import type User from '../models/User';
|
||||||
|
import type { SaveAttributes, SaveRelationships } from '../Model';
|
||||||
|
|
||||||
/**
|
export interface IEditUserModalAttrs extends IInternalModalAttrs {
|
||||||
* The `EditUserModal` component displays a modal dialog with a login form.
|
user: User;
|
||||||
*/
|
}
|
||||||
export default class EditUserModal extends Modal {
|
|
||||||
oninit(vnode) {
|
export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEditUserModalAttrs> extends Modal<CustomAttrs> {
|
||||||
|
protected username!: Stream<string>;
|
||||||
|
protected email!: Stream<string>;
|
||||||
|
protected isEmailConfirmed!: Stream<boolean>;
|
||||||
|
protected setPassword!: Stream<boolean>;
|
||||||
|
protected password!: Stream<string>;
|
||||||
|
protected groups: Record<string, Stream<boolean>> = {};
|
||||||
|
|
||||||
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
const user = this.attrs.user;
|
const user = this.attrs.user;
|
||||||
@ -19,14 +30,15 @@ export default class EditUserModal extends Modal {
|
|||||||
this.username = Stream(user.username() || '');
|
this.username = Stream(user.username() || '');
|
||||||
this.email = Stream(user.email() || '');
|
this.email = Stream(user.email() || '');
|
||||||
this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false);
|
this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false);
|
||||||
this.setPassword = Stream(false);
|
this.setPassword = Stream(false as boolean);
|
||||||
this.password = Stream(user.password() || '');
|
this.password = Stream(user.password() || '');
|
||||||
this.groups = {};
|
|
||||||
|
const userGroups = user.groups() || [];
|
||||||
|
|
||||||
app.store
|
app.store
|
||||||
.all('groups')
|
.all<Group>('groups')
|
||||||
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
.filter((group) => ![Group.GUEST_ID, Group.MEMBER_ID].includes(group.id()!))
|
||||||
.forEach((group) => (this.groups[group.id()] = Stream(user.groups().indexOf(group) !== -1)));
|
.forEach((group) => (this.groups[group.id()!] = Stream(userGroups.includes(group))));
|
||||||
}
|
}
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
@ -49,7 +61,7 @@ export default class EditUserModal extends Modal {
|
|||||||
fields() {
|
fields() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
if (app.session.user.canEditCredentials()) {
|
if (app.session.user?.canEditCredentials()) {
|
||||||
items.add(
|
items.add(
|
||||||
'username',
|
'username',
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
@ -103,10 +115,11 @@ export default class EditUserModal extends Modal {
|
|||||||
<label className="checkbox">
|
<label className="checkbox">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onchange={(e) => {
|
onchange={(e: KeyboardEvent) => {
|
||||||
this.setPassword(e.target.checked);
|
const target = e.target as HTMLInputElement;
|
||||||
|
this.setPassword(target.checked);
|
||||||
m.redraw.sync();
|
m.redraw.sync();
|
||||||
if (e.target.checked) this.$('[name=password]').select();
|
if (target.checked) this.$('[name=password]').select();
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
}}
|
}}
|
||||||
disabled={this.nonAdminEditingAdmin()}
|
disabled={this.nonAdminEditingAdmin()}
|
||||||
@ -132,24 +145,31 @@ export default class EditUserModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.session.user.canEditGroups()) {
|
if (app.session.user?.canEditGroups()) {
|
||||||
items.add(
|
items.add(
|
||||||
'groups',
|
'groups',
|
||||||
<div className="Form-group EditUserModal-groups">
|
<div className="Form-group EditUserModal-groups">
|
||||||
<label>{app.translator.trans('core.lib.edit_user.groups_heading')}</label>
|
<label>{app.translator.trans('core.lib.edit_user.groups_heading')}</label>
|
||||||
<div>
|
<div>
|
||||||
{Object.keys(this.groups)
|
{Object.keys(this.groups)
|
||||||
.map((id) => app.store.getById('groups', id))
|
.map((id) => app.store.getById<Group>('groups', id))
|
||||||
.map((group) => (
|
.filter(Boolean)
|
||||||
<label className="checkbox">
|
.map(
|
||||||
<input
|
(group) =>
|
||||||
type="checkbox"
|
// Necessary because filter(Boolean) doesn't narrow out falsy values.
|
||||||
bidi={this.groups[group.id()]}
|
group && (
|
||||||
disabled={group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user))}
|
<label className="checkbox">
|
||||||
/>
|
<input
|
||||||
{GroupBadge.component({ group, label: '' })} {group.nameSingular()}
|
type="checkbox"
|
||||||
</label>
|
bidi={this.groups[group.id()!]}
|
||||||
))}
|
disabled={
|
||||||
|
group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{GroupBadge.component({ group, label: '' })} {group.nameSingular()}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
10
|
10
|
||||||
@ -194,9 +214,8 @@ export default class EditUserModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
const data = {
|
const data: SaveAttributes = {};
|
||||||
relationships: {},
|
const relationships: SaveRelationships = {};
|
||||||
};
|
|
||||||
|
|
||||||
if (this.attrs.user.canEditCredentials() && !this.nonAdminEditingAdmin()) {
|
if (this.attrs.user.canEditCredentials() && !this.nonAdminEditingAdmin()) {
|
||||||
data.username = this.username();
|
data.username = this.username();
|
||||||
@ -211,15 +230,18 @@ export default class EditUserModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.attrs.user.canEditGroups()) {
|
if (this.attrs.user.canEditGroups()) {
|
||||||
data.relationships.groups = Object.keys(this.groups)
|
relationships.groups = Object.keys(this.groups)
|
||||||
.filter((id) => this.groups[id]())
|
.filter((id) => this.groups[id]())
|
||||||
.map((id) => app.store.getById('groups', id));
|
.map((id) => app.store.getById<Group>('groups', id))
|
||||||
|
.filter((g): g is Group => g instanceof Group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.relationships = relationships;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
onsubmit(e) {
|
onsubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@ -239,9 +261,8 @@ export default class EditUserModal extends Modal {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
userIsAdmin(user) {
|
protected userIsAdmin(user: User | null) {
|
||||||
return user.groups().some((g) => g.id() === Group.ADMINISTRATOR_ID);
|
return user && (user.groups() || []).some((g) => g?.id() === Group.ADMINISTRATOR_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -30,9 +30,9 @@ export default abstract class Modal<ModalAttrs extends IInternalModalAttrs = IIn
|
|||||||
/**
|
/**
|
||||||
* Attributes for an alert component to show below the header.
|
* Attributes for an alert component to show below the header.
|
||||||
*/
|
*/
|
||||||
alertAttrs!: AlertAttrs;
|
alertAttrs: AlertAttrs | null = null;
|
||||||
|
|
||||||
oninit(vnode: Mithril.VnodeDOM<ModalAttrs, this>) {
|
oninit(vnode: Mithril.Vnode<ModalAttrs, this>) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
// TODO: [Flarum 2.0] Remove the code below.
|
// TODO: [Flarum 2.0] Remove the code below.
|
||||||
@ -122,7 +122,7 @@ export default abstract class Modal<ModalAttrs extends IInternalModalAttrs = IIn
|
|||||||
/**
|
/**
|
||||||
* Get the title of the modal dialog.
|
* Get the title of the modal dialog.
|
||||||
*/
|
*/
|
||||||
abstract title(): string;
|
abstract title(): Mithril.Children;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content of the modal.
|
* Get the content of the modal.
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import Modal from './Modal';
|
import type RequestError from '../utils/RequestError';
|
||||||
|
import Modal, { IInternalModalAttrs } from './Modal';
|
||||||
|
|
||||||
export default class RequestErrorModal extends Modal {
|
export interface IRequestErrorModalAttrs extends IInternalModalAttrs {
|
||||||
|
error: RequestError;
|
||||||
|
formattedError: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RequestErrorModal<CustomAttrs extends IRequestErrorModalAttrs = IRequestErrorModalAttrs> extends Modal<CustomAttrs> {
|
||||||
className() {
|
className() {
|
||||||
return 'RequestErrorModal Modal--large';
|
return 'RequestErrorModal Modal--large';
|
||||||
}
|
}
|
||||||
@ -18,14 +24,10 @@ export default class RequestErrorModal extends Modal {
|
|||||||
// else try to parse it as JSON and stringify it with indentation
|
// else try to parse it as JSON and stringify it with indentation
|
||||||
if (formattedError) {
|
if (formattedError) {
|
||||||
responseText = formattedError.join('\n\n');
|
responseText = formattedError.join('\n\n');
|
||||||
|
} else if (error.response) {
|
||||||
|
responseText = JSON.stringify(error.response, null, 2);
|
||||||
} else {
|
} else {
|
||||||
try {
|
responseText = error.responseText;
|
||||||
const json = error.response || JSON.parse(error.responseText);
|
|
||||||
|
|
||||||
responseText = JSON.stringify(json, null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
responseText = error.responseText;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
@ -27,7 +27,7 @@ export default class AlertManagerState {
|
|||||||
*/
|
*/
|
||||||
show(children: Mithril.Children): AlertIdentifier;
|
show(children: Mithril.Children): AlertIdentifier;
|
||||||
show(attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
|
show(attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
|
||||||
show(componentClass: Alert, attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
|
show(componentClass: typeof Alert, attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
|
||||||
|
|
||||||
show(arg1: any, arg2?: any, arg3?: any) {
|
show(arg1: any, arg2?: any, arg3?: any) {
|
||||||
// Assigns variables as per the above signatures
|
// Assigns variables as per the above signatures
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
|
import type Component from '../Component';
|
||||||
import Modal from '../components/Modal';
|
import Modal from '../components/Modal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ideally, `show` would take a higher-kinded generic, ala:
|
||||||
|
* `show<Attrs, C>(componentClass: C<Attrs>, attrs: Attrs): void`
|
||||||
|
* Unfortunately, TypeScript does not support this:
|
||||||
|
* https://github.com/Microsoft/TypeScript/issues/1213
|
||||||
|
* Therefore, we have to use this ugly, messy workaround.
|
||||||
|
*/
|
||||||
|
type UnsafeModalClass = ComponentClass<any, Modal> & { isDismissible: boolean; component: typeof Component.component };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to manage modal state.
|
* Class used to manage modal state.
|
||||||
*
|
*
|
||||||
@ -10,7 +20,7 @@ export default class ModalManagerState {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
modal: null | {
|
modal: null | {
|
||||||
componentClass: typeof Modal;
|
componentClass: UnsafeModalClass;
|
||||||
attrs?: Record<string, unknown>;
|
attrs?: Record<string, unknown>;
|
||||||
} = null;
|
} = null;
|
||||||
|
|
||||||
@ -28,7 +38,7 @@ export default class ModalManagerState {
|
|||||||
* // This "hack" is needed due to quirks with nested redraws in Mithril.
|
* // This "hack" is needed due to quirks with nested redraws in Mithril.
|
||||||
* setTimeout(() => app.modal.show(MyCoolModal, { attr: 'value' }), 0);
|
* setTimeout(() => app.modal.show(MyCoolModal, { attr: 'value' }), 0);
|
||||||
*/
|
*/
|
||||||
show(componentClass: typeof Modal, attrs: Record<string, unknown> = {}): void {
|
show(componentClass: UnsafeModalClass, attrs: Record<string, unknown> = {}): void {
|
||||||
if (!(componentClass.prototype instanceof Modal)) {
|
if (!(componentClass.prototype instanceof Modal)) {
|
||||||
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
||||||
const invalidModalWarning = 'The ModalManager can only show Modals.';
|
const invalidModalWarning = 'The ModalManager can only show Modals.';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import app from '../../common/app';
|
import app from '../../common/app';
|
||||||
|
import extractText from './extractText';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `abbreviateNumber` utility converts a number to a shorter localized form.
|
* The `abbreviateNumber` utility converts a number to a shorter localized form.
|
||||||
@ -10,9 +11,9 @@ import app from '../../common/app';
|
|||||||
export default function abbreviateNumber(number: number): string {
|
export default function abbreviateNumber(number: number): string {
|
||||||
// TODO: translation
|
// TODO: translation
|
||||||
if (number >= 1000000) {
|
if (number >= 1000000) {
|
||||||
return Math.floor(number / 1000000) + app.translator.trans('core.lib.number_suffix.mega_text');
|
return Math.floor(number / 1000000) + extractText(app.translator.trans('core.lib.number_suffix.mega_text'));
|
||||||
} else if (number >= 1000) {
|
} else if (number >= 1000) {
|
||||||
return (number / 1000).toFixed(1) + app.translator.trans('core.lib.number_suffix.kilo_text');
|
return (number / 1000).toFixed(1) + extractText(app.translator.trans('core.lib.number_suffix.kilo_text'));
|
||||||
} else {
|
} else {
|
||||||
return number.toString();
|
return number.toString();
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import isSafariMobile from './utils/isSafariMobile';
|
|||||||
import type Notification from './components/Notification';
|
import type Notification from './components/Notification';
|
||||||
import type Post from './components/Post';
|
import type Post from './components/Post';
|
||||||
import Discussion from '../common/models/Discussion';
|
import Discussion from '../common/models/Discussion';
|
||||||
|
import extractText from '../common/utils/extractText';
|
||||||
|
|
||||||
export default class ForumApplication extends Application {
|
export default class ForumApplication extends Application {
|
||||||
/**
|
/**
|
||||||
@ -99,7 +100,7 @@ export default class ForumApplication extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.routes[defaultAction].path = '/';
|
this.routes[defaultAction].path = '/';
|
||||||
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
|
this.history.push(defaultAction, extractText(this.translator.trans('core.forum.header.back_to_index_tooltip')), '/');
|
||||||
|
|
||||||
this.pane = new Pane(document.getElementById('app'));
|
this.pane = new Pane(document.getElementById('app'));
|
||||||
|
|
||||||
@ -124,8 +125,9 @@ export default class ForumApplication extends Application {
|
|||||||
app.history.home();
|
app.history.home();
|
||||||
|
|
||||||
// Reload the current user so that their unread notification count is refreshed.
|
// Reload the current user so that their unread notification count is refreshed.
|
||||||
if (app.session.user) {
|
const userId = app.session.user?.id();
|
||||||
app.store.find('users', app.session.user.id());
|
if (userId) {
|
||||||
|
app.store.find('users', userId);
|
||||||
m.redraw();
|
m.redraw();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,34 +1,31 @@
|
|||||||
import app from '../../forum/app';
|
import app from '../../forum/app';
|
||||||
import Modal from '../../common/components/Modal';
|
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import extractText from '../../common/utils/extractText';
|
import extractText from '../../common/utils/extractText';
|
||||||
import Stream from '../../common/utils/Stream';
|
import Stream from '../../common/utils/Stream';
|
||||||
|
import Mithril from 'mithril';
|
||||||
|
import RequestError from '../../common/utils/RequestError';
|
||||||
|
|
||||||
|
export interface IForgotPasswordModalAttrs extends IInternalModalAttrs {
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `ForgotPasswordModal` component displays a modal which allows the user to
|
* The `ForgotPasswordModal` component displays a modal which allows the user to
|
||||||
* enter their email address and request a link to reset their password.
|
* enter their email address and request a link to reset their password.
|
||||||
*
|
|
||||||
* ### Attrs
|
|
||||||
*
|
|
||||||
* - `email`
|
|
||||||
*/
|
*/
|
||||||
export default class ForgotPasswordModal extends Modal {
|
export default class ForgotPasswordModal<CustomAttrs extends IForgotPasswordModalAttrs = IForgotPasswordModalAttrs> extends Modal<CustomAttrs> {
|
||||||
oninit(vnode) {
|
/**
|
||||||
|
* The value of the email input.
|
||||||
|
*/
|
||||||
|
email!: Stream<string>;
|
||||||
|
|
||||||
|
success: boolean = false;
|
||||||
|
|
||||||
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the email input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.email = Stream(this.attrs.email || '');
|
this.email = Stream(this.attrs.email || '');
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the password reset email was sent successfully.
|
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
|
||||||
this.success = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
@ -84,7 +81,7 @@ export default class ForgotPasswordModal extends Modal {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onsubmit(e) {
|
onsubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@ -98,14 +95,14 @@ export default class ForgotPasswordModal extends Modal {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.success = true;
|
this.success = true;
|
||||||
this.alert = null;
|
this.alertAttrs = null;
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.then(this.loaded.bind(this));
|
.then(this.loaded.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
onerror(error) {
|
onerror(error: RequestError) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404 && error.alert) {
|
||||||
error.alert.content = app.translator.trans('core.forum.forgot_password.not_found_message');
|
error.alert.content = app.translator.trans('core.forum.forgot_password.not_found_message');
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import app from '../../forum/app';
|
import app from '../../forum/app';
|
||||||
import Modal from '../../common/components/Modal';
|
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
|
||||||
import ForgotPasswordModal from './ForgotPasswordModal';
|
import ForgotPasswordModal from './ForgotPasswordModal';
|
||||||
import SignUpModal from './SignUpModal';
|
import SignUpModal from './SignUpModal';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
@ -7,38 +7,34 @@ import LogInButtons from './LogInButtons';
|
|||||||
import extractText from '../../common/utils/extractText';
|
import extractText from '../../common/utils/extractText';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import Stream from '../../common/utils/Stream';
|
import Stream from '../../common/utils/Stream';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
import RequestError from '../../common/utils/RequestError';
|
||||||
|
|
||||||
/**
|
export interface ILoginModalAttrs extends IInternalModalAttrs {
|
||||||
* The `LogInModal` component displays a modal dialog with a login form.
|
identification?: string;
|
||||||
*
|
password?: string;
|
||||||
* ### Attrs
|
remember?: boolean;
|
||||||
*
|
}
|
||||||
* - `identification`
|
|
||||||
* - `password`
|
export default class LogInModal<CustomAttrs extends ILoginModalAttrs = ILoginModalAttrs> extends Modal<CustomAttrs> {
|
||||||
*/
|
/**
|
||||||
export default class LogInModal extends Modal {
|
* The value of the identification input.
|
||||||
oninit(vnode) {
|
*/
|
||||||
|
identification!: Stream<string>;
|
||||||
|
/**
|
||||||
|
* The value of the password input.
|
||||||
|
*/
|
||||||
|
password!: Stream<string>;
|
||||||
|
/**
|
||||||
|
* The value of the remember me input.
|
||||||
|
*/
|
||||||
|
remember!: Stream<boolean>;
|
||||||
|
|
||||||
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the identification input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.identification = Stream(this.attrs.identification || '');
|
this.identification = Stream(this.attrs.identification || '');
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the password input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.password = Stream(this.attrs.password || '');
|
this.password = Stream(this.attrs.password || '');
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the remember me input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.remember = Stream(!!this.attrs.remember);
|
this.remember = Stream(!!this.attrs.remember);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,12 +136,10 @@ export default class LogInModal extends Modal {
|
|||||||
/**
|
/**
|
||||||
* Open the forgot password modal, prefilling it with an email if the user has
|
* Open the forgot password modal, prefilling it with an email if the user has
|
||||||
* entered one.
|
* entered one.
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
*/
|
||||||
forgotPassword() {
|
forgotPassword() {
|
||||||
const email = this.identification();
|
const email = this.identification();
|
||||||
const attrs = email.indexOf('@') !== -1 ? { email } : undefined;
|
const attrs = email.includes('@') ? { email } : undefined;
|
||||||
|
|
||||||
app.modal.show(ForgotPasswordModal, attrs);
|
app.modal.show(ForgotPasswordModal, attrs);
|
||||||
}
|
}
|
||||||
@ -153,13 +147,14 @@ export default class LogInModal extends Modal {
|
|||||||
/**
|
/**
|
||||||
* Open the sign up modal, prefilling it with an email/username/password if
|
* Open the sign up modal, prefilling it with an email/username/password if
|
||||||
* the user has entered one.
|
* the user has entered one.
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
*/
|
||||||
signUp() {
|
signUp() {
|
||||||
const attrs = { password: this.password() };
|
|
||||||
const identification = this.identification();
|
const identification = this.identification();
|
||||||
attrs[identification.indexOf('@') !== -1 ? 'email' : 'username'] = identification;
|
|
||||||
|
const attrs = {
|
||||||
|
[identification.includes('@') ? 'email' : 'username']: identification,
|
||||||
|
password: this.password(),
|
||||||
|
};
|
||||||
|
|
||||||
app.modal.show(SignUpModal, attrs);
|
app.modal.show(SignUpModal, attrs);
|
||||||
}
|
}
|
||||||
@ -168,7 +163,7 @@ export default class LogInModal extends Modal {
|
|||||||
this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').select();
|
this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').select();
|
||||||
}
|
}
|
||||||
|
|
||||||
onsubmit(e) {
|
onsubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@ -182,8 +177,8 @@ export default class LogInModal extends Modal {
|
|||||||
.then(() => window.location.reload(), this.loaded.bind(this));
|
.then(() => window.location.reload(), this.loaded.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
onerror(error) {
|
onerror(error: RequestError) {
|
||||||
if (error.status === 401) {
|
if (error.status === 401 && error.alert) {
|
||||||
error.alert.content = app.translator.trans('core.forum.log_in.invalid_login_message');
|
error.alert.content = app.translator.trans('core.forum.log_in.invalid_login_message');
|
||||||
}
|
}
|
||||||
|
|
@ -1,45 +1,47 @@
|
|||||||
import app from '../../forum/app';
|
import app from '../../forum/app';
|
||||||
import Modal from '../../common/components/Modal';
|
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
|
||||||
import LogInModal from './LogInModal';
|
import LogInModal from './LogInModal';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import LogInButtons from './LogInButtons';
|
import LogInButtons from './LogInButtons';
|
||||||
import extractText from '../../common/utils/extractText';
|
import extractText from '../../common/utils/extractText';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import Stream from '../../common/utils/Stream';
|
import Stream from '../../common/utils/Stream';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
|
||||||
/**
|
export interface ISignupModalAttrs extends IInternalModalAttrs {
|
||||||
* The `SignUpModal` component displays a modal dialog with a singup form.
|
username?: string;
|
||||||
*
|
email?: string;
|
||||||
* ### Attrs
|
password?: string;
|
||||||
*
|
token?: string;
|
||||||
* - `username`
|
provided?: string[];
|
||||||
* - `email`
|
}
|
||||||
* - `password`
|
|
||||||
* - `token` An email token to sign up with.
|
export type SignupBody = {
|
||||||
*/
|
username: string;
|
||||||
export default class SignUpModal extends Modal {
|
email: string;
|
||||||
oninit(vnode) {
|
} & ({ token: string } | { password: string });
|
||||||
|
|
||||||
|
export default class SignUpModal<CustomAttrs extends ISignupModalAttrs = ISignupModalAttrs> extends Modal<CustomAttrs> {
|
||||||
|
/**
|
||||||
|
* The value of the username input.
|
||||||
|
*/
|
||||||
|
username!: Stream<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the email input.
|
||||||
|
*/
|
||||||
|
email!: Stream<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the password input.
|
||||||
|
*/
|
||||||
|
password!: Stream<string>;
|
||||||
|
|
||||||
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the username input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.username = Stream(this.attrs.username || '');
|
this.username = Stream(this.attrs.username || '');
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the email input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.email = Stream(this.attrs.email || '');
|
this.email = Stream(this.attrs.email || '');
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of the password input.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
this.password = Stream(this.attrs.password || '');
|
this.password = Stream(this.attrs.password || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,12 +57,12 @@ export default class SignUpModal extends Modal {
|
|||||||
return [<div className="Modal-body">{this.body()}</div>, <div className="Modal-footer">{this.footer()}</div>];
|
return [<div className="Modal-body">{this.body()}</div>, <div className="Modal-footer">{this.footer()}</div>];
|
||||||
}
|
}
|
||||||
|
|
||||||
isProvided(field) {
|
isProvided(field: string): boolean {
|
||||||
return this.attrs.provided && this.attrs.provided.indexOf(field) !== -1;
|
return this.attrs.provided?.includes(field) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
body() {
|
body() {
|
||||||
return [this.attrs.token ? '' : <LogInButtons />, <div className="Form Form--centered">{this.fields().toArray()}</div>];
|
return [!this.attrs.token && <LogInButtons />, <div className="Form Form--centered">{this.fields().toArray()}</div>];
|
||||||
}
|
}
|
||||||
|
|
||||||
fields() {
|
fields() {
|
||||||
@ -156,7 +158,7 @@ export default class SignUpModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onsubmit(e) {
|
onsubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@ -175,22 +177,16 @@ export default class SignUpModal extends Modal {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the data that should be submitted in the sign-up request.
|
* Get the data that should be submitted in the sign-up request.
|
||||||
*
|
|
||||||
* @return {Object}
|
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
submitData() {
|
submitData(): SignupBody {
|
||||||
|
const authData = this.attrs.token ? { token: this.attrs.token } : { password: this.password() };
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
username: this.username(),
|
username: this.username(),
|
||||||
email: this.email(),
|
email: this.email(),
|
||||||
|
...authData,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.attrs.token) {
|
|
||||||
data.token = this.attrs.token;
|
|
||||||
} else {
|
|
||||||
data.password = this.password();
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,7 +15,7 @@ export default class NotificationListState extends PaginatedListState<Notificati
|
|||||||
* Load the next page of notification results.
|
* Load the next page of notification results.
|
||||||
*/
|
*/
|
||||||
load(): Promise<void> {
|
load(): Promise<void> {
|
||||||
if (app.session.user.newNotificationCount()) {
|
if (app.session.user?.newNotificationCount()) {
|
||||||
this.pages = [];
|
this.pages = [];
|
||||||
this.location = { page: 1 };
|
this.location = { page: 1 };
|
||||||
}
|
}
|
||||||
@ -24,7 +24,7 @@ export default class NotificationListState extends PaginatedListState<Notificati
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.session.user.pushAttributes({ newNotificationCount: 0 });
|
app.session.user?.pushAttributes({ newNotificationCount: 0 });
|
||||||
|
|
||||||
return super.loadNext();
|
return super.loadNext();
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export default class NotificationListState extends PaginatedListState<Notificati
|
|||||||
markAllAsRead() {
|
markAllAsRead() {
|
||||||
if (this.pages.length === 0) return;
|
if (this.pages.length === 0) return;
|
||||||
|
|
||||||
app.session.user.pushAttributes({ unreadNotificationCount: 0 });
|
app.session.user?.pushAttributes({ unreadNotificationCount: 0 });
|
||||||
|
|
||||||
this.pages.forEach((page) => {
|
this.pages.forEach((page) => {
|
||||||
page.items.forEach((notification) => notification.pushAttributes({ isRead: true }));
|
page.items.forEach((notification) => notification.pushAttributes({ isRead: true }));
|
||||||
|
Reference in New Issue
Block a user