aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2021-06-03 23:20:22 +0800
committercrupest <crupest@outlook.com>2021-06-03 23:20:22 +0800
commit5a96a88121f3ecb846c5cd55c3f51624b4e21402 (patch)
tree68ebf231a0887440c4390cba494fe43f34050b2d /FrontEnd
parentb8a43d19cc3e850d42959ca019a3de6a453de11d (diff)
downloadtimeline-5a96a88121f3ecb846c5cd55c3f51624b4e21402.tar.gz
timeline-5a96a88121f3ecb846c5cd55c3f51624b4e21402.tar.bz2
timeline-5a96a88121f3ecb846c5cd55c3f51624b4e21402.zip
feat: Add change post property dialog.
Diffstat (limited to 'FrontEnd')
-rw-r--r--FrontEnd/src/app/http/timeline.ts23
-rw-r--r--FrontEnd/src/app/index.sass3
-rw-r--r--FrontEnd/src/app/locales/en/translation.json7
-rw-r--r--FrontEnd/src/app/locales/zh/translation.json7
-rw-r--r--FrontEnd/src/app/views/common/OperationDialog.tsx55
-rw-r--r--FrontEnd/src/app/views/timeline-common/PostPropertyChangeDialog.tsx36
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx1
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx46
8 files changed, 159 insertions, 19 deletions
diff --git a/FrontEnd/src/app/http/timeline.ts b/FrontEnd/src/app/http/timeline.ts
index efc402c1..9697c1a0 100644
--- a/FrontEnd/src/app/http/timeline.ts
+++ b/FrontEnd/src/app/http/timeline.ts
@@ -77,6 +77,11 @@ export interface HttpTimelinePatchRequest {
description?: string;
}
+export interface HttpTimelinePostPatchRequest {
+ time?: string;
+ color?: string;
+}
+
export class HttpTimelineNameConflictError extends Error {
constructor(public innerError?: AxiosError) {
super();
@@ -101,6 +106,11 @@ export interface IHttpTimelineClient {
timelineName: string,
req: HttpTimelinePostPostRequest
): Promise<HttpTimelinePostInfo>;
+ patchPost(
+ timelineName: string,
+ postId: number,
+ req: HttpTimelinePostPatchRequest
+ ): Promise<HttpTimelinePostInfo>;
deletePost(timelineName: string, postId: number): Promise<void>;
}
@@ -189,6 +199,19 @@ export class HttpTimelineClient implements IHttpTimelineClient {
.then(extractResponseData);
}
+ patchPost(
+ timelineName: string,
+ postId: number,
+ req: HttpTimelinePostPatchRequest
+ ): Promise<HttpTimelinePostInfo> {
+ return axios
+ .patch<HttpTimelinePostInfo>(
+ `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}`,
+ req
+ )
+ .then(extractResponseData);
+ }
+
deletePost(timelineName: string, postId: number): Promise<void> {
return axios
.delete(`${apiBaseUrl}/timelines/${timelineName}/posts/${postId}`)
diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass
index 0ba65bb1..4cee155f 100644
--- a/FrontEnd/src/app/index.sass
+++ b/FrontEnd/src/app/index.sass
@@ -18,6 +18,9 @@
.tl-color-primary
color: var(--tl-primary-color)
+.tl-color-danger
+ color: var(--tl-danger-color)
+
small
line-height: 1.2
diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json
index 49268961..a2766b4e 100644
--- a/FrontEnd/src/app/locales/en/translation.json
+++ b/FrontEnd/src/app/locales/en/translation.json
@@ -5,6 +5,8 @@
"image": "Image",
"done": "Done",
"preview": "Preview",
+ "delete": "Delete",
+ "changeProperty": "Change Property",
"loadFailReload": "Load failed, <1>click here to reload</1>.",
"error": {
"network": "Network error.",
@@ -95,6 +97,11 @@
"description": "Description",
"color": "Color"
},
+ "changePostPropertyDialog": {
+ "title": "Change Post Properties",
+ "time": "Date and time",
+ "timeEmpty": "You must select a time."
+ },
"member": {
"noUserAvailableToAdd": "Sorry, no user available to be a member in search result.",
"add": "Add",
diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json
index 728c3b81..5a5a6843 100644
--- a/FrontEnd/src/app/locales/zh/translation.json
+++ b/FrontEnd/src/app/locales/zh/translation.json
@@ -6,6 +6,8 @@
"done": "完成",
"preview": "预览",
"loadFailReload": "加载失败,<1>点击重试</1>。",
+ "delete": "删除",
+ "changeProperty": "修改属性",
"error": {
"network": "网络错误。",
"unknown": "未知错误。"
@@ -95,6 +97,11 @@
"description": "描述",
"color": "颜色"
},
+ "changePostPropertyDialog": {
+ "title": "修改消息属性",
+ "time": "时间",
+ "timeEmpty": "你必须选择一个时间。"
+ },
"member": {
"noUserAvailableToAdd": "搜索结果显示没有可以添加为成员的用户。",
"add": "添加",
diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx
index 40c14e9e..0ede42e5 100644
--- a/FrontEnd/src/app/views/common/OperationDialog.tsx
+++ b/FrontEnd/src/app/views/common/OperationDialog.tsx
@@ -66,11 +66,18 @@ export interface OperationDialogColorInput {
canBeNull?: boolean;
}
+export interface OperationDialogDateTimeInput {
+ type: "datetime";
+ label?: I18nText;
+ initValue?: string;
+}
+
export type OperationDialogInput =
| OperationDialogTextInput
| OperationDialogBoolInput
| OperationDialogSelectInput
- | OperationDialogColorInput;
+ | OperationDialogColorInput
+ | OperationDialogDateTimeInput;
type MapOperationInputInfoValueType<T> = T extends OperationDialogTextInput
? string
@@ -80,8 +87,20 @@ type MapOperationInputInfoValueType<T> = T extends OperationDialogTextInput
? string
: T extends OperationDialogColorInput
? string | null
+ : T extends OperationDialogDateTimeInput
+ ? string
: never;
+const defaultValueMap: {
+ [T in OperationDialogInput as T["type"]]: MapOperationInputInfoValueType<T>;
+} = {
+ bool: false,
+ color: null,
+ datetime: "",
+ select: "",
+ text: "",
+};
+
type MapOperationInputInfoValueTypeList<
Tuple extends readonly OperationDialogInput[]
> = {
@@ -153,14 +172,9 @@ const OperationDialog = <
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 if (i.type === "color") {
- return i.initValue ?? null;
- }
- {
+ if (i.type in defaultValueMap) {
+ return i.initValue ?? defaultValueMap[i.type];
+ } else {
throw new UiLogicError("Unknown input scheme.");
}
})
@@ -342,6 +356,29 @@ const OperationDialog = <
)}
</Form.Group>
);
+ } else if (item.type === "datetime") {
+ return (
+ <Form.Group key={index}>
+ {item.label && (
+ <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
+ )}
+ <Form.Control
+ type="datetime-local"
+ value={value as string}
+ onChange={(e) => {
+ const v = e.target.value;
+ updateValue(index, v);
+ }}
+ isInvalid={error != null}
+ disabled={process}
+ />
+ {error != null && (
+ <Form.Control.Feedback type="invalid">
+ {error}
+ </Form.Control.Feedback>
+ )}
+ </Form.Group>
+ );
}
})}
</Modal.Body>
diff --git a/FrontEnd/src/app/views/timeline-common/PostPropertyChangeDialog.tsx b/FrontEnd/src/app/views/timeline-common/PostPropertyChangeDialog.tsx
new file mode 100644
index 00000000..001e52d7
--- /dev/null
+++ b/FrontEnd/src/app/views/timeline-common/PostPropertyChangeDialog.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
+
+import OperationDialog from "../common/OperationDialog";
+
+function PostPropertyChangeDialog(props: {
+ onClose: () => void;
+ post: HttpTimelinePostInfo;
+ onSuccess: (post: HttpTimelinePostInfo) => void;
+}): React.ReactElement | null {
+ const { onClose, post, onSuccess } = props;
+
+ return (
+ <OperationDialog
+ title="timeline.changePostPropertyDialog.title"
+ close={onClose}
+ open
+ inputScheme={[
+ {
+ label: "timeline.changePostPropertyDialog.time",
+ type: "datetime",
+ initValue: post.time,
+ },
+ ]}
+ onProcess={([time]) => {
+ return getHttpTimelineClient().patchPost(post.timelineName, post.id, {
+ time: time === "" ? undefined : new Date(time).toISOString(),
+ });
+ }}
+ onSuccessAndClose={onSuccess}
+ />
+ );
+}
+
+export default PostPropertyChangeDialog;
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx
index d9c45a4c..ba204b72 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx
@@ -64,6 +64,7 @@ const TimelinePostListView: React.FC<TimelinePostListViewProps> = (props) => {
key={post.id}
post={post}
current={posts.length - 1 === post.index}
+ onChanged={onReload}
onDeleted={onReload}
/>
);
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx
index 2f778ab1..a008d8e5 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx
@@ -1,6 +1,7 @@
import React from "react";
import classnames from "classnames";
import { Link } from "react-router-dom";
+import { useTranslation } from "react-i18next";
import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
@@ -10,6 +11,7 @@ import UserAvatar from "../common/user/UserAvatar";
import TimelineLine from "./TimelineLine";
import TimelinePostContentView from "./TimelinePostContentView";
import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog";
+import PostPropertyChangeDialog from "./PostPropertyChangeDialog";
export interface TimelinePostViewProps {
post: HttpTimelinePostInfo;
@@ -17,16 +19,20 @@ export interface TimelinePostViewProps {
className?: string;
style?: React.CSSProperties;
cardStyle?: React.CSSProperties;
- onDeleted?: () => void;
+ onChanged: (post: HttpTimelinePostInfo) => void;
+ onDeleted: () => void;
}
const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => {
- const { post, className, style, cardStyle, onDeleted } = props;
+ const { post, className, style, cardStyle, onChanged, onDeleted } = props;
const current = props.current === true;
+ const { t } = useTranslation();
+
const [operationMaskVisible, setOperationMaskVisible] =
React.useState<boolean>(false);
- const [deleteDialog, setDeleteDialog] = React.useState<boolean>(false);
+ const [dialog, setDialog] =
+ React.useState<"delete" | "changeproperty" | null>(null);
const cardRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
@@ -84,25 +90,36 @@ const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => {
</div>
{operationMaskVisible ? (
<div
- className="position-absolute position-lt w-100 h-100 mask d-flex justify-content-center align-items-center"
+ className="position-absolute position-lt w-100 h-100 mask d-flex justify-content-around align-items-center"
onClick={() => {
setOperationMaskVisible(false);
}}
>
- <i
- className="bi-trash text-danger icon-button large"
+ <span
+ className="tl-color-primary"
+ onClick={(e) => {
+ setDialog("changeproperty");
+ e.stopPropagation();
+ }}
+ >
+ {t("changeProperty")}
+ </span>
+ <span
+ className="tl-color-danger"
onClick={(e) => {
- setDeleteDialog(true);
+ setDialog("delete");
e.stopPropagation();
}}
- />
+ >
+ {t("delete")}
+ </span>
</div>
) : null}
</div>
- {deleteDialog ? (
+ {dialog === "delete" ? (
<TimelinePostDeleteConfirmDialog
onClose={() => {
- setDeleteDialog(false);
+ setDialog(null);
setOperationMaskVisible(false);
}}
onConfirm={() => {
@@ -116,6 +133,15 @@ const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => {
});
}}
/>
+ ) : dialog === "changeproperty" ? (
+ <PostPropertyChangeDialog
+ onClose={() => {
+ setDialog(null);
+ setOperationMaskVisible(false);
+ }}
+ post={post}
+ onSuccess={onChanged}
+ />
) : null}
</div>
);