feat: add recentlyViewed (#1357)

Co-authored-by: himself65 <himself65@outlook.com>
This commit is contained in:
JimmFly
2023-03-07 02:02:50 +08:00
committed by GitHub
parent 2a08e0b704
commit 776d30613f
5 changed files with 195 additions and 23 deletions

View File

@@ -5,7 +5,6 @@ import { unstable_batchedUpdates } from 'react-dom';
import { WorkspacePlugins } from '../plugins';
import { RemWorkspace, RemWorkspaceFlavour } from '../shared';
// workspace necessary atoms
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
@@ -60,3 +59,32 @@ export const workspacesAtom = atom<Promise<RemWorkspace[]>>(async get => {
);
return workspaces.filter(workspace => workspace !== null) as RemWorkspace[];
});
type View = { title: string; id: string; mode: 'page' | 'edgeless' };
export type WorkspaceRecentViews = Record<string, View[]>;
export const workspaceRecentViewsAtom = atomWithStorage<WorkspaceRecentViews>(
'recentViews',
{}
);
export const workspaceRecentViresWriteAtom = atom<null, [string, View], View[]>(
null,
(get, set, id, value) => {
const record = get(workspaceRecentViewsAtom);
if (Array.isArray(record[id])) {
const idx = record[id].findIndex(view => view.id === value.id);
if (idx !== -1) {
record[id].splice(idx, 1);
}
record[id] = [value, ...record[id]];
} else {
record[id] = [value];
}
record[id] = record[id].slice(0, 3);
set(workspaceRecentViewsAtom, { ...record });
return record[id];
}
);

View File

@@ -5,6 +5,7 @@ import { Command } from 'cmdk';
import { NextRouter } from 'next/router';
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useRecentlyViewed } from '../../../hooks/affine/use-recent-views';
import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper';
import { usePageMeta } from '../../../hooks/use-page-meta';
import { BlockSuiteWorkspace } from '../../../shared';
@@ -33,6 +34,7 @@ export const Results: React.FC<ResultsProps> = ({
assertExists(blockSuiteWorkspace.room);
const List = useSwitchToConfig(blockSuiteWorkspace.room);
const [results, setResults] = useState(new Map<string, string | undefined>());
const recentlyViewed = useRecentlyViewed();
const { t } = useTranslation();
useEffect(() => {
setResults(blockSuiteWorkspace.search(query));
@@ -93,25 +95,56 @@ export const Results: React.FC<ResultsProps> = ({
</StyledNotFound>
)
) : (
<Command.Group heading={t('Jump to')}>
{List.map(link => {
return (
<Command.Item
key={link.title}
value={link.title}
onSelect={() => {
onClose();
router.push(link.href);
}}
>
<StyledListItem>
<link.icon />
<span>{link.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
<div>
<Command.Group heading={t('Recently viewed')}>
{recentlyViewed.map(recent => {
return (
<Command.Item
key={recent.id}
value={recent.id}
onSelect={() => {
onClose();
router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId: blockSuiteWorkspace.room,
pageId: recent.id,
},
});
}}
>
<StyledListItem>
{recent.mode === 'edgeless' ? (
<EdgelessIcon />
) : (
<PaperIcon />
)}
<span>{recent.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
<Command.Group heading={t('Jump to')}>
{List.map(link => {
return (
<Command.Item
key={link.title}
value={link.title}
onSelect={() => {
onClose();
router.push(link.href);
}}
>
<StyledListItem>
<link.icon />
<span>{link.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
</div>
)}
</>
);

View File

@@ -13,10 +13,23 @@ import { useRouter } from 'next/router';
import routerMock from 'next-router-mock';
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
import React from 'react';
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
import { workspacesAtom } from '../../atoms';
import { BlockSuiteWorkspace, RemWorkspaceFlavour } from '../../shared';
import {
currentWorkspaceIdAtom,
jotaiWorkspacesAtom,
workspacesAtom,
} from '../../atoms';
import { LocalPlugin } from '../../plugins/local';
import {
BlockSuiteWorkspace,
LocalWorkspace,
RemWorkspaceFlavour,
} from '../../shared';
import {
useRecentlyViewed,
useSyncRecentViewsWithRouter,
} from '../affine/use-recent-views';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
@@ -248,3 +261,59 @@ describe('useBlockSuiteWorkspaceName', () => {
expect(blockSuiteWorkspace.meta.name).toBe('test 3');
});
});
describe('useRecentlyViewed', () => {
test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const workspaceId = blockSuiteWorkspace.room as string;
const pageId = 'page0';
store.set(jotaiWorkspacesAtom, [
{
id: workspaceId,
flavour: RemWorkspaceFlavour.LOCAL,
},
]);
LocalPlugin.CRUD.get = vi.fn().mockResolvedValue({
id: workspaceId,
flavour: RemWorkspaceFlavour.LOCAL,
blockSuiteWorkspace,
providers: [],
} satisfies LocalWorkspace);
store.set(currentWorkspaceIdAtom, blockSuiteWorkspace.room as string);
const workspace = await store.get(currentWorkspaceAtom);
expect(workspace?.id).toBe(blockSuiteWorkspace.room as string);
const currentHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
expect(currentHook.result.current[0]?.id).toEqual(workspaceId);
await store.get(currentWorkspaceAtom);
const recentlyViewedHook = renderHook(() => useRecentlyViewed(), {
wrapper: ProviderWrapper,
});
expect(recentlyViewedHook.result.current).toEqual([]);
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId,
pageId,
},
});
routerHook.rerender();
const syncHook = renderHook(
router => useSyncRecentViewsWithRouter(router),
{
wrapper: ProviderWrapper,
initialProps: routerHook.result.current,
}
);
syncHook.rerender(routerHook.result.current);
expect(recentlyViewedHook.result.current).toEqual([
{
id: 'page0',
mode: 'page',
title: 'Untitled',
},
]);
});
});

View File

@@ -0,0 +1,40 @@
import { useAtomValue, useSetAtom } from 'jotai';
import { NextRouter } from 'next/router';
import { useEffect } from 'react';
import {
workspaceRecentViewsAtom,
workspaceRecentViresWriteAtom,
} from '../../atoms';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { usePageMeta } from '../use-page-meta';
export function useRecentlyViewed() {
const [workspace] = useCurrentWorkspace();
const workspaceId = workspace?.id || null;
const recentlyViewed = useAtomValue(workspaceRecentViewsAtom);
if (!workspaceId) return [];
return recentlyViewed[workspaceId] ?? [];
}
export function useSyncRecentViewsWithRouter(router: NextRouter) {
const [workspace] = useCurrentWorkspace();
const workspaceId = workspace?.id || null;
const blockSuiteWorkspace = workspace?.blockSuiteWorkspace || null;
const pageId = router.query.pageId as string;
const set = useSetAtom(workspaceRecentViresWriteAtom);
const meta = usePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
useEffect(() => {
if (!workspaceId) return;
if (pageId && meta) {
set(workspaceId, {
title: meta.title || 'Untitled',
id: pageId as string,
mode: meta.mode || 'page',
});
}
}, [pageId, meta, workspaceId, set]);
}

View File

@@ -3,6 +3,7 @@ import React, { useEffect } from 'react';
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading';
import { useSyncRecentViewsWithRouter } from '../../../hooks/affine/use-recent-views';
import { useCurrentPageId } from '../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useSyncRouterWithCurrentWorkspaceAndPage } from '../../../hooks/use-sync-router-with-current-workspace-and-page';
@@ -27,6 +28,7 @@ function enableFullFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
const WorkspaceDetail: React.FC = () => {
const [pageId] = useCurrentPageId();
const [currentWorkspace] = useCurrentWorkspace();
useSyncRecentViewsWithRouter(useRouter());
useEffect(() => {
if (currentWorkspace) {
enableFullFlags(currentWorkspace.blockSuiteWorkspace);