diff --git a/packages/frontend/component/src/ui/dnd/context.ts b/packages/frontend/component/src/ui/dnd/context.ts new file mode 100644 index 0000000000..0e19803f2a --- /dev/null +++ b/packages/frontend/component/src/ui/dnd/context.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +import type { DNDData, ExternalDataAdapter } from './types'; + +export const DNDContext = createContext<{ + /** + * external data adapter. + * if this is provided, the drop target will handle external elements as well. + * + * @default undefined + */ + externalDataAdapter?: ExternalDataAdapter; +}>({}); diff --git a/packages/frontend/component/src/ui/dnd/drop-target.ts b/packages/frontend/component/src/ui/dnd/drop-target.ts index 9de6c58205..d00d54c578 100644 --- a/packages/frontend/component/src/ui/dnd/drop-target.ts +++ b/packages/frontend/component/src/ui/dnd/drop-target.ts @@ -17,9 +17,10 @@ import { type Instruction, type ItemMode, } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import type { DNDData } from './types'; +import { DNDContext } from './context'; +import type { DNDData, ExternalDataAdapter } from './types'; export type DropTargetDropEvent = { treeInstruction: Instruction | null; @@ -58,25 +59,27 @@ type DropTargetGet = | T | ((data: DropTargetGetFeedback) => T); -export type ExternalGetDataFeedbackArgs = Parameters< - NonNullable[0]['getData']> ->[0]; - -export type ExternalDataAdapter = ( - args: ExternalGetDataFeedbackArgs -) => D['draggable']; +const isExternalDrag = ( + args: Pick, 'source'> +) => { + return !args.source['data']; +}; const getAdaptedEventArgs = < D extends DNDData, Args extends Pick, 'source'>, >( options: DropTargetOptions, - args: Args + args: Args, + isDropEvent = false ): 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) + isExternalDrag(args) && options.externalDataAdapter + ? options.externalDataAdapter( + // @ts-expect-error hack for external data adapter (source has no data field) + args as ExternalGetDataFeedbackArgs, + isDropEvent + ) : args.source['data']; return { @@ -167,11 +170,16 @@ export interface DropTargetOptions { onDrag?: (data: DropTargetDragEvent) => void; /** * external data adapter. - * if this is provided, the drop target will handle external elements as well. + * Will use the external data adapter from the context if not provided. + */ + externalDataAdapter?: ExternalDataAdapter; + /** + * Make the drop target allow external data. + * If this is undefined, it will be set to true if externalDataAdapter is provided. * * @default undefined */ - externalDataAdapter?: ExternalDataAdapter; + allowExternal?: boolean; } export const useDropTarget = ( @@ -205,19 +213,56 @@ export const useDropTarget = ( const enableDraggedOverPosition = useRef(false); const enableDropEffect = useRef(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - const options = useMemo(getOptions, deps); + const dropTargetContext = useContext(DNDContext); + + const options = useMemo(() => { + const opts = getOptions(); + const allowExternal = opts.allowExternal ?? !!opts.externalDataAdapter; + return { + ...opts, + allowExternal, + externalDataAdapter: allowExternal + ? (opts.externalDataAdapter ?? + (dropTargetContext.externalDataAdapter as ExternalDataAdapter)) + : undefined, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...deps, dropTargetContext.externalDataAdapter]); const dropTargetOptions = useMemo(() => { + const wrappedCanDrop = dropTargetGet(options.canDrop, options); return { get element() { return dropTargetRef.current; }, - canDrop: dropTargetGet(options.canDrop, options), + canDrop: wrappedCanDrop + ? (args: DropTargetGetFeedback) => { + // check if args has data. if not, it's an external drag + // we always allow external drag since the data is only + // available in drop event + if (isExternalDrag(args) && options.externalDataAdapter) { + return true; + } + return wrappedCanDrop(args); + } + : undefined, getDropEffect: dropTargetGet(options.dropEffect, options), getIsSticky: dropTargetGet(options.isSticky, options), - onDrop: (args: DropTargetDropEvent) => { - args = getAdaptedEventArgs(options, args); + onDrop: (_args: DropTargetDropEvent) => { + // external data is only available in drop event thus + // this is the only case for getAdaptedEventArgs + const args = getAdaptedEventArgs(options, _args, true); + if ( + isExternalDrag(_args) && + options.externalDataAdapter && + typeof options.canDrop === 'function' && + // there is a small flaw that canDrop called in onDrop misses + // `input and `element` arguments + !options.canDrop(args as any) + ) { + return; + } + if (enableDraggedOver.current) { setDraggedOver(false); } diff --git a/packages/frontend/component/src/ui/dnd/index.ts b/packages/frontend/component/src/ui/dnd/index.ts index b56004fe3c..59aea6d5b9 100644 --- a/packages/frontend/component/src/ui/dnd/index.ts +++ b/packages/frontend/component/src/ui/dnd/index.ts @@ -1,3 +1,4 @@ +export * from './context'; export * from './draggable'; export * from './drop-indicator'; export * from './drop-target'; diff --git a/packages/frontend/component/src/ui/dnd/types.ts b/packages/frontend/component/src/ui/dnd/types.ts index 7300c4753e..01555cf0f5 100644 --- a/packages/frontend/component/src/ui/dnd/types.ts +++ b/packages/frontend/component/src/ui/dnd/types.ts @@ -1,3 +1,5 @@ +import type { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter'; + export interface DNDData< Draggable extends Record = Record, DropTarget extends Record = Record, @@ -5,3 +7,12 @@ export interface DNDData< draggable: Draggable; dropTarget: DropTarget; } + +export type ExternalGetDataFeedbackArgs = Parameters< + NonNullable[0]['getData']> +>[0]; + +export type ExternalDataAdapter = ( + args: ExternalGetDataFeedbackArgs, + isDropEvent?: boolean +) => D['draggable']; diff --git a/packages/frontend/core/src/desktop/pages/workspace/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/index.tsx index 42e4214469..04ca2a1d55 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/index.tsx @@ -1,19 +1,22 @@ +import { DNDContext } from '@affine/component'; import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout'; import { workbenchRoutes } from '@affine/core/desktop/workbench-router'; import { DefaultServerService, WorkspaceServerService, } from '@affine/core/modules/cloud'; +import { DndService } from '@affine/core/modules/dnd/services'; import { ZipTransformer } from '@blocksuite/affine/blocks'; import type { Workspace, WorkspaceMetadata } from '@toeverything/infra'; import { FrameworkScope, GlobalContextService, useLiveData, + useService, useServices, WorkspacesService, } from '@toeverything/infra'; -import type { ReactElement } from 'react'; +import type { PropsWithChildren, ReactElement } from 'react'; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { matchPath, useLocation, useParams } from 'react-router-dom'; @@ -128,6 +131,18 @@ export const Component = (): ReactElement => { return ; }; +const DNDContextProvider = ({ children }: PropsWithChildren) => { + const dndService = useService(DndService); + const contextValue = useMemo(() => { + return { + externalDataAdapter: dndService.externalDataAdapter, + }; + }, [dndService.externalDataAdapter]); + return ( + {children} + ); +}; + const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => { const { workspacesService, globalContextService, defaultServerService } = useServices({ @@ -229,7 +244,9 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => { return ( - + + + ); @@ -238,11 +255,13 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => { return ( - - - - - + + + + + + + ); diff --git a/packages/frontend/core/src/modules/dnd/services/index.ts b/packages/frontend/core/src/modules/dnd/services/index.ts index 6de19bd119..421fec4876 100644 --- a/packages/frontend/core/src/modules/dnd/services/index.ts +++ b/packages/frontend/core/src/modules/dnd/services/index.ts @@ -1,4 +1,7 @@ -import type { ExternalGetDataFeedbackArgs } from '@affine/component'; +import type { + ExternalDataAdapter, + ExternalGetDataFeedbackArgs, +} from '@affine/component'; import type { AffineDNDData } from '@affine/core/types/dnd'; import type { DocsService, WorkspaceService } from '@toeverything/infra'; import { Service } from '@toeverything/infra'; @@ -23,7 +26,13 @@ export class DndService extends Service { private readonly resolvers = new Map(); - externalDataAdapter = (args: ExternalGetDataFeedbackArgs) => { + externalDataAdapter: ExternalDataAdapter = ( + args: ExternalGetDataFeedbackArgs, + isDropEvent?: boolean + ) => { + if (!isDropEvent) { + return {}; + } const from: AffineDNDData['draggable']['from'] = { at: 'external', }; @@ -43,6 +52,10 @@ export class DndService extends Service { } } + if (!entity) { + return {}; // no resolver can handle this data + } + return { from, entity, diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx index a4b132480e..b289cebc89 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx @@ -189,10 +189,9 @@ export const ExplorerCollectionNode = ({ const handleCanDrop = useMemo['canDrop']>( () => args => { const entityType = args.source.data.entity?.type; - const isExternalDrop = args.source.data.from?.at === 'external'; return args.treeInstruction?.type !== 'make-child' ? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true) - : entityType === 'doc' || isExternalDrop; + : entityType === 'doc'; }, [canDrop] ); diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx index 52f8e69779..08276dd4db 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx @@ -180,10 +180,9 @@ export const ExplorerDocNode = ({ const handleCanDrop = useMemo['canDrop']>( () => args => { const entityType = args.source.data.entity?.type; - const isExternalDrop = args.source.data.from?.at === 'external'; return args.treeInstruction?.type !== 'make-child' ? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true) - : entityType === 'doc' || isExternalDrop; + : entityType === 'doc'; }, [canDrop] ); diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/dnd.ts b/packages/frontend/core/src/modules/explorer/views/sections/favorites/dnd.ts index 32ba14fdf9..32aede8269 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/favorites/dnd.ts +++ b/packages/frontend/core/src/modules/explorer/views/sections/favorites/dnd.ts @@ -16,10 +16,8 @@ export const favoriteChildrenDropEffect: ExplorerTreeNodeDropEffect = data => { ) { return 'move'; } else if ( - (data.source.data.entity?.type && - isFavoriteSupportType(data.source.data.entity.type)) || - // always allow external drop - data.source.data.from?.at === 'external' + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) ) { return 'link'; } @@ -39,7 +37,7 @@ export const favoriteRootCanDrop: DropTargetOptions['canDrop'] = data => { return data.source.data.entity?.type ? isFavoriteSupportType(data.source.data.entity.type) - : data.source.data.from?.at === 'external'; // always allow external drop + : false; }; export const favoriteChildrenCanDrop: DropTargetOptions['canDrop'] = diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx b/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx index c514f6f0d4..e7c91b8bfd 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx @@ -3,11 +3,9 @@ import { Skeleton, useDropTarget, } from '@affine/component'; -import { DndService } from '@affine/core/modules/dnd/services'; import type { AffineDNDData } from '@affine/core/types/dnd'; import { useI18n } from '@affine/i18n'; import { FavoriteIcon } from '@blocksuite/icons/rc'; -import { useService } from '@toeverything/infra'; import { ExplorerEmptySection } from '../../layouts/empty-section'; import { DropEffect } from '../../tree'; @@ -23,7 +21,6 @@ const RootEmptyLoading = () => { }; const RootEmptyReady = ({ onDrop }: Omit) => { const t = useI18n(); - const dndService = useService(DndService); const { dropTargetRef, draggedOverDraggable, draggedOverPosition } = useDropTarget( @@ -33,9 +30,9 @@ const RootEmptyReady = ({ onDrop }: Omit) => { }, onDrop: onDrop, canDrop: favoriteRootCanDrop, - externalDataAdapter: dndService.externalDataAdapter, + allowExternal: true, }), - [dndService.externalDataAdapter, onDrop] + [onDrop] ); return ( diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/index.tsx b/packages/frontend/core/src/modules/explorer/views/sections/favorites/index.tsx index c10a936ba6..456a7941c6 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/favorites/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/favorites/index.tsx @@ -4,7 +4,6 @@ import { useDropTarget, } from '@affine/component'; import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils'; -import { DndService } from '@affine/core/modules/dnd/services'; import { DropEffect, ExplorerTreeRoot, @@ -21,7 +20,6 @@ import { track } from '@affine/track'; import { PlusIcon } from '@blocksuite/icons/rc'; import { useLiveData, - useService, useServices, WorkspaceService, } from '@toeverything/infra'; @@ -151,8 +149,6 @@ export const ExplorerFavorites = () => { [favoriteService] ); - const dndService = useService(DndService); - const { dropTargetRef, draggedOverDraggable, draggedOverPosition } = useDropTarget( () => ({ @@ -161,9 +157,9 @@ export const ExplorerFavorites = () => { }, onDrop: handleDrop, canDrop: favoriteRootCanDrop, - externalDataAdapter: dndService.externalDataAdapter, + allowExternal: true, }), - [dndService.externalDataAdapter, handleDrop] + [handleDrop] ); return ( 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 d47ed83d6f..9ec597e5b9 100644 --- a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx +++ b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx @@ -11,7 +11,6 @@ import { } from '@affine/component'; import { RenameModal } from '@affine/component/rename-modal'; import { AppSidebarService } from '@affine/core/modules/app-sidebar'; -import { DndService } from '@affine/core/modules/dnd/services'; import { WorkbenchLink } from '@affine/core/modules/workbench'; import type { AffineDNDData } from '@affine/core/types/dnd'; import { extractEmojiIcon } from '@affine/core/utils'; @@ -186,7 +185,6 @@ export const ExplorerTreeNode = ({ }, [canDrop, reorderable] ); - const dndService = useService(DndService); const { dropTargetRef, @@ -224,9 +222,7 @@ export const ExplorerTreeNode = ({ } }, canDrop: handleCanDrop, - externalDataAdapter(args) { - return dndService.externalDataAdapter(args) as any; - }, + allowExternal: true, }), [ dndData?.dropTarget, @@ -238,7 +234,6 @@ export const ExplorerTreeNode = ({ cid, onDrop, setCollapsed, - dndService, ] ); const isSelfDraggedOver = draggedOverDraggable?.data.__cid === cid;