From ae48ac24c5533653f1e1f1e6d2a33e0238222297 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 24 Jul 2023 00:41:53 +0800 Subject: ... --- FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx | 6 ------ 1 file changed, 6 deletions(-) (limited to 'FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx') diff --git a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx index 63750445..bd5bef4c 100644 --- a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx @@ -52,12 +52,6 @@ const TimelinePropertyChangeDialog: React.FC< label: "timeline.dialogChangeProperty.description", initValue: timeline.description, }, - { - type: "color", - label: "timeline.dialogChangeProperty.color", - initValue: timeline.color ?? null, - canBeNull: true, - }, ] as const } open={props.open} -- cgit v1.2.3 From a9dc6b16d6730d8d1dc1ea2fab8ab3830fe56ce4 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 28 Jul 2023 00:40:58 +0800 Subject: ... --- .../src/pages/setting/ChangeNicknameDialog.tsx | 2 +- .../src/pages/setting/ChangePasswordDialog.tsx | 4 +- FrontEnd/src/views/admin/UserAdmin.tsx | 8 +- FrontEnd/src/views/center/TimelineCreateDialog.tsx | 2 +- .../src/views/common/dialog/OperationDialog.tsx | 108 ++++++------ FrontEnd/src/views/common/input/InputGroup.tsx | 184 +++++++++++++++------ .../views/timeline/PostPropertyChangeDialog.tsx | 2 +- .../src/views/timeline/TimelineDeleteDialog.tsx | 2 +- .../timeline/TimelinePropertyChangeDialog.tsx | 2 +- 9 files changed, 194 insertions(+), 120 deletions(-) (limited to 'FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx') diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx index 11c86222..58bbac5f 100644 --- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx @@ -26,7 +26,7 @@ const ChangeNicknameDialog: React.FC = (props) => { nickname: newNickname, }); }} - onClose={props.close} + close={props.close} /> ); }; diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index 5505137e..9ca95168 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -21,7 +21,7 @@ export function ChangePasswordDialog(props: ChangePasswordDialogProps) { { + close={() => { props.close(); if (redirect) { navigate("/login"); diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index d5179bf5..6003bd5a 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -35,7 +35,7 @@ const CreateUserDialog: React.FC<{ password, }) } - onClose={close} + close={close} open={open} onSuccessAndClose={onSuccess} /> @@ -55,7 +55,7 @@ const UserDeleteDialog: React.FC<{ return ( ( @@ -78,7 +78,7 @@ const UserModifyDialog: React.FC<{ return ( ( @@ -126,7 +126,7 @@ const UserPermissionModifyDialog: React.FC<{ return ( ( diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx index 63742936..8d4dde10 100644 --- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx +++ b/FrontEnd/src/views/center/TimelineCreateDialog.tsx @@ -20,7 +20,7 @@ const TimelineCreateDialog: React.FC = (props) => { return ( { +export interface OperationDialogProps { open: boolean; - onClose: () => void; + close: () => void; color?: ThemeColor; title: Text; inputPrompt?: Text; - processPrompt?: Text; successPrompt?: (data: TData) => ReactNode; failurePrompt?: (error: unknown) => ReactNode; - inputs: Inputs; - validator?: Validator; + inputInit?: InputInitializer; + inputScheme?: InputScheme; - onProcess: (inputs: Values) => Promise; + onProcess: (inputs: InputValueDict) => Promise; onSuccessAndClose?: (data: TData) => void; } -function OperationDialog( - props: OperationDialogProps, -) { +function OperationDialog(props: OperationDialogProps) { const { open, - onClose, + close, color, title, inputPrompt, - processPrompt, successPrompt, failurePrompt, - inputs, - validator, + inputInit, + inputScheme, onProcess, onSuccessAndClose, } = props; + if (process.env.NODE_ENV === "development") { + if (inputScheme == null && inputInit == null) { + throw Error("Scheme or Init? Choose one and create one."); + } + if (inputScheme != null && inputInit != null) { + throw Error("Scheme or Init? Choose one and drop one"); + } + } + const c = useC(); type Step = @@ -87,15 +91,17 @@ function OperationDialog( }; const [step, setStep] = useState({ type: "input" }); - const [values, setValues] = useState>(); - const [errors, setErrors] = useState(); - const [dirties, setDirties, dirtyAll] = useDirties(); - function close() { + const { inputGroupProps, hasError, setAllDisabled, confirm } = useInputs({ + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + init: inputInit ?? { scheme: inputScheme! }, + }); + + function onClose() { if (step.type !== "process") { - props.onClose(); + close(); if (step.type === "success" && props.onSuccessAndClose) { - props.onSuccessAndClose(step.data); + onSuccessAndClose?.(step.data); } } else { console.log("Attempt to close modal dialog when processing."); @@ -103,14 +109,11 @@ function OperationDialog( } function onConfirm() { - setStep({ type: "process" }); - props - .onProcess( - values.map((value, index) => - finalValueMapperMap[inputScheme[index].type](value as never), - ) as Values, - ) - .then( + const result = confirm(); + if (result.type === "ok") { + setStep({ type: "process" }); + setAllDisabled(true); + onProcess(result.values).then( (d) => { setStep({ type: "success", @@ -124,31 +127,21 @@ function OperationDialog( }); }, ); + } } let body: ReactNode; if (step.type === "input" || step.type === "process") { const isProcessing = step.type === "process"; - const hasError = errors.length > 0; body = (
- + { - setValues(values); - setErrors(errors); - }} - dirties={dirties} - onDirty={setDirties} + {...inputGroupProps} />

@@ -157,19 +150,14 @@ function OperationDialog( text="operationDialog.cancel" color="secondary" outline - onClick={close} + onClick={onClose} disabled={isProcessing} /> { - dirtyAll(); - if (validate(values)) { - onConfirm(); - } - }} + onClick={onConfirm} > {c("operationDialog.confirm")} @@ -183,32 +171,32 @@ function OperationDialog( result.type === "success" ? { message: "operationDialog.success", - customMessage: props.successPrompt?.(result.data), + customMessage: successPrompt?.(result.data), } : { message: "operationDialog.error", - customMessage: props.failurePrompt?.(result.data), + customMessage: failurePrompt?.(result.data), }; body = (

-
); } return ( - +
-
{c(props.title)}
+
{c(title)}

{body}
diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx index 7c33def7..232edfc9 100644 --- a/FrontEnd/src/views/common/input/InputGroup.tsx +++ b/FrontEnd/src/views/common/input/InputGroup.tsx @@ -23,7 +23,7 @@ * `useInputs` hook takes care of logic and generate props for `InputGroup`. */ -import { useState, useRef, Ref } from "react"; +import { useState, Ref } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; @@ -86,7 +86,7 @@ export type InputScheme = { validator?: Validator; }; -export type InputState = { +export type InputData = { values: InputValueDict; errors: InputErrorDict; disabled: InputDisabledDict; @@ -95,16 +95,18 @@ export type InputState = { export type State = { scheme: InputScheme; - state: InputState; + data: InputData; }; -export type StateInitializer = Partial; +export type DataInitializeInfo = Partial; -export type Initializer = { +export type InitializeInfo = { scheme: InputScheme; - stateInit?: Partial; + dataInit?: DataInitializeInfo; }; +export type Initialize + export interface InputGroupProps { color?: ThemeColor; containerClassName?: string; @@ -114,7 +116,7 @@ export interface InputGroupProps { onChange: (index: number, value: Input["value"]) => void; } -function cleanObject>(o: O): O { +function cleanObject(o: Record): Record { const result = { ...o }; for (const key of Object.keys(result)) { if (result[key] == null) { @@ -124,8 +126,23 @@ function cleanObject>(o: O): O { return result; } -export function useInputs(options: { init?: () => Initializer }): { +export type ConfirmResult = + | { + type: "ok"; + values: InputValueDict; + } + | { + type: "error"; + errors: InputErrorDict; + }; + +export function useInputs(options: { + init: InitializeInfo | (() => InitializeInfo); +}): { inputGroupProps: InputGroupProps; + hasError: boolean; + confirm: () => ConfirmResult; + setAllDisabled: (disabled: boolean) => void; } { function initializeValue( input: InputInfo, @@ -141,54 +158,59 @@ export function useInputs(options: { init?: () => Initializer }): { throw new Error("Unknown input type"); } - function initialize(initializer: Initializer): State { - const { scheme, stateInit } = initializer; + function initialize(info: InitializeInfo): State { + const { scheme, dataInit } = info; const { inputs, validator } = scheme; const keys = inputs.map((input) => input.key); if (process.env.NODE_ENV === "development") { - const checkKeys = (dict: Record) => { - for (const key of Object.keys(dict)) { - if (!keys.includes(key)) { - console.warn(""); + const checkKeys = (dict: Record | undefined) => { + if (dict != null) { + for (const key of Object.keys(dict)) { + if (!keys.includes(key)) { + console.warn(""); + } } } }; - checkKeys(stateInit?.values ?? {}); - checkKeys(stateInit?.errors ?? {}); - checkKeys(stateInit?.disabled ?? {}); - checkKeys(stateInit?.dirties ?? {}); + checkKeys(dataInit?.values); + checkKeys(dataInit?.errors); + checkKeys(dataInit?.disabled); + checkKeys(dataInit?.dirties); + } + + function clean(dict: Record | undefined): Record { + return dict != null ? cleanObject(dict) : {}; } const values: InputValueDict = {}; - let errors: InputErrorDict = cleanObject( - initializer.stateInit?.errors ?? {}, - ); - const disabled: InputDisabledDict = cleanObject( - initializer.stateInit?.disabled ?? {}, - ); - const dirties: InputDirtyDict = cleanObject( - initializer.stateInit?.dirties ?? {}, - ); + const disabled: InputDisabledDict = clean(info.dataInit?.disabled); + const dirties: InputDirtyDict = clean(info.dataInit?.dirties); for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; const { key } = input; - values[key] = initializeValue(input, stateInit?.values?.[key]); - if (!(key in dirties)) { - dirties[key] = false; - } + values[key] = initializeValue(input, dataInit?.values?.[key]); } - if (Object.keys(errors).length === 0 && validator != null) { - errors = validator(values, inputs); + let errors = info.dataInit?.errors; + + if (errors != null) { + if (process.env.NODE_ENV === "development") { + console.log( + "You explicitly set errors (not undefined) in initializer, so validator won't run.", + ); + } + errors = cleanObject(errors); + } else { + errors = validator?.(values, inputs) ?? {}; } return { scheme, - state: { + data: { values, errors, disabled, @@ -198,31 +220,95 @@ export function useInputs(options: { init?: () => Initializer }): { } const { init } = options; + const initializer = typeof init === "function" ? init : () => init; + + const [state, setState] = useState(() => initialize(initializer())); + + const { scheme, data } = state; + const { validator } = scheme; + + function createAllBooleanDict(value: boolean): Record { + const result: InputDirtyDict = {}; + for (const key of scheme.inputs.map((input) => input.key)) { + result[key] = value; + } + return result; + } + + const createAllDirties = () => createAllBooleanDict(true); const componentInputs: Input[] = []; - for (let i = 0; i < inputs.length; i++) { - const input = { ...inputs[i] }; - const error = dirties[i] - ? errors.find((e) => e.index === i)?.message - : undefined; - const componentInput: ExtendInputForComponent = { + for (let i = 0; i < scheme.inputs.length; i++) { + const input = scheme.inputs[i]; + const value = data.values[input.key]; + const error = data.errors[input.key]; + const disabled = data.disabled[input.key] ?? false; + const dirty = data.dirties[input.key] ?? false; + const componentInput: Input = { ...input, - value: values[i], + value: value as never, disabled, - error, + error: dirty ? error : undefined, }; componentInputs.push(componentInput); } - const dirtyAll = () => { - if (dirties != null) { - setDirties(new Array(dirties.length).fill(true) as Dirties); - } - }; - return { - inputGroupProps: {}, + inputGroupProps: { + inputs: componentInputs, + onChange: (index, value) => { + const input = scheme.inputs[index]; + const { key } = input; + const newValues = { ...data.values, [key]: value }; + const newDirties = { ...data.dirties, [key]: true }; + const newErrors = validator?.(newValues, scheme.inputs) ?? {}; + setState({ + scheme, + data: { + ...data, + values: newValues, + errors: newErrors, + dirties: newDirties, + }, + }); + }, + }, + hasError: Object.keys(data.errors).length > 0, + confirm() { + const newDirties = createAllDirties(); + const newErrors = validator?.(data.values, scheme.inputs) ?? {}; + + setState({ + scheme, + data: { + ...data, + dirties: newDirties, + errors: newErrors, + }, + }); + + if (Object.keys(newErrors).length === 0) { + return { + type: "error", + errors: newErrors, + }; + } else { + return { + type: "ok", + values: data.values, + }; + } + }, + setAllDisabled(disabled: boolean) { + setState({ + scheme, + data: { + ...data, + disabled: createAllBooleanDict(disabled), + }, + }); + }, }; } diff --git a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx index fc55185c..76f542c1 100644 --- a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx @@ -15,7 +15,7 @@ function PostPropertyChangeDialog(props: { return ( = (props) => { return ( { diff --git a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx index bd5bef4c..a0eebdbb 100644 --- a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx @@ -55,7 +55,7 @@ const TimelinePropertyChangeDialog: React.FC< ] as const } open={props.open} - onClose={props.close} + close={props.close} onProcess={([newTitle, newVisibility, newDescription, newColor]) => { const req: HttpTimelinePatchRequest = {}; if (newTitle !== timeline.title) { -- cgit v1.2.3 From 22e8f24e7f7574915e4c75d3c6a5498f6e621ee8 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 29 Jul 2023 22:00:59 +0800 Subject: ... --- FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 15 +++--- .../src/pages/setting/ChangeNicknameDialog.tsx | 6 +-- .../src/pages/setting/ChangePasswordDialog.tsx | 6 +-- FrontEnd/src/pages/setting/index.tsx | 46 +++++++----------- FrontEnd/src/views/admin/UserAdmin.tsx | 8 ++-- FrontEnd/src/views/center/TimelineCreateDialog.tsx | 2 +- FrontEnd/src/views/common/button/LoadingButton.tsx | 5 +- .../src/views/common/dialog/OperationDialog.tsx | 22 +++++---- FrontEnd/src/views/common/dialog/index.ts | 56 ++++++++++++++++++++++ FrontEnd/src/views/common/input/InputGroup.tsx | 7 ++- .../views/timeline/PostPropertyChangeDialog.tsx | 2 +- .../src/views/timeline/TimelineDeleteDialog.tsx | 2 +- .../timeline/TimelinePropertyChangeDialog.tsx | 2 +- 13 files changed, 116 insertions(+), 63 deletions(-) create mode 100644 FrontEnd/src/views/common/dialog/index.ts (limited to 'FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx') diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index b2a4e2a8..8c8e04fe 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -9,13 +9,16 @@ import { useUser } from "@/services/user"; import { getHttpUserClient } from "@/http/user"; -import ImageCropper, { Clip, applyClipToImage } from "@/views/common/ImageCropper"; +import ImageCropper, { + Clip, + applyClipToImage, +} from "@/views/common/ImageCropper"; import Button from "@/views/common/button/Button"; import Dialog from "@/views/common/dialog/Dialog"; export interface ChangeAvatarDialogProps { open: boolean; - close: () => void; + onClose: () => void; } const ChangeAvatarDialog: React.FC = (props) => { @@ -42,12 +45,12 @@ const ChangeAvatarDialog: React.FC = (props) => { >("select"); const [message, setMessage] = useState( - "settings.dialogChangeAvatar.prompt.select" + "settings.dialogChangeAvatar.prompt.select", ); const trueMessage = convertI18nText(message, t); - const closeDialog = props.close; + const closeDialog = props.onClose; const close = React.useCallback((): void => { if (!(state === "uploading")) { @@ -92,7 +95,7 @@ const ChangeAvatarDialog: React.FC = (props) => { setFile(files[0]); } }, - [] + [], ); const onCropNext = React.useCallback(() => { @@ -140,7 +143,7 @@ const ChangeAvatarDialog: React.FC = (props) => { (e: unknown) => { setState("error"); setMessage({ type: "custom", value: (e as AxiosError).message }); - } + }, ); }, [user, resultBlob]); diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx index 5606ce94..4d318543 100644 --- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx @@ -5,11 +5,11 @@ import OperationDialog from "@/views/common/dialog/OperationDialog"; export interface ChangeNicknameDialogProps { open: boolean; - close: () => void; + onClose: () => void; } export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) { - const { open, close } = props; + const { open, onClose } = props; const user = useUserLoggedIn(); @@ -29,7 +29,7 @@ export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) { nickname: newNickname as string, }); }} - close={close} + onClose={onClose} /> ); } diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index 407f3051..87a970a5 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -9,11 +9,11 @@ import OperationDialog, { interface ChangePasswordDialogProps { open: boolean; - close: () => void; + onClose: () => void; } export function ChangePasswordDialog(props: ChangePasswordDialogProps) { - const { open, close } = props; + const { open, onClose } = props; const navigate = useNavigate(); @@ -22,7 +22,7 @@ export function ChangePasswordDialog(props: ChangePasswordDialogProps) { return ( (null); - - function dialogOpener(name: DialogName): () => void { - return () => setDialog(name); - } + const { dialogPropsMap, createDialogSwitch } = useDialog([ + "change-password", + "change-avatar", + "change-nickname", + "logout", + "renew-register-code", + ]); return ( @@ -257,20 +253,20 @@ export default function SettingPage() { @@ -278,31 +274,21 @@ export default function SettingPage() { - setDialog(null)} - /> + {user && ( <> setDialog(null)} - open={dialog === "logout"} onConfirm={() => { void userService.logout().then(() => { navigate("/"); }); }} + {...dialogPropsMap["logout"]} /> - setDialog(null)} - /> - setDialog(null)} - /> + + )} diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index 6003bd5a..d5179bf5 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -35,7 +35,7 @@ const CreateUserDialog: React.FC<{ password, }) } - close={close} + onClose={close} open={open} onSuccessAndClose={onSuccess} /> @@ -55,7 +55,7 @@ const UserDeleteDialog: React.FC<{ return ( ( @@ -78,7 +78,7 @@ const UserModifyDialog: React.FC<{ return ( ( @@ -126,7 +126,7 @@ const UserPermissionModifyDialog: React.FC<{ return ( ( diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx index 8d4dde10..63742936 100644 --- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx +++ b/FrontEnd/src/views/center/TimelineCreateDialog.tsx @@ -20,7 +20,7 @@ const TimelineCreateDialog: React.FC = (props) => { return ( { export default function LoadingButton(props: LoadingButtonProps) { const c = useC(); - const { color, text, loading, className, children, ...otherProps } = props; + const { color, text, loading, disabled, className, children, ...otherProps } = + props; if (text != null && children != null) { console.warn("You can't set both text and children props."); @@ -23,7 +24,7 @@ export default function LoadingButton(props: LoadingButtonProps) { return (
); } return ( - +
= { + [K in D]: V; +}; + +type DialogKeyMap = DialogMap; + +type DialogPropsMap = DialogMap< + D, + { key: number | string; open: boolean; onClose: () => void } +>; + +export function useDialog( + dialogs: D[], + initDialog?: D | null, +): { + dialog: D | null; + switchDialog: (newDialog: D | null) => void; + dialogPropsMap: DialogPropsMap; + createDialogSwitch: (newDialog: D | null) => () => void; +} { + const [dialog, setDialog] = useState(initDialog ?? null); + + const [dialogKeys, setDialogKeys] = useState>( + () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap, + ); + + const switchDialog = (newDialog: D | null) => { + if (dialog !== null) { + setDialogKeys({ ...dialogKeys, [dialog]: dialogKeys[dialog] + 1 }); + } + setDialog(newDialog); + }; + + return { + dialog, + switchDialog, + dialogPropsMap: Object.fromEntries( + dialogs.map((d) => [ + d, + { + key: `${d}-${dialogKeys[d]}`, + open: dialog === d, + onClose: () => switchDialog(null), + }, + ]), + ) as DialogPropsMap, + createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog), + }; +} diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx index 858fa1a5..3d1e3ada 100644 --- a/FrontEnd/src/views/common/input/InputGroup.tsx +++ b/FrontEnd/src/views/common/input/InputGroup.tsx @@ -141,6 +141,7 @@ export type ConfirmResult = export function useInputs(options: { init: Initializer }): { inputGroupProps: InputGroupProps; hasError: boolean; + hasErrorAndDirty: boolean; confirm: () => ConfirmResult; setAllDisabled: (disabled: boolean) => void; } { @@ -260,6 +261,9 @@ export function useInputs(options: { init: Initializer }): { componentInputs.push(componentInput); } + const hasError = Object.keys(data.errors).length > 0; + const hasDirty = Object.keys(data.dirties).some((key) => data.dirties[key]); + return { inputGroupProps: { inputs: componentInputs, @@ -280,7 +284,8 @@ export function useInputs(options: { init: Initializer }): { }); }, }, - hasError: Object.keys(data.errors).length > 0, + hasError, + hasErrorAndDirty: hasError && hasDirty, confirm() { const newDirties = createAllDirties(); const newErrors = validator?.(data.values, scheme.inputs) ?? {}; diff --git a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx index 76f542c1..fc55185c 100644 --- a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx @@ -15,7 +15,7 @@ function PostPropertyChangeDialog(props: { return ( = (props) => { return ( { diff --git a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx index a0eebdbb..bd5bef4c 100644 --- a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx @@ -55,7 +55,7 @@ const TimelinePropertyChangeDialog: React.FC< ] as const } open={props.open} - close={props.close} + onClose={props.close} onProcess={([newTitle, newVisibility, newDescription, newColor]) => { const req: HttpTimelinePatchRequest = {}; if (newTitle !== timeline.title) { -- cgit v1.2.3 From d9c1d512fa64ef4f8c08ca34f7a5842642879bcc Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 31 Jul 2023 00:08:48 +0800 Subject: ... --- FrontEnd/src/pages/timeline/Timeline.css | 7 - FrontEnd/src/pages/timeline/Timeline.tsx | 15 +- FrontEnd/src/pages/timeline/TimelineCard.css | 18 ++ FrontEnd/src/pages/timeline/TimelineCard.tsx | 59 ++--- .../src/pages/timeline/TimelineDeleteDialog.tsx | 4 +- .../timeline/TimelinePropertyChangeDialog.tsx | 4 +- FrontEnd/src/views/timeline/CollapseButton.tsx | 21 -- .../src/views/timeline/ConnectionStatusBadge.css | 36 --- .../src/views/timeline/ConnectionStatusBadge.tsx | 41 ---- FrontEnd/src/views/timeline/MarkdownPostEdit.css | 21 -- FrontEnd/src/views/timeline/MarkdownPostEdit.tsx | 215 ----------------- .../views/timeline/PostPropertyChangeDialog.tsx | 42 ---- FrontEnd/src/views/timeline/Timeline.css | 244 ------------------- FrontEnd/src/views/timeline/Timeline.tsx | 207 ---------------- FrontEnd/src/views/timeline/TimelineCard.tsx | 167 ------------- FrontEnd/src/views/timeline/TimelineDateLabel.tsx | 19 -- .../src/views/timeline/TimelineDeleteDialog.tsx | 61 ----- FrontEnd/src/views/timeline/TimelineEmptyItem.tsx | 25 -- FrontEnd/src/views/timeline/TimelineLine.tsx | 51 ---- FrontEnd/src/views/timeline/TimelineLoading.tsx | 16 -- FrontEnd/src/views/timeline/TimelineMember.css | 8 - FrontEnd/src/views/timeline/TimelineMember.tsx | 202 ---------------- .../src/views/timeline/TimelinePostContentView.tsx | 187 --------------- FrontEnd/src/views/timeline/TimelinePostEdit.css | 10 - FrontEnd/src/views/timeline/TimelinePostEdit.tsx | 267 --------------------- .../src/views/timeline/TimelinePostEditCard.tsx | 31 --- .../src/views/timeline/TimelinePostEditNoLogin.tsx | 18 -- .../src/views/timeline/TimelinePostListView.tsx | 76 ------ FrontEnd/src/views/timeline/TimelinePostView.tsx | 159 ------------ .../timeline/TimelinePropertyChangeDialog.tsx | 82 ------- FrontEnd/src/views/timeline/index.tsx | 23 -- 31 files changed, 55 insertions(+), 2281 deletions(-) create mode 100644 FrontEnd/src/pages/timeline/TimelineCard.css delete mode 100644 FrontEnd/src/views/timeline/CollapseButton.tsx delete mode 100644 FrontEnd/src/views/timeline/ConnectionStatusBadge.css delete mode 100644 FrontEnd/src/views/timeline/ConnectionStatusBadge.tsx delete mode 100644 FrontEnd/src/views/timeline/MarkdownPostEdit.css delete mode 100644 FrontEnd/src/views/timeline/MarkdownPostEdit.tsx delete mode 100644 FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx delete mode 100644 FrontEnd/src/views/timeline/Timeline.css delete mode 100644 FrontEnd/src/views/timeline/Timeline.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelineCard.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelineDateLabel.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelineEmptyItem.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelineLine.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelineLoading.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelineMember.css delete mode 100644 FrontEnd/src/views/timeline/TimelineMember.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelinePostContentView.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelinePostEdit.css delete mode 100644 FrontEnd/src/views/timeline/TimelinePostEdit.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelinePostEditCard.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelinePostEditNoLogin.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelinePostListView.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelinePostView.tsx delete mode 100644 FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx delete mode 100644 FrontEnd/src/views/timeline/index.tsx (limited to 'FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx') diff --git a/FrontEnd/src/pages/timeline/Timeline.css b/FrontEnd/src/pages/timeline/Timeline.css index 4dd4fdcc..f071f163 100644 --- a/FrontEnd/src/pages/timeline/Timeline.css +++ b/FrontEnd/src/pages/timeline/Timeline.css @@ -230,13 +230,6 @@ margin-right: 0.6em; } -.timeline-card { - position: fixed; - z-index: 1029; - top: 56px; - right: 0; - margin: 0.5em; -} .timeline-top { position: sticky; diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx index 3a7fbd00..f93e1623 100644 --- a/FrontEnd/src/pages/timeline/Timeline.tsx +++ b/FrontEnd/src/pages/timeline/Timeline.tsx @@ -41,7 +41,7 @@ const Timeline: React.FC = (props) => { const [timeline, setTimeline] = React.useState(null); const [posts, setPosts] = React.useState(null); const [signalrState, setSignalrState] = React.useState( - HubConnectionState.Connecting + HubConnectionState.Connecting, ); const [error, setError] = React.useState< "offline" | "forbid" | "notfound" | "error" | null @@ -81,7 +81,7 @@ const Timeline: React.FC = (props) => { console.error(error); setError("error"); } - } + }, ); }, [timelineOwner, timelineName, timelineReloadKey]); @@ -91,7 +91,7 @@ const Timeline: React.FC = (props) => { .then( (page) => { setPosts( - page.items.filter((p): p is HttpTimelinePostInfo => !p.deleted) + page.items.filter((p): p is HttpTimelinePostInfo => !p.deleted), ); setTotalPage(page.totalPageCount); }, @@ -106,14 +106,14 @@ const Timeline: React.FC = (props) => { console.error(error); setError("error"); } - } + }, ); }, [timelineOwner, timelineName, postsReloadKey]); React.useEffect(() => { const timelinePostUpdate$ = getTimelinePostUpdate$( timelineOwner, - timelineName + timelineName, ); const subscription = timelinePostUpdate$.subscribe(({ update, state }) => { if (update) { @@ -134,7 +134,7 @@ const Timeline: React.FC = (props) => { .then( (page) => { const ps = page.items.filter( - (p): p is HttpTimelinePostInfo => !p.deleted + (p): p is HttpTimelinePostInfo => !p.deleted, ); setPosts((old) => [...(old ?? []), ...ps]); }, @@ -149,7 +149,7 @@ const Timeline: React.FC = (props) => { console.error(error); setError("error"); } - } + }, ); }, currentPage < totalPage); @@ -183,7 +183,6 @@ const Timeline: React.FC = (props) => { {timeline == null && posts == null && } {timeline && ( void; } -const TimelineCard: React.FC = (props) => { - const { timeline, connectionStatus, onReload, className } = props; +export default function TimelineCard(props: TimelinePageCardProps) { + const { timeline, connectionStatus, onReload } = props; - const { t } = useTranslation(); + const user = useUser(); - const [dialog, setDialog] = React.useState< - "member" | "property" | "delete" | null - >(null); + const c = useC(); - const [collapse, setCollapse] = React.useState(true); + const [collapse, setCollapse] = useState(true); const toggleCollapse = (): void => { setCollapse((o) => !o); }; const isSmallScreen = useIsSmallScreen(); - const user = useUser(); + const { createDialogSwitch, dialog, dialogPropsMap, switchDialog } = + useDialog(["member", "property", "delete"]); const content = ( - <> -

+
+

{timeline.title} - {timeline.nameV2} + {timeline.nameV2}

= (props) => {

{timeline.description}

- {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} + {c(timelineVisibilityTooltipTranslationMap[timeline.visibility])}
{user != null ? ( @@ -92,7 +93,7 @@ const TimelineCard: React.FC = (props) => { setDialog("member")} + onClick={createDialogSwitch("member")} /> {timeline.manageable ? ( = (props) => { { type: "button", text: "timeline.manageItem.property", - onClick: () => setDialog("property"), + onClick: createDialogSwitch("property"), }, { type: "divider" }, { type: "button", - onClick: () => setDialog("delete"), + onClick: createDialogSwitch("delete"), color: "danger", text: "timeline.manageItem.delete", }, @@ -116,12 +117,12 @@ const TimelineCard: React.FC = (props) => { ) : null}
- +
); return ( <> - +
= (props) => { setDialog(null)} - open={dialog === "member"} onChange={onReload} + {...dialogPropsMap["member"]} /> setDialog(null)} - open={dialog === "property"} onChange={onReload} + {...dialogPropsMap["property"]} /> - setDialog(null)} - /> + ); -}; - -export default TimelineCard; +} diff --git a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx index d5b22aee..0a5a2491 100644 --- a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx @@ -9,7 +9,7 @@ import OperationDialog from "@/views/common/dialog/OperationDialog"; interface TimelineDeleteDialog { timeline: HttpTimelineInfo; open: boolean; - close: () => void; + onClose: () => void; } const TimelineDeleteDialog: React.FC = (props) => { @@ -20,7 +20,7 @@ const TimelineDeleteDialog: React.FC = (props) => { return ( ( diff --git a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx index e26df3eb..b57135bb 100644 --- a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx @@ -12,7 +12,7 @@ import OperationDialog from "@/views/common/dialog/OperationDialog"; export interface TimelinePropertyChangeDialogProps { open: boolean; - close: () => void; + onClose: () => void; timeline: HttpTimelineInfo; onChange: () => void; } @@ -64,7 +64,7 @@ const TimelinePropertyChangeDialog: React.FC< }, }} open={props.open} - onClose={props.close} + onClose={props.onClose} onProcess={({ title, visibility, description }) => { const req: HttpTimelinePatchRequest = {}; if (title !== timeline.title) { diff --git a/FrontEnd/src/views/timeline/CollapseButton.tsx b/FrontEnd/src/views/timeline/CollapseButton.tsx deleted file mode 100644 index 374ccc2e..00000000 --- a/FrontEnd/src/views/timeline/CollapseButton.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -import IconButton from "../common/button/IconButton"; - -const CollapseButton: React.FC<{ - collapse: boolean; - onClick: () => void; - className?: string; - style?: React.CSSProperties; -}> = ({ collapse, onClick, className, style }) => { - return ( - - ); -}; - -export default CollapseButton; diff --git a/FrontEnd/src/views/timeline/ConnectionStatusBadge.css b/FrontEnd/src/views/timeline/ConnectionStatusBadge.css deleted file mode 100644 index 7fe83b9b..00000000 --- a/FrontEnd/src/views/timeline/ConnectionStatusBadge.css +++ /dev/null @@ -1,36 +0,0 @@ -.connection-status-badge { - font-size: 0.8em; - border-radius: 5px; - padding: 0.1em 1em; - background-color: #eaf2ff; -} -.connection-status-badge::before { - width: 10px; - height: 10px; - border-radius: 50%; - display: inline-block; - content: ""; - margin-right: 0.6em; -} -.connection-status-badge.success { - color: #006100; -} -.connection-status-badge.success::before { - background-color: #006100; -} - -.connection-status-badge.warning { - color: #e4a700; -} - -.connection-status-badge.warning::before { - background-color: #e4a700; -} - -.connection-status-badge.danger { - color: #fd1616; -} - -.connection-status-badge.danger::before { - background-color: #fd1616; -} diff --git a/FrontEnd/src/views/timeline/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline/ConnectionStatusBadge.tsx deleted file mode 100644 index 2b820454..00000000 --- a/FrontEnd/src/views/timeline/ConnectionStatusBadge.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react"; -import classnames from "classnames"; -import { HubConnectionState } from "@microsoft/signalr"; -import { useTranslation } from "react-i18next"; - -import "./ConnectionStatusBadge.css"; - -export interface ConnectionStatusBadgeProps { - status: HubConnectionState; - className?: string; - style?: React.CSSProperties; -} - -const classNameMap: Record = { - Connected: "success", - Connecting: "warning", - Disconnected: "danger", - Disconnecting: "warning", - Reconnecting: "warning", -}; - -const ConnectionStatusBadge: React.FC = (props) => { - const { status, className, style } = props; - - const { t } = useTranslation(); - - return ( -
- {t(`connectionState.${status}`)} -
- ); -}; - -export default ConnectionStatusBadge; diff --git a/FrontEnd/src/views/timeline/MarkdownPostEdit.css b/FrontEnd/src/views/timeline/MarkdownPostEdit.css deleted file mode 100644 index e36be992..00000000 --- a/FrontEnd/src/views/timeline/MarkdownPostEdit.css +++ /dev/null @@ -1,21 +0,0 @@ -.timeline-markdown-post-edit-page { - overflow: auto; - max-height: 300px; -} - -.timeline-markdown-post-edit-image-container { - position: relative; - text-align: center; - margin-bottom: 1em; -} - -.timeline-markdown-post-edit-image { - max-width: 100%; - max-height: 200px; -} - -.timeline-markdown-post-edit-image-delete-button { - position: absolute; - right: 10px; - top: 2px; -} diff --git a/FrontEnd/src/views/timeline/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline/MarkdownPostEdit.tsx deleted file mode 100644 index 6401cfaa..00000000 --- a/FrontEnd/src/views/timeline/MarkdownPostEdit.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import * as React from "react"; -import classnames from "classnames"; -import { useTranslation } from "react-i18next"; - -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; - -import TimelinePostBuilder from "@/services/TimelinePostBuilder"; - -import FlatButton from "../common/button/FlatButton"; -import TabPages from "../common/tab/TabPages"; -import ConfirmDialog from "../common/dialog/ConfirmDialog"; -import Spinner from "../common/Spinner"; -import IconButton from "../common/button/IconButton"; - -import "./MarkdownPostEdit.css"; - -export interface MarkdownPostEditProps { - owner: string; - timeline: string; - onPosted: (post: HttpTimelinePostInfo) => void; - onPostError: () => void; - onClose: () => void; - className?: string; - style?: React.CSSProperties; -} - -const MarkdownPostEdit: React.FC = ({ - owner: ownerUsername, - timeline: timelineName, - onPosted, - onClose, - onPostError, - className, - style, -}) => { - const { t } = useTranslation(); - - const [canLeave, setCanLeave] = React.useState(true); - - const [process, setProcess] = React.useState(false); - - const [showLeaveConfirmDialog, setShowLeaveConfirmDialog] = - React.useState(false); - - const [text, _setText] = React.useState(""); - const [images, _setImages] = React.useState<{ file: File; url: string }[]>( - [] - ); - const [previewHtml, _setPreviewHtml] = React.useState(""); - - const _builder = React.useRef(null); - - const getBuilder = (): TimelinePostBuilder => { - if (_builder.current == null) { - const builder = new TimelinePostBuilder(() => { - setCanLeave(builder.isEmpty); - _setText(builder.text); - _setImages(builder.images); - _setPreviewHtml(builder.renderHtml()); - }); - _builder.current = builder; - } - return _builder.current; - }; - - const canSend = text.length > 0; - - React.useEffect(() => { - return () => { - getBuilder().dispose(); - }; - }, []); - - React.useEffect(() => { - window.onbeforeunload = (): unknown => { - if (!canLeave) { - return t("timeline.confirmLeave"); - } - }; - - return () => { - window.onbeforeunload = null; - }; - }, [canLeave, t]); - - const send = async (): Promise => { - setProcess(true); - try { - const dataList = await getBuilder().build(); - const post = await getHttpTimelineClient().postPost( - ownerUsername, - timelineName, - { - dataList, - } - ); - onPosted(post); - onClose(); - } catch (e) { - setProcess(false); - onPostError(); - } - }; - - return ( - <> - - ) : ( -
- { - if (canLeave) { - onClose(); - } else { - setShowLeaveConfirmDialog(true); - } - }} - /> - {canSend && ( - void send()} /> - )} -
- ) - } - pages={[ - { - name: "text", - text: "edit", - page: ( -