Skip to content

Commit be15f62

Browse files
WIP - drag and drop functionally works well, except for rearranging rows of 3. looks ugly though
1 parent 5d047ae commit be15f62

File tree

11 files changed

+2437
-2039
lines changed

11 files changed

+2437
-2039
lines changed

datahub-web-react/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
"@ant-design/colors": "^5.0.0",
1010
"@ant-design/icons": "^4.3.0",
1111
"@apollo/client": "^3.3.19",
12+
"@dnd-kit/core": "^6.3.1",
13+
"@dnd-kit/sortable": "^10.0.0",
14+
"@dnd-kit/utilities": "^3.2.2",
1215
"@fontsource/mulish": "^5.0.16",
1316
"@geometricpanda/storybook-addon-badges": "^2.0.2",
1417
"@graphql-codegen/fragment-matcher": "^5.0.0",

datahub-web-react/src/app/homeV3/context/PageTemplateContext.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const PageTemplateProvider = ({
3434
const { updateTemplateWithModule, removeModuleFromTemplate, upsertTemplate } = useTemplateOperations();
3535

3636
// Module operations
37-
const { addModule, removeModule, createModule } = useModuleOperations(
37+
const { addModule, removeModule, createModule, moveModule } = useModuleOperations(
3838
isEditingGlobalTemplate,
3939
personalTemplate,
4040
globalTemplate,
@@ -58,6 +58,7 @@ export const PageTemplateProvider = ({
5858
addModule,
5959
removeModule,
6060
createModule,
61+
moveModule,
6162
}),
6263
[
6364
personalTemplate,
@@ -71,6 +72,7 @@ export const PageTemplateProvider = ({
7172
addModule,
7273
removeModule,
7374
createModule,
75+
moveModule,
7476
],
7577
);
7678

datahub-web-react/src/app/homeV3/context/__tests__/PageTemplateContext.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const mockSetTemplate = vi.fn();
8888
const mockAddModule = vi.fn();
8989
const mockRemoveModule = vi.fn();
9090
const mockCreateModule = vi.fn();
91+
const mockMoveModule = vi.fn();
9192
const mockUpdateTemplateWithModule = vi.fn();
9293
const mockRemoveModuleFromTemplate = vi.fn();
9394
const mockUpsertTemplate = vi.fn();
@@ -118,6 +119,7 @@ describe('PageTemplateContext', () => {
118119
addModule: mockAddModule,
119120
removeModule: mockRemoveModule,
120121
createModule: mockCreateModule,
122+
moveModule: mockMoveModule,
121123
});
122124
});
123125

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import { vi } from 'vitest';
3+
4+
import { useDragAndDrop } from '@app/homeV3/context/hooks/useDragAndDrop';
5+
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
6+
7+
import { DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
8+
9+
// Mock the PageTemplateContext
10+
vi.mock('@app/homeV3/context/PageTemplateContext', () => ({
11+
usePageTemplateContext: vi.fn(),
12+
}));
13+
14+
const mockUsePageTemplateContext = vi.mocked(usePageTemplateContext);
15+
16+
// Mock template data
17+
const mockModule1 = {
18+
urn: 'urn:li:pageModule:1',
19+
type: EntityType.DatahubPageModule,
20+
properties: {
21+
name: 'Module 1',
22+
type: DataHubPageModuleType.OwnedAssets,
23+
visibility: { scope: PageModuleScope.Personal },
24+
params: {},
25+
},
26+
};
27+
28+
const mockModule2 = {
29+
urn: 'urn:li:pageModule:2',
30+
type: EntityType.DatahubPageModule,
31+
properties: {
32+
name: 'Module 2',
33+
type: DataHubPageModuleType.Domains,
34+
visibility: { scope: PageModuleScope.Personal },
35+
params: {},
36+
},
37+
};
38+
39+
const mockTemplate = {
40+
urn: 'urn:li:pageTemplate:test',
41+
type: EntityType.DatahubPageTemplate,
42+
properties: {
43+
rows: [
44+
{
45+
modules: [mockModule1, mockModule2],
46+
},
47+
],
48+
},
49+
};
50+
51+
describe('useDragAndDrop', () => {
52+
const mockMoveModule = vi.fn();
53+
54+
beforeEach(() => {
55+
vi.clearAllMocks();
56+
57+
mockUsePageTemplateContext.mockReturnValue({
58+
moveModule: mockMoveModule,
59+
} as any);
60+
});
61+
62+
it('should call moveModule with correct parameters on drag end', async () => {
63+
const { result } = renderHook(() => useDragAndDrop());
64+
65+
const mockDragEndEvent = {
66+
active: {
67+
data: {
68+
current: {
69+
module: mockModule1,
70+
position: { rowIndex: 0, moduleIndex: 0 },
71+
},
72+
},
73+
},
74+
over: {
75+
data: {
76+
current: {
77+
rowIndex: 0,
78+
moduleIndex: 1,
79+
},
80+
},
81+
},
82+
};
83+
84+
await act(async () => {
85+
await result.current.handleDragEnd(mockDragEndEvent as any);
86+
});
87+
88+
expect(mockMoveModule).toHaveBeenCalledWith({
89+
module: mockModule1,
90+
fromPosition: { rowIndex: 0, moduleIndex: 0 },
91+
toPosition: {
92+
rowIndex: 0,
93+
moduleIndex: 1,
94+
rowSide: 'right',
95+
},
96+
});
97+
});
98+
99+
it('should not call moveModule when dropping in the same position', async () => {
100+
const { result } = renderHook(() => useDragAndDrop());
101+
102+
const mockDragEndEvent = {
103+
active: {
104+
data: {
105+
current: {
106+
module: mockModule1,
107+
position: { rowIndex: 0, moduleIndex: 0 },
108+
},
109+
},
110+
},
111+
over: {
112+
data: {
113+
current: {
114+
rowIndex: 0,
115+
moduleIndex: 0,
116+
},
117+
},
118+
},
119+
};
120+
121+
await act(async () => {
122+
await result.current.handleDragEnd(mockDragEndEvent as any);
123+
});
124+
125+
expect(mockMoveModule).not.toHaveBeenCalled();
126+
});
127+
128+
it('should not call moveModule when drag or drop data is missing', async () => {
129+
const { result } = renderHook(() => useDragAndDrop());
130+
131+
const mockDragEndEvent = {
132+
active: {
133+
data: null,
134+
},
135+
over: {
136+
data: {
137+
current: {
138+
rowIndex: 0,
139+
moduleIndex: 1,
140+
},
141+
},
142+
},
143+
};
144+
145+
await act(async () => {
146+
await result.current.handleDragEnd(mockDragEndEvent as any);
147+
});
148+
149+
expect(mockMoveModule).not.toHaveBeenCalled();
150+
});
151+
152+
it('should set correct rowSide based on module index', async () => {
153+
const { result } = renderHook(() => useDragAndDrop());
154+
155+
// Test dropping at index 0 (should be 'left')
156+
const mockDragEndEventLeft = {
157+
active: {
158+
data: {
159+
current: {
160+
module: mockModule1,
161+
position: { rowIndex: 0, moduleIndex: 1 },
162+
},
163+
},
164+
},
165+
over: {
166+
data: {
167+
current: {
168+
rowIndex: 1,
169+
moduleIndex: 0,
170+
},
171+
},
172+
},
173+
};
174+
175+
await act(async () => {
176+
await result.current.handleDragEnd(mockDragEndEventLeft as any);
177+
});
178+
179+
expect(mockMoveModule).toHaveBeenCalledWith({
180+
module: mockModule1,
181+
fromPosition: { rowIndex: 0, moduleIndex: 1 },
182+
toPosition: {
183+
rowIndex: 1,
184+
moduleIndex: 0,
185+
rowSide: 'left',
186+
},
187+
});
188+
});
189+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useCallback } from 'react';
2+
import { DragEndEvent } from '@dnd-kit/core';
3+
4+
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
5+
import { ModulePositionInput } from '@app/homeV3/template/types';
6+
7+
import { PageModuleFragment } from '@graphql/template.generated';
8+
9+
interface DraggedModuleData {
10+
module: PageModuleFragment;
11+
position: ModulePositionInput;
12+
}
13+
14+
export interface DroppableData {
15+
rowIndex: number;
16+
moduleIndex?: number; // If undefined, drop at the end of the row
17+
}
18+
19+
export function useDragAndDrop() {
20+
const { moveModule } = usePageTemplateContext();
21+
22+
const handleDragEnd = useCallback(
23+
async (event: DragEndEvent) => {
24+
const { active, over } = event;
25+
26+
if (!over || !active.data.current || !over.data.current) {
27+
return;
28+
}
29+
30+
const draggedData = active.data.current as DraggedModuleData;
31+
const droppableData = over.data.current as DroppableData;
32+
33+
// Check if we're dropping in the same position
34+
if (
35+
draggedData.position.rowIndex === droppableData.rowIndex &&
36+
draggedData.position.moduleIndex === droppableData.moduleIndex
37+
) {
38+
return;
39+
}
40+
41+
// Create the to position based on the drop data
42+
const toPosition: ModulePositionInput = {
43+
rowIndex: droppableData.rowIndex,
44+
moduleIndex: droppableData.moduleIndex,
45+
// Set rowSide based on module index
46+
rowSide: droppableData.moduleIndex === 0 ? 'left' : 'right',
47+
};
48+
49+
// Use the moveModule function which handles validation, persistence, and error handling
50+
moveModule({
51+
module: draggedData.module,
52+
fromPosition: draggedData.position,
53+
toPosition,
54+
});
55+
},
56+
[moveModule],
57+
);
58+
59+
return {
60+
handleDragEnd,
61+
};
62+
}

0 commit comments

Comments
 (0)