From 26b861b9ce3b3966feab51b865bd3bd0630ae4c9 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 12 Mar 2021 17:46:27 +0800 Subject: ... --- FrontEnd/src/app/locales/en/translation.json | 2 + FrontEnd/src/app/locales/zh/translation.json | 2 + FrontEnd/src/app/services/TimelinePostBuilder.ts | 6 +- .../app/views/timeline-common/MarkdownPostEdit.tsx | 116 +++++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx (limited to 'FrontEnd') diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json index 4002ee4f..5613668e 100644 --- a/FrontEnd/src/app/locales/en/translation.json +++ b/FrontEnd/src/app/locales/en/translation.json @@ -2,7 +2,9 @@ "welcome": "Welcome!", "search": "Search", "edit": "Edit", + "image": "Image", "done": "Done", + "preview": "Preview", "loadFailReload": "Load failed, <1>click here to reload.", "error": { "network": "Network error.", diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json index 3f966d7c..c73f2876 100644 --- a/FrontEnd/src/app/locales/zh/translation.json +++ b/FrontEnd/src/app/locales/zh/translation.json @@ -2,7 +2,9 @@ "welcome": "欢迎!", "search": "搜索", "edit": "编辑", + "image": "图片", "done": "完成", + "preview": "预览", "loadFailReload": "加载失败,<1>点击重试。", "error": { "network": "网络错误。", diff --git a/FrontEnd/src/app/services/TimelinePostBuilder.ts b/FrontEnd/src/app/services/TimelinePostBuilder.ts index 8594fa49..f6c8f881 100644 --- a/FrontEnd/src/app/services/TimelinePostBuilder.ts +++ b/FrontEnd/src/app/services/TimelinePostBuilder.ts @@ -10,7 +10,7 @@ import { UiLogicError } from "@/common"; import { base64 } from "@/http/common"; import { HttpTimelinePostPostRequest } from "@/http/timeline"; -export class TimelinePostBuilder { +export default class TimelinePostBuilder { private _onChange: () => void; private _text = ""; private _images: { file: File; url: string }[] = []; @@ -86,6 +86,10 @@ export class TimelinePostBuilder { this._onChange(); } + get text(): string { + return this._text; + } + get images(): { file: File; url: string }[] { return this._images; } diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx new file mode 100644 index 00000000..079344e1 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import { Nav, Form } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; + +import TimelinePostBuilder from "@/services/TimelinePostBuilder"; + +export interface MarkdownPostEditProps { + timeline: string; + onPosted: (post: HttpTimelinePostInfo) => void; +} + +const MarkdownPostEdit: React.FC = ({ + timeline: timelineName, + onPosted, +}) => { + const { t } = useTranslation(); + + const [tab, setTab] = React.useState<"text" | "images" | "preview">("text"); + + const [process, setProcess] = React.useState(false); + + const [text, _setText] = React.useState(""); + const [images, _setImages] = React.useState<{ file: File; url: string }[]>( + [] + ); + const [previewHtml, _setPreviewHtml] = React.useState(""); + + const _builder = React.useRef(null); + + const getBuilder = (): TimelinePostBuilder => { + if (_builder.current == null) { + const builder = new TimelinePostBuilder(() => { + _setText(builder.text); + _setImages(builder.images); + _setPreviewHtml(builder.renderHtml()); + }); + _builder.current = builder; + } + return _builder.current; + }; + + React.useEffect(() => { + return () => { + getBuilder().dispose(); + }; + }, []); + + const send = async (): Promise => { + const dataList = await getBuilder().build(); + const post = await getHttpTimelineClient().postPost(timelineName, { + dataList, + }); + onPosted(post); + }; + + return ( +
+ +
+ {(() => { + if (tab === "text") { + return ( + { + getBuilder().setMarkdownText(event.currentTarget.value); + }} + /> + ); + } else if (tab === "images") { + return
; + } else { + return
; + } + })()} +
+
+ ); +}; + +export default MarkdownPostEdit; -- cgit v1.2.3 From e83da106a259f4a8ab11324eceef2c15a9a08bf0 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Mar 2021 21:01:07 +0800 Subject: ... --- FrontEnd/src/app/index.sass | 5 + FrontEnd/src/app/services/TimelinePostBuilder.ts | 32 ++-- FrontEnd/src/app/views/common/TabPages.tsx | 74 +++++++++ FrontEnd/src/app/views/common/common.sass | 4 + .../app/views/timeline-common/MarkdownPostEdit.tsx | 147 ++++++++++-------- .../timeline-common/TimelinePostContentView.tsx | 2 +- .../app/views/timeline-common/TimelinePostEdit.tsx | 167 ++++++++++----------- .../app/views/timeline-common/timeline-common.sass | 8 + 8 files changed, 269 insertions(+), 170 deletions(-) create mode 100644 FrontEnd/src/app/views/common/TabPages.tsx (limited to 'FrontEnd') diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass index 39dce693..60f274c2 100644 --- a/FrontEnd/src/app/index.sass +++ b/FrontEnd/src/app/index.sass @@ -95,3 +95,8 @@ textarea i line-height: 1 + +.markdown-container + img + max-height: 200px + max-width: 100% diff --git a/FrontEnd/src/app/services/TimelinePostBuilder.ts b/FrontEnd/src/app/services/TimelinePostBuilder.ts index f6c8f881..92d8484b 100644 --- a/FrontEnd/src/app/services/TimelinePostBuilder.ts +++ b/FrontEnd/src/app/services/TimelinePostBuilder.ts @@ -1,8 +1,3 @@ -import { - escapeHtml, - replaceEntities, - unescapeMd, -} from "remarkable/lib/common/utils"; import { Remarkable } from "remarkable"; import { UiLogicError } from "@/common"; @@ -18,28 +13,16 @@ export default class TimelinePostBuilder { constructor(onChange: () => void) { this._onChange = onChange; + const oldImageRenderer = this._md.renderer.rules.image; this._md.renderer.rules.image = (( _t: TimelinePostBuilder ): Remarkable.Rule => function (tokens, idx, options /*, env */) { const i = parseInt(tokens[idx].src); - const src = - ' src="' + - (isNaN(i) && i > 0 && i <= _t._images.length - ? escapeHtml(tokens[idx].src) - : _t._images[i - 1].url) + - '"'; - const title = tokens[idx].title - ? ' title="' + escapeHtml(replaceEntities(tokens[idx].title)) + '"' - : ""; - const alt = - ' alt="' + - (tokens[idx].alt - ? escapeHtml(replaceEntities(unescapeMd(tokens[idx].alt))) - : "") + - '"'; - const suffix = options?.xhtmlOut ? " /" : ""; - return ""; + if (!isNaN(i) && i > 0 && i <= _t._images.length) { + tokens[idx].src = _t._images[i - 1].url; + } + return oldImageRenderer(tokens, idx, options); })(this); } @@ -49,6 +32,7 @@ export default class TimelinePostBuilder { } appendImage(file: File): void { + this._images = this._images.slice(); this._images.push({ file, url: URL.createObjectURL(file), @@ -69,6 +53,8 @@ export default class TimelinePostBuilder { newIndex = this._images.length - 1; } + this._images = this._images.slice(); + const [old] = this._images.splice(oldIndex, 1); this._images.splice(newIndex, 0, old); @@ -80,6 +66,8 @@ export default class TimelinePostBuilder { throw new UiLogicError("Old index out of range."); } + this._images = this._images.slice(); + URL.revokeObjectURL(this._images[index].url); this._images.splice(index, 1); diff --git a/FrontEnd/src/app/views/common/TabPages.tsx b/FrontEnd/src/app/views/common/TabPages.tsx new file mode 100644 index 00000000..424e769f --- /dev/null +++ b/FrontEnd/src/app/views/common/TabPages.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { Nav } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +import { convertI18nText, I18nText, UiLogicError } from "@/common"; + +export interface TabPage { + id: string; + tabText: I18nText; + page: React.ReactNode; +} + +export interface TabPagesProps { + pages: TabPage[]; + actions?: React.ReactNode; + className?: string; + style?: React.CSSProperties; + navClassName?: string; + navStyle?: React.CSSProperties; + pageContainerClassName?: string; + pageContainerStyle?: React.CSSProperties; +} + +const TabPages: React.FC = ({ + pages, + actions, + className, + style, + navClassName, + navStyle, + pageContainerClassName, + pageContainerStyle, +}) => { + if (pages.length === 0) { + throw new UiLogicError("Page list can't be empty."); + } + + const { t } = useTranslation(); + + const [tab, setTab] = React.useState(pages[0].id); + + const currentPage = pages.find((p) => p.id === tab); + + if (currentPage == null) { + throw new UiLogicError("Current tab value is bad."); + } + + return ( +
+ +
+ {currentPage.page} +
+
+ ); +}; + +export default TabPages; diff --git a/FrontEnd/src/app/views/common/common.sass b/FrontEnd/src/app/views/common/common.sass index 0a30d995..f3022d19 100644 --- a/FrontEnd/src/app/views/common/common.sass +++ b/FrontEnd/src/app/views/common/common.sass @@ -92,3 +92,7 @@ .cru-menu-divider border-top: 1px solid $gray-200 + +.cru-tab-pages-action-area + display: flex + align-items: center diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx index 079344e1..d43077b4 100644 --- a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx @@ -1,24 +1,31 @@ import React from "react"; -import { Nav, Form } from "react-bootstrap"; +import { Form } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; +import TabPages from "../common/TabPages"; import TimelinePostBuilder from "@/services/TimelinePostBuilder"; export interface MarkdownPostEditProps { timeline: string; onPosted: (post: HttpTimelinePostInfo) => void; + onPostError: () => void; + onClose: () => void; + className?: string; + style?: React.CSSProperties; } const MarkdownPostEdit: React.FC = ({ timeline: timelineName, onPosted, + onClose, + onPostError, + className, + style, }) => { const { t } = useTranslation(); - const [tab, setTab] = React.useState<"text" | "images" | "preview">("text"); - const [process, setProcess] = React.useState(false); const [text, _setText] = React.useState(""); @@ -48,68 +55,88 @@ const MarkdownPostEdit: React.FC = ({ }, []); const send = async (): Promise => { - const dataList = await getBuilder().build(); - const post = await getHttpTimelineClient().postPost(timelineName, { - dataList, - }); - onPosted(post); + setProcess(true); + try { + const dataList = await getBuilder().build(); + const post = await getHttpTimelineClient().postPost(timelineName, { + dataList, + }); + onPosted(post); + onClose(); + } catch (e) { + setProcess(false); + onPostError(); + } }; return ( -
- -
- {(() => { - if (tab === "text") { - return ( - { - getBuilder().setMarkdownText(event.currentTarget.value); + +
+ {t("operationDialog.cancel")} +
+
+ {t("timeline.send")} +
+ + } + pages={[ + { + id: "text", + tabText: "edit", + page: ( + { + getBuilder().setMarkdownText(event.currentTarget.value); + }} + /> + ), + }, + { + id: "images", + tabText: "image", + page: ( +
+ {images.map((image) => ( + + ))} + ) => { + const { files } = event.currentTarget; + if (files != null && files.length !== 0) { + getBuilder().appendImage(files[0]); + } }} + disabled={process} /> - ); - } else if (tab === "images") { - return
; - } else { - return
; - } - })()} -
-
+
+ ), + }, + { + id: "preview", + tabText: "preview", + page: ( +
+ ), + }, + ]} + /> ); }; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx index d836d1db..58fae4c7 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx @@ -152,7 +152,7 @@ const MarkdownView: React.FC = (props) => { } return (
= (props) => { const [process, setProcess] = React.useState(false); - const [kind, setKind] = React.useState("text"); + const [kind, setKind] = React.useState>("text"); + const [showMarkdown, setShowMarkdown] = React.useState(false); const [text, setText] = React.useState(""); - const [markdown, setMarkdown] = React.useState(""); const [image, setImage] = React.useState(null); const draftTextLocalStorageKey = `timeline.${timeline.name}.postDraft.text`; - const draftMarkdownLocalStorageKey = `timeline.${timeline.name}.postDraft.markdown`; React.useEffect(() => { setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? ""); - setMarkdown( - window.localStorage.getItem(draftMarkdownLocalStorageKey) ?? "" - ); - }, [draftTextLocalStorageKey, draftMarkdownLocalStorageKey]); + }, [draftTextLocalStorageKey]); const canSend = (kind === "text" && text.length !== 0) || - (kind === "image" && image != null) || - (kind === "markdown" && markdown.length !== 0); + (kind === "image" && image != null); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = React.useRef(null!); @@ -160,6 +156,13 @@ const TimelinePostEdit: React.FC = (props) => { }; }); + const onPostError = (): void => { + pushAlert({ + type: "danger", + message: "timeline.sendPostFailed", + }); + }; + const onSend = async (): Promise => { setProcess(true); @@ -171,12 +174,6 @@ const TimelinePostEdit: React.FC = (props) => { data: await base64(text), }; break; - case "markdown": - requestData = { - contentType: "text/markdown", - data: await base64(markdown), - }; - break; case "image": if (image == null) { throw new UiLogicError( @@ -201,20 +198,14 @@ const TimelinePostEdit: React.FC = (props) => { if (kind === "text") { setText(""); window.localStorage.removeItem(draftTextLocalStorageKey); - } else if (kind === "markdown") { - setMarkdown(""); - window.localStorage.removeItem(draftMarkdownLocalStorageKey); } setProcess(false); setKind("text"); onPosted(data); }, (_) => { - pushAlert({ - type: "danger", - message: "timeline.sendPostFailed", - }); setProcess(false); + onPostError(); } ); }; @@ -224,73 +215,75 @@ const TimelinePostEdit: React.FC = (props) => { ref={containerRef} className={clsx("container-fluid bg-light", className)} > - - - {(() => { - if (kind === "text") { - return ( - { - setText(t); - window.localStorage.setItem(draftTextLocalStorageKey, t); - }} - /> - ); - } else if (kind === "image") { - return ( - - ); - } else if (kind === "markdown") { - return ( - { - setMarkdown(t); - window.localStorage.setItem( - draftMarkdownLocalStorageKey, - t - ); - }} + {showMarkdown ? ( + setShowMarkdown(false)} + timeline={timeline.name} + onPosted={onPosted} + onPostError={onPostError} + /> + ) : ( + + + {(() => { + if (kind === "text") { + return ( + { + setText(t); + window.localStorage.setItem(draftTextLocalStorageKey, t); + }} + /> + ); + } else if (kind === "image") { + return ( + + ); + } + })()} + + +
+ ({ + type: "button", + text: `timeline.post.type.${kind}`, + iconClassName: postKindIconClassNameMap[kind], + onClick: () => { + if (kind === "markdown") { + setShowMarkdown(true); + } else { + setKind(kind); + } + }, + }))} + > + - ); - } - })()} - - -
- ({ - type: "button", - text: `timeline.post.type.${kind}`, - iconClassName: postKindIconClassNameMap[kind], - onClick: () => { - setKind(kind); - }, - }))} + +
+ - -
-
- - {t("timeline.send")} - - -
+ {t("timeline.send")} + + +
+ )}
); }; diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass index 43c4d0f7..04318674 100644 --- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass +++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass @@ -170,3 +170,11 @@ $timeline-line-color-current: #36c2e6 top: 56px right: 0 margin: 0.5em + +.timeline-markdown-post-edit-page + overflow: scroll + max-height: 300px + +.timeline-markdown-post-edit-image + max-width: 100% + max-height: 200px -- cgit v1.2.3 From 36145a18463e0ad4fb372df82cd488f1b4dae2f7 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Mar 2021 21:17:12 +0800 Subject: feat: Prevent leave. --- FrontEnd/src/app/locales/en/translation.json | 1 + FrontEnd/src/app/locales/zh/translation.json | 1 + FrontEnd/src/app/services/TimelinePostBuilder.ts | 4 + .../app/views/timeline-common/MarkdownPostEdit.tsx | 149 ++++++++++++--------- 4 files changed, 90 insertions(+), 65 deletions(-) (limited to 'FrontEnd') diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json index 5613668e..8fcd5bcd 100644 --- a/FrontEnd/src/app/locales/en/translation.json +++ b/FrontEnd/src/app/locales/en/translation.json @@ -67,6 +67,7 @@ "send": "Send", "deletePostFailed": "Failed to delete post.", "sendPostFailed": "Failed to send post.", + "confirmLeave": "Are you sure to leave? All content you typed would be lost.", "visibility": { "public": "public to everyone", "register": "only registed people can see", diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json index c73f2876..f03c4c03 100644 --- a/FrontEnd/src/app/locales/zh/translation.json +++ b/FrontEnd/src/app/locales/zh/translation.json @@ -67,6 +67,7 @@ "send": "发送", "deletePostFailed": "删除消息失败。", "sendPostFailed": "发送消息失败。", + "confirmLeave":"确定要离开吗?所有输入的内容将会丢失。", "visibility": { "public": "对所有人公开", "register": "仅注册可见", diff --git a/FrontEnd/src/app/services/TimelinePostBuilder.ts b/FrontEnd/src/app/services/TimelinePostBuilder.ts index 92d8484b..40279eca 100644 --- a/FrontEnd/src/app/services/TimelinePostBuilder.ts +++ b/FrontEnd/src/app/services/TimelinePostBuilder.ts @@ -82,6 +82,10 @@ export default class TimelinePostBuilder { return this._images; } + get isEmpty(): boolean { + return this._text.length === 0 && this._images.length === 0; + } + renderHtml(): string { return this._md.render(this._text); } diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx index d43077b4..ab6aafea 100644 --- a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Form } from "react-bootstrap"; import { useTranslation } from "react-i18next"; +import { Prompt } from "react-router"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; @@ -26,6 +27,8 @@ const MarkdownPostEdit: React.FC = ({ }) => { const { t } = useTranslation(); + const [canLeave, setCanLeave] = React.useState(true); + const [process, setProcess] = React.useState(false); const [text, _setText] = React.useState(""); @@ -39,6 +42,7 @@ const MarkdownPostEdit: React.FC = ({ const getBuilder = (): TimelinePostBuilder => { if (_builder.current == null) { const builder = new TimelinePostBuilder(() => { + setCanLeave(builder.isEmpty); _setText(builder.text); _setImages(builder.images); _setPreviewHtml(builder.renderHtml()); @@ -54,6 +58,18 @@ const MarkdownPostEdit: React.FC = ({ }; }, []); + React.useEffect(() => { + window.onbeforeunload = () => { + if (!canLeave) { + return t("timeline.confirmLeave"); + } + }; + + return () => { + window.onbeforeunload = null; + }; + }, [canLeave, t]); + const send = async (): Promise => { setProcess(true); try { @@ -70,73 +86,76 @@ const MarkdownPostEdit: React.FC = ({ }; return ( - -
- {t("operationDialog.cancel")} -
-
- {t("timeline.send")} -
- - } - pages={[ - { - id: "text", - tabText: "edit", - page: ( - { - getBuilder().setMarkdownText(event.currentTarget.value); - }} - /> - ), - }, - { - id: "images", - tabText: "image", - page: ( -
- {images.map((image) => ( - - ))} - ) => { - const { files } = event.currentTarget; - if (files != null && files.length !== 0) { - getBuilder().appendImage(files[0]); - } - }} + <> + + +
+ {t("operationDialog.cancel")} +
+
+ {t("timeline.send")} +
+ + } + pages={[ + { + id: "text", + tabText: "edit", + page: ( + { + getBuilder().setMarkdownText(event.currentTarget.value); + }} /> -
- ), - }, - { - id: "preview", - tabText: "preview", - page: ( -
- ), - }, - ]} - /> + ), + }, + { + id: "images", + tabText: "image", + page: ( +
+ {images.map((image) => ( + + ))} + ) => { + const { files } = event.currentTarget; + if (files != null && files.length !== 0) { + getBuilder().appendImage(files[0]); + } + }} + disabled={process} + /> +
+ ), + }, + { + id: "preview", + tabText: "preview", + page: ( +
+ ), + }, + ]} + /> + ); }; -- cgit v1.2.3 From cf161b23fb1eaecec1c4b4ab3cf5ec620cd897bc Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Mar 2021 21:28:30 +0800 Subject: feat: Prevent leave. --- FrontEnd/src/app/locales/en/translation.json | 1 + FrontEnd/src/app/locales/zh/translation.json | 3 +- FrontEnd/src/app/views/common/ConfirmDialog.tsx | 40 ++++++++++++++++++++++ .../app/views/timeline-common/MarkdownPostEdit.tsx | 25 +++++++++++++- 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 FrontEnd/src/app/views/common/ConfirmDialog.tsx (limited to 'FrontEnd') diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json index 8fcd5bcd..65ddbe0c 100644 --- a/FrontEnd/src/app/locales/en/translation.json +++ b/FrontEnd/src/app/locales/en/translation.json @@ -67,6 +67,7 @@ "send": "Send", "deletePostFailed": "Failed to delete post.", "sendPostFailed": "Failed to send post.", + "dropDraft": "Drop Draft", "confirmLeave": "Are you sure to leave? All content you typed would be lost.", "visibility": { "public": "public to everyone", diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json index f03c4c03..f6971241 100644 --- a/FrontEnd/src/app/locales/zh/translation.json +++ b/FrontEnd/src/app/locales/zh/translation.json @@ -67,7 +67,8 @@ "send": "发送", "deletePostFailed": "删除消息失败。", "sendPostFailed": "发送消息失败。", - "confirmLeave":"确定要离开吗?所有输入的内容将会丢失。", + "dropDraft": "放弃草稿", + "confirmLeave": "确定要离开吗?所有输入的内容将会丢失。", "visibility": { "public": "对所有人公开", "register": "仅注册可见", diff --git a/FrontEnd/src/app/views/common/ConfirmDialog.tsx b/FrontEnd/src/app/views/common/ConfirmDialog.tsx new file mode 100644 index 00000000..72940c51 --- /dev/null +++ b/FrontEnd/src/app/views/common/ConfirmDialog.tsx @@ -0,0 +1,40 @@ +import { convertI18nText, I18nText } from "@/common"; +import React from "react"; +import { Modal, Button } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +const ConfirmDialog: React.FC<{ + onClose: () => void; + onConfirm: () => void; + title: I18nText; + body: I18nText; +}> = ({ onClose, onConfirm, title, body }) => { + const { t } = useTranslation(); + + return ( + + + + {convertI18nText(title, t)} + + + {convertI18nText(body, t)} + + + + + + ); +}; + +export default ConfirmDialog; diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx index ab6aafea..bad6b2b0 100644 --- a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx @@ -7,6 +7,7 @@ import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import TabPages from "../common/TabPages"; import TimelinePostBuilder from "@/services/TimelinePostBuilder"; +import ConfirmDialog from "../common/ConfirmDialog"; export interface MarkdownPostEditProps { timeline: string; @@ -31,6 +32,11 @@ const MarkdownPostEdit: React.FC = ({ const [process, setProcess] = React.useState(false); + const [ + showLeaveConfirmDialog, + setShowLeaveConfirmDialog, + ] = React.useState(false); + const [text, _setText] = React.useState(""); const [images, _setImages] = React.useState<{ file: File; url: string }[]>( [] @@ -94,7 +100,16 @@ const MarkdownPostEdit: React.FC = ({ pageContainerClassName="py-2" actions={ <> -
+
{ + if (canLeave) { + onClose(); + } else { + setShowLeaveConfirmDialog(true); + } + }} + > {t("operationDialog.cancel")}
@@ -155,6 +170,14 @@ const MarkdownPostEdit: React.FC = ({ }, ]} /> + {showLeaveConfirmDialog && ( + setShowLeaveConfirmDialog(false)} + onConfirm={onClose} + title="timeline.dropDraft" + body="timeline.confirmLeave" + /> + )} ); }; -- cgit v1.2.3 From 4aecd115b435b3217329824ddf54c87b32e77b89 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Mar 2021 21:41:11 +0800 Subject: feat: Prevent send empty. --- FrontEnd/src/app/index.sass | 17 +++++++--- FrontEnd/src/app/views/common/FlatButton.tsx | 36 ++++++++++++++++++++++ .../app/views/timeline-common/MarkdownPostEdit.tsx | 12 +++++--- 3 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 FrontEnd/src/app/views/common/FlatButton.tsx (limited to 'FrontEnd') diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass index 60f274c2..2079cad8 100644 --- a/FrontEnd/src/app/index.sass +++ b/FrontEnd/src/app/index.sass @@ -45,8 +45,15 @@ small cursor: pointer padding: 0.2em 0.5em border-radius: 0.2em - &:hover + &:hover:not(.disabled) background-color: $gray-200 + &.disabled + cursor: default + @each $color, $value in $theme-colors + &.#{$color} + color: $value + &.disabled + color: adjust-color($value, $lightness: +15%) .cursor-pointer cursor: pointer @@ -81,10 +88,10 @@ textarea .text-yellow color: $yellow -@each $color, $value in $theme-colors - .text-button - background: transparent - border: none +.text-button + background: transparent + border: none + @each $color, $value in $theme-colors &.#{$color} color: $value &:hover diff --git a/FrontEnd/src/app/views/common/FlatButton.tsx b/FrontEnd/src/app/views/common/FlatButton.tsx new file mode 100644 index 00000000..80bb654c --- /dev/null +++ b/FrontEnd/src/app/views/common/FlatButton.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import clsx from "clsx"; + +import { BootstrapThemeColor } from "@/common"; + +export interface FlatButtonProps { + variant?: BootstrapThemeColor | string; + disabled?: boolean; + className?: string; + style?: React.CSSProperties; + onClick?: () => void; +} + +const FlatButton: React.FC = (props) => { + const { disabled, className, style } = props; + const variant = props.variant ?? "primary"; + + const onClick = disabled ? undefined : props.onClick; + + return ( +
+ {props.children} +
+ ); +}; + +export default FlatButton; diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx index bad6b2b0..f4351db0 100644 --- a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx @@ -5,6 +5,7 @@ import { Prompt } from "react-router"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; +import FlatButton from "../common/FlatButton"; import TabPages from "../common/TabPages"; import TimelinePostBuilder from "@/services/TimelinePostBuilder"; import ConfirmDialog from "../common/ConfirmDialog"; @@ -100,8 +101,9 @@ const MarkdownPostEdit: React.FC = ({ pageContainerClassName="py-2" actions={ <> -
{ if (canLeave) { onClose(); @@ -111,10 +113,10 @@ const MarkdownPostEdit: React.FC = ({ }} > {t("operationDialog.cancel")} -
-
+ + {t("timeline.send")} -
+ } pages={[ -- cgit v1.2.3 From 0a944720fecb3a2fdfdc483449c5ab0e58df7c02 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Mar 2021 21:48:46 +0800 Subject: fix: Fix a list with no key. --- FrontEnd/src/app/views/common/Menu.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'FrontEnd') diff --git a/FrontEnd/src/app/views/common/Menu.tsx b/FrontEnd/src/app/views/common/Menu.tsx index 54650f22..d942d452 100644 --- a/FrontEnd/src/app/views/common/Menu.tsx +++ b/FrontEnd/src/app/views/common/Menu.tsx @@ -30,12 +30,13 @@ const Menu: React.FC = ({ items, className, onItemClicked }) => { return (
- {items.map((item) => { + {items.map((item, index) => { if (item.type === "divider") { - return
; + return
; } else { return (
Date: Thu, 18 Mar 2021 21:59:16 +0800 Subject: feat: Delete image. --- .../app/views/timeline-common/MarkdownPostEdit.tsx | 20 +++++++++++++++----- .../app/views/timeline-common/timeline-common.sass | 10 ++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) (limited to 'FrontEnd') diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx index f4351db0..68dee848 100644 --- a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx @@ -139,12 +139,22 @@ const MarkdownPostEdit: React.FC = ({ tabText: "image", page: (
- {images.map((image) => ( - ( +
+ className="timeline-markdown-post-edit-image-container" + > + + { + getBuilder().deleteImage(index); + }} + /> +
))} Date: Thu, 18 Mar 2021 22:04:04 +0800 Subject: feat: Only support image types. --- FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FrontEnd') diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx index 68dee848..fb38d2f7 100644 --- a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx @@ -158,7 +158,7 @@ const MarkdownPostEdit: React.FC = ({ ))} ) => { const { files } = event.currentTarget; if (files != null && files.length !== 0) { -- cgit v1.2.3