Modal typescript cleanup and conversions

This commit is contained in:
Alexander Skvortsov 2021-12-12 18:36:51 -05:00
parent da6ae898b2
commit e1c6028dce
11 changed files with 182 additions and 162 deletions

View File

@ -1,7 +1,9 @@
import app from '../../admin/app';
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
export default class LoadingModal<ModalAttrs extends IInternalModalAttrs = IInternalModalAttrs> extends Modal<ModalAttrs> {
export interface ILoadingModalAttrs extends IInternalModalAttrs {}
export default class LoadingModal<ModalAttrs extends ILoadingModalAttrs = ILoadingModalAttrs> extends Modal<ModalAttrs> {
/**
* @inheritdoc
*/

View File

@ -1,11 +1,21 @@
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 Placeholder from '../../common/components/Placeholder';
import ExtensionReadme from '../models/ExtensionReadme';
import type Mithril from 'mithril';
import { Extension } from '../AdminApplication';
export default class ReadmeModal extends Modal {
oninit(vnode) {
export interface IReadmeModalAttrs extends IInternalModalAttrs {
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);
app.store.models['extension-readmes'] = ExtensionReadme;

View File

@ -32,11 +32,11 @@ export interface SavedModelData {
export type ModelData = UnsavedModelData | SavedModelData;
interface SaveRelationships {
export interface SaveRelationships {
[relationship: string]: Model | Model[];
}
interface SaveAttributes {
export interface SaveAttributes {
[key: string]: unknown;
relationships?: SaveRelationships;
}

View File

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

View File

@ -1,17 +1,28 @@
import app from '../../common/app';
import Modal from './Modal';
import Modal, { IInternalModalAttrs } from './Modal';
import Button from './Button';
import GroupBadge from './GroupBadge';
import Group from '../models/Group';
import extractText from '../utils/extractText';
import ItemList from '../utils/ItemList';
import Stream from '../utils/Stream';
import Mithril from 'mithril';
import User from '../models/User';
import { SaveAttributes, SaveRelationships } from '../Model';
/**
* The `EditUserModal` component displays a modal dialog with a login form.
*/
export default class EditUserModal extends Modal {
oninit(vnode) {
export interface IEditUserModalAttrs extends IInternalModalAttrs {
user: User;
}
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);
const user = this.attrs.user;
@ -19,14 +30,15 @@ export default class EditUserModal extends Modal {
this.username = Stream(user.username() || '');
this.email = Stream(user.email() || '');
this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false);
this.setPassword = Stream(false);
this.setPassword = Stream(false as boolean);
this.password = Stream(user.password() || '');
this.groups = {};
const userGroups = user.groups() || [];
app.store
.all('groups')
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.forEach((group) => (this.groups[group.id()] = Stream(user.groups().indexOf(group) !== -1)));
.all<Group>('groups')
.filter((group) => ![Group.GUEST_ID, Group.MEMBER_ID].includes(group.id()!))
.forEach((group) => (this.groups[group.id()!] = Stream(userGroups.includes(group))));
}
className() {
@ -49,7 +61,7 @@ export default class EditUserModal extends Modal {
fields() {
const items = new ItemList();
if (app.session.user.canEditCredentials()) {
if (app.session.user?.canEditCredentials()) {
items.add(
'username',
<div className="Form-group">
@ -103,10 +115,11 @@ export default class EditUserModal extends Modal {
<label className="checkbox">
<input
type="checkbox"
onchange={(e) => {
this.setPassword(e.target.checked);
onchange={(e: KeyboardEvent) => {
const target = e.target as HTMLInputElement;
this.setPassword(target.checked);
m.redraw.sync();
if (e.target.checked) this.$('[name=password]').select();
if (target.checked) this.$('[name=password]').select();
e.redraw = false;
}}
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(
'groups',
<div className="Form-group EditUserModal-groups">
<label>{app.translator.trans('core.lib.edit_user.groups_heading')}</label>
<div>
{Object.keys(this.groups)
.map((id) => app.store.getById('groups', id))
.map((group) => (
<label className="checkbox">
<input
type="checkbox"
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>
))}
.map((id) => app.store.getById<Group>('groups', id))
.filter(Boolean)
.map(
(group) =>
// Necessary because filter(Boolean) doesn't narrow out falsy values.
group && (
<label className="checkbox">
<input
type="checkbox"
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>,
10
@ -194,9 +214,8 @@ export default class EditUserModal extends Modal {
}
data() {
const data = {
relationships: {},
};
const data: SaveAttributes = {};
const relationships: SaveRelationships = {};
if (this.attrs.user.canEditCredentials() && !this.nonAdminEditingAdmin()) {
data.username = this.username();
@ -211,15 +230,18 @@ export default class EditUserModal extends Modal {
}
if (this.attrs.user.canEditGroups()) {
data.relationships.groups = Object.keys(this.groups)
relationships.groups = Object.keys(this.groups)
.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;
}
onsubmit(e) {
onsubmit(e: SubmitEvent) {
e.preventDefault();
this.loading = true;
@ -239,9 +261,8 @@ export default class EditUserModal extends Modal {
/**
* @internal
* @protected
*/
userIsAdmin(user) {
return user.groups().some((g) => g.id() === Group.ADMINISTRATOR_ID);
protected userIsAdmin(user: User | null) {
return user && (user.groups() || []).some((g) => g?.id() === Group.ADMINISTRATOR_ID);
}
}

View File

@ -32,7 +32,7 @@ export default abstract class Modal<ModalAttrs extends IInternalModalAttrs = IIn
*/
alertAttrs: AlertAttrs | null = null;
oninit(vnode: Mithril.VnodeDOM<ModalAttrs, this>) {
oninit(vnode: Mithril.Vnode<ModalAttrs, this>) {
super.oninit(vnode);
// TODO: [Flarum 2.0] Remove the code below.

View File

@ -1,6 +1,12 @@
import Modal from './Modal';
import 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() {
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
if (formattedError) {
responseText = formattedError.join('\n\n');
} else if (error.response) {
responseText = JSON.stringify(error.response, null, 2);
} 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 (

View File

@ -125,7 +125,7 @@ export default class ForumApplication extends Application {
app.history.home();
// Reload the current user so that their unread notification count is refreshed.
const userId = app.session.user?.id()
const userId = app.session.user?.id();
if (userId) {
app.store.find('users', userId);
m.redraw();

View File

@ -1,34 +1,31 @@
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 extractText from '../../common/utils/extractText';
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
* enter their email address and request a link to reset their password.
*
* ### Attrs
*
* - `email`
*/
export default class ForgotPasswordModal extends Modal {
oninit(vnode) {
export default class ForgotPasswordModal<CustomAttrs extends IForgotPasswordModalAttrs = IForgotPasswordModalAttrs> extends Modal<CustomAttrs> {
/**
* The value of the email input.
*/
email!: Stream<string>;
success: boolean = false;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
/**
* The value of the email input.
*
* @type {Function}
*/
this.email = Stream(this.attrs.email || '');
/**
* Whether or not the password reset email was sent successfully.
*
* @type {Boolean}
*/
this.success = false;
}
className() {
@ -84,7 +81,7 @@ export default class ForgotPasswordModal extends Modal {
);
}
onsubmit(e) {
onsubmit(e: SubmitEvent) {
e.preventDefault();
this.loading = true;
@ -98,14 +95,14 @@ export default class ForgotPasswordModal extends Modal {
})
.then(() => {
this.success = true;
this.alert = null;
this.alertAttrs = null;
})
.catch(() => {})
.then(this.loaded.bind(this));
}
onerror(error) {
if (error.status === 404) {
onerror(error: RequestError) {
if (error.status === 404 && error.alert) {
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 Modal from '../../common/components/Modal';
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
import ForgotPasswordModal from './ForgotPasswordModal';
import SignUpModal from './SignUpModal';
import Button from '../../common/components/Button';
@ -7,38 +7,34 @@ import LogInButtons from './LogInButtons';
import extractText from '../../common/utils/extractText';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
import type Mithril from 'mithril';
import RequestError from '../../common/utils/RequestError';
/**
* The `LogInModal` component displays a modal dialog with a login form.
*
* ### Attrs
*
* - `identification`
* - `password`
*/
export default class LogInModal extends Modal {
oninit(vnode) {
export interface ILoginModalAttrs extends IInternalModalAttrs {
identification?: string;
password?: string;
remember?: boolean;
}
export default class LogInModal<CustomAttrs extends ILoginModalAttrs = ILoginModalAttrs> extends Modal<CustomAttrs> {
/**
* The value of the identification input.
*/
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);
/**
* The value of the identification input.
*
* @type {Function}
*/
this.identification = Stream(this.attrs.identification || '');
/**
* The value of the password input.
*
* @type {Function}
*/
this.password = Stream(this.attrs.password || '');
/**
* The value of the remember me input.
*
* @type {Function}
*/
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
* entered one.
*
* @public
*/
forgotPassword() {
const email = this.identification();
const attrs = email.indexOf('@') !== -1 ? { email } : undefined;
const attrs = email.includes('@') ? { email } : undefined;
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
* the user has entered one.
*
* @public
*/
signUp() {
const attrs = { password: this.password() };
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);
}
@ -168,7 +163,7 @@ export default class LogInModal extends Modal {
this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').select();
}
onsubmit(e) {
onsubmit(e: SubmitEvent) {
e.preventDefault();
this.loading = true;
@ -182,8 +177,8 @@ export default class LogInModal extends Modal {
.then(() => window.location.reload(), this.loaded.bind(this));
}
onerror(error) {
if (error.status === 401) {
onerror(error: RequestError) {
if (error.status === 401 && error.alert) {
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 Modal from '../../common/components/Modal';
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
import LogInModal from './LogInModal';
import Button from '../../common/components/Button';
import LogInButtons from './LogInButtons';
import extractText from '../../common/utils/extractText';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
import type Mithril from 'mithril';
/**
* The `SignUpModal` component displays a modal dialog with a singup form.
*
* ### Attrs
*
* - `username`
* - `email`
* - `password`
* - `token` An email token to sign up with.
*/
export default class SignUpModal extends Modal {
oninit(vnode) {
export interface ISignupModalAttrs extends IInternalModalAttrs {
username?: string;
email?: string;
password?: string;
token?: string;
provided?: string[];
}
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.
*/
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);
/**
* The value of the username input.
*
* @type {Function}
*/
this.username = Stream(this.attrs.username || '');
/**
* The value of the email input.
*
* @type {Function}
*/
this.email = Stream(this.attrs.email || '');
/**
* The value of the password input.
*
* @type {Function}
*/
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>];
}
isProvided(field) {
return this.attrs.provided && this.attrs.provided.indexOf(field) !== -1;
isProvided(field: string): boolean {
return this.attrs.provided?.includes(field) ?? false;
}
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() {
@ -156,7 +158,7 @@ export default class SignUpModal extends Modal {
}
}
onsubmit(e) {
onsubmit(e: SubmitEvent) {
e.preventDefault();
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.
*
* @return {Object}
* @protected
*/
submitData() {
submitData(): SignupBody {
const authData = this.attrs.token ? { token: this.attrs.token } : { password: this.password() };
const data = {
username: this.username(),
email: this.email(),
...authData,
};
if (this.attrs.token) {
data.token = this.attrs.token;
} else {
data.password = this.password();
}
return data;
}
}