feat(core): dnd support external types (#9033)

fix AF-1847

two issues:
1. original `dropTargetForExternal` only works if dragging target is from another window context. patched the library to bypass this issue
2. `dataTransfer`'s content is only available on `drop` event. This means we cannot have `canDrop` checks for external elements (like blocksuite links).
This commit is contained in:
pengx17
2024-12-06 13:45:29 +00:00
committed by Peng Xiao
parent 6b14e1cf10
commit fafacdb265
17 changed files with 285 additions and 44 deletions

View File

@@ -0,0 +1,19 @@
diff --git a/dist/esm/adapter/external-adapter.js b/dist/esm/adapter/external-adapter.js
index ef7a963d91f08c9e70c8ed9c6b41972bec349319..e682841ec10a4a8a9ce7a79642e58de5c9e664d5 100644
--- a/dist/esm/adapter/external-adapter.js
+++ b/dist/esm/adapter/external-adapter.js
@@ -54,9 +54,11 @@ var adapter = makeAdapter({
type: 'dragenter',
listener: function listener(event) {
// drag operation was started within the document, it won't be an "external" drag
- if (didDragStartLocally) {
- return;
- }
+
+ // we will handle all events actually
+ // if (didDragStartLocally) {
+ // return;
+ // }
// Note: not checking if event was cancelled (`event.defaultPrevented`) as
// cancelling a "dragenter" accepts the drag operation (not prevent it)

View File

@@ -157,6 +157,7 @@
"macos-alias": "npm:@napi-rs/macos-alias@0.0.4", "macos-alias": "npm:@napi-rs/macos-alias@0.0.4",
"fs-xattr": "npm:@napi-rs/xattr@latest", "fs-xattr": "npm:@napi-rs/xattr@latest",
"vite": "6.0.2", "vite": "6.0.2",
"decode-named-character-reference@npm:^1.0.0": "patch:decode-named-character-reference@npm%3A1.0.2#~/.yarn/patches/decode-named-character-reference-npm-1.0.2-db17a755fd.patch" "decode-named-character-reference@npm:^1.0.0": "patch:decode-named-character-reference@npm%3A1.0.2#~/.yarn/patches/decode-named-character-reference-npm-1.0.2-db17a755fd.patch",
"@atlaskit/pragmatic-drag-and-drop@npm:^1.1.0": "patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch"
} }
} }

View File

@@ -22,7 +22,7 @@
"@affine/electron-api": "workspace:*", "@affine/electron-api": "workspace:*",
"@affine/graphql": "workspace:*", "@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*", "@affine/i18n": "workspace:*",
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1", "@atlaskit/pragmatic-drag-and-drop": "patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blocksuite/icons": "2.1.75", "@blocksuite/icons": "2.1.75",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",

View File

@@ -86,12 +86,17 @@ export const DropTarget: StoryFn<{ canDrop: boolean }> = ({ canDrop }) => {
}), }),
[] []
); );
const { dropTargetRef } = useDropTarget( const { dropTargetRef } = useDropTarget<DNDData<{ text: string }>>(
() => ({ () => ({
canDrop, canDrop,
onDrop(data) { onDrop(data) {
setDropData(prev => prev + data.source.data.text); setDropData(prev => prev + data.source.data.text);
}, },
externalDataAdapter(args) {
return {
text: args.source.getStringData(args.source.types[0]) || 'no value',
};
},
}), }),
[canDrop] [canDrop]
); );
@@ -115,6 +120,8 @@ export const DropTarget: StoryFn<{ canDrop: boolean }> = ({ canDrop }) => {
}`} }`}
</style> </style>
<div ref={dragRef}>👉 hello</div> <div ref={dragRef}>👉 hello</div>
<a href="https://www.google.com">https://www.google.com</a>
<p>Some random texts</p>
<div className="drop-target" ref={dropTargetRef}> <div className="drop-target" ref={dropTargetRef}>
{dropData || 'Drop here'} {dropData || 'Drop here'}
</div> </div>

View File

