Skip to content

Commit 65d9af8

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

5 files changed

Lines changed: 124 additions & 17 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: 79 additions & 16 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,68 @@ 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+
// Check if any source is the target itself
119+
if (sources.some((source) => getId(target) === getId(source))) return false
120+
// Check if any source is a descendant of the target
121+
if (sources.some((source) => isDescendantOf(target, source))) return false
122+
// Recursively check children
123+
return getChildren(target).every(($) => canDropMultiple($, sources))
124+
},
125+
[getId, getChildren]
126+
)
127+
128+
const isDescendantOf = useCallback(
129+
(ancestor: T, descendant: T): boolean => {
130+
const children = getChildren(ancestor)
131+
if (children.some((child) => getId(child) === getId(descendant))) return true
132+
return children.some((child) => isDescendantOf(child, descendant))
133+
},
134+
[getId, getChildren]
121135
)
122136

123137
const [{ isHover }, drop] = useDrop(
124138
() => ({
125139
accept: dndType,
126-
drop: ({ value: item }: { value: T }, monitor) => {
140+
drop: (item: { items: T[]; context: unknown }, monitor) => {
127141
const dropTypeValue = dropType || dropTypeRef.current
128-
if (monitor.didDrop() || !canDrop(item, value) || !dropTypeValue) return
129-
onDrop(item, value, dropTypeValue)
142+
if (monitor.didDrop() || !dropTypeValue) return
143+
144+
const { items } = item
145+
const isMultipleDrag = items.length > 1
146+
147+
if (isMultipleDrag) {
148+
if (!canDropMultiple(value, items)) return
149+
items.forEach((sourceItem) => onDrop(sourceItem, value, dropTypeValue))
150+
} else {
151+
const sourceItem = items[0]
152+
if (!canDrop(sourceItem, value)) return
153+
onDrop(sourceItem, value, dropTypeValue)
154+
}
130155
},
131-
hover: ({ value: item }, monitor) => {
132-
if (!ref.current || item === value) {
156+
hover: (item: { items: T[]; context: unknown }, monitor) => {
157+
if (!ref.current) {
158+
dropTypeRef.current = ''
159+
return setDropType('')
160+
}
161+
162+
const { items } = item
163+
164+
// check if hovering over one of the dragged items
165+
if (items.some((sourceItem) => getId(sourceItem) === getId(value))) {
133166
dropTypeRef.current = ''
134167
return setDropType('')
135168
}
136169

137170
const coords = monitor.getClientOffset() as XYCoord
138171
const rect = ref.current.getBoundingClientRect()
139172
const dropType = calculateDropType(coords.y, rect)
140-
const enableReorder = canReorder ? canReorder(item, value, dropType) : true
173+
174+
const enableReorder = canReorder
175+
? items.every((sourceItem) => canReorder(sourceItem, value, dropType))
176+
: true
177+
141178
const newDropTypeValue = enableReorder ? dropType : ''
142179

143180
setDropType(newDropTypeValue)
@@ -147,15 +184,16 @@ export function Tree<T>() {
147184
isHover: monitor.isOver({ shallow: true })
148185
})
149186
}),
150-
[value, dropType, onDrop, canDrop]
187+
[value, dropType, onDrop, canDrop, canDropMultiple, canReorder, getId]
151188
)
152189

153190
const quitEditMode = () => setEditMode(false)
154191
const quitInsertMode = () => setInsertMode(false)
155192

156193
const handleSelect = (event: React.MouseEvent) => {
157194
const isMac = /Mac|iPhone|iPod|iPad/.test(navigator.userAgent)
158-
const isCtrlClick = (isMac ? event.type === ClickType.CONTEXT_MENU : event.type === ClickType.CLICK) && event.ctrlKey
195+
const isCtrlClick =
196+
(isMac ? event.type === ClickType.CONTEXT_MENU : event.type === ClickType.CLICK) && event.ctrlKey
159197
const isShiftClick = event.type === ClickType.CLICK && event.shiftKey
160198
const isDoubleClick = event.type === ClickType.CLICK && event.detail > 1 && onDoubleSelect
161199
const clickType = isCtrlClick ? 'ctrl' : isShiftClick ? 'shift' : 'single'
@@ -193,6 +231,29 @@ export function Tree<T>() {
193231
}
194232

195233
const sdk = useSdk()
234+
235+
const [, drag] = useDrag(
236+
() => ({
237+
type: dndType,
238+
canDrag: enableDrag,
239+
item: () => {
240+
const selectedItems = getSelectedItems ? getSelectedItems() : []
241+
// if this item is selected and there are multiple selections, drag all selected items
242+
if (selectedItems.length > 1 && selectedItems.some((item) => getId(item) === getId(value))) {
243+
return {
244+
items: selectedItems,
245+
context: getDragContext()
246+
}
247+
}
248+
return {
249+
items: [value],
250+
context: getDragContext()
251+
}
252+
},
253+
}),
254+
[value, getSelectedItems, getId]
255+
)
256+
196257
const handleRemove = () => {
197258
if (isEntity && sdk) {
198259
const selectedEntities = sdk.operations.getSelectedEntities()
@@ -269,7 +330,9 @@ export function Tree<T>() {
269330
)}
270331
</div>
271332
<TreeChildren {...props} />
272-
{insertMode && <Input value="" onCancel={quitInsertMode} onSubmit={handleAddChild} onBlur={quitInsertMode} />}
333+
{insertMode && (
334+
<Input value="" onCancel={quitInsertMode} onSubmit={handleAddChild} onBlur={quitInsertMode} />
335+
)}
273336
</div>
274337
)
275338
})

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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,28 @@ 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+
// Create test entities
125+
await Hierarchy.addChild(ROOT, 'multi-parent')
126+
await Hierarchy.addChild(ROOT, 'multi-child-1')
127+
await Hierarchy.addChild(ROOT, 'multi-child-2')
128+
await Hierarchy.addChild(ROOT, 'multi-child-3')
129+
130+
const parent = await Hierarchy.getId('multi-parent')
131+
const child1 = await Hierarchy.getId('multi-child-1')
132+
const child2 = await Hierarchy.getId('multi-child-2')
133+
const child3 = await Hierarchy.getId('multi-child-3')
134+
135+
// Select multiple children (Ctrl+click)
136+
await Hierarchy.selectMultiple([child1, child2, child3])
137+
138+
// Drag the selected children to the parent
139+
await Hierarchy.setParent(child1, parent) // This should move all selected children
140+
141+
// Verify all children are now under the parent
142+
await expect(Hierarchy.isAncestor(child1, parent)).resolves.toBe(true)
143+
await expect(Hierarchy.isAncestor(child2, parent)).resolves.toBe(true)
144+
await expect(Hierarchy.isAncestor(child3, parent)).resolves.toBe(true)
145+
}, 100_000)
122146
})

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+
// Click the first entity to select it
139+
const firstItem = await this.getItem(entityIds[0], this.getItemSelectorById)
140+
await firstItem.click()
141+
142+
// Ctrl+click the remaining entities to add them to selection
143+
for (let i = 1; i < entityIds.length; i++) {
144+
const item = await this.getItem(entityIds[i], this.getItemSelectorById)
145+
await item.click({ modifiers: ['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)