diff options
Diffstat (limited to 'FrontEnd/src/app/views')
3 files changed, 112 insertions, 56 deletions
diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index 61140bb1..26c40a33 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -3,7 +3,7 @@ import classnames from "classnames"; import { ListGroup, Row, Col, Spinner, Button } from "react-bootstrap"; import OperationDialog, { - OperationBoolInputInfo, + OperationDialogBoolInput, } from "../common/OperationDialog"; import { AuthUser } from "@/services/user"; @@ -145,7 +145,7 @@ const UserPermissionModifyDialog: React.FC< 0<UsernameLabel>{username}</UsernameLabel>2 </Trans> )} - inputScheme={kUserPermissionList.map<OperationBoolInputInfo>( + inputScheme={kUserPermissionList.map<OperationDialogBoolInput>( (permission, index) => ({ type: "bool", label: permission, diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx index 5887be48..c9fa709f 100644 --- a/FrontEnd/src/app/views/common/OperationDialog.tsx +++ b/FrontEnd/src/app/views/common/OperationDialog.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Form, Button, Modal } from "react-bootstrap"; +import { ChromePicker } from "react-color"; import { convertI18nText, I18nText, UiLogicError } from "@/common"; @@ -27,7 +28,7 @@ const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => { return result; }; -export interface OperationTextInputInfo { +export interface OperationDialogTextInput { type: "text"; label?: I18nText; password?: boolean; @@ -39,40 +40,51 @@ export interface OperationTextInputInfo { helperText?: string; } -export interface OperationBoolInputInfo { +export interface OperationDialogBoolInput { type: "bool"; label: I18nText; initValue?: boolean; } -export interface OperationSelectInputInfoOption { +export interface OperationDialogSelectInputOption { value: string; label: I18nText; icon?: React.ReactElement; } -export interface OperationSelectInputInfo { +export interface OperationDialogSelectInput { type: "select"; label: I18nText; - options: OperationSelectInputInfoOption[]; + options: OperationDialogSelectInputOption[]; initValue?: string; } -export type OperationInputInfo = - | OperationTextInputInfo - | OperationBoolInputInfo - | OperationSelectInputInfo; +export interface OperationDialogColorInput { + type: "color"; + label?: I18nText; + initValue?: string | null; + disableAlpha?: boolean; + canBeNull?: boolean; +} -type MapOperationInputInfoValueType<T> = T extends OperationTextInputInfo +export type OperationDialogInput = + | OperationDialogTextInput + | OperationDialogBoolInput + | OperationDialogSelectInput + | OperationDialogColorInput; + +type MapOperationInputInfoValueType<T> = T extends OperationDialogTextInput ? string - : T extends OperationBoolInputInfo + : T extends OperationDialogBoolInput ? boolean - : T extends OperationSelectInputInfo + : T extends OperationDialogSelectInput ? string + : T extends OperationDialogColorInput + ? string | null : never; type MapOperationInputInfoValueTypeList< - Tuple extends readonly OperationInputInfo[] + Tuple extends readonly OperationDialogInput[] > = { [Index in keyof Tuple]: MapOperationInputInfoValueType<Tuple[Index]>; } & { length: Tuple["length"] }; @@ -94,7 +106,7 @@ const isNoError = (error: OperationInputError): boolean => { export interface OperationDialogProps< TData, - OperationInputInfoList extends readonly OperationInputInfo[] + OperationInputInfoList extends readonly OperationDialogInput[] > { open: boolean; close: () => void; @@ -116,18 +128,18 @@ export interface OperationDialogProps< const OperationDialog = < TData, - OperationInputInfoList extends readonly OperationInputInfo[] + OperationInputInfoList extends readonly OperationDialogInput[] >( props: OperationDialogProps<TData, OperationInputInfoList> ): React.ReactElement => { const inputScheme = (props.inputScheme ?? - []) as readonly OperationInputInfo[]; + []) as readonly OperationDialogInput[]; const { t } = useTranslation(); type Step = - | "input" - | "process" + | { type: "input" } + | { type: "process" } | { type: "success"; data: TData; @@ -136,14 +148,20 @@ const OperationDialog = < type: "failure"; data: unknown; }; - const [step, setStep] = useState<Step>("input"); - const [values, setValues] = useState<(boolean | string)[]>( + const [step, setStep] = useState<Step>({ type: "input" }); + + type ValueType = boolean | string | null | undefined; + + const [values, setValues] = useState<ValueType[]>( inputScheme.map((i) => { if (i.type === "bool") { return i.initValue ?? false; } else if (i.type === "text" || i.type === "select") { return i.initValue ?? ""; - } else { + } else if (i.type === "color") { + return i.initValue ?? null; + } + { throw new UiLogicError("Unknown input scheme."); } }) @@ -154,13 +172,9 @@ const OperationDialog = < const [inputError, setInputError] = useState<OperationInputError>(); const close = (): void => { - if (step !== "process") { + if (step.type !== "process") { props.close(); - if ( - typeof step === "object" && - step.type === "success" && - props.onSuccessAndClose - ) { + if (step.type === "success" && props.onSuccessAndClose) { props.onSuccessAndClose(step.data); } } else { @@ -169,7 +183,7 @@ const OperationDialog = < }; const onConfirm = (): void => { - setStep("process"); + setStep({ type: "process" }); props .onProcess( (values as unknown) as MapOperationInputInfoValueTypeList<OperationInputInfoList> @@ -191,8 +205,8 @@ const OperationDialog = < }; let body: React.ReactNode; - if (step === "input" || step === "process") { - const process = step === "process"; + if (step.type === "input" || step.type === "process") { + const process = step.type === "process"; let inputPrompt = typeof props.inputPrompt === "function" @@ -200,7 +214,7 @@ const OperationDialog = < : convertI18nText(props.inputPrompt, t); inputPrompt = <h6>{inputPrompt}</h6>; - const validate = (values: (string | boolean)[]): boolean => { + const validate = (values: ValueType[]): boolean => { const { inputValidator } = props; if (inputValidator != null) { const result = inputValidator( @@ -212,7 +226,7 @@ const OperationDialog = < return true; }; - const updateValue = (index: number, newValue: string | boolean): void => { + const updateValue = (index: number, newValue: ValueType): void => { const oldValues = values; const newValues = oldValues.slice(); newValues[index] = newValue; @@ -301,6 +315,35 @@ const OperationDialog = < </Form.Control> </Form.Group> ); + } else if (item.type === "color") { + return ( + <Form.Group key={index}> + {item.canBeNull ? ( + <Form.Check<"input"> + 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} + /> + ) : ( + <Form.Label>{convertI18nText(item.label, t)}</Form.Label> + )} + {value !== null && ( + <ChromePicker + color={value as string} + onChange={(result) => updateValue(index, result.hex)} + disableAlpha={item.disableAlpha} + /> + )} + </Form.Group> + ); } })} </Modal.Body> diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx index a5628a9a..c65e097d 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -31,30 +31,39 @@ const TimelinePropertyChangeDialog: React.FC<TimelinePropertyChangeDialogProps> return ( <OperationDialog title={"timeline.dialogChangeProperty.title"} - inputScheme={[ - { - type: "text", - label: "timeline.dialogChangeProperty.titleField", - initValue: timeline.title, - }, - { - type: "select", - label: "timeline.dialogChangeProperty.visibility", - options: kTimelineVisibilities.map((v) => ({ - label: labelMap[v], - value: v, - })), - initValue: timeline.visibility, - }, - { - type: "text", - label: "timeline.dialogChangeProperty.description", - initValue: timeline.description, - }, - ]} + inputScheme={ + [ + { + type: "text", + label: "timeline.dialogChangeProperty.titleField", + initValue: timeline.title, + }, + { + type: "select", + label: "timeline.dialogChangeProperty.visibility", + options: kTimelineVisibilities.map((v) => ({ + label: labelMap[v], + value: v, + })), + initValue: timeline.visibility, + }, + { + type: "text", + label: "timeline.dialogChangeProperty.description", + initValue: timeline.description, + }, + { + type: "color", + label: "timeline.dialogChangeProperty.color", + initValue: timeline.color ?? null, + disableAlpha: true, + canBeNull: true, + }, + ] as const + } open={props.open} close={props.close} - onProcess={([newTitle, newVisibility, newDescription]) => { + onProcess={([newTitle, newVisibility, newDescription, newColor]) => { const req: HttpTimelinePatchRequest = {}; if (newTitle !== timeline.title) { req.title = newTitle; @@ -65,6 +74,10 @@ const TimelinePropertyChangeDialog: React.FC<TimelinePropertyChangeDialogProps> if (newDescription !== timeline.description) { req.description = newDescription; } + const nc = newColor ?? "#007bff"; + if (nc !== timeline.color) { + req.color = nc; + } return getHttpTimelineClient() .patchTimeline(timeline.name, req) .then(onChange); |