feat: connect pinboard and reference link (#1859)

This commit is contained in:
Qi 2023-04-11 00:49:51 +08:00 committed by GitHub
parent 9acbba7016
commit ea2a146c82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 243 additions and 91 deletions

View File

@ -45,7 +45,7 @@ export const PinboardMenu = ({
meta => !meta.trash && meta.title.includes(query)
);
const { handleDrop } = usePinboardHandler({
const { dropPin } = usePinboardHandler({
blockSuiteWorkspace,
metas,
});
@ -54,7 +54,7 @@ export const PinboardMenu = ({
(dropId: string) => {
const targetTitle = metas.find(m => m.id === dropId)?.title;
handleDrop(currentMeta.id, dropId, {
dropPin(currentMeta.id, dropId, {
bottomLine: false,
topLine: false,
internal: true,
@ -62,7 +62,7 @@ export const PinboardMenu = ({
onPinboardClick?.({ dragId: currentMeta.id, dropId });
toast(`Moved "${currentMeta.title}" to "${targetTitle}"`);
},
[currentMeta.id, currentMeta.title, handleDrop, metas, onPinboardClick]
[currentMeta.id, currentMeta.title, dropPin, metas, onPinboardClick]
);
const { data } = usePinboardData({

View File

@ -80,7 +80,13 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
},
[onInit, setEditor]
)}
onLoad={onLoad}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
setEditor(editor);
onLoad?.(page, editor);
},
[onLoad, setEditor]
)}
/>
</>
);

View File

@ -41,7 +41,7 @@ export const Pinboard = ({
showOperationButton: true,
});
const { handleAdd, handleDelete, handleDrop } = usePinboardHandler({
const { addPin, deletePin, dropPin } = usePinboardHandler({
blockSuiteWorkspace: blockSuiteWorkspace,
metas: allMetas,
onAdd,
@ -54,9 +54,9 @@ export const Pinboard = ({
<div data-testid="sidebar-pinboard-container">
<TreeView
data={data}
onAdd={handleAdd}
onDelete={handleDelete}
onDrop={handleDrop}
onAdd={addPin}
onDelete={deletePin}
onDrop={dropPin}
indent={16}
/>
</div>

View File

@ -0,0 +1,42 @@
import { useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { currentEditorAtom } from '../../atoms';
export function useReferenceLink(props?: {
pageLinkClicked?: (params: { pageId: string }) => void;
subpageLinked?: (params: { pageId: string }) => void;
subpageUnlinked?: (params: { pageId: string }) => void;
}) {
const { pageLinkClicked, subpageLinked, subpageUnlinked } = props ?? {};
const editor = useAtomValue(currentEditorAtom);
useEffect(() => {
if (!editor) {
return;
}
const linkClickedDisposable = editor.slots.pageLinkClicked.on(
({ pageId }) => {
pageLinkClicked?.({ pageId });
}
);
const subpageLinkedDisposable = editor.slots.subpageLinked.on(
({ pageId }) => {
subpageLinked?.({ pageId });
}
);
const subpageUnlinkedDisposable = editor.slots.subpageUnlinked.on(
({ pageId }) => {
subpageUnlinked?.({ pageId });
}
);
return () => {
linkClickedDisposable.dispose();
subpageLinkedDisposable.dispose();
subpageUnlinkedDisposable.dispose();
};
}, [editor, pageLinkClicked, subpageLinked, subpageUnlinked]);
}

View File

@ -7,7 +7,7 @@ import { useCallback } from 'react';
import type { BlockSuiteWorkspace } from '../shared';
import { useBlockSuiteWorkspaceHelper } from './use-blocksuite-workspace-helper';
import { usePageMetaHelper } from './use-page-meta';
import type { NodeRenderProps, PinboardNode } from './use-pinboard-data';
import type { NodeRenderProps } from './use-pinboard-data';
const logger = new DebugLogger('pinboard');
@ -25,7 +25,7 @@ export function usePinboardHandler({
onDelete,
onDrop,
}: {
blockSuiteWorkspace: BlockSuiteWorkspace;
blockSuiteWorkspace: BlockSuiteWorkspace | null;
metas: PageMeta[];
onAdd?: (addedId: string, parentId: string) => void;
onDelete?: TreeViewProps<NodeRenderProps>['onDelete'];
@ -34,17 +34,43 @@ export function usePinboardHandler({
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { getPageMeta, setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const handleAdd = useCallback(
(node: PinboardNode) => {
const id = nanoid();
createPage(id, node.id);
onAdd?.(id, node.id);
// Just need handle add operation, delete check is handled in blockSuite's reference link
const addReferenceLink = useCallback(
(pageId: string, referenceId: string) => {
const page = blockSuiteWorkspace?.getPage(pageId);
if (!page) {
return;
}
const text = page.Text.fromDelta([
{
insert: ' ',
attributes: {
reference: {
type: 'Subpage',
pageId: referenceId,
},
},
},
]);
const [frame] = page.getBlockByFlavour('affine:frame');
frame && page.addBlock('affine:paragraph', { text }, frame.id);
},
[createPage, onAdd]
[blockSuiteWorkspace]
);
const handleDelete = useCallback(
(node: PinboardNode) => {
const addPin = useCallback(
(parentId: string) => {
const id = nanoid();
createPage(id, parentId);
onAdd?.(id, parentId);
addReferenceLink(parentId, id);
},
[addReferenceLink, createPage, onAdd]
);
const deletePin = useCallback(
(deleteId: string) => {
const removeToTrash = (currentMeta: PageMeta) => {
const { subpageIds = [] } = currentMeta;
setPageMeta(currentMeta.id, {
@ -52,17 +78,17 @@ export function usePinboardHandler({
trashDate: +new Date(),
});
subpageIds.forEach(id => {
const subcurrentMeta = getPageMeta(id);
subcurrentMeta && removeToTrash(subcurrentMeta);
const subCurrentMeta = getPageMeta(id);
subCurrentMeta && removeToTrash(subCurrentMeta);
});
};
removeToTrash(metas.find(m => m.id === node.id)!);
onDelete?.(node);
removeToTrash(metas.find(m => m.id === deleteId)!);
onDelete?.(deleteId);
},
[metas, getPageMeta, onDelete, setPageMeta]
);
const handleDrop = useCallback(
const dropPin = useCallback(
(
dragId: string,
dropId: string,
@ -96,7 +122,7 @@ export function usePinboardHandler({
const dropParentMeta = metas.find(m => m.subpageIds?.includes(dropId));
if (dropParentMeta?.id === dragParentMeta?.id) {
// same parent
// same parent, resort node
const newSubpageIds = [...(dragParentMeta?.subpageIds ?? [])];
const deleteIndex = newSubpageIds.findIndex(id => id === dragId);
newSubpageIds.splice(deleteIndex, 1);
@ -109,6 +135,7 @@ export function usePinboardHandler({
});
return onDrop?.(dragId, dropId, position);
}
// Old parent will delete drag node, new parent will be added
const newDragParentSubpageIds = [...(dragParentMeta?.subpageIds ?? [])];
const deleteIndex = newDragParentSubpageIds.findIndex(
id => id === dragId
@ -127,6 +154,7 @@ export function usePinboardHandler({
setPageMeta(dropParentMeta.id, {
subpageIds: newDropParentSubpageIds,
});
dropParentMeta && addReferenceLink(dropParentMeta.id, dragId);
return onDrop?.(dragId, dropId, position);
}
@ -149,14 +177,15 @@ export function usePinboardHandler({
setPageMeta(dropMeta.id, {
subpageIds: newSubpageIds,
});
addReferenceLink(dropMeta.id, dragId);
},
[metas, onDrop, setPageMeta]
[addReferenceLink, metas, onDrop, setPageMeta]
);
return {
handleDrop,
handleAdd,
handleDelete,
dropPin,
addPin,
deletePin,
};
}

View File

@ -1,5 +1,5 @@
import type { NextRouter } from 'next/router';
import { useMemo } from 'react';
import { useCallback } from 'react';
import type { WorkspaceSubPath } from '../shared';
@ -9,47 +9,70 @@ export const enum RouteLogic {
}
export function useRouterHelper(router: NextRouter) {
return useMemo(
() => ({
jumpToPage: (
workspaceId: string,
pageId: string,
logic: RouteLogic = RouteLogic.PUSH
) => {
return router[logic]({
pathname: `/workspace/[workspaceId]/[pageId]`,
query: {
workspaceId,
pageId,
},
});
},
jumpToPublicWorkspacePage: (
workspaceId: string,
pageId: string,
logic: RouteLogic = RouteLogic.PUSH
) => {
return router[logic]({
pathname: `/public-workspace/[workspaceId]/[pageId]`,
query: {
workspaceId,
pageId,
},
});
},
jumpToSubPath: (
workspaceId: string,
subPath: WorkspaceSubPath,
logic: RouteLogic = RouteLogic.PUSH
): Promise<boolean> => {
return router[logic]({
pathname: `/workspace/[workspaceId]/${subPath}`,
query: {
workspaceId,
},
});
},
}),
const jumpToPage = useCallback(
(
workspaceId: string,
pageId: string,
logic: RouteLogic = RouteLogic.PUSH
) => {
return router[logic]({
pathname: `/workspace/[workspaceId]/[pageId]`,
query: {
workspaceId,
pageId,
},
});
},
[router]
);
const jumpToPublicWorkspacePage = useCallback(
(
workspaceId: string,
pageId: string,
logic: RouteLogic = RouteLogic.PUSH
) => {
return router[logic]({
pathname: `/public-workspace/[workspaceId]/[pageId]`,
query: {
workspaceId,
pageId,
},
});
},
[router]
);
const jumpToSubPath = useCallback(
(
workspaceId: string,
subPath: WorkspaceSubPath,
logic: RouteLogic = RouteLogic.PUSH
): Promise<boolean> => {
return router[logic]({
pathname: `/workspace/[workspaceId]/${subPath}`,
query: {
workspaceId,
},
});
},
[router]
);
const openPage = useCallback(
(workspaceId: string, pageId: string) => {
const isPublicWorkspace =
router.pathname.split('/')[1] === 'public-workspace';
if (isPublicWorkspace) {
return jumpToPublicWorkspacePage(workspaceId, pageId);
} else {
return jumpToPage(workspaceId, pageId);
}
},
[jumpToPage, jumpToPublicWorkspacePage, router.pathname]
);
return {
jumpToPage,
jumpToPublicWorkspacePage,
jumpToSubPath,
openPage,
};
}

View File

@ -233,7 +233,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
}
}, [currentWorkspace]);
const router = useRouter();
const { jumpToPage, jumpToPublicWorkspacePage } = useRouterHelper(router);
const { openPage } = useRouterHelper(router);
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace?.blockSuiteWorkspace ?? null
@ -241,17 +241,6 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const isPublicWorkspace =
router.pathname.split('/')[1] === 'public-workspace';
const title = useRouterTitle(router);
const handleOpenPage = useCallback(
(pageId: string) => {
assertExists(currentWorkspace);
if (isPublicWorkspace) {
jumpToPublicWorkspacePage(currentWorkspace.id, pageId);
} else {
jumpToPage(currentWorkspace.id, pageId);
}
},
[currentWorkspace, isPublicWorkspace, jumpToPage, jumpToPublicWorkspacePage]
);
const handleCreatePage = useCallback(() => {
return helper.createPage(nanoid());
}, [helper]);
@ -319,7 +308,13 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
currentWorkspace={currentWorkspace}
currentPageId={currentPageId}
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={handleOpenPage}
openPage={useCallback(
(pageId: string) => {
assertExists(currentWorkspace);
return openPage(currentWorkspace.id, pageId);
},
[currentWorkspace, openPage]
)}
createPage={handleCreatePage}
currentPath={router.asPath.split('?')[0]}
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}

View File

@ -1,13 +1,14 @@
import { Breadcrumbs, displayFlex, styled } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { PageIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-blocksuite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-blocksuite-workspace-name';
import { useAtomValue, useSetAtom } from 'jotai';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type React from 'react';
import { Suspense, useEffect } from 'react';
import { Suspense, useCallback, useEffect } from 'react';
import {
publicPageBlockSuiteAtom,
@ -18,6 +19,8 @@ import { QueryParamError } from '../../../components/affine/affine-error-eoundar
import { PageDetailEditor } from '../../../components/page-detail-editor';
import { WorkspaceAvatar } from '../../../components/pure/footer';
import { PageLoading } from '../../../components/pure/loading';
import { useReferenceLink } from '../../../hooks/affine/use-reference-link';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { PublicWorkspaceLayout } from '../../../layouts/public-workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
import { initPage } from '../../../utils';
@ -56,9 +59,21 @@ const PublicWorkspaceDetailPageInner: React.FC<{
if (!blockSuiteWorkspace) {
throw new Error('cannot find workspace');
}
const router = useRouter();
const { openPage } = useRouterHelper(router);
useEffect(() => {
blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false);
}, [blockSuiteWorkspace]);
useReferenceLink({
pageLinkClicked: useCallback(
({ pageId }: { pageId: string }) => {
assertExists(currentWorkspace);
return openPage(blockSuiteWorkspace.id, pageId);
},
[blockSuiteWorkspace.id, openPage]
),
});
const { t } = useTranslation();
const [name] = useBlockSuiteWorkspaceName(blockSuiteWorkspace);
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(blockSuiteWorkspace);

View File

@ -1,13 +1,18 @@
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { useRouter } from 'next/router';
import type React from 'react';
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading';
import { useReferenceLink } from '../../../hooks/affine/use-reference-link';
import { useCurrentPageId } from '../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { usePageMeta } from '../../../hooks/use-page-meta';
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
import { useSyncRecentViewsWithRouter } from '../../../hooks/use-recent-views';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspaceAndPage } from '../../../hooks/use-sync-router-with-current-workspace-and-page';
import { WorkspaceLayout } from '../../../layouts';
import { WorkspacePlugins } from '../../../plugins';
@ -21,12 +26,37 @@ function enableFullFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', true);
blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', true);
blockSuiteWorkspace.awarenessStore.setFlag('enable_surface', true);
blockSuiteWorkspace.awarenessStore.setFlag('enable_linked_page', true);
}
const WorkspaceDetail: React.FC = () => {
const router = useRouter();
const { openPage } = useRouterHelper(router);
const [pageId] = useCurrentPageId();
const [currentWorkspace] = useCurrentWorkspace();
useSyncRecentViewsWithRouter(useRouter());
const { deletePin } = usePinboardHandler({
blockSuiteWorkspace: currentWorkspace?.blockSuiteWorkspace ?? null,
metas: usePageMeta(currentWorkspace?.blockSuiteWorkspace ?? null ?? null),
});
useSyncRecentViewsWithRouter(router);
useReferenceLink({
pageLinkClicked: useCallback(
({ pageId }: { pageId: string }) => {
assertExists(currentWorkspace);
return openPage(currentWorkspace.id, pageId);
},
[currentWorkspace, openPage]
),
subpageUnlinked: useCallback(
({ pageId }: { pageId: string }) => {
deletePin(pageId);
},
[deletePin]
),
});
useEffect(() => {
if (currentWorkspace) {
enableFullFlags(currentWorkspace.blockSuiteWorkspace);

View File

@ -110,8 +110,8 @@ const TreeNodeItem = <RenderProps,>({
<div ref={dropRef}>
{node.render?.(node, {
isOver: isOver && canDrop,
onAdd: () => onAdd?.(node),
onDelete: () => onDelete?.(node),
onAdd: () => onAdd?.(node.id),
onDelete: () => onDelete?.(node.id),
collapsed,
setCollapsed,
isSelected: selectedId === node.id,

View File

@ -34,8 +34,8 @@ type CommonProps<RenderProps = unknown> = {
enableDnd?: boolean;
enableKeyboardSelection?: boolean;
indent?: CSSProperties['paddingLeft'];
onAdd?: (node: Node<RenderProps>) => void;
onDelete?: (node: Node<RenderProps>) => void;
onAdd?: (parentId: string) => void;
onDelete?: (deleteId: string) => void;
onDrop?: OnDrop;
// Only trigger when the enableKeyboardSelection is true
onSelect?: (id: string) => void;

View File

@ -34,6 +34,16 @@ async function openPinboardPageOperationMenu(page: Page, id: string) {
await node.getByTestId('pinboard-operation-button').click();
}
async function checkIsChildInsertToParentInEditor(page: Page, pageId: string) {
await page
.getByTestId('sidebar-pinboard-container')
.getByTestId(`pinboard-${pageId}`)
.click();
await page.waitForTimeout(200);
const referenceLink = await page.locator('.affine-reference');
expect(referenceLink).not.toBeNull();
}
test.describe('PinBoard interaction', () => {
test('Have initial root pinboard page when first in', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
@ -77,6 +87,7 @@ test.describe('PinBoard interaction', () => {
.getByTestId('[data-testid="sidebar-pinboard-container"]')
.getByTestId(`pinboard-${meta?.id}`)
).not.toBeNull();
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
});
test('Add pinboard by sidebar operation menu', async ({ page }) => {
@ -92,6 +103,8 @@ test.describe('PinBoard interaction', () => {
.getByTestId('sidebar-pinboard-container')
.getByTestId(`pinboard-${newPageMeta?.id}`)
).not.toBeNull();
console.log('rootPinboardMeta', rootPinboardMeta);
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
});
test('Move pinboard to another in sidebar', async ({ page }) => {
@ -116,7 +129,6 @@ test.describe('PinBoard interaction', () => {
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2');
await page.getByTestId('all-pages').click();
await page