From 3aa87cc26fd58836b82c067b58a47e08e30a7784 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 4 Jun 2020 00:18:50 +0800 Subject: refactor(front): Make codes lint-clean! --- Timeline/ClientApp/src/App.tsx | 2 +- Timeline/ClientApp/src/admin/UserAdmin.tsx | 45 +++++++++-------- Timeline/ClientApp/src/common.ts | 4 ++ Timeline/ClientApp/src/common/AppBar.tsx | 2 +- Timeline/ClientApp/src/common/ImageCropper.tsx | 40 ++++++++++----- Timeline/ClientApp/src/common/OperationDialog.tsx | 35 ++++++------- Timeline/ClientApp/src/data/common.ts | 9 +++- Timeline/ClientApp/src/data/timeline.ts | 5 +- Timeline/ClientApp/src/data/user.ts | 59 +++++++++++++--------- Timeline/ClientApp/src/home/Home.tsx | 6 +-- .../ClientApp/src/home/TimelineCreateDialog.tsx | 16 +++--- Timeline/ClientApp/src/i18n.ts | 11 ++-- Timeline/ClientApp/src/settings/Settings.tsx | 9 ++-- .../src/timeline/TimelineDeleteDialog.tsx | 12 ++--- .../ClientApp/src/timeline/TimelineInfoCard.tsx | 17 +++---- Timeline/ClientApp/src/timeline/TimelineMember.tsx | 24 +++++---- .../src/timeline/TimelinePageTemplate.tsx | 35 +++++++++---- .../src/timeline/TimelinePageTemplateUI.tsx | 4 +- .../ClientApp/src/timeline/TimelinePostEdit.tsx | 41 +++++++++------ Timeline/ClientApp/src/user/ChangeAvatarDialog.tsx | 11 ++-- Timeline/ClientApp/src/user/Login.tsx | 3 +- Timeline/ClientApp/src/user/User.tsx | 13 ++++- Timeline/ClientApp/src/user/UserInfoCard.tsx | 20 ++------ Timeline/ClientApp/src/user/api.ts | 5 +- 24 files changed, 244 insertions(+), 184 deletions(-) create mode 100644 Timeline/ClientApp/src/common.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/App.tsx b/Timeline/ClientApp/src/App.tsx index d3cfbd8a..befa2a9c 100644 --- a/Timeline/ClientApp/src/App.tsx +++ b/Timeline/ClientApp/src/App.tsx @@ -32,7 +32,7 @@ const App: React.FC = () => { const user = useOptionalUser(); React.useEffect(() => { - checkUserLoginState(); + void checkUserLoginState(); }, []); let body; diff --git a/Timeline/ClientApp/src/admin/UserAdmin.tsx b/Timeline/ClientApp/src/admin/UserAdmin.tsx index afef79c9..fc752011 100644 --- a/Timeline/ClientApp/src/admin/UserAdmin.tsx +++ b/Timeline/ClientApp/src/admin/UserAdmin.tsx @@ -18,7 +18,7 @@ import { User, UserWithToken } from '../data/user'; import { apiBaseUrl } from '../config'; async function fetchUserList(_token: string): Promise { - const res = await axios.get(`${apiBaseUrl}/users`); + const res = await axios.get(`${apiBaseUrl}/users`); return res.data; } @@ -295,7 +295,7 @@ const UserAdmin: React.FC = (props) => { | { type: 'create'; } - | { type: 'delete'; username: string } + | { type: TDelete; username: string } | { type: TChangeUsername; username: string; @@ -317,7 +317,7 @@ const UserAdmin: React.FC = (props) => { useEffect(() => { let subscribe = true; - fetchUserList(props.user.token).then((us) => { + void fetchUserList(props.user.token).then((us) => { if (subscribe) { setUsers(us); } @@ -337,7 +337,7 @@ const UserAdmin: React.FC = (props) => { close={() => setDialog(null)} process={async (user) => { const u = await createUser(user, token); - setUsers((oldUsers) => [...oldUsers!, u]); + setUsers((oldUsers) => [...(oldUsers ?? []), u]); }} /> ); @@ -351,7 +351,7 @@ const UserAdmin: React.FC = (props) => { process={async () => { await deleteUser(dialog.username, token); setUsers((oldUsers) => - oldUsers!.filter((u) => u.username !== dialog.username) + (oldUsers ?? []).filter((u) => u.username !== dialog.username) ); }} /> @@ -366,10 +366,11 @@ const UserAdmin: React.FC = (props) => { process={async (newUsername) => { await changeUsername(dialog.username, newUsername, token); setUsers((oldUsers) => { - const users = oldUsers!.slice(); - users.find( + const users = (oldUsers ?? []).slice(); + const findedUser = users.find( (u) => u.username === dialog.username - )!.username = newUsername; + ); + if (findedUser) findedUser.username = newUsername; return users; }); }} @@ -399,10 +400,11 @@ const UserAdmin: React.FC = (props) => { process={async () => { await changePermission(dialog.username, newPermission, token); setUsers((oldUsers) => { - const users = oldUsers!.slice(); - users.find( + const users = (oldUsers ?? []).slice(); + const findedUser = users.find( (u) => u.username === dialog.username - )!.administrator = newPermission; + ); + if (findedUser) findedUser.administrator = newPermission; return users; }); }} @@ -419,15 +421,18 @@ const UserAdmin: React.FC = (props) => { key={user.username} user={user} onContextMenu={(item) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dialogInfo: any = { - type: item, - username: user.username, - }; - if (item === 'changepermission') { - dialogInfo.newPermission = !user.administrator; - } - setDialog(dialogInfo); + setDialog( + item === kChangePermission + ? { + type: kChangePermission, + username: user.username, + newPermission: !user.administrator, + } + : { + type: item, + username: user.username, + } + ); }} /> ); diff --git a/Timeline/ClientApp/src/common.ts b/Timeline/ClientApp/src/common.ts new file mode 100644 index 00000000..673f1c98 --- /dev/null +++ b/Timeline/ClientApp/src/common.ts @@ -0,0 +1,4 @@ +// This error is thrown when ui goes wrong with bad logic. +// Such as am variable should not be null, but it does. +// This error should never occur. If it does, it indicates there is some logic bug in codes. +export class UiLogicError extends Error {} diff --git a/Timeline/ClientApp/src/common/AppBar.tsx b/Timeline/ClientApp/src/common/AppBar.tsx index 39794b0f..4f61798c 100644 --- a/Timeline/ClientApp/src/common/AppBar.tsx +++ b/Timeline/ClientApp/src/common/AppBar.tsx @@ -10,7 +10,7 @@ import { useOptionalVersionedAvatarUrl } from '../user/api'; import TimelineLogo from './TimelineLogo'; -const AppBar: React.FC<{}> = (_) => { +const AppBar: React.FC = (_) => { const history = useHistory(); const user = useUser(); const avatarUrl = useOptionalVersionedAvatarUrl(user?._links?.avatar); diff --git a/Timeline/ClientApp/src/common/ImageCropper.tsx b/Timeline/ClientApp/src/common/ImageCropper.tsx index 3ef35b44..fb9bb4f9 100644 --- a/Timeline/ClientApp/src/common/ImageCropper.tsx +++ b/Timeline/ClientApp/src/common/ImageCropper.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import clsx from 'clsx'; +import { UiLogicError } from '../common'; + export interface Clip { left: number; top: number; @@ -58,11 +60,11 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => { const c = normalizeClip(clip); - const imgElement = React.useRef(null); + const imgElementRef = React.useRef(null); const onImageRef = React.useCallback( (e: HTMLImageElement | null) => { - imgElement.current = e; + imgElementRef.current = e; if (imageElementCallback != null && e == null) { imageElementCallback(null); } @@ -123,9 +125,13 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => { const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; + const { current: imgElement } = imgElementRef; + + if (imgElement == null) throw new UiLogicError('Image element is null.'); + const moveRatio = { - x: movement.x / imgElement.current!.width, - y: movement.y / imgElement.current!.height, + x: movement.x / imgElement.width, + y: movement.y / imgElement.height, }; const newRatio = { @@ -158,9 +164,13 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => { const ratio = imageInfo == null ? 1 : imageInfo.ratio; + const { current: imgElement } = imgElementRef; + + if (imgElement == null) throw new UiLogicError('Image element is null.'); + const moveRatio = { - x: movement.x / imgElement.current!.width, - y: movement.x / imgElement.current!.width / ratio, + x: movement.x / imgElement.width, + y: movement.x / imgElement.width / ratio, }; const newRatio = { @@ -189,6 +199,8 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => { [imageInfo, oldState, onChange] ); + const toPercentage = (n: number): string => `${n}%`; + // fuck!!! I just can't find a better way to implement this in pure css const containerStyle: React.CSSProperties = (() => { if (imageInfo == null) { @@ -196,14 +208,14 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => { } else { if (imageInfo.ratio > 1) { return { - width: 100 / imageInfo.ratio + '%', + width: toPercentage(100 / imageInfo.ratio), paddingTop: '100%', height: 0, }; } else { return { width: '100%', - paddingTop: 100 * imageInfo.ratio + '%', + paddingTop: toPercentage(100 * imageInfo.ratio), height: 0, }; } @@ -221,10 +233,10 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => { className="image-cropper-mask" touch-action="none" style={{ - left: c.left * 100 + '%', - top: c.top * 100 + '%', - width: c.width * 100 + '%', - height: c.height * 100 + '%', + left: toPercentage(c.left * 100), + top: toPercentage(c.top * 100), + width: toPercentage(c.width * 100), + height: toPercentage(c.height * 100), }} onPointerMove={onPointerMove} onPointerDown={onPointerDown} @@ -235,8 +247,8 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => { className="image-cropper-handler" touch-action="none" style={{ - left: 'calc(' + (c.left + c.width) * 100 + '% - 15px)', - top: 'calc(' + (c.top + c.height) * 100 + '% - 15px)', + left: `calc(${(c.left + c.width) * 100}% - 15px)`, + top: `calc(${(c.top + c.height) * 100}% - 15px)`, }} onPointerMove={onHandlerPointerMove} onPointerDown={onPointerDown} diff --git a/Timeline/ClientApp/src/common/OperationDialog.tsx b/Timeline/ClientApp/src/common/OperationDialog.tsx index e7b6612c..501a353e 100644 --- a/Timeline/ClientApp/src/common/OperationDialog.tsx +++ b/Timeline/ClientApp/src/common/OperationDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState, InputHTMLAttributes } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Spinner, @@ -12,10 +12,12 @@ import { Button, Modal, ModalHeader, - FormText + FormText, } from 'reactstrap'; -const DefaultProcessPrompt: React.FC = _ => { +import { UiLogicError } from '../common'; + +const DefaultProcessPrompt: React.FC = (_) => { return ( @@ -27,7 +29,7 @@ interface DefaultErrorPromptProps { error?: string; } -const DefaultErrorPrompt: React.FC = props => { +const DefaultErrorPrompt: React.FC = (props) => { const { t } = useTranslation(); let result =

{t('operationDialog.error')}

; @@ -111,7 +113,7 @@ interface OperationDialogProps { onSuccessAndClose?: () => void; } -const OperationDialog: React.FC = props => { +const OperationDialog: React.FC = (props) => { const inputScheme = props.inputScheme ?? []; const { t } = useTranslation(); @@ -119,13 +121,13 @@ const OperationDialog: React.FC = props => { type Step = 'input' | 'process' | OperationResult; const [step, setStep] = useState('input'); const [values, setValues] = useState<(boolean | string)[]>( - inputScheme.map(i => { + inputScheme.map((i) => { if (i.type === 'bool') { return i.initValue ?? false; } else if (i.type === 'text' || i.type === 'select') { return i.initValue ?? ''; } else { - throw new Error('Unknown input scheme.'); + throw new UiLogicError('Unknown input scheme.'); } }) ); @@ -149,16 +151,16 @@ const OperationDialog: React.FC = props => { const onConfirm = (): void => { setStep('process'); props.onProcess(values).then( - d => { + (d: unknown) => { setStep({ type: 'success', - data: d + data: d, }); }, - e => { + (e: unknown) => { setStep({ type: 'failure', - data: e + data: e, }); } ); @@ -205,8 +207,7 @@ const OperationDialog: React.FC = props => { const newInputError: OperationInputErrorInfo = { ...oldError }; for (const [index, error] of Object.entries(newError)) { if (error !== undefined) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (newInputError as any)[index] = error; + newInputError[+index] = error as OperationInputOptionalError; } } return newInputError; @@ -236,7 +237,7 @@ const OperationDialog: React.FC = props => { {inputPrompt} {inputScheme.map((item, index) => { const value = values[index]; - const error: string | undefined = (e => + const error: string | undefined = ((e) => typeof e === 'string' ? t(e) : undefined)(inputError?.[index]); if (item.type === 'text') { @@ -246,7 +247,7 @@ const OperationDialog: React.FC = props => { { + onChange={(e) => { const v = e.target.value; const newValues = updateValue(index, v); setInputError( @@ -270,7 +271,7 @@ const OperationDialog: React.FC = props => { { + onChange={(e) => { updateValue( index, (e.target as HTMLInputElement).checked @@ -287,7 +288,7 @@ const OperationDialog: React.FC = props => { { + onChange={(event) => { updateValue(index, event.target.value); }} > diff --git a/Timeline/ClientApp/src/data/common.ts b/Timeline/ClientApp/src/data/common.ts index 61db8bd2..e2882a80 100644 --- a/Timeline/ClientApp/src/data/common.ts +++ b/Timeline/ClientApp/src/data/common.ts @@ -9,9 +9,14 @@ export function extractStatusCode(error: AxiosError): number | null { } } +export interface CommonErrorResponse { + code: number; + message: string; +} + export function extractErrorCode(error: AxiosError): number | null { - const code = - error.response && error.response.data && error.response.data.code; + const { response } = error as AxiosError; + const code = response && response.data && response.data.code; if (typeof code === 'number') { return code; } else { diff --git a/Timeline/ClientApp/src/data/timeline.ts b/Timeline/ClientApp/src/data/timeline.ts index bc5e1658..66d9e57a 100644 --- a/Timeline/ClientApp/src/data/timeline.ts +++ b/Timeline/ClientApp/src/data/timeline.ts @@ -4,6 +4,7 @@ import XRegExp from 'xregexp'; import { base64 } from './base64'; import { apiBaseUrl } from '../config'; import { User, UserAuthInfo, getCurrentUser, UserWithToken } from './user'; +import { UiLogicError } from '../common'; export const kTimelineVisibilities = ['Public', 'Register', 'Private'] as const; @@ -159,7 +160,7 @@ export class TimelineServiceTemplate< private checkUser(): UserWithToken { const user = getCurrentUser(); if (user == null) { - throw new Error('You must login to perform the operation.'); + throw new UiLogicError('You must login to perform the operation.'); } return user; } @@ -204,7 +205,7 @@ export class TimelineServiceTemplate< RawCreatePostRequestContent >((resolve) => { if (request.content.type === 'image') { - base64(request.content.data).then((d) => + void base64(request.content.data).then((d) => resolve({ ...request.content, data: d, diff --git a/Timeline/ClientApp/src/data/user.ts b/Timeline/ClientApp/src/data/user.ts index 755aecf6..b85f4f27 100644 --- a/Timeline/ClientApp/src/data/user.ts +++ b/Timeline/ClientApp/src/data/user.ts @@ -3,8 +3,10 @@ import { useState, useEffect } from 'react'; import { BehaviorSubject, Observable } from 'rxjs'; import { apiBaseUrl } from '../config'; +import { extractErrorCode } from './common'; import { pushAlert } from '../common/alert-service'; import { i18nPromise } from '../i18n'; +import { UiLogicError } from '../common'; export interface UserAuthInfo { username: string; @@ -63,30 +65,30 @@ const verifyTokenUrl = apiBaseUrl + kVerifyTokenUrl; function verifyToken(token: string): Promise { return axios .post(verifyTokenUrl, { - token: token + token: token, } as VerifyTokenRequest) - .then(res => res.data.user); + .then((res) => res.data.user); } const TOKEN_STORAGE_KEY = 'token'; export function checkUserLoginState(): Promise { if (getCurrentUser() !== undefined) - throw new Error("Already checked user. Can't check twice."); + throw new UiLogicError("Already checked user. Can't check twice."); const savedToken = window.localStorage.getItem(TOKEN_STORAGE_KEY); if (savedToken) { return verifyToken(savedToken) .then( - u => { + (u) => { const user: UserWithToken = { ...u, - token: savedToken + token: savedToken, }; - i18nPromise.then(t => { + void i18nPromise.then((t) => { pushAlert({ type: 'success', - message: t('user.welcomeBack') + message: t('user.welcomeBack'), }); }); return user; @@ -94,17 +96,17 @@ export function checkUserLoginState(): Promise { (e: AxiosError) => { if (e.response != null) { window.localStorage.removeItem(TOKEN_STORAGE_KEY); - i18nPromise.then(t => { + void i18nPromise.then((t) => { pushAlert({ type: 'danger', - message: t('user.verifyTokenFailed') + message: t('user.verifyTokenFailed'), }); }); } else { - i18nPromise.then(t => { + void i18nPromise.then((t) => { pushAlert({ type: 'danger', - message: t('user.verifyTokenFailedNetwork') + message: t('user.verifyTokenFailedNetwork'), }); }); } @@ -112,7 +114,7 @@ export function checkUserLoginState(): Promise { return null; } ) - .then(u => { + .then((u) => { userSubject.next(u); return u; }); @@ -132,18 +134,17 @@ export function userLogin( rememberMe: boolean ): Promise { if (getCurrentUser()) { - throw new Error('Already login.'); + throw new UiLogicError('Already login.'); } return axios .post(createTokenUrl, { ...credentials, expire: 30 }) - .catch(e => { - const error = e as AxiosError; - if (error.response?.data?.code === 11010101) { + .catch((e: AxiosError) => { + if (extractErrorCode(e) === 11010101) { throw new BadCredentialError(e); } throw e; }) - .then(res => { + .then((res) => { const body = res.data; const token = body.token; if (rememberMe) { @@ -151,7 +152,7 @@ export function userLogin( } const user = { ...body.user, - token + token, }; userSubject.next(user); return user; @@ -160,10 +161,10 @@ export function userLogin( export function userLogout(): void { if (getCurrentUser() === undefined) { - throw new Error('Please check user first.'); + throw new UiLogicError('Please check user first.'); } if (getCurrentUser() === null) { - throw new Error('No login.'); + throw new UiLogicError('No login.'); } window.localStorage.removeItem(TOKEN_STORAGE_KEY); userSubject.next(null); @@ -174,7 +175,7 @@ export function useOptionalUser(): UserWithToken | null | undefined { userSubject.value ); useEffect(() => { - const sub = user$.subscribe(u => setUser(u)); + const sub = user$.subscribe((u) => setUser(u)); return () => { sub.unsubscribe(); }; @@ -186,16 +187,16 @@ export function useUser(): UserWithToken | null { const [user, setUser] = useState(() => { const initUser = userSubject.value; if (initUser === undefined) { - throw new Error( + throw new UiLogicError( "This is a logic error in user module. Current user can't be undefined in useUser." ); } return initUser; }); useEffect(() => { - const sub = user$.subscribe(u => { + const sub = user$.subscribe((u) => { if (u === undefined) { - throw new Error( + throw new UiLogicError( "This is a logic error in user module. User emitted can't be undefined later." ); } @@ -208,8 +209,16 @@ export function useUser(): UserWithToken | null { return user; } +export function useUserLoggedIn(): UserWithToken { + const user = useUser(); + if (user == null) { + throw new UiLogicError('You assert user has logged in but actually not.'); + } + return user; +} + export function fetchUser(username: string): Promise { return axios .get(`${apiBaseUrl}/users/${username}`) - .then(res => res.data); + .then((res) => res.data); } diff --git a/Timeline/ClientApp/src/home/Home.tsx b/Timeline/ClientApp/src/home/Home.tsx index 25398a0a..495781d0 100644 --- a/Timeline/ClientApp/src/home/Home.tsx +++ b/Timeline/ClientApp/src/home/Home.tsx @@ -38,7 +38,7 @@ const Home: React.FC = (_) => { if (user == null) { setOwnTimelines(undefined); setJoinTimelines(undefined); - axios + void axios .get(`${apiBaseUrl}/timelines?visibility=public`) .then((res) => { if (subscribe) { @@ -47,7 +47,7 @@ const Home: React.FC = (_) => { }); } else { setPublicTimelines(undefined); - axios + void axios .get( `${apiBaseUrl}/timelines?relate=${user.username}&relateType=own` ) @@ -56,7 +56,7 @@ const Home: React.FC = (_) => { setOwnTimelines(res.data); } }); - axios + void axios .get( `${apiBaseUrl}/timelines?relate=${user.username}&relateType=join` ) diff --git a/Timeline/ClientApp/src/home/TimelineCreateDialog.tsx b/Timeline/ClientApp/src/home/TimelineCreateDialog.tsx index 30d29bc8..27c9d0d6 100644 --- a/Timeline/ClientApp/src/home/TimelineCreateDialog.tsx +++ b/Timeline/ClientApp/src/home/TimelineCreateDialog.tsx @@ -3,7 +3,7 @@ import { useHistory } from 'react-router'; import axios from 'axios'; import { apiBaseUrl } from '../config'; -import { useUser } from '../data/user'; +import { useUserLoggedIn } from '../data/user'; import { validateTimelineName } from '../data/timeline'; import OperationDialog from '../common/OperationDialog'; @@ -13,9 +13,9 @@ interface TimelineCreateDialogProps { close: () => void; } -const TimelineCreateDialog: React.FC = props => { +const TimelineCreateDialog: React.FC = (props) => { const history = useHistory(); - const user = useUser()!; + const user = useUserLoggedIn(); let nameSaved: string; @@ -30,7 +30,7 @@ const TimelineCreateDialog: React.FC = props => { type: 'text', label: 'home.createDialog.name', helperText: 'home.createDialog.nameFormat', - validator: name => { + validator: (name) => { if (name.length === 0) { return 'home.createDialog.noEmpty'; } else if (name.length > 26) { @@ -40,19 +40,19 @@ const TimelineCreateDialog: React.FC = props => { } else { return null; } - } - } + }, + }, ]} onProcess={([name]) => { nameSaved = name as string; return axios.post(`${apiBaseUrl}/timelines?token=${user.token}`, { - name + name, }); }} onSuccessAndClose={() => { history.push(`timelines/${nameSaved}`); }} - failurePrompt={e => (e as object).toString()} + failurePrompt={(e) => `${e as string}`} /> ); }; diff --git a/Timeline/ClientApp/src/i18n.ts b/Timeline/ClientApp/src/i18n.ts index 9f3a0801..a752cfa4 100644 --- a/Timeline/ClientApp/src/i18n.ts +++ b/Timeline/ClientApp/src/i18n.ts @@ -6,8 +6,7 @@ const backend: BackendModule = { type: 'backend', async read(language, namespace, callback) { function error(message: string): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback(new Error(message), false as any); + callback(new Error(message), false); } function success(result: ResourceKey): void { @@ -37,7 +36,7 @@ const backend: BackendModule = { } }, init() {}, // eslint-disable-line @typescript-eslint/no-empty-function - create() {} // eslint-disable-line @typescript-eslint/no-empty-function + create() {}, // eslint-disable-line @typescript-eslint/no-empty-function }; export const i18nPromise = i18n @@ -51,8 +50,8 @@ export const i18nPromise = i18n debug: process.env.NODE_ENV === 'development', interpolation: { - escapeValue: false // not needed for react!! - } + escapeValue: false, // not needed for react!! + }, // react i18next special options (optional) // override if needed - omit if ok with defaults @@ -72,7 +71,7 @@ if (module.hot) { module.hot.accept( ['./locales/en/translation', './locales/zh/translation'], () => { - i18n.reloadResources(); + void i18n.reloadResources(); } ); } diff --git a/Timeline/ClientApp/src/settings/Settings.tsx b/Timeline/ClientApp/src/settings/Settings.tsx index b3996fdc..075f8699 100644 --- a/Timeline/ClientApp/src/settings/Settings.tsx +++ b/Timeline/ClientApp/src/settings/Settings.tsx @@ -6,12 +6,13 @@ import { Container, Row, Col, Input } from 'reactstrap'; import { apiBaseUrl } from '../config'; -import { useUser, userLogout } from '../data/user'; +import { useUser, userLogout, useUserLoggedIn } from '../data/user'; import AppBar from '../common/AppBar'; import OperationDialog, { OperationInputErrorInfo, } from '../common/OperationDialog'; +import { CommonErrorResponse } from '../data/common'; interface ChangePasswordDialogProps { open: boolean; @@ -30,7 +31,7 @@ async function changePassword( newPassword, }); } catch (e) { - const error = e as AxiosError; + const error = e as AxiosError; if ( error.response && error.response.status === 400 && @@ -44,7 +45,7 @@ async function changePassword( } const ChangePasswordDialog: React.FC = (props) => { - const user = useUser()!; + const user = useUserLoggedIn(); const history = useHistory(); const { t } = useTranslation(); @@ -176,7 +177,7 @@ const Settings: React.FC = (_) => { type="select" value={language} onChange={(e) => { - i18n.changeLanguage(e.target.value); + void i18n.changeLanguage(e.target.value); }} > diff --git a/Timeline/ClientApp/src/timeline/TimelineDeleteDialog.tsx b/Timeline/ClientApp/src/timeline/TimelineDeleteDialog.tsx index 2b682a6b..e2d8ad5a 100644 --- a/Timeline/ClientApp/src/timeline/TimelineDeleteDialog.tsx +++ b/Timeline/ClientApp/src/timeline/TimelineDeleteDialog.tsx @@ -4,7 +4,7 @@ import { useHistory } from 'react-router'; import { Trans } from 'react-i18next'; import { apiBaseUrl } from '../config'; -import { useUser } from '../data/user'; +import { useUserLoggedIn } from '../data/user'; import OperationDialog from '../common/OperationDialog'; interface TimelineDeleteDialog { @@ -13,8 +13,8 @@ interface TimelineDeleteDialog { close: () => void; } -const TimelineDeleteDialog: React.FC = props => { - const user = useUser()!; +const TimelineDeleteDialog: React.FC = (props) => { + const user = useUserLoggedIn(); const history = useHistory(); const { name } = props; @@ -35,14 +35,14 @@ const TimelineDeleteDialog: React.FC = props => { inputScheme={[ { type: 'text', - validator: value => { + validator: (value) => { if (value !== name) { return 'timeline.deleteDialog.notMatch'; } else { return null; } - } - } + }, + }, ]} onProcess={() => { return axios.delete( diff --git a/Timeline/ClientApp/src/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/timeline/TimelineInfoCard.tsx index 2ce7c378..b3af4cab 100644 --- a/Timeline/ClientApp/src/timeline/TimelineInfoCard.tsx +++ b/Timeline/ClientApp/src/timeline/TimelineInfoCard.tsx @@ -28,6 +28,7 @@ const TimelineInfoCard: React.FC = (props) => { const { t } = useTranslation(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = React.useRef(null!); const notifyHeight = React.useCallback((): void => { @@ -48,13 +49,6 @@ const TimelineInfoCard: React.FC = (props) => { (): void => setManageDropdownOpen((old) => !old), [] ); - const onManageProperty = React.useCallback( - (): void => onManage!('property'), - [onManage] - ); - const onManageDelete = React.useCallback((): void => onManage!('delete'), [ - onManage, - ]); return (
= (props) => { {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])}
- {props.onManage != null ? ( + {onManage != null ? ( {t('timeline.manage')} - + onManage('property')}> {t('timeline.manageItem.property')} {t('timeline.manageItem.member')} - + onManage('delete')} + > {t('timeline.manageItem.delete')} diff --git a/Timeline/ClientApp/src/timeline/TimelineMember.tsx b/Timeline/ClientApp/src/timeline/TimelineMember.tsx index eac8d417..b8bb49ee 100644 --- a/Timeline/ClientApp/src/timeline/TimelineMember.tsx +++ b/Timeline/ClientApp/src/timeline/TimelineMember.tsx @@ -11,7 +11,7 @@ import { Modal, Row, Col, - Button + Button, } from 'reactstrap'; export interface TimelineMemberCallbacks { @@ -25,7 +25,7 @@ export interface TimelineMemberProps { edit: TimelineMemberCallbacks | null | undefined; } -const TimelineMember: React.FC = props => { +const TimelineMember: React.FC = (props) => { const { t } = useTranslation(); const [userSearchText, setUserSearchText] = useState(''); @@ -87,7 +87,7 @@ const TimelineMember: React.FC = props => { <> { + onChange={(v) => { setUserSearchText(v); }} loading={userSearchState.type === 'loading'} @@ -95,27 +95,27 @@ const TimelineMember: React.FC = props => { if (userSearchText === '') { setUserSearchState({ type: 'error', - data: 'login.emptyUsername' + data: 'login.emptyUsername', }); return; } setUserSearchState({ type: 'loading' }); edit.onCheckUser(userSearchText).then( - u => { + (u) => { if (u == null) { setUserSearchState({ type: 'error', - data: 'timeline.userNotExist' + data: 'timeline.userNotExist', }); } else { setUserSearchState({ type: 'user', data: u }); } }, - e => { + (e) => { setUserSearchState({ type: 'error', - data: e.toString() + data: `${e as string}`, }); } ); @@ -125,7 +125,7 @@ const TimelineMember: React.FC = props => { if (userSearchState.type === 'user') { const u = userSearchState.data; const addable = - members.findIndex(m => m.username === u.username) === -1; + members.findIndex((m) => m.username === u.username) === -1; return ( <> {!addable ? ( @@ -150,7 +150,7 @@ const TimelineMember: React.FC = props => { className="align-self-center" disabled={!addable} onClick={() => { - edit.onAddUser(u).then(_ => { + void edit.onAddUser(u).then((_) => { setUserSearchText(''); setUserSearchState({ type: 'init' }); }); @@ -185,7 +185,9 @@ export interface TimelineMemberDialogProps extends TimelineMemberProps { onClose: () => void; } -export const TimelineMemberDialog: React.FC = props => { +export const TimelineMemberDialog: React.FC = ( + props +) => { return ( diff --git a/Timeline/ClientApp/src/timeline/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/timeline/TimelinePageTemplate.tsx index 3660ad78..2cfb4341 100644 --- a/Timeline/ClientApp/src/timeline/TimelinePageTemplate.tsx +++ b/Timeline/ClientApp/src/timeline/TimelinePageTemplate.tsx @@ -19,6 +19,7 @@ import { TimelineMemberDialog } from './TimelineMember'; import TimelinePropertyChangeDialog from './TimelinePropertyChangeDialog'; import { TimelinePageTemplateUIProps } from './TimelinePageTemplateUI'; import { TimelinePostSendCallback } from './TimelinePostEdit'; +import { UiLogicError } from '../common'; export interface TimelinePageTemplateProps< TManageItem, @@ -88,7 +89,7 @@ export default function TimelinePageTemplate< }, (error) => { if (subscribe) { - setError(error.toString()); + setError(`${error as string}`); } } ); @@ -129,13 +130,19 @@ export default function TimelinePageTemplate< let dialogElement: React.ReactElement | undefined; if (dialog === 'property') { + if (timeline == null) { + throw new UiLogicError( + 'Timeline is null but attempt to open change property dialog.' + ); + } + dialogElement = ( { return service.changeProperty(name, req).then((newTimeline) => { @@ -145,13 +152,19 @@ export default function TimelinePageTemplate< /> ); } else if (dialog === 'member') { + if (timeline == null) { + throw new UiLogicError( + 'Timeline is null but attempt to open change property dialog.' + ); + } + dialogElement = ( { return fetchUser(u).catch((e) => { @@ -168,18 +181,18 @@ export default function TimelinePageTemplate< onAddUser: (u) => { return service.addMember(name, u.username).then((_) => { setTimeline({ - ...timeline!, - members: concat(timeline!.members, u), + ...timeline, + members: concat(timeline.members, u), }); }); }, onRemoveUser: (u) => { - service.removeMember(name, u).then((_) => { + void service.removeMember(name, u).then((_) => { setTimeline({ - ...timeline!, + ...timeline, members: without( - timeline!.members, - timeline!.members.find((m) => m.username === u) + timeline.members, + timeline.members.find((m) => m.username === u) ), }); }); diff --git a/Timeline/ClientApp/src/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/timeline/TimelinePageTemplateUI.tsx index d96b3260..22fb1987 100644 --- a/Timeline/ClientApp/src/timeline/TimelinePageTemplateUI.tsx +++ b/Timeline/ClientApp/src/timeline/TimelinePageTemplateUI.tsx @@ -44,7 +44,7 @@ export default function TimelinePageTemplateUI( const onPostEditHeightChange = React.useCallback((height: number): void => { const { current: bottomSpaceDiv } = bottomSpaceRef; if (bottomSpaceDiv != null) { - bottomSpaceDiv.style.height = height + 'px'; + bottomSpaceDiv.style.height = `${height}px`; } if (height === 0) { const alertHost = getAlertHost(); @@ -54,7 +54,7 @@ export default function TimelinePageTemplateUI( } else { const alertHost = getAlertHost(); if (alertHost != null) { - alertHost.style.marginBottom = height + 'px'; + alertHost.style.marginBottom = `${height}px`; } } }, []); diff --git a/Timeline/ClientApp/src/timeline/TimelinePostEdit.tsx b/Timeline/ClientApp/src/timeline/TimelinePostEdit.tsx index fe1fda9b..2b76b03b 100644 --- a/Timeline/ClientApp/src/timeline/TimelinePostEdit.tsx +++ b/Timeline/ClientApp/src/timeline/TimelinePostEdit.tsx @@ -1,12 +1,13 @@ import React from 'react'; import clsx from 'clsx'; -import { Container, Button, Spinner, Row, Col } from 'reactstrap'; +import { Button, Spinner, Row, Col } from 'reactstrap'; import { useTranslation } from 'react-i18next'; import { pushAlert } from '../common/alert-service'; import { CreatePostRequest } from '../data/timeline'; import FileInput from '../common/FileInput'; +import { UiLogicError } from '../common'; interface TimelinePostEditImageProps { onSelect: (blob: Blob | null) => void; @@ -96,11 +97,12 @@ const TimelinePostEdit: React.FC = (props) => { const canSend = kind === 'text' || (kind === 'image' && imageBlob != null); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const containerRef = React.useRef(null!); + React.useEffect(() => { if (props.onHeightChange) { - props.onHeightChange( - document.getElementById('timeline-post-edit-area')!.clientHeight - ); + props.onHeightChange(containerRef.current.clientHeight); } return () => { if (props.onHeightChange) { @@ -117,20 +119,31 @@ const TimelinePostEdit: React.FC = (props) => { const onSend = React.useCallback(() => { setState('process'); - const req: CreatePostRequest = - kind === 'text' - ? { + const req: CreatePostRequest = (() => { + switch (kind) { + case 'text': + return { content: { type: 'text', text: text, }, + } as CreatePostRequest; + case 'image': + if (imageBlob == null) { + throw new UiLogicError( + 'Content type is image but image blob is null.' + ); } - : { + return { content: { type: 'image', - data: imageBlob!, + data: imageBlob, }, - }; + } as CreatePostRequest; + default: + throw new UiLogicError('Unknown content type.'); + } + })(); onPost(req).then( (_) => { @@ -155,11 +168,7 @@ const TimelinePostEdit: React.FC = (props) => { }, []); return ( - +
{kind === 'text' ? ( @@ -198,7 +207,7 @@ const TimelinePostEdit: React.FC = (props) => { })()} - +
); }; diff --git a/Timeline/ClientApp/src/user/ChangeAvatarDialog.tsx b/Timeline/ClientApp/src/user/ChangeAvatarDialog.tsx index e082d5a0..f7b25252 100644 --- a/Timeline/ClientApp/src/user/ChangeAvatarDialog.tsx +++ b/Timeline/ClientApp/src/user/ChangeAvatarDialog.tsx @@ -11,6 +11,7 @@ import { import { AxiosError } from 'axios'; import ImageCropper, { Clip, applyClipToImage } from '../common/ImageCropper'; +import { UiLogicError } from '../common'; export interface ChangeAvatarDialogProps { open: boolean; @@ -107,11 +108,11 @@ const ChangeAvatarDialog: React.FC = (props) => { clip.width === 0 || file == null ) { - throw new Error('Invalid state.'); + throw new UiLogicError(); } setState('processcrop'); - applyClipToImage(cropImgElement, clip, file.type).then((b) => { + void applyClipToImage(cropImgElement, clip, file.type).then((b) => { setResultBlob(b); }); }, [cropImgElement, clip, file]); @@ -130,7 +131,7 @@ const ChangeAvatarDialog: React.FC = (props) => { const upload = React.useCallback(() => { if (resultBlob == null) { - throw new Error('Invalid state.'); + throw new UiLogicError(); } setState('uploading'); @@ -147,7 +148,7 @@ const ChangeAvatarDialog: React.FC = (props) => { const createPreviewRow = (): React.ReactElement => { if (resultUrl == null) { - throw new Error('Invalid state.'); + throw new UiLogicError(); } return ( @@ -182,7 +183,7 @@ const ChangeAvatarDialog: React.FC = (props) => { ); } else if (state === 'crop') { if (fileUrl == null) { - throw new Error('Invalid state.'); + throw new UiLogicError(); } return ( <> diff --git a/Timeline/ClientApp/src/user/Login.tsx b/Timeline/ClientApp/src/user/Login.tsx index fc9364c0..24cd58d0 100644 --- a/Timeline/ClientApp/src/user/Login.tsx +++ b/Timeline/ClientApp/src/user/Login.tsx @@ -1,6 +1,7 @@ import React, { Fragment, useState, useEffect } from 'react'; import { useHistory } from 'react-router'; import { useTranslation } from 'react-i18next'; +import { AxiosError } from 'axios'; import AppBar from '../common/AppBar'; @@ -68,7 +69,7 @@ const Login: React.FC = (_) => { history.goBack(); } }, - (e) => { + (e: AxiosError | Error) => { setProcess(false); setError(e.message); } diff --git a/Timeline/ClientApp/src/user/User.tsx b/Timeline/ClientApp/src/user/User.tsx index 7bdd64b7..a281be42 100644 --- a/Timeline/ClientApp/src/user/User.tsx +++ b/Timeline/ClientApp/src/user/User.tsx @@ -10,6 +10,7 @@ import ChangeNicknameDialog from './ChangeNicknameDialog'; import ChangeAvatarDialog from './ChangeAvatarDialog'; import TimelinePageTemplate from '../timeline/TimelinePageTemplate'; import { PersonalTimelineManageItem } from './UserInfoCard'; +import { UiLogicError } from '../common'; const User: React.FC = (_) => { const { username } = useParams<{ username: string }>(); @@ -26,12 +27,16 @@ const User: React.FC = (_) => { }; if (dialog === 'nickname') { + if (user == null) { + throw new UiLogicError('Change nickname without login.'); + } + dialogElement = ( { - const p = changeNickname(user!.token, username, newNickname); + const p = changeNickname(user.token, username, newNickname); return p.then((_) => { setDataKey(dataKey + 1); }); @@ -39,11 +44,15 @@ const User: React.FC = (_) => { /> ); } else if (dialog === 'avatar') { + if (user == null) { + throw new UiLogicError('Change avatar without login.'); + } + dialogElement = ( changeAvatar(user!.token, username, file, file.type)} + process={(file) => changeAvatar(user.token, username, file, file.type)} /> ); } diff --git a/Timeline/ClientApp/src/user/UserInfoCard.tsx b/Timeline/ClientApp/src/user/UserInfoCard.tsx index 280cddce..3f812a8b 100644 --- a/Timeline/ClientApp/src/user/UserInfoCard.tsx +++ b/Timeline/ClientApp/src/user/UserInfoCard.tsx @@ -39,6 +39,7 @@ const UserInfoCard: React.FC = (props) => { props.timeline.owner._links.avatar ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = React.useRef(null!); const notifyHeight = React.useCallback((): void => { @@ -59,17 +60,6 @@ const UserInfoCard: React.FC = (props) => { (): void => setManageDropdownOpen((old) => !old), [] ); - const onManageProperty = React.useCallback( - (): void => onManage!('property'), - [onManage] - ); - const onManageAvatar = React.useCallback((): void => onManage!('avatar'), [ - onManage, - ]); - const onManageNickname = React.useCallback( - (): void => onManage!('nickname'), - [onManage] - ); return (
= (props) => { {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])}
- {props.onManage != null ? ( + {onManage != null ? ( {t('timeline.manage')} - + onManage('nickname')}> {t('timeline.manageItem.nickname')} - + onManage('avatar')}> {t('timeline.manageItem.avatar')} - + onManage('property')}> {t('timeline.manageItem.property')} diff --git a/Timeline/ClientApp/src/user/api.ts b/Timeline/ClientApp/src/user/api.ts index 184c011f..583370e2 100644 --- a/Timeline/ClientApp/src/user/api.ts +++ b/Timeline/ClientApp/src/user/api.ts @@ -60,7 +60,7 @@ export function useOptionalVersionedAvatarUrl( ? undefined : updateQueryString( 'v', - avatarVersion == null ? null : avatarVersion + '', + avatarVersion == null ? null : avatarVersion.toString(), url ), [avatarVersion, url] @@ -72,7 +72,8 @@ export function useAvatarUrlWithGivenVersion( url: string ): string { return React.useMemo( - () => updateQueryString('v', version == null ? null : version + '', url), + () => + updateQueryString('v', version == null ? null : version.toString(), url), [version, url] ); } -- cgit v1.2.3