Skip to content

Commit 8690180

Browse files
authored
feat: remove rc-tree and develop the new Tree component (#418)
* feat: remove rc-tree and develop the new Tree component * feat: support onDropTree in tree component * fix: add data-* for treeNode * fix: improve select in tree component * fix: reset the dragProvider in workbench * feat: support to expand editable node * test: update snapshot * fix: remove useless styleSheet * chore: remove rc-tree dependency * test: improve test for paneView which is on top of TreeView * fix: remove the style of rc-tree * test: improve unit test for tree component * fix: prevent drag to itself will expand itself * feat: support to controll the expand keys * feat: support to distinguish loadData in tree * fix: improve the color theme for indent * feat: support treeClick event for reset the current active node * fix: improve the porblems caused by rebase * chore: remove useless theme color * refactor: rename onSelectNode to onSelect
1 parent 8626600 commit 8690180

File tree

30 files changed

+1077
-552
lines changed

30 files changed

+1077
-552
lines changed

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
"rc-dialog": "8.2.1",
4646
"rc-textarea": "~0.3.1",
4747
"rc-tooltip": "^5.1.1",
48-
"rc-tree": "~3.10.0",
4948
"rc-util": "~5.5.0",
5049
"react-dnd": "^9.3.4",
5150
"react-dnd-html5-backend": "^9.3.4",

src/common/event/eventBus.ts

+8
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,12 @@ export abstract class GlobalEvent {
2020
public emit(name: string, ...args: any) {
2121
EventBus.emit(name, ...args);
2222
}
23+
24+
/**
25+
* Count the service event
26+
* @param name Event name
27+
*/
28+
public count(name: string) {
29+
return EventBus.count(name);
30+
}
2331
}

src/common/event/eventEmitter.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export class EventEmitter {
22
private _events = new Map<string, Function[]>();
33

4+
public count(name: string) {
5+
const events = this._events.get(name) || [];
6+
return events.length;
7+
}
8+
49
public emit(name: string, ...args) {
510
const events = this._events.get(name);
611
if (events && events.length > 0) {

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

+226-23
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import React from 'react';
22
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react';
33
import '@testing-library/jest-dom';
44
import TreeView, { ITreeNodeItemProps } from '../index';
5-
import { dragToTargetNode } from '@test/utils';
5+
import { dragToTargetNode, expectFnCalled, sleep } from '@test/utils';
6+
import { act } from 'react-test-renderer';
7+
import { unexpandTreeNodeClassName, expandTreeNodeClassName } from '../base';
68

79
const mockData: ITreeNodeItemProps[] = [
810
{
@@ -23,6 +25,15 @@ const mockData: ITreeNodeItemProps[] = [
2325
},
2426
];
2527

28+
// mock Scrollable component
29+
jest.mock('lodash', () => {
30+
const originalModule = jest.requireActual('lodash');
31+
return {
32+
...originalModule,
33+
debounce: (fn) => fn,
34+
};
35+
});
36+
2637
describe('Test the Tree component', () => {
2738
afterEach(cleanup);
2839

@@ -85,7 +96,7 @@ describe('Test the Tree component', () => {
8596

8697
const parentIcon = container
8798
.querySelector<HTMLDivElement>('div[data-id="mo_treeNode_1"]')
88-
?.querySelector('span.codicon-chevron-right');
99+
?.querySelector('span.codicon-chevron-down');
89100

90101
const childNode = await waitFor(() =>
91102
container.querySelector<HTMLDivElement>(
@@ -191,6 +202,25 @@ describe('Test the Tree component', () => {
191202
expect(await findByTitle('test2')).toBeInTheDocument();
192203
});
193204

205+
test('Should support to drag into children', async () => {
206+
const data = [
207+
{
208+
id: '1',
209+
name: 'test1',
210+
children: [
211+
{
212+
id: '2',
213+
name: 'test2',
214+
isEditable: true,
215+
},
216+
],
217+
},
218+
];
219+
const { findByTitle } = render(<TreeView data={data} />);
220+
221+
expect(await findByTitle('test2')).toBeInTheDocument();
222+
});
223+
194224
test('Should NOT support to sort via drag', async () => {
195225
const data = [
196226
{ id: '1', name: 'test1', isLeaf: true },
@@ -220,15 +250,12 @@ describe('Test the Tree component', () => {
220250
},
221251
];
222252
const mockFn = jest.fn();
223-
const { findByTitle } = render(
224-
<TreeView
225-
draggable
226-
onDropTree={mockFn}
227-
defaultExpandAll
228-
data={data}
229-
/>
253+
const { findByTitle, getByTitle } = render(
254+
<TreeView draggable onDropTree={mockFn} data={data} />
230255
);
231256

257+
fireEvent.click(getByTitle('test1'));
258+
232259
dragToTargetNode(
233260
await findByTitle('test1-1'),
234261
await findByTitle('test1')
@@ -250,15 +277,12 @@ describe('Test the Tree component', () => {
250277
source,
251278
];
252279
const mockFn = jest.fn();
253-
const { findByTitle } = render(
254-
<TreeView
255-
draggable
256-
onDropTree={mockFn}
257-
defaultExpandAll
258-
data={data}
259-
/>
280+
const { findByTitle, getByTitle } = render(
281+
<TreeView draggable onDropTree={mockFn} data={data} />
260282
);
261283

284+
fireEvent.click(getByTitle('test1'));
285+
262286
dragToTargetNode(
263287
await findByTitle(source.name),
264288
await findByTitle(target.name)
@@ -287,15 +311,13 @@ describe('Test the Tree component', () => {
287311
},
288312
];
289313
const mockFn = jest.fn();
290-
const { findByTitle } = render(
291-
<TreeView
292-
draggable
293-
onDropTree={mockFn}
294-
defaultExpandAll
295-
data={data}
296-
/>
314+
const { findByTitle, getByTitle } = render(
315+
<TreeView draggable onDropTree={mockFn} data={data} />
297316
);
298317

318+
fireEvent.click(getByTitle('test1'));
319+
fireEvent.click(getByTitle('test2'));
320+
299321
dragToTargetNode(
300322
await findByTitle(source.name),
301323
await findByTitle(target.name)
@@ -310,4 +332,185 @@ describe('Test the Tree component', () => {
310332
children: [target],
311333
});
312334
});
335+
336+
test('Should NOT drag node to its parent node or drag node to its siblings or drag node to itself', async () => {
337+
const data = [
338+
{
339+
id: '1',
340+
name: 'test1',
341+
children: [
342+
{ id: '1-1', isLeaf: true, name: 'test1-1' },
343+
{ id: '1-2', isLeaf: true, name: 'test1-2' },
344+
],
345+
},
346+
{
347+
id: '2',
348+
name: 'test2',
349+
isLeaf: true,
350+
},
351+
];
352+
const mockFn = jest.fn();
353+
const { findByTitle } = render(
354+
<TreeView draggable onDropTree={mockFn} data={data} />
355+
);
356+
357+
fireEvent.click(await findByTitle('test1'));
358+
359+
dragToTargetNode(
360+
await findByTitle('test1-1'),
361+
await findByTitle('test1')
362+
);
363+
364+
expect(mockFn).not.toBeCalled();
365+
366+
dragToTargetNode(
367+
await findByTitle('test1-2'),
368+
await findByTitle('test1-1')
369+
);
370+
expect(mockFn).not.toBeCalled();
371+
372+
dragToTargetNode(
373+
await findByTitle('test1'),
374+
await findByTitle('test1')
375+
);
376+
expect(mockFn).not.toBeCalled();
377+
});
378+
379+
test('Should end drop when drag node out of tree', async () => {
380+
const data = [
381+
{
382+
id: '1',
383+
name: 'test1',
384+
children: [
385+
{ id: '1-1', isLeaf: true, name: 'test1-1' },
386+
{ id: '1-2', isLeaf: true, name: 'test1-2' },
387+
],
388+
},
389+
];
390+
const mockFn = jest.fn();
391+
const { findByTitle, container, getByTestId } = render(
392+
<TreeView draggable onDropTree={mockFn} data={data} />
393+
);
394+
395+
// creat a dom insert into body as the drop node
396+
const outOfTree = document.createElement('div');
397+
outOfTree.dataset.testid = 'outOfTree';
398+
outOfTree.style.width = '100px';
399+
outOfTree.style.height = '100px';
400+
container.appendChild(outOfTree);
401+
402+
// expand the parent node
403+
fireEvent.click(await findByTitle('test1'));
404+
fireEvent.dragStart(await findByTitle('test1'));
405+
fireEvent.dragOver(await findByTitle('test1'));
406+
407+
expect(container.querySelectorAll('.drag-over').length).not.toBe(0);
408+
409+
// drag node out of tree and drop it
410+
fireEvent.dragOver(getByTestId('outOfTree'));
411+
fireEvent.dragEnd(getByTestId('outOfTree'));
412+
413+
expect(container.querySelectorAll('.drag-over').length).toBe(0);
414+
});
415+
416+
test('Should expand the drop node if this node is a folder', async () => {
417+
const data = [
418+
{
419+
id: '1',
420+
name: 'test1',
421+
children: [{ id: '1-1', isLeaf: true, name: 'test1-1' }],
422+
},
423+
{ id: '2', isLeaf: true, name: 'test2' },
424+
];
425+
const { getByText } = render(<TreeView draggable data={data} />);
426+
427+
expect(getByText('test1').parentElement!.classList).toContain(
428+
unexpandTreeNodeClassName
429+
);
430+
dragToTargetNode(getByText('test2'), getByText('test1'));
431+
432+
expect(getByText('test1').parentElement!.classList).toContain(
433+
expandTreeNodeClassName
434+
);
435+
436+
// drag to itself won't expand
437+
dragToTargetNode(getByText('test1'), getByText('test1'));
438+
expect(getByText('test1').parentElement!.classList).toContain(
439+
unexpandTreeNodeClassName
440+
);
441+
});
442+
443+
test('Should support to loadData in sync', async () => {
444+
const data = [
445+
{
446+
id: '1',
447+
name: 'test1',
448+
isLeaf: false,
449+
children: [],
450+
},
451+
];
452+
const mockFn = jest.fn().mockImplementation(() => sleep(1000));
453+
const { getByText, container } = render(
454+
<TreeView data={data} onLoadData={mockFn} />
455+
);
456+
457+
act(() => {
458+
fireEvent.click(getByText('test1'));
459+
});
460+
461+
expect(mockFn).toBeCalledTimes(1);
462+
expect(container.querySelector('.codicon-spin')).toBeInTheDocument();
463+
await sleep(1000);
464+
expect(container.querySelector('.codicon-spin')).toBeNull();
465+
466+
act(() => {
467+
// unfold it and open it again
468+
fireEvent.click(getByText('test1'));
469+
fireEvent.click(getByText('test1'));
470+
});
471+
472+
// didn't trigger onLoadData this time
473+
expect(mockFn).toBeCalledTimes(1);
474+
});
475+
476+
test('Should support to be controlled', () => {
477+
const mockFn = jest.fn();
478+
const { getByText, rerender } = render(
479+
<TreeView data={mockData} expandKeys={[]} onExpand={mockFn} />
480+
);
481+
482+
expect(getByText('test1').parentElement?.classList).toContain(
483+
unexpandTreeNodeClassName
484+
);
485+
486+
fireEvent.click(getByText('test1'));
487+
488+
expect(getByText('test1').parentElement?.classList).toContain(
489+
unexpandTreeNodeClassName
490+
);
491+
expect(mockFn).toBeCalled();
492+
493+
rerender(
494+
<TreeView
495+
data={mockData}
496+
expandKeys={[mockData[0].key!]}
497+
onExpand={mockFn}
498+
/>
499+
);
500+
501+
expect(getByText('test1').parentElement?.classList).toContain(
502+
expandTreeNodeClassName
503+
);
504+
});
505+
506+
test('Should support to trigger tree click event', () => {
507+
expectFnCalled((fn) => {
508+
const { getByRole } = render(
509+
<TreeView data={mockData} onTreeClick={fn} />
510+
);
511+
512+
const wrapper = getByRole('tree');
513+
fireEvent.click(wrapper);
514+
});
515+
});
313516
});

src/components/tree/base.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {
2+
getBEMElement,
3+
getBEMModifier,
4+
prefixClaName,
5+
} from 'mo/common/className';
6+
7+
export const defaultTreeClassName = prefixClaName('tree');
8+
export const defaultTreeNodeClassName = getBEMElement(
9+
defaultTreeClassName,
10+
'treenode'
11+
);
12+
export const activeTreeNodeClassName = getBEMModifier(
13+
defaultTreeNodeClassName,
14+
'active'
15+
);
16+
17+
export const expandTreeNodeClassName = getBEMModifier(
18+
defaultTreeNodeClassName,
19+
'open'
20+
);
21+
22+
export const unexpandTreeNodeClassName = getBEMModifier(
23+
defaultTreeNodeClassName,
24+
'close'
25+
);
26+
27+
export const indentClassName = getBEMElement(defaultTreeClassName, 'indent');
28+
export const indentGuideClassName = getBEMElement(indentClassName, 'guide');
29+
30+
export const treeNodeTitleClassName = getBEMElement(
31+
defaultTreeNodeClassName,
32+
'title'
33+
);

0 commit comments

Comments
 (0)