From dd5a23e36e70a912d7a99d0c8441c9ed8cc0edb7 Mon Sep 17 00:00:00 2001 From: boojack Date: Sun, 22 Jan 2023 21:16:28 +0800 Subject: [PATCH] feat: support creating resource with external link (#988) --- api/resource.go | 2 +- web/src/components/CreateResourceDialog.tsx | 243 ++++++++++++++++++++ web/src/components/MemoEditor.tsx | 34 +-- web/src/components/ResourcesDialog.tsx | 31 +-- web/src/types/modules/resource.d.ts | 1 + 5 files changed, 253 insertions(+), 58 deletions(-) create mode 100644 web/src/components/CreateResourceDialog.tsx diff --git a/api/resource.go b/api/resource.go index 0184c9b2..606b5d4d 100644 --- a/api/resource.go +++ b/api/resource.go @@ -27,7 +27,7 @@ type ResourceCreate struct { Filename string `json:"filename"` Blob []byte `json:"-"` ExternalLink string `json:"externalLink"` - Type string `json:"-"` + Type string `json:"type"` Size int64 `json:"-"` } diff --git a/web/src/components/CreateResourceDialog.tsx b/web/src/components/CreateResourceDialog.tsx new file mode 100644 index 00000000..ec80ea36 --- /dev/null +++ b/web/src/components/CreateResourceDialog.tsx @@ -0,0 +1,243 @@ +import { Button, Input, Select, Option, Typography, List, ListItem, Autocomplete } from "@mui/joy"; +import React, { useRef, useState } from "react"; +import { useResourceStore } from "../store/module"; +import Icon from "./Icon"; +import toastHelper from "./Toast"; +import { generateDialog } from "./Dialog"; + +const fileTypeAutocompleteOptions = ["image/*", "text/*", "audio/*", "video/*", "application/*"]; + +interface Props extends DialogProps { + onCancel?: () => void; + onConfirm?: (resourceList: Resource[]) => void; +} + +type SelectedMode = "local-file" | "external-link"; + +interface State { + selectedMode: SelectedMode; + uploadingFlag: boolean; +} + +const CreateResourceDialog: React.FC = (props: Props) => { + const { destroy, onCancel, onConfirm } = props; + const resourceStore = useResourceStore(); + const [state, setState] = useState({ + selectedMode: "local-file", + uploadingFlag: false, + }); + const [resourceCreate, setResourceCreate] = useState({ + filename: "", + externalLink: "", + type: "", + }); + const [fileList, setFileList] = useState([]); + const fileInputRef = useRef(null); + + const handleCloseDialog = () => { + if (onCancel) { + onCancel(); + } + destroy(); + }; + + const handleSelectedModeChanged = (mode: "local-file" | "external-link") => { + setState((state) => { + return { + ...state, + selectedMode: mode, + }; + }); + }; + + const handleExternalLinkChanged = (event: React.ChangeEvent) => { + const externalLink = event.target.value; + setResourceCreate((state) => { + return { + ...state, + externalLink, + }; + }); + }; + + const handleFileNameChanged = (event: React.ChangeEvent) => { + const filename = event.target.value; + setResourceCreate((state) => { + return { + ...state, + filename, + }; + }); + }; + + const handleFileTypeChanged = (fileType: string) => { + setResourceCreate((state) => { + return { + ...state, + type: fileType, + }; + }); + }; + + const handleFileInputChange = async () => { + if (!fileInputRef.current || !fileInputRef.current.files) { + return; + } + + const files: File[] = []; + for (const file of fileInputRef.current.files) { + files.push(file); + } + setFileList(files); + }; + + const allowConfirmAction = () => { + if (state.selectedMode === "local-file") { + if (!fileInputRef.current || !fileInputRef.current.files || fileInputRef.current.files.length === 0) { + return false; + } + } else if (state.selectedMode === "external-link") { + if (resourceCreate.filename === "" || resourceCreate.externalLink === "" || resourceCreate.type === "") { + return false; + } + } + return true; + }; + + const handleConfirmBtnClick = async () => { + if (state.uploadingFlag) { + return; + } + + setState((state) => { + return { + ...state, + uploadingFlag: true, + }; + }); + + const createdResourceList: Resource[] = []; + try { + if (state.selectedMode === "local-file") { + if (!fileInputRef.current || !fileInputRef.current.files) { + return; + } + for (const file of fileInputRef.current.files) { + const resource = await resourceStore.createResourceWithBlob(file); + createdResourceList.push(resource); + } + } else { + const resource = await resourceStore.createResource(resourceCreate); + createdResourceList.push(resource); + } + } catch (error: any) { + console.error(error); + toastHelper.error(error.response.data.message); + } + + if (onConfirm) { + onConfirm(createdResourceList); + } + destroy(); + }; + + return ( + <> +
+

Create Resource

+ +
+
+ + Upload method + + + + {state.selectedMode === "local-file" && ( + <> +
+ + +
+ + {fileList.map((file) => ( + {file.name} + ))} + + + )} + + {state.selectedMode === "external-link" && ( + <> + + Link + + + + File name + + + + Type + + handleFileTypeChanged(value || "")} + /> + + )} + +
+ + +
+
+ + ); +}; + +function showCreateResourceDialog(props: Omit) { + generateDialog( + { + dialogName: "create-resource-dialog", + }, + CreateResourceDialog, + props + ); +} + +export default showCreateResourceDialog; diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index 41856d71..1912d852 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -12,6 +12,7 @@ import Selector from "./common/Selector"; import Editor, { EditorRefActions } from "./Editor/Editor"; import ResourceIcon from "./ResourceIcon"; import showResourcesSelectorDialog from "./ResourcesSelectorDialog"; +import showCreateResourceDialog from "./CreateResourceDialog"; import "../less/memo-editor.less"; const listItemSymbolList = ["- [ ] ", "- [x] ", "- [X] ", "* ", "- "]; @@ -418,33 +419,11 @@ const MemoEditor = () => { }; const handleUploadFileBtnClick = () => { - const inputEl = document.createElement("input"); - inputEl.style.position = "fixed"; - inputEl.style.top = "-100vh"; - inputEl.style.left = "-100vw"; - document.body.appendChild(inputEl); - inputEl.type = "file"; - inputEl.multiple = true; - inputEl.accept = "*"; - inputEl.onchange = async () => { - if (!inputEl.files || inputEl.files.length === 0) { - return; - } - - const resourceList: Resource[] = []; - for (const file of inputEl.files) { - const resource = await handleUploadResource(file); - if (resource) { - resourceList.push(resource); - if (editorState.editMemoId) { - await upsertMemoResource(editorState.editMemoId, resource.id); - } - } - } - editorStore.setResourceList([...editorState.resourceList, ...resourceList]); - document.body.removeChild(inputEl); - }; - inputEl.click(); + showCreateResourceDialog({ + onConfirm: (resourceList) => { + editorStore.setResourceList([...editorState.resourceList, ...resourceList]); + }, + }); }; const handleFullscreenBtnClick = () => { @@ -536,7 +515,6 @@ const MemoEditor = () => {
- Uploading
diff --git a/web/src/components/ResourcesDialog.tsx b/web/src/components/ResourcesDialog.tsx index d4a91525..919b060c 100644 --- a/web/src/components/ResourcesDialog.tsx +++ b/web/src/components/ResourcesDialog.tsx @@ -11,6 +11,7 @@ import Dropdown from "./common/Dropdown"; import { generateDialog } from "./Dialog"; import { showCommonDialog } from "./Dialog/CommonDialog"; import showPreviewImageDialog from "./PreviewImageDialog"; +import showCreateResourceDialog from "./CreateResourceDialog"; import showChangeResourceFilenameDialog from "./ChangeResourceFilenameDialog"; import "../less/resources-dialog.less"; @@ -35,34 +36,6 @@ const ResourcesDialog: React.FC = (props: Props) => { }); }, []); - const handleUploadFileBtnClick = async () => { - const inputEl = document.createElement("input"); - inputEl.style.position = "fixed"; - inputEl.style.top = "-100vh"; - inputEl.style.left = "-100vw"; - document.body.appendChild(inputEl); - inputEl.type = "file"; - inputEl.multiple = true; - inputEl.accept = "*"; - inputEl.onchange = async () => { - if (!inputEl.files || inputEl.files.length === 0) { - return; - } - - for (const file of inputEl.files) { - try { - await resourceStore.createResourceWithBlob(file); - } catch (error: any) { - console.error(error); - toastHelper.error(error.response.data.message); - } - } - - document.body.removeChild(inputEl); - }; - inputEl.click(); - }; - const handlePreviewBtnClick = (resource: Resource) => { const resourceUrl = getResourceUrl(resource); if (resource.type.startsWith("image")) { @@ -139,7 +112,7 @@ const ResourcesDialog: React.FC = (props: Props) => {
-
diff --git a/web/src/types/modules/resource.d.ts b/web/src/types/modules/resource.d.ts index 2436bc76..0ce5f9d0 100644 --- a/web/src/types/modules/resource.d.ts +++ b/web/src/types/modules/resource.d.ts @@ -17,6 +17,7 @@ interface Resource { interface ResourceCreate { filename: string; externalLink: string; + type: string; } interface ResourcePatch {