From dd8c10743d56b00f26f249ce74de54c702ff4c17 Mon Sep 17 00:00:00 2001 From: boojack Date: Tue, 30 May 2023 20:23:26 +0800 Subject: [PATCH] feat: memo editor dialog (#1772) * feat: memo editor dialog * chore: update mark * chore: update --- web/src/components/Header.tsx | 34 +++- web/src/components/Memo.tsx | 39 ++-- .../ActionButton/MemoVisibilitySelector.tsx | 26 +-- .../ActionButton/ResourceSelector.tsx | 41 ---- .../MemoEditor/MemoEditorDialog.tsx | 39 ++++ .../MemoEditor/RelationListView.tsx | 13 +- .../MemoEditor/ResourceListView.tsx | 17 +- web/src/components/MemoEditor/index.tsx | 190 ++++++++++-------- .../components/ResourcesSelectorDialog.tsx | 138 ------------- web/src/less/memo-editor.less | 4 - web/src/store/index.ts | 2 - web/src/store/module/editor.ts | 28 --- web/src/store/module/index.ts | 1 - web/src/store/reducer/editor.ts | 47 ----- 14 files changed, 215 insertions(+), 404 deletions(-) delete mode 100644 web/src/components/MemoEditor/ActionButton/ResourceSelector.tsx create mode 100644 web/src/components/MemoEditor/MemoEditorDialog.tsx delete mode 100644 web/src/components/ResourcesSelectorDialog.tsx delete mode 100644 web/src/store/module/editor.ts delete mode 100644 web/src/store/reducer/editor.ts diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 8171a8ce..5bfac2c5 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,17 +1,19 @@ import { useEffect } from "react"; import { NavLink, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { useLayoutStore, useUserStore } from "@/store/module"; +import { useGlobalStore, useLayoutStore, useUserStore } from "@/store/module"; import { resolution } from "@/utils/layout"; import Icon from "./Icon"; +import UserBanner from "./UserBanner"; import showSettingDialog from "./SettingDialog"; import showAskAIDialog from "./AskAIDialog"; import showArchivedMemoDialog from "./ArchivedMemoDialog"; import showAboutSiteDialog from "./AboutSiteDialog"; -import UserBanner from "./UserBanner"; +import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog"; const Header = () => { const { t } = useTranslation(); + const globalStore = useGlobalStore(); const location = useLocation(); const userStore = useUserStore(); const layoutStore = useLayoutStore(); @@ -57,7 +59,7 @@ const Header = () => { className={({ isActive }) => `${ isActive && "bg-white dark:bg-zinc-700 shadow" - } px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` + } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } > <> @@ -70,7 +72,7 @@ const Header = () => { className={({ isActive }) => `${ isActive && "bg-white dark:bg-zinc-700 shadow" - } px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` + } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } > <> @@ -83,7 +85,7 @@ const Header = () => { className={({ isActive }) => `${ isActive && "bg-white dark:bg-zinc-700 shadow" - } px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` + } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } > <> @@ -98,7 +100,7 @@ const Header = () => { className={({ isActive }) => `${ isActive && "bg-white dark:bg-zinc-700 shadow" - } px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` + } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } > <> @@ -109,25 +111,35 @@ const Header = () => { <> + {globalStore.isDev() && ( +
+ +
+ )} )} {isVisitorMode && ( @@ -138,7 +150,7 @@ const Header = () => { className={({ isActive }) => `${ isActive && "bg-white dark:bg-zinc-700 shadow" - } px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` + } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } > <> @@ -147,7 +159,7 @@ const Header = () => { + +
+ +
+ + ); +}; + +export default function showMemoEditorDialog(props: Pick = {}): void { + generateDialog( + { + className: "memo-editor-dialog", + dialogName: "memo-editor-dialog", + }, + MemoEditorDialog, + props + ); +} diff --git a/web/src/components/MemoEditor/RelationListView.tsx b/web/src/components/MemoEditor/RelationListView.tsx index 37b8be0c..b456e499 100644 --- a/web/src/components/MemoEditor/RelationListView.tsx +++ b/web/src/components/MemoEditor/RelationListView.tsx @@ -1,17 +1,20 @@ import { useEffect, useState } from "react"; -import { useEditorStore } from "@/store/module"; import { useMemoCacheStore } from "@/store/zustand"; import Icon from "../Icon"; +interface Props { + relationList: MemoRelation[]; + setRelationList: (relationList: MemoRelation[]) => void; +} + interface FormatedMemoRelation extends MemoRelation { relatedMemo: Memo; } -const RelationListView = () => { - const editorStore = useEditorStore(); +const RelationListView = (props: Props) => { + const { relationList, setRelationList } = props; const memoCacheStore = useMemoCacheStore(); const [formatedMemoRelationList, setFormatedMemoRelationList] = useState([]); - const relationList = editorStore.state.relationList; useEffect(() => { const fetchRelatedMemoList = async () => { @@ -30,7 +33,7 @@ const RelationListView = () => { const handleDeleteRelation = async (memoRelation: FormatedMemoRelation) => { const newRelationList = relationList.filter((relation) => relation.relatedMemoId !== memoRelation.relatedMemoId); - editorStore.setRelationList(newRelationList); + setRelationList(newRelationList); }; return ( diff --git a/web/src/components/MemoEditor/ResourceListView.tsx b/web/src/components/MemoEditor/ResourceListView.tsx index af79e839..48c07b40 100644 --- a/web/src/components/MemoEditor/ResourceListView.tsx +++ b/web/src/components/MemoEditor/ResourceListView.tsx @@ -1,20 +1,23 @@ -import { useEditorStore } from "@/store/module"; import Icon from "../Icon"; import ResourceIcon from "../ResourceIcon"; -const ResourceListView = () => { - const editorStore = useEditorStore(); - const editorState = editorStore.state; +interface Props { + resourceList: Resource[]; + setResourceList: (resourceList: Resource[]) => void; +} + +const ResourceListView = (props: Props) => { + const { resourceList, setResourceList } = props; const handleDeleteResource = async (resourceId: ResourceId) => { - editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId)); + setResourceList(resourceList.filter((resource) => resource.id !== resourceId)); }; return ( <> - {editorState.resourceList && editorState.resourceList.length > 0 && ( + {resourceList.length > 0 && (
- {editorState.resourceList.map((resource) => { + {resourceList.map((resource) => { return (
{ return getContentQueryParam() ?? storage.get(["editorContentCache"]).editorContentCache ?? ""; }; -const setEditorContentCache = (content: string) => { - storage.set({ - editorContentCache: content, - }); -}; +interface Props { + className?: string; + memoId?: MemoId; + relationList?: MemoRelation[]; + onConfirm?: () => void; +} interface State { + memoVisibility: Visibility; + resourceList: Resource[]; + relationList: MemoRelation[]; fullscreen: boolean; isUploadingResource: boolean; isRequesting: boolean; } -const MemoEditor = () => { +const MemoEditor = (props: Props) => { + const { className, memoId, onConfirm } = props; const { t, i18n } = useTranslation(); + const { + state: { systemStatus }, + } = useGlobalStore(); const userStore = useUserStore(); - const editorStore = useEditorStore(); const filterStore = useFilterStore(); const memoStore = useMemoStore(); const tagStore = useTagStore(); const resourceStore = useResourceStore(); const [state, setState] = useState({ + memoVisibility: "PRIVATE", + resourceList: [], + relationList: props.relationList ?? [], fullscreen: false, isUploadingResource: false, isRequesting: false, }); - const [allowSave, setAllowSave] = useState(false); + const [hasContent, setHasContent] = useState(false); const [isInIME, setIsInIME] = useState(false); - const editorState = editorStore.state; - const prevEditorStateRef = useRef(editorState); const editorRef = useRef(null); const user = userStore.state.user as User; const setting = user.setting; useEffect(() => { - const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]); - if (editingMemoIdCache) { - editorStore.setEditMemoWithId(editingMemoIdCache); - } else { - editorStore.setMemoVisibility(setting.memoVisibility); + let visibility = setting.memoVisibility; + if (systemStatus.disablePublicMemos && visibility === "PUBLIC") { + visibility = "PRIVATE"; } - }, []); + setState((prevState) => ({ + ...prevState, + memoVisibility: visibility, + })); + }, [setting.memoVisibility, systemStatus.disablePublicMemos]); useEffect(() => { - if (editorState.editMemoId) { - memoStore.getMemoById(editorState.editMemoId ?? UNKNOWN_ID).then((memo) => { + if (memoId) { + memoStore.getMemoById(memoId ?? UNKNOWN_ID).then((memo) => { if (memo) { handleEditorFocus(); - editorStore.setMemoVisibility(memo.visibility); - editorStore.setResourceList(memo.resourceList); - editorStore.setRelationList(memo.relationList); + setState((prevState) => ({ + ...prevState, + memoVisibility: memo.visibility, + resourceList: memo.resourceList, + relationList: memo.relationList, + })); editorRef.current?.setContent(memo.content ?? ""); } }); - storage.set({ - editingMemoIdCache: editorState.editMemoId, - }); - } else { - storage.remove(["editingMemoIdCache"]); } - - prevEditorStateRef.current = editorState; - }, [editorState.editMemoId]); - - useEffect(() => { - handleEditorFocus(); - }, [editorStore.state.relationList]); + }, [memoId]); const handleKeyDown = (event: React.KeyboardEvent) => { if (!editorRef.current) { @@ -159,6 +161,38 @@ const MemoEditor = () => { } }; + const handleMemoVisibilityChange = (visibility: Visibility) => { + setState((prevState) => ({ + ...prevState, + memoVisibility: visibility, + })); + }; + + const handleUploadFileBtnClick = () => { + showCreateResourceDialog({ + onConfirm: (resourceList) => { + setState((prevState) => ({ + ...prevState, + resourceList: [...prevState.resourceList, ...resourceList], + })); + }, + }); + }; + + const handleSetResourceList = (resourceList: Resource[]) => { + setState((prevState) => ({ + ...prevState, + resourceList, + })); + }; + + const handleSetRelationList = (relationList: MemoRelation[]) => { + setState((prevState) => ({ + ...prevState, + relationList, + })); + }; + const handleUploadResource = async (file: File) => { setState((state) => { return { @@ -190,14 +224,16 @@ const MemoEditor = () => { const resource = await handleUploadResource(file); if (resource) { uploadedResourceList.push(resource); - if (editorState.editMemoId) { - await upsertMemoResource(editorState.editMemoId, resource.id); + if (memoId) { + await upsertMemoResource(memoId, resource.id); } } } if (uploadedResourceList.length > 0) { - const resourceList = editorStore.getState().resourceList; - editorStore.setResourceList([...resourceList, ...uploadedResourceList]); + setState((prevState) => ({ + ...prevState, + resourceList: [...prevState.resourceList, ...uploadedResourceList], + })); } }; @@ -215,6 +251,10 @@ const MemoEditor = () => { } }; + const handleContentChange = (content: string) => { + setHasContent(content !== ""); + }; + const handleSaveBtnClick = async () => { if (state.isRequesting) { return; @@ -228,26 +268,24 @@ const MemoEditor = () => { }); const content = editorRef.current?.getContent() ?? ""; try { - const { editMemoId } = editorStore.getState(); - if (editMemoId && editMemoId !== UNKNOWN_ID) { - const prevMemo = await memoStore.getMemoById(editMemoId ?? UNKNOWN_ID); + if (memoId && memoId !== UNKNOWN_ID) { + const prevMemo = await memoStore.getMemoById(memoId ?? UNKNOWN_ID); if (prevMemo) { await memoStore.patchMemo({ id: prevMemo.id, content, - visibility: editorState.memoVisibility, - resourceIdList: editorState.resourceList.map((resource) => resource.id), - relationList: editorState.relationList, + visibility: state.memoVisibility, + resourceIdList: state.resourceList.map((resource) => resource.id), + relationList: state.relationList, }); } - editorStore.clearEditMemo(); } else { await memoStore.createMemo({ content, - visibility: editorState.memoVisibility, - resourceIdList: editorState.resourceList.map((resource) => resource.id), - relationList: editorState.relationList, + visibility: state.memoVisibility, + resourceIdList: state.resourceList.map((resource) => resource.id), + relationList: state.relationList, }); filterStore.clearFilter(); } @@ -275,28 +313,18 @@ const MemoEditor = () => { fullscreen: false, }; }); - editorStore.setResourceList([]); - editorStore.setRelationList([]); - setEditorContentCache(""); + setState((prevState) => ({ + ...prevState, + resourceList: [], + relationList: [], + })); editorRef.current?.setContent(""); clearContentQueryParam(); - }; - - const handleCancelEdit = () => { - if (editorState.editMemoId) { - editorStore.clearEditMemo(); - editorStore.setResourceList([]); - editorStore.setRelationList([]); - editorRef.current?.setContent(""); - setEditorContentCache(""); + if (onConfirm) { + onConfirm(); } }; - const handleContentChange = (content: string) => { - setAllowSave(content !== ""); - setEditorContentCache(content); - }; - const handleCheckBoxBtnClick = () => { if (!editorRef.current) { return; @@ -344,12 +372,6 @@ const MemoEditor = () => { editorRef.current?.focus(); }; - const handleEditorBlur = () => { - // do nothing - }; - - const isEditing = Boolean(editorState.editMemoId && editorState.editMemoId !== UNKNOWN_ID); - const editorConfig = useMemo( () => ({ className: `memo-editor`, @@ -362,14 +384,15 @@ const MemoEditor = () => { [state.fullscreen, i18n.language] ); + const allowSave = (hasContent || state.resourceList.length > 0) && !state.isUploadingResource && !state.isRequesting; + return (
setIsInIME(true)} onCompositionEnd={() => setIsInIME(false)} > @@ -383,25 +406,20 @@ const MemoEditor = () => { - +
- - + +
- +
- -
diff --git a/web/src/components/ResourcesSelectorDialog.tsx b/web/src/components/ResourcesSelectorDialog.tsx deleted file mode 100644 index 5a0b82bf..00000000 --- a/web/src/components/ResourcesSelectorDialog.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { Button, Checkbox } from "@mui/joy"; -import { useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; -import useLoading from "@/hooks/useLoading"; -import { useEditorStore, useResourceStore } from "@/store/module"; -import { getResourceUrl } from "@/utils/resource"; -import Icon from "./Icon"; -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 editorStore = useEditorStore(); - const resourceStore = useResourceStore(); - const resources = resourceStore.state.resources; - const [state, setState] = useState({ - checkedArray: [], - }); - - useEffect(() => { - resourceStore - .fetchResourceList() - .catch((error) => { - console.error(error); - toast.error(error.response.data.message); - }) - .finally(() => { - loadingState.setFinish(); - }); - }, []); - - useEffect(() => { - const checkedResourceIdArray = editorStore.state.resourceList.map((resource) => resource.id); - setState({ - checkedArray: resources.map((resource) => { - return checkedResourceIdArray.includes(resource.id); - }), - }); - }, [resources]); - - 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]; - }); - editorStore.setResourceList(resourceList); - destroy(); - }; - - return ( - <> -
-

{t("common.resources")}

- -
-
- {loadingState.isLoading ? ( -
-

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

-
- ) : ( -
-
- {t("common.name")} - Type - -
- {resources.length === 0 ? ( -

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

- ) : ( - resources.map((resource, index) => ( -
- handlePreviewBtnClick(resource)}> - {resource.filename} - - {resource.type} -
- handleCheckboxChange(index)} /> -
-
- )) - )} -
- )} -
- - {t("message.count-selected-resources")}: {state.checkedArray.filter((checked) => checked).length} - -
- -
-
-
- - ); -}; - -export default function showResourcesSelectorDialog() { - generateDialog( - { - className: "resources-selector-dialog", - dialogName: "resources-selector-dialog", - }, - ResourcesSelectorDialog, - {} - ); -} diff --git a/web/src/less/memo-editor.less b/web/src/less/memo-editor.less index ac378e9a..596d5bbe 100644 --- a/web/src/less/memo-editor.less +++ b/web/src/less/memo-editor.less @@ -22,10 +22,6 @@ } } - &.edit-ing { - @apply border-blue-500; - } - > .memo-editor { @apply mt-4 flex flex-col justify-start items-start relative w-full h-auto bg-inherit dark:text-gray-200; } diff --git a/web/src/store/index.ts b/web/src/store/index.ts index a661cb2f..c0645b6f 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -3,7 +3,6 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import globalReducer from "./reducer/global"; import userReducer from "./reducer/user"; import memoReducer from "./reducer/memo"; -import editorReducer from "./reducer/editor"; import shortcutReducer from "./reducer/shortcut"; import filterReducer from "./reducer/filter"; import resourceReducer from "./reducer/resource"; @@ -17,7 +16,6 @@ const store = configureStore({ user: userReducer, memo: memoReducer, tag: tagReducer, - editor: editorReducer, shortcut: shortcutReducer, filter: filterReducer, resource: resourceReducer, diff --git a/web/src/store/module/editor.ts b/web/src/store/module/editor.ts deleted file mode 100644 index 2528842f..00000000 --- a/web/src/store/module/editor.ts +++ /dev/null @@ -1,28 +0,0 @@ -import store, { useAppSelector } from ".."; -import { setEditMemoId, setMemoVisibility, setRelationList, setResourceList } from "../reducer/editor"; - -export const useEditorStore = () => { - const state = useAppSelector((state) => state.editor); - - return { - state, - getState: () => { - return store.getState().editor; - }, - setEditMemoWithId: (editMemoId: MemoId) => { - store.dispatch(setEditMemoId(editMemoId)); - }, - clearEditMemo: () => { - store.dispatch(setEditMemoId()); - }, - setMemoVisibility: (memoVisibility: Visibility) => { - store.dispatch(setMemoVisibility(memoVisibility)); - }, - setResourceList: (resourceList: Resource[]) => { - store.dispatch(setResourceList(resourceList)); - }, - setRelationList: (relationList: MemoRelation[]) => { - store.dispatch(setRelationList(relationList)); - }, - }; -}; diff --git a/web/src/store/module/index.ts b/web/src/store/module/index.ts index 63178bc2..6510f116 100644 --- a/web/src/store/module/index.ts +++ b/web/src/store/module/index.ts @@ -1,4 +1,3 @@ -export * from "./editor"; export * from "./global"; export * from "./filter"; export * from "./memo"; diff --git a/web/src/store/reducer/editor.ts b/web/src/store/reducer/editor.ts deleted file mode 100644 index a833338a..00000000 --- a/web/src/store/reducer/editor.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -interface State { - memoVisibility: Visibility; - resourceList: Resource[]; - relationList: MemoRelation[]; - editMemoId?: MemoId; -} - -const editorSlice = createSlice({ - name: "editor", - initialState: { - memoVisibility: "PRIVATE", - resourceList: [], - relationList: [], - } as State, - reducers: { - setEditMemoId: (state, action: PayloadAction>) => { - return { - ...state, - editMemoId: action.payload, - }; - }, - setMemoVisibility: (state, action: PayloadAction) => { - return { - ...state, - memoVisibility: action.payload, - }; - }, - setResourceList: (state, action: PayloadAction) => { - return { - ...state, - resourceList: action.payload, - }; - }, - setRelationList: (state, action: PayloadAction) => { - return { - ...state, - relationList: action.payload, - }; - }, - }, -}); - -export const { setEditMemoId, setMemoVisibility, setResourceList, setRelationList } = editorSlice.actions; - -export default editorSlice.reducer;