feat(core): add new bs dnd adapter (#9717)

This commit is contained in:
pengx17
2025-01-16 13:54:50 +00:00
parent 99717196c5
commit 7dc470e7ea
8 changed files with 121 additions and 10 deletions

View File

@@ -5,3 +5,4 @@ declare type _GLOBAL_ = typeof SurfaceEffects;
export * from './consts'; export * from './consts';
export * from './drag-handle'; export * from './drag-handle';
export * from './utils'; export * from './utils';
export type { DragBlockPayload } from './watchers/drag-event-watcher';

View File

@@ -137,6 +137,7 @@ export {
openFileOrFiles, openFileOrFiles,
printToPdf, printToPdf,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
export type { DragBlockPayload } from '@blocksuite/affine-widget-drag-handle';
export const BlocksUtils = { export const BlocksUtils = {
splitElements, splitElements,

View File

@@ -10,7 +10,7 @@ import { getAdaptedEventArgs } from './common';
import { DNDContext } from './context'; import { DNDContext } from './context';
import type { DNDData, fromExternalData } from './types'; import type { DNDData, fromExternalData } from './types';
type MonitorGetFeedback<D extends DNDData = DNDData> = Parameters< export type MonitorGetFeedback<D extends DNDData = DNDData> = Parameters<
NonNullable<Parameters<typeof monitorForElements>[0]['canMonitor']> NonNullable<Parameters<typeof monitorForElements>[0]['canMonitor']>
>[0] & { >[0] & {
source: { source: {
@@ -22,7 +22,7 @@ type MonitorGet<T, D extends DNDData = DNDData> =
| T | T
| ((data: MonitorGetFeedback<D>) => T); | ((data: MonitorGetFeedback<D>) => T);
type MonitorDragEvent<D extends DNDData = DNDData> = { export type MonitorDragEvent<D extends DNDData = DNDData> = {
/** /**
* Location history for the drag operation * Location history for the drag operation
*/ */
@@ -119,3 +119,5 @@ export const useDndMonitor = <D extends DNDData = DNDData>(
return monitorForExternal(monitorOptions); return monitorForExternal(monitorOptions);
}, [monitorOptions, options.fromExternalData]); }, [monitorOptions, options.fromExternalData]);
}; };
export { monitorForElements };

View File

@@ -180,7 +180,11 @@ export const ExplorerTreeNode = ({
> >
<div className={styles.contentContainer} data-open={!collapsed}> <div className={styles.contentContainer} data-open={!collapsed}>
{to ? ( {to ? (
<LinkComponent to={to} className={styles.linkItemRoot}> <LinkComponent
to={to}
className={styles.linkItemRoot}
draggable={false}
>
{content} {content}
</LinkComponent> </LinkComponent>
) : ( ) : (

View File

@@ -1,12 +1,17 @@
import { import {
type ExternalGetDataFeedbackArgs, type ExternalGetDataFeedbackArgs,
type fromExternalData, type fromExternalData,
monitorForElements,
type MonitorGetFeedback,
type toExternalData, type toExternalData,
} from '@affine/component'; } from '@affine/component';
import { createPageModeSpecs } from '@affine/core/components/blocksuite/block-suite-editor/specs/page'; import { createPageModeSpecs } from '@affine/core/components/blocksuite/block-suite-editor/specs/page';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { BlockStdScope } from '@blocksuite/affine/block-std'; 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 { type SliceSnapshot } from '@blocksuite/affine/store';
import { Service } from '@toeverything/infra'; import { Service } from '@toeverything/infra';
@@ -19,6 +24,10 @@ type EntityResolver = (data: string) => Entity | null;
type ExternalDragPayload = ExternalGetDataFeedbackArgs['source']; type ExternalDragPayload = ExternalGetDataFeedbackArgs['source'];
type MixedDNDData = AffineDNDData & {
draggable: DragBlockPayload;
};
export class DndService extends Service { export class DndService extends Service {
constructor( constructor(
private readonly docsService: DocsService, private readonly docsService: DocsService,
@@ -53,6 +62,90 @@ export class DndService extends Service {
return null; return null;
}); });
}); });
this.setupBlocksuiteAdapter();
}
private setupBlocksuiteAdapter() {
/**
* Migrate from affine to blocksuite
* For now, we only support doc
*/
const affineToBlocksuite = (args: MonitorGetFeedback<MixedDNDData>) => {
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<MixedDNDData>) => {
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<MixedDNDData>) {
affineToBlocksuite(args);
blocksuiteToAffine(args);
}
function canMonitor(args: MonitorGetFeedback<MixedDNDData>) {
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<MixedDNDData>) => {
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: (( private readonly resolvers: ((
@@ -161,6 +254,9 @@ export class DndService extends Service {
return null; return null;
}; };
/**
* @deprecated Blocksuite DND is now using pragmatic-dnd as well
*/
private readonly resolveBlocksuiteExternalData = ( private readonly resolveBlocksuiteExternalData = (
source: ExternalDragPayload source: ExternalDragPayload
): AffineDNDData['draggable'] | null => { ): AffineDNDData['draggable'] | null => {

View File

@@ -72,7 +72,8 @@ export interface BaseExplorerTreeNodeProps {
childrenOperations?: NodeOperation[]; childrenOperations?: NodeOperation[];
childrenPlaceholder?: React.ReactNode; childrenPlaceholder?: React.ReactNode;
linkComponent?: React.ComponentType< linkComponent?: React.ComponentType<
React.PropsWithChildren<{ to: To; className?: string }> & RefAttributes<any> React.PropsWithChildren<{ to: To; className?: string }> &
RefAttributes<any> & { draggable?: boolean }
>; >;
[key: `data-${string}`]: any; [key: `data-${string}`]: any;
} }
@@ -433,7 +434,12 @@ export const ExplorerTreeNode = ({
ref={dropTargetRef} ref={dropTargetRef}
> >
{to ? ( {to ? (
<LinkComponent to={to} className={styles.linkItemRoot} ref={dragRef}> <LinkComponent
to={to}
className={styles.linkItemRoot}
ref={dragRef}
draggable={false}
>
{content} {content}
</LinkComponent> </LinkComponent>
) : ( ) : (

View File

@@ -85,9 +85,10 @@ export const SplitView = ({
useDndMonitor<AffineDNDData>(() => { useDndMonitor<AffineDNDData>(() => {
return { return {
// todo(@pengx17): external data for monitor is not supported yet
// allowExternal: true,
canMonitor(data) { canMonitor(data) {
if (!BUILD_CONFIG.isElectron) {
return false;
}
// allow dropping doc && tab view to split view panel // allow dropping doc && tab view to split view panel
const from = data.source.data.from; const from = data.source.data.from;
const entity = data.source.data.entity; const entity = data.source.data.entity;

View File

@@ -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 clickNewPageButton(page);
await page.waitForTimeout(500); await page.waitForTimeout(500);
await page.keyboard.press('Enter'); 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 clickNewPageButton(page, 'hi from page');
await page.getByTestId('pin-button').click(); await page.getByTestId('pin-button').click();
const pageId = getCurrentDocIdFromUrl(page); const pageId = getCurrentDocIdFromUrl(page);