From cdc2b449a90b3bab40c395f4b9452c9935d84118 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Wed, 15 Feb 2023 21:41:43 -0600 Subject: [PATCH] refactor(store): extract workspace out of `AppState` (#1037) --- .../enable-workspace-modal/index.tsx | 5 +- apps/web/package.json | 1 + .../enable-workspace-modal/index.tsx | 7 +- .../header/header-right-items/SyncUser.tsx | 8 +- .../header-right-items/TrashButtonGroup.tsx | 7 +- apps/web/src/components/import/index.tsx | 8 +- apps/web/src/components/page-list/index.tsx | 8 +- .../src/components/quick-search/Results.tsx | 17 ++- .../workspace-modal/WorkspaceCard.tsx | 6 +- .../workspace-setting/general/General.tsx | 7 +- .../WorkspaceSelector/WorkspaceSelector.tsx | 18 ++- .../components/workspace-slider-bar/index.tsx | 10 +- apps/web/src/hooks/use-change-page-meta.ts | 7 +- apps/web/src/hooks/use-ensure-workspace.ts | 46 +++--- apps/web/src/hooks/use-members.ts | 5 +- apps/web/src/hooks/use-page-helper.ts | 7 +- apps/web/src/hooks/use-workspace-helper.ts | 9 +- apps/web/src/pages/_app.tsx | 3 +- .../workspace/[workspaceId]/[pageId].tsx | 17 ++- .../src/pages/workspace/[workspaceId]/all.tsx | 9 +- .../workspace/[workspaceId]/favorite.tsx | 5 +- .../pages/workspace/[workspaceId]/index.tsx | 8 +- .../pages/workspace/[workspaceId]/setting.tsx | 6 +- .../pages/workspace/[workspaceId]/trash.tsx | 4 +- apps/web/src/pages/workspace/index.tsx | 8 +- .../providers/app-state-provider/Provider.tsx | 127 ++-------------- .../providers/app-state-provider/interface.ts | 15 +- apps/web/src/store/app/datacenter/index.ts | 139 ++++++++++++++++++ apps/web/src/store/app/index.tsx | 49 ++---- pnpm-lock.yaml | 2 + 30 files changed, 323 insertions(+), 245 deletions(-) create mode 100644 apps/web/src/store/app/datacenter/index.ts diff --git a/apps/desktop/src/preload/components/enable-workspace-modal/index.tsx b/apps/desktop/src/preload/components/enable-workspace-modal/index.tsx index f4d7fda9f3..968a7b71c3 100644 --- a/apps/desktop/src/preload/components/enable-workspace-modal/index.tsx +++ b/apps/desktop/src/preload/components/enable-workspace-modal/index.tsx @@ -1,7 +1,6 @@ import { Modal, ModalWrapper } from '@affine/component'; import { IconButton } from '@affine/component'; import { useTranslation } from '@affine/i18n'; -import { useAppState } from '@/providers/app-state-provider'; import { useState } from 'react'; import router from 'next/router'; import { toast } from '@affine/component'; @@ -22,7 +21,9 @@ export const EnableWorkspaceModal = ({ const login = useGlobalState(store => store.login); const user = useGlobalState(store => store.user); const dataCenter = useGlobalState(store => store.dataCenter); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const [loading, setLoading] = useState(false); return ( diff --git a/apps/web/package.json b/apps/web/package.json index 9d7d60e7a2..3548443c77 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,7 @@ "@affine/i18n": "workspace:*", "@blocksuite/blocks": "0.4.0-20230216011811-2776d93", "@blocksuite/editor": "0.4.0-20230216011811-2776d93", + "@blocksuite/global": "0.4.0-20230216011811-2776d93", "@blocksuite/icons": "^2.0.14", "@blocksuite/store": "0.4.0-20230216011811-2776d93", "@emotion/css": "^11.10.5", diff --git a/apps/web/src/components/enable-workspace-modal/index.tsx b/apps/web/src/components/enable-workspace-modal/index.tsx index e65e5055c1..a272d099ed 100644 --- a/apps/web/src/components/enable-workspace-modal/index.tsx +++ b/apps/web/src/components/enable-workspace-modal/index.tsx @@ -1,9 +1,8 @@ -import { useAppState } from '@/providers/app-state-provider'; import { IconButton, Modal, ModalWrapper, toast } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { CloseIcon } from '@blocksuite/icons'; import { useRouter } from 'next/router'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style'; import { useGlobalState } from '@/store/app'; @@ -20,7 +19,9 @@ export const EnableWorkspaceModal = ({ const login = useGlobalState(store => store.login); const user = useGlobalState(store => store.user); const dataCenter = useGlobalState(store => store.dataCenter); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const [loading, setLoading] = useState(false); const router = useRouter(); diff --git a/apps/web/src/components/header/header-right-items/SyncUser.tsx b/apps/web/src/components/header/header-right-items/SyncUser.tsx index 28a552a4f2..f9798cda0f 100644 --- a/apps/web/src/components/header/header-right-items/SyncUser.tsx +++ b/apps/web/src/components/header/header-right-items/SyncUser.tsx @@ -1,10 +1,10 @@ import { LocalWorkspaceIcon, CloudWorkspaceIcon } from '@blocksuite/icons'; -import { useAppState } from '@/providers/app-state-provider'; import { displayFlex, styled, Tooltip, IconButton } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { useModal } from '@/store/globalModal'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { WorkspaceUnit } from '@affine/datacenter'; +import { useGlobalState } from '@/store/app'; const NoNetWorkIcon = () => { return ( @@ -43,7 +43,9 @@ const getStatus = (workspace: WorkspaceUnit | null) => { return 'cloud'; }; export const SyncUser = () => { - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { triggerEnableWorkspaceModal } = useModal(); const [status, setStatus] = useState<'offline' | 'local' | 'cloud'>( diff --git a/apps/web/src/components/header/header-right-items/TrashButtonGroup.tsx b/apps/web/src/components/header/header-right-items/TrashButtonGroup.tsx index 9c573b0ca0..b511434319 100644 --- a/apps/web/src/components/header/header-right-items/TrashButtonGroup.tsx +++ b/apps/web/src/components/header/header-right-items/TrashButtonGroup.tsx @@ -1,14 +1,17 @@ import { Button } from '@affine/component'; import { usePageHelper } from '@/hooks/use-page-helper'; -import { useAppState } from '@/providers/app-state-provider'; import { useConfirm } from '@/providers/ConfirmProvider'; import { useRouter } from 'next/router'; import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import { useTranslation } from '@affine/i18n'; +import { useGlobalState } from '@/store/app'; +import { useCallback } from 'react'; export const TrashButtonGroup = () => { const { permanentlyDeletePage } = usePageHelper(); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { toggleDeletePage } = usePageHelper(); const confirm = useConfirm(store => store.confirm); const router = useRouter(); diff --git a/apps/web/src/components/import/index.tsx b/apps/web/src/components/import/index.tsx index 27a57921f4..15c917bbbf 100644 --- a/apps/web/src/components/import/index.tsx +++ b/apps/web/src/components/import/index.tsx @@ -4,9 +4,9 @@ import { Button } from '@affine/component'; import { Content, FlexWrapper } from '@affine/component'; import Loading from '@/components/loading'; import { usePageHelper } from '@/hooks/use-page-helper'; -import { useAppState } from '@/providers/app-state-provider'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from '@affine/i18n'; +import { useGlobalState } from '@/store/app'; // import { Tooltip } from '@affine/component'; type ImportModalProps = { open: boolean; @@ -19,7 +19,9 @@ type Template = { export const ImportModal = ({ open, onClose }: ImportModalProps) => { const [status, setStatus] = useState<'unImported' | 'importing'>('importing'); const { openPage, createPage } = usePageHelper(); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { t } = useTranslation(); const _applyTemplate = function (pageId: string, template: Template) { const page = currentWorkspace?.blocksuiteWorkspace?.getPage(pageId); diff --git a/apps/web/src/components/page-list/index.tsx b/apps/web/src/components/page-list/index.tsx index 8386f902cc..ad6a9369c5 100644 --- a/apps/web/src/components/page-list/index.tsx +++ b/apps/web/src/components/page-list/index.tsx @@ -21,16 +21,16 @@ import { import { OperationCell, TrashOperationCell } from './OperationCell'; import Empty from './Empty'; import { Content } from '@affine/component'; -import React from 'react'; +import React, { useCallback } from 'react'; import DateCell from '@/components/page-list/DateCell'; import { IconButton } from '@affine/component'; import { Tooltip } from '@affine/component'; import { useRouter } from 'next/router'; -import { useAppState } from '@/providers/app-state-provider'; import { toast } from '@affine/component'; import { usePageHelper } from '@/hooks/use-page-helper'; import { useTheme } from '@/providers/ThemeProvider'; import { useTranslation } from '@affine/i18n'; +import { useGlobalState } from '@/store/app'; const FavoriteTag = ({ pageMeta: { favorite, id }, }: { @@ -83,7 +83,9 @@ export const PageList = ({ listType?: 'all' | 'trash' | 'favorite'; }) => { const router = useRouter(); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { t } = useTranslation(); if (pageList.length === 0) { return ; diff --git a/apps/web/src/components/quick-search/Results.tsx b/apps/web/src/components/quick-search/Results.tsx index d7c8eddb0b..1f698c9b3e 100644 --- a/apps/web/src/components/quick-search/Results.tsx +++ b/apps/web/src/components/quick-search/Results.tsx @@ -1,13 +1,19 @@ import { Command } from 'cmdk'; import { StyledListItem, StyledNotFound } from './style'; import { PaperIcon, EdgelessIcon } from '@blocksuite/icons'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import { useAppState } from '@/providers/app-state-provider'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from 'react'; import { useRouter } from 'next/router'; import { useSwitchToConfig } from './config'; import { NoResultSVG } from './NoResultSVG'; import { useTranslation } from '@affine/i18n'; import usePageHelper from '@/hooks/use-page-helper'; +import { useGlobalState } from '@/store/app'; export const Results = (props: { query: string; loading: boolean; @@ -18,7 +24,12 @@ export const Results = (props: { const { query, loading, setLoading, setShowCreatePage, onClose } = props; const { openPage } = usePageHelper(); const router = useRouter(); - const { currentWorkspace, pageList } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); + const pageList = useGlobalState( + useCallback(store => store.dataCenterPageList, []) + ); const { search } = usePageHelper(); const List = useSwitchToConfig(currentWorkspace?.id); const [results, setResults] = useState(new Map()); diff --git a/apps/web/src/components/workspace-modal/WorkspaceCard.tsx b/apps/web/src/components/workspace-modal/WorkspaceCard.tsx index 1a0f355f39..26e8d0ef6f 100644 --- a/apps/web/src/components/workspace-modal/WorkspaceCard.tsx +++ b/apps/web/src/components/workspace-modal/WorkspaceCard.tsx @@ -7,10 +7,10 @@ import { PublishIcon, } from '@/components/icons'; import { WorkspaceUnit } from '@affine/datacenter'; -import { useAppState } from '@/providers/app-state-provider'; import { StyleWorkspaceInfo, StyleWorkspaceTitle, StyledCard } from './styles'; import { useTranslation } from '@affine/i18n'; import { useGlobalState } from '@/store/app'; +import { useCallback } from 'react'; const WorkspaceType = ({ workspaceData }: { workspaceData: WorkspaceUnit }) => { const user = useGlobalState(store => store.user); @@ -46,7 +46,9 @@ export const WorkspaceCard = ({ workspaceData: WorkspaceUnit; onClick: (data: WorkspaceUnit) => void; }) => { - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { t } = useTranslation(); return ( { const [workspaceName, setWorkspaceName] = useState(workspace?.name); const [showEditInput, setShowEditInput] = useState(false); const isOwner = useGlobalState(store => store.isOwner); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { updateWorkspace } = useWorkspaceHelper(); const { t } = useTranslation(); diff --git a/apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx b/apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx index ac9c8b0ca9..67587642bc 100644 --- a/apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx +++ b/apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx @@ -1,17 +1,19 @@ import { WorkspaceName, SelectorWrapper } from './styles'; -import { useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { WorkspaceModal } from '@/components/workspace-modal'; import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; -import { useAppState } from '@/providers/app-state-provider'; +import { useGlobalState } from '@/store/app'; + export const WorkspaceSelector = () => { const [workspaceListShow, setWorkspaceListShow] = useState(false); - const { currentWorkspace, workspaceList } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); + const dataCenter = useGlobalState(useCallback(store => store.dataCenter, [])); - useEffect(() => { - if (workspaceList.length === 0) { - setWorkspaceListShow(true); - } - }, [workspaceList]); + if (dataCenter.workspaces.length === 0) { + setWorkspaceListShow(true); + } return ( <> { const { openPage } = usePageHelper(); - const { pageList } = useAppState(); + const pageList = useGlobalState(store => store.dataCenterPageList); const router = useRouter(); const { t } = useTranslation(); const favoriteList = pageList.filter(p => p.favorite && !p.trash); @@ -66,7 +66,9 @@ const FavoriteList = ({ showList }: { showList: boolean }) => { export const WorkSpaceSliderBar = () => { const { triggerQuickSearchModal } = useModal(); const [showSubFavorite, setShowSubFavorite] = useState(true); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { openPage, createPage } = usePageHelper(); const router = useRouter(); const { t } = useTranslation(); diff --git a/apps/web/src/hooks/use-change-page-meta.ts b/apps/web/src/hooks/use-change-page-meta.ts index a01b7f1868..482b646562 100644 --- a/apps/web/src/hooks/use-change-page-meta.ts +++ b/apps/web/src/hooks/use-change-page-meta.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react'; -import { useAppState, PageMeta } from '@/providers/app-state-provider'; +import { PageMeta } from '@/providers/app-state-provider'; +import { useGlobalState } from '@/store/app'; export type ChangePageMeta = ( pageId: string, @@ -7,7 +8,9 @@ export type ChangePageMeta = ( ) => void; export const useChangePageMeta = () => { - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); return useCallback( (pageId, pageMeta) => { diff --git a/apps/web/src/hooks/use-ensure-workspace.ts b/apps/web/src/hooks/use-ensure-workspace.ts index 473787d057..b112f18143 100644 --- a/apps/web/src/hooks/use-ensure-workspace.ts +++ b/apps/web/src/hooks/use-ensure-workspace.ts @@ -1,35 +1,42 @@ -import { useAppState } from '@/providers/app-state-provider'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useGlobalState } from '@/store/app'; +import { assertEquals } from '@blocksuite/global/utils'; + +// todo: refactor with suspense mode // It is a fully effective hook // Cause it not just ensure workspace loaded, but also have router change. export const useEnsureWorkspace = () => { - const [workspaceLoaded, setWorkspaceLoaded] = useState(false); - const dataCenter = useGlobalState(store => store.dataCenter); - const { loadWorkspace } = useAppState(); + const dataCenter = useGlobalState(useCallback(store => store.dataCenter, [])); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); + const loadWorkspace = useGlobalState( + useCallback(store => store.loadWorkspace, []) + ); const router = useRouter(); - const [activeWorkspaceId, setActiveWorkspaceId] = useState( - router.query.workspaceId as string + const [currentWorkspaceId, setCurrentWorkspaceId] = useState( + typeof router.query.workspaceId === 'string' + ? router.query.workspaceId + : null ); // const defaultOutLineWorkspaceId = '99ce7eb7'; // console.log(defaultOutLineWorkspaceId); useEffect(() => { - setWorkspaceLoaded(false); - let aborted = false; const abortController = new AbortController(); - const workspaceList = dataCenter.workspaces; const workspaceId = - (router.query.workspaceId as string) || workspaceList[0]?.id; + (router.query.workspaceId as string) || dataCenter.workspaces[0]?.id; // If router.query.workspaceId is not in workspace list, jump to 404 page // If workspaceList is empty, we need to create a default workspace but not jump to 404 if ( workspaceId && - workspaceList.length && - workspaceList.findIndex(meta => meta.id.toString() === workspaceId) === -1 + dataCenter.workspaces.length && + dataCenter.workspaces.findIndex( + meta => meta.id.toString() === workspaceId + ) === -1 ) { router.push('/404'); return; @@ -44,22 +51,21 @@ export const useEnsureWorkspace = () => { // return; // } - loadWorkspace.current(workspaceId, abortController.signal).then(unit => { - if (!aborted && unit) { - setWorkspaceLoaded(true); - setActiveWorkspaceId(workspaceId); + loadWorkspace(workspaceId, abortController.signal).then(unit => { + if (!abortController.signal.aborted && unit) { + setCurrentWorkspaceId(unit.id); + assertEquals(unit.id, workspaceId); } }); return () => { - aborted = true; abortController.abort(); }; }, [dataCenter, loadWorkspace, router]); return { - workspaceLoaded, - activeWorkspaceId, + workspaceLoaded: currentWorkspace?.id === currentWorkspaceId, + activeWorkspaceId: currentWorkspace?.id ?? router.query.workspaceId, }; }; diff --git a/apps/web/src/hooks/use-members.ts b/apps/web/src/hooks/use-members.ts index 5869840cb0..96304aeb21 100644 --- a/apps/web/src/hooks/use-members.ts +++ b/apps/web/src/hooks/use-members.ts @@ -1,10 +1,11 @@ import { useCallback, useEffect, useState } from 'react'; import { Member } from '@affine/datacenter'; -import { useAppState } from '@/providers/app-state-provider'; import { useGlobalState } from '@/store/app'; export const useMembers = () => { const dataCenter = useGlobalState(store => store.dataCenter); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const [members, setMembers] = useState([]); const [loaded, setLoaded] = useState(false); const refreshMembers = useCallback(async () => { diff --git a/apps/web/src/hooks/use-page-helper.ts b/apps/web/src/hooks/use-page-helper.ts index 05085974a9..dc92c7e198 100644 --- a/apps/web/src/hooks/use-page-helper.ts +++ b/apps/web/src/hooks/use-page-helper.ts @@ -1,11 +1,12 @@ import { uuidv4, Workspace } from '@blocksuite/store'; import { QueryContent } from '@blocksuite/store/dist/workspace/search'; -import { PageMeta, useAppState } from '@/providers/app-state-provider'; +import { PageMeta } from '@/providers/app-state-provider'; import { EditorContainer } from '@blocksuite/editor'; import { useChangePageMeta } from '@/hooks/use-change-page-meta'; import { useRouter } from 'next/router'; import { WorkspaceUnit } from '@affine/datacenter'; import { useGlobalState } from '@/store/app'; +import { useCallback } from 'react'; export type EditorHandlers = { createPage: (params?: { @@ -41,7 +42,9 @@ export const usePageHelper = (): EditorHandlers => { const router = useRouter(); const changePageMeta = useChangePageMeta(); const editor = useGlobalState(store => store.editor); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); return { createPage: ({ diff --git a/apps/web/src/hooks/use-workspace-helper.ts b/apps/web/src/hooks/use-workspace-helper.ts index e69e8dc8c4..79c5fffb0f 100644 --- a/apps/web/src/hooks/use-workspace-helper.ts +++ b/apps/web/src/hooks/use-workspace-helper.ts @@ -1,16 +1,19 @@ -import { useAppState } from '@/providers/app-state-provider'; import { WorkspaceUnit } from '@affine/datacenter'; import { useGlobalState } from '@/store/app'; +import { useCallback } from 'react'; export const useWorkspaceHelper = () => { const dataCenter = useGlobalState(store => store.dataCenter); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); + const loadWorkspace = useGlobalState(store => store.loadWorkspace); const createWorkspace = async (name: string) => { const workspaceInfo = await dataCenter.createWorkspace({ name: name, }); if (workspaceInfo && workspaceInfo.id) { - return await dataCenter.loadWorkspace(workspaceInfo.id); + return loadWorkspace(workspaceInfo.id); } return null; }; diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index 77f360a433..dec67e6cb8 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -22,7 +22,8 @@ import Head from 'next/head'; import '@affine/i18n'; import { useTranslation } from '@affine/i18n'; import React from 'react'; -import { DataCenterLoader, GlobalAppProvider } from '@/store/app'; +import { GlobalAppProvider } from '@/store/app'; +import { DataCenterLoader } from '@/store/app/datacenter'; const ThemeProvider = dynamic(() => import('@/providers/ThemeProvider'), { ssr: false, diff --git a/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx b/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx index 4cfe46f257..94d8e9cfb2 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx @@ -1,7 +1,12 @@ -import { PropsWithChildren, ReactElement, useEffect, useState } from 'react'; +import { + PropsWithChildren, + ReactElement, + useCallback, + useEffect, + useState, +} from 'react'; import { EditorHeader } from '@/components/header'; import MobileModal from '@/components/mobile-modal'; -import { useAppState } from '@/providers/app-state-provider'; import type { NextPageWithLayout } from '../..//_app'; import WorkspaceLayout from '@/components/workspace-layout'; import { useRouter } from 'next/router'; @@ -53,7 +58,9 @@ const BlockHubAppender = () => { const Page: NextPageWithLayout = () => { const currentPage = useGlobalState(store => store.currentPage); const setEditor = useGlobalState(store => store.setEditor); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { t } = useTranslation(); @@ -95,7 +102,9 @@ const PageDefender = ({ children }: PropsWithChildren) => { const router = useRouter(); const [pageLoaded, setPageLoaded] = useState(false); const loadPage = useGlobalState(store => store.loadPage); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { createPage } = usePageHelper(); useEffect(() => { diff --git a/apps/web/src/pages/workspace/[workspaceId]/all.tsx b/apps/web/src/pages/workspace/[workspaceId]/all.tsx index 72ad34c485..3b906fb09f 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/all.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/all.tsx @@ -1,14 +1,17 @@ import { PageList } from '@/components/page-list'; import { AllPagesIcon } from '@blocksuite/icons'; import { PageListHeader } from '@/components/header'; -import { ReactElement } from 'react'; +import { ReactElement, useCallback } from 'react'; import WorkspaceLayout from '@/components/workspace-layout'; import { useTranslation } from '@affine/i18n'; -import { PageMeta, useAppState } from '@/providers/app-state-provider'; +import { PageMeta } from '@/providers/app-state-provider'; import Head from 'next/head'; +import { useGlobalState } from '@/store/app'; const All = () => { - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const pageList = (currentWorkspace?.blocksuiteWorkspace?.meta.pageMetas || []) as PageMeta[]; const { t } = useTranslation(); diff --git a/apps/web/src/pages/workspace/[workspaceId]/favorite.tsx b/apps/web/src/pages/workspace/[workspaceId]/favorite.tsx index ebab1134f8..0063474b92 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/favorite.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/favorite.tsx @@ -4,10 +4,11 @@ import { FavouritesIcon } from '@blocksuite/icons'; import { ReactElement } from 'react'; import WorkspaceLayout from '@/components/workspace-layout'; import { useTranslation } from '@affine/i18n'; -import { useAppState } from '@/providers/app-state-provider'; import Head from 'next/head'; +import { useGlobalState } from '@/store/app'; + export const Favorite = () => { - const { pageList } = useAppState(); + const pageList = useGlobalState(store => store.dataCenterPageList); const { t } = useTranslation(); return ( <> diff --git a/apps/web/src/pages/workspace/[workspaceId]/index.tsx b/apps/web/src/pages/workspace/[workspaceId]/index.tsx index f946e29267..5d4bc7101b 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/index.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/index.tsx @@ -1,13 +1,15 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useRouter } from 'next/router'; -import { useAppState } from '@/providers/app-state-provider'; import useEnsureWorkspace from '@/hooks/use-ensure-workspace'; import { PageLoading } from '@/components/loading'; import usePageHelper from '@/hooks/use-page-helper'; +import { useGlobalState } from '@/store/app'; const WorkspaceIndex = () => { const router = useRouter(); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { createPage } = usePageHelper(); const { workspaceLoaded, activeWorkspaceId } = useEnsureWorkspace(); diff --git a/apps/web/src/pages/workspace/[workspaceId]/setting.tsx b/apps/web/src/pages/workspace/[workspaceId]/setting.tsx index 9ddd114fdf..e650db1995 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/setting.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/setting.tsx @@ -10,6 +10,7 @@ import { CSSProperties, useEffect, startTransition, + useCallback, } from 'react'; import { GeneralPage, @@ -18,7 +19,6 @@ import { ExportPage, } from '@/components/workspace-setting'; import { SettingsIcon } from '@blocksuite/icons'; -import { useAppState } from '@/providers/app-state-provider'; import WorkspaceLayout from '@/components/workspace-layout'; import { WorkspaceUnit } from '@affine/datacenter'; import { useTranslation } from '@affine/i18n'; @@ -98,7 +98,9 @@ const StyledTabButtonWrapper = styled.div(() => { }); const WorkspaceSetting = () => { const { t } = useTranslation(); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { activeTabPanelRender, tableArr, handleTabChange, activeTab } = useTabMap(); const [indicatorState, setIndicatorState] = useState< diff --git a/apps/web/src/pages/workspace/[workspaceId]/trash.tsx b/apps/web/src/pages/workspace/[workspaceId]/trash.tsx index 10c786c4fc..58008d5aa2 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/trash.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/trash.tsx @@ -4,10 +4,10 @@ import { TrashIcon } from '@blocksuite/icons'; import { ReactElement } from 'react'; import WorkspaceLayout from '@/components/workspace-layout'; import { useTranslation } from '@affine/i18n'; -import { useAppState } from '@/providers/app-state-provider'; import Head from 'next/head'; +import { useGlobalState } from '@/store/app'; export const Trash = () => { - const { pageList } = useAppState(); + const pageList = useGlobalState(store => store.dataCenterPageList); const { t } = useTranslation(); return ( <> diff --git a/apps/web/src/pages/workspace/index.tsx b/apps/web/src/pages/workspace/index.tsx index ec96879e5a..dacc2ab4fd 100644 --- a/apps/web/src/pages/workspace/index.tsx +++ b/apps/web/src/pages/workspace/index.tsx @@ -1,12 +1,14 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useRouter } from 'next/router'; -import { useAppState } from '@/providers/app-state-provider'; import useEnsureWorkspace from '@/hooks/use-ensure-workspace'; import { PageLoading } from '@/components/loading'; +import { useGlobalState } from '@/store/app'; export const WorkspaceIndex = () => { const router = useRouter(); - const { currentWorkspace } = useAppState(); + const currentWorkspace = useGlobalState( + useCallback(store => store.currentDataCenterWorkspace, []) + ); const { workspaceLoaded } = useEnsureWorkspace(); useEffect(() => { diff --git a/apps/web/src/providers/app-state-provider/Provider.tsx b/apps/web/src/providers/app-state-provider/Provider.tsx index 80b42e4d30..9c0979b203 100644 --- a/apps/web/src/providers/app-state-provider/Provider.tsx +++ b/apps/web/src/providers/app-state-provider/Provider.tsx @@ -1,16 +1,8 @@ -import { createContext, useContext, useEffect, useState, useRef } from 'react'; +import { createContext, useContext, useEffect, useState } from 'react'; import type { PropsWithChildren } from 'react'; -import { - AppStateContext, - AppStateFunction, - AppStateValue, - PageMeta, -} from './interface'; -import { useGlobalState, useGlobalStateApi } from '@/store/app'; - -export interface Disposable { - dispose(): void; -} +import { AppStateContext } from './interface'; +import type { Disposable } from '@blocksuite/global/utils'; +import { useGlobalState } from '@/store/app'; type AppStateContextProps = PropsWithChildren>; @@ -20,113 +12,14 @@ export const useAppState = () => useContext(AppState); export const AppStateProvider = ({ children, }: PropsWithChildren) => { - const globalStateApi = useGlobalStateApi(); - const [appState, setAppState] = useState({} as AppStateValue); + const currentDataCenterWorkspace = useGlobalState( + store => store.currentDataCenterWorkspace + ); const [blobState, setBlobState] = useState(false); - useEffect(() => { - if (!appState?.currentWorkspace?.blocksuiteWorkspace) { - return; - } - const currentWorkspace = appState.currentWorkspace; - const dispose = currentWorkspace?.blocksuiteWorkspace?.meta.pagesUpdated.on( - () => { - setAppState({ - ...appState, - pageList: currentWorkspace.blocksuiteWorkspace?.meta - .pageMetas as PageMeta[], - }); - } - ).dispose; - return () => { - dispose && dispose(); - }; - }, [appState]); - - const onceRef = useRef(true); - const dataCenter = useGlobalState(store => store.dataCenter); - if (onceRef.current && dataCenter) { - setAppState({ - workspaceList: dataCenter.workspaces, - currentWorkspace: null, - pageList: [], - }); - onceRef.current = false; - } - - useEffect(() => { - // FIXME: onWorkspacesChange should have dispose function - return dataCenter?.onWorkspacesChange( - () => { - setAppState(_appState => ({ - ..._appState, - workspaceList: dataCenter.workspaces, - })); - }, - { immediate: false } - ); - }, [dataCenter]); - - const loadWorkspace: AppStateFunction['loadWorkspace'] = - useRef() as AppStateFunction['loadWorkspace']; - loadWorkspace.current = async (workspaceId, abort) => { - const { dataCenter } = globalStateApi.getState(); - const { workspaceList, currentWorkspace } = appState; - if (!workspaceList.find(v => v.id.toString() === workspaceId)) { - return null; - } - if (workspaceId === currentWorkspace?.id) { - return currentWorkspace; - } - - let aborted = false; - - const onAbort = () => { - aborted = true; - }; - - abort?.addEventListener('abort', onAbort); - - const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null; - - if (aborted) { - // do not update state if aborted - return null; - } - - let isOwner; - if (workspace?.provider === 'local') { - // isOwner is useful only in the cloud - isOwner = true; - } else { - const userInfo = globalStateApi.getState().user; - // We must ensure workspace.owner exists, then ensure id same. - isOwner = workspace?.owner && userInfo?.id === workspace.owner.id; - } - - const pageList = - (workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? []; - if (workspace?.blocksuiteWorkspace) { - globalStateApi.getState().setWorkspace(workspace.blocksuiteWorkspace); - } - globalStateApi.setState({ - isOwner, - }); - - setAppState({ - ...appState, - currentWorkspace: workspace, - pageList: pageList, - }); - - abort?.removeEventListener('abort', onAbort); - - return workspace; - }; - useEffect(() => { let syncChangeDisposable: Disposable | undefined; - const currentWorkspace = appState.currentWorkspace; + const currentWorkspace = currentDataCenterWorkspace; if (!currentWorkspace) { return; } @@ -142,13 +35,11 @@ export const AppStateProvider = ({ return () => { syncChangeDisposable?.dispose(); }; - }, [appState.currentWorkspace]); + }, [currentDataCenterWorkspace]); return ( diff --git a/apps/web/src/providers/app-state-provider/interface.ts b/apps/web/src/providers/app-state-provider/interface.ts index 1cd54018f2..cf484fa34d 100644 --- a/apps/web/src/providers/app-state-provider/interface.ts +++ b/apps/web/src/providers/app-state-provider/interface.ts @@ -1,11 +1,10 @@ -import { WorkspaceUnit } from '@affine/datacenter'; import type { EditorContainer } from '@blocksuite/editor'; import type { Page as StorePage, PageMeta as StorePageMeta, } from '@blocksuite/store'; -import { MutableRefObject } from 'react'; + export interface PageMeta extends StorePageMeta { favorite: boolean; trash: boolean; @@ -15,16 +14,14 @@ export interface PageMeta extends StorePageMeta { } export type AppStateValue = { - workspaceList: WorkspaceUnit[]; - currentWorkspace: WorkspaceUnit | null; - pageList: PageMeta[]; - blobDataSynced?: boolean; + blobDataSynced: boolean; }; +/** + * @deprecated + */ export type AppStateFunction = { - loadWorkspace: MutableRefObject< - (workspaceId: string, abort?: AbortSignal) => Promise - >; + // todo: remove this in the future }; export type AppStateContext = AppStateValue & AppStateFunction; diff --git a/apps/web/src/store/app/datacenter/index.ts b/apps/web/src/store/app/datacenter/index.ts new file mode 100644 index 0000000000..5027256a89 --- /dev/null +++ b/apps/web/src/store/app/datacenter/index.ts @@ -0,0 +1,139 @@ +import { + GlobalActionsCreator, + useGlobalState, + useGlobalStateApi, +} from '@/store/app'; +import type { DataCenter } from '@affine/datacenter'; +import { PageMeta } from '@/providers/app-state-provider'; +import { getDataCenter, WorkspaceUnit } from '@affine/datacenter'; +import { createDefaultWorkspace } from '@/providers/app-state-provider/utils'; +import { useCallback, useEffect } from 'react'; +import { DisposableGroup } from '@blocksuite/global/utils'; + +export type DataCenterState = { + readonly dataCenter: DataCenter; + readonly dataCenterPromise: Promise; + currentDataCenterWorkspace: WorkspaceUnit | null; + dataCenterPageList: PageMeta[]; +}; + +export type DataCenterActions = { + loadWorkspace: ( + workspaceId: string, + signal?: AbortSignal + ) => Promise; +}; + +export const createDataCenterState = (): DataCenterState => ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + dataCenter: null!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + dataCenterPromise: null!, + currentDataCenterWorkspace: null, + dataCenterPageList: [], +}); +export const createDataCenterActions: GlobalActionsCreator< + DataCenterActions +> = (set, get) => ({ + loadWorkspace: async (workspaceId, signal) => { + const { dataCenter, currentDataCenterWorkspace } = get(); + if (!dataCenter.workspaces.find(v => v.id.toString() === workspaceId)) { + return null; + } + if (workspaceId === currentDataCenterWorkspace?.id) { + return currentDataCenterWorkspace; + } + const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null; + + if (signal?.aborted) { + // do not update state if aborted + return null; + } + + let isOwner; + if (workspace?.provider === 'local') { + // isOwner is useful only in the cloud + isOwner = true; + } else { + const userInfo = get().user; + // We must ensure workspace.owner exists, then ensure id same. + isOwner = workspace?.owner && userInfo?.id === workspace.owner.id; + } + + const pageList = + (workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? []; + if (workspace?.blocksuiteWorkspace) { + set({ + currentWorkspace: workspace.blocksuiteWorkspace, + }); + } + set({ + isOwner, + }); + + set({ + currentDataCenterWorkspace: workspace, + dataCenterPageList: pageList, + }); + + return workspace; + }, +}); + +export function DataCenterLoader() { + const dataCenter = useGlobalState(useCallback(store => store.dataCenter, [])); + const dataCenterPromise = useGlobalState( + useCallback(store => store.dataCenterPromise, []) + ); + const api = useGlobalStateApi(); + //# region effect for updating workspace page list + useEffect(() => { + return api.subscribe( + store => store.currentDataCenterWorkspace, + currentWorkspace => { + const disposableGroup = new DisposableGroup(); + disposableGroup.add( + currentWorkspace?.blocksuiteWorkspace?.meta.pagesUpdated.on(() => { + if ( + Array.isArray( + currentWorkspace.blocksuiteWorkspace?.meta.pageMetas + ) + ) { + api.setState({ + dataCenterPageList: currentWorkspace.blocksuiteWorkspace?.meta + .pageMetas as PageMeta[], + }); + } + }) + ); + return () => { + disposableGroup.dispose(); + }; + } + ); + }, [api]); + //# endregion + + if (!dataCenter && !dataCenterPromise) { + const promise = getDataCenter(); + api.setState({ dataCenterPromise: promise }); + promise.then(async dataCenter => { + // Ensure datacenter has at least one workspace + if (dataCenter.workspaces.length === 0) { + await createDefaultWorkspace(dataCenter); + } + // set initial state + api.setState({ + dataCenter, + currentWorkspace: null, + currentDataCenterWorkspace: null, + dataCenterPageList: [], + }); + }); + throw promise; + } + if (!dataCenter) { + throw dataCenterPromise; + } + return null; +} diff --git a/apps/web/src/store/app/index.tsx b/apps/web/src/store/app/index.tsx index 8ea54ed60f..65fffcbf29 100644 --- a/apps/web/src/store/app/index.tsx +++ b/apps/web/src/store/app/index.tsx @@ -15,8 +15,12 @@ import { UserActions, UserState, } from '@/store/app/user'; -import { DataCenter, getDataCenter } from '@affine/datacenter'; -import { createDefaultWorkspace } from '@/providers/app-state-provider/utils'; +import { + createDataCenterActions, + createDataCenterState, + DataCenterActions, + DataCenterState, +} from '@/store/app/datacenter'; export type GlobalActionsCreator = StateCreator< Store, @@ -25,12 +29,15 @@ export type GlobalActionsCreator = StateCreator< Actions >; -export interface GlobalState extends BlockSuiteState, UserState { - readonly dataCenter: DataCenter; - readonly dataCenterPromise: Promise; -} +export interface GlobalState + extends BlockSuiteState, + UserState, + DataCenterState {} -export interface GlobalActions extends BlockSuiteActions, UserActions {} +export interface GlobalActions + extends BlockSuiteActions, + UserActions, + DataCenterActions {} const create = () => createStore( @@ -39,15 +46,13 @@ const create = () => { ...createBlockSuiteState(), ...createUserState(), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - dataCenter: null!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - dataCenterPromise: null!, + ...createDataCenterState(), }, /* deepscan-disable TOO_MANY_ARGS */ (set, get, api) => ({ ...createBlockSuiteActions(set, get, api), ...createUserActions(set, get, api), + ...createDataCenterActions(set, get, api), }) /* deepscan-enable TOO_MANY_ARGS */ ) @@ -74,28 +79,6 @@ export const useGlobalState: UseBoundStore = (( // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any; -export function DataCenterLoader() { - const dataCenter = useGlobalState(store => store.dataCenter); - const dataCenterPromise = useGlobalState(store => store.dataCenterPromise); - const api = useGlobalStateApi(); - if (!dataCenter && !dataCenterPromise) { - const promise = getDataCenter(); - api.setState({ dataCenterPromise: promise }); - promise.then(async dataCenter => { - // Ensure datacenter has at least one workspace - if (dataCenter.workspaces.length === 0) { - await createDefaultWorkspace(dataCenter); - } - api.setState({ dataCenter }); - }); - throw promise; - } - if (!dataCenter) { - throw dataCenterPromise; - } - return null; -} - export const GlobalAppProvider: React.FC = function ModelProvider({ children }) { return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea2c7cd98d..4eeef07db5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,6 +164,7 @@ importers: '@affine/i18n': workspace:* '@blocksuite/blocks': 0.4.0-20230216011811-2776d93 '@blocksuite/editor': 0.4.0-20230216011811-2776d93 + '@blocksuite/global': 0.4.0-20230216011811-2776d93 '@blocksuite/icons': ^2.0.14 '@blocksuite/store': 0.4.0-20230216011811-2776d93 '@emotion/css': ^11.10.5 @@ -207,6 +208,7 @@ importers: '@affine/i18n': link:../../packages/i18n '@blocksuite/blocks': 0.4.0-20230216011811-2776d93_trq6tjva7swiv4fhloorah5nty '@blocksuite/editor': 0.4.0-20230216011811-2776d93_6yfgczikbxxddpvfspgrnx2z2q + '@blocksuite/global': 0.4.0-20230216011811-2776d93_lit@2.6.1 '@blocksuite/icons': 2.0.14_w5j4k42lgipnm43s3brx6h3c34 '@blocksuite/store': 0.4.0-20230216011811-2776d93_lit@2.6.1+yjs@13.5.45 '@emotion/css': 11.10.5