Skip to content

Commit b3bea7d

Browse files
committed
feat(ui): enable child reordering in management modal
1 parent b88416a commit b3bea7d

File tree

10 files changed

+315
-52
lines changed

10 files changed

+315
-52
lines changed

ui/src/components/canvas/ChildrenNavigationPopover.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const ChildTree = () => {
4343
}
4444

4545
return (
46-
<div>
46+
<div className="nodrag">
4747
<TreeView
4848
className={"child-tree-view"}
4949
label={"children-menu"}
@@ -61,7 +61,7 @@ export const ChildrenNavigationPopover = () => {
6161

6262
return (
6363
<Popover
64-
className="eip-children-popover"
64+
className="eip-children-popover nowheel"
6565
highContrast
6666
open={open}
6767
// Keep popover open when interacting with node config side panel
@@ -71,7 +71,7 @@ export const ChildrenNavigationPopover = () => {
7171
}
7272
>
7373
<Button
74-
className="eip-children-popover__button nodrag"
74+
className="eip-children-popover__button"
7575
hasIconOnly
7676
iconDescription="children"
7777
kind="primary"

ui/src/components/canvas/nodes.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,9 @@ $icon-button-focus-shadow:
3939
overflow-y: auto;
4040
}
4141

42-
4342
.child-tree-view__node {
4443
background-color: themes.$background;
4544
}
46-
4745
}
4846

4947
.eip-node-selected {

ui/src/components/config-panel/ChildManagementModal.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import {
22
Breadcrumb,
33
BreadcrumbItem,
44
Button,
5-
ContainedList,
6-
ContainedListItem,
75
Dropdown,
86
Layer,
97
Modal,
@@ -22,10 +20,12 @@ import { lookupEipComponent } from "../../singletons/eipDefinitions"
2220
import {
2321
disableChild,
2422
enableChild,
25-
updateSelectedChildNode
23+
reorderEnabledChildren,
24+
updateSelectedChildNode,
2625
} from "../../singletons/store/appActions"
2726
import { useGetEnabledChildren } from "../../singletons/store/getterHooks"
2827
import { getEipId } from "../../singletons/store/storeViews"
28+
import { DraggableList, DraggableListItem } from "./DraggableList"
2929

3030
interface ChildPathBreadcrumbProps {
3131
path: string[]
@@ -73,7 +73,11 @@ const ChildPathBreadcrumb = ({
7373
}: ChildPathBreadcrumbProps) => (
7474
<Breadcrumb>
7575
{path.map((id, idx) => (
76-
<BreadcrumbItem key={idx} onClick={() => navigatePath(idx)}>
76+
<BreadcrumbItem
77+
className="breadcrumb__item"
78+
key={idx}
79+
onClick={() => navigatePath(idx)}
80+
>
7781
{getEipId(id)?.name}
7882
</BreadcrumbItem>
7983
))}
@@ -105,16 +109,18 @@ const ChildrenDisplay = ({
105109
openChildConfigPanel,
106110
}: ChildrenDisplayProps) => (
107111
<Layer>
108-
<ContainedList
112+
<DraggableList
109113
className="child-modal__list"
110114
label="Children"
111115
kind="on-page"
116+
handleDrop={(items) => reorderEnabledChildren(parentId, items)}
112117
>
113118
{enabledChildren.map((childId) => {
114119
const eipId = getEipId(childId)
115120
return (
116-
<ContainedListItem
121+
<DraggableListItem
117122
key={childId}
123+
id={childId}
118124
action={
119125
<>
120126
<Button
@@ -138,10 +144,10 @@ const ChildrenDisplay = ({
138144
onClick={() => updatePath(childId)}
139145
>
140146
<span>{eipId?.name}</span> <span>({childId})</span>
141-
</ContainedListItem>
147+
</DraggableListItem>
142148
)
143149
})}
144-
</ContainedList>
150+
</DraggableList>
145151
</Layer>
146152
)
147153

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { ContainedList, ContainedListItem } from "@carbon/react"
2+
3+
import { Draggable } from "@carbon/react/icons"
4+
import { ContainedListProps } from "@carbon/react/lib/components/ContainedList/ContainedList"
5+
import { Identifier } from "dnd-core"
6+
import { Children, useEffect, useRef, useState } from "react"
7+
import { DropTargetMonitor, useDrag, useDrop } from "react-dnd"
8+
9+
const DragTypes = {
10+
ROW: "list-row",
11+
}
12+
13+
interface DragItem {
14+
index: number
15+
id: string
16+
}
17+
18+
// "ContainedListItem" prop type not exported by Carbon
19+
// See https://react.carbondesignsystem.com/?path=/docs/components-containedlist--overview#component-api
20+
interface DraggableListItemProps {
21+
id: string
22+
[key: string]: unknown
23+
}
24+
25+
interface ListItemWrapperProps {
26+
id: string
27+
index: number
28+
move: (dragIdx: number, hoverIdx: number) => void
29+
onDrop: () => void
30+
renderListItem: React.ReactNode
31+
}
32+
33+
type DraggableListProps = ContainedListProps & {
34+
handleDrop?: (ids: string[]) => void
35+
}
36+
37+
interface HoverInput {
38+
item: DragItem
39+
monitor: DropTargetMonitor<DragItem, void>
40+
hoverIdx: number
41+
move: (dragIdx: number, hoverIdx: number) => void
42+
ref: React.RefObject<HTMLDivElement>
43+
}
44+
45+
type ListElementsType = React.ReactElement<DraggableListItemProps>[]
46+
47+
const handleHover = ({ item, monitor, hoverIdx, move, ref }: HoverInput) => {
48+
if (!ref.current) {
49+
return
50+
}
51+
52+
const dragIdx = item.index
53+
54+
// Don't replace items with themselves
55+
if (dragIdx === hoverIdx) {
56+
return
57+
}
58+
59+
const hoverBoundingRect = ref.current?.getBoundingClientRect()
60+
61+
// Get vertical middle
62+
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
63+
64+
// Determine mouse position
65+
const clientOffset = monitor.getClientOffset()
66+
67+
// Get pixels to the top
68+
const hoverClientY = clientOffset!.y - hoverBoundingRect.top
69+
70+
// Only perform the move when the mouse has crossed half of the items height
71+
72+
// Dragging downwards
73+
if (dragIdx < hoverIdx && hoverClientY < hoverMiddleY) {
74+
return
75+
}
76+
77+
// Dragging upwards
78+
if (dragIdx > hoverIdx && hoverClientY > hoverMiddleY) {
79+
return
80+
}
81+
82+
// Apply the reordering
83+
move(dragIdx, hoverIdx)
84+
85+
item.index = hoverIdx
86+
}
87+
88+
const ListItemWrapper = ({
89+
id,
90+
index,
91+
move,
92+
onDrop,
93+
renderListItem,
94+
}: ListItemWrapperProps) => {
95+
const ref = useRef<HTMLDivElement>(null)
96+
const [{ handlerId }, drop] = useDrop<
97+
DragItem,
98+
void,
99+
{ handlerId: Identifier | null }
100+
>({
101+
accept: DragTypes.ROW,
102+
collect: (monitor) => ({
103+
handlerId: monitor.getHandlerId(),
104+
}),
105+
drop: () => onDrop && onDrop(),
106+
hover: (item: DragItem, monitor) =>
107+
handleHover({ item, monitor, hoverIdx: index, move, ref }),
108+
})
109+
110+
const [{ isDragging }, drag] = useDrag({
111+
type: DragTypes.ROW,
112+
item: () => ({
113+
id,
114+
index,
115+
}),
116+
collect: (monitor) => ({
117+
isDragging: monitor.isDragging(),
118+
}),
119+
})
120+
121+
drag(drop(ref))
122+
123+
const opacity = isDragging ? 0 : 1
124+
return (
125+
<div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
126+
{renderListItem}
127+
</div>
128+
)
129+
}
130+
131+
// Drag and drop code adapted from https://react-dnd.github.io/react-dnd/examples/sortable/simple
132+
export const DraggableListItem = (props: DraggableListItemProps) => (
133+
<ContainedListItem {...props} renderIcon={Draggable} />
134+
)
135+
136+
export const DraggableList = ({ handleDrop, ...props }: DraggableListProps) => {
137+
const [rows, setRows] = useState(
138+
() => Children.toArray(props.children) as ListElementsType
139+
)
140+
141+
useEffect(
142+
() => setRows(Children.toArray(props.children) as ListElementsType),
143+
[props.children]
144+
)
145+
146+
const moveRow = (dragIdx: number, hoverIdx: number) =>
147+
setRows((prev) => {
148+
const items = [...prev]
149+
const item = items.splice(dragIdx, 1)[0]
150+
items.splice(hoverIdx, 0, item)
151+
return items
152+
})
153+
154+
return (
155+
<ContainedList {...props}>
156+
{rows.map((row, idx) => (
157+
<ListItemWrapper
158+
key={row.props.id}
159+
id={row.props.id}
160+
index={idx}
161+
move={moveRow}
162+
onDrop={() => handleDrop && handleDrop(rows.map((r) => r.props.id))}
163+
renderListItem={row}
164+
></ListItemWrapper>
165+
))}
166+
</ContainedList>
167+
)
168+
}

ui/src/components/config-panel/EipConfigSidePanel.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,13 @@ const EipConfigSidePanel = () => {
8080
},
8181
})
8282

83-
const selectedNodeEipId = selectedNode && getEipId(selectedNode.id)
84-
const eipComponent = selectedNodeEipId
85-
? lookupEipComponent(selectedNodeEipId)
86-
: null
87-
8883
let sidePanelContent
8984
// TODO: Simplify conditionals
90-
if (selectedChildPath && selectedNode && eipComponent) {
85+
if (selectedChildPath) {
86+
// TODO: Handle error case if childElement or rootComponent is undefined
9187
const childId = selectedChildPath[selectedChildPath.length - 1]
92-
93-
// TODO: Handle error case if childElement is undefined
94-
const childElement = findChildDefinition(eipComponent, selectedChildPath)
88+
const rootComponent = lookupEipComponent(getEipId(selectedChildPath[0])!)!
89+
const childElement = findChildDefinition(rootComponent, selectedChildPath)
9590
const configurableAttrs = filterConfigurableAttributes(
9691
childElement?.attributes
9792
)
@@ -102,9 +97,13 @@ const EipConfigSidePanel = () => {
10297
attributes={configurableAttrs}
10398
/>
10499
)
105-
} else if (selectedNodeEipId && eipComponent) {
100+
} else if (selectedNode) {
101+
// TODO: Handle error case if eipComponent is undefined
102+
const selectedNodeEipId = selectedNode && getEipId(selectedNode.id)
103+
const eipComponent =
104+
selectedNodeEipId && lookupEipComponent(selectedNodeEipId)
106105
const configurableAttrs = filterConfigurableAttributes(
107-
eipComponent.attributes,
106+
eipComponent?.attributes,
108107
selectedNodeEipId
109108
)
110109
sidePanelContent = (

0 commit comments

Comments
 (0)