Skip to content

Commit be151b7

Browse files
committed
add multiple drag & drop behaviour
1 parent c9c398f commit be151b7

5 files changed

Lines changed: 114 additions & 16 deletions

File tree

packages/@dcl/inspector/src/components/Hierarchy/Hierarchy.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const Hierarchy: React.FC = () => {
7676
getId,
7777
getChildren,
7878
getLabel,
79+
getSelectedItems,
7980
isOpen,
8081
isHidden,
8182
canRename,
@@ -162,6 +163,7 @@ const Hierarchy: React.FC = () => {
162163
getId: getId,
163164
getChildren: getChildren,
164165
getLabel: getLabel,
166+
getSelectedItems: getSelectedItems,
165167
getIcon: (val: Entity) => <HierarchyIcon value={val} />,
166168
isOpen: isOpen,
167169
isSelected: isSelected,

packages/@dcl/inspector/src/components/Tree/Tree.tsx

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Props<T> = {
2323
getChildren: (value: T) => T[]
2424
getIcon?: (value: T) => JSX.Element
2525
getLabel: (value: T) => string | JSX.Element
26+
getSelectedItems?: () => T[]
2627
isOpen: (value: T) => boolean
2728
isSelected: (value: T) => boolean
2829
isHidden: (value: T) => boolean
@@ -64,6 +65,7 @@ export function Tree<T>() {
6465
getId,
6566
getChildren,
6667
getLabel,
68+
getSelectedItems,
6769
isOpen,
6870
isSelected,
6971
onSelect,
@@ -111,33 +113,65 @@ export function Tree<T>() {
111113
[getId, getChildren]
112114
)
113115

114-
const [, drag] = useDrag(
115-
() => ({
116-
type: dndType,
117-
canDrag: enableDrag,
118-
item: { value, context: getDragContext() }
119-
}),
120-
[value]
116+
const canDropMultiple = useCallback(
117+
(target: T, sources: T[]): boolean => {
118+
if (sources.some((source) => getId(target) === getId(source))) return false
119+
if (sources.some((source) => isDescendantOf(target, source))) return false
120+
return getChildren(target).every(($) => canDropMultiple($, sources))
121+
},
122+
[getId, getChildren]
123+
)
124+
125+
const isDescendantOf = useCallback(
126+
(ancestor: T, descendant: T): boolean => {
127+
const children = getChildren(ancestor)
128+
if (children.some((child) => getId(child) === getId(descendant))) return true
129+
return children.some((child) => isDescendantOf(child, descendant))
130+
},
131+
[getId, getChildren]
121132
)
122133

123134
const [{ isHover }, drop] = useDrop(
124135
() => ({
125136
accept: dndType,
126-
drop: ({ value: item }: { value: T }, monitor) => {
137+
drop: (item: { items: T[]; context: unknown }, monitor) => {
127138
const dropTypeValue = dropType || dropTypeRef.current
128-
if (monitor.didDrop() || !canDrop(item, value) || !dropTypeValue) return
129-
onDrop(item, value, dropTypeValue)
139+
if (monitor.didDrop() || !dropTypeValue) return
140+
141+
const { items } = item
142+
const isMultipleDrag = items.length > 1
143+
144+
if (isMultipleDrag) {
145+
if (!canDropMultiple(value, items)) return
146+
items.forEach((sourceItem) => onDrop(sourceItem, value, dropTypeValue))
147+
} else {
148+
const sourceItem = items[0]
149+
if (!canDrop(sourceItem, value)) return
150+
onDrop(sourceItem, value, dropTypeValue)
151+
}
130152
},
131-
hover: ({ value: item }, monitor) => {
132-
if (!ref.current || item === value) {
153+
hover: (item: { items: T[]; context: unknown }, monitor) => {
154+
if (!ref.current) {
155+
dropTypeRef.current = ''
156+
return setDropType('')
157+
}
158+
159+
const { items } = item
160+
161+
// check if hovering over one of the dragged items
162+
if (items.some((sourceItem) => getId(sourceItem) === getId(value))) {
133163
dropTypeRef.current = ''
134164
return setDropType('')
135165
}
136166

137167
const coords = monitor.getClientOffset() as XYCoord
138168
const rect = ref.current.getBoundingClientRect()
139169
const dropType = calculateDropType(coords.y, rect)
140-
const enableReorder = canReorder ? canReorder(item, value, dropType) : true
170+
171+
const enableReorder = canReorder
172+
? items.every((sourceItem) => canReorder(sourceItem, value, dropType))
173+
: true
174+
141175
const newDropTypeValue = enableReorder ? dropType : ''
142176

143177
setDropType(newDropTypeValue)
@@ -147,15 +181,16 @@ export function Tree<T>() {
147181
isHover: monitor.isOver({ shallow: true })
148182
})
149183
}),
150-
[value, dropType, onDrop, canDrop]
184+
[value, dropType, onDrop, canDrop, canDropMultiple, canReorder, getId]
151185
)
152186

153187
const quitEditMode = () => setEditMode(false)
154188
const quitInsertMode = () => setInsertMode(false)
155189

156190
const handleSelect = (event: React.MouseEvent) => {
157191
const isMac = /Mac|iPhone|iPod|iPad/.test(navigator.userAgent)
158-
const isCtrlClick = (isMac ? event.type === ClickType.CONTEXT_MENU : event.type === ClickType.CLICK) && event.ctrlKey
192+
const isCtrlClick =
193+
(isMac ? event.type === ClickType.CONTEXT_MENU : event.type === ClickType.CLICK) && event.ctrlKey
159194
const isShiftClick = event.type === ClickType.CLICK && event.shiftKey
160195
const isDoubleClick = event.type === ClickType.CLICK && event.detail > 1 && onDoubleSelect
161196
const clickType = isCtrlClick ? 'ctrl' : isShiftClick ? 'shift' : 'single'
@@ -193,6 +228,29 @@ export function Tree<T>() {
193228
}
194229

195230
const sdk = useSdk()
231+
232+
const [, drag] = useDrag(
233+
() => ({
234+
type: dndType,
235+
canDrag: enableDrag,
236+
item: () => {
237+
const selectedItems = getSelectedItems ? getSelectedItems() : []
238+
// if this item is selected and there are multiple selections, drag all selected items
239+
if (selectedItems.length > 1 && selectedItems.some((item) => getId(item) === getId(value))) {
240+
return {
241+
items: selectedItems,
242+
context: getDragContext()
243+
}
244+
}
245+
return {
246+
items: [value],
247+
context: getDragContext()
248+
}
249+
}
250+
}),
251+
[value, getSelectedItems, getId]
252+
)
253+
196254
const handleRemove = () => {
197255
if (isEntity && sdk) {
198256
const selectedEntities = sdk.operations.getSelectedEntities()

packages/@dcl/inspector/src/hooks/sdk/useTree.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ export const useTree = () => {
100100
[sdk, handleUpdate, tree]
101101
)
102102

103+
const getSelectedItems = useCallback((): Entity[] => {
104+
if (!sdk) return []
105+
return sdk.operations.getSelectedEntities()
106+
}, [sdk])
107+
103108
const setParent = useCallback(
104109
async (source: Entity, target: Entity, type: DropType) => {
105110
if (source === ROOT || !sdk) return
@@ -222,6 +227,7 @@ export const useTree = () => {
222227
canDuplicate,
223228
canDrag,
224229
canReorder,
225-
centerViewOnEntity
230+
centerViewOnEntity,
231+
getSelectedItems
226232
}
227233
}

packages/@dcl/inspector/test/e2e/Hierarchy.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,24 @@ describe('Hierarchy', () => {
119119
await expect(Hierarchy.exists(parent)).resolves.toBe(false)
120120
await expect(Hierarchy.exists(child)).resolves.toBe(false)
121121
}, 100_000)
122+
123+
test('drag and drop multiple selected entities', async () => {
124+
await Hierarchy.addChild(ROOT, 'multi-parent')
125+
await Hierarchy.addChild(ROOT, 'multi-child-1')
126+
await Hierarchy.addChild(ROOT, 'multi-child-2')
127+
await Hierarchy.addChild(ROOT, 'multi-child-3')
128+
129+
const parent = await Hierarchy.getId('multi-parent')
130+
const child1 = await Hierarchy.getId('multi-child-1')
131+
const child2 = await Hierarchy.getId('multi-child-2')
132+
const child3 = await Hierarchy.getId('multi-child-3')
133+
134+
await Hierarchy.selectMultiple([child1, child2, child3])
135+
136+
await Hierarchy.setParent(child1, parent) // this should move all selected children
137+
138+
await expect(Hierarchy.isAncestor(child1, parent)).resolves.toBe(true)
139+
await expect(Hierarchy.isAncestor(child2, parent)).resolves.toBe(true)
140+
await expect(Hierarchy.isAncestor(child3, parent)).resolves.toBe(true)
141+
}, 100_000)
122142
})

packages/@dcl/inspector/test/e2e/pageObjects/Hierarchy.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ class HierarchyPageObject {
134134
}
135135
}
136136

137+
async selectMultiple(entityIds: number[]) {
138+
const firstItem = await this.getItem(entityIds[0], this.getItemSelectorById)
139+
await firstItem.click()
140+
141+
for (let i = 1; i < entityIds.length; i++) {
142+
const item = await this.getItem(entityIds[i], this.getItemSelectorById)
143+
await page.keyboard.down('Control')
144+
await item.click()
145+
await page.keyboard.up('Control')
146+
}
147+
}
148+
137149
async addComponent(entityId: number, componentName: string) {
138150
const item = await this.getItem(entityId, this.getItemSelectorById)
139151
await item.click({ button: 'right' })

0 commit comments

Comments
 (0)