From 785c250f3cdbac7c1b1731fa2d5257a2918fa056 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 27 May 2025 08:26:13 +0800 Subject: [PATCH] refactor: migrate memo store --- .../components/HomeSidebar/HomeSidebar.tsx | 6 +- .../components/Inbox/MemoCommentMessage.tsx | 9 +- web/src/components/MemoActionMenu.tsx | 8 +- .../EmbeddedContent/EmbeddedMemo.tsx | 8 +- .../ReferencedContent/ReferencedMemo.tsx | 8 +- .../components/MemoContent/TaskListItem.tsx | 8 +- web/src/components/MemoContent/index.tsx | 8 +- .../MemoEditor/RelationListView.tsx | 8 +- web/src/components/MemoEditor/index.tsx | 4 +- web/src/components/MemoView.tsx | 8 +- .../PagedMemoList/PagedMemoList.tsx | 9 +- web/src/components/ReactionSelector.tsx | 8 +- web/src/components/ReactionView.tsx | 8 +- web/src/pages/MemoDetail.tsx | 8 +- web/src/pages/Resources.tsx | 8 +- web/src/store/v1/index.ts | 1 - web/src/store/v1/memo.ts | 128 --------------- web/src/store/v2/index.ts | 3 +- web/src/store/v2/memo.ts | 150 ++++++++++++++++++ 19 files changed, 206 insertions(+), 192 deletions(-) delete mode 100644 web/src/store/v1/memo.ts create mode 100644 web/src/store/v2/memo.ts diff --git a/web/src/components/HomeSidebar/HomeSidebar.tsx b/web/src/components/HomeSidebar/HomeSidebar.tsx index fdf62e246..4be07aff2 100644 --- a/web/src/components/HomeSidebar/HomeSidebar.tsx +++ b/web/src/components/HomeSidebar/HomeSidebar.tsx @@ -6,8 +6,7 @@ import useDebounce from "react-use/lib/useDebounce"; import SearchBar from "@/components/SearchBar"; import useCurrentUser from "@/hooks/useCurrentUser"; import { Routes } from "@/router"; -import { useMemoList } from "@/store/v1"; -import { userStore } from "@/store/v2"; +import { memoStore, userStore } from "@/store/v2"; import { cn } from "@/utils"; import { useTranslate } from "@/utils/i18n"; import MemoFilters from "../MemoFilters"; @@ -30,7 +29,6 @@ const HomeSidebar = observer((props: Props) => { const t = useTranslate(); const location = useLocation(); const currentUser = useCurrentUser(); - const memoList = useMemoList(); const homeNavLink: NavLinkItem = { id: "header-home", @@ -61,7 +59,7 @@ const HomeSidebar = observer((props: Props) => { await userStore.fetchUserStats(parent); }, 300, - [memoList.size(), userStore.state.statsStateId, location.pathname], + [memoStore.state.memos.length, userStore.state.statsStateId, location.pathname], ); return ( diff --git a/web/src/components/Inbox/MemoCommentMessage.tsx b/web/src/components/Inbox/MemoCommentMessage.tsx index 6642ac092..cb98a4d76 100644 --- a/web/src/components/Inbox/MemoCommentMessage.tsx +++ b/web/src/components/Inbox/MemoCommentMessage.tsx @@ -1,13 +1,13 @@ import { Tooltip } from "@mui/joy"; import { InboxIcon, LoaderIcon, MessageCircleIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useState } from "react"; import toast from "react-hot-toast"; import { activityServiceClient } from "@/grpcweb"; import useAsyncEffect from "@/hooks/useAsyncEffect"; import useNavigateTo from "@/hooks/useNavigateTo"; import { activityNamePrefix } from "@/store/common"; -import { useMemoStore } from "@/store/v1"; -import { userStore } from "@/store/v2"; +import { memoStore, userStore } from "@/store/v2"; import { Inbox, Inbox_Status } from "@/types/proto/api/v1/inbox_service"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { User } from "@/types/proto/api/v1/user_service"; @@ -18,10 +18,9 @@ interface Props { inbox: Inbox; } -const MemoCommentMessage = ({ inbox }: Props) => { +const MemoCommentMessage = observer(({ inbox }: Props) => { const t = useTranslate(); const navigateTo = useNavigateTo(); - const memoStore = useMemoStore(); const [relatedMemo, setRelatedMemo] = useState(undefined); const [sender, setSender] = useState(undefined); const [initialized, setInitialized] = useState(false); @@ -124,6 +123,6 @@ const MemoCommentMessage = ({ inbox }: Props) => { ); -}; +}); export default MemoCommentMessage; diff --git a/web/src/components/MemoActionMenu.tsx b/web/src/components/MemoActionMenu.tsx index 7c955e6a5..0548e53b7 100644 --- a/web/src/components/MemoActionMenu.tsx +++ b/web/src/components/MemoActionMenu.tsx @@ -11,11 +11,12 @@ import { TrashIcon, SquareCheckIcon, } from "lucide-react"; +import { observer } from "mobx-react-lite"; import toast from "react-hot-toast"; import { useLocation } from "react-router-dom"; import { markdownServiceClient } from "@/grpcweb"; import useNavigateTo from "@/hooks/useNavigateTo"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { userStore } from "@/store/v2"; import { State } from "@/types/proto/api/v1/common"; import { NodeType } from "@/types/proto/api/v1/markdown_service"; @@ -43,12 +44,11 @@ const checkHasCompletedTaskList = (memo: Memo) => { return false; }; -const MemoActionMenu = (props: Props) => { +const MemoActionMenu = observer((props: Props) => { const { memo, readonly } = props; const t = useTranslate(); const location = useLocation(); const navigateTo = useNavigateTo(); - const memoStore = useMemoStore(); const hasCompletedTaskList = checkHasCompletedTaskList(memo); const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`); const isComment = Boolean(memo.parent); @@ -212,6 +212,6 @@ const MemoActionMenu = (props: Props) => { ); -}; +}); export default MemoActionMenu; diff --git a/web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx b/web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx index d030bc204..90b23d32a 100644 --- a/web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx +++ b/web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx @@ -1,12 +1,13 @@ import copy from "copy-to-clipboard"; import { ArrowUpRightIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useContext, useEffect } from "react"; import toast from "react-hot-toast"; import { Link } from "react-router-dom"; import MemoResourceListView from "@/components/MemoResourceListView"; import useLoading from "@/hooks/useLoading"; import { extractMemoIdFromName } from "@/store/common"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { cn } from "@/utils"; import MemoContent from ".."; import { RendererContext } from "../types"; @@ -17,10 +18,9 @@ interface Props { params: string; } -const EmbeddedMemo = ({ resourceId: uid, params: paramsStr }: Props) => { +const EmbeddedMemo = observer(({ resourceId: uid, params: paramsStr }: Props) => { const context = useContext(RendererContext); const loadingState = useLoading(); - const memoStore = useMemoStore(); const memoName = `memos/${uid}`; const memo = memoStore.getMemoByName(memoName); @@ -87,6 +87,6 @@ const EmbeddedMemo = ({ resourceId: uid, params: paramsStr }: Props) => { {contentNode} ); -}; +}); export default EmbeddedMemo; diff --git a/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx b/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx index 66ea2307c..e7847fc6a 100644 --- a/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx +++ b/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx @@ -1,8 +1,9 @@ +import { observer } from "mobx-react-lite"; import { useContext, useEffect } from "react"; import useLoading from "@/hooks/useLoading"; import useNavigateTo from "@/hooks/useNavigateTo"; import { memoNamePrefix } from "@/store/common"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { RendererContext } from "../types"; import Error from "./Error"; @@ -11,10 +12,9 @@ interface Props { params: string; } -const ReferencedMemo = ({ resourceId: uid, params: paramsStr }: Props) => { +const ReferencedMemo = observer(({ resourceId: uid, params: paramsStr }: Props) => { const navigateTo = useNavigateTo(); const loadingState = useLoading(); - const memoStore = useMemoStore(); const memoName = `${memoNamePrefix}${uid}`; const memo = memoStore.getMemoByName(memoName); const params = new URLSearchParams(paramsStr); @@ -50,6 +50,6 @@ const ReferencedMemo = ({ resourceId: uid, params: paramsStr }: Props) => { {displayContent} ); -}; +}); export default ReferencedMemo; diff --git a/web/src/components/MemoContent/TaskListItem.tsx b/web/src/components/MemoContent/TaskListItem.tsx index a62487545..560612579 100644 --- a/web/src/components/MemoContent/TaskListItem.tsx +++ b/web/src/components/MemoContent/TaskListItem.tsx @@ -1,7 +1,8 @@ import { Checkbox } from "@usememos/mui"; +import { observer } from "mobx-react-lite"; import { useContext } from "react"; import { markdownServiceClient } from "@/grpcweb"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { Node, TaskListItemNode } from "@/types/proto/api/v1/markdown_service"; import { cn } from "@/utils"; import Renderer from "./Renderer"; @@ -16,9 +17,8 @@ interface Props { children: Node[]; } -const TaskListItem: React.FC = ({ node, complete, children }: Props) => { +const TaskListItem = observer(({ node, complete, children }: Props) => { const context = useContext(RendererContext); - const memoStore = useMemoStore(); const handleCheckboxChange = async (on: boolean) => { if (context.readonly || !context.memoName) { @@ -48,6 +48,6 @@ const TaskListItem: React.FC = ({ node, complete, children }: Props) => {

); -}; +}); export default TaskListItem; diff --git a/web/src/components/MemoContent/index.tsx b/web/src/components/MemoContent/index.tsx index 9a6c849b3..afba1d529 100644 --- a/web/src/components/MemoContent/index.tsx +++ b/web/src/components/MemoContent/index.tsx @@ -1,6 +1,7 @@ +import { observer } from "mobx-react-lite"; import { memo, useEffect, useRef, useState } from "react"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { Node, NodeType } from "@/types/proto/api/v1/markdown_service"; import { cn } from "@/utils"; import { useTranslate } from "@/utils/i18n"; @@ -29,11 +30,10 @@ interface Props { type ContentCompactView = "ALL" | "SNIPPET"; -const MemoContent: React.FC = (props: Props) => { +const MemoContent = observer((props: Props) => { const { className, contentClassName, nodes, memoName, embeddedMemos, onClick, onDoubleClick } = props; const t = useTranslate(); const currentUser = useCurrentUser(); - const memoStore = useMemoStore(); const memoContentContainerRef = useRef(null); const [showCompactMode, setShowCompactMode] = useState(undefined); const memo = memoName ? memoStore.getMemoByName(memoName) : null; @@ -122,6 +122,6 @@ const MemoContent: React.FC = (props: Props) => { ); -}; +}); export default memo(MemoContent); diff --git a/web/src/components/MemoEditor/RelationListView.tsx b/web/src/components/MemoEditor/RelationListView.tsx index da9ba9e24..ad709acfa 100644 --- a/web/src/components/MemoEditor/RelationListView.tsx +++ b/web/src/components/MemoEditor/RelationListView.tsx @@ -1,6 +1,7 @@ import { LinkIcon, XIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { Memo, MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_service"; interface Props { @@ -8,9 +9,8 @@ interface Props { setRelationList: (relationList: MemoRelation[]) => void; } -const RelationListView = (props: Props) => { +const RelationListView = observer((props: Props) => { const { relationList, setRelationList } = props; - const memoStore = useMemoStore(); const [referencingMemoList, setReferencingMemoList] = useState([]); useEffect(() => { @@ -50,6 +50,6 @@ const RelationListView = (props: Props) => { )} ); -}; +}); export default RelationListView; diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 724ffeaa8..42c6fe0c4 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -13,8 +13,7 @@ import { TAB_SPACE_WIDTH } from "@/helpers/consts"; import { isValidUrl } from "@/helpers/utils"; import useAsyncEffect from "@/hooks/useAsyncEffect"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { useMemoStore } from "@/store/v1"; -import { resourceStore, userStore, workspaceStore } from "@/store/v2"; +import { memoStore, resourceStore, userStore, workspaceStore } from "@/store/v2"; import { Location, Memo, MemoRelation, MemoRelation_Type, Visibility } from "@/types/proto/api/v1/memo_service"; import { Resource } from "@/types/proto/api/v1/resource_service"; import { UserSetting } from "@/types/proto/api/v1/user_service"; @@ -61,7 +60,6 @@ const MemoEditor = observer((props: Props) => { const { className, cacheKey, memoName, parentMemoName, autoFocus, onConfirm, onCancel } = props; const t = useTranslate(); const { i18n } = useTranslation(); - const memoStore = useMemoStore(); const currentUser = useCurrentUser(); const [state, setState] = useState({ memoVisibility: Visibility.PRIVATE, diff --git a/web/src/components/MemoView.tsx b/web/src/components/MemoView.tsx index 6118c7105..0e166bd25 100644 --- a/web/src/components/MemoView.tsx +++ b/web/src/components/MemoView.tsx @@ -1,11 +1,12 @@ import { Tooltip } from "@mui/joy"; import { BookmarkIcon, EyeOffIcon, MessageCircleMoreIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { memo, useCallback, useRef, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import useAsyncEffect from "@/hooks/useAsyncEffect"; import useCurrentUser from "@/hooks/useCurrentUser"; import useNavigateTo from "@/hooks/useNavigateTo"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { userStore, workspaceStore } from "@/store/v2"; import { State } from "@/types/proto/api/v1/common"; import { Memo, MemoRelation_Type, Visibility } from "@/types/proto/api/v1/memo_service"; @@ -36,14 +37,13 @@ interface Props { parentPage?: string; } -const MemoView: React.FC = (props: Props) => { +const MemoView: React.FC = observer((props: Props) => { const { memo, className } = props; const t = useTranslate(); const location = useLocation(); const navigateTo = useNavigateTo(); const currentUser = useCurrentUser(); const user = useCurrentUser(); - const memoStore = useMemoStore(); const [showEditor, setShowEditor] = useState(false); const [creator, setCreator] = useState(userStore.getUserByName(memo.creator)); const [showNSFWContent, setShowNSFWContent] = useState(props.showNsfwContent); @@ -250,6 +250,6 @@ const MemoView: React.FC = (props: Props) => { )} ); -}; +}); export default memo(MemoView); diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx index 211ffcb1c..cac610b27 100644 --- a/web/src/components/PagedMemoList/PagedMemoList.tsx +++ b/web/src/components/PagedMemoList/PagedMemoList.tsx @@ -7,8 +7,7 @@ import PullToRefresh from "react-simple-pull-to-refresh"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import { Routes } from "@/router"; -import { useMemoList, useMemoStore } from "@/store/v1"; -import { viewStore } from "@/store/v2"; +import { memoStore, viewStore } from "@/store/v2"; import { Direction, State } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; @@ -35,13 +34,11 @@ interface LocalState { const PagedMemoList = observer((props: Props) => { const t = useTranslate(); const { md } = useResponsiveWidth(); - const memoStore = useMemoStore(); - const memoList = useMemoList(); const [state, setState] = useState({ isRequesting: true, // Initial request nextPageToken: "", }); - const sortedMemoList = props.listSort ? props.listSort(memoList.value) : memoList.value; + const sortedMemoList = props.listSort ? props.listSort(memoStore.state.memos) : memoStore.state.memos; const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname)); const fetchMoreMemos = async (nextPageToken: string) => { @@ -62,7 +59,7 @@ const PagedMemoList = observer((props: Props) => { }; const refreshList = async () => { - memoList.reset(); + memoStore.state.updateStateId(); setState((state) => ({ ...state, nextPageToken: "" })); await fetchMoreMemos(""); }; diff --git a/web/src/components/ReactionSelector.tsx b/web/src/components/ReactionSelector.tsx index 457834340..5a07fb293 100644 --- a/web/src/components/ReactionSelector.tsx +++ b/web/src/components/ReactionSelector.tsx @@ -1,10 +1,11 @@ import { Dropdown, Menu, MenuButton } from "@mui/joy"; import { SmilePlusIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useRef, useState } from "react"; import useClickAway from "react-use/lib/useClickAway"; import { memoServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { workspaceStore } from "@/store/v2"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { cn } from "@/utils"; @@ -14,10 +15,9 @@ interface Props { className?: string; } -const ReactionSelector = (props: Props) => { +const ReactionSelector = observer((props: Props) => { const { memo, className } = props; const currentUser = useCurrentUser(); - const memoStore = useMemoStore(); const [open, setOpen] = useState(false); const containerRef = useRef(null); const workspaceMemoRelatedSetting = workspaceStore.state.memoRelatedSetting; @@ -86,6 +86,6 @@ const ReactionSelector = (props: Props) => { ); -}; +}); export default ReactionSelector; diff --git a/web/src/components/ReactionView.tsx b/web/src/components/ReactionView.tsx index 6e52cb1dd..48a82344a 100644 --- a/web/src/components/ReactionView.tsx +++ b/web/src/components/ReactionView.tsx @@ -1,7 +1,8 @@ import { Tooltip } from "@mui/joy"; +import { observer } from "mobx-react-lite"; import { memoServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { State } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { User } from "@/types/proto/api/v1/user_service"; @@ -28,10 +29,9 @@ const stringifyUsers = (users: User[], reactionType: string): string => { ); }; -const ReactionView = (props: Props) => { +const ReactionView = observer((props: Props) => { const { memo, reactionType, users } = props; const currentUser = useCurrentUser(); - const memoStore = useMemoStore(); const hasReaction = users.some((user) => currentUser && user.username === currentUser.username); const readonly = memo.state === State.ARCHIVED; @@ -80,6 +80,6 @@ const ReactionView = (props: Props) => { ); -}; +}); export default ReactionView; diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 383890598..e3c665bfe 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -1,5 +1,6 @@ import { Button } from "@usememos/mui"; import { ArrowUpLeftFromCircleIcon, MessageCircleIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { ClientError } from "nice-grpc-web"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; @@ -12,20 +13,19 @@ import useCurrentUser from "@/hooks/useCurrentUser"; import useNavigateTo from "@/hooks/useNavigateTo"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import { memoNamePrefix } from "@/store/common"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { workspaceStore } from "@/store/v2"; import { Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_service"; import { cn } from "@/utils"; import { useTranslate } from "@/utils/i18n"; -const MemoDetail = () => { +const MemoDetail = observer(() => { const t = useTranslate(); const { md } = useResponsiveWidth(); const params = useParams(); const navigateTo = useNavigateTo(); const { state: locationState } = useLocation(); const currentUser = useCurrentUser(); - const memoStore = useMemoStore(); const uid = params.uid; const memoName = `${memoNamePrefix}${uid}`; const memo = memoStore.getMemoByName(memoName); @@ -176,6 +176,6 @@ const MemoDetail = () => { ); -}; +}); export default MemoDetail; diff --git a/web/src/pages/Resources.tsx b/web/src/pages/Resources.tsx index 5587d2960..125d37db3 100644 --- a/web/src/pages/Resources.tsx +++ b/web/src/pages/Resources.tsx @@ -3,6 +3,7 @@ import { Button, Input } from "@usememos/mui"; import dayjs from "dayjs"; import { includes } from "lodash-es"; import { PaperclipIcon, SearchIcon, TrashIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; import Empty from "@/components/Empty"; import MobileHeader from "@/components/MobileHeader"; @@ -11,7 +12,7 @@ import { resourceServiceClient } from "@/grpcweb"; import useLoading from "@/hooks/useLoading"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import i18n from "@/i18n"; -import { useMemoStore } from "@/store/v1"; +import { memoStore } from "@/store/v2"; import { Resource } from "@/types/proto/api/v1/resource_service"; import { useTranslate } from "@/utils/i18n"; @@ -33,14 +34,13 @@ interface State { searchQuery: string; } -const Resources = () => { +const Resources = observer(() => { const t = useTranslate(); const { md } = useResponsiveWidth(); const loadingState = useLoading(); const [state, setState] = useState({ searchQuery: "", }); - const memoStore = useMemoStore(); const [resources, setResources] = useState([]); const filteredResources = resources.filter((resource) => includes(resource.filename, state.searchQuery)); const groupedResources = groupResourcesByDate(filteredResources.filter((resource) => resource.memo)); @@ -165,6 +165,6 @@ const Resources = () => { ); -}; +}); export default Resources; diff --git a/web/src/store/v1/index.ts b/web/src/store/v1/index.ts index b090b900a..1d5a627d9 100644 --- a/web/src/store/v1/index.ts +++ b/web/src/store/v1/index.ts @@ -1,2 +1 @@ -export * from "./memo"; export * from "./memoFilter"; diff --git a/web/src/store/v1/memo.ts b/web/src/store/v1/memo.ts deleted file mode 100644 index 14c1abb91..000000000 --- a/web/src/store/v1/memo.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { uniqueId } from "lodash-es"; -import { create } from "zustand"; -import { combine } from "zustand/middleware"; -import { memoServiceClient } from "@/grpcweb"; -import { CreateMemoRequest, ListMemosRequest, Memo } from "@/types/proto/api/v1/memo_service"; - -interface State { - // stateId is used to identify the store instance state. - // It should be update when any state change. - stateId: string; - memoMapByName: Record; - currentRequest: AbortController | null; -} - -const getDefaultState = (): State => ({ - stateId: uniqueId(), - memoMapByName: {}, - currentRequest: null, -}); - -export const useMemoStore = create( - combine(getDefaultState(), (set, get) => ({ - setState: (state: State) => set(state), - getState: () => get(), - updateStateId: () => set({ stateId: uniqueId() }), - fetchMemos: async (request: Partial) => { - const currentRequest = get().currentRequest; - if (currentRequest) { - currentRequest.abort(); - } - - const controller = new AbortController(); - set({ currentRequest: controller }); - - try { - const { memos, nextPageToken } = await memoServiceClient.listMemos( - { - ...request, - }, - { signal: controller.signal }, - ); - - if (!controller.signal.aborted) { - const memoMap = request.pageToken ? { ...get().memoMapByName } : {}; - for (const memo of memos) { - memoMap[memo.name] = memo; - } - set({ stateId: uniqueId(), memoMapByName: memoMap }); - return { memos, nextPageToken }; - } - } catch (error: any) { - if (error.name === "AbortError") { - return; - } - throw error; - } finally { - if (get().currentRequest === controller) { - set({ currentRequest: null }); - } - } - }, - getOrFetchMemoByName: async (name: string, options?: { skipCache?: boolean; skipStore?: boolean }) => { - const memoMap = get().memoMapByName; - const memoCache = memoMap[name]; - if (memoCache && !options?.skipCache) { - return memoCache; - } - - const memo = await memoServiceClient.getMemo({ - name, - }); - if (!options?.skipStore) { - memoMap[name] = memo; - set({ stateId: uniqueId(), memoMapByName: memoMap }); - } - return memo; - }, - getMemoByName: (name: string) => { - return get().memoMapByName[name]; - }, - createMemo: async (request: CreateMemoRequest) => { - const memo = await memoServiceClient.createMemo(request); - const memoMap = get().memoMapByName; - memoMap[memo.name] = memo; - set({ stateId: uniqueId(), memoMapByName: memoMap }); - return memo; - }, - updateMemo: async (update: Partial, updateMask: string[]) => { - const memo = await memoServiceClient.updateMemo({ - memo: update, - updateMask, - }); - - const memoMap = get().memoMapByName; - memoMap[memo.name] = memo; - set({ stateId: uniqueId(), memoMapByName: memoMap }); - return memo; - }, - deleteMemo: async (name: string) => { - await memoServiceClient.deleteMemo({ - name, - }); - - const memoMap = get().memoMapByName; - delete memoMap[name]; - set({ stateId: uniqueId(), memoMapByName: memoMap }); - }, - })), -); - -export const useMemoList = () => { - const memoStore = useMemoStore(); - const memos = Object.values(memoStore.getState().memoMapByName); - - const reset = () => { - memoStore.updateStateId(); - }; - - const size = () => { - return Object.keys(memoStore.getState().memoMapByName).length; - }; - - return { - value: memos, - reset, - size, - }; -}; diff --git a/web/src/store/v2/index.ts b/web/src/store/v2/index.ts index 4747e2bbf..78f6377bb 100644 --- a/web/src/store/v2/index.ts +++ b/web/src/store/v2/index.ts @@ -1,6 +1,7 @@ +import memoStore from "./memo"; import resourceStore from "./resource"; import userStore from "./user"; import viewStore from "./view"; import workspaceStore from "./workspace"; -export { resourceStore, workspaceStore, userStore, viewStore }; +export { memoStore, resourceStore, workspaceStore, userStore, viewStore }; diff --git a/web/src/store/v2/memo.ts b/web/src/store/v2/memo.ts new file mode 100644 index 000000000..0ff5d79d2 --- /dev/null +++ b/web/src/store/v2/memo.ts @@ -0,0 +1,150 @@ +import { uniqueId } from "lodash-es"; +import { makeAutoObservable } from "mobx"; +import { memoServiceClient } from "@/grpcweb"; +import { CreateMemoRequest, ListMemosRequest, Memo } from "@/types/proto/api/v1/memo_service"; + +class LocalState { + stateId: string = uniqueId(); + memoMapByName: Record = {}; + currentRequest: AbortController | null = null; + + constructor() { + makeAutoObservable(this); + } + + setPartial(partial: Partial) { + Object.assign(this, partial); + } + + updateStateId() { + this.stateId = uniqueId(); + } + + get memos() { + return Object.values(this.memoMapByName); + } + + get size() { + return Object.keys(this.memoMapByName).length; + } +} + +const memoStore = (() => { + const state = new LocalState(); + + const fetchMemos = async (request: Partial) => { + if (state.currentRequest) { + state.currentRequest.abort(); + } + + const controller = new AbortController(); + state.setPartial({ currentRequest: controller }); + + try { + const { memos, nextPageToken } = await memoServiceClient.listMemos( + { + ...request, + }, + { signal: controller.signal }, + ); + + if (!controller.signal.aborted) { + const memoMap = request.pageToken ? { ...state.memoMapByName } : {}; + for (const memo of memos) { + memoMap[memo.name] = memo; + } + state.setPartial({ + stateId: uniqueId(), + memoMapByName: memoMap, + }); + return { memos, nextPageToken }; + } + } catch (error: any) { + if (error.name === "AbortError") { + return; + } + throw error; + } finally { + if (state.currentRequest === controller) { + state.setPartial({ currentRequest: null }); + } + } + }; + + const getOrFetchMemoByName = async (name: string, options?: { skipCache?: boolean; skipStore?: boolean }) => { + const memoCache = state.memoMapByName[name]; + if (memoCache && !options?.skipCache) { + return memoCache; + } + + const memo = await memoServiceClient.getMemo({ + name, + }); + + if (!options?.skipStore) { + const memoMap = { ...state.memoMapByName }; + memoMap[name] = memo; + state.setPartial({ + stateId: uniqueId(), + memoMapByName: memoMap, + }); + } + + return memo; + }; + + const getMemoByName = (name: string) => { + return state.memoMapByName[name]; + }; + + const createMemo = async (request: CreateMemoRequest) => { + const memo = await memoServiceClient.createMemo(request); + const memoMap = { ...state.memoMapByName }; + memoMap[memo.name] = memo; + state.setPartial({ + stateId: uniqueId(), + memoMapByName: memoMap, + }); + return memo; + }; + + const updateMemo = async (update: Partial, updateMask: string[]) => { + const memo = await memoServiceClient.updateMemo({ + memo: update, + updateMask, + }); + + const memoMap = { ...state.memoMapByName }; + memoMap[memo.name] = memo; + state.setPartial({ + stateId: uniqueId(), + memoMapByName: memoMap, + }); + return memo; + }; + + const deleteMemo = async (name: string) => { + await memoServiceClient.deleteMemo({ + name, + }); + + const memoMap = { ...state.memoMapByName }; + delete memoMap[name]; + state.setPartial({ + stateId: uniqueId(), + memoMapByName: memoMap, + }); + }; + + return { + state, + fetchMemos, + getOrFetchMemoByName, + getMemoByName, + createMemo, + updateMemo, + deleteMemo, + }; +})(); + +export default memoStore;