diff --git a/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-helper.spec.ts b/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-helper.spec.ts deleted file mode 100644 index b9a315a06b60b..0000000000000 --- a/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-helper.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @vitest-environment happy-dom - */ -import 'fake-indexeddb/auto'; - -import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; -import { Schema, Workspace } from '@blocksuite/store'; -import { renderHook } from '@testing-library/react'; -import { initEmptyPage } from '@toeverything/infra/blocksuite'; -import { beforeEach, describe, expect, test } from 'vitest'; - -import { useBlockSuitePageMeta } from '../use-block-suite-page-meta'; -import { useBlockSuiteWorkspaceHelper } from '../use-block-suite-workspace-helper'; - -let blockSuiteWorkspace: Workspace; - -const schema = new Schema(); -schema.register(AffineSchemas).register(__unstableSchemas); - -beforeEach(async () => { - blockSuiteWorkspace = new Workspace({ - id: 'test', - schema, - }); - initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page0' })); - initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page1' })); - initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page2' })); -}); - -describe('useBlockSuiteWorkspaceHelper', () => { - test('should create page', () => { - expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3); - const helperHook = renderHook(() => - useBlockSuiteWorkspaceHelper(blockSuiteWorkspace) - ); - const pageMetaHook = renderHook(() => - useBlockSuitePageMeta(blockSuiteWorkspace) - ); - expect(pageMetaHook.result.current.length).toBe(3); - expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3); - const page = helperHook.result.current.createPage('page4'); - expect(page.id).toBe('page4'); - expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(4); - pageMetaHook.rerender(); - expect(pageMetaHook.result.current.length).toBe(4); - }); -}); diff --git a/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-helper.spec.tsx b/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-helper.spec.tsx new file mode 100644 index 0000000000000..33d470807f8ee --- /dev/null +++ b/packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-helper.spec.tsx @@ -0,0 +1,68 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import { configureTestingEnvironment } from '@affine/core/testing'; +import { renderHook } from '@testing-library/react'; +import type { Workspace } from '@toeverything/infra'; +import { initEmptyPage, ServiceProviderContext } from '@toeverything/infra'; +import type { PropsWithChildren } from 'react'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +import { useBlockSuitePageMeta } from '../use-block-suite-page-meta'; +import { useBlockSuiteWorkspaceHelper } from '../use-block-suite-workspace-helper'; + +const configureTestingWorkspace = async () => { + const { workspace } = await configureTestingEnvironment(); + const blockSuiteWorkspace = workspace.blockSuiteWorkspace; + + // await initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page-0' })); + initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page1' })); + initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page2' })); + + return workspace; +}; + +beforeEach(async () => { + vi.useFakeTimers({ toFake: ['requestIdleCallback'] }); +}); + +const getWrapper = (workspace: Workspace) => + function Provider({ children }: PropsWithChildren) { + return ( + + {children} + + ); + }; + +describe('useBlockSuiteWorkspaceHelper', () => { + test('should create page', async () => { + const workspace = await configureTestingWorkspace(); + const blockSuiteWorkspace = workspace.blockSuiteWorkspace; + const Wrapper = getWrapper(workspace); + + expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3); + const helperHook = renderHook( + () => useBlockSuiteWorkspaceHelper(blockSuiteWorkspace), + { + wrapper: Wrapper, + } + ); + const pageMetaHook = renderHook( + () => useBlockSuitePageMeta(blockSuiteWorkspace), + { + wrapper: Wrapper, + } + ); + await new Promise(resolve => setTimeout(resolve)); + expect(pageMetaHook.result.current.length).toBe(3); + expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3); + const page = helperHook.result.current.createPage('page4'); + expect(page.id).toBe('page4'); + expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(4); + pageMetaHook.rerender(); + expect(pageMetaHook.result.current.length).toBe(4); + }); +}); diff --git a/packages/frontend/core/src/hooks/use-affine-adapter.ts b/packages/frontend/core/src/hooks/use-affine-adapter.ts index 6d84816dca63f..ed826996dcf20 100644 --- a/packages/frontend/core/src/hooks/use-affine-adapter.ts +++ b/packages/frontend/core/src/hooks/use-affine-adapter.ts @@ -4,7 +4,7 @@ import { use } from 'foxact/use'; import { useEffect, useMemo, useState } from 'react'; import { WorkspacePropertiesAdapter } from '../modules/workspace/properties'; -import { useBlockSuitePageMeta } from './use-block-suite-page-meta'; +import { useAllBlockSuitePageMeta } from './use-all-block-suite-page-meta'; function getProxy(obj: T) { return new Proxy(obj, {}); @@ -14,7 +14,7 @@ const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => { use(adapter.workspace.blockSuiteWorkspace.doc.whenSynced); const [proxy, setProxy] = useState(adapter); // fixme: this is a hack to force re-render when default meta changed - useBlockSuitePageMeta(adapter.workspace.blockSuiteWorkspace); + useAllBlockSuitePageMeta(adapter.workspace.blockSuiteWorkspace); useEffect(() => { // todo: track which properties are used and then filter by property path change // using Y.YEvent.path diff --git a/packages/frontend/core/src/hooks/use-all-block-suite-page-meta.ts b/packages/frontend/core/src/hooks/use-all-block-suite-page-meta.ts new file mode 100644 index 0000000000000..3869ce85fd478 --- /dev/null +++ b/packages/frontend/core/src/hooks/use-all-block-suite-page-meta.ts @@ -0,0 +1,25 @@ +import type { PageMeta, Workspace } from '@blocksuite/store'; +import type { Atom } from 'jotai'; +import { atom, useAtomValue } from 'jotai'; + +const weakMap = new WeakMap>(); + +// this hook is extracted from './use-block-suite-page-meta.ts' to avoid circular dependency +export function useAllBlockSuitePageMeta( + blockSuiteWorkspace: Workspace +): PageMeta[] { + if (!weakMap.has(blockSuiteWorkspace)) { + const baseAtom = atom(blockSuiteWorkspace.meta.pageMetas); + weakMap.set(blockSuiteWorkspace, baseAtom); + baseAtom.onMount = set => { + set(blockSuiteWorkspace.meta.pageMetas); + const dispose = blockSuiteWorkspace.meta.pageMetasUpdated.on(() => { + set(blockSuiteWorkspace.meta.pageMetas); + }); + return () => { + dispose.dispose(); + }; + }; + } + return useAtomValue(weakMap.get(blockSuiteWorkspace) as Atom); +} diff --git a/packages/frontend/core/src/hooks/use-block-suite-page-meta.ts b/packages/frontend/core/src/hooks/use-block-suite-page-meta.ts index 3ea236a54dcc6..06fd7d2a8cbc5 100644 --- a/packages/frontend/core/src/hooks/use-block-suite-page-meta.ts +++ b/packages/frontend/core/src/hooks/use-block-suite-page-meta.ts @@ -1,29 +1,26 @@ import type { PageBlockModel } from '@blocksuite/blocks'; import { assertExists } from '@blocksuite/global/utils'; import type { PageMeta, Workspace } from '@blocksuite/store'; -import type { Atom } from 'jotai'; -import { atom, useAtomValue } from 'jotai'; import { useMemo } from 'react'; -const weakMap = new WeakMap>(); +import { useAllBlockSuitePageMeta } from './use-all-block-suite-page-meta'; +import { useJournalHelper } from './use-journal'; -export function useBlockSuitePageMeta( - blockSuiteWorkspace: Workspace -): PageMeta[] { - if (!weakMap.has(blockSuiteWorkspace)) { - const baseAtom = atom(blockSuiteWorkspace.meta.pageMetas); - weakMap.set(blockSuiteWorkspace, baseAtom); - baseAtom.onMount = set => { - set(blockSuiteWorkspace.meta.pageMetas); - const dispose = blockSuiteWorkspace.meta.pageMetasUpdated.on(() => { - set(blockSuiteWorkspace.meta.pageMetas); - }); - return () => { - dispose.dispose(); - }; - }; - } - return useAtomValue(weakMap.get(blockSuiteWorkspace) as Atom); +/** + * Get pageMetas excluding journal pages without updatedDate + * If you want to get all pageMetas, use `useAllBlockSuitePageMeta` instead + * @returns + */ +export function useBlockSuitePageMeta(blocksuiteWorkspace: Workspace) { + const pageMetas = useAllBlockSuitePageMeta(blocksuiteWorkspace); + const { isPageJournal } = useJournalHelper(blocksuiteWorkspace); + return useMemo( + () => + pageMetas.filter( + pageMeta => !isPageJournal(pageMeta.id) || !!pageMeta.updatedDate + ), + [isPageJournal, pageMetas] + ); } export function usePageMetaHelper(blockSuiteWorkspace: Workspace) { diff --git a/packages/frontend/core/src/pages/index.tsx b/packages/frontend/core/src/pages/index.tsx index ad3f2055e4708..98efe6e0af333 100644 --- a/packages/frontend/core/src/pages/index.tsx +++ b/packages/frontend/core/src/pages/index.tsx @@ -32,19 +32,22 @@ export const Component = () => { const [creating, setCreating] = useState(false); const list = useLiveData(useService(WorkspaceListService).workspaceList); + const workspaceStatus = useLiveData(useService(WorkspaceListService).status); + const { openPage } = useNavigateHelper(); useLayoutEffect(() => { - if (list.length === 0) { + if (list.length === 0 || workspaceStatus.loading) { return; } // open last workspace const lastId = localStorage.getItem('last_workspace_id'); + const openWorkspace = list.find(w => w.id === lastId) ?? list[0]; openPage(openWorkspace.id, WorkspaceSubPath.ALL); setNavigating(true); - }, [list, openPage]); + }, [list, workspaceStatus, openPage]); const workspaceManager = useService(WorkspaceManager); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.tsx index 26da3d69aa747..d5857732da319 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/journal.tsx @@ -7,6 +7,7 @@ import { } from '@affine/component'; import { MoveToTrash } from '@affine/core/components/page-list'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; +import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title'; import { useJournalHelper, @@ -14,6 +15,7 @@ import { useJournalRouteHelper, } from '@affine/core/hooks/use-journal'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; +import type { BlockSuiteWorkspace } from '@affine/core/shared'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { EdgelessIcon, @@ -21,7 +23,7 @@ import { PageIcon, TodayIcon, } from '@blocksuite/icons'; -import type { Page } from '@blocksuite/store'; +import type { Page, PageMeta } from '@blocksuite/store'; import { assignInlineVars } from '@vanilla-extract/dynamic'; import clsx from 'clsx'; import dayjs from 'dayjs'; @@ -42,21 +44,28 @@ const CountDisplay = ({ return {count > max ? `${max}+` : count}; }; interface PageItemProps extends HTMLAttributes { - page: Page; + pageMeta: PageMeta; + workspace: BlockSuiteWorkspace; right?: ReactNode; } -const PageItem = ({ page, right, className, ...attrs }: PageItemProps) => { - const { isJournal } = useJournalInfoHelper(page.workspace, page.id); - const title = useBlockSuiteWorkspacePageTitle(page.workspace, page.id); +const PageItem = ({ + pageMeta, + workspace, + right, + className, + ...attrs +}: PageItemProps) => { + const { isJournal } = useJournalInfoHelper(workspace, pageMeta.id); + const title = useBlockSuiteWorkspacePageTitle(workspace, pageMeta.id); const Icon = isJournal ? TodayIcon - : page.meta.mode === 'edgeless' + : pageMeta.mode === 'edgeless' ? EdgelessIcon : PageIcon; return (
@@ -147,15 +156,12 @@ const EditorJournalPanel = (props: EditorExtensionProps) => { }; const sortPagesByDate = ( - pages: Page[], + pages: PageMeta[], field: 'updatedDate' | 'createDate', order: 'asc' | 'desc' = 'desc' ) => { return [...pages].sort((a, b) => { - return ( - (order === 'asc' ? 1 : -1) * - dayjs(b.meta[field]).diff(dayjs(a.meta[field])) - ); + return (order === 'asc' ? 1 : -1) * dayjs(b[field]).diff(dayjs(a[field])); }); }; @@ -174,21 +180,21 @@ const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => { const nodeRef = useRef(null); const t = useAFFiNEI18N(); const [activeItem, setActiveItem] = useState('createdToday'); + const pageMetas = useBlockSuitePageMeta(workspace); const navigateHelper = useNavigateHelper(); const getTodaysPages = useCallback( (field: 'createDate' | 'updatedDate') => { - const pages: Page[] = []; - Array.from(workspace.pages.values()).forEach(page => { - if (page.meta.trash) return; - if (page.meta[field] && dayjs(page.meta[field]).isSame(date, 'day')) { - pages.push(page); - } - }); - return sortPagesByDate(pages, field); + return sortPagesByDate( + pageMetas.filter(pageMeta => { + if (pageMeta.trash) return false; + return pageMeta[field] && dayjs(pageMeta[field]).isSame(date, 'day'); + }), + field + ); }, - [date, workspace.pages] + [date, pageMetas] ); const createdToday = useMemo( @@ -257,14 +263,15 @@ const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => {
- {renderList.map((page, index) => ( + {renderList.map((pageMeta, index) => ( - navigateHelper.openPage(workspace.id, page.id) + navigateHelper.openPage(workspace.id, pageMeta.id) } tabIndex={name === activeItem ? 0 : -1} key={index} - page={page} + pageMeta={pageMeta} + workspace={workspace} /> ))}
@@ -315,7 +322,8 @@ const ConflictList = ({