1
0
mirror of https://github.com/flarum/core.git synced 2025-07-16 14:26:25 +02:00

Merge pull request #3196 from flarum/as/finish-typing

Finish typing, enable error on TypeScript check failure
This commit is contained in:
Alexander Skvortsov
2021-12-13 22:07:39 -05:00
committed by GitHub
20 changed files with 228 additions and 181 deletions

View File

@ -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:

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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
*/ */

View File

@ -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;

View File

@ -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.
*/ */

View File

@ -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;
} }

View File

@ -7,11 +7,8 @@ export type LoginParams = {
* The username/email * The username/email
*/ */
identification: string; identification: string;
/**
* Password
*/
password: string; password: string;
remember: boolean;
}; };
/** /**

View File

@ -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)
.map(
(group) =>
// Necessary because filter(Boolean) doesn't narrow out falsy values.
group && (
<label className="checkbox"> <label className="checkbox">
<input <input
type="checkbox" type="checkbox"
bidi={this.groups[group.id()]} bidi={this.groups[group.id()!]}
disabled={group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user))} disabled={
group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user))
}
/> />
{GroupBadge.component({ group, label: '' })} {group.nameSingular()} {GroupBadge.component({ group, label: '' })} {group.nameSingular()}
</label> </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);
} }
} }

View File

@ -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.

View File

@ -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,15 +24,11 @@ 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 {
const json = error.response || JSON.parse(error.responseText);
responseText = JSON.stringify(json, null, 2);
} catch (e) {
responseText = error.responseText; responseText = error.responseText;
} }
}
return ( return (
<div className="Modal-body"> <div className="Modal-body">

View File

@ -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

View File

@ -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.';

View File

@ -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();
} }

View File

@ -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();
} }
}); });

View File

@ -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) {
super.oninit(vnode);
/** /**
* The value of the email input. * The value of the email input.
*
* @type {Function}
*/ */
this.email = Stream(this.attrs.email || ''); email!: Stream<string>;
/** success: boolean = false;
* Whether or not the password reset email was sent successfully.
* oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
* @type {Boolean} super.oninit(vnode);
*/
this.success = false; this.email = Stream(this.attrs.email || '');
} }
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');
} }

View File

@ -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 extends Modal {
oninit(vnode) {
super.oninit(vnode);
export default class LogInModal<CustomAttrs extends ILoginModalAttrs = ILoginModalAttrs> extends Modal<CustomAttrs> {
/** /**
* The value of the identification input. * The value of the identification input.
*
* @type {Function}
*/ */
this.identification = Stream(this.attrs.identification || ''); identification!: Stream<string>;
/** /**
* The value of the password input. * The value of the password input.
*
* @type {Function}
*/ */
this.password = Stream(this.attrs.password || ''); password!: Stream<string>;
/** /**
* The value of the remember me input. * The value of the remember me input.
*
* @type {Function}
*/ */
remember!: Stream<boolean>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.identification = Stream(this.attrs.identification || '');
this.password = Stream(this.attrs.password || '');
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');
} }

View File

@ -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 default class SignUpModal extends Modal {
oninit(vnode) {
super.oninit(vnode);
export type SignupBody = {
username: string;
email: string;
} & ({ token: string } | { password: string });
export default class SignUpModal<CustomAttrs extends ISignupModalAttrs = ISignupModalAttrs> extends Modal<CustomAttrs> {
/** /**
* The value of the username input. * The value of the username input.
*
* @type {Function}
*/ */
this.username = Stream(this.attrs.username || ''); username!: Stream<string>;
/** /**
* The value of the email input. * The value of the email input.
*
* @type {Function}
*/ */
this.email = Stream(this.attrs.email || ''); email!: Stream<string>;
/** /**
* The value of the password input. * The value of the password input.
*
* @type {Function}
*/ */
password!: Stream<string>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.username = Stream(this.attrs.username || '');
this.email = Stream(this.attrs.email || '');
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;
} }
} }

View File

@ -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 }));