diff --git a/packages/frontend/component/src/ui/dnd/draggable.ts b/packages/frontend/component/src/ui/dnd/draggable.ts index 21a1c703e8..0d98cc0a04 100644 --- a/packages/frontend/component/src/ui/dnd/draggable.ts +++ b/packages/frontend/component/src/ui/dnd/draggable.ts @@ -4,7 +4,11 @@ import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/elem import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview'; import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source'; import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'; -import type { DropTargetRecord } from '@atlaskit/pragmatic-drag-and-drop/types'; +import type { + BaseEventPayload, + DropTargetRecord, + ElementDragType, +} from '@atlaskit/pragmatic-drag-and-drop/types'; import { useContext, useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM, { flushSync } from 'react-dom'; @@ -20,6 +24,10 @@ import { export interface DraggableOptions { data?: DraggableGet; toExternalData?: toExternalData; + onDragStart?: (data: BaseEventPayload) => void; + onDrag?: (data: BaseEventPayload) => void; + onDrop?: (data: BaseEventPayload) => void; + onDropTargetChange?: (data: BaseEventPayload) => void; canDrag?: DraggableGet; disableDragPreview?: boolean; dragPreviewPosition?: DraggableDragPreviewPosition; @@ -126,8 +134,9 @@ export const useDraggable = ( if (dragRef.current) { dragRef.current.dataset['dragging'] = 'true'; } + options.onDragStart?.(args); }, - onDrop: () => { + onDrop: args => { if (enableDragging.current) { setDragging(false); } @@ -148,6 +157,7 @@ export const useDraggable = ( if (dragRef.current) { delete dragRef.current.dataset['dragging']; } + options.onDrop?.(args); }, onDrag: args => { if (enableDraggingPosition.current) { @@ -163,11 +173,13 @@ export const useDraggable = ( outWindow: prev.outWindow, })); } + options.onDrag?.(args); }, onDropTargetChange(args) { if (enableDropTarget.current) { setDropTarget(args.location.current.dropTargets); } + options.onDropTargetChange?.(args); }, onGenerateDragPreview({ nativeSetDragImage, source, location }) { if (options.disableDragPreview) { diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx index 1fb14a4a32..bbe446b366 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx @@ -22,6 +22,7 @@ import { JournalService } from '@affine/core/modules/journal'; import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench'; import type { AffineDNDData } from '@affine/core/types/dnd'; import { useI18n } from '@affine/i18n'; +import { track } from '@affine/track'; import type { Doc } from '@blocksuite/affine/store'; import { useLiveData, useService, type Workspace } from '@toeverything/infra'; import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; @@ -187,6 +188,9 @@ export function DetailPageHeader( id: page.id, }, }, + onDragStart: () => { + track.$.header.$.dragStart(); + }, dragPreviewPosition: 'pointer-outside', }; }, [page.id]); diff --git a/packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx b/packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx index 1ea0541fcb..2d61dade33 100644 --- a/packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx +++ b/packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx @@ -219,6 +219,11 @@ const WorkbenchTab = ({ 'text/uri-list': urls.join('\n'), }; }, + onDragStart: () => { + track.$.appTabsHeader.$.dragStart({ + type: 'tab', + }); + }, }; }, [dnd, workbench.basename, workbench.id, workbench.views]); diff --git a/packages/frontend/core/src/modules/dnd/services/index.ts b/packages/frontend/core/src/modules/dnd/services/index.ts index a099ddb19a..12d928a315 100644 --- a/packages/frontend/core/src/modules/dnd/services/index.ts +++ b/packages/frontend/core/src/modules/dnd/services/index.ts @@ -38,7 +38,15 @@ export class DndService extends Service { if (source.types.includes(type)) { const stringData = source.getStringData(type); if (stringData) { - return resolver(stringData); + const entity = resolver(stringData); + if (entity) { + return { + entity, + from: { + at: 'external', + }, + }; + } } } return null; @@ -48,7 +56,7 @@ export class DndService extends Service { private readonly resolvers: (( source: ExternalDragPayload - ) => Entity | null)[] = []; + ) => AffineDNDData['draggable'] | null)[] = []; getBlocksuiteDndAPI(sourceDocId?: string) { const collection = this.workspaceService.workspace.docCollection; @@ -74,29 +82,23 @@ export class DndService extends Service { if (!isDropEvent) { return {}; } - const from: AffineDNDData['draggable']['from'] = { - at: 'external', - }; - let entity: Entity | null = null; + let resolved: AffineDNDData['draggable'] | null = null; // in the order of the resolvers instead of the order of the types for (const resolver of this.resolvers) { const candidate = resolver(args.source); if (candidate) { - entity = candidate; + resolved = candidate; break; } } - if (!entity) { + if (!resolved) { return {}; // no resolver can handle this data } - return { - from, - entity, - }; + return resolved; }; toExternalData: toExternalData = (args, data) => { @@ -160,7 +162,7 @@ export class DndService extends Service { private readonly resolveBlocksuiteExternalData = ( source: ExternalDragPayload - ): Entity | null => { + ): AffineDNDData['draggable'] | null => { const dndAPI = this.getBlocksuiteDndAPI(); if (!dndAPI) { return null; @@ -173,7 +175,16 @@ export class DndService extends Service { if (!snapshot) { return null; } - return this.resolveBlockSnapshot(snapshot); + const entity = this.resolveBlockSnapshot(snapshot); + if (!entity) { + return null; + } + return { + entity, + from: { + at: 'blocksuite-editor', + }, + }; }; private readonly resolveHTML: EntityResolver = html => { 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 b289cebc89..22a8c097a0 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 @@ -125,6 +125,9 @@ export const ExplorerCollectionNode = ({ target: 'doc', control: 'drag', }); + track.$.navigationPanel.collections.drop({ + type: data.source.data.entity.type, + }); } } else { onDrop?.(data); 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 08276dd4db..526f5c5dc0 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 @@ -138,6 +138,9 @@ export const ExplorerDocNode = ({ track.$.navigationPanel.docs.linkDoc({ control: 'drag', }); + track.$.navigationPanel.docs.drop({ + type: data.source.data.entity.type, + }); } else { toast(t['com.affine.rootAppSidebar.doc.link-doc-only']()); } @@ -170,6 +173,9 @@ export const ExplorerDocNode = ({ track.$.navigationPanel.docs.linkDoc({ control: 'drag', }); + track.$.navigationPanel.docs.drop({ + type: data.source.data.entity.type, + }); } else { toast(t['com.affine.rootAppSidebar.doc.link-doc-only']()); } diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/folder/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/folder/index.tsx index 9da003d49d..e49fb08a51 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/folder/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/folder/index.tsx @@ -241,6 +241,11 @@ const ExplorerFolderNodeFolder = ({ const handleDropOnFolder = useCallback( (data: DropTargetDropEvent) => { + if (data.source.data.entity?.type) { + track.$.navigationPanel.folders.drop({ + type: data.source.data.entity.type, + }); + } if (data.treeInstruction?.type === 'make-child') { if (data.source.data.entity?.type === 'folder') { if ( @@ -313,6 +318,11 @@ const ExplorerFolderNodeFolder = ({ const handleDropOnPlaceholder = useCallback( (data: DropTargetDropEvent) => { + if (data.source.data.entity?.type) { + track.$.navigationPanel.folders.drop({ + type: data.source.data.entity.type, + }); + } if (data.source.data.entity?.type === 'folder') { if ( node.id === data.source.data.entity.id || @@ -353,6 +363,11 @@ const ExplorerFolderNodeFolder = ({ if (!dropAtNode || !dropAtNode.id) { return; } + if (data.source.data.entity?.type) { + track.$.navigationPanel.folders.drop({ + type: data.source.data.entity.type, + }); + } if ( data.treeInstruction?.type === 'reorder-above' || data.treeInstruction?.type === 'reorder-below' diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/tag/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/tag/index.tsx index e2c4af2e0f..0a17b2edab 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/tag/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/tag/index.tsx @@ -100,6 +100,9 @@ export const ExplorerTagNode = ({ track.$.navigationPanel.tags.tagDoc({ control: 'drag', }); + track.$.navigationPanel.tags.drop({ + type: data.source.data.entity.type, + }); } else { toast(t['com.affine.rootAppSidebar.tag.doc-only']()); } 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 e403eb6c45..165b273258 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 @@ -73,6 +73,9 @@ export const ExplorerFavorites = () => { type: data.source.data.entity.type, on: true, }); + track.$.navigationPanel.favorites.drop({ + type: data.source.data.entity.type, + }); explorerSection.setCollapsed(false); } }, @@ -141,6 +144,9 @@ export const ExplorerFavorites = () => { type: data.source.data.entity.type, on: true, }); + track.$.navigationPanel.favorites.drop({ + type: data.source.data.entity.type, + }); } else { return; // not supported } diff --git a/packages/frontend/core/src/types/dnd.ts b/packages/frontend/core/src/types/dnd.ts index 03efb7fb1a..51f8abee51 100644 --- a/packages/frontend/core/src/types/dnd.ts +++ b/packages/frontend/core/src/types/dnd.ts @@ -80,7 +80,10 @@ export interface AffineDNDData extends DNDData { docId: string; } | { - at: 'external'; // for blocksuite or external apps + at: 'blocksuite-editor'; + } + | { + at: 'external'; // for external apps }; }; dropTarget: diff --git a/packages/frontend/track/src/events.ts b/packages/frontend/track/src/events.ts index 10405f8291..638d540067 100644 --- a/packages/frontend/track/src/events.ts +++ b/packages/frontend/track/src/events.ts @@ -86,6 +86,8 @@ type OrganizeEvents = | FolderEvents | TagEvents | FavoriteEvents; + +type DNDEvents = 'dragStart' | 'drag' | 'drop'; // END SECTION // SECTION: cloud events @@ -127,7 +129,8 @@ type UserEvents = | ShareEvents | AuthEvents | AccountEvents - | PaymentEvents; + | PaymentEvents + | DNDEvents; interface PageDivision { [page: string]: { [segment: string]: { @@ -209,12 +212,18 @@ const PageEvents = { 'openInNewTab', 'openInSplitView', 'toggleFavorite', + 'drop', ], - docs: ['createDoc', 'deleteDoc', 'linkDoc'], - collections: ['createDoc', 'addDocToCollection', 'removeOrganizeItem'], - folders: ['createDoc'], - tags: ['createDoc', 'tagDoc'], - favorites: ['createDoc'], + docs: ['createDoc', 'deleteDoc', 'linkDoc', 'drop'], + collections: [ + 'createDoc', + 'addDocToCollection', + 'removeOrganizeItem', + 'drop', + ], + folders: ['createDoc', 'drop'], + tags: ['createDoc', 'tagDoc', 'drop'], + favorites: ['createDoc', 'drop'], migrationData: ['openMigrationDataHelp'], bottomButtons: [ 'downloadApp', @@ -248,9 +257,10 @@ const PageEvents = { aiAction: ['viewPlans'], }, appTabsHeader: { - $: ['tabAction'], + $: ['tabAction', 'dragStart'], }, header: { + $: ['dragStart'], actions: [ 'createDoc', 'createWorkspace', @@ -423,6 +433,8 @@ export type EventArgs = { editProperty: { type: string }; addProperty: { type: string; control: 'at menu' | 'property list' }; linkDoc: { type: string; journal: boolean }; + drop: { type: string }; + dragStart: { type: string }; }; // for type checking