Skip to content

Commit

Permalink
feat: add new TreePicker
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaofan2406 authored and chaofan232 committed Sep 1, 2023
1 parent e395e5a commit 283291d
Show file tree
Hide file tree
Showing 52 changed files with 1,506 additions and 263 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
"react-select": "^5.7.3",
"react-slick": "^0.29.0",
"react-toastify": "^9.1.3",
"ryze": "^0.1.1",
"slick-carousel": "^1.8.1"
},
"peerDependencies": {
Expand Down
7 changes: 3 additions & 4 deletions scripts/generate-types/generateTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ async function generateTypeDefs() {
.join(path.posix.sep);

// get all the jsx components using glob. Ignore spec & test files.
const allComponentFiles = glob.sync(globString);
const allComponentFiles = glob.sync(globString).filter((entry) => !entry.includes('/TreePicker/'));

if (!allComponentFiles || allComponentFiles.length === 0) {
console.log(
chalk.red(`No component files were found for ${options.only}
chalk.red(`No component files were found for ${options.only}
${globString}`)
);
return;
Expand Down Expand Up @@ -138,7 +138,7 @@ async function generateTypeDefs() {
await Promise.all(
parsed.map(async (code, i) => {
const result = await generateFromSource(null, code, {
babylonPlugins: ['exportDefaultFrom', 'transformImports'],
babylonPlugins: ['exportDefaultFrom', 'transformImports', 'nullishCoalescingOperator'],
});

const component = allComponents[i];
Expand All @@ -164,7 +164,6 @@ async function generateTypeDefs() {
console.log(
chalk.cyan(
`Generated type defs for ${allComponents.map(({ componentName }) => chalk.bold(componentName)).join(', ')}
`
)
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ListPickerPure/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Empty from '../Empty';
import Grid from '../Grid';
import GridRow from '../Grid/Row';
import GridCell from '../Grid/Cell';
import { useArrowFocus } from '../../hooks';
import useArrowFocus from '../../hooks/useArrowFocus';
import './styles.css';

const ListPickerPure = ({
Expand Down
2 changes: 1 addition & 1 deletion src/components/RadioGroup/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useArrowFocus } from '../../hooks';
import useArrowFocus from '../../hooks/useArrowFocus';
import { expandDts } from '../../utils';
import invariant from '../../invariant';
import '../RadioGroup/style.css';
Expand Down
32 changes: 32 additions & 0 deletions src/components/TreePicker/TreePicker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@import url('../../styles/variable.css');

.aui--tree-picker {
& .aui--tree-picker-row + .aui--tree-picker-tree {
border-top: 1px solid $color-border-base;
}
}

.aui--tree-picker-section {
margin-top: 16px;
margin-bottom: 16px;
}

.aui--tree-picker-row {
padding: 0 2px;
min-height: 36px;
display: flex;
align-items: center;
position: relative;
gap: 12px;
border-top: 1px solid $color-border-base;

& .aui--button.aui-icon {
width: 24px;
height: 24px;
min-height: 24px;
}
}

.aui--tree-picker-row-content {
flex: 1;
}
38 changes: 38 additions & 0 deletions src/components/TreePicker/TreePicker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import cc from 'classnames';
import { TreePickerProvider } from './TreePickerContext';
import TreePickerTree from './TreePickerTree';
import TreePickerHeader from './TreePickerHeader';
import TreePickerNode from './TreePickerNode';
import TreePickerNav from './TreePickerNav';
import TreePickerSearch from './TreePickerSearch';

import './TreePicker.css';

const TreePicker = ({ children, renderNode, className }) => {
const renderNodeWithKey = React.useCallback(
(node, index) => <React.Fragment key={node.id}>{renderNode(node, index)}</React.Fragment>,
[renderNode]
);

return (
<TreePickerProvider renderNode={renderNodeWithKey}>
<div className={cc('tree-picker', className)}>{children}</div>
</TreePickerProvider>
);
};

TreePicker.propTypes = {
children: PropTypes.node.isRequired,
renderNode: PropTypes.func.isRequired,
className: PropTypes.string,
};

TreePicker.Tree = TreePickerTree;
TreePicker.Header = TreePickerHeader;
TreePicker.Node = TreePickerNode;
TreePicker.Nav = TreePickerNav;
TreePicker.Search = TreePickerSearch;

export default TreePicker;
5 changes: 5 additions & 0 deletions src/components/TreePicker/TreePicker.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Meta } from '@storybook/blocks';

<Meta title="Components/TreePicker" />

ok
166 changes: 70 additions & 96 deletions src/components/TreePicker/TreePicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,114 +1,88 @@
import React from 'react';
import axios from 'axios';
import type { Meta, StoryObj } from '@storybook/react';
import _ from 'lodash';

import TreePicker from './index';
import SvgSymbol from '../SvgSymbol';
import Search from '../Search';

const meta = {
title: 'Pending Review/TreePicker',
title: 'Components/TreePicker',
component: TreePicker,
tags: ['autodocs'],
} satisfies Meta<typeof TreePicker>;

export default meta;

type Story = StoryObj<typeof meta>;

const DefaultComponent = () => {
const [selectedSearchValue, setSelectedSearchValue] = React.useState('');
const [selectedNodes, setSelectedNodes] = React.useState([]);
const [pickerSearchValue, setPickerSearchValue] = React.useState('');
const subTree = [
{
id: '0',
label: 'Northern Territory',
path: [{ id: '10', label: 'Australia' }],
type: 'Territory',
},
{
id: '1',
label: 'Australian Capital Territory',
path: [{ id: '10', label: 'Australia' }],
type: 'Territory',
},
{
id: '2',
label: 'Victoria',
path: [{ id: '10', label: 'Australia' }],
type: 'State',
},
];

const getSelectedNodes = () => {
if (_.isEmpty(selectedSearchValue)) return selectedNodes;

return _.filter(selectedNodes, ({ label }) => _.includes(label.toLowerCase(), selectedSearchValue.toLowerCase()));
};

const getSubtree = () => {
let treePickerPureSubtree = [];

// filter out nodes that do not contain search string
if (!_.isEmpty(pickerSearchValue)) {
treePickerPureSubtree = _.filter(subTree, ({ label }) =>
_.includes(label.toLowerCase(), pickerSearchValue.toLowerCase())
);
}
const NodeRender = ({ node }) => (
<TreePicker.Node node={node}>
<TreePicker.Node.Content>{node.label}</TreePicker.Node.Content>
{node.type === 'comment' ? null : (
<TreePicker.Node.Expand
className="expand"
disabled={node.type === 'test'}
resolveNodes={async () => {
if (node.type === 'user') {
const res = await axios.get(`https://dummyjson.com/users/${node.id}/posts?limit=5`);
return res.data.posts.map((entry) => ({
...entry,
label: entry.title,
type: 'post',
}));
}
const res = await axios.get(`https://dummyjson.com/posts/${node.id}/comments?limit=5`);

// filter out nodes that do not contain the selected search string
// however keep the nodes that are not selected but do not contain the selected search string
if (!_.isEmpty(selectedSearchValue)) {
treePickerPureSubtree = _.filter(treePickerPureSubtree, ({ id, label }) => {
if (_.find(selectedNodes, { id })) {
return _.includes(label.toLowerCase(), selectedSearchValue.toLowerCase());
}
return res.data.comments.map((entry) => ({
...entry,
label: entry.body,
type: 'comment',
}));
}}
/>
)}
<TreePicker.Node.Add
disabled={node.type !== 'comment'}
onAdd={() => {
// eslint-disable-next-line no-console
console.log(`Added ${node.id}: ${node.label}`);
}}
/>
</TreePicker.Node>
);

return true;
});
}
export const Default: Story = {
args: {
renderNode: (node) => <NodeRender node={node} />,

return treePickerPureSubtree;
};
children: (
<>
<TreePicker.Nav />
<TreePicker.Header>Name</TreePicker.Header>
<TreePicker.Search
resolveNodes={async (searchText) => {
const res = await axios.get(`https://dummyjson.com/posts/search?q=${searchText}`);

return (
<div style={{ width: 900 }}>
<TreePicker
itemType={'segment value'}
hideIcon
selectedNodes={getSelectedNodes()}
subtree={getSubtree()}
emptySvgSymbol={
<SvgSymbol classSuffixes={['gray-darker', '70']} href="/svg-symbols.svg#checklist-incomplete" isCircle />
}
emptySelectedListText={
<div>
<b>Choose items of interest</b>
</div>
}
initialStateNode={
<div>
<b>Start by searching for items</b>
</div>
}
searchValue={pickerSearchValue}
onChange={setPickerSearchValue}
searchOnClear={() => setPickerSearchValue('')}
includeNode={(node) => setSelectedNodes(_.concat([], selectedNodes, node))}
removeNode={(node) => setSelectedNodes(_.reject(selectedNodes, { id: node.id }))}
additionalClassNames={pickerSearchValue ? undefined : ['background-highlighted', 'test-class']}
selectedTopSearch={
<div className="selected-search">
<Search onSearch={setSelectedSearchValue} />
</div>
}
/>
return res.data.posts.map((post) => ({
...post,
label: post.title,
type: 'post',
}));
}}
/>
<TreePicker.Tree
resolveRootNodes={async () => {
const res = await axios.get('https://dummyjson.com/users?limit=5');
return res.data.users.map((entry) => ({
...entry,
label: entry.firstName + ' ' + entry.lastName,
type: 'user',
}));
}}
/>
</>
),
},
render: (args) => (
<div style={{ width: 500 }}>
<TreePicker {...args} />
</div>
);
};

export const Default: Story = {
args: {},
render: () => DefaultComponent(),
),
};
Loading

0 comments on commit 283291d

Please sign in to comment.