Skip to content

Commit 8626600

Browse files
authored
feat: support to subscribe the drop event in tree (#450)
* feat: support to subscribe the drop event in tree * test: improve the test about dragging * test: improve test for folder tree service
1 parent f8fdbcc commit 8626600

File tree

9 files changed

+147
-108
lines changed

9 files changed

+147
-108
lines changed

src/components/tree/__tests__/tree.test.tsx

+79-28
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,6 @@ const mockData: ITreeNodeItemProps[] = [
2323
},
2424
];
2525

26-
async function dragExpect(fn: jest.Mock, result: any) {
27-
await waitFor(() => {
28-
expect(fn).toBeCalled();
29-
expect(fn.mock.calls[0][0]).toEqual(result);
30-
});
31-
}
32-
3326
describe('Test the Tree component', () => {
3427
afterEach(cleanup);
3528

@@ -198,11 +191,11 @@ describe('Test the Tree component', () => {
198191
expect(await findByTitle('test2')).toBeInTheDocument();
199192
});
200193

201-
test('Should support to sort via drag', async () => {
194+
test('Should NOT support to sort via drag', async () => {
202195
const data = [
203-
{ id: '1', name: 'test1' },
204-
{ id: '2', name: 'test2' },
205-
{ id: '3', name: 'test3' },
196+
{ id: '1', name: 'test1', isLeaf: true },
197+
{ id: '2', name: 'test2', isLeaf: true },
198+
{ id: '3', name: 'test3', isLeaf: true },
206199
];
207200
const mockFn = jest.fn();
208201
const { findByTitle } = render(
@@ -214,24 +207,47 @@ describe('Test the Tree component', () => {
214207
await findByTitle('test1')
215208
);
216209

217-
await dragExpect(mockFn, [
218-
{ id: '1', name: 'test1' },
219-
{ id: '3', name: 'test3' },
220-
{ id: '2', name: 'test2' },
221-
]);
210+
expect(mockFn).not.toBeCalled();
222211
});
223212

224-
test('Should support to drag into children', async () => {
213+
test('Should NOT darg to the its parent node', async () => {
225214
const data = [
226215
{
227216
id: '1',
228217
name: 'test1',
229-
children: [{ id: '1-1', name: 'test1-1' }],
218+
isLeaf: false,
219+
children: [{ id: '1-1', name: 'test1-1', isLeaf: true }],
230220
},
221+
];
222+
const mockFn = jest.fn();
223+
const { findByTitle } = render(
224+
<TreeView
225+
draggable
226+
onDropTree={mockFn}
227+
defaultExpandAll
228+
data={data}
229+
/>
230+
);
231+
232+
dragToTargetNode(
233+
await findByTitle('test1-1'),
234+
await findByTitle('test1')
235+
);
236+
237+
expect(mockFn).not.toBeCalled();
238+
});
239+
240+
test('Should support to drag into children', async () => {
241+
const source = { id: '2', name: 'test2', isLeaf: true };
242+
const target = { id: '1-1', name: 'test1-1', isLeaf: false };
243+
const data = [
231244
{
232-
id: '2',
233-
name: 'test2',
245+
id: '1',
246+
name: 'test1',
247+
isLeaf: false,
248+
children: [target],
234249
},
250+
source,
235251
];
236252
const mockFn = jest.fn();
237253
const { findByTitle } = render(
@@ -244,19 +260,54 @@ describe('Test the Tree component', () => {
244260
);
245261

246262
dragToTargetNode(
247-
await findByTitle('test2'),
248-
await findByTitle('test1-1')
263+
await findByTitle(source.name),
264+
await findByTitle(target.name)
249265
);
250266

251-
await dragExpect(mockFn, [
267+
expect(mockFn).toBeCalled();
268+
expect(mockFn.mock.calls[0][0]).toEqual(source);
269+
expect(mockFn.mock.calls[0][1]).toEqual(target);
270+
});
271+
272+
test('Should support to drag to the folder rather than a file', async () => {
273+
const source = { id: '2-1', name: 'test2-1', isLeaf: true };
274+
const target = { id: '1-1', name: 'test1-1', isLeaf: true };
275+
const data = [
252276
{
253277
id: '1',
254278
name: 'test1',
255-
children: [
256-
{ id: '1-1', name: 'test1-1' },
257-
{ id: '2', name: 'test2' },
258-
],
279+
isLeaf: false,
280+
children: [target],
259281
},
260-
]);
282+
{
283+
id: '2',
284+
name: 'test2',
285+
isLeaf: false,
286+
children: [source],
287+
},
288+
];
289+
const mockFn = jest.fn();
290+
const { findByTitle } = render(
291+
<TreeView
292+
draggable
293+
onDropTree={mockFn}
294+
defaultExpandAll
295+
data={data}
296+
/>
297+
);
298+
299+
dragToTargetNode(
300+
await findByTitle(source.name),
301+
await findByTitle(target.name)
302+
);
303+
304+
expect(mockFn).toBeCalled();
305+
expect(mockFn.mock.calls[0][0]).toEqual(source);
306+
expect(mockFn.mock.calls[0][1]).toEqual({
307+
id: '1',
308+
name: 'test1',
309+
isLeaf: false,
310+
children: [target],
311+
});
261312
});
262313
});

src/components/tree/index.tsx

+27-57
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { prefixClaName, classNames } from 'mo/common/className';
55
import type { DataNode } from 'rc-tree/lib/interface';
66
import { FileTypes } from 'mo/model';
77
import type { LoadEventData } from 'mo/controller';
8+
import { TreeViewUtil } from 'mo/services/helper';
89

910
export interface ITreeNodeItemProps {
1011
disabled?: boolean;
@@ -26,7 +27,7 @@ export interface ITreeProps extends Partial<TreeProps> {
2627
index: number,
2728
isLeaf: boolean
2829
) => JSX.Element | string;
29-
onDropTree?(treeNode: ITreeNodeItemProps[]): void;
30+
onDropTree?(source: ITreeNodeItemProps, target: ITreeNodeItemProps): void;
3031
onLoadData?: (treeNode: LoadEventData) => Promise<void>;
3132
}
3233

@@ -46,66 +47,35 @@ const TreeView = ({
4647

4748
const onDrop = (info) => {
4849
if (!draggable) return;
49-
const dropId = info.node.data.id;
50-
const dragId = info.dragNode.data.id;
51-
const dropPos = info.node.pos.split('-');
52-
const dropPosition =
53-
info.dropPosition - Number(dropPos[dropPos.length - 1]);
54-
55-
const loopTree = (
56-
data: ITreeNodeItemProps[],
57-
key: string,
58-
callback: (
59-
item: ITreeNodeItemProps,
60-
index: number,
61-
arr: ITreeNodeItemProps[]
62-
) => void
63-
) => {
64-
data.forEach((item, index, arr) => {
65-
if (item.id === key) {
66-
return callback(item, index, arr);
67-
}
68-
if (item.children) {
69-
return loopTree(item.children, key, callback);
70-
}
71-
});
72-
};
73-
const treeData = [...data];
74-
75-
let dragObj;
76-
loopTree(treeData, dragId, (item, index, arr) => {
77-
arr.splice(index, 1);
78-
dragObj = item;
50+
const source = info.dragNode;
51+
const target = info.node;
52+
const treeViewUtil = new TreeViewUtil({
53+
id: Number.MAX_SAFE_INTEGER,
54+
children: data,
7955
});
80-
81-
if (!info.dropToGap) {
82-
loopTree(treeData, dropId, (item) => {
83-
item.children = item.children || [];
84-
item.children.push(dragObj);
85-
});
86-
} else if (
87-
(info.node.data.children || []).length > 0 &&
88-
info.node.expanded &&
89-
dropPosition === 1
90-
) {
91-
loopTree(treeData, dropId, (item) => {
92-
item.children = item.children || [];
93-
item.children.unshift(dragObj);
94-
});
56+
if (target.data.isLeaf) {
57+
// Can't drag into a file, so the target would to be the parent of this target
58+
const obj = treeViewUtil.indexes[target.data.id];
59+
const targetParentId = obj.parent!;
60+
61+
const sourceParentId = treeViewUtil.indexes[source.data.id].parent;
62+
// Can't drag under same folder
63+
if (targetParentId === sourceParentId) {
64+
return;
65+
}
66+
onDropTree?.(
67+
source.data,
68+
treeViewUtil.indexes[targetParentId].node!
69+
);
9570
} else {
96-
let ar;
97-
let i;
98-
loopTree(treeData, dropId, (item, index, arr) => {
99-
ar = arr;
100-
i = index;
101-
});
102-
if (dropPosition === -1) {
103-
ar.splice(i, 0, dragObj);
104-
} else {
105-
ar.splice(i + 1, 0, dragObj);
71+
const sourceParentId = treeViewUtil.indexes[source.data.id].parent;
72+
// Can't drag to the parent node
73+
if (sourceParentId === target.data.id) {
74+
return;
10675
}
76+
77+
onDropTree?.(source.data, target.data);
10778
}
108-
onDropTree?.(treeData);
10979
};
11080

11181
const renderTreeNodes = (

src/controller/explorer/folderTree.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export interface IFolderTreeController {
3030
) => void;
3131
readonly onUpdateFileName?: (file: ITreeNodeItemProps) => void;
3232
readonly onSelectFile?: (file: ITreeNodeItemProps) => void;
33-
readonly onDropTree?: (treeNode: ITreeNodeItemProps[]) => void;
33+
readonly onDropTree?: (
34+
source: ITreeNodeItemProps,
35+
target: ITreeNodeItemProps
36+
) => void;
3437
readonly onLoadData?: (treeNode: LoadEventData) => Promise<void>;
3538
readonly onRightClick?: (treeNode: ITreeNodeItemProps) => IMenuItemProps[];
3639
}
@@ -124,8 +127,12 @@ export class FolderTreeController
124127
return menus;
125128
};
126129

127-
public readonly onDropTree = (treeNode: ITreeNodeItemProps[]) => {
128-
this.folderTreeService.onDropTree(treeNode);
130+
public readonly onDropTree = (
131+
source: ITreeNodeItemProps,
132+
target: ITreeNodeItemProps
133+
) => {
134+
// this.folderTreeService.onDropTree(treeNode);
135+
this.emit(FolderTreeEvent.onDrop, source, target);
129136
};
130137

131138
public onUpdateFileName = (file: ITreeNodeItemProps) => {

src/model/workbench/explorer/folderTree.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export enum FolderTreeEvent {
2222
onContextMenuClick = 'folderTree.onContextMenuClick',
2323
onCreate = 'folderTree.onCreate',
2424
onLoadData = 'folderTree.onLoadData',
25+
onDrop = 'folderTree.onDrop',
2526
}
2627

2728
export interface IFolderInputEvent {

src/services/workbench/__tests__/folderTreeService.test.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ describe('Test StatusBarService', () => {
155155
});
156156
});
157157

158-
test('Should support to right click', () => {
158+
test('Should support to create event', () => {
159159
expectFnCalled((fn) => {
160160
folderTreeService.onCreate(fn);
161161
folderTreeService.emit(FolderTreeEvent.onCreate);
162162
});
163163
});
164164

165-
test('Should support to right click', () => {
165+
test('Should support to contextMenu event', () => {
166166
expectFnCalled((fn) => {
167167
folderTreeService.onContextMenu(fn);
168168
folderTreeService.emit(FolderTreeEvent.onContextMenuClick);
@@ -175,4 +175,11 @@ describe('Test StatusBarService', () => {
175175
folderTreeService.emit(FolderTreeEvent.onLoadData);
176176
});
177177
});
178+
179+
test('Should support to subscribe drop tree event', () => {
180+
expectFnCalled((fn) => {
181+
folderTreeService.onDropTree(fn);
182+
folderTreeService.emit(FolderTreeEvent.onDrop);
183+
});
184+
});
178185
});

src/services/workbench/explorer/folderTreeService.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@ export interface IFolderTreeService extends Component<IFolderTree> {
9898
* Listen to drop event
9999
* @param treeData
100100
*/
101-
onDropTree(treeData: ITreeNodeItemProps[]): void;
101+
onDropTree(
102+
callback: (
103+
source: ITreeNodeItemProps,
104+
target: ITreeNodeItemProps
105+
) => void
106+
): void;
102107
/**
103108
* Listen to right click event
104109
* @param callback
@@ -345,12 +350,13 @@ export class FolderTreeService
345350
this.subscribe(FolderTreeEvent.onSelectFile, callback);
346351
}
347352

348-
public onDropTree = (treeData: ITreeNodeItemProps[]) => {
349-
this.setState({
350-
folderTree: Object.assign(this.state.folderTree?.data, {
351-
data: treeData,
352-
}),
353-
});
353+
public onDropTree = (
354+
callback: (
355+
source: ITreeNodeItemProps,
356+
target: ITreeNodeItemProps
357+
) => void
358+
) => {
359+
this.subscribe(FolderTreeEvent.onDrop, callback);
354360
};
355361

356362
public onRightClick = (

src/workbench/sidebar/__tests__/folderTree.test.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,8 @@ describe('The FolderTree Component', () => {
200200
dragToTargetNode(getByTitle('file'), getByTitle('folder'));
201201

202202
expect(mockFn).toBeCalled();
203-
expect(mockFn.mock.calls[0][0]).toEqual([
204-
{
205-
...mockTreeData[0],
206-
children: [mockFolder, mockFile],
207-
},
208-
]);
203+
expect(mockFn.mock.calls[0][0]).toEqual(mockFile);
204+
expect(mockFn.mock.calls[0][1]).toEqual(mockFolder);
209205
});
210206

211207
test('Should suppor to init contextMenu', () => {

src/workbench/sidebar/explore/folderTree.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'reflect-metadata';
22
import React, { memo, useRef, useEffect, useLayoutEffect } from 'react';
3-
import cloneDeep from 'lodash/cloneDeep';
43
import { IFolderTree, IFolderTreeSubItem } from 'mo/model';
54
import { select, getEventPosition } from 'mo/common/dom';
65
import Tree, { ITreeNodeItemProps } from 'mo/components/tree';
@@ -207,10 +206,8 @@ const FolderTree: React.FunctionComponent<IFolderTreeProps> = (props) => {
207206
);
208207
};
209208

210-
const handleDropTree = (treeData) => {
211-
const newFolderTreeData = cloneDeep(data);
212-
newFolderTreeData[0].children = treeData;
213-
onDropTree?.(newFolderTreeData);
209+
const handleDropTree = (source, target) => {
210+
onDropTree?.(source, target);
214211
};
215212

216213
useEffect(() => {

0 commit comments

Comments
 (0)