@@ -103,6 +103,8 @@ export const useDraggable = <D extends DNDData = DNDData>(
}, },
}; };
dragRef.current.dataset.affineDraggable = 'true';
const cleanupDraggable = draggable({ const cleanupDraggable = draggable({
element: dragRef.current, element: dragRef.current,
dragHandle: dragHandleRef.current ?? undefined, dragHandle: dragHandleRef.current ?? undefined,

View File

@@ -1,4 +1,11 @@
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
import type {
DragLocationHistory,
DropTargetRecord,
ElementDragType,
ExternalDragType,
} from '@atlaskit/pragmatic-drag-and-drop/types';
import { import {
attachClosestEdge, attachClosestEdge,
type Edge, type Edge,
@@ -14,6 +21,28 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import type { DNDData } from './types'; import type { DNDData } from './types';
export type DropTargetDropEvent<D extends DNDData> = {
treeInstruction: Instruction | null;
closestEdge: Edge | null;
/**
* Location history for the drag operation
*/
location: DragLocationHistory;
/**
* Data associated with the entity that is being dragged
*/
source: Exclude<ElementDragType['payload'], 'data'> & {
data: D['draggable'];
};
self: DropTargetRecord;
};
export type DropTargetDragEvent<D extends DNDData> = DropTargetDropEvent<D>;
export type DropTargetTreeInstruction = Instruction;
export type ExternalDragPayload = ExternalDragType['payload'];
type DropTargetGetFeedback<D extends DNDData> = Parameters< type DropTargetGetFeedback<D extends DNDData> = Parameters<
NonNullable<Parameters<typeof dropTargetForElements>[0]['canDrop']> NonNullable<Parameters<typeof dropTargetForElements>[0]['canDrop']>
>[0] & { >[0] & {
@@ -29,6 +58,36 @@ type DropTargetGet<T, D extends DNDData> =
| T | T
| ((data: DropTargetGetFeedback<D>) => T); | ((data: DropTargetGetFeedback<D>) => T);
export type ExternalGetDataFeedbackArgs = Parameters<
NonNullable<Parameters<typeof dropTargetForExternal>[0]['getData']>
>[0];
export type ExternalDataAdapter<D extends DNDData> = (
args: ExternalGetDataFeedbackArgs
) => D['draggable'];
const getAdaptedEventArgs = <
D extends DNDData,
Args extends Pick<DropTargetGetFeedback<D>, 'source'>,
>(
options: DropTargetOptions<D>,
args: Args
): Args => {
const data =
!args.source['data'] && options.externalDataAdapter
? // @ts-expect-error hack for external data adapter (source has no data field)
options.externalDataAdapter(args as ExternalGetDataFeedbackArgs)
: args.source['data'];
return {
...args,
source: {
...args.source,
data,
},
};
};
function dropTargetGet<T, D extends DNDData>( function dropTargetGet<T, D extends DNDData>(
get: T, get: T,
options: DropTargetOptions<D> options: DropTargetOptions<D>
@@ -42,12 +101,13 @@ function dropTargetGet<T, D extends DNDData>(
if (get === undefined) { if (get === undefined) {
return undefined as any; return undefined as any;
} }
return (( return ((
args: Omit<DropTargetGetFeedback<D>, 'treeInstruction' | 'closestEdge'> args: Omit<DropTargetGetFeedback<D>, 'treeInstruction' | 'closestEdge'>
) => { ) => {
if (typeof get === 'function') { if (typeof get === 'function') {
return (get as any)({ return (get as any)({
...args, ...getAdaptedEventArgs(options, args),
get treeInstruction() { get treeInstruction() {
return options.treeInstruction return options.treeInstruction
? extractInstruction( ? extractInstruction(
@@ -81,25 +141,14 @@ function dropTargetGet<T, D extends DNDData>(
}, },
}); });
} else { } else {
return get; return {
...get,
...getAdaptedEventArgs(options, args),
};
} }
}) as any; }) as any;
} }
export type DropTargetDropEvent<D extends DNDData> = Parameters<
NonNullable<Parameters<typeof dropTargetForElements>[0]['onDrop']>
>[0] & { treeInstruction: Instruction | null; closestEdge: Edge | null } & {
source: { data: D['draggable'] };
};
export type DropTargetDragEvent<D extends DNDData> = Parameters<
NonNullable<Parameters<typeof dropTargetForElements>[0]['onDrag']>
>[0] & { treeInstruction: Instruction | null; closestEdge: Edge | null } & {
source: { data: D['draggable'] };
};
export type DropTargetTreeInstruction = Instruction;
export interface DropTargetOptions<D extends DNDData = DNDData> { export interface DropTargetOptions<D extends DNDData = DNDData> {
data?: DropTargetGet<D['dropTarget'], D>; data?: DropTargetGet<D['dropTarget'], D>;
canDrop?: DropTargetGet<boolean, D>; canDrop?: DropTargetGet<boolean, D>;
@@ -116,6 +165,13 @@ export interface DropTargetOptions<D extends DNDData = DNDData> {
}; };
onDrop?: (data: DropTargetDropEvent<D>) => void; onDrop?: (data: DropTargetDropEvent<D>) => void;
onDrag?: (data: DropTargetDragEvent<D>) => void; onDrag?: (data: DropTargetDragEvent<D>) => void;
/**
* external data adapter.
* if this is provided, the drop target will handle external elements as well.
*
* @default undefined
*/
externalDataAdapter?: ExternalDataAdapter<D>;
} }
export const useDropTarget = <D extends DNDData = DNDData>( export const useDropTarget = <D extends DNDData = DNDData>(
@@ -131,9 +187,9 @@ export const useDropTarget = <D extends DNDData = DNDData>(
const [dropEffect, setDropEffect] = useState<'copy' | 'link' | 'move' | null>( const [dropEffect, setDropEffect] = useState<'copy' | 'link' | 'move' | null>(
null null
); );
const [draggedOverDraggable, setDraggedOverDraggable] = useState<{ const [draggedOverDraggable, setDraggedOverDraggable] = useState<
data: D['draggable']; DropTargetDropEvent<D>['source'] | null
} | null>(null); >(null);
const [draggedOverPosition, setDraggedOverPosition] = useState<{ const [draggedOverPosition, setDraggedOverPosition] = useState<{
/** /**
* relative position to the drop target element top-left corner * relative position to the drop target element top-left corner
@@ -152,16 +208,16 @@ export const useDropTarget = <D extends DNDData = DNDData>(
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const options = useMemo(getOptions, deps); const options = useMemo(getOptions, deps);
useEffect(() => { const dropTargetOptions = useMemo(() => {
if (!dropTargetRef.current) { return {
return; get element() {
} return dropTargetRef.current;
return dropTargetForElements({ },
element: dropTargetRef.current,
canDrop: dropTargetGet(options.canDrop, options), canDrop: dropTargetGet(options.canDrop, options),
getDropEffect: dropTargetGet(options.dropEffect, options), getDropEffect: dropTargetGet(options.dropEffect, options),
getIsSticky: dropTargetGet(options.isSticky, options), getIsSticky: dropTargetGet(options.isSticky, options),
onDrop: args => { onDrop: (args: DropTargetDropEvent<D>) => {
args = getAdaptedEventArgs(options, args);
if (enableDraggedOver.current) { if (enableDraggedOver.current) {
setDraggedOver(false); setDraggedOver(false);
} }
@@ -202,7 +258,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
} as DropTargetDropEvent<D>); } as DropTargetDropEvent<D>);
} }
}, },
getData: args => { getData: (args: DropTargetGetFeedback<D>) => {
args = getAdaptedEventArgs(options, args);
const originData = dropTargetGet(options.data ?? {}, options)(args); const originData = dropTargetGet(options.data ?? {}, options)(args);
const { input, element } = args; const { input, element } = args;
const withInstruction = options.treeInstruction const withInstruction = options.treeInstruction
@@ -224,13 +281,14 @@ export const useDropTarget = <D extends DNDData = DNDData>(
: withInstruction; : withInstruction;
return withClosestEdge; return withClosestEdge;
}, },
onDrag: args => { onDrag: (args: DropTargetDragEvent<D>) => {
args = getAdaptedEventArgs(options, args);
if ( if (
args.location.current.dropTargets[0]?.element === args.location.current.dropTargets[0]?.element ===
dropTargetRef.current dropTargetRef.current
) { ) {
if (enableDraggedOverDraggable.current) { if (enableDraggedOverDraggable.current) {
setDraggedOverDraggable({ data: args.source.data }); setDraggedOverDraggable(args.source);
} }
let instruction = null; let instruction = null;
let closestEdge = null; let closestEdge = null;
@@ -266,7 +324,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
} as DropTargetDropEvent<D>); } as DropTargetDropEvent<D>);
} }
}, },
onDropTargetChange: args => { onDropTargetChange: (args: DropTargetDropEvent<D>) => {
args = getAdaptedEventArgs(options, args);
if ( if (
args.location.current.dropTargets[0]?.element === args.location.current.dropTargets[0]?.element ===
dropTargetRef.current dropTargetRef.current
@@ -290,7 +349,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
setDropEffect(args.self.dropEffect); setDropEffect(args.self.dropEffect);
} }
if (enableDraggedOverDraggable.current) { if (enableDraggedOverDraggable.current) {
setDraggedOverDraggable({ data: args.source.data }); setDraggedOverDraggable(args.source);
} }
if (enableDraggedOverPosition.current) { if (enableDraggedOverPosition.current) {
const rect = args.self.element.getBoundingClientRect(); const rect = args.self.element.getBoundingClientRect();
@@ -336,9 +395,23 @@ export const useDropTarget = <D extends DNDData = DNDData>(
} }
} }
}, },
}); };
}, [options]); }, [options]);
useEffect(() => {
if (!dropTargetRef.current) {
return;
}
return dropTargetForElements(dropTargetOptions as any);
}, [dropTargetOptions]);
useEffect(() => {
if (!dropTargetRef.current || !options.externalDataAdapter) {
return;
}
return dropTargetForExternal(dropTargetOptions as any);
}, [dropTargetOptions, options.externalDataAdapter]);
return { return {
dropTargetRef, dropTargetRef,
get draggedOver() { get draggedOver() {

View File

@@ -0,0 +1,14 @@
import {
DocsService,
type Framework,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
import { DndService } from './services';
export function configureDndModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(DndService, [DocsService, WorkspaceService]);
}

View File

@@ -0,0 +1,89 @@
import type { ExternalGetDataFeedbackArgs } from '@affine/component';
import type { AffineDNDData } from '@affine/core/types/dnd';
import type { DocsService, WorkspaceService } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { resolveLinkToDoc } from '../../navigation';
type EntityResolver = (
data: string
) => AffineDNDData['draggable']['entity'] | null;
export class DndService extends Service {
constructor(
private readonly docsService: DocsService,
private readonly workspaceService: WorkspaceService
) {
super();
// order matters
this.resolvers.set('text/html', this.resolveHTML);
this.resolvers.set('text/uri-list', this.resolveUriList);
}
private readonly resolvers = new Map<string, EntityResolver>();
externalDataAdapter = (args: ExternalGetDataFeedbackArgs) => {
const from: AffineDNDData['draggable']['from'] = {
at: 'external',
};
let entity: AffineDNDData['draggable']['entity'];
// in the order of the resolvers instead of the order of the types
for (const [type, resolver] of this.resolvers) {
if (args.source.types.includes(type)) {
const stringData = args.source.getStringData(type);
if (stringData) {
const candidate = resolver(stringData);
if (candidate) {
entity = candidate;
break;
}
}
}
}
return {
from,
entity,
};
};
private readonly resolveUriList: EntityResolver = urls => {
// only deal with the first url
const url = urls
?.split('\n')
.filter(u => u.trim() && !u.trim().startsWith('#'))[0];
if (url) {
const maybeDocLink = resolveLinkToDoc(url);
// check if the doc is in the current workspace
if (
maybeDocLink?.workspaceId === this.workspaceService.workspace.id &&
this.docsService.list.doc$(maybeDocLink.docId).value &&
// skip for block references for now
!maybeDocLink.blockIds?.length
) {
return {
type: 'doc',
id: maybeDocLink.docId,
};
}
}
return null;
};
// todo: implement this
private readonly resolveHTML: EntityResolver = _html => {
try {
// const parser = new DOMParser();
// const doc = parser.parseFromString(html, 'text/html');
// return doc.body.innerText;
} catch {
// ignore the error
return null;
}
return null;
};
}

View File

@@ -189,9 +189,10 @@ export const ExplorerCollectionNode = ({
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>( const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
() => args => { () => args => {
const entityType = args.source.data.entity?.type; const entityType = args.source.data.entity?.type;
const isExternalDrop = args.source.data.from?.at === 'external';
return args.treeInstruction?.type !== 'make-child' return args.treeInstruction?.type !== 'make-child'
? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true) ? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true)
: entityType === 'doc'; : entityType === 'doc' || isExternalDrop;
}, },
[canDrop] [canDrop]
); );

View File

@@ -180,9 +180,10 @@ export const ExplorerDocNode = ({
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>( const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
() => args => { () => args => {
const entityType = args.source.data.entity?.type; const entityType = args.source.data.entity?.type;
const isExternalDrop = args.source.data.from?.at === 'external';
return args.treeInstruction?.type !== 'make-child' return args.treeInstruction?.type !== 'make-child'
? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true) ? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true)
: entityType === 'doc'; : entityType === 'doc' || isExternalDrop;
}, },
[canDrop] [canDrop]
); );

View File

@@ -16,8 +16,10 @@ export const favoriteChildrenDropEffect: ExplorerTreeNodeDropEffect = data => {
) { ) {
return 'move'; return 'move';
} else if ( } else if (
data.source.data.entity?.type && (data.source.data.entity?.type &&
isFavoriteSupportType(data.source.data.entity.type) isFavoriteSupportType(data.source.data.entity.type)) ||
// always allow external drop
data.source.data.from?.at === 'external'
) { ) {
return 'link'; return 'link';
} }
@@ -37,7 +39,7 @@ export const favoriteRootCanDrop: DropTargetOptions<AffineDNDData>['canDrop'] =
data => { data => {
return data.source.data.entity?.type return data.source.data.entity?.type
? isFavoriteSupportType(data.source.data.entity.type) ? isFavoriteSupportType(data.source.data.entity.type)
: false; : data.source.data.from?.at === 'external'; // always allow external drop
}; };
export const favoriteChildrenCanDrop: DropTargetOptions<AffineDNDData>['canDrop'] = export const favoriteChildrenCanDrop: DropTargetOptions<AffineDNDData>['canDrop'] =

View File

@@ -3,9 +3,11 @@ import {
Skeleton, Skeleton,
useDropTarget, useDropTarget,
} from '@affine/component'; } from '@affine/component';
import { DndService } from '@affine/core/modules/dnd/services';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { FavoriteIcon } from '@blocksuite/icons/rc'; import { FavoriteIcon } from '@blocksuite/icons/rc';
import { useService } from '@toeverything/infra';
import { ExplorerEmptySection } from '../../layouts/empty-section'; import { ExplorerEmptySection } from '../../layouts/empty-section';
import { DropEffect } from '../../tree'; import { DropEffect } from '../../tree';
@@ -21,6 +23,7 @@ const RootEmptyLoading = () => {
}; };
const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => { const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => {
const t = useI18n(); const t = useI18n();
const dndService = useService(DndService);
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } = const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
useDropTarget<AffineDNDData>( useDropTarget<AffineDNDData>(
@@ -30,8 +33,9 @@ const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => {
}, },
onDrop: onDrop, onDrop: onDrop,
canDrop: favoriteRootCanDrop, canDrop: favoriteRootCanDrop,
externalDataAdapter: dndService.externalDataAdapter,
}), }),
[onDrop] [dndService.externalDataAdapter, onDrop]
); );
return ( return (

View File

@@ -4,6 +4,7 @@ import {
useDropTarget, useDropTarget,
} from '@affine/component'; } from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { DndService } from '@affine/core/modules/dnd/services';
import { import {
DropEffect, DropEffect,
ExplorerTreeRoot, ExplorerTreeRoot,
@@ -20,6 +21,7 @@ import { track } from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc'; import { PlusIcon } from '@blocksuite/icons/rc';
import { import {
useLiveData, useLiveData,
useService,
useServices, useServices,
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } from '@toeverything/infra';
@@ -149,6 +151,8 @@ export const ExplorerFavorites = () => {
[favoriteService] [favoriteService]
); );
const dndService = useService(DndService);
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } = const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
useDropTarget<AffineDNDData>( useDropTarget<AffineDNDData>(
() => ({ () => ({
@@ -157,8 +161,9 @@ export const ExplorerFavorites = () => {
}, },
onDrop: handleDrop, onDrop: handleDrop,
canDrop: favoriteRootCanDrop, canDrop: favoriteRootCanDrop,
externalDataAdapter: dndService.externalDataAdapter,
}), }),
[handleDrop] [dndService.externalDataAdapter, handleDrop]
); );
return ( return (

View File

@@ -11,6 +11,7 @@ import {
} from '@affine/component'; } from '@affine/component';
import { RenameModal } from '@affine/component/rename-modal'; import { RenameModal } from '@affine/component/rename-modal';
import { AppSidebarService } from '@affine/core/modules/app-sidebar'; import { AppSidebarService } from '@affine/core/modules/app-sidebar';
import { DndService } from '@affine/core/modules/dnd/services';
import { WorkbenchLink } from '@affine/core/modules/workbench'; import { WorkbenchLink } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { extractEmojiIcon } from '@affine/core/utils'; import { extractEmojiIcon } from '@affine/core/utils';
@@ -185,6 +186,8 @@ export const ExplorerTreeNode = ({
}, },
[canDrop, reorderable] [canDrop, reorderable]
); );
const dndService = useService(DndService);
const { const {
dropTargetRef, dropTargetRef,
treeInstruction, treeInstruction,
@@ -221,6 +224,9 @@ export const ExplorerTreeNode = ({
} }
}, },
canDrop: handleCanDrop, canDrop: handleCanDrop,
externalDataAdapter(args) {
return dndService.externalDataAdapter(args) as any;
},
}), }),
[ [
dndData?.dropTarget, dndData?.dropTarget,
@@ -232,6 +238,7 @@ export const ExplorerTreeNode = ({
cid, cid,
onDrop, onDrop,
setCollapsed, setCollapsed,
dndService,
] ]
); );
const isSelfDraggedOver = draggedOverDraggable?.data.__cid === cid; const isSelfDraggedOver = draggedOverDraggable?.data.__cid === cid;

View File

@@ -6,6 +6,7 @@ import { configAtMenuConfigModule } from './at-menu-config';
import { configureCloudModule } from './cloud'; import { configureCloudModule } from './cloud';
import { configureCollectionModule } from './collection'; import { configureCollectionModule } from './collection';
import { configureDialogModule } from './dialogs'; import { configureDialogModule } from './dialogs';
import { configureDndModule } from './dnd';
import { configureDocDisplayMetaModule } from './doc-display-meta'; import { configureDocDisplayMetaModule } from './doc-display-meta';
import { configureDocInfoModule } from './doc-info'; import { configureDocInfoModule } from './doc-info';
import { configureDocLinksModule } from './doc-link'; import { configureDocLinksModule } from './doc-link';
@@ -69,4 +70,5 @@ export function configureCommonModules(framework: Framework) {
configureDocInfoModule(framework); configureDocInfoModule(framework);
configureOpenInApp(framework); configureOpenInApp(framework);
configAtMenuConfigModule(framework); configAtMenuConfigModule(framework);
configureDndModule(framework);
} }

View File

@@ -74,6 +74,9 @@ export interface AffineDNDData extends DNDData {
| { | {
at: 'doc-property:manager'; at: 'doc-property:manager';
workspaceId: string; workspaceId: string;
}
| {
at: 'external'; // for blocksuite or external apps
}; };
}; };
dropTarget: dropTarget:

View File

@@ -316,7 +316,7 @@ __metadata:
"@affine/electron-api": "workspace:*" "@affine/electron-api": "workspace:*"
"@affine/graphql": "workspace:*" "@affine/graphql": "workspace:*"
"@affine/i18n": "workspace:*" "@affine/i18n": "workspace:*"
"@atlaskit/pragmatic-drag-and-drop": "npm:^1.2.1" "@atlaskit/pragmatic-drag-and-drop": "patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch"
"@atlaskit/pragmatic-drag-and-drop-hitbox": "npm:^1.0.3" "@atlaskit/pragmatic-drag-and-drop-hitbox": "npm:^1.0.3"
"@blocksuite/affine": "npm:0.18.4" "@blocksuite/affine": "npm:0.18.4"
"@blocksuite/icons": "npm:2.1.75" "@blocksuite/icons": "npm:2.1.75"
@@ -1209,7 +1209,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@atlaskit/pragmatic-drag-and-drop@npm:^1.1.0, @atlaskit/pragmatic-drag-and-drop@npm:^1.2.1": "@atlaskit/pragmatic-drag-and-drop@npm:1.4.0":
version: 1.4.0 version: 1.4.0
resolution: "@atlaskit/pragmatic-drag-and-drop@npm:1.4.0" resolution: "@atlaskit/pragmatic-drag-and-drop@npm:1.4.0"
dependencies: dependencies:
@@ -1220,6 +1220,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@atlaskit/pragmatic-drag-and-drop@patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch":
version: 1.4.0
resolution: "@atlaskit/pragmatic-drag-and-drop@patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch::version=1.4.0&hash=9e529b"
dependencies:
"@babel/runtime": "npm:^7.0.0"
bind-event-listener: "npm:^3.0.0"
raf-schd: "npm:^4.0.3"
checksum: 10/4376cd2dfe2771081b601ce33d79ec2d2f412739bfa096b6f7bc098cca73659e2ae84ef69f4632f03708b814ed920794fbf9e1826c92c042d397597098de1f6b
languageName: node
linkType: hard
"@aws-crypto/crc32@npm:5.2.0": "@aws-crypto/crc32@npm:5.2.0":
version: 5.2.0 version: 5.2.0
resolution: "@aws-crypto/crc32@npm:5.2.0" resolution: "@aws-crypto/crc32@npm:5.2.0"