diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index 370ccda6..8a0409de 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -13,6 +13,7 @@ import Selector from "./common/Selector"; import Editor, { EditorRefActions } from "./Editor/Editor"; import EmojiPicker from "./Editor/EmojiPicker"; import ResourceIcon from "./ResourceIcon"; +import showResourcesSelectorDialog from "./ResourcesSelectorDialog"; import "../less/memo-editor.less"; const getEditorContentCache = (): string => { @@ -34,7 +35,6 @@ const setEditingMemoVisibilityCache = (visibility: Visibility) => { interface State { fullscreen: boolean; isUploadingResource: boolean; - resourceList: Resource[]; shouldShowEmojiPicker: boolean; } @@ -48,7 +48,6 @@ const MemoEditor = () => { isUploadingResource: false, fullscreen: false, shouldShowEmojiPicker: false, - resourceList: [], }); const [allowSave, setAllowSave] = useState(false); const prevGlobalStateRef = useRef(editorState); @@ -79,13 +78,8 @@ const MemoEditor = () => { if (memo) { handleEditorFocus(); editorStateService.setMemoVisibility(memo.visibility); + editorStateService.setResourceList(memo.resourceList); editorRef.current?.setContent(memo.content ?? ""); - setState((state) => { - return { - ...state, - resourceList: memo.resourceList, - }; - }); } }); storage.set({ @@ -148,12 +142,7 @@ const MemoEditor = () => { } } } - setState((state) => { - return { - ...state, - resourceList: [...state.resourceList, ...resourceList], - }; - }); + editorStateService.setResourceList([...editorState.resourceList, ...resourceList]); } }; @@ -163,12 +152,7 @@ const MemoEditor = () => { const file = event.clipboardData.files[0]; const resource = await handleUploadResource(file); if (resource) { - setState((state) => { - return { - ...state, - resourceList: [...state.resourceList, resource], - }; - }); + editorStateService.setResourceList([...editorState.resourceList, resource]); } } }; @@ -216,7 +200,7 @@ const MemoEditor = () => { id: prevMemo.id, content, visibility: editorState.memoVisibility, - resourceIdList: state.resourceList.map((resource) => resource.id), + resourceIdList: editorState.resourceList.map((resource) => resource.id), }); } editorStateService.clearEditMemo(); @@ -224,7 +208,7 @@ const MemoEditor = () => { await memoService.createMemo({ content, visibility: editorState.memoVisibility, - resourceIdList: state.resourceList.map((resource) => resource.id), + resourceIdList: editorState.resourceList.map((resource) => resource.id), }); locationService.clearQuery(); } @@ -237,20 +221,17 @@ const MemoEditor = () => { return { ...state, fullscreen: false, - resourceList: [], }; }); + editorStateService.clearResourceList(); setEditorContentCache(""); storage.remove(["editingMemoVisibilityCache"]); editorRef.current?.setContent(""); }; const handleCancelEdit = () => { - setState({ - ...state, - resourceList: [], - }); editorStateService.clearEditMemo(); + editorStateService.clearResourceList(); editorRef.current?.setContent(""); setEditorContentCache(""); storage.remove(["editingMemoVisibilityCache"]); @@ -317,12 +298,7 @@ const MemoEditor = () => { } } } - setState((state) => { - return { - ...state, - resourceList: [...state.resourceList, ...resourceList], - }; - }); + editorStateService.setResourceList([...editorState.resourceList, ...resourceList]); document.body.removeChild(inputEl); }; inputEl.click(); @@ -361,13 +337,7 @@ const MemoEditor = () => { }; const handleDeleteResource = async (resourceId: ResourceId) => { - setState((state) => { - return { - ...state, - resourceList: state.resourceList.filter((resource) => resource.id !== resourceId), - }; - }); - + editorStateService.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId)); if (editorState.editMemoId) { await deleteMemoResource(editorState.editMemoId, resourceId); } @@ -440,10 +410,20 @@ const MemoEditor = () => { - +
+
+ + {t("editor.local")} +
+
+ + {t("editor.resources")} +
+
+ @@ -454,9 +434,9 @@ const MemoEditor = () => { /> - {state.resourceList.length > 0 && ( + {editorState.resourceList && editorState.resourceList.length > 0 && (
- {state.resourceList.map((resource) => { + {editorState.resourceList.map((resource) => { return (
diff --git a/web/src/components/ResourcesSelectorDialog.tsx b/web/src/components/ResourcesSelectorDialog.tsx new file mode 100644 index 00000000..f61b3ce0 --- /dev/null +++ b/web/src/components/ResourcesSelectorDialog.tsx @@ -0,0 +1,155 @@ +import { Checkbox, Tooltip } from "@mui/joy"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import useLoading from "../hooks/useLoading"; +import { editorStateService, resourceService } from "../services"; +import { useAppSelector } from "../store"; +import Icon from "./Icon"; +import toastHelper from "./Toast"; +import { generateDialog } from "./Dialog"; +import showPreviewImageDialog from "./PreviewImageDialog"; +import "../less/resources-selector-dialog.less"; + +type Props = DialogProps; + +interface State { + checkedArray: boolean[]; +} + +const ResourcesSelectorDialog: React.FC = (props: Props) => { + const { destroy } = props; + const { t } = useTranslation(); + const loadingState = useLoading(); + const { resources } = useAppSelector((state) => state.resource); + const editorState = useAppSelector((state) => state.editor); + const [state, setState] = useState({ + checkedArray: [], + }); + + useEffect(() => { + resourceService + .fetchResourceList() + .catch((error) => { + console.error(error); + toastHelper.error(error.response.data.message); + }) + .finally(() => { + loadingState.setFinish(); + }); + }, []); + + useEffect(() => { + const checkedResourceIdArray = editorState.resourceList.map((resource) => resource.id); + setState({ + checkedArray: resources.map((resource) => { + return checkedResourceIdArray.includes(resource.id); + }), + }); + }, [resources]); + + const getResourceUrl = useCallback((resource: Resource) => { + return `${window.location.origin}/o/r/${resource.id}/${resource.filename}`; + }, []); + + const handlePreviewBtnClick = (resource: Resource) => { + const resourceUrl = getResourceUrl(resource); + if (resource.type.startsWith("image")) { + showPreviewImageDialog( + resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)), + resources.findIndex((r) => r.id === resource.id) + ); + } else { + window.open(resourceUrl); + } + }; + + const handleCheckboxChange = (index: number) => { + const newCheckedArr = state.checkedArray; + newCheckedArr[index] = !newCheckedArr[index]; + setState({ + checkedArray: newCheckedArr, + }); + }; + + const handleConfirmBtnClick = () => { + const resourceList = resources.filter((_, index) => { + return state.checkedArray[index]; + }); + editorStateService.setResourceList(resourceList); + destroy(); + }; + + return ( + <> +
+

+ 🌄 + {t("sidebar.resources")} +

+ +
+
+ {loadingState.isLoading ? ( +
+

{t("resources.fetching-data")}

+
+ ) : ( +
+
+ ID + NAME + +
+ {resources.length === 0 ? ( +

{t("resources.no-resources")}

+ ) : ( + resources.map((resource, index) => ( +
+ {resource.id} + + {resource.filename} + +
+ handlePreviewBtnClick(resource)} + > + {t("resources.preview")} + + handleCheckboxChange(index)} /> +
+
+ )) + )} +
+ )} +
+ + {t("message.count-selected-resources")}: {state.checkedArray.filter((checked) => checked).length} + +
+
+ + {t("common.confirm")} +
+
+
+
+ + ); +}; + +export default function showResourcesSelectorDialog() { + generateDialog( + { + className: "resources-selector-dialog", + }, + ResourcesSelectorDialog, + {} + ); +} diff --git a/web/src/less/memo-editor.less b/web/src/less/memo-editor.less index ee259e1f..65ba6c70 100644 --- a/web/src/less/memo-editor.less +++ b/web/src/less/memo-editor.less @@ -67,6 +67,30 @@ } } + &.resource-btn{ + @apply relative ; + + &:hover { + > .resource-action-list { + @apply flex; + } + } + + >.resource-action-list{ + @apply hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-36 max-h-52 overflow-auto font-mono bg-zinc-100; + + >.resource-action-item{ + @apply w-full flex text-black cursor-pointer rounded text-sm leading-6 px-2 truncate hover:bg-zinc-300 shrink-0; + + >.icon-img{ + @apply w-4 mr-2; + } + + } + + } + } + > .icon-img { @apply w-5 h-5 mx-auto flex flex-row justify-center items-center; } diff --git a/web/src/less/resources-selector-dialog.less b/web/src/less/resources-selector-dialog.less new file mode 100644 index 00000000..afc4b225 --- /dev/null +++ b/web/src/less/resources-selector-dialog.less @@ -0,0 +1,75 @@ +.resources-selector-dialog { + @apply px-4; + + > .dialog-container { + @apply w-112 max-w-full mb-8; + + > .dialog-content-container { + @apply flex flex-col justify-start items-start w-full; + + > .action-buttons-container { + @apply w-full flex flex-row justify-between items-center mb-2; + + > .buttons-wrapper { + @apply flex flex-row justify-start items-center; + + > .upload-resource-btn { + @apply text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-blue-600 text-blue-600 bg-blue-50 hover:opacity-80; + + > .icon-img { + @apply w-4 h-auto mr-1; + } + } + + > .delete-unused-resource-btn { + @apply text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-red-600 text-red-600 bg-red-100 hover:opacity-80; + + > .icon-img { + @apply w-4 h-auto mr-1; + } + } + } + } + + > .loading-text-container { + @apply flex flex-col justify-center items-center w-full h-32; + } + + > .resource-table-container { + @apply flex flex-col justify-start items-start w-full; + + > .fields-container { + @apply px-2 py-2 w-full grid grid-cols-7 border-b; + + > .field-text { + @apply font-mono text-gray-400; + } + } + + > .tip-text { + @apply w-full text-center text-base my-6 mt-8; + } + + > .resource-container { + @apply px-2 py-2 w-full grid grid-cols-7; + + > .buttons-container { + @apply w-full flex flex-row justify-end items-center; + } + } + + .field-text { + @apply w-full truncate text-base pr-2 last:pr-0; + + &.id-text { + @apply col-span-2; + } + + &.name-text { + @apply col-span-4; + } + } + } + } + } +} diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 38e200e1..3f11e75f 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -88,7 +88,9 @@ "save": "Save", "placeholder": "Any thoughts...", "only-image-supported": "Only image file supported.", - "cant-empty": "Content can't be empty" + "cant-empty": "Content can't be empty", + "local": "Local", + "resources": "Resources" }, "memo": { "view-detail": "View Detail", diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index 4d32e439..d3fe83bc 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -88,7 +88,9 @@ "save": "Sauvegarder", "placeholder": "Une idée...", "only-image-supported": "Seul le fichier image est pris en charge.", - "cant-empty": "Le contenu ne doit pas être vide" + "cant-empty": "Le contenu ne doit pas être vide", + "local": "Local", + "resources": "Resources" }, "memo": { "view-detail": "Voir le détail", diff --git a/web/src/locales/vi.json b/web/src/locales/vi.json index 75c81403..35adecdb 100644 --- a/web/src/locales/vi.json +++ b/web/src/locales/vi.json @@ -88,7 +88,9 @@ "save": "Lưu", "placeholder": "Bất cứ gì bạn đang nghĩ...", "only-image-supported": "Chỉ hỗ trợ hình ảnh.", - "cant-empty": "Nội dung không thể trống" + "cant-empty": "Nội dung không thể trống", + "local": "Local", + "resources": "Resources" }, "memo": { "view-detail": "Xem chi tiết", diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index 46fb3dab..95495be6 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -88,7 +88,9 @@ "save": "记下", "placeholder": "现在的想法是...", "only-image-supported": "仅支持图片文件。", - "cant-empty": "内容不能为空" + "cant-empty": "内容不能为空", + "local": "本地上传", + "resources": "资源库" }, "memo": { "view-detail": "查看详情", diff --git a/web/src/services/editorStateService.ts b/web/src/services/editorStateService.ts index fe11a812..a20b0d2f 100644 --- a/web/src/services/editorStateService.ts +++ b/web/src/services/editorStateService.ts @@ -1,5 +1,5 @@ import store from "../store"; -import { setEditMemoId, setMemoVisibility } from "../store/modules/editor"; +import { setEditMemoId, setMemoVisibility, setResourceList } from "../store/modules/editor"; const editorStateService = { getState: () => { @@ -17,6 +17,14 @@ const editorStateService = { setMemoVisibility: (memoVisibility: Visibility) => { store.dispatch(setMemoVisibility(memoVisibility)); }, + + setResourceList: (resourceList: Resource[]) => { + store.dispatch(setResourceList(resourceList)); + }, + + clearResourceList: () => { + store.dispatch(setResourceList([])); + }, }; export default editorStateService; diff --git a/web/src/store/modules/editor.ts b/web/src/store/modules/editor.ts index e88f823a..996d5629 100644 --- a/web/src/store/modules/editor.ts +++ b/web/src/store/modules/editor.ts @@ -3,11 +3,15 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; interface State { editMemoId?: MemoId; memoVisibility: Visibility; + resourceList: Resource[]; } const editorSlice = createSlice({ name: "editor", - initialState: {} as State, + initialState: { + memoVisibility: "PRIVATE", + resourceList: [], + } as State, reducers: { setEditMemoId: (state, action: PayloadAction>) => { return { @@ -21,9 +25,15 @@ const editorSlice = createSlice({ memoVisibility: action.payload, }; }, + setResourceList: (state, action: PayloadAction) => { + return { + ...state, + resourceList: action.payload, + }; + }, }, }); -export const { setEditMemoId, setMemoVisibility } = editorSlice.actions; +export const { setEditMemoId, setMemoVisibility, setResourceList } = editorSlice.actions; export default editorSlice.reducer;