mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat: add recentlyViewed (#1357)
Co-authored-by: himself65 <himself65@outlook.com>
This commit is contained in:
@@ -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];
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
40
apps/web/src/hooks/affine/use-recent-views.ts
Normal file
40
apps/web/src/hooks/affine/use-recent-views.ts
Normal 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]);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user