aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2021-05-06 22:01:12 +0800
committercrupest <crupest@outlook.com>2021-05-06 22:01:12 +0800
commitccc9fd220ac52b2b24ebe9e5978a63fd2ec8c968 (patch)
tree6184aac232f9f327d4a8ddacdc7b226ff7617499 /FrontEnd/src
parent669b425e4795ce245c664c82223ee9fa4611dfa9 (diff)
downloadtimeline-ccc9fd220ac52b2b24ebe9e5978a63fd2ec8c968.tar.gz
timeline-ccc9fd220ac52b2b24ebe9e5978a63fd2ec8c968.tar.bz2
timeline-ccc9fd220ac52b2b24ebe9e5978a63fd2ec8c968.zip
feat: Timeline color.
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/app/http/timeline.ts1
-rw-r--r--FrontEnd/src/app/locales/en/translation.json3
-rw-r--r--FrontEnd/src/app/locales/zh/translation.json3
-rw-r--r--FrontEnd/src/app/palette.ts4
-rw-r--r--FrontEnd/src/app/views/admin/UserAdmin.tsx4
-rw-r--r--FrontEnd/src/app/views/common/OperationDialog.tsx107
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx57
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);