diff --git a/blocksuite/affine/widget-drag-handle/src/index.ts b/blocksuite/affine/widget-drag-handle/src/index.ts index 508e663d33..c7b9579d66 100644 --- a/blocksuite/affine/widget-drag-handle/src/index.ts +++ b/blocksuite/affine/widget-drag-handle/src/index.ts @@ -5,3 +5,4 @@ declare type _GLOBAL_ = typeof SurfaceEffects; export * from './consts'; export * from './drag-handle'; export * from './utils'; +export type { DragBlockPayload } from './watchers/drag-event-watcher'; diff --git a/blocksuite/blocks/src/index.ts b/blocksuite/blocks/src/index.ts index 0a37a53f55..003ecf9ec7 100644 --- a/blocksuite/blocks/src/index.ts +++ b/blocksuite/blocks/src/index.ts @@ -137,6 +137,7 @@ export { openFileOrFiles, printToPdf, } from '@blocksuite/affine-shared/utils'; +export type { DragBlockPayload } from '@blocksuite/affine-widget-drag-handle'; export const BlocksUtils = { splitElements, diff --git a/packages/frontend/component/src/ui/dnd/monitor.tsx b/packages/frontend/component/src/ui/dnd/monitor.tsx index fd837b7e76..852708a454 100644 --- a/packages/frontend/component/src/ui/dnd/monitor.tsx +++ b/packages/frontend/component/src/ui/dnd/monitor.tsx @@ -10,7 +10,7 @@ import { getAdaptedEventArgs } from './common'; import { DNDContext } from './context'; import type { DNDData, fromExternalData } from './types'; -type MonitorGetFeedback = Parameters< +export type MonitorGetFeedback = Parameters< NonNullable[0]['canMonitor']> >[0] & { source: { @@ -22,7 +22,7 @@ type MonitorGet = | T | ((data: MonitorGetFeedback) => T); -type MonitorDragEvent = { +export type MonitorDragEvent = { /** * Location history for the drag operation */ @@ -119,3 +119,5 @@ export const useDndMonitor = ( return monitorForExternal(monitorOptions); }, [monitorOptions, options.fromExternalData]); }; + +export { monitorForElements }; diff --git a/packages/frontend/core/src/mobile/components/explorer/tree/node.tsx b/packages/frontend/core/src/mobile/components/explorer/tree/node.tsx index 7fddd94179..c77287461b 100644 --- a/packages/frontend/core/src/mobile/components/explorer/tree/node.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/tree/node.tsx @@ -180,7 +180,11 @@ export const ExplorerTreeNode = ({ >
{to ? ( - + {content} ) : ( diff --git a/packages/frontend/core/src/modules/dnd/services/index.ts b/packages/frontend/core/src/modules/dnd/services/index.ts index 36bda604fa..5695c1ac1a 100644 --- a/packages/frontend/core/src/modules/dnd/services/index.ts +++ b/packages/frontend/core/src/modules/dnd/services/index.ts @@ -1,12 +1,17 @@ import { type ExternalGetDataFeedbackArgs, type fromExternalData, + monitorForElements, + type MonitorGetFeedback, type toExternalData, } from '@affine/component'; import { createPageModeSpecs } from '@affine/core/components/blocksuite/block-suite-editor/specs/page'; import type { AffineDNDData } from '@affine/core/types/dnd'; import { BlockStdScope } from '@blocksuite/affine/block-std'; -import { DndApiExtensionIdentifier } from '@blocksuite/affine/blocks'; +import { + DndApiExtensionIdentifier, + type DragBlockPayload, +} from '@blocksuite/affine/blocks'; import { type SliceSnapshot } from '@blocksuite/affine/store'; import { Service } from '@toeverything/infra'; @@ -19,6 +24,10 @@ type EntityResolver = (data: string) => Entity | null; type ExternalDragPayload = ExternalGetDataFeedbackArgs['source']; +type MixedDNDData = AffineDNDData & { + draggable: DragBlockPayload; +}; + export class DndService extends Service { constructor( private readonly docsService: DocsService, @@ -53,6 +62,90 @@ export class DndService extends Service { return null; }); }); + + this.setupBlocksuiteAdapter(); + } + + private setupBlocksuiteAdapter() { + /** + * Migrate from affine to blocksuite + * For now, we only support doc + */ + const affineToBlocksuite = (args: MonitorGetFeedback) => { + const data = args.source.data; + if (data.entity && !data.bsEntity) { + if (data.entity.type !== 'doc') { + return; + } + const dndAPI = this.getBlocksuiteDndAPI(); + if (!dndAPI) { + return; + } + const snapshotSlice = dndAPI.fromEntity({ + docId: data.entity.id, + flavour: 'affine:embed-linked-doc', + }); + if (!snapshotSlice) { + return; + } + data.bsEntity = { + type: 'blocks', + modelIds: [], + snapshot: snapshotSlice, + }; + } + }; + + /** + * Migrate from blocksuite to affine + */ + const blocksuiteToAffine = (args: MonitorGetFeedback) => { + const data = args.source.data; + if (!data.entity && data.bsEntity) { + if (data.bsEntity.type !== 'blocks' || !data.bsEntity.snapshot) { + return; + } + const dndAPI = this.getBlocksuiteDndAPI(); + if (!dndAPI) { + return; + } + const entity = this.resolveBlockSnapshot(data.bsEntity.snapshot); + if (!entity) { + return; + } + data.entity = entity; + } + }; + + function adaptDragEvent(args: MonitorGetFeedback) { + affineToBlocksuite(args); + blocksuiteToAffine(args); + } + + function canMonitor(args: MonitorGetFeedback) { + return ( + args.source.data.entity?.type === 'doc' || + (args.source.data.bsEntity?.type === 'blocks' && + !!args.source.data.bsEntity.snapshot) + ); + } + + this.disposables.push( + monitorForElements({ + canMonitor: (args: MonitorGetFeedback) => { + if (canMonitor(args)) { + // HACK ahead: + // canMonitor shall be used a pure function, which means + // we may need to adapt the drag event to make sure the data is applied onDragStart. + // However, canMonitor in blocksuite is also called BEFORE onDragStart, + // so we need to adapt it here in onMonitor + adaptDragEvent(args); + return true; + } + return false; + }, + }) + ); } private readonly resolvers: (( @@ -161,6 +254,9 @@ export class DndService extends Service { return null; }; + /** + * @deprecated Blocksuite DND is now using pragmatic-dnd as well + */ private readonly resolveBlocksuiteExternalData = ( source: ExternalDragPayload ): AffineDNDData['draggable'] | null => { diff --git a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx index 9ec597e5b9..ddc21d3d48 100644 --- a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx +++ b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx @@ -72,7 +72,8 @@ export interface BaseExplorerTreeNodeProps { childrenOperations?: NodeOperation[]; childrenPlaceholder?: React.ReactNode; linkComponent?: React.ComponentType< - React.PropsWithChildren<{ to: To; className?: string }> & RefAttributes + React.PropsWithChildren<{ to: To; className?: string }> & + RefAttributes & { draggable?: boolean } >; [key: `data-${string}`]: any; } @@ -433,7 +434,12 @@ export const ExplorerTreeNode = ({ ref={dropTargetRef} > {to ? ( - + {content} ) : ( diff --git a/packages/frontend/core/src/modules/workbench/view/split-view/split-view.tsx b/packages/frontend/core/src/modules/workbench/view/split-view/split-view.tsx index 8b59e9426a..6c59b07039 100644 --- a/packages/frontend/core/src/modules/workbench/view/split-view/split-view.tsx +++ b/packages/frontend/core/src/modules/workbench/view/split-view/split-view.tsx @@ -85,9 +85,10 @@ export const SplitView = ({ useDndMonitor(() => { return { - // todo(@pengx17): external data for monitor is not supported yet - // allowExternal: true, canMonitor(data) { + if (!BUILD_CONFIG.isElectron) { + return false; + } // allow dropping doc && tab view to split view panel const from = data.source.data.from; const entity = data.source.data.entity; diff --git a/tests/affine-local/e2e/drag-page.spec.ts b/tests/affine-local/e2e/drag-page.spec.ts index 79edcdae63..961ced37f3 100644 --- a/tests/affine-local/e2e/drag-page.spec.ts +++ b/tests/affine-local/e2e/drag-page.spec.ts @@ -243,7 +243,7 @@ test('drag a page link in editor to favourites', async ({ page }) => { ); }); -test.skip('drag a page card block to another page', async ({ page }) => { +test('drag a page card block to another page', async ({ page }) => { await clickNewPageButton(page); await page.waitForTimeout(500); await page.keyboard.press('Enter'); @@ -293,7 +293,7 @@ test.skip('drag a page card block to another page', async ({ page }) => { ); }); -test.skip('drag a favourite page into blocksuite', async ({ page }) => { +test('drag a favourite page into blocksuite', async ({ page }) => { await clickNewPageButton(page, 'hi from page'); await page.getByTestId('pin-button').click(); const pageId = getCurrentDocIdFromUrl(page);