Skip to content

Commit

Permalink
feat(design): support multiple tree
Browse files Browse the repository at this point in the history
  • Loading branch information
Gggpound committed May 16, 2024
1 parent 62ee96e commit b7972a8
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 43 deletions.
13 changes: 7 additions & 6 deletions packages/design/src/components/tree/Tree.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import type { Meta } from '@storybook/react';
import React, { useState } from 'react';

import { Tree, TreeSelectionMode } from './Tree';
import { Tree, TreeSelectionMode, ITreeNodeProps } from './Tree';

Check failure on line 20 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

Imports "ITreeNodeProps" are only used as type

Check failure on line 20 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

Member 'ITreeNodeProps' of the import declaration should be sorted alphabetically

const meta: Meta<typeof Tree> = {
title: 'Components / Tree',
Expand All @@ -32,7 +32,7 @@ export default meta;

export const TreeBasic = {
render() {
const [value, setValue] = useState<string | number | boolean>();
const [valueGroup, valueGroupSet] = useState<string[]>([]);

const data = [
{
Expand Down Expand Up @@ -111,17 +111,18 @@ export const TreeBasic = {
},
];

function handleChange(value: string | number | boolean) {
setValue(value);
function handleSelected(node: ITreeNodeProps, result: ITreeNodeProps[]) {
valueGroupSet(result.map(e => e.key));

Check failure on line 115 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

Expected parentheses around arrow function argument
// console.log('all leafNode', node, result)
}

return (
<Tree
data={data}
defaultExpandAll
selectionMode={TreeSelectionMode.ONLY_LEAF_NODE}
value={value}
onChange={handleChange}
valueGroup={valueGroup}
onChange={handleSelected}
/>
);
},
Expand Down
93 changes: 60 additions & 33 deletions packages/design/src/components/tree/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
* limitations under the License.
*/

import { CheckMarkSingle, DropdownSingle } from '@univerjs/icons';
import { DropdownSingle } from '@univerjs/icons';
import clsx from 'clsx';
import { Checkbox } from '../checkbox'

Check failure on line 19 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Missing semicolon
import React, { useEffect, useMemo, useState } from 'react';

Check failure on line 20 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

`react` import should occur before import of `../checkbox`

import styles from './index.module.less';

import { isIntermediated, createCacheWithFindNodePathFromTree, mergeTreeSelected, filterLeafNode } from './util'

Check failure on line 23 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Expected 1 empty line after import statement not followed by another import

Check failure on line 23 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Member 'createCacheWithFindNodePathFromTree' of the import declaration should be sorted alphabetically

Check failure on line 23 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Missing semicolon
export enum TreeSelectionMode {
ONLY_LEAF_NODE,

Expand Down Expand Up @@ -51,33 +52,46 @@ export interface ITreeProps {
selectionMode?: TreeSelectionMode;

/**
* Used for setting the currently selected value
* Used for setting the currently selected value,leaf node.
*/
value?: string | number | boolean;
valueGroup?: string[];

/**
* Set the handler to handle `click` event
*/
onChange?: (value: string | number | boolean) => void;
onChange?: (node: ITreeNodeProps, allSelectedNode: ITreeNodeProps[]) => void;

onExpend?: (value: string) => void

Check failure on line 64 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Expected a semicolon
}

type TreeItemProps = ITreeNodeProps & {
_selected?: boolean;
_expand?: boolean;
_intermediated?: boolean

Check failure on line 70 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Expected a semicolon
};

/**
* Tree Component
*/
export function Tree(props: ITreeProps) {
const { data = [], defaultExpandAll = false, selectionMode = TreeSelectionMode.ALL, value, onChange } = props;
const { data = [], defaultExpandAll = false, selectionMode = TreeSelectionMode.ALL, valueGroup = [], onChange, onExpend } = props;

const [expandKeys, setExpandKeys] = useState<Array<string | number | boolean>>([]);

const findNode = useMemo(() => createCacheWithFindNodePathFromTree(data), [data])

const selectedNodeKeySet = useMemo(() => {
const set = new Set<string>()
valueGroup.forEach(key => {
const path = findNode.findNodePathFromTreeWithCache(key)
path.forEach(k => set.add(k))
})
return set
}, [valueGroup, findNode])

useEffect(() => {
function walkData(item: ITreeNodeProps) {
setExpandKeys((prev) => [...prev, item.key]);

item.children?.forEach(walkData);
}
if (defaultExpandAll) {
Expand All @@ -86,21 +100,34 @@ export function Tree(props: ITreeProps) {
}, [defaultExpandAll, data]);

const computedData = useMemo(() => {
return data.map(function walkData(item): TreeItemProps {
const { title, key, children } = item;

return {
title,
key,
children: children && children.map(walkData),
_selected: key === value,
_expand: expandKeys.includes(key),
};
return data.map((item) => {
function walkData(item: ITreeNodeProps): TreeItemProps {
const { title, key, children } = item;
const isExpand = expandKeys.includes(key)
const isSelected = selectedNodeKeySet.has(key)
const intermediated = isIntermediated(selectedNodeKeySet, item)

return {
title,
key,
children: children && children.map((item) => walkData(item)),
_selected: isSelected,
_expand: isExpand,
_intermediated: intermediated
};
}
return walkData(item)
});
}, [value, expandKeys]);
}, [selectedNodeKeySet, expandKeys]);

function handleSelectItem(treeItem: ITreeNodeProps) {
if (treeItem.children) {
function handleChange(treeItem: TreeItemProps) {
const path: string[] = findNode.findNodePathFromTreeWithCache(treeItem.key)
const result = mergeTreeSelected(data, [...selectedNodeKeySet], path)
onChange?.(treeItem, filterLeafNode(data, result))
}

function handleExpendItem(treeItem: ITreeNodeProps) {
if (treeItem.children?.length) {
setExpandKeys((prev) => {
const index = prev.findIndex((key) => key === treeItem.key);

Expand All @@ -111,27 +138,21 @@ export function Tree(props: ITreeProps) {
return [...prev.slice(0, index), ...prev.slice(index + 1)];
});
}

if (selectionMode === TreeSelectionMode.ONLY_LEAF_NODE) {
if (treeItem.children) {
return;
}
}

onChange?.(treeItem.key);
onExpend?.(treeItem.key)
}

function walkTree(treeItem: TreeItemProps) {
const { title, key, children, _selected, _expand } = treeItem;

const { title, key, children, _selected, _expand, _intermediated } = treeItem;
return (
<li
key={key}
className={styles.treeListItem}
onClick={(e) => {
e.stopPropagation();
handleSelectItem(treeItem);
}}
>
<a
className={clsx(styles.treeListItemContent, {
Expand All @@ -143,15 +164,21 @@ export function Tree(props: ITreeProps) {
className={clsx(styles.treeIcon, {
[styles.treeIconExpand]: _expand,
})}

onClick={(e) => {
e.stopPropagation();
handleExpendItem(treeItem);
}}
>
<DropdownSingle />
</span>
)}
{_selected && (
<span className={styles.treeListItemContentSelectedIcon}>
<CheckMarkSingle />
</span>
)}
<Checkbox
checked={_selected && !_intermediated}
indeterminate={_selected && _intermediated}
onChange={() => {
handleChange(treeItem)
}} />
<span>{title}</span>
</a>
{children && (
Expand Down
59 changes: 59 additions & 0 deletions packages/design/src/components/tree/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,67 @@ import React from 'react';
import { describe, expect, it } from 'vitest';

import { Tree } from '../Tree';
import { findNodePathFromTree, findSubTreeFromPath, findNodeFromPath, mergeTreeSelected, isIntermediated } from '../util'

describe('Tree', () => {
const data = [
{
key: '0',
title: 'node 0',
children: [
{ key: '0-0', title: 'node 0-0' },
{ key: '0-1', title: 'node 0-1' },
],
},
]
it('test findNodePathFromTree', () => {
expect(findNodePathFromTree(data, '0-0')).toEqual(['0', '0-0'])
expect(findNodePathFromTree(data, '2-0')).toEqual([])
})

it('test findSubTreeFromPath', () => {
expect(findSubTreeFromPath(data, ['0'])).toEqual([
{ key: '0-0', title: 'node 0-0' },
{ key: '0-1', title: 'node 0-1' },
])
expect(findSubTreeFromPath(data, [])).toEqual(data)
})

it('test findNodeFromPath', () => {
expect(findNodeFromPath(data, ['0'])).toEqual({
key: '0',
title: 'node 0',
children: [
{ key: '0-0', title: 'node 0-0' },
{ key: '0-1', title: 'node 0-1' },
],
})
expect(findNodeFromPath(data, [])).toEqual(undefined)
})

it('test mergeTreeSelected', () => {
expect(mergeTreeSelected(data, [], ['0', '0-0'])).toEqual(['0', '0-0'])
})

it('test isIntermediated', () => {
expect(isIntermediated(new Set(['0-0', '0']), {
key: '0',
title: 'node 0',
children: [
{ key: '0-0', title: 'node 0-0' },
{ key: '0-1', title: 'node 0-1' },
],
})).toBeTruthy()
expect(isIntermediated(new Set(['0-1', '0-0', '0']), {
key: '0',
title: 'node 0',
children: [
{ key: '0-0', title: 'node 0-0' },
{ key: '0-1', title: 'node 0-1' },
],
})).toBeFalsy()
})

it('defaultExpandAll', async () => {
const { container } = render(
<Tree
Expand Down
7 changes: 4 additions & 3 deletions packages/design/src/components/tree/index.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@
position: relative;
padding-left: var(--padding-xl);

> a {
>a {
cursor: pointer;
position: relative;
display: block;
display: flex;
align-items: center;
}

&-content {
margin-bottom: 8px;

&-selected {
font-weight: 500;
// font-weight: 500;
color: rgb(var(--primary-color));

&-icon {
Expand Down
1 change: 1 addition & 0 deletions packages/design/src/components/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
*/

export { type ITreeNodeProps, type ITreeProps, Tree, TreeSelectionMode } from './Tree';
export { mergeTreeSelected, findSubTreeFromPath, findNodePathFromTree, filterLeafNode } from './util'
Loading

0 comments on commit b7972a8

Please sign in to comment.