aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx15
-rw-r--r--FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx6
-rw-r--r--FrontEnd/src/pages/setting/ChangePasswordDialog.tsx6
-rw-r--r--FrontEnd/src/pages/setting/index.tsx46
-rw-r--r--FrontEnd/src/views/admin/UserAdmin.tsx8
-rw-r--r--FrontEnd/src/views/center/TimelineCreateDialog.tsx2
-rw-r--r--FrontEnd/src/views/common/button/LoadingButton.tsx5
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.tsx22
-rw-r--r--FrontEnd/src/views/common/dialog/index.ts56
-rw-r--r--FrontEnd/src/views/common/input/InputGroup.tsx7
-rw-r--r--FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx2
-rw-r--r--FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx2
-rw-r--r--FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx2
13 files changed, 116 insertions, 63 deletions
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<ChangeAvatarDialogProps> = (props) => {
@@ -42,12 +45,12 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
>("select");
const [message, setMessage] = useState<I18nText>(
- "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<ChangeAvatarDialogProps> = (props) => {
setFile(files[0]);
}
},
- []
+ [],
);
const onCropNext = React.useCallback(() => {
@@ -140,7 +143,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (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 (
<OperationDialog
open={open}
- close={close}
+ onClose={onClose}
title="settings.dialogChangePassword.title"
color="danger"
inputPrompt="settings.dialogChangePassword.prompt"
diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx
index cec81530..12a7670e 100644
--- a/FrontEnd/src/pages/setting/index.tsx
+++ b/FrontEnd/src/pages/setting/index.tsx
@@ -12,6 +12,7 @@ import { useC, Text } from "@/common";
import { useUser, userService } from "@/services/user";
import { getHttpUserClient } from "@/http/user";
+import { useDialog } from "@/views/common/dialog";
import ConfirmDialog from "@/views/common/dialog/ConfirmDialog";
import Card from "@/views/common/Card";
import Spinner from "@/views/common/Spinner";
@@ -237,18 +238,13 @@ export default function SettingPage() {
const user = useUser();
const navigate = useNavigate();
- type DialogName =
- | "change-password"
- | "change-avatar"
- | "change-nickname"
- | "logout"
- | "renew-register-code";
-
- const [dialog, setDialog] = useState<null | DialogName>(null);
-
- function dialogOpener(name: DialogName): () => void {
- return () => setDialog(name);
- }
+ const { dialogPropsMap, createDialogSwitch } = useDialog([
+ "change-password",
+ "change-avatar",
+ "change-nickname",
+ "logout",
+ "renew-register-code",
+ ]);
return (
<Page noTopPadding>
@@ -257,20 +253,20 @@ export default function SettingPage() {
<RegisterCodeSettingItem />
<ButtonSettingItem
title="settings.changeAvatar"
- onClick={dialogOpener("change-avatar")}
+ onClick={createDialogSwitch("change-avatar")}
/>
<ButtonSettingItem
title="settings.changeNickname"
- onClick={dialogOpener("change-nickname")}
+ onClick={createDialogSwitch("change-nickname")}
/>
<ButtonSettingItem
title="settings.changePassword"
- onClick={dialogOpener("change-password")}
+ onClick={createDialogSwitch("change-password")}
danger
/>
<ButtonSettingItem
title="settings.logout"
- onClick={dialogOpener("logout")}
+ onClick={createDialogSwitch("logout")}
danger
/>
</SettingSection>
@@ -278,31 +274,21 @@ export default function SettingPage() {
<SettingSection title="settings.subheader.customization">
<LanguageChangeSettingItem />
</SettingSection>
- <ChangePasswordDialog
- open={dialog === "change-password"}
- close={() => setDialog(null)}
- />
+ <ChangePasswordDialog {...dialogPropsMap["change-password"]} />
{user && (
<>
<ConfirmDialog
title="settings.dialogConfirmLogout.title"
body="settings.dialogConfirmLogout.prompt"
- onClose={() => setDialog(null)}
- open={dialog === "logout"}
onConfirm={() => {
void userService.logout().then(() => {
navigate("/");
});
}}
+ {...dialogPropsMap["logout"]}
/>
- <ChangeAvatarDialog
- open={dialog === "change-avatar"}
- close={() => setDialog(null)}
- />
- <ChangeNicknameDialog
- open={dialog === "change-nickname"}
- close={() => setDialog(null)}
- />
+ <ChangeAvatarDialog {...dialogPropsMap["change-avatar"]} />
+ <ChangeNicknameDialog {...dialogPropsMap["change-nickname"]} />
</>
)}
</Page>
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 (
<OperationDialog
open={open}
- close={close}
+ onClose={close}
title="admin:user.dialog.delete.title"
themeColor="danger"
inputPrompt={() => (
@@ -78,7 +78,7 @@ const UserModifyDialog: React.FC<{
return (
<OperationDialog
open={open}
- close={close}
+ onClose={close}
title="admin:user.dialog.modify.title"
themeColor="danger"
inputPrompt={() => (
@@ -126,7 +126,7 @@ const UserPermissionModifyDialog: React.FC<{
return (
<OperationDialog
open={open}
- close={close}
+ onClose={close}
title="admin:user.dialog.modifyPermissions.title"
themeColor="danger"
inputPrompt={() => (
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<TimelineCreateDialogProps> = (props) => {
return (
<OperationDialog
open={props.open}
- close={props.close}
+ onClose={props.close}
themeColor="success"
title="home.createDialog.title"
inputScheme={
diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx
index f23369de..bfa5b6b8 100644
--- a/FrontEnd/src/views/common/button/LoadingButton.tsx
+++ b/FrontEnd/src/views/common/button/LoadingButton.tsx
@@ -15,7 +15,8 @@ interface LoadingButtonProps extends React.ComponentPropsWithoutRef<"button"> {
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 (
<button
- disabled={loading}
+ disabled={disabled || loading}
className={classNames(
`cru-${color ?? "primary"} cru-button outline cru-loading-button`,
className,
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
index be3f7158..74b4a5fa 100644
--- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx
+++ b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
@@ -39,7 +39,7 @@ function OperationDialogPrompt(props: OperationDialogPromptProps) {
export interface OperationDialogProps<TData> {
open: boolean;
- close: () => void;
+ onClose: () => void;
color?: ThemeColor;
inputColor?: ThemeColor;
@@ -57,7 +57,7 @@ export interface OperationDialogProps<TData> {
function OperationDialog<TData>(props: OperationDialogProps<TData>) {
const {
open,
- close,
+ onClose,
color,
inputColor,
title,
@@ -85,13 +85,14 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
const [step, setStep] = useState<Step>({ type: "input" });
- const { inputGroupProps, hasError, setAllDisabled, confirm } = useInputs({
- init: inputs,
- });
+ const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } =
+ useInputs({
+ init: inputs,
+ });
- function onClose() {
+ function close() {
if (step.type !== "process") {
- close();
+ onClose();
if (step.type === "success" && onSuccessAndClose) {
onSuccessAndClose?.(step.data);
}
@@ -142,12 +143,13 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
text="operationDialog.cancel"
color="secondary"
outline
- onClick={onClose}
+ onClick={close}
disabled={isProcessing}
/>
<LoadingButton
color={color}
loading={isProcessing}
+ disabled={hasErrorAndDirty}
onClick={onConfirm}
>
{c("operationDialog.confirm")}
@@ -173,14 +175,14 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
<OperationDialogPrompt {...promptProps} />
<hr />
<div className="cru-dialog-bottom-area">
- <Button text="operationDialog.ok" color="primary" onClick={onClose} />
+ <Button text="operationDialog.ok" color="primary" onClick={close} />
</div>
</div>
);
}
return (
- <Dialog open={open} onClose={onClose}>
+ <Dialog open={open} onClose={close}>
<div
className={classNames(
"cru-operation-dialog-container",
diff --git a/FrontEnd/src/views/common/dialog/index.ts b/FrontEnd/src/views/common/dialog/index.ts
new file mode 100644
index 00000000..e37b9ed2
--- /dev/null
+++ b/FrontEnd/src/views/common/dialog/index.ts
@@ -0,0 +1,56 @@
+import { useState } from "react";
+
+export { default as Dialog } from "./Dialog";
+export { default as FullPageDialog } from "./FullPageDialog";
+export { default as OperationDialog } from "./OperationDialog";
+export { default as ConfirmDialog } from "./ConfirmDialog";
+
+type DialogMap<D extends string, V> = {
+ [K in D]: V;
+};
+
+type DialogKeyMap<D extends string> = DialogMap<D, number>;
+
+type DialogPropsMap<D extends string> = DialogMap<
+ D,
+ { key: number | string; open: boolean; onClose: () => void }
+>;
+
+export function useDialog<D extends string>(
+ dialogs: D[],
+ initDialog?: D | null,
+): {
+ dialog: D | null;
+ switchDialog: (newDialog: D | null) => void;
+ dialogPropsMap: DialogPropsMap<D>;
+ createDialogSwitch: (newDialog: D | null) => () => void;
+} {
+ const [dialog, setDialog] = useState<D | null>(initDialog ?? null);
+
+ const [dialogKeys, setDialogKeys] = useState<DialogKeyMap<D>>(
+ () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap<D>,
+ );
+
+ 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<D>,
+ 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 (
<OperationDialog
title="timeline.changePostPropertyDialog.title"
- close={onClose}
+ onClose={onClose}
open={open}
inputScheme={[
{
diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
index 3b5ba42f..c960b3c2 100644
--- a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
+++ b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
@@ -20,7 +20,7 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
return (
<OperationDialog
open={props.open}
- close={props.close}
+ onClose={props.close}
title="timeline.deleteDialog.title"
themeColor="danger"
inputPrompt={() => {
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) {