diff options
Diffstat (limited to 'FrontEnd/src')
-rw-r--r-- | FrontEnd/src/app/http/timeline.ts | 1 | ||||
-rw-r--r-- | FrontEnd/src/app/locales/en/translation.json | 3 | ||||
-rw-r--r-- | FrontEnd/src/app/locales/zh/translation.json | 3 | ||||
-rw-r--r-- | FrontEnd/src/app/palette.ts | 4 | ||||
-rw-r--r-- | FrontEnd/src/app/views/admin/UserAdmin.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/app/views/common/OperationDialog.tsx | 107 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx | 57 |
7 files changed, 120 insertions, 59 deletions
diff --git a/FrontEnd/src/app/http/timeline.ts b/FrontEnd/src/app/http/timeline.ts index 50af259e..efc402c1 100644 --- a/FrontEnd/src/app/http/timeline.ts +++ b/FrontEnd/src/app/http/timeline.ts @@ -72,6 +72,7 @@ export interface HttpTimelinePostPostRequest { export interface HttpTimelinePatchRequest { name?: string; title?: string; + color?: string; visibility?: TimelineVisibility; description?: string; } diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json index 73cee2e6..1261b086 100644 --- a/FrontEnd/src/app/locales/en/translation.json +++ b/FrontEnd/src/app/locales/en/translation.json @@ -85,7 +85,8 @@ "title": "Change Timeline Properties", "titleField": "Title", "visibility": "Visibility", - "description": "Description" + "description": "Description", + "color": "Color" }, "member": { "noUserAvailableToAdd": "Sorry, no user available to be a member in search result.", diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json index 1a1a70ab..b2c651f6 100644 --- a/FrontEnd/src/app/locales/zh/translation.json +++ b/FrontEnd/src/app/locales/zh/translation.json @@ -85,7 +85,8 @@ "title": "修改时间线属性", "titleField": "标题", "visibility": "可见性", - "description": "描述" + "description": "描述", + "color": "颜色" }, "member": { "noUserAvailableToAdd": "搜索结果显示没有可以添加为成员的用户。", diff --git a/FrontEnd/src/app/palette.ts b/FrontEnd/src/app/palette.ts index 98e7d814..c4f4f4f9 100644 --- a/FrontEnd/src/app/palette.ts +++ b/FrontEnd/src/app/palette.ts @@ -95,9 +95,11 @@ const paletteSubject: BehaviorSubject<Palette> = new BehaviorSubject<Palette>( export const palette$: Observable<Palette> = paletteSubject.asObservable(); palette$.subscribe((palette) => { - let styleTag = document.getElementById("timeline-palette-css"); + const styleTagId = "timeline-palette-css"; + let styleTag = document.getElementById(styleTagId); if (styleTag == null) { styleTag = document.createElement("style"); + styleTag.id = styleTagId; document.head.append(styleTag); } styleTag.innerHTML = generatePaletteCSS(palette); 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); |