From 29da3cef05ef138af2028ad44a27d706e9ae2df8 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 26 Jun 2021 18:46:58 +0800 Subject: ... --- .../src/views/common/dailog/OperationDialog.tsx | 472 +++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 FrontEnd/src/views/common/dailog/OperationDialog.tsx (limited to 'FrontEnd/src/views/common/dailog/OperationDialog.tsx') diff --git a/FrontEnd/src/views/common/dailog/OperationDialog.tsx b/FrontEnd/src/views/common/dailog/OperationDialog.tsx new file mode 100644 index 00000000..129e85d5 --- /dev/null +++ b/FrontEnd/src/views/common/dailog/OperationDialog.tsx @@ -0,0 +1,472 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { TwitterPicker } from "react-color"; +import moment from "moment"; + +import { convertI18nText, I18nText, UiLogicError } from "@/common"; + +import Button from "../button/Button"; +import LoadingButton from "../button/LoadingButton"; +import Dialog from "./Dialog"; + +interface DefaultErrorPromptProps { + error?: string; +} + +const DefaultErrorPrompt: React.FC = (props) => { + const { t } = useTranslation(); + + let result =

{t("operationDialog.error")}

; + + if (props.error != null) { + result = ( + <> + {result} +

{props.error}

+ + ); + } + + return result; +}; + +export interface OperationDialogTextInput { + type: "text"; + label?: I18nText; + password?: boolean; + initValue?: string; + textFieldProps?: Omit< + React.InputHTMLAttributes, + "type" | "value" | "onChange" | "aria-relevant" + >; + helperText?: string; +} + +export interface OperationDialogBoolInput { + type: "bool"; + label: I18nText; + initValue?: boolean; +} + +export interface OperationDialogSelectInputOption { + value: string; + label: I18nText; + icon?: React.ReactElement; +} + +export interface OperationDialogSelectInput { + type: "select"; + label: I18nText; + options: OperationDialogSelectInputOption[]; + initValue?: string; +} + +export interface OperationDialogColorInput { + type: "color"; + label?: I18nText; + initValue?: string | null; + canBeNull?: boolean; +} + +export interface OperationDialogDateTimeInput { + type: "datetime"; + label?: I18nText; + initValue?: string; +} + +export type OperationDialogInput = + | OperationDialogTextInput + | OperationDialogBoolInput + | OperationDialogSelectInput + | OperationDialogColorInput + | OperationDialogDateTimeInput; + +interface OperationInputTypeStringToValueTypeMap { + text: string; + bool: boolean; + select: string; + color: string | null; + datetime: string; +} + +type MapOperationInputTypeStringToValueType = + Type extends keyof OperationInputTypeStringToValueTypeMap + ? OperationInputTypeStringToValueTypeMap[Type] + : never; + +type MapOperationInputInfoValueType = T extends OperationDialogInput + ? MapOperationInputTypeStringToValueType + : T; + +const initValueMapperMap: { + [T in OperationDialogInput as T["type"]]: ( + item: T + ) => MapOperationInputInfoValueType; +} = { + bool: (item) => item.initValue ?? false, + color: (item) => item.initValue ?? null, + datetime: (item) => { + if (item.initValue != null) { + return moment(item.initValue).format("YYYY-MM-DDTHH:mm:ss"); + } else { + return ""; + } + }, + select: (item) => item.initValue ?? item.options[0].value, + text: (item) => item.initValue ?? "", +}; + +type MapOperationInputInfoValueTypeList< + Tuple extends readonly OperationDialogInput[] +> = { + [Index in keyof Tuple]: MapOperationInputInfoValueType; +} & { length: Tuple["length"] }; + +export type OperationInputError = + | { + [index: number]: I18nText | null | undefined; + } + | null + | undefined; + +const isNoError = (error: OperationInputError): boolean => { + if (error == null) return true; + for (const key in error) { + if (error[key] != null) return false; + } + return true; +}; + +export interface OperationDialogProps< + TData, + OperationInputInfoList extends readonly OperationDialogInput[] +> { + open: boolean; + onClose: () => void; + title: I18nText | (() => React.ReactNode); + themeColor?: "danger" | "success" | string; + onProcess: ( + inputs: MapOperationInputInfoValueTypeList + ) => Promise; + inputScheme?: OperationInputInfoList; + inputValidator?: ( + inputs: MapOperationInputInfoValueTypeList + ) => OperationInputError; + inputPrompt?: I18nText | (() => React.ReactNode); + processPrompt?: () => React.ReactNode; + successPrompt?: (data: TData) => React.ReactNode; + failurePrompt?: (error: unknown) => React.ReactNode; + onSuccessAndClose?: (data: TData) => void; +} + +const OperationDialog = < + TData, + OperationInputInfoList extends readonly OperationDialogInput[] +>( + props: OperationDialogProps +): React.ReactElement => { + const inputScheme = (props.inputScheme ?? + []) as readonly OperationDialogInput[]; + + const { t } = useTranslation(); + + type Step = + | { type: "input" } + | { type: "process" } + | { + type: "success"; + data: TData; + } + | { + type: "failure"; + data: unknown; + }; + const [step, setStep] = useState({ type: "input" }); + + type ValueType = boolean | string | null | undefined; + + const [values, setValues] = useState( + inputScheme.map((item) => { + if (item.type in initValueMapperMap) { + return ( + initValueMapperMap[item.type] as ( + i: OperationDialogInput + ) => ValueType + )(item); + } else { + throw new UiLogicError("Unknown input scheme."); + } + }) + ); + const [dirtyList, setDirtyList] = useState(() => + inputScheme.map(() => false) + ); + const [inputError, setInputError] = useState(); + + const close = (): void => { + if (step.type !== "process") { + props.onClose(); + if (step.type === "success" && props.onSuccessAndClose) { + props.onSuccessAndClose(step.data); + } + } else { + console.log("Attempt to close modal when processing."); + } + }; + + const onConfirm = (): void => { + setStep({ type: "process" }); + props + .onProcess( + values.map((v, index) => { + if (inputScheme[index].type === "datetime" && v !== "") + return new Date(v as string).toISOString(); + else return v; + }) as unknown as MapOperationInputInfoValueTypeList + ) + .then( + (d) => { + setStep({ + type: "success", + data: d, + }); + }, + (e: unknown) => { + setStep({ + type: "failure", + data: e, + }); + } + ); + }; + + let body: React.ReactNode; + if (step.type === "input" || step.type === "process") { + const process = step.type === "process"; + + let inputPrompt = + typeof props.inputPrompt === "function" + ? props.inputPrompt() + : convertI18nText(props.inputPrompt, t); + inputPrompt =
{inputPrompt}
; + + const validate = (values: ValueType[]): boolean => { + const { inputValidator } = props; + if (inputValidator != null) { + const result = inputValidator( + values as unknown as MapOperationInputInfoValueTypeList + ); + setInputError(result); + return isNoError(result); + } + return true; + }; + + const updateValue = (index: number, newValue: ValueType): void => { + const oldValues = values; + const newValues = oldValues.slice(); + newValues[index] = newValue; + setValues(newValues); + if (dirtyList[index] === false) { + const newDirtyList = dirtyList.slice(); + newDirtyList[index] = true; + setDirtyList(newDirtyList); + } + validate(newValues); + }; + + const canProcess = isNoError(inputError); + + body = ( + <> +
+ {inputPrompt} + {inputScheme.map((item, index) => { + const value = values[index]; + const error: string | null = + dirtyList[index] && inputError != null + ? convertI18nText(inputError[index], t) + : null; + + if (item.type === "text") { + return ( + + {item.label && ( + {convertI18nText(item.label, t)} + )} + { + const v = e.target.value; + updateValue(index, v); + }} + isInvalid={error != null} + disabled={process} + /> + {error != null && ( + + {error} + + )} + {item.helperText && ( + {t(item.helperText)} + )} + + ); + } else if (item.type === "bool") { + return ( + + + type="checkbox" + checked={value as boolean} + onChange={(event) => { + updateValue(index, event.currentTarget.checked); + }} + label={convertI18nText(item.label, t)} + disabled={process} + /> + + ); + } else if (item.type === "select") { + return ( + + {convertI18nText(item.label, t)} + { + updateValue(index, event.target.value); + }} + disabled={process} + > + {item.options.map((option, i) => { + return ( + + ); + })} + + + ); + } else if (item.type === "color") { + return ( + + {item.canBeNull ? ( + + type="checkbox" + checked={value !== null} + onChange={(event) => { + if (event.currentTarget.checked) { + updateValue(index, "#007bff"); + } else { + updateValue(index, null); + } + }} + label={convertI18nText(item.label, t)} + disabled={process} + /> + ) : ( + {convertI18nText(item.label, t)} + )} + {value !== null && ( + updateValue(index, result.hex)} + /> + )} + + ); + } else if (item.type === "datetime") { + return ( + + {item.label && ( + {convertI18nText(item.label, t)} + )} + { + const v = e.target.value; + updateValue(index, v); + }} + isInvalid={error != null} + disabled={process} + /> + {error != null && ( + + {error} + + )} + + ); + } + })} +
+
+
+ + ); + } else { + let content: React.ReactNode; + const result = step; + if (result.type === "success") { + content = + props.successPrompt?.(result.data) ?? t("operationDialog.success"); + if (typeof content === "string") + content =

{content}

; + } else { + content = props.failurePrompt?.(result.data) ?? ; + if (typeof content === "string") + content = ; + } + body = ( + <> +
{content}
+
+
+ + ); + } + + const title = + typeof props.title === "function" + ? props.title() + : convertI18nText(props.title, t); + + return ( + +

+ {title} +

+ {body} +
+ ); +}; + +export default OperationDialog; -- cgit v1.2.3 From a168336c0761b263ee5371218cbf6da236c0acce Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 26 Jun 2021 19:13:19 +0800 Subject: ... --- FrontEnd/src/views/admin/UserAdmin.tsx | 9 +- FrontEnd/src/views/common/Menu.tsx | 26 ++--- FrontEnd/src/views/common/SearchInput.tsx | 13 +-- FrontEnd/src/views/common/Spinner.tsx | 13 +++ FrontEnd/src/views/common/TabPages.tsx | 20 +--- FrontEnd/src/views/common/alert/AlertHost.tsx | 11 +- FrontEnd/src/views/common/button/LoadingButton.tsx | 23 ++--- .../src/views/common/dailog/OperationDialog.tsx | 71 ++++++------- FrontEnd/src/views/login/index.tsx | 114 ++++++++++----------- .../src/views/timeline-common/MarkdownPostEdit.tsx | 8 +- .../src/views/timeline-common/TimelineMember.tsx | 21 ++-- .../views/timeline-common/TimelinePageTemplate.tsx | 1 - .../TimelinePostDeleteConfirmDialog.tsx | 40 -------- .../src/views/timeline-common/TimelinePostEdit.tsx | 1 - .../src/views/timeline-common/TimelinePostView.tsx | 6 +- 15 files changed, 142 insertions(+), 235 deletions(-) create mode 100644 FrontEnd/src/views/common/Spinner.tsx delete mode 100644 FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx (limited to 'FrontEnd/src/views/common/dailog/OperationDialog.tsx') diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index 125219c3..481db1cc 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -15,6 +15,7 @@ import { import { Trans, useTranslation } from "react-i18next"; import Button from "../common/button/Button"; import TextButton from "../common/button/TextButton"; +import Spinner from "../common/Spinner"; interface DialogProps { open: boolean; @@ -203,7 +204,7 @@ const UserItem: React.FC = ({ user, on }) => { const [editMaskVisible, setEditMaskVisible] = React.useState(false); return ( - +
setEditMaskVisible(true)} @@ -242,7 +243,7 @@ const UserItem: React.FC = ({ user, on }) => { onClick={on[kDelete]} />
-
+ ); }; @@ -251,8 +252,6 @@ interface UserAdminProps { } const UserAdmin: React.FC = () => { - const { t } = useTranslation(); - type DialogInfo = | null | { @@ -390,7 +389,7 @@ const UserAdmin: React.FC = () => { ); } else { - return ; + return ; } }; diff --git a/FrontEnd/src/views/common/Menu.tsx b/FrontEnd/src/views/common/Menu.tsx index ae73a331..a5d2ec2c 100644 --- a/FrontEnd/src/views/common/Menu.tsx +++ b/FrontEnd/src/views/common/Menu.tsx @@ -1,9 +1,9 @@ import React from "react"; import classnames from "classnames"; -import { OverlayTrigger, OverlayTriggerProps, Popover } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import { BootstrapThemeColor, convertI18nText, I18nText } from "@/common"; +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; export type MenuItem = | { @@ -13,7 +13,7 @@ export type MenuItem = type: "button"; text: I18nText; iconClassName?: string; - color?: BootstrapThemeColor; + color?: PaletteColorType; onClick: () => void; }; @@ -67,26 +67,14 @@ export default Menu; export interface PopupMenuProps { items: MenuItems; - children: OverlayTriggerProps["children"]; + children: React.ReactElement; } export const PopupMenu: React.FC = ({ items, children }) => { const [show, setShow] = React.useState(false); const toggle = (): void => setShow(!show); - return ( - - setShow(false)} /> - - } - show={show} - onToggle={toggle} - > - {children} - - ); + // TODO: + + return setShow(false)} />; }; diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx index 79eb2732..fff11b95 100644 --- a/FrontEnd/src/views/common/SearchInput.tsx +++ b/FrontEnd/src/views/common/SearchInput.tsx @@ -1,7 +1,8 @@ import React, { useCallback } from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { Spinner, Form, Button } from "react-bootstrap"; + +import LoadingButton from "./button/LoadingButton"; export interface SearchInputProps { value: string; @@ -64,13 +65,9 @@ const SearchInput: React.FC = (props) => { "flex-shrink-0" )} > - {props.loading ? ( - - ) : ( - - )} + + {props.buttonText ?? t("search")} + ); diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx new file mode 100644 index 00000000..783c9be2 --- /dev/null +++ b/FrontEnd/src/views/common/Spinner.tsx @@ -0,0 +1,13 @@ +import { PaletteColorType } from "@/palette"; +import React from "react"; + +export interface SpinnerProps { + size?: "sm" | "md" | "lg" | number; + color?: PaletteColorType; +} + +export default function Spinner( + props: SpinnerProps +): React.ReactElement | null { + return ; +} diff --git a/FrontEnd/src/views/common/TabPages.tsx b/FrontEnd/src/views/common/TabPages.tsx index 2b1d91cb..b7a9fb36 100644 --- a/FrontEnd/src/views/common/TabPages.tsx +++ b/FrontEnd/src/views/common/TabPages.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Nav } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { convertI18nText, I18nText, UiLogicError } from "@/common"; @@ -31,6 +30,8 @@ const TabPages: React.FC = ({ pageContainerClassName, pageContainerStyle, }) => { + // TODO: + if (pages.length === 0) { throw new UiLogicError("Page list can't be empty."); } @@ -47,23 +48,6 @@ const TabPages: React.FC = ({ return (
-
{currentPage.page}
diff --git a/FrontEnd/src/views/common/alert/AlertHost.tsx b/FrontEnd/src/views/common/alert/AlertHost.tsx index 949be7ed..21b9882d 100644 --- a/FrontEnd/src/views/common/alert/AlertHost.tsx +++ b/FrontEnd/src/views/common/alert/AlertHost.tsx @@ -1,7 +1,6 @@ import React from "react"; import without from "lodash/without"; import { useTranslation } from "react-i18next"; -import { Alert } from "react-bootstrap"; import { alertService, @@ -52,13 +51,7 @@ export const AutoCloseAlert: React.FC = (props) => { }; return ( - +
{(() => { const { message } = alert; if (typeof message === "function") { @@ -66,7 +59,7 @@ export const AutoCloseAlert: React.FC = (props) => { return ; } else return convertI18nText(message, t); })()} - +
); }; diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx index fd1c19b3..aee83aa2 100644 --- a/FrontEnd/src/views/common/button/LoadingButton.tsx +++ b/FrontEnd/src/views/common/button/LoadingButton.tsx @@ -1,26 +1,19 @@ import React from "react"; -const LoadingButton: React.FC<{ loading?: boolean } & ButtonProps> = ({ +import { CommonButtonProps } from "./common"; +import Button from "./Button"; +import Spinner from "../Spinner"; + +const LoadingButton: React.FC<{ loading?: boolean } & CommonButtonProps> = ({ loading, - variant, disabled, + color, ...otherProps }) => { return ( - ); }; diff --git a/FrontEnd/src/views/common/dailog/OperationDialog.tsx b/FrontEnd/src/views/common/dailog/OperationDialog.tsx index 129e85d5..1e765276 100644 --- a/FrontEnd/src/views/common/dailog/OperationDialog.tsx +++ b/FrontEnd/src/views/common/dailog/OperationDialog.tsx @@ -5,6 +5,8 @@ import moment from "moment"; import { convertI18nText, I18nText, UiLogicError } from "@/common"; +import { PaletteColorType } from "@/palette"; + import Button from "../button/Button"; import LoadingButton from "../button/LoadingButton"; import Dialog from "./Dialog"; @@ -144,7 +146,7 @@ export interface OperationDialogProps< open: boolean; onClose: () => void; title: I18nText | (() => React.ReactNode); - themeColor?: "danger" | "success" | string; + themeColor?: PaletteColorType; onProcess: ( inputs: MapOperationInputInfoValueTypeList ) => Promise; @@ -290,50 +292,42 @@ const OperationDialog = < if (item.type === "text") { return ( - +
{item.label && ( - {convertI18nText(item.label, t)} + )} - { const v = e.target.value; updateValue(index, v); }} - isInvalid={error != null} disabled={process} /> - {error != null && ( - - {error} - - )} - {item.helperText && ( - {t(item.helperText)} - )} - + {error != null &&
{error}
} + {item.helperText &&
{t(item.helperText)}
} +
); } else if (item.type === "bool") { return ( - - +
+ { updateValue(index, event.currentTarget.checked); }} - label={convertI18nText(item.label, t)} disabled={process} /> - + +
); } else if (item.type === "select") { return ( - - {convertI18nText(item.label, t)} - + + +
); } else if (item.type === "color") { return ( - +
{item.canBeNull ? ( - + { @@ -365,42 +359,35 @@ const OperationDialog = < updateValue(index, null); } }} - label={convertI18nText(item.label, t)} disabled={process} /> - ) : ( - {convertI18nText(item.label, t)} - )} + ) : null} + {value !== null && ( updateValue(index, result.hex)} /> )} - +
); } else if (item.type === "datetime") { return ( - +
{item.label && ( - {convertI18nText(item.label, t)} + )} - { const v = e.target.value; updateValue(index, v); }} - isInvalid={error != null} disabled={process} /> - {error != null && ( - - {error} - - )} - + {error != null &&
{error}
} +
); } })} @@ -412,7 +399,7 @@ const OperationDialog = < onClick={close} /> { diff --git a/FrontEnd/src/views/login/index.tsx b/FrontEnd/src/views/login/index.tsx index 6c0aaf67..6d70c64a 100644 --- a/FrontEnd/src/views/login/index.tsx +++ b/FrontEnd/src/views/login/index.tsx @@ -81,67 +81,59 @@ const LoginPage: React.FC = (_) => {

{t("welcome")}

- {t("user.username")} - { - setUsername(e.target.value); - setUsernameDirty(true); - }} - value={username} - isInvalid={usernameDirty && username === ""} - /> - {usernameDirty && username === "" && ( - - {t("login.emptyUsername")} - - )} - {t("user.password")} - { - setPassword(e.target.value); - setPasswordDirty(true); - }} - value={password} - onKeyDown={onEnterPressInPassword} - isInvalid={passwordDirty && password === ""} - /> - {passwordDirty && password === "" && ( - - {t("login.emptyPassword")} - - )} - - - - id="remember-me" - type="checkbox" - checked={rememberMe} - onChange={(e) => { - setRememberMe(e.currentTarget.checked); - }} - label={t("user.rememberMe")} - /> - - {error ?

{t(error)}

: null} -
- { - submit(); - e.preventDefault(); - }} - disabled={username === "" || password === "" ? true : undefined} - > - {t("user.login")} - -
- + + { + setUsername(e.target.value); + setUsernameDirty(true); + }} + value={username} + /> + {usernameDirty && username === "" && ( +
{t("login.emptyUsername")}
+ )} + + { + setPassword(e.target.value); + setPasswordDirty(true); + }} + value={password} + onKeyDown={onEnterPressInPassword} + /> + {passwordDirty && password === "" && ( +
{t("login.emptyPassword")}
+ )} +
+
+ { + setRememberMe(e.currentTarget.checked); + }} + /> + +
+ {error ?

{t(error)}

: null} +
+ { + submit(); + e.preventDefault(); + }} + disabled={username === "" || password === "" ? true : undefined} + > + {t("user.login")} + +
); }; diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx index 6cb64dd3..9f0753b9 100644 --- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx @@ -1,15 +1,17 @@ +/* eslint-disable react/jsx-no-undef */ import React from "react"; import classnames from "classnames"; -import { Form, Spinner } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { Prompt } from "react-router"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; +import TimelinePostBuilder from "@/services/TimelinePostBuilder"; + import FlatButton from "../common/button/FlatButton"; import TabPages from "../common/TabPages"; -import TimelinePostBuilder from "@/services/TimelinePostBuilder"; import ConfirmDialog from "../common/dailog/ConfirmDialog"; +import Spinner from "../common/Spinner"; export interface MarkdownPostEditProps { timeline: string; @@ -102,7 +104,7 @@ const MarkdownPostEdit: React.FC = ({ pageContainerClassName="py-2" actions={ process ? ( - + ) : ( <> void; }> = ({ user, add, onAction }) => { - const { t } = useTranslation(); - return ( - +
@@ -42,7 +41,7 @@ const TimelineMemberItem: React.FC<{
) : null}
- +
); }; @@ -109,7 +108,7 @@ const TimelineMemberUserSearch: React.FC<{ return
{t("timeline.member.noUserAvailableToAdd")}
; } else { return ( - +
{users.map((user) => ( ))} - +
); } } else if (userSearchState.type === "error") { @@ -152,7 +151,7 @@ const TimelineMember: React.FC = (props) => { return (
- +
{members.map((member, index) => ( = (props) => { } /> ))} - +
{timeline.manageable ? ( ) : null} @@ -187,8 +186,8 @@ export const TimelineMemberDialog: React.FC = ( props ) => { return ( - + - + ); }; diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx index d05f18d4..ea6e8d40 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Container } from "react-bootstrap"; import { HubConnectionState } from "@microsoft/signalr"; import { HttpTimelineInfo } from "@/http/timeline"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx deleted file mode 100644 index e04bb7e1..00000000 --- a/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; - -import Button from "../common/button/Button"; - -const TimelinePostDeleteConfirmDialog: React.FC<{ - onClose: () => void; - onConfirm: () => void; -}> = ({ onClose, onConfirm }) => { - const { t } = useTranslation(); - - return ( - - - - {t("timeline.post.deleteDialog.title")} - - - {t("timeline.post.deleteDialog.prompt")} - - - - - ); -}; - -export default TimelinePostDeleteConfirmDialog; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx index 5e59bee4..13aacb54 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx @@ -265,7 +265,6 @@ const TimelinePostEdit: React.FC = (props) => {
= (props) => { ) : null} {dialog === "delete" ? ( - { setDialog(null); setOperationMaskVisible(false); -- cgit v1.2.3 From 81e54efaae0ec1d5f1f90c3ca40448f8c61f0e80 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 1 Jul 2021 16:43:59 +0800 Subject: ... --- FrontEnd/src/views/admin/UserAdmin.tsx | 12 ++++++------ FrontEnd/src/views/center/TimelineBoard.tsx | 6 +++--- FrontEnd/src/views/common/button/Button.css | 16 +++++++++------- FrontEnd/src/views/common/dailog/ConfirmDialog.tsx | 7 +++++-- FrontEnd/src/views/common/dailog/Dialog.css | 9 +++++++++ FrontEnd/src/views/common/dailog/OperationDialog.css | 0 FrontEnd/src/views/common/dailog/OperationDialog.tsx | 16 +++++++++++----- FrontEnd/src/views/common/index.css | 3 ++- FrontEnd/src/views/common/menu/Menu.css | 2 +- FrontEnd/src/views/login/index.tsx | 2 +- FrontEnd/src/views/settings/index.tsx | 11 ++++++----- FrontEnd/src/views/timeline-common/TimelinePostView.tsx | 2 +- 12 files changed, 54 insertions(+), 32 deletions(-) create mode 100644 FrontEnd/src/views/common/dailog/OperationDialog.css (limited to 'FrontEnd/src/views/common/dailog/OperationDialog.tsx') diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index 481db1cc..6d2760f2 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -206,23 +206,23 @@ const UserItem: React.FC = ({ user, on }) => { return (
setEditMaskVisible(true)} /> -

{user.username}

-
+

{user.username}

+
{t("admin:user.nickname")} {user.nickname}
-
+
{t("admin:user.uniqueId")} {user.uniqueId}
-
+
{t("admin:user.permissions")} {user.permissions.map((permission) => { return ( - + {permission}{" "} ); diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index 3961a7bc..8c1f5fac 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -48,16 +48,16 @@ const TimelineBoardItem: React.FC = ({ )} {title} - {name} + {name} {actions != null ? (
{ e.currentTarget.setPointerCapture(e.pointerId); actions.onMove.start(e); diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css index ec2dd798..54127f05 100644 --- a/FrontEnd/src/views/common/button/Button.css +++ b/FrontEnd/src/views/common/button/Button.css @@ -57,7 +57,8 @@ } .cru-button:not(.outline):disabled { - background-color: var(--cru-button-f3-color); + background-color: var(--cru-disable-color); + cursor: auto; } .cru-button.outline { @@ -67,23 +68,24 @@ padding: 0.2em 0.5em; border-radius: 0.2em; transition: all 0.6s; - background-color: var(--cru-background-color); + background-color: white; } .cru-button.outline:hover { color: var(--cru-button-f1-color); border-color: var(--cru-button-f1-color); - background-color: var(--cru-background-1-color); + background-color: var(--cru-background-color); } .cru-button.outline:active { color: var(--cru-button-f2-color); border-color: var(--cru-button-f2-color); - background-color: var(--cru-background-2-color); + background-color: var(--cru-background-1-color); } .cru-button.outline:disabled { - color: var(--cru-button-f3-color); - border-color: var(--cru-button-f3-color); - background-color: var(--cru-background-3-color); + color: var(--cru-disable-color); + border-color: var(--cru-disable-color); + background-color: white; + cursor: auto; } diff --git a/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx b/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx index 1ad52350..c10b1cdb 100644 --- a/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx +++ b/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx @@ -16,12 +16,15 @@ const ConfirmDialog: React.FC<{ return ( -

{convertI18nText(title, t)}

+

{convertI18nText(title, t)}

+

{convertI18nText(body, t)}

-
+
+
-
+
+
@@ -446,11 +451,12 @@ const OperationDialog = <

{title}

+
{body}
); diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css index 02eed6d9..62167cfc 100644 --- a/FrontEnd/src/views/common/index.css +++ b/FrontEnd/src/views/common/index.css @@ -2,7 +2,8 @@ --cru-background-color: #f8f9fa; --cru-background-1-color: #e9ecef; --cru-background-2-color: #dee2e6; - --cru-background-3-color: #ced4da; + + --cru-disable-color: #ced4da; --cru-primary-color: rgb(0, 123, 255); --cru-primary-l1-color: rgb(26, 136, 255); diff --git a/FrontEnd/src/views/common/menu/Menu.css b/FrontEnd/src/views/common/menu/Menu.css index c933b34f..a30eb5c6 100644 --- a/FrontEnd/src/views/common/menu/Menu.css +++ b/FrontEnd/src/views/common/menu/Menu.css @@ -22,7 +22,7 @@ } .cru-menu-item.color-secondary:hover { - color: var(--cru-text-on-secondary-color); + color: var(--cru-secondary-t-color); background-color: var(--cru-secondary-color); } diff --git a/FrontEnd/src/views/login/index.tsx b/FrontEnd/src/views/login/index.tsx index 6d70c64a..782acdaa 100644 --- a/FrontEnd/src/views/login/index.tsx +++ b/FrontEnd/src/views/login/index.tsx @@ -122,7 +122,7 @@ const LoginPage: React.FC = (_) => {
{error ?

{t(error)}

: null} -
+
{ diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx index f25911d7..69a74327 100644 --- a/FrontEnd/src/views/settings/index.tsx +++ b/FrontEnd/src/views/settings/index.tsx @@ -28,7 +28,7 @@ const SettingsPage: React.FC = (_) => {
{user ? ( -

+

{t("settings.subheaders.account")}

{ {t("settings.changeNickname")}
setDialog("changepassword")} > {t("settings.changePassword")}
{ setDialog("logout"); }} @@ -60,13 +60,13 @@ const SettingsPage: React.FC = (_) => { ) : null} -

+

{t("settings.subheaders.customization")}

{t("settings.languagePrimary")}
- + {t("settings.languageSecondary")}
@@ -94,6 +94,7 @@ const SettingsPage: React.FC = (_) => { title="settings.dialogConfirmLogout.title" body="settings.dialogConfirmLogout.prompt" onClose={() => setDialog(null)} + open onConfirm={() => { void userService.logout().then(() => { history.push("/"); diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx index 995c43df..652ff9c9 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -64,7 +64,7 @@ const TimelinePostView: React.FC = (props) => { > {post.editable ? ( { setOperationMaskVisible(true); e.stopPropagation(); -- cgit v1.2.3 From 672778ca5d9de5513c86d70394b2dd048639cdea Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 1 Jul 2021 17:16:20 +0800 Subject: ... --- FrontEnd/src/views/common/dailog/Dialog.css | 1 + .../src/views/common/dailog/OperationDialog.css | 26 ++++++++++++ .../src/views/common/dailog/OperationDialog.tsx | 49 ++++++++++++++++++---- .../src/views/timeline/TimelineDeleteDialog.tsx | 2 +- 4 files changed, 68 insertions(+), 10 deletions(-) (limited to 'FrontEnd/src/views/common/dailog/OperationDialog.tsx') diff --git a/FrontEnd/src/views/common/dailog/Dialog.css b/FrontEnd/src/views/common/dailog/Dialog.css index 67bfe2a9..22b420fc 100644 --- a/FrontEnd/src/views/common/dailog/Dialog.css +++ b/FrontEnd/src/views/common/dailog/Dialog.css @@ -15,6 +15,7 @@ .cru-dialog-container { max-width: 100%; + min-width: 30vw; margin: auto; diff --git a/FrontEnd/src/views/common/dailog/OperationDialog.css b/FrontEnd/src/views/common/dailog/OperationDialog.css index e69de29b..26c3920b 100644 --- a/FrontEnd/src/views/common/dailog/OperationDialog.css +++ b/FrontEnd/src/views/common/dailog/OperationDialog.css @@ -0,0 +1,26 @@ +.cru-operation-dialog-group { + display: block; + margin: 0.4em 0; +} + +.cru-operation-dialog-label { + display: block; + font-size: 0.8em; + color: var(--cru-primary-color); +} + +.cru-operation-dialog-inline-label { + margin-inline-start: 0.5em; +} + +.cru-operation-dialog-error-text { + display: block; + font-size: 0.8em; + color: var(--cru-danger-color); +} + +.cru-operation-dialog-helper-text { + display: block; + font-size: 0.8em; + color: var(--cru-primary-color); +} diff --git a/FrontEnd/src/views/common/dailog/OperationDialog.tsx b/FrontEnd/src/views/common/dailog/OperationDialog.tsx index f394a3d3..083d60da 100644 --- a/FrontEnd/src/views/common/dailog/OperationDialog.tsx +++ b/FrontEnd/src/views/common/dailog/OperationDialog.tsx @@ -12,6 +12,7 @@ import LoadingButton from "../button/LoadingButton"; import Dialog from "./Dialog"; import "./OperationDialog.css"; +import classNames from "classnames"; interface DefaultErrorPromptProps { error?: string; @@ -294,9 +295,17 @@ const OperationDialog = < if (item.type === "text") { return ( -
+
{item.label && ( - + )} - {error != null &&
{error}
} - {item.helperText &&
{t(item.helperText)}
} + {error != null && ( +
+ {error} +
+ )} + {item.helperText && ( +
+ {t(item.helperText)} +
+ )}
); } else if (item.type === "bool") { @@ -327,8 +344,16 @@ const OperationDialog = < ); } else if (item.type === "select") { return ( -
- +
+ - + + {error != null && ( +
+ {error} +
+ )} + {item.helperText && ( +
+ {t(item.helperText)} +
+ )}
); } else if (item.type === "select") { @@ -374,7 +394,13 @@ const OperationDialog = < ); } else if (item.type === "color") { return ( -
+
{item.canBeNull ? ( +
{item.label && ( - + )}