diff --git a/server/memo.go b/server/memo.go index ec0dc19e..8f098333 100644 --- a/server/memo.go +++ b/server/memo.go @@ -160,8 +160,17 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err) } + memo, err = s.Store.ComposeMemo(ctx, memo) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err) + } - for _, resourceID := range memoPatch.ResourceIDList { + resourceIDList := make([]int, 0) + for _, resource := range memo.ResourceList { + resourceIDList = append(resourceIDList, resource.ID) + } + addedResourceIDList, removedResourceIDList := getIDListDiff(resourceIDList, memoPatch.ResourceIDList) + for _, resourceID := range addedResourceIDList { if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{ MemoID: memo.ID, ResourceID: resourceID, @@ -169,19 +178,47 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err) } } + for _, resourceID := range removedResourceIDList { + if err := s.Store.DeleteMemoResource(ctx, &api.MemoResourceDelete{ + MemoID: &memo.ID, + ResourceID: &resourceID, + }); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo resource").SetInternal(err) + } + } if s.Profile.IsDev() { + patchMemoRelationList := make([]*api.MemoRelation, 0) for _, memoRelationUpsert := range memoPatch.RelationList { - if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{ + patchMemoRelationList = append(patchMemoRelationList, &api.MemoRelation{ MemoID: memo.ID, RelatedMemoID: memoRelationUpsert.RelatedMemoID, - Type: store.MemoRelationType(memoRelationUpsert.Type), + Type: memoRelationUpsert.Type, + }) + } + addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memo.RelationList, patchMemoRelationList) + for _, memoRelation := range addedMemoRelationList { + if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{ + MemoID: memo.ID, + RelatedMemoID: memoRelation.RelatedMemoID, + Type: store.MemoRelationType(memoRelation.Type), }); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err) } } + for _, memoRelation := range removedMemoRelationList { + memoRelationType := store.MemoRelationType(memoRelation.Type) + if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelationMessage{ + MemoID: &memo.ID, + RelatedMemoID: &memoRelation.RelatedMemoID, + Type: &memoRelationType, + }); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err) + } + } } + // After patching memo resources and relations, we need to re-compose it to get the latest data. memo, err = s.Store.ComposeMemo(ctx, memo) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err) @@ -452,3 +489,49 @@ func (s *Server) createMemoCreateActivity(c echo.Context, memo *api.Memo) error } return err } + +func getIDListDiff(oldList, newList []int) (addedList, removedList []int) { + oldMap := map[int]bool{} + for _, id := range oldList { + oldMap[id] = true + } + newMap := map[int]bool{} + for _, id := range newList { + newMap[id] = true + } + for id := range oldMap { + if !newMap[id] { + removedList = append(removedList, id) + } + } + for id := range newMap { + if !oldMap[id] { + addedList = append(addedList, id) + } + } + return addedList, removedList +} + +func getMemoRelationListDiff(oldList, newList []*api.MemoRelation) (addedList, removedList []*api.MemoRelation) { + oldMap := map[string]bool{} + for _, relation := range oldList { + oldMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true + } + newMap := map[string]bool{} + for _, relation := range newList { + newMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true + } + for _, relation := range oldList { + key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type) + if !newMap[key] { + removedList = append(removedList, relation) + } + } + for _, relation := range newList { + key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type) + if !oldMap[key] { + addedList = append(addedList, relation) + } + } + return addedList, removedList +} diff --git a/store/memo.go b/store/memo.go index 65ff91f2..71bbfb02 100644 --- a/store/memo.go +++ b/store/memo.go @@ -40,9 +40,11 @@ func (raw *memoRaw) toMemo() *api.Memo { UpdatedTs: raw.UpdatedTs, // Domain specific fields - Content: raw.Content, - Visibility: raw.Visibility, - Pinned: raw.Pinned, + Content: raw.Content, + Visibility: raw.Visibility, + Pinned: raw.Pinned, + ResourceList: []*api.Resource{}, + RelationList: []*api.MemoRelation{}, } } diff --git a/store/memo_relation.go b/store/memo_relation.go index 06cd6397..7243bc89 100644 --- a/store/memo_relation.go +++ b/store/memo_relation.go @@ -18,6 +18,7 @@ func (s *Store) ComposeMemoRelationList(ctx context.Context, memo *api.Memo) err return err } + memo.RelationList = []*api.MemoRelation{} for _, memoRelation := range memoRelationList { memo.RelationList = append(memo.RelationList, &api.MemoRelation{ MemoID: memoRelation.MemoID, diff --git a/web/src/components/AskAIDialog.tsx b/web/src/components/AskAIDialog.tsx index 0c4ab74c..d6e7217f 100644 --- a/web/src/components/AskAIDialog.tsx +++ b/web/src/components/AskAIDialog.tsx @@ -98,7 +98,7 @@ const AskAIDialog: React.FC = (props: Props) => { ) : (
- +
{marked(message.content)}
diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index f4ff0031..8ec27f03 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -32,7 +32,7 @@ const Header = () => { return (
diff --git a/web/src/components/HomeSidebar.tsx b/web/src/components/HomeSidebar.tsx index b6b88b82..66af855b 100644 --- a/web/src/components/HomeSidebar.tsx +++ b/web/src/components/HomeSidebar.tsx @@ -42,7 +42,7 @@ const HomeSidebar = () => { return (
diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 395615d9..d65ec82a 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -1,9 +1,12 @@ -import { getRelativeTimeString } from "@/helpers/datetime"; +import { isEqual, uniqWith } from "lodash-es"; import { memo, useEffect, useRef, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { useEditorStore, useFilterStore, useMemoStore, useUserStore } from "@/store/module"; +import { getRelativeTimeString } from "@/helpers/datetime"; +import { UNKNOWN_ID } from "@/helpers/consts"; +import { useMemoCacheStore } from "@/store/zustand"; import Tooltip from "./kit/Tooltip"; import Divider from "./kit/Divider"; import { showCommonDialog } from "./Dialog/CommonDialog"; @@ -13,24 +16,34 @@ import MemoResources from "./MemoResources"; import showShareMemo from "./ShareMemoDialog"; import showPreviewImageDialog from "./PreviewImageDialog"; import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; +import MemoRelationListView from "./MemoRelationListView"; import "@/less/memo.less"; interface Props { memo: Memo; readonly?: boolean; + showRelatedMemos?: boolean; } const Memo: React.FC = (props: Props) => { - const { memo, readonly } = props; + const { memo, readonly, showRelatedMemos } = props; const { t, i18n } = useTranslation(); const editorStore = useEditorStore(); const filterStore = useFilterStore(); const userStore = useUserStore(); const memoStore = useMemoStore(); + const memoCacheStore = useMemoCacheStore(); const [createdTimeStr, setCreatedTimeStr] = useState(getRelativeTimeString(memo.createdTs)); + const [relatedMemoList, setRelatedMemoList] = useState([]); const memoContainerRef = useRef(null); const isVisitorMode = userStore.isVisitorMode() || readonly; + useEffect(() => { + Promise.all(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then((memoList) => { + setRelatedMemoList(uniqWith(memoList, isEqual)); + }); + }, [memo.relationList]); + useEffect(() => { let intervalFlag: any = -1; if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) { @@ -178,71 +191,106 @@ const Memo: React.FC = (props: Props) => { } }; + const handleMarkMemo = () => { + const relation: MemoRelation = { + memoId: UNKNOWN_ID, + relatedMemoId: memo.id, + type: "REFERENCE", + }; + editorStore.setRelationList(uniqWith([...editorStore.state.relationList, relation], isEqual)); + }; + return ( -
-
-
- - {createdTimeStr} - - {isVisitorMode && ( - - @{memo.creatorName} + <> +
0 && "pinned"} ${memo.pinned ? "pinned" : ""}`} + ref={memoContainerRef} + > +
+
+ + {createdTimeStr} - )} -
- {!isVisitorMode && ( -
- {memo.visibility !== "PRIVATE" && ( - -
handleMemoVisibilityClick(memo.visibility)}> - {memo.visibility === "PUBLIC" ? ( - - ) : ( - - )} -
-
+ {isVisitorMode && ( + + @{memo.creatorName} + )} - {memo.pinned && } - - - -
-
- - {memo.pinned ? : } - {memo.pinned ? t("common.unpin") : t("common.pin")} - - - - {t("common.edit")} - - - - {t("common.share")} - - - - - {t("common.archive")} - - - - {t("common.delete")} - +
+ {!isVisitorMode && ( +
+ {memo.visibility !== "PRIVATE" && ( + +
handleMemoVisibilityClick(memo.visibility)}> + {memo.visibility === "PUBLIC" ? ( + + ) : ( + + )} +
+
+ )} + {memo.pinned && } + + + +
+
+ + {memo.pinned ? : } + {memo.pinned ? t("common.unpin") : t("common.pin")} + + + + {t("common.edit")} + + + + {t("common.share")} + + + + Mark + + + + + {t("common.archive")} + + + + {t("common.delete")} + +
-
- )} + )} +
+ + + {!showRelatedMemos && }
- - -
+ + {showRelatedMemos && relatedMemoList.length > 0 && ( + <> +

+ + Related memos +

+ {relatedMemoList.map((relatedMemo) => { + return ( +
+ +
+ ); + })} + + )} + ); }; diff --git a/web/src/components/MemoEditor/RelationListView.tsx b/web/src/components/MemoEditor/RelationListView.tsx new file mode 100644 index 00000000..03ec5948 --- /dev/null +++ b/web/src/components/MemoEditor/RelationListView.tsx @@ -0,0 +1,60 @@ +import { useEffect, useState } from "react"; +import { useEditorStore } from "@/store/module"; +import { useMemoCacheStore } from "@/store/zustand"; +import Icon from "../Icon"; + +interface FormatedMemoRelation extends MemoRelation { + relatedMemo: Memo; +} + +const RelationListView = () => { + const editorStore = useEditorStore(); + const memoCacheStore = useMemoCacheStore(); + const [formatedMemoRelationList, setFormatedMemoRelationList] = useState([]); + const relationList = editorStore.state.relationList; + + useEffect(() => { + const fetchRelatedMemoList = async () => { + const requests = relationList.map(async (relation) => { + const relatedMemo = await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId); + return { + ...relation, + relatedMemo, + }; + }); + const list = await Promise.all(requests); + setFormatedMemoRelationList(list); + }; + fetchRelatedMemoList(); + }, [relationList]); + + const handleDeleteRelation = async (memoRelation: FormatedMemoRelation) => { + const newRelationList = relationList.filter((relation) => relation.relatedMemoId !== memoRelation.relatedMemoId); + editorStore.setRelationList(newRelationList); + }; + + return ( + <> + {formatedMemoRelationList.length > 0 && ( +
+ {formatedMemoRelationList.map((memoRelation) => { + return ( +
+ + + {memoRelation.relatedMemo.content} + + handleDeleteRelation(memoRelation)} /> +
+ ); + })} +
+ )} + + ); +}; + +export default RelationListView; diff --git a/web/src/components/MemoEditor/ResourceListView.tsx b/web/src/components/MemoEditor/ResourceListView.tsx index 5b9002a7..6bc9afa9 100644 --- a/web/src/components/MemoEditor/ResourceListView.tsx +++ b/web/src/components/MemoEditor/ResourceListView.tsx @@ -1,5 +1,4 @@ import { useEditorStore } from "@/store/module"; -import { deleteMemoResource } from "@/helpers/api"; import Icon from "../Icon"; import ResourceIcon from "../ResourceIcon"; @@ -9,21 +8,21 @@ const ResourceListView = () => { const handleDeleteResource = async (resourceId: ResourceId) => { editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId)); - if (editorState.editMemoId) { - await deleteMemoResource(editorState.editMemoId, resourceId); - } }; return ( <> {editorState.resourceList && editorState.resourceList.length > 0 && ( -
+
{editorState.resourceList.map((resource) => { return ( -
- - {resource.filename} - handleDeleteResource(resource.id)} /> +
+ + {resource.filename} + handleDeleteResource(resource.id)} />
); })} @@ -32,4 +31,5 @@ const ResourceListView = () => { ); }; + export default ResourceListView; diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 58eddf2e..7944d7ef 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -13,8 +13,9 @@ import Editor, { EditorRefActions } from "./Editor"; import TagSelector from "./ActionButton/TagSelector"; import ResourceSelector from "./ActionButton/ResourceSelector"; import MemoVisibilitySelector from "./ActionButton/MemoVisibilitySelector"; -import "@/less/memo-editor.less"; import ResourceListView from "./ResourceListView"; +import RelationListView from "./RelationListView"; +import "@/less/memo-editor.less"; const listItemSymbolList = ["- [ ] ", "- [x] ", "- [X] ", "* ", "- "]; const emptyOlReg = /^(\d+)\. $/; @@ -73,6 +74,7 @@ const MemoEditor = () => { handleEditorFocus(); editorStore.setMemoVisibility(memo.visibility); editorStore.setResourceList(memo.resourceList); + editorStore.setRelationList(memo.relationList); editorRef.current?.setContent(memo.content ?? ""); } }); @@ -232,6 +234,7 @@ const MemoEditor = () => { content, visibility: editorState.memoVisibility, resourceIdList: editorState.resourceList.map((resource) => resource.id), + relationList: editorState.relationList, }); } editorStore.clearEditMemo(); @@ -240,7 +243,7 @@ const MemoEditor = () => { content, visibility: editorState.memoVisibility, resourceIdList: editorState.resourceList.map((resource) => resource.id), - relationList: [], + relationList: editorState.relationList, }); filterStore.clearFilter(); } @@ -268,7 +271,8 @@ const MemoEditor = () => { fullscreen: false, }; }); - editorStore.clearResourceList(); + editorStore.setResourceList([]); + editorStore.setRelationList([]); setEditorContentCache(""); editorRef.current?.setContent(""); clearContentQueryParam(); @@ -277,7 +281,8 @@ const MemoEditor = () => { const handleCancelEdit = () => { if (editorState.editMemoId) { editorStore.clearEditMemo(); - editorStore.clearResourceList(); + editorStore.setResourceList([]); + editorStore.setRelationList([]); editorRef.current?.setContent(""); setEditorContentCache(""); } @@ -381,6 +386,7 @@ const MemoEditor = () => {
+
diff --git a/web/src/components/MemoRelationListView.tsx b/web/src/components/MemoRelationListView.tsx new file mode 100644 index 00000000..ff0fbecf --- /dev/null +++ b/web/src/components/MemoRelationListView.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import { useMemoCacheStore } from "@/store/zustand"; +import Icon from "./Icon"; + +interface Props { + relationList: MemoRelation[]; +} + +const MemoRelationListView = (props: Props) => { + const memoCacheStore = useMemoCacheStore(); + const [relatedMemoList, setRelatedMemoList] = useState([]); + const relationList = props.relationList; + + useEffect(() => { + const fetchRelatedMemoList = async () => { + const requests = relationList.map((relation) => memoCacheStore.getOrFetchMemoById(relation.relatedMemoId)); + const memoList = await Promise.all(requests); + setRelatedMemoList(memoList); + }; + fetchRelatedMemoList(); + }, [relationList]); + + return ( + <> + {relatedMemoList.length > 0 && ( +
+ {relatedMemoList.map((memo) => { + return ( +
+
+ +
+ {memo.content} +
+ ); + })} +
+ )} + + ); +}; + +export default MemoRelationListView; diff --git a/web/src/layouts/Root.tsx b/web/src/layouts/Root.tsx index 76b3b12a..fd27ddfd 100644 --- a/web/src/layouts/Root.tsx +++ b/web/src/layouts/Root.tsx @@ -10,7 +10,7 @@ function Root() {
-
+
diff --git a/web/src/less/create-shortcut-dialog.less b/web/src/less/create-shortcut-dialog.less index 68c019db..bcb512f4 100644 --- a/web/src/less/create-shortcut-dialog.less +++ b/web/src/less/create-shortcut-dialog.less @@ -11,7 +11,7 @@ @apply w-full mt-2 py-1 flex sm:flex-row flex-col justify-start items-start; > .normal-text { - @apply block flex-shrink-0 w-12 mr-3 sm:text-right text-left text-sm leading-8; + @apply block shrink-0 w-12 mr-3 sm:text-right text-left text-sm leading-8; color: gray; } diff --git a/web/src/less/memo-detail.less b/web/src/less/memo-detail.less deleted file mode 100644 index 05ea3f8e..00000000 --- a/web/src/less/memo-detail.less +++ /dev/null @@ -1,65 +0,0 @@ -.page-wrapper.memo-detail { - @apply relative top-0 w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800; - - > .page-container { - @apply relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8; - - > .page-header { - @apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 pt-6 mb-2 bg-zinc-100 dark:bg-zinc-800; - - > .title-container { - @apply flex flex-row justify-start items-center; - - > .logo-img { - @apply h-12 sm:h-14 w-auto mr-2; - } - - > .logo-text { - @apply text-4xl tracking-wide text-black dark:text-white; - } - - > .title-text { - @apply text-xl sm:text-3xl font-mono text-gray-700; - } - } - - > .action-button-container { - > .btn { - @apply block text-gray-600 dark:text-gray-300 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline; - - > .icon { - @apply text-lg; - } - } - } - } - - > .memos-wrapper { - @apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4; - - > .memo-container { - @apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-700; - - > .memo-header { - @apply mb-2 w-full flex flex-row justify-between items-center; - - > .status-container { - @apply flex flex-row justify-start items-center text-sm text-gray-400; - - > .name-text { - @apply ml-2 hover:text-green-600 hover:underline; - } - } - } - - > .memo-content { - @apply cursor-default; - - > * { - @apply cursor-default; - } - } - } - } - } -} diff --git a/web/src/less/memo-editor.less b/web/src/less/memo-editor.less index d0f72cc8..ac378e9a 100644 --- a/web/src/less/memo-editor.less +++ b/web/src/less/memo-editor.less @@ -50,26 +50,6 @@ } } - > .resource-list-wrapper { - @apply w-full flex flex-row justify-start flex-wrap; - - > .resource-container { - @apply max-w-full mt-1 mr-1 flex flex-row justify-start items-center flex-nowrap bg-gray-100 px-2 py-1 rounded cursor-pointer hover:bg-gray-200; - - > .icon-img { - @apply w-4 h-auto mr-1 text-gray-500; - } - - > .name-text { - @apply text-gray-500 text-sm max-w-xs truncate font-mono; - } - - > .close-icon { - @apply w-4 h-auto ml-1 text-gray-500 hover:text-gray-800; - } - } - } - > .editor-footer-container { @apply w-full flex flex-row justify-between items-center border-t border-t-gray-100 dark:border-t-zinc-500 py-3 mt-2; diff --git a/web/src/less/memo.less b/web/src/less/memo.less index 0da21200..62dfdefd 100644 --- a/web/src/less/memo.less +++ b/web/src/less/memo.less @@ -41,7 +41,7 @@ } > .btns-container { - @apply flex flex-row justify-end items-center relative flex-shrink-0; + @apply flex flex-row justify-end items-center relative shrink-0; > .more-action-btns-wrapper { @apply hidden flex-col justify-start items-center absolute top-2 -right-4 flex-nowrap hover:flex p-3; diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index 2dd55a5c..f1e6ae99 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -140,7 +140,7 @@ const Auth = () => {
@@ -156,7 +156,7 @@ const Auth = () => {
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index c96bb336..d0559d86 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -32,7 +32,7 @@ function Home() { return (
-
+
{!userStore.isVisitorMode() && } diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 7babc144..8d0a797e 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -5,10 +5,7 @@ import { Link, useLocation, useParams } from "react-router-dom"; import { UNKNOWN_ID } from "@/helpers/consts"; import { useGlobalStore, useMemoStore, useUserStore } from "@/store/module"; import useLoading from "@/hooks/useLoading"; -import MemoContent from "@/components/MemoContent"; -import MemoResources from "@/components/MemoResources"; -import { getDateTimeString } from "@/helpers/datetime"; -import "@/less/memo-detail.less"; +import Memo from "@/components/Memo"; interface State { memo: Memo; @@ -49,23 +46,29 @@ const MemoDetail = () => { }, [location]); return ( -
-
-
-
+
+
+
+
-

{customizedProfile.name}

+

{customizedProfile.name}

{!loadingState.isLoading && ( <> {user ? ( - - 🏠 {t("router.back-to-home")} + + 🏠 {t("router.back-to-home")} ) : ( - - 👉 {t("common.sign-in")} + + 👉 {t("common.sign-in")} )} @@ -73,19 +76,8 @@ const MemoDetail = () => {
{!loadingState.isLoading && ( -
-
-
-
- {getDateTimeString(state.memo.createdTs)} - - @{state.memo.creatorName} - -
-
- undefined} /> - -
+
+
)}
diff --git a/web/src/store/module/editor.ts b/web/src/store/module/editor.ts index 93608de3..2528842f 100644 --- a/web/src/store/module/editor.ts +++ b/web/src/store/module/editor.ts @@ -1,5 +1,5 @@ import store, { useAppSelector } from ".."; -import { setEditMemoId, setMemoVisibility, setResourceList } from "../reducer/editor"; +import { setEditMemoId, setMemoVisibility, setRelationList, setResourceList } from "../reducer/editor"; export const useEditorStore = () => { const state = useAppSelector((state) => state.editor); @@ -21,8 +21,8 @@ export const useEditorStore = () => { setResourceList: (resourceList: Resource[]) => { store.dispatch(setResourceList(resourceList)); }, - clearResourceList: () => { - store.dispatch(setResourceList([])); + setRelationList: (relationList: MemoRelation[]) => { + store.dispatch(setRelationList(relationList)); }, }; }; diff --git a/web/src/store/module/memo.ts b/web/src/store/module/memo.ts index cbd4aace..6252217c 100644 --- a/web/src/store/module/memo.ts +++ b/web/src/store/module/memo.ts @@ -4,8 +4,9 @@ import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import { useUserStore } from "./"; import store, { useAppSelector } from "../"; import { createMemo, deleteMemo, patchMemo, setIsFetching, upsertMemos } from "../reducer/memo"; +import { useMemoCacheStore } from "../zustand/memo"; -const convertResponseModelMemo = (memo: Memo): Memo => { +export const convertResponseModelMemo = (memo: Memo): Memo => { return { ...memo, createdTs: memo.createdTs * 1000, @@ -16,6 +17,7 @@ const convertResponseModelMemo = (memo: Memo): Memo => { export const useMemoStore = () => { const state = useAppSelector((state) => state.memo); const userStore = useUserStore(); + const memoCacheStore = useMemoCacheStore(); const fetchMemoById = async (memoId: MemoId) => { const { data } = (await api.getMemoById(memoId)).data; @@ -44,6 +46,10 @@ export const useMemoStore = () => { store.dispatch(upsertMemos(fetchedMemos)); store.dispatch(setIsFetching(false)); + for (const m of fetchedMemos) { + memoCacheStore.setMemoCache(m); + } + return fetchedMemos; }, fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => { @@ -54,8 +60,13 @@ export const useMemoStore = () => { }; const { data } = (await api.getAllMemos(memoFind)).data; - const memos = data.map((m) => convertResponseModelMemo(m)); - return memos; + const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); + + for (const m of fetchedMemos) { + memoCacheStore.setMemoCache(m); + } + + return fetchedMemos; }, fetchArchivedMemos: async () => { const memoFind: MemoFind = { @@ -88,12 +99,14 @@ export const useMemoStore = () => { const { data } = (await api.createMemo(memoCreate)).data; const memo = convertResponseModelMemo(data); store.dispatch(createMemo(memo)); + memoCacheStore.setMemoCache(memo); return memo; }, patchMemo: async (memoPatch: MemoPatch): Promise => { const { data } = (await api.patchMemo(memoPatch)).data; const memo = convertResponseModelMemo(data); store.dispatch(patchMemo(omit(memo, "pinned"))); + memoCacheStore.setMemoCache(memo); return memo; }, pinMemo: async (memoId: MemoId) => { @@ -117,6 +130,7 @@ export const useMemoStore = () => { deleteMemoById: async (memoId: MemoId) => { await api.deleteMemo(memoId); store.dispatch(deleteMemo(memoId)); + memoCacheStore.deleteMemoCache(memoId); }, }; }; diff --git a/web/src/store/reducer/editor.ts b/web/src/store/reducer/editor.ts index 4a960d67..a833338a 100644 --- a/web/src/store/reducer/editor.ts +++ b/web/src/store/reducer/editor.ts @@ -3,6 +3,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; interface State { memoVisibility: Visibility; resourceList: Resource[]; + relationList: MemoRelation[]; editMemoId?: MemoId; } @@ -11,6 +12,7 @@ const editorSlice = createSlice({ initialState: { memoVisibility: "PRIVATE", resourceList: [], + relationList: [], } as State, reducers: { setEditMemoId: (state, action: PayloadAction>) => { @@ -31,9 +33,15 @@ const editorSlice = createSlice({ resourceList: action.payload, }; }, + setRelationList: (state, action: PayloadAction) => { + return { + ...state, + relationList: action.payload, + }; + }, }, }); -export const { setEditMemoId, setMemoVisibility, setResourceList } = editorSlice.actions; +export const { setEditMemoId, setMemoVisibility, setResourceList, setRelationList } = editorSlice.actions; export default editorSlice.reducer; diff --git a/web/src/store/zustand/index.ts b/web/src/store/zustand/index.ts new file mode 100644 index 00000000..1d52aeed --- /dev/null +++ b/web/src/store/zustand/index.ts @@ -0,0 +1,2 @@ +export { useMemoCacheStore } from "./memo"; +export { useMessageStore } from "./message"; diff --git a/web/src/store/zustand/memo.ts b/web/src/store/zustand/memo.ts new file mode 100644 index 00000000..09b15490 --- /dev/null +++ b/web/src/store/zustand/memo.ts @@ -0,0 +1,41 @@ +import { create } from "zustand"; +import { combine } from "zustand/middleware"; +import * as api from "@/helpers/api"; +import { convertResponseModelMemo } from "../module"; + +export const useMemoCacheStore = create( + combine({ memoById: new Map() }, (set, get) => ({ + getState: () => get(), + getOrFetchMemoById: async (memoId: MemoId) => { + const memo = get().memoById.get(memoId); + if (memo) { + return memo; + } + + const { data } = (await api.getMemoById(memoId)).data; + const formatedMemo = convertResponseModelMemo(data); + + set((state) => { + state.memoById.set(memoId, formatedMemo); + return state; + }); + + return formatedMemo; + }, + getMemoById: (memoId: MemoId) => { + return get().memoById.get(memoId); + }, + setMemoCache: (memo: Memo) => { + set((state) => { + state.memoById.set(memo.id, memo); + return state; + }); + }, + deleteMemoCache: (memoId: MemoId) => { + set((state) => { + state.memoById.delete(memoId); + return state; + }); + }, + })) +);