diff options
author | crupest <crupest@outlook.com> | 2020-06-04 00:18:50 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-06-04 00:18:50 +0800 |
commit | fccd6b4ca8ed7420f25f0c4298fde311bc1e09d0 (patch) | |
tree | 787cda66f8997ba842601a261a36b6de95398675 /Timeline/ClientApp/src | |
parent | 92e50c4a3ea250dc18c76bc8c29d86d486e63772 (diff) | |
download | timeline-fccd6b4ca8ed7420f25f0c4298fde311bc1e09d0.tar.gz timeline-fccd6b4ca8ed7420f25f0c4298fde311bc1e09d0.tar.bz2 timeline-fccd6b4ca8ed7420f25f0c4298fde311bc1e09d0.zip |
refactor(front): Make codes lint-clean!
Diffstat (limited to 'Timeline/ClientApp/src')
24 files changed, 244 insertions, 184 deletions
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<User[]> {
- const res = await axios.get(`${apiBaseUrl}/users`);
+ const res = await axios.get<User[]>(`${apiBaseUrl}/users`);
return res.data;
}
@@ -295,7 +295,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => { | {
type: 'create';
}
- | { type: 'delete'; username: string }
+ | { type: TDelete; username: string }
| {
type: TChangeUsername;
username: string;
@@ -317,7 +317,7 @@ const UserAdmin: React.FC<UserAdminProps> = (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<UserAdminProps> = (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<UserAdminProps> = (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<UserAdminProps> = (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<UserAdminProps> = (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<UserAdminProps> = (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<HTMLImageElement | null>(null);
+ const imgElementRef = React.useRef<HTMLImageElement | null>(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 (
<Container className="justify-content-center align-items-center">
<Spinner />
@@ -27,7 +29,7 @@ interface DefaultErrorPromptProps { error?: string;
}
-const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = props => {
+const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => {
const { t } = useTranslation();
let result = <p className="text-danger">{t('operationDialog.error')}</p>;
@@ -111,7 +113,7 @@ interface OperationDialogProps { onSuccessAndClose?: () => void;
}
-const OperationDialog: React.FC<OperationDialogProps> = props => {
+const OperationDialog: React.FC<OperationDialogProps> = (props) => {
const inputScheme = props.inputScheme ?? [];
const { t } = useTranslation();
@@ -119,13 +121,13 @@ const OperationDialog: React.FC<OperationDialogProps> = props => { type Step = 'input' | 'process' | OperationResult;
const [step, setStep] = useState<Step>('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<OperationDialogProps> = 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<OperationDialogProps> = 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<OperationDialogProps> = 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<OperationDialogProps> = props => { <Input
type={item.password === true ? 'password' : 'text'}
value={value as string}
- onChange={e => {
+ onChange={(e) => {
const v = e.target.value;
const newValues = updateValue(index, v);
setInputError(
@@ -270,7 +271,7 @@ const OperationDialog: React.FC<OperationDialogProps> = props => { <Input
type="checkbox"
value={value as string}
- onChange={e => {
+ onChange={(e) => {
updateValue(
index,
(e.target as HTMLInputElement).checked
@@ -287,7 +288,7 @@ const OperationDialog: React.FC<OperationDialogProps> = props => { <Input
type="select"
value={value as string}
- onChange={event => {
+ 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<CommonErrorResponse>;
+ 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<User> {
return axios
.post<VerifyTokenResponse>(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<UserWithToken | null> {
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<UserWithToken | null> { (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<UserWithToken | null> { return null;
}
)
- .then(u => {
+ .then((u) => {
userSubject.next(u);
return u;
});
@@ -132,18 +134,17 @@ export function userLogin( rememberMe: boolean
): Promise<UserWithToken> {
if (getCurrentUser()) {
- throw new Error('Already login.');
+ throw new UiLogicError('Already login.');
}
return axios
.post<CreateTokenResponse>(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<UserWithToken | null>(() => {
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<User> {
return axios
.get<User>(`${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<TimelineInfo[]>(`${apiBaseUrl}/timelines?visibility=public`)
.then((res) => {
if (subscribe) {
@@ -47,7 +47,7 @@ const Home: React.FC = (_) => { });
} else {
setPublicTimelines(undefined);
- axios
+ void axios
.get<TimelineInfo[]>(
`${apiBaseUrl}/timelines?relate=${user.username}&relateType=own`
)
@@ -56,7 +56,7 @@ const Home: React.FC = (_) => { setOwnTimelines(res.data);
}
});
- axios
+ void axios
.get<TimelineInfo[]>(
`${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<TimelineCreateDialogProps> = props => {
+const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = (props) => {
const history = useHistory();
- const user = useUser()!;
+ const user = useUserLoggedIn();
let nameSaved: string;
@@ -30,7 +30,7 @@ const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = 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<TimelineCreateDialogProps> = 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<CommonErrorResponse>;
if (
error.response &&
error.response.status === 400 &&
@@ -44,7 +45,7 @@ async function changePassword( }
const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (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);
}}
>
<option value="zh">中文</option>
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<TimelineDeleteDialog> = props => {
- const user = useUser()!;
+const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
+ const user = useUserLoggedIn();
const history = useHistory();
const { name } = props;
@@ -35,14 +35,14 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = 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<TimelineInfoCardProps> = (props) => { const { t } = useTranslation();
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const containerRef = React.useRef<HTMLDivElement>(null!);
const notifyHeight = React.useCallback((): void => {
@@ -48,13 +49,6 @@ const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { (): void => setManageDropdownOpen((old) => !old),
[]
);
- const onManageProperty = React.useCallback(
- (): void => onManage!('property'),
- [onManage]
- );
- const onManageDelete = React.useCallback((): void => onManage!('delete'), [
- onManage,
- ]);
return (
<div
@@ -81,20 +75,23 @@ const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])}
</small>
<div className="text-right mt-2">
- {props.onManage != null ? (
+ {onManage != null ? (
<Dropdown isOpen={manageDropdownOpen} toggle={toggleManageDropdown}>
<DropdownToggle outline color="primary">
{t('timeline.manage')}
</DropdownToggle>
<DropdownMenu>
- <DropdownItem onClick={onManageProperty}>
+ <DropdownItem onClick={() => onManage('property')}>
{t('timeline.manageItem.property')}
</DropdownItem>
<DropdownItem onClick={props.onMember}>
{t('timeline.manageItem.member')}
</DropdownItem>
<DropdownItem divider />
- <DropdownItem className="text-danger" onClick={onManageDelete}>
+ <DropdownItem
+ className="text-danger"
+ onClick={() => onManage('delete')}
+ >
{t('timeline.manageItem.delete')}
</DropdownItem>
</DropdownMenu>
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<TimelineMemberProps> = props => {
+const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
const { t } = useTranslation();
const [userSearchText, setUserSearchText] = useState<string>('');
@@ -87,7 +87,7 @@ const TimelineMember: React.FC<TimelineMemberProps> = props => { <>
<SearchInput
value={userSearchText}
- onChange={v => {
+ onChange={(v) => {
setUserSearchText(v);
}}
loading={userSearchState.type === 'loading'}
@@ -95,27 +95,27 @@ const TimelineMember: React.FC<TimelineMemberProps> = 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<TimelineMemberProps> = 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<TimelineMemberProps> = 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<TimelineMemberDialogProps> = props => {
+export const TimelineMemberDialog: React.FC<TimelineMemberDialogProps> = (
+ props
+) => {
return (
<Modal isOpen={props.open} toggle={props.onClose}>
<TimelineMember {...props} />
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 = (
<TimelinePropertyChangeDialog
open
close={closeDialog}
oldInfo={{
- visibility: timeline!.visibility,
- description: timeline!.description,
+ visibility: timeline.visibility,
+ description: timeline.description,
}}
onProcess={(req) => {
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 = (
<TimelineMemberDialog
open
onClose={closeDialog}
- members={[timeline!.owner, ...timeline!.members]}
+ members={[timeline.owner, ...timeline.members]}
edit={
- service.hasManagePermission(user, timeline!)
+ service.hasManagePermission(user, timeline)
? {
onCheckUser: (u) => {
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<TTimeline, TEditItems>( 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<TTimeline, TEditItems>( } 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<TimelinePostEditProps> = (props) => { const canSend = kind === 'text' || (kind === 'image' && imageBlob != null);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const containerRef = React.useRef<HTMLDivElement>(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<TimelinePostEditProps> = (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<TimelinePostEditProps> = (props) => { }, []);
return (
- <Container
- id="timeline-post-edit-area"
- fluid
- className="fixed-bottom bg-light"
- >
+ <div ref={containerRef} className="container-fluid fixed-bottom bg-light">
<Row>
<Col className="px-0">
{kind === 'text' ? (
@@ -198,7 +207,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { })()}
</Col>
</Row>
- </Container>
+ </div>
);
};
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<ChangeAvatarDialogProps> = (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<ChangeAvatarDialogProps> = (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<ChangeAvatarDialogProps> = (props) => { const createPreviewRow = (): React.ReactElement => {
if (resultUrl == null) {
- throw new Error('Invalid state.');
+ throw new UiLogicError();
}
return (
<Row className="justify-content-center">
@@ -182,7 +183,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (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 = (
<ChangeNicknameDialog
open
close={closeDialogHandler}
onProcess={(newNickname) => {
- 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 = (
<ChangeAvatarDialog
open
close={closeDialogHandler}
- process={(file) => 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<UserInfoCardProps> = (props) => { props.timeline.owner._links.avatar
);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const containerRef = React.useRef<HTMLDivElement>(null!);
const notifyHeight = React.useCallback((): void => {
@@ -59,17 +60,6 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (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 (
<div
@@ -93,19 +83,19 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])}
</small>
<div className="text-right mt-2">
- {props.onManage != null ? (
+ {onManage != null ? (
<Dropdown isOpen={manageDropdownOpen} toggle={toggleManageDropdown}>
<DropdownToggle outline color="primary">
{t('timeline.manage')}
</DropdownToggle>
<DropdownMenu>
- <DropdownItem onClick={onManageNickname}>
+ <DropdownItem onClick={() => onManage('nickname')}>
{t('timeline.manageItem.nickname')}
</DropdownItem>
- <DropdownItem onClick={onManageAvatar}>
+ <DropdownItem onClick={() => onManage('avatar')}>
{t('timeline.manageItem.avatar')}
</DropdownItem>
- <DropdownItem onClick={onManageProperty}>
+ <DropdownItem onClick={() => onManage('property')}>
{t('timeline.manageItem.property')}
</DropdownItem>
<DropdownItem onClick={props.onMember}>
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]
);
}
|