diff --git a/webview/package.json b/webview/package.json
index 7f85fb28b7..95d422d1f2 100644
--- a/webview/package.json
+++ b/webview/package.json
@@ -26,7 +26,6 @@
"@vscode/webview-ui-toolkit": "^1.0.0",
"classnames": "^2.2.6",
"react": "^17.0.1",
- "react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1",
"react-table": "^7.7.0",
"react-vega": "^7.4.4",
@@ -49,7 +48,6 @@
"@types/jsdom": "^16.2.6",
"@types/node": "^16.11.8",
"@types/react": "^17.0.35",
- "@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.11",
"@types/react-measure": "^2.0.8",
"@types/react-table": "^7.7.8",
@@ -66,7 +64,6 @@
"jest-canvas-mock": "^2.3.1",
"lint-staged": "^12.3.7",
"raw-loader": "^4.0.2",
- "react-beautiful-dnd-test-utils": "^3.2.2",
"sass": "^1.43.4",
"sass-loader": "^12.3.0",
"style-loader": "^3.3.1",
diff --git a/webview/src/experiments/components/App.test.tsx b/webview/src/experiments/components/App.test.tsx
index 3809a0f1b1..5bcaf5df05 100644
--- a/webview/src/experiments/components/App.test.tsx
+++ b/webview/src/experiments/components/App.test.tsx
@@ -18,12 +18,6 @@ import {
MessageFromWebviewType,
MessageToWebviewType
} from 'dvc/src/webview/contract'
-import {
- mockDndElSpacing,
- mockGetComputedSpacing,
- makeDnd,
- DND_DIRECTION_RIGHT
-} from 'react-beautiful-dnd-test-utils'
import {
Column,
ColumnType,
@@ -38,7 +32,6 @@ import { vsCodeApi } from '../../shared/api'
import {
commonColumnFields,
expectHeaders,
- makeGetDragEl,
tableData as sortingTableDataFixture
} from '../../test/sort'
import {
@@ -46,6 +39,8 @@ import {
HEADER_TOOLTIP_DELAY
} from '../../shared/components/tooltip/Tooltip'
import { getRow } from '../../test/queries'
+import { dragAndDrop } from '../../test/dragDrop'
+import { DragEnterDirection } from '../../shared/components/dragDrop/util'
jest.mock('../../shared/api')
jest.mock('../../util/styles')
@@ -200,9 +195,7 @@ describe('App', () => {
})
it('should be able to order a column to the final space after a new column is added', async () => {
- const view = render()
- mockDndElSpacing(view)
- mockGetComputedSpacing()
+ render()
fireEvent(
window,
new MessageEvent('message', {
@@ -236,12 +229,10 @@ describe('App', () => {
})
)
- await makeDnd({
- direction: DND_DIRECTION_RIGHT,
- getByText: view.getByText,
- getDragEl: makeGetDragEl('B'),
- positions: 2
- })
+ const headerB = screen.getByText('B')
+ const headerD = screen.getByText('D')
+
+ dragAndDrop(headerB, headerD, DragEnterDirection.AUTO)
await expectHeaders(['A', 'C', 'D', 'B'])
})
diff --git a/webview/src/experiments/components/Experiments.tsx b/webview/src/experiments/components/Experiments.tsx
index e6d26f7d55..a5cfc8660b 100644
--- a/webview/src/experiments/components/Experiments.tsx
+++ b/webview/src/experiments/components/Experiments.tsx
@@ -24,6 +24,7 @@ import buildDynamicColumns from '../util/buildDynamicColumns'
import { sendMessage } from '../../shared/vscode'
import { useThemeVariables } from '../../shared/components/theme/Theme'
import { EmptyState } from '../../shared/components/emptyState/EmptyState'
+import { DragDropProvider } from '../../shared/components/dragDrop/DragDropContext'
const DEFAULT_COLUMN_WIDTH = 75
const MINIMUM_COLUMN_WIDTH = 50
@@ -208,7 +209,11 @@ export const ExperimentsTable: React.FC<{
return No Experiments to Display.
}
- return
+ return (
+
+
+
+ )
}
const Experiments: React.FC<{
diff --git a/webview/src/experiments/components/table/MergeHeaderGroups.tsx b/webview/src/experiments/components/table/MergeHeaderGroups.tsx
index 66b9a500bb..fcbffd3869 100644
--- a/webview/src/experiments/components/table/MergeHeaderGroups.tsx
+++ b/webview/src/experiments/components/table/MergeHeaderGroups.tsx
@@ -3,54 +3,50 @@ import cx from 'classnames'
import { SortDefinition } from 'dvc/src/experiments/model/sortBy'
import { Experiment, Column } from 'dvc/src/experiments/webview/contract'
import { HeaderGroup } from 'react-table'
-import { DragDropContext, Droppable, Responders } from 'react-beautiful-dnd'
import { TableHeader } from './TableHeader'
import styles from './styles.module.scss'
+import {
+ OnDragOver,
+ OnDragStart,
+ OnDrop
+} from '../../../shared/components/dragDrop/DragDropWorkbench'
-export const MergedHeaderGroup: React.FC<
- {
- headerGroup: HeaderGroup
- columns: HeaderGroup[]
- sorts: SortDefinition[]
- orderedColumns: Column[]
- } & Responders
-> = ({
+export const MergedHeaderGroups: React.FC<{
+ headerGroup: HeaderGroup
+ columns: HeaderGroup[]
+ sorts: SortDefinition[]
+ orderedColumns: Column[]
+ onDragUpdate: OnDragOver
+ onDragStart: OnDragStart
+ onDragEnd: OnDrop
+}> = ({
headerGroup,
sorts,
columns,
orderedColumns,
- onDragStart,
onDragUpdate,
- onDragEnd
+ onDragEnd,
+ onDragStart
}) => {
return (
-
-
- {provided => (
-
- {headerGroup.headers.map((column: HeaderGroup
, i) => (
-
- ))}
- {provided.placeholder}
-
- )}
-
-
+ {headerGroup.headers.map((column: HeaderGroup) => (
+
+ ))}
+
)
}
diff --git a/webview/src/experiments/components/table/Table.test.tsx b/webview/src/experiments/components/table/Table.test.tsx
index 6fac74354c..9c0a5a65a5 100644
--- a/webview/src/experiments/components/table/Table.test.tsx
+++ b/webview/src/experiments/components/table/Table.test.tsx
@@ -14,13 +14,7 @@ import { Experiment, TableData } from 'dvc/src/experiments/webview/contract'
import { MessageFromWebviewType } from 'dvc/src/webview/contract'
import React from 'react'
import { TableInstance } from 'react-table'
-import {
- mockGetComputedSpacing,
- mockDndElSpacing,
- makeDnd,
- DND_DIRECTION_LEFT,
- DND_DIRECTION_RIGHT
-} from 'react-beautiful-dnd-test-utils'
+import tableDataFixture from 'dvc/src/test/fixtures/expShow/tableData'
import { SortOrderLabel } from './SortPicker'
import { Table } from './Table'
import styles from './styles.module.scss'
@@ -30,9 +24,11 @@ import * as ColumnOrder from '../../hooks/useColumnOrder'
import { vsCodeApi } from '../../../shared/api'
import {
expectHeaders,
- makeGetDragEl,
+ getHeaders,
tableData as sortingTableDataFixture
} from '../../../test/sort'
+import { dragAndDrop } from '../../../test/dragDrop'
+import { DragEnterDirection } from '../../../shared/components/dragDrop/util'
jest.mock('../../../shared/api')
const { postMessage } = vsCodeApi
@@ -127,11 +123,7 @@ describe('Table', () => {
const renderExperimentsTable = (
data: TableData = sortingTableDataFixture
) => {
- const view = render()
-
- mockDndElSpacing(view)
-
- return view
+ return render()
}
beforeAll(() => {
@@ -293,45 +285,38 @@ describe('Table', () => {
})
describe('Columns order', () => {
- beforeEach(() => {
- mockGetComputedSpacing()
- })
-
it('should move a column from its current position to its new position', async () => {
- const { getByText } = renderExperimentsTable()
+ renderExperimentsTable()
await expectHeaders(['A', 'B', 'C'])
- await makeDnd({
- direction: DND_DIRECTION_LEFT,
- getByText,
- getDragEl: makeGetDragEl('C'),
- positions: 1
- })
+ dragAndDrop(
+ screen.getByText('B'),
+ screen.getByText('C'),
+ DragEnterDirection.AUTO
+ )
await expectHeaders(['A', 'C', 'B'])
- await makeDnd({
- direction: DND_DIRECTION_RIGHT,
- getByText,
- getDragEl: makeGetDragEl('A'),
- positions: 2
- })
+ dragAndDrop(
+ screen.getByText('A'),
+ screen.getByText('B'),
+ DragEnterDirection.AUTO
+ )
await expectHeaders(['C', 'B', 'A'])
})
it('should not move a column before the default columns', async () => {
- const { getByText } = renderExperimentsTable()
+ renderExperimentsTable()
- await makeDnd({
- direction: DND_DIRECTION_LEFT,
- getByText,
- getDragEl: makeGetDragEl('B'),
- positions: 3
- })
+ dragAndDrop(
+ screen.getByText('B'),
+ screen.getByText('Timestamp'),
+ DragEnterDirection.AUTO
+ )
- await expectHeaders(['B', 'A', 'C'])
+ await expectHeaders(['A', 'B', 'C'])
})
it('should order the columns with the columnOrder from the data', async () => {
@@ -378,5 +363,42 @@ describe('Table', () => {
type: MessageFromWebviewType.RESIZE_COLUMN
})
})
+
+ it('should move all the columns from a group from their current position to their new position', async () => {
+ renderExperimentsTable({ ...tableDataFixture })
+
+ let headers = await getHeaders()
+
+ expect(headers.indexOf('threshold')).toBeGreaterThan(
+ headers.indexOf('loss')
+ )
+ expect(headers.indexOf('test')).toBeGreaterThan(
+ headers.indexOf('accuracy')
+ )
+
+ dragAndDrop(
+ screen.getByText('process'),
+ screen.getByText('loss'),
+ DragEnterDirection.AUTO
+ )
+
+ headers = await getHeaders()
+
+ expect(headers.indexOf('loss')).toBeGreaterThan(
+ headers.indexOf('threshold')
+ )
+
+ dragAndDrop(
+ screen.getByText('summary.json'),
+ screen.getByText('test'),
+ DragEnterDirection.AUTO
+ )
+
+ headers = await getHeaders()
+
+ expect(headers.indexOf('accuracy')).toBeGreaterThan(
+ headers.indexOf('test')
+ )
+ })
})
})
diff --git a/webview/src/experiments/components/table/TableHead.tsx b/webview/src/experiments/components/table/TableHead.tsx
index 7847f51db0..1cdfaddd4d 100644
--- a/webview/src/experiments/components/table/TableHead.tsx
+++ b/webview/src/experiments/components/table/TableHead.tsx
@@ -2,12 +2,16 @@ import { SortDefinition } from 'dvc/src/experiments/model/sortBy'
import { Experiment, Column } from 'dvc/src/experiments/webview/contract'
import React, { useRef } from 'react'
import { HeaderGroup, TableInstance } from 'react-table'
-import { DragUpdate } from 'react-beautiful-dnd'
import { MessageFromWebviewType } from 'dvc/src/webview/contract'
import styles from './styles.module.scss'
-import { MergedHeaderGroup } from './MergeHeaderGroups'
+import { MergedHeaderGroups } from './MergeHeaderGroups'
import { useColumnOrder } from '../../hooks/useColumnOrder'
import { sendMessage } from '../../../shared/vscode'
+import { leafColumnIds, reorderColumnIds } from '../../util/columns'
+import {
+ OnDragOver,
+ OnDragStart
+} from '../../../shared/components/dragDrop/DragDropWorkbench'
interface TableHeadProps {
instance: TableInstance
@@ -32,26 +36,44 @@ export const TableHead: React.FC = ({
}
const fullColumnOrder = useRef()
+ const draggingIds = useRef()
- const onDragStart = () => {
- fullColumnOrder.current = allColumns.map(column => column.id)
+ const onDragStart: OnDragStart = draggedId => {
+ const displacerHeader = allHeaders.find(header => header.id === draggedId)
+ if (displacerHeader) {
+ draggingIds.current = leafColumnIds(displacerHeader)
+ fullColumnOrder.current = allColumns.map(({ id }) => id)
+ }
}
- const onDragUpdate = (column: DragUpdate) => {
- if (!column.destination) {
- return
- }
- const { draggableId, destination } = column
- if (destination.index > 1) {
- const newColumnOrder = [...(fullColumnOrder.current as string[])]
- const oldIndex = newColumnOrder.indexOf(draggableId)
- newColumnOrder.splice(oldIndex, 1)
- newColumnOrder.splice(destination.index, 0, draggableId)
- setColumnOrder(newColumnOrder)
- }
+ const findDisplacedHeader = (
+ draggedOverId: string,
+ cb: (displacedHeader: HeaderGroup) => void
+ ) => {
+ const displacedHeader = allHeaders.find(
+ header => header.id === draggedOverId
+ )
+
+ displacedHeader && cb(displacedHeader)
+ }
+
+ const onDragUpdate: OnDragOver = (_, draggedOverId: string) => {
+ const displacer = draggingIds.current
+ displacer &&
+ findDisplacedHeader(draggedOverId, displacedHeader => {
+ const displaced = leafColumnIds(displacedHeader)
+ if (!displaced.some(id => displacer.includes(id))) {
+ fullColumnOrder.current &&
+ setColumnOrder(
+ reorderColumnIds(fullColumnOrder.current, displacer, displaced)
+ )
+ }
+ })
}
const onDragEnd = () => {
+ draggingIds.current = undefined
+ fullColumnOrder.current = undefined
sendMessage({
payload: columnOrder,
type: MessageFromWebviewType.REORDER_COLUMNS
@@ -62,7 +84,7 @@ export const TableHead: React.FC = ({
{headerGroups.map(headerGroup => (
// eslint-disable-next-line react/jsx-key
-
- provided: DraggableProvided
- snapshot: DraggableStateSnapshot
-}> = ({ provided, snapshot, column }) => {
+ onDragOver: OnDragOver
+ onDragStart: OnDragStart
+ onDrop: OnDrop
+}> = ({ disabled, column, onDragOver, onDragStart, onDrop }) => {
+ const DropTarget = {column?.name}
+
return (
{
e.stopPropagation()
}}
role={'columnheader'}
tabIndex={0}
>
- {column.render('Header')}
+
+ {column?.render('Header')}
+
)
}
@@ -52,19 +62,21 @@ const TableHeaderCell: React.FC<{
columns: HeaderGroup[]
orderedColumns: Column[]
sortOrder: SortOrder
- provided: DraggableProvided
- snapshot: DraggableStateSnapshot
menuDisabled: boolean
menuContent: React.ReactNode
+ onDragOver: OnDragOver
+ onDragStart: OnDragStart
+ onDrop: OnDrop
}> = ({
column,
columns,
orderedColumns,
sortOrder,
- provided,
- snapshot,
menuContent,
- menuDisabled
+ menuDisabled,
+ onDragOver,
+ onDragStart,
+ onDrop
}) => {
const isPlaceholder = !!column.placeholderOf
const canResize = column.canResize && !isPlaceholder
@@ -94,6 +106,8 @@ const TableHeaderCell: React.FC<{
)
}
}
+ const isDraggable =
+ !column.placeholderOf && !['id', 'timestamp'].includes(column.id)
return (
@@ -104,8 +118,10 @@ const TableHeaderCell: React.FC<{
>
{canResize && (
columns: HeaderGroup
[]
sorts: SortDefinition[]
- index: number
orderedColumns: Column[]
+ onDragOver: OnDragOver
+ onDragStart: OnDragStart
+ onDrop: OnDrop
}
export const TableHeader: React.FC = ({
column,
columns,
sorts,
- index,
- orderedColumns
+ orderedColumns,
+ onDragOver,
+ onDragStart,
+ onDrop
}) => {
const baseColumn = column.placeholderOf || column
const sort = sorts.find(sort => sort.path === baseColumn.id)
- const isDraggable =
+ const isSortable =
!column.placeholderOf &&
!['id', 'timestamp'].includes(column.id) &&
!column.columns
- const isSortable = isDraggable
const sortOrder: SortOrder = (() => {
const possibleOrders = {
@@ -177,31 +196,23 @@ export const TableHeader: React.FC = ({
}
return (
-
- {(provided, snapshot) => (
- {
- setColumnSort(order)
- }}
- />
- }
+ setSelectedOrder={order => {
+ setColumnSort(order)
+ }}
/>
- )}
-
+ }
+ />
)
}
diff --git a/webview/src/experiments/components/table/styles.module.scss b/webview/src/experiments/components/table/styles.module.scss
index 1a0a9b386a..901f59934f 100644
--- a/webview/src/experiments/components/table/styles.module.scss
+++ b/webview/src/experiments/components/table/styles.module.scss
@@ -249,6 +249,11 @@ $workspace-row-edge-margin: $edge-padding - $cell-padding;
}
.cellContents {
@extend %truncateLeftChild;
+ display: block;
+
+ span[draggable='true'] {
+ display: block;
+ }
}
&:last-child,
@@ -335,6 +340,7 @@ $workspace-row-edge-margin: $edge-padding - $cell-padding;
white-space: nowrap;
min-width: 0;
position: relative;
+ height: 100%;
}
.td {
font-size: 0.8rem;
@@ -406,6 +412,8 @@ $workspace-row-edge-margin: $edge-padding - $cell-padding;
}
.draggingColumn {
+ border: 1px solid $fg-color;
+ font-size: 0.7rem;
opacity: 0.7;
}
diff --git a/webview/src/experiments/util/columns.test.ts b/webview/src/experiments/util/columns.test.ts
new file mode 100644
index 0000000000..fd23e5ff63
--- /dev/null
+++ b/webview/src/experiments/util/columns.test.ts
@@ -0,0 +1,46 @@
+import { reorderColumnIds } from './columns'
+
+describe('reorderColumnIds()', () => {
+ it('should reorder the column ids given a displacer array of ids and a displaced array of ids', () => {
+ const emptyColumnIds: string[] = []
+ expect(reorderColumnIds(emptyColumnIds, [], [])).toStrictEqual([])
+
+ const twoColumnIds = ['id_1', 'id_2']
+ expect(reorderColumnIds(twoColumnIds, [], [])).toStrictEqual(twoColumnIds)
+ expect(reorderColumnIds(twoColumnIds, ['id_1'], ['id_1'])).toStrictEqual(
+ twoColumnIds
+ )
+ expect(reorderColumnIds(twoColumnIds, ['id_1'], ['id_2'])).toStrictEqual([
+ 'id_2',
+ 'id_1'
+ ])
+ expect(reorderColumnIds(twoColumnIds, ['id_2'], ['id_1'])).toStrictEqual([
+ 'id_2',
+ 'id_1'
+ ])
+
+ const threeColumnIds = [...twoColumnIds, 'id_3']
+ expect(reorderColumnIds(threeColumnIds, [], [])).toStrictEqual(
+ threeColumnIds
+ )
+ expect(reorderColumnIds(threeColumnIds, ['id_2'], ['id_2'])).toStrictEqual(
+ threeColumnIds
+ )
+ expect(reorderColumnIds(threeColumnIds, ['id_2'], ['id_3'])).toStrictEqual([
+ 'id_1',
+ 'id_3',
+ 'id_2'
+ ])
+ expect(reorderColumnIds(threeColumnIds, ['id_1'], ['id_3'])).toStrictEqual([
+ 'id_2',
+ 'id_3',
+ 'id_1'
+ ])
+ expect(
+ reorderColumnIds(threeColumnIds, ['id_1', 'id_2'], ['id_3'])
+ ).toStrictEqual(['id_3', 'id_1', 'id_2'])
+ expect(
+ reorderColumnIds(threeColumnIds, ['id_2', 'id_3'], ['id_1'])
+ ).toStrictEqual(['id_2', 'id_3', 'id_1'])
+ })
+})
diff --git a/webview/src/experiments/util/columns.ts b/webview/src/experiments/util/columns.ts
index 5349ed69b7..c5bd003d14 100644
--- a/webview/src/experiments/util/columns.ts
+++ b/webview/src/experiments/util/columns.ts
@@ -74,3 +74,43 @@ export const countUpperLevels = (
}
export const isFirstLevelHeader = (id: string) => id.split(':').length - 1 === 1
+
+export const reorderColumnIds = (
+ columnIds: string[],
+ displacer: string[],
+ displaced: string[]
+) => {
+ if (columnIds.length === 0 || displacer[0] === displaced[0]) {
+ return columnIds
+ }
+
+ const displacerIndex = columnIds.indexOf(displacer[0])
+ const displacedIndex = columnIds.indexOf(displaced[0])
+
+ if (displacerIndex < displacedIndex) {
+ return [
+ ...columnIds.slice(0, displacerIndex),
+ ...columnIds.slice(displacerIndex + displacer.length, displacedIndex),
+ ...displaced,
+ ...displacer,
+ ...columnIds.slice(displacedIndex + displaced.length)
+ ]
+ }
+ return [
+ ...columnIds.slice(0, displacedIndex),
+ ...displacer,
+ ...displaced,
+ ...columnIds.slice(displacedIndex + displaced.length, displacerIndex),
+ ...columnIds.slice(displacerIndex + displacer.length)
+ ]
+}
+
+export const leafColumnIds: (
+ column: HeaderGroup
+) => string[] = column => {
+ if (column.headers) {
+ return (column as HeaderGroup).headers.flatMap(leafColumnIds)
+ }
+
+ return [column.id]
+}
diff --git a/webview/src/react-beautiful-dnd-test-utils.d.ts b/webview/src/react-beautiful-dnd-test-utils.d.ts
deleted file mode 100644
index a6d129463a..0000000000
--- a/webview/src/react-beautiful-dnd-test-utils.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module 'react-beautiful-dnd-test-utils'
diff --git a/webview/src/shared/components/dragDrop/DragDropContainer.tsx b/webview/src/shared/components/dragDrop/DragDropContainer.tsx
index 763121d53e..b14e6c879e 100644
--- a/webview/src/shared/components/dragDrop/DragDropContainer.tsx
+++ b/webview/src/shared/components/dragDrop/DragDropContainer.tsx
@@ -3,7 +3,8 @@ import React, {
useEffect,
useState,
useRef,
- useContext
+ useContext,
+ DragEventHandler
} from 'react'
import { DragEnterDirection, getDragEnterDirection } from './util'
import { DragDropContext, DragDropContextValue } from './DragDropContext'
@@ -31,6 +32,28 @@ export type OnDrop = (
groupId: string,
position: number
) => void
+
+export type DropTargetInfo = {
+ element: JSX.Element
+ wrapperTag: 'div' | 'th'
+}
+
+export const makeTarget = (
+ dropTarget: DropTargetInfo,
+ handleDragOver: DragEventHandler,
+ handleOnDrop: DragEventHandler,
+ id: string
+) => (
+
+ {dropTarget.element}
+
+)
interface DragDropContainerProps {
order: string[]
setOrder: (order: string[]) => void
@@ -38,10 +61,7 @@ interface DragDropContainerProps {
items: JSX.Element[] // Every item must have a id prop for drag and drop to work
group: string
onDrop?: OnDrop
- dropTarget: {
- element: JSX.Element
- wrapperTag: 'div' | 'th'
- }
+ dropTarget: DropTargetInfo
wrapperComponent?: {
component: React.FC
props: {
@@ -193,17 +213,8 @@ export const DragDropContainer: React.FC = ({
const item = id && buildItem(id, draggable)
if (id === draggedOverId) {
- const target = (
-
- {dropTarget.element}
-
- )
+ const target = makeTarget(dropTarget, handleDragOver, handleOnDrop, id)
+
return direction === DragEnterDirection.RIGHT
? [item, target]
: [target, item]
diff --git a/webview/src/shared/components/dragDrop/DragDropContext.tsx b/webview/src/shared/components/dragDrop/DragDropContext.tsx
index 68b888c7ef..0b35d3d8d7 100644
--- a/webview/src/shared/components/dragDrop/DragDropContext.tsx
+++ b/webview/src/shared/components/dragDrop/DragDropContext.tsx
@@ -4,28 +4,65 @@ export type DraggedInfo =
| {
itemIndex: string
itemId: string
- group: string
+ group?: string
}
| undefined
+export interface DragDropGroupState {
+ draggedId?: string
+ draggedOverId?: string
+}
+
+export type GroupStates = {
+ [group: string]: DragDropGroupState | undefined
+}
+
export type DragDropContextValue = {
draggedRef: DraggedInfo
setDraggedRef: ((draggedRef: DraggedInfo) => void) | undefined
+ groupStates?: GroupStates
+ setGroupState?: (group: string, handlers: DragDropGroupState) => void
+ removeGroupState?: (group: string) => void
}
export const DragDropContext = createContext({
draggedRef: undefined,
- setDraggedRef: undefined
+ groupStates: undefined,
+ removeGroupState: undefined,
+ setDraggedRef: undefined,
+ setGroupState: undefined
})
export const DragDropProvider: React.FC = ({ children }) => {
const [draggedRef, setDraggedRef] = useState(undefined)
+ const [groupStates, setGroupStates] = useState({})
+
const changeDraggedRef = (d: DraggedInfo) => setDraggedRef(d)
+ const setGroupState = (group: string, handlers: DragDropGroupState) => {
+ setGroupStates({
+ ...groupStates,
+ [group]: handlers
+ })
+ }
+
+ const removeGroupState = (group: string) => {
+ setGroupStates({
+ ...groupStates,
+ [group]: undefined
+ })
+ }
+
return (
{children}
diff --git a/webview/src/shared/components/dragDrop/DragDropWorkbench.tsx b/webview/src/shared/components/dragDrop/DragDropWorkbench.tsx
new file mode 100644
index 0000000000..cfaa3d4d7f
--- /dev/null
+++ b/webview/src/shared/components/dragDrop/DragDropWorkbench.tsx
@@ -0,0 +1,96 @@
+import React, { DragEvent, useContext } from 'react'
+import { DropTargetInfo, makeTarget } from './DragDropContainer'
+import { DragDropContext, DragDropContextValue } from './DragDropContext'
+
+export type OnDrop = (draggedId: string, draggedOverId: string) => void
+export type OnDragStart = (draggedId: string) => void
+export type OnDragOver = (draggedId: string, draggedOverId: string) => void
+
+export interface DraggableProps {
+ id: string
+ group: string
+ disabled: boolean
+ dropTarget: DropTargetInfo
+ children: JSX.Element
+ onDrop?: OnDrop
+ onDragStart?: OnDragStart
+ onDragOver?: OnDragOver
+}
+
+export const Draggable: React.FC = ({
+ id,
+ group,
+ children,
+ disabled,
+ dropTarget,
+ onDrop,
+ onDragOver,
+ onDragStart
+}) => {
+ const { groupStates, setGroupState } =
+ useContext(DragDropContext)
+
+ const groupState = groupStates?.[group] || {}
+ const { draggedOverId, draggedId } = groupState
+
+ const handleDragStart = (e: DragEvent) => {
+ const { id } = e.currentTarget
+ e.dataTransfer.effectAllowed = 'move'
+ e.dataTransfer.dropEffect = 'move'
+ setGroupState?.(group, {
+ ...groupState,
+ draggedId: id
+ })
+ onDragStart?.(id)
+ }
+
+ const handleOnDrop = () => {
+ !disabled &&
+ draggedId &&
+ draggedOverId &&
+ onDrop?.(draggedId, draggedOverId)
+ }
+
+ const handleDragEnter = (e: DragEvent) => {
+ const { id } = e.currentTarget
+ !disabled &&
+ draggedId &&
+ id !== draggedId &&
+ id !== draggedOverId &&
+ (setGroupState?.(group, {
+ ...groupState,
+ draggedOverId: id
+ }) ||
+ onDragOver?.(draggedId, id))
+ }
+
+ const handleDragOver = (e: DragEvent) => {
+ e.preventDefault()
+ }
+
+ const handleDragEnd = () => {
+ setGroupState?.(group, {
+ ...groupState,
+ draggedId: undefined,
+ draggedOverId: undefined
+ })
+ }
+
+ const item = (
+
+ )
+ if (id === draggedOverId) {
+ return makeTarget(dropTarget, handleDragOver, handleOnDrop, id)
+ }
+
+ return item
+}
diff --git a/webview/src/shared/components/dragDrop/util.ts b/webview/src/shared/components/dragDrop/util.ts
index 25f85d4249..7e5492da40 100644
--- a/webview/src/shared/components/dragDrop/util.ts
+++ b/webview/src/shared/components/dragDrop/util.ts
@@ -2,7 +2,8 @@ import { DragEvent } from 'react'
export enum DragEnterDirection {
RIGHT = 'RIGHT',
- LEFT = 'LEFT'
+ LEFT = 'LEFT',
+ AUTO = 'AUTO'
}
export const getEventCurrentTargetDistances = (e: DragEvent) => {
diff --git a/webview/src/test/dragDrop.ts b/webview/src/test/dragDrop.ts
index 35aadfa110..4394796cc1 100644
--- a/webview/src/test/dragDrop.ts
+++ b/webview/src/test/dragDrop.ts
@@ -30,15 +30,20 @@ export const dragEnter = (
draggedOver.dispatchEvent(createBubbledEvent('dragenter'))
- const clientX =
- 100 + (direction === DragDropUtils.DragEnterDirection.LEFT ? 1 : 51)
- const left = 100
- const right = left + 100
- const dragOverEvent = createBubbledEvent('dragover', { clientX })
- jest
- .spyOn(DragDropUtils, 'getEventCurrentTargetDistances')
- .mockImplementationOnce(() => ({ left, right }))
- draggedOver.dispatchEvent(dragOverEvent)
+ if (direction !== DragDropUtils.DragEnterDirection.AUTO) {
+ const clientX =
+ 100 + (direction === DragDropUtils.DragEnterDirection.LEFT ? 1 : 51)
+ const left = 100
+ const right = left + 100
+ const dragOverEvent = createBubbledEvent('dragover', { clientX })
+ jest
+ .spyOn(DragDropUtils, 'getEventCurrentTargetDistances')
+ .mockImplementationOnce(() => ({ left, right }))
+ draggedOver.dispatchEvent(dragOverEvent)
+ } else {
+ draggedOver.dispatchEvent(createBubbledEvent('dragover'))
+ }
+
jest.useRealTimers()
}
diff --git a/webview/src/test/sort.ts b/webview/src/test/sort.ts
index 90aeb8570d..457993f574 100644
--- a/webview/src/test/sort.ts
+++ b/webview/src/test/sort.ts
@@ -1,6 +1,5 @@
import { screen } from '@testing-library/react'
import { ColumnType, TableData } from 'dvc/src/experiments/webview/contract'
-import { DND_DRAGGABLE_DATA_ATTR } from 'react-beautiful-dnd-test-utils'
export const defaultColumns = ['Experiment', 'Timestamp']
@@ -50,10 +49,6 @@ export const tableData: TableData = {
sorts: []
}
-export const makeGetDragEl = (text: string) => () =>
- // eslint-disable-next-line testing-library/no-node-access
- screen.getByText(text).closest(DND_DRAGGABLE_DATA_ATTR)
-
export const getHeaders = async () => {
const renderedHeader = await screen.findAllByTestId('rendered-header')
return renderedHeader.map(header => header.textContent)
diff --git a/yarn.lock b/yarn.lock
index 445926c3aa..66d3e9695f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1079,7 +1079,7 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.17.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
@@ -3036,14 +3036,6 @@
dependencies:
"@types/unist" "*"
-"@types/hoist-non-react-statics@^3.3.0":
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
- integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
- dependencies:
- "@types/react" "*"
- hoist-non-react-statics "^3.3.0"
-
"@types/html-minifier-terser@^5.0.0":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57"
@@ -3271,13 +3263,6 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
-"@types/react-beautiful-dnd@^13.1.2":
- version "13.1.2"
- resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130"
- integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==
- dependencies:
- "@types/react" "*"
-
"@types/react-dom@*", "@types/react-dom@^17.0.11":
version "17.0.11"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466"
@@ -3292,16 +3277,6 @@
dependencies:
"@types/react" "*"
-"@types/react-redux@^7.1.20":
- version "7.1.22"
- resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.22.tgz#0eab76a37ef477cc4b53665aeaf29cb60631b72a"
- integrity sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ==
- dependencies:
- "@types/hoist-non-react-statics" "^3.3.0"
- "@types/react" "*"
- hoist-non-react-statics "^3.3.0"
- redux "^4.0.0"
-
"@types/react-syntax-highlighter@11.0.5":
version "11.0.5"
resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz#0d546261b4021e1f9d85b50401c0a42acb106087"
@@ -5842,13 +5817,6 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
-css-box-model@^1.2.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
- integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
- dependencies:
- tiny-invariant "^1.0.6"
-
css-loader@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645"
@@ -8216,7 +8184,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
-hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -10223,11 +10191,6 @@ memfs@^3.1.2, memfs@^3.2.2, memfs@^3.4.1:
dependencies:
fs-monkey "1.0.3"
-memoize-one@^5.1.1:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
- integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
-
memoizerific@^1.11.3:
version "1.11.3"
resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a"
@@ -11890,11 +11853,6 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-raf-schd@^4.0.2:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
- integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
-
ramda@^0.21.0:
version "0.21.0"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35"
@@ -11948,24 +11906,6 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-beautiful-dnd-test-utils@^3.2.2:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/react-beautiful-dnd-test-utils/-/react-beautiful-dnd-test-utils-3.2.2.tgz#afc7d718a63f47c5c18cc74038a4841c1a476992"
- integrity sha512-/Nyn3zww8UaJbNNP6CTikkFV29rrqmxjbbgN3XknfKD1Cq2IIlEyO/Jn8X5Zj06gE3lac8H9NXC7lJeadNOrzA==
-
-react-beautiful-dnd@^13.1.0:
- version "13.1.0"
- resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
- integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
- dependencies:
- "@babel/runtime" "^7.9.2"
- css-box-model "^1.2.0"
- memoize-one "^5.1.1"
- raf-schd "^4.0.2"
- react-redux "^7.2.0"
- redux "^4.0.4"
- use-memo-one "^1.1.1"
-
react-colorful@^5.1.2:
version "5.5.1"
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
@@ -12075,18 +12015,6 @@ react-popper@^2.2.4:
react-fast-compare "^3.0.1"
warning "^4.0.2"
-react-redux@^7.2.0:
- version "7.2.6"
- resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa"
- integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==
- dependencies:
- "@babel/runtime" "^7.15.4"
- "@types/react-redux" "^7.1.20"
- hoist-non-react-statics "^3.3.2"
- loose-envify "^1.4.0"
- prop-types "^15.7.2"
- react-is "^17.0.2"
-
react-refresh@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
@@ -12266,13 +12194,6 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
-redux@^4.0.0, redux@^4.0.4:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
- integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
- dependencies:
- "@babel/runtime" "^7.9.2"
-
refractor@^3.1.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.5.0.tgz#334586f352dda4beaf354099b48c2d18e0819aec"
@@ -13777,11 +13698,6 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
-tiny-invariant@^1.0.6:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
- integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
-
tippy.js@^6.3.1:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
@@ -14404,11 +14320,6 @@ use-latest@^1.0.0:
dependencies:
use-isomorphic-layout-effect "^1.0.0"
-use-memo-one@^1.1.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
- integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
-
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"