feat(core): drop doc onto split view (#9487)

fix AF-2068, AF-2069, AF-1175, AF-2061, AF-2079, AF-2034, AF-2080, AF-1960, AF-2081

1. replace `dnd-kit` with `@atlaskit/pragmatic-drag-and-drop`
2. allow creating split views by drag & drop the following
   a. WorkbenchLinks (route links), like journals, trash, all docs
   b. doc refs
   c. tags/collection
3. style adjustments to split view
4. remove split view's feature flag and make it GA for electron

https://github.com/user-attachments/assets/6a3e4a25-faa2-4215-8eb0-983f44db6e8c
This commit is contained in:
pengx17
2025-01-08 05:05:33 +00:00
parent c0ed74dfed
commit a4841bbfa3
53 changed files with 1574 additions and 905 deletions

View File

@@ -0,0 +1,33 @@
import type { DNDData, fromExternalData } from './types';
export const isExternalDrag = <D extends DNDData>(args: {
source: { data: D['draggable'] };
}) => {
return !args.source['data'];
};
export const getAdaptedEventArgs = <
D extends DNDData,
Args extends { source: { data: D['draggable'] } },
>(
args: Args,
fromExternalData?: fromExternalData<D>,
isDropEvent = false
): Args => {
const data =
isExternalDrag(args) && fromExternalData
? fromExternalData(
// @ts-expect-error hack for external data adapter (source has no data field)
args as ExternalGetDataFeedbackArgs,
isDropEvent
)
: args.source['data'];
return {
...args,
source: {
...args.source,
data,
},
};
};

View File

@@ -71,7 +71,6 @@ export const useDraggable = <D extends DNDData = DNDData>(
const context = useContext(DNDContext);
// eslint-disable-next-line react-hooks/exhaustive-deps
const options = useMemo(() => {
const opts = getOptions();

View File

@@ -19,6 +19,8 @@ import {
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { shallowUpdater } from '../../utils';
import { getAdaptedEventArgs, isExternalDrag } from './common';
import { DNDContext } from './context';
import type { DNDData, fromExternalData } from './types';
@@ -44,7 +46,7 @@ export type DropTargetTreeInstruction = Instruction;
export type ExternalDragPayload = ExternalDragType['payload'];
type DropTargetGetFeedback<D extends DNDData> = Parameters<
export type DropTargetGetFeedback<D extends DNDData> = Parameters<
NonNullable<Parameters<typeof dropTargetForElements>[0]['canDrop']>
>[0] & {
source: {
@@ -59,38 +61,6 @@ type DropTargetGet<T, D extends DNDData> =
| T
| ((data: DropTargetGetFeedback<D>) => T);
const isExternalDrag = <D extends DNDData>(
args: Pick<DropTargetGetFeedback<D>, 'source'>
) => {
return !args.source['data'];
};
const getAdaptedEventArgs = <
D extends DNDData,
Args extends Pick<DropTargetGetFeedback<D>, 'source'>,
>(
options: DropTargetOptions<D>,
args: Args,
isDropEvent = false
): Args => {
const data =
isExternalDrag(args) && options.fromExternalData
? options.fromExternalData(
// @ts-expect-error hack for external data adapter (source has no data field)
args as ExternalGetDataFeedbackArgs,
isDropEvent
)
: args.source['data'];
return {
...args,
source: {
...args.source,
data,
},
};
};
function dropTargetGet<T, D extends DNDData>(
get: T,
options: DropTargetOptions<D>
@@ -110,7 +80,7 @@ function dropTargetGet<T, D extends DNDData>(
) => {
if (typeof get === 'function') {
return (get as any)({
...getAdaptedEventArgs(options, args),
...getAdaptedEventArgs(args, options.fromExternalData),
get treeInstruction() {
return options.treeInstruction
? extractInstruction(
@@ -145,8 +115,8 @@ function dropTargetGet<T, D extends DNDData>(
});
} else {
return {
...getAdaptedEventArgs(args, options.fromExternalData),
...get,
...getAdaptedEventArgs(options, args),
};
}
}) as any;
@@ -168,6 +138,8 @@ export interface DropTargetOptions<D extends DNDData = DNDData> {
};
onDrop?: (data: DropTargetDropEvent<D>) => void;
onDrag?: (data: DropTargetDragEvent<D>) => void;
onDragEnter?: (data: DropTargetDragEvent<D>) => void;
onDragLeave?: (data: DropTargetDragEvent<D>) => void;
/**
* external data adapter.
* Will use the external data adapter from the context if not provided.
@@ -232,6 +204,55 @@ export const useDropTarget = <D extends DNDData = DNDData>(
const dropTargetOptions = useMemo(() => {
const wrappedCanDrop = dropTargetGet(options.canDrop, options);
let _element: HTMLElement | null = null;
const updateDragOver = (
args: DropTargetDragEvent<D>,
handler?: (data: DropTargetDragEvent<D>) => void
) => {
args = getAdaptedEventArgs(args, options.fromExternalData);
if (
args.location.current.dropTargets[0]?.element === dropTargetRef.current
) {
if (enableDraggedOverDraggable.current) {
setDraggedOverDraggable(shallowUpdater(args.source));
}
let instruction = null;
let closestEdge = null;
if (options.treeInstruction) {
instruction = extractInstruction(args.self.data);
setTreeInstruction(shallowUpdater(instruction));
if (dropTargetRef.current) {
dropTargetRef.current.dataset['treeInstruction'] =
instruction?.type;
}
}
if (options.closestEdge) {
closestEdge = extractClosestEdge(args.self.data);
setClosestEdge(shallowUpdater(closestEdge));
}
if (enableDropEffect.current) {
setDropEffect(shallowUpdater(args.self.dropEffect));
}
if (enableDraggedOverPosition.current) {
const rect = args.self.element.getBoundingClientRect();
const { clientX, clientY } = args.location.current.input;
setDraggedOverPosition(
shallowUpdater({
relativeX: clientX - rect.x,
relativeY: clientY - rect.y,
clientX: clientX,
clientY: clientY,
})
);
}
handler?.({
...args,
treeInstruction: instruction,
closestEdge,
} as DropTargetDropEvent<D>);
}
};
return {
get element() {
if (!_element) {
@@ -285,7 +306,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
// external data is only available in drop event thus
// this is the only case for getAdaptedEventArgs
const args = getAdaptedEventArgs(options, _args, true);
const args = getAdaptedEventArgs(_args, options.fromExternalData, true);
if (
isExternalDrag(_args) &&
options.fromExternalData &&
@@ -309,7 +330,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
}
},
getData: (args: DropTargetGetFeedback<D>) => {
args = getAdaptedEventArgs(options, args);
args = getAdaptedEventArgs(args, options.fromExternalData);
const originData = dropTargetGet(options.data ?? {}, options)(args);
const { input, element } = args;
const withInstruction = options.treeInstruction
@@ -332,50 +353,29 @@ export const useDropTarget = <D extends DNDData = DNDData>(
return withClosestEdge;
},
onDrag: (args: DropTargetDragEvent<D>) => {
args = getAdaptedEventArgs(options, args);
if (
args.location.current.dropTargets[0]?.element ===
dropTargetRef.current
) {
if (enableDraggedOverDraggable.current) {
setDraggedOverDraggable(args.source);
}
let instruction = null;
let closestEdge = null;
if (options.treeInstruction) {
instruction = extractInstruction(args.self.data);
setTreeInstruction(instruction);
if (dropTargetRef.current) {
dropTargetRef.current.dataset['treeInstruction'] =
instruction?.type;
}
}
if (options.closestEdge) {
closestEdge = extractClosestEdge(args.self.data);
setClosestEdge(closestEdge);
}
if (enableDropEffect.current) {
setDropEffect(args.self.dropEffect);
}
if (enableDraggedOverPosition.current) {
const rect = args.self.element.getBoundingClientRect();
const { clientX, clientY } = args.location.current.input;
setDraggedOverPosition({
relativeX: clientX - rect.x,
relativeY: clientY - rect.y,
clientX: clientX,
clientY: clientY,
});
}
options.onDrag?.({
...args,
treeInstruction: instruction,
closestEdge,
} as DropTargetDropEvent<D>);
}
updateDragOver(args, options.onDrag);
},
onDragEnter: (args: DropTargetDragEvent<D>) => {
updateDragOver(args, options.onDragEnter);
},
onDragLeave: (args: DropTargetDragEvent<D>) => {
args = getAdaptedEventArgs(args, options.fromExternalData);
const withClosestEdge = options.closestEdge
? attachClosestEdge(args.self.data, {
element: args.self.element,
input: args.location.current.input,
allowedEdges: options.closestEdge.allowedEdges,
})
: args.self.data;
options.onDragLeave?.({
...args,
self: { ...args.self, data: withClosestEdge },
});
},
onDropTargetChange: (args: DropTargetDropEvent<D>) => {
args = getAdaptedEventArgs(options, args);
args = getAdaptedEventArgs(args, options.fromExternalData);
if (
args.location.current.dropTargets[0]?.element ===
dropTargetRef.current

View File

@@ -2,4 +2,5 @@ export * from './context';
export * from './draggable';
export * from './drop-indicator';
export * from './drop-target';
export * from './monitor';
export * from './types';

View File

@@ -0,0 +1,121 @@
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
import type {
DragLocationHistory,
ElementDragType,
} from '@atlaskit/pragmatic-drag-and-drop/types';
import { useContext, useEffect, useMemo } from 'react';
import { getAdaptedEventArgs } from './common';
import { DNDContext } from './context';
import type { DNDData, fromExternalData } from './types';
type MonitorGetFeedback<D extends DNDData = DNDData> = Parameters<
NonNullable<Parameters<typeof monitorForElements>[0]['canMonitor']>
>[0] & {
source: {
data: D['draggable'];
};
};
type MonitorGet<T, D extends DNDData = DNDData> =
| T
| ((data: MonitorGetFeedback<D>) => T);
type MonitorDragEvent<D extends DNDData = DNDData> = {
/**
* 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'];
};
};
export interface MonitorOptions<D extends DNDData = DNDData> {
canMonitor?: MonitorGet<boolean, D>;
onDragStart?: (data: MonitorDragEvent<D>) => void;
onDrag?: (data: MonitorDragEvent<D>) => void;
onDrop?: (data: MonitorDragEvent<D>) => void;
onDropTargetChange?: (data: MonitorDragEvent<D>) => void;
/**
* external data adapter.
* Will use the external data adapter from the context if not provided.
*/
fromExternalData?: fromExternalData<D>;
/**
* Make the drop target allow external data.
* If this is undefined, it will be set to true if fromExternalData is provided.
*
* @default undefined
*/
allowExternal?: boolean;
}
function monitorGet<D extends DNDData, T>(
get: T,
options: MonitorOptions<D>
): T extends undefined
? undefined
: T extends MonitorGet<infer I>
? (args: MonitorGetFeedback<D>) => I
: never {
if (get === undefined) {
return undefined as any;
}
return ((args: MonitorGetFeedback<D>) => {
const adaptedArgs = getAdaptedEventArgs(args, options.fromExternalData);
return typeof get === 'function'
? (get as any)(adaptedArgs)
: {
...adaptedArgs,
...get,
};
}) as any;
}
export const useDndMonitor = <D extends DNDData = DNDData>(
getOptions: () => MonitorOptions<D> = () => ({}),
deps: any[] = []
) => {
const dropTargetContext = useContext(DNDContext);
const options = useMemo(() => {
const opts = getOptions();
const allowExternal = opts.allowExternal ?? !!opts.fromExternalData;
return {
...opts,
allowExternal,
fromExternalData: allowExternal
? (opts.fromExternalData ??
(dropTargetContext.fromExternalData as fromExternalData<D>))
: undefined,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, getOptions]);
const monitorOptions = useMemo(() => {
return {
canMonitor: monitorGet(options.canMonitor, options),
onDragStart: monitorGet(options.onDragStart, options),
onDrag: monitorGet(options.onDrag, options),
onDrop: monitorGet(options.onDrop, options),
onDropTargetChange: monitorGet(options.onDropTargetChange, options),
};
}, [options]);
useEffect(() => {
return monitorForElements(monitorOptions);
}, [monitorOptions]);
useEffect(() => {
if (!options.fromExternalData) {
return;
}
// @ts-expect-error external & element adapter types have some subtle differences
return monitorForExternal(monitorOptions);
}, [monitorOptions, options.fromExternalData]);
};

View File

@@ -1,6 +1,8 @@
import type { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import type { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
export type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
export interface DNDData<
Draggable extends Record<string, unknown> = Record<string, unknown>,
DropTarget extends Record<string, unknown> = Record<string, unknown>,