diff --git a/package-lock.json b/package-lock.json index 8d6d7a90a..8b20f6d28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,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" }, "devDependencies": { @@ -26907,6 +26908,21 @@ "tslib": "^2.1.0" } }, + "node_modules/ryze": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ryze/-/ryze-0.1.1.tgz", + "integrity": "sha512-e6wgwf1U+Ho5YqPSRtSH3jwChWhBGFxgcjevLfVsSnjoYtETcU7QRXfYPtcbJ4+J6qbEP5TXZO7NQBu/97z/Rg==", + "dependencies": { + "memoize-one": "^6.0.0" + }, + "engines": { + "node": "^v18.17.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -50513,6 +50529,14 @@ "tslib": "^2.1.0" } }, + "ryze": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ryze/-/ryze-0.1.1.tgz", + "integrity": "sha512-e6wgwf1U+Ho5YqPSRtSH3jwChWhBGFxgcjevLfVsSnjoYtETcU7QRXfYPtcbJ4+J6qbEP5TXZO7NQBu/97z/Rg==", + "requires": { + "memoize-one": "^6.0.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", diff --git a/package.json b/package.json index 9f8d31f3a..590f64716 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/components/ListPickerPure/index.jsx b/src/components/ListPickerPure/index.jsx index 6275cdb12..6beb2a550 100644 --- a/src/components/ListPickerPure/index.jsx +++ b/src/components/ListPickerPure/index.jsx @@ -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 = ({ diff --git a/src/components/RadioGroup/index.jsx b/src/components/RadioGroup/index.jsx index c47479da5..379fe41c8 100644 --- a/src/components/RadioGroup/index.jsx +++ b/src/components/RadioGroup/index.jsx @@ -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'; diff --git a/src/components/TreePicker/Grid/index.d.ts b/src/components/TreePicker/Grid/index.d.ts deleted file mode 100644 index b73f054e9..000000000 --- a/src/components/TreePicker/Grid/index.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react'; - -export interface TreePickerGridNodes { - id?: any; - label: string; - isExpandable?: boolean; - path?: { - id?: any; - label: string; - }[]; - ancestors?: { - id?: any; - label: string; - }[]; - type: string; - value?: number; - accent?: 'warning' | 'success' | 'info' | 'error'; -} - -export interface TreePickerGridProps { - disabled?: boolean; - emptySvgSymbol?: React.ReactNode; - emptyText: React.ReactNode; - expandNode?: (...args: any[]) => any; - groupFormatter?: (...args: any[]) => any; - hideIcon?: boolean; - includeNode?: (...args: any[]) => any; - itemType: string; - isLoading?: boolean; - nodes?: TreePickerGridNodes[]; - nodeRenderer?: (...args: any[]) => any; - removeNode?: (...args: any[]) => any; - selected: boolean; - valueFormatter?: (...args: any[]) => any; - displayGroupHeader?: boolean; - addNodePopoverInfoProps?: Object; - removeNodePopoverInfoProps?: Object; -} - -declare const TreePickerGrid: React.FC; - -export default TreePickerGrid; diff --git a/src/components/TreePicker/Grid/index.jsx b/src/components/TreePicker/Grid/index.jsx deleted file mode 100644 index c5fd6eed6..000000000 --- a/src/components/TreePicker/Grid/index.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import PropTypes from 'prop-types'; -import TreePickerNode from '../Node'; -import Empty from '../../Empty'; -import Grid from '../../Grid'; -import GridRow from '../../Grid/Row'; -import Spinner from '../../Spinner'; -import { TreePickerPropTypesNode } from '../../../prop-types/TreePickerPropTypes'; -import './styles.css'; - -const TreePickerGrid = ({ - disabled, - emptySvgSymbol, - expandNode, - groupFormatter, - hideIcon, - includeNode, - itemType, - isLoading, - nodes, - nodeRenderer, - removeNode, - selected, - valueFormatter, - emptyText, - displayGroupHeader, - addNodePopoverInfoProps, - removeNodePopoverInfoProps, -}) => { - const nodesByGroupLabel = _.groupBy(nodes, groupFormatter); - const emptySvgIcon = hideIcon ? null : emptySvgSymbol; - - return ( - - {isLoading ? ( -
- -

Loading…

-
- ) : ( - _.map(nodesByGroupLabel, (groupedNodes, label) => ( -
- {displayGroupHeader ? ( -
- {label} -
- ) : null} - {_.map(groupedNodes, (node) => ( - - ))} -
- )) - )} - {nodes && !isLoading ? : null} -
- ); -}; - -TreePickerGrid.propTypes = { - disabled: PropTypes.bool, - emptySvgSymbol: PropTypes.node, - emptyText: PropTypes.node.isRequired, - expandNode: PropTypes.func, - groupFormatter: PropTypes.func, - hideIcon: PropTypes.bool, - includeNode: PropTypes.func, - itemType: PropTypes.string.isRequired, - isLoading: PropTypes.bool, - nodes: PropTypes.arrayOf(TreePickerPropTypesNode), - nodeRenderer: PropTypes.func, - removeNode: PropTypes.func, - selected: PropTypes.bool.isRequired, - valueFormatter: PropTypes.func, - displayGroupHeader: PropTypes.bool, - addNodePopoverInfoProps: PropTypes.object, - removeNodePopoverInfoProps: PropTypes.object, -}; - -TreePickerGrid.defaultProps = { - disabled: false, - displayGroupHeader: true, - groupFormatter: () => 'Default Group', - hideIcon: false, - isLoading: false, -}; - -export default TreePickerGrid; diff --git a/src/components/TreePicker/Grid/index.spec.jsx b/src/components/TreePicker/Grid/index.spec.jsx deleted file mode 100644 index b5356b643..000000000 --- a/src/components/TreePicker/Grid/index.spec.jsx +++ /dev/null @@ -1,176 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import { render, screen } from 'testing'; -import TreePickerGrid from '.'; -import TreePickerMocks from '../mocks'; - -const { itemType, qldNode, saNode, nodeRenderer, valueFormatter } = TreePickerMocks; -const svgSymbol =
; - -it('should render with props', () => { - const props = { - emptySvgSymbol: svgSymbol, - emptyText: 'Empty!', - expandNode: jest.fn(), - includeNode: jest.fn(), - itemType, - nodes: [qldNode, saNode], - nodeRenderer, - removeNode: jest.fn(), - selected: false, - valueFormatter, - displayGroupHeader: true, - isLoading: false, - }; - - render(); - expect(screen.getByTestId('grid-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('grid-wrapper').children).toHaveLength(2); - - expect(screen.getByTestId('treepicker-grid-node-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('treepicker-grid-node-wrapper')).toHaveClass('treepickergrid-component-group'); - expect(screen.getByTestId('treepicker-grid-node-wrapper').children).toHaveLength(3); - - expect(screen.getByTestId('grid-wrapper')).toContainElement(screen.getByTestId('treepicker-grid-node-wrapper')); - expect(screen.getByTestId('treepicker-grid-node-wrapper')).toHaveClass('treepickergrid-component-group'); - - expect(screen.getByTestId('empty-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('grid-wrapper')).toContainElement(screen.getByTestId('empty-wrapper')); -}); - -describe('should render with groups', () => { - let props = {}; - - beforeEach(() => { - props = { - emptySvgSymbol: svgSymbol, - emptyText: 'Empty!', - expandNode: jest.fn(), - groupFormatter: (node) => node.id, - includeNode: jest.fn(), - itemType, - nodes: [qldNode, saNode], - nodeRenderer, - removeNode: jest.fn(), - selected: false, - valueFormatter, - isLoading: false, - }; - }); - - it('should render with groups by default', () => { - render(); - expect(screen.getByTestId('grid-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('grid-wrapper').children).toHaveLength(3); - expect(screen.getAllByTestId('treepicker-grid-node-wrapper')).toHaveLength(2); - screen - .getAllByTestId('treepicker-grid-node-wrapper') - .forEach((group) => expect(group).toHaveClass('treepickergrid-component-group')); - - _.forEach(props.nodes, (node, index) => { - const currentNode = screen.getAllByTestId('treepicker-grid-node-wrapper')[index]; - const groupLabel = screen.getByDts(`group-label-${node.id}`); - - expect(currentNode).toContainElement(groupLabel); - expect(groupLabel).toHaveTextContent(node.id); - }); - }); - - it('should render with no group header when displayGroupHeader is false', () => { - props = _.assign({}, props, { - nodes: [qldNode], - displayGroupHeader: false, - }); - render(); - expect(screen.getByTestId('grid-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('grid-wrapper').children).toHaveLength(2); - expect(screen.getByTestId('treepicker-grid-node-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('treepicker-grid-node-wrapper')).toHaveClass('treepickergrid-component-group'); - expect(screen.getByTestId('treepicker-grid-node-wrapper').children).toHaveLength(1); - }); -}); - -it('should display empty when there is no valid group', () => { - const props = { - emptySvgSymbol: svgSymbol, - emptyText: 'Empty!', - expandNode: jest.fn(), - groupFormatter: (node) => node.randomAttr, - includeNode: jest.fn(), - itemType, - nodes: [qldNode], - nodeRenderer, - removeNode: jest.fn(), - selected: false, - valueFormatter, - isLoading: false, - }; - render(); - expect(screen.getByTestId('empty-wrapper')).toBeInTheDocument(); -}); - -it('should not display empty with an undefined nodes list', () => { - const props = { - emptySvgSymbol: svgSymbol, - emptyText: 'Empty!', - expandNode: jest.fn(), - includeNode: jest.fn(), - itemType, - nodes: undefined, - removeNode: jest.fn(), - selected: false, - valueFormatter, - isLoading: false, - }; - render(); - expect(screen.queryByTestId('empty-wrapper')).not.toBeInTheDocument(); -}); - -it('should display a loading state instead of empty state when isLoading is set to true', () => { - const props = { - selected: false, - itemType: 'test', - emptyText: 'nothing here', - isLoading: true, - }; - render(); - expect(screen.getByTestId('spinner-wrapper')).toBeInTheDocument(); -}); - -it('should display a loading state instead of nodes when isLoading is set to true', () => { - const props = { - selected: false, - itemType: 'test', - emptyText: 'nothing here', - nodes: [qldNode], - }; - - render(); - expect(screen.getByTestId('spinner-wrapper')).toBeInTheDocument(); -}); - -it('should hide the emptySvgSymbol of the tree picker grid as expected', () => { - const props = { - emptySvgSymbol: svgSymbol, - emptyText: 'Empty!', - nodes: [], - expandNode: jest.fn(), - includeNode: jest.fn(), - itemType, - nodeRenderer, - removeNode: jest.fn(), - selected: false, - valueFormatter, - displayGroupHeader: true, - isLoading: false, - }; - - const view = render(); - expect(screen.getByTestId('empty-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('testing-svg-symbol')).toBeInTheDocument(); - expect(screen.getByTestId('empty-wrapper')).toContainElement(screen.getByTestId('testing-svg-symbol')); - - view.rerender(); - - expect(screen.queryByTestId('testing-svg-symbol')).not.toBeInTheDocument(); -}); diff --git a/src/components/TreePicker/Grid/styles.css b/src/components/TreePicker/Grid/styles.css deleted file mode 100644 index 5a80c6784..000000000 --- a/src/components/TreePicker/Grid/styles.css +++ /dev/null @@ -1,7 +0,0 @@ -@import url('../../../styles/variable.css'); - -.treepickergrid-component-group-label .grid-component-row { - background-color: $color-grey-100; - font-weight: $font-weight-bold; - padding: 7px 10px; -} diff --git a/src/components/TreePicker/Nav/index.d.ts b/src/components/TreePicker/Nav/index.d.ts deleted file mode 100644 index 94327bdb5..000000000 --- a/src/components/TreePicker/Nav/index.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; - -export interface TreePickerNavBreadcrumbNodes { - id?: any; - label: string; -} - -export interface TreePickerNavBreadcrumbRootNode { - id?: any; - label: string; -} - -export interface TreePickerNavProps { - breadcrumbNodes?: TreePickerNavBreadcrumbNodes[]; - breadcrumbOnClick?: (...args: any[]) => any; - disabled?: boolean; - isLoading?: boolean; - onChange?: (...args: any[]) => any; - onClear?: (...args: any[]) => any; - onSearch?: (...args: any[]) => any; - debounceInterval?: number; - searchOnEnter?: boolean; - searchPlaceholder?: string; - searchValue?: string; - showSearch?: boolean; - svgSymbolCancel?: React.ReactNode; - svgSymbolSearch?: React.ReactNode; - breadcrumbRootNode?: TreePickerNavBreadcrumbRootNode; -} - -declare const TreePickerNav: React.FC; - -export default TreePickerNav; diff --git a/src/components/TreePicker/Nav/index.jsx b/src/components/TreePicker/Nav/index.jsx deleted file mode 100644 index b456dbac0..000000000 --- a/src/components/TreePicker/Nav/index.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import _ from 'lodash'; -import classnames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import Search from '../../Search'; -import Breadcrumb from '../../Breadcrumb'; -import { TreePickerPropTypesBreadCrumbNode } from '../../../prop-types/TreePickerPropTypes'; -import './styles.css'; - -const TreePickerNav = ({ - breadcrumbRootNode, - breadcrumbNodes, - breadcrumbOnClick, - debounceInterval, - disabled, - isLoading, - onClear, - onChange, - onSearch, - searchOnEnter, - searchPlaceholder, - searchValue, - showSearch, - svgSymbolCancel, - svgSymbolSearch, -}) => { - const icons = {}; - if (svgSymbolSearch) icons.search = svgSymbolSearch; - if (svgSymbolCancel) icons.close = svgSymbolCancel; - - const breadcrumbProps = { - disabled, - nodes: breadcrumbNodes, - onClick: breadcrumbOnClick, - rootNode: breadcrumbRootNode, - }; - const className = classnames('treepickernav-component', { disabled }); - - return ( -
- {showSearch && ( - - )} - -
- ); -}; - -TreePickerNav.propTypes = { - breadcrumbRootNode: TreePickerPropTypesBreadCrumbNode, - breadcrumbNodes: PropTypes.arrayOf(TreePickerPropTypesBreadCrumbNode), - breadcrumbOnClick: PropTypes.func, - disabled: PropTypes.bool, - isLoading: PropTypes.bool, - onChange: PropTypes.func, - onClear: PropTypes.func, - onSearch: PropTypes.func, - debounceInterval: PropTypes.number, - searchOnEnter: PropTypes.bool, - searchPlaceholder: PropTypes.string, - searchValue: PropTypes.string, - showSearch: PropTypes.bool, - svgSymbolCancel: PropTypes.node, - svgSymbolSearch: PropTypes.node, -}; - -TreePickerNav.defaultProps = { - debounceInterval: 0, - disabled: false, - isLoading: false, - searchOnEnter: false, - searchPlaceholder: '', - showSearch: true, - onSearch: _.noop, -}; - -export default TreePickerNav; diff --git a/src/components/TreePicker/Nav/index.spec.jsx b/src/components/TreePicker/Nav/index.spec.jsx deleted file mode 100644 index 3efa56deb..000000000 --- a/src/components/TreePicker/Nav/index.spec.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import { render, screen, user } from 'testing'; -import TreePickerNav from '.'; - -const breadcrumbNodes = [ - { id: 'a', label: 'UK' }, - { id: 'b', label: 'London' }, -]; - -const mockProps = (overrides) => ({ - breadcrumbNodes, - breadcrumbOnClick: jest.fn(), - onChange: jest.fn(), - onClear: jest.fn(), - searchValue: 'needle', - disabled: false, - ...overrides, -}); - -it('should render with defaults', () => { - render(); - - expect(screen.getAllByClass('treepickernav-component')).toHaveLength(1); - expect(screen.getByClass('treepickernav-component')).not.toHaveClass('disabled'); - - expect(screen.getByClass('treepickernav-component')).toContainElement(screen.getByTestId('search-wrapper')); - expect(screen.getByTestId('search-wrapper')).toBeInTheDocument(); - - expect(screen.getByClass('treepickernav-component')).toContainElement(screen.getByTestId('breadcrumb-wrapper')); - expect(screen.getByTestId('breadcrumb-wrapper')).toBeInTheDocument(); -}); - -it('should render with props', () => { - render(); - - expect(screen.getAllByClass('treepickernav-component')).toHaveLength(1); - expect(screen.getByClass('treepickernav-component')).not.toHaveClass('disabled'); - - expect(screen.getByClass('treepickernav-component')).toContainElement(screen.getByTestId('search-wrapper')); - expect(screen.getByTestId('search-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('search-input')).toHaveValue('needle'); - - expect(screen.getByClass('treepickernav-component')).toContainElement(screen.getByTestId('breadcrumb-wrapper')); - expect(screen.getByTestId('breadcrumb-wrapper')).toBeInTheDocument(); - expect(screen.queryAllByTestId('breadcrumb-node-wrapper')).toHaveLength(3); -}); - -it('should render icons with given svgSymbol and pass them to Search', () => { - render( - } - svgSymbolCancel={
} - /> - ); - - expect(screen.getAllByClass('testing-search-icon')).toHaveLength(1); - expect(screen.getAllByClass('testing-cancel-icon')).toHaveLength(1); -}); - -it('should call breadcrumbOnClick when clicked on breadcrumbs node', async () => { - const props = mockProps(); - render(); - - await user.click(screen.queryAllByTestId('breadcrumb-node-wrapper')[0]); - expect(props.breadcrumbOnClick).toHaveBeenCalledTimes(1); -}); - -it('should hide the search when showSearch is false', () => { - render(); - expect(screen.queryByTestId('search-wrapper')).not.toBeInTheDocument(); -}); - -describe('disabled', () => { - it('should have disabled class', () => { - render(); - expect(screen.getByTestId('treepicker-nav-wrapper')).toHaveClass('disabled'); - }); - - it('should render breadcrumbs', () => { - render(); - expect(screen.getByTestId('breadcrumb-wrapper')).toBeInTheDocument(); - }); - - it('should render breadcrumbs node', () => { - render(); - expect(screen.getAllByTestId('breadcrumb-node-wrapper')).toHaveLength(3); - }); - - it('should not call breadcrumbOnClick when clicked on breadcrumbs node', async () => { - const props = mockProps({ disabled: true }); - render(); - await user.click(screen.getAllByTestId('breadcrumb-node-wrapper')[0]); - expect(props.breadcrumbOnClick).toHaveBeenCalledTimes(0); - }); -}); diff --git a/src/components/TreePicker/Nav/styles.css b/src/components/TreePicker/Nav/styles.css deleted file mode 100644 index 149f4b6b3..000000000 --- a/src/components/TreePicker/Nav/styles.css +++ /dev/null @@ -1,15 +0,0 @@ -@import url('../../../styles/variable.css'); - -.treepickernav-component { - border-left: 1px solid $color-border-base; - border-right: 1px solid $color-border-base; - padding: 10px; -} - -.treepickernav-component > div + div { - margin-top: 10px; -} - -.treepickernav-component > .aui--breadcrumb { - padding-left: 2px; -} diff --git a/src/components/TreePicker/Node/Expander/index.d.ts b/src/components/TreePicker/Node/Expander/index.d.ts deleted file mode 100644 index 3fd017f5c..000000000 --- a/src/components/TreePicker/Node/Expander/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; - -export interface TreePickerNodeExpanderProps { - isLoading?: boolean; - onClick: (...args: any[]) => any; -} - -declare const TreePickerNodeExpander: React.FC; - -export default TreePickerNodeExpander; diff --git a/src/components/TreePicker/Node/Expander/index.jsx b/src/components/TreePicker/Node/Expander/index.jsx deleted file mode 100644 index c4e264da6..000000000 --- a/src/components/TreePicker/Node/Expander/index.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import GridCell from '../../../Grid/Cell'; -import Spinner from '../../../Spinner'; - -const TreePickerNodeExpander = ({ isLoading, onClick }) => { - const props = { - dts: 'expander', - onClick: isLoading ? null : onClick, - }; - - return ( - - {isLoading ? :
} - - ); -}; - -TreePickerNodeExpander.propTypes = { - isLoading: PropTypes.bool, - onClick: PropTypes.func.isRequired, -}; - -TreePickerNodeExpander.defaultProps = { - isLoading: false, -}; - -export default TreePickerNodeExpander; diff --git a/src/components/TreePicker/Node/Expander/index.spec.jsx b/src/components/TreePicker/Node/Expander/index.spec.jsx deleted file mode 100644 index 3102e772d..000000000 --- a/src/components/TreePicker/Node/Expander/index.spec.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { render, screen, user } from 'testing'; -import TreePickerNodeExpander from '.'; - -it('should render with default isLoading false', async () => { - const onClickMock = jest.fn(); - render(); - - expect(screen.getByTestId('grid-cell-wrapper')).toBeInTheDocument(); - expect(screen.getByTestId('grid-cell-wrapper')).toHaveAttribute('data-test-selector', 'expander'); - expect(screen.queryByTestId('spinner-wrapper')).not.toBeInTheDocument(); - - await user.click(screen.getByTestId('grid-cell-wrapper')); - expect(onClickMock).toHaveBeenCalledTimes(1); -}); - -it('should render with isLoading true', async () => { - const onClickMock = jest.fn(); - render(); - - expect(screen.getByTestId('grid-cell-wrapper')).not.toBeEmptyDOMElement(); - expect(screen.getByTestId('spinner-wrapper')).toBeInTheDocument(); - await user.click(screen.getByTestId('grid-cell-wrapper')); - expect(onClickMock).toHaveBeenCalledTimes(0); -}); diff --git a/src/components/TreePicker/Node/index.d.ts b/src/components/TreePicker/Node/index.d.ts deleted file mode 100644 index 397a09bee..000000000 --- a/src/components/TreePicker/Node/index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react'; - -export interface TreePickerNodeProps { - disabled?: boolean; - expandNode?: (...args: any[]) => any; - includeNode?: (...args: any[]) => any; - itemType: string; - node?: any; - nodeRenderer?: (...args: any[]) => any; - removeNode?: (...args: any[]) => any; - selected?: boolean; - valueFormatter?: (...args: any[]) => any; - addNodePopoverInfoProps?: Object; - removeNodePopoverInfoProps?: Object; -} - -export default class TreePickerNode extends React.Component { - render(): JSX.Element; -} diff --git a/src/components/TreePicker/Node/index.jsx b/src/components/TreePicker/Node/index.jsx deleted file mode 100644 index c9948ba6f..000000000 --- a/src/components/TreePicker/Node/index.jsx +++ /dev/null @@ -1,170 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import React from 'react'; -import Button from '../../Button'; -import GridCell from '../../Grid/Cell'; -import GridRow from '../../Grid/Row'; -import TextEllipsis from '../../TextEllipsis'; -import TreePickerNodeExpander from './Expander'; -import Popover from '../../Popover'; -import { TreePickerPropTypesNode } from '../../../prop-types/TreePickerPropTypes'; -import invariant from '../../../invariant'; -import './styles.css'; - -const baseClass = 'treepickernode-component'; - -const printPathText = (node) => _(node.path).map('label').clone().reverse().join(', '); - -const printAncestorText = (node) => _(node.ancestors).map('label').join(', '); - -const pathPrefix = ({ type }) => (_.isEmpty(type) ? '' : `${type} in `); - -const ConditionalPopoverWrapper = ({ condition, wrapper, children }) => (condition ? wrapper(children) : children); - -class TreePickerNode extends React.PureComponent { - state = { - isLoading: false, - }; - - componentDidMount() { - invariant( - !(_.isUndefined(this.props.node.path) && _.isUndefined(this.props.node.ancestors)), - `TreePickerNode needs property 'path' or property 'ancestors' for ${this.props.node.id}` - ); - } - - setLoadingAndExpandNode = () => { - this.setState({ isLoading: true }, () => this.props.expandNode(this.props.node)); - }; - - handleRemove = () => this.props.removeNode(this.props.node); - - handleInclude = () => this.props.includeNode(this.props.node); - - render() { - const { - disabled, - itemType, - node, - expandNode, - nodeRenderer, - selected, - valueFormatter, - addNodePopoverInfoProps, - removeNodePopoverInfoProps, - } = this.props; - const isChildNode = !(_.isEmpty(node.path) && _.isEmpty(node.ancestors)); - const isExpandable = expandNode && node.isExpandable; - - const pathElement = isChildNode ? ( - - {_.isEmpty(node.path) ? printAncestorText(node) : printPathText(node)} - - ) : null; - - const labelCellProps = isExpandable && !this.state.isLoading ? { onClick: this.setLoadingAndExpandNode } : {}; - - const classNames = classnames(baseClass, { - 'child-node': isChildNode, - [`is-${node.accent}`]: node.accent, - }); - - return ( -
- - {selected ? ( - - {children}} - > - - - - ) : null} - - - {nodeRenderer(node)} - {!_.isEmpty(pathElement) ? ( - - ({pathPrefix(node)} - {pathElement}) - - ) : null} - - - - {isExpandable ? ( - - ) : null} - - {_.isNumber(node.value) ? {valueFormatter(node.value)} : null} - {!selected ? ( - - {children}} - > - - - - ) : null} - -
- ); - } -} - -TreePickerNode.propTypes = { - disabled: PropTypes.bool, - expandNode: PropTypes.func, - includeNode: PropTypes.func, - itemType: PropTypes.string.isRequired, - node: TreePickerPropTypesNode.isRequired, - nodeRenderer: PropTypes.func, - removeNode: PropTypes.func, - selected: PropTypes.bool, - valueFormatter: PropTypes.func, - addNodePopoverInfoProps: PropTypes.object, - removeNodePopoverInfoProps: PropTypes.object, -}; - -TreePickerNode.defaultProps = { - disabled: false, - includeNode: (node) => { - console.error(`AdslotUi TreePickerNode needs an includeNode handler for ${node.id}`); - }, - removeNode: (node) => { - console.error(`AdslotUi TreePickerNode needs a removeNode handler for ${node.id}`); - }, - selected: false, - valueFormatter: (value) => value, - nodeRenderer: (node) => node.label, -}; - -export default TreePickerNode; diff --git a/src/components/TreePicker/Node/index.spec.jsx b/src/components/TreePicker/Node/index.spec.jsx deleted file mode 100644 index 4dd2e6263..000000000 --- a/src/components/TreePicker/Node/index.spec.jsx +++ /dev/null @@ -1,350 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import { render, screen, user } from 'testing'; -import TreePickerNode from '.'; -import invariant from '../../../invariant'; -import TreePickerMocks from '../mocks'; - -jest.mock('../../../invariant'); - -const { cbrNode, cbrNodeAlreadySelected, actNode, maleNode, itemType, nodeRenderer } = TreePickerMocks; - -it('should render a node with defaults', () => { - render(); - - expect(screen.getByTestId('treepicker-node-wrapper')).toHaveClass('treepickernode-component child-node'); - - expect(screen.getByTestId('grid-row-wrapper')).toHaveAttribute( - 'data-test-selector', - `${_.kebabCase(itemType)}-${cbrNode.id}` - ); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); - expect(screen.getAllByTestId('grid-cell-wrapper')[2]).toContainElement(screen.getByText('+')); - expect(screen.getByText('+').parentElement.tagName).toBe('BUTTON'); - expect(screen.getByText('+')).toBeInTheDocument(); - - expect(screen.getAllByTestId('grid-cell-wrapper')[0]).toContainElement(screen.getByTestId('text-ellipsis')); - expect(screen.getByTestId('text-ellipsis')).toBeInTheDocument(); - expect(screen.getByTestId('text-ellipsis')).toHaveTextContent('Canberra'); - - expect(screen.getByClass('treepickernode-component-metadata')).toBeInTheDocument(); - expect(screen.getByClass('treepickernode-component-metadata')).toHaveTextContent('(City in ACT, AU)'); - expect(screen.getByText('ACT, AU')).toHaveClass('treepickernode-component-path'); - - expect(screen.getByText('2000')).toHaveClass('grid-component-cell'); - expect(screen.getAllByTestId('grid-cell-wrapper')[2]).toHaveClass('grid-component-cell-button'); -}); - -it('should render metadata of nodes already selected containing ancestory data', () => { - render( - - ); - - expect(screen.getAllByClass('treepickernode-component-metadata')).toHaveLength(1); - expect(screen.getByClass('treepickernode-component-metadata')).toHaveTextContent('(City in ACT, AU)'); - expect(screen.getByText('ACT, AU')).toHaveClass('treepickernode-component-path'); -}); - -it('should render node via nodeRenderer', () => { - render( - - ); - - expect(screen.getByTestId('text-ellipsis')).toContainElement( - screen.getByText('Test value: Australian Capital Territory') - ); -}); - -it('should have correct accent color', () => { - const view = render( - - ); - - expect(screen.getByTestId('treepicker-node-wrapper')).toHaveClass('is-error'); - view.rerender( - - ); - expect(screen.getByTestId('treepicker-node-wrapper')).toHaveClass('is-warning'); -}); - -it('should render unselectable nodes with an include button', () => { - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // meta data cell and value cell - expect(screen.getByText('+').parentElement.tagName).toBe('BUTTON'); - expect(screen.getByText('+')).toBeInTheDocument(); -}); - -it('should render the button first when selected is true', () => { - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // remove button cell, meta data cell and value cell - expect(screen.getAllByTestId('grid-cell-wrapper')[0]).toHaveAttribute('data-test-selector', 'button-remove'); - expect(screen.getByText('−').parentElement.tagName).toBe('BUTTON'); - expect(screen.getByText('−')).toBeInTheDocument(); -}); - -it('should render button as disabled when disabled is true', async () => { - const testFunction = jest.fn(); - render( - - ); - expect(screen.getByText('−').closest('button')).toBeDisabled(); - - await user.click(screen.getByText('−')); - expect(testFunction).toHaveBeenCalledTimes(0); -}); - -it('should filter value when provided', () => { - const valueFormatter = (value) => `€${value / 100}`; - render( - - ); - - expect(screen.getAllByTestId('grid-cell-wrapper')[1]).toHaveTextContent('€20'); -}); - -it('should hide value when no value Number', () => { - const node = _.clone(cbrNode); - delete node.value; - const props = { itemType, node }; - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(2); - expect(screen.getAllByTestId('grid-cell-wrapper')[0]).toHaveTextContent('Canberra(City in ACT, AU)'); - expect(screen.getAllByTestId('grid-cell-wrapper')[1]).toHaveTextContent('+'); -}); - -it('should not have the child node class for root nodes', () => { - render(); - expect(screen.getByTestId('treepicker-node-wrapper')).toHaveClass('treepickernode-component'); -}); - -it('should have the child node class for child nodes', () => { - render(); - expect(screen.getByTestId('treepicker-node-wrapper')).toHaveClass('treepickernode-component child-node'); -}); - -it('should fire expandNode when clicking on the label cell', async () => { - const mockExpand = jest.fn(); - render( - - ); - - expect(screen.getAllByTestId('grid-cell-wrapper')[0]).toHaveAttribute('data-test-selector', 'label'); - - await user.click(screen.getAllByTestId('grid-cell-wrapper')[0]); - expect(mockExpand).toHaveBeenCalledTimes(1); -}); - -it('should not show the expander element when the node is not expandable', async () => { - const props = { - mockExpand: jest.fn(), - node: _.defaults({ isExpandable: false }, cbrNode), - itemType, - }; - - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // meta data cell, value cell and include button cell - - await user.click(screen.getAllByTestId('grid-cell-wrapper')[0]); - expect(props.mockExpand).toHaveBeenCalledTimes(0); -}); - -it('should not show the expander element when the node is expandable and no expandNode is given', () => { - const props = { - node: _.defaults({ isExpandable: true }, cbrNode), - itemType, - }; - - render(); - expect(screen.queryByDts('expander')).not.toBeInTheDocument(); -}); - -it('should fire includeNode when clicking on the `include` button', async () => { - const nodes = []; - const includeNode = (node) => nodes.push(node); - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // meta data cell, value cell and include button cell - await user.click(screen.getByText('+')); - expect(nodes).toEqual([cbrNode]); -}); - -it('should error on click of `include` button without includeNode handler', async () => { - jest.spyOn(console, 'error').mockReturnValue(); - render(); - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // meta data cell, value cell and include button cell - - await user.click(screen.getByText('+')); - expect(console.error).toHaveBeenCalledWith('AdslotUi TreePickerNode needs an includeNode handler for au-act-cbr'); -}); - -it('should render a provided node with an empty breadcrumb array', () => { - const node = { - id: '3', - label: 'Cameroon', - type: 'Country', - value: 400, - path: [], - }; - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // meta data cell, value cell and include button cell - - expect(screen.getAllByTestId('grid-cell-wrapper')[0]).toContainElement(screen.getByTestId('text-ellipsis')); - expect(screen.getByTestId('text-ellipsis')).toHaveTextContent('Cameroon'); - - expect(screen.queryByClass('treepickernode-component-metadata')).not.toBeInTheDocument(); - - expect(screen.getAllByTestId('grid-cell-wrapper')[1]).toHaveTextContent('400'); -}); - -it('should render a provided node with an empty type', () => { - const node = { - id: '3', - label: 'Toyota', - type: '', - value: 400, - path: [{ id: '30', label: 'Cars' }], - }; - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // meta data cell, value cell and include button cell - expect(screen.getByTestId('text-ellipsis').children).toHaveLength(2); - expect(screen.getByTestId('text-ellipsis')).toHaveTextContent('Toyota(Cars)'); - expect(screen.getByClass('treepickernode-component-metadata')).toHaveTextContent('Cars'); - - expect(screen.getAllByTestId('grid-cell-wrapper')[1]).toHaveTextContent('400'); -}); - -it('should fire removeNode when clicking on the `remove` button', async () => { - const nodes = [cbrNode]; - const removeNode = (node) => _.remove(nodes, { id: node.id }); - const props = { itemType, node: cbrNode, removeNode, selected: true }; - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // remove button cell, meta data cell and value cell - expect(screen.getAllByTestId('grid-cell-wrapper')[0]).toContainElement(screen.getByText('−')); - expect(screen.getByText('−').parentElement.tagName).toBe('BUTTON'); - expect(screen.getByText('−')).toBeInTheDocument(); - - expect(nodes).toEqual([cbrNode]); - await user.click(screen.getByText('−')); - expect(nodes).toEqual([]); -}); - -it('should error on click of `remove` button without removeNode handler', async () => { - jest.spyOn(console, 'error').mockReturnValue(); - render(); - - expect(screen.getAllByTestId('grid-cell-wrapper')).toHaveLength(3); // remove button cell, meta data cell and value cell - - await user.click(screen.getByText('−')); - - expect(console.error).toHaveBeenCalledWith('AdslotUi TreePickerNode needs a removeNode handler for au-act-cbr'); -}); - -it('should accept both strings and numbers as node ids', () => { - const view = render( - - ); - - expect(screen.getByTestId('grid-row-wrapper')).toHaveAttribute('data-test-selector', 'example-item-type-au-act-cbr'); - - view.rerender(); - - expect(screen.getByTestId('grid-row-wrapper')).toHaveAttribute('data-test-selector', 'example-item-type-4'); -}); - -it('should throw error message when props does not contain `path` or `ancestors`', () => { - const nodeWithoutPathAndAncestors = _.omit(cbrNode, ['path', 'ancestors']); - render( - - ); - expect(invariant).toHaveBeenCalledWith( - false, - `TreePickerNode needs property 'path' or property 'ancestors' for au-act-cbr` - ); -}); - -it('should have popover element if addNodePopoverInfoProps exists', () => { - const addInfoProps = { - popoverContent: 'add', - }; - - render( - - ); - - expect(screen.getAllByTestId('popover-element')).toHaveLength(2); // text ellipsis and popover for add button -}); - -it('should have popover element if removeNodePopoverInfoProps exists', () => { - const removeInfoProps = { - popoverContent: 'remove', - }; - - render( - - ); - - expect(screen.getAllByTestId('popover-element')).toHaveLength(2); // text ellipsis and popover for remove button -}); diff --git a/src/components/TreePicker/Node/styles.css b/src/components/TreePicker/Node/styles.css deleted file mode 100644 index d961f7d89..000000000 --- a/src/components/TreePicker/Node/styles.css +++ /dev/null @@ -1,60 +0,0 @@ -@import url('../../../styles/variable.css'); - -.treepickernode-component:hover { - background-color: $color-grey-100; -} - -.treepickernode-component-expander { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%23AAAFB3' d='M12.5 4c.3 0 .5.2.5.5V6h1.8c.4.1.5.5.4.8l-1.8 6.4c-.1.3-.3.4-.5.4H2.5c-.3 0-.5-.2-.5-.5V4.5c0-.3.2-.5.5-.5h10z'/%3E%3Cpath fill='%23fff' d='M3 5v5.6l1.4-4.2c.1-.1.2-.4.6-.4h7V5H3z'/%3E%3C/svg%3E"); - height: 16px; - width: 16px; -} - -.treepickernode-component:hover .treepickernode-component-expander { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%23505359' d='M12.5 4c.3 0 .5.2.5.5v4h2.3c.4 0 .6.4.4.7l-2.3 4.1c-.1.3-.3.4-.5.4H2.5c-.3 0-.5-.2-.5-.5V4.5c0-.3.2-.5.5-.5h10z'/%3E%3Cpath fill='%23fff' d='M3 5v6.3l1.4-2.6c.1-.2.3-.2.4-.2H12V5H3z'/%3E%3C/svg%3E"); -} - -.treepickernode-component.is-error { - background-color: $color-danger-soft; -} - -.treepickernode-component.is-warning { - background-color: $color-warning-soft; -} - -.treepickernode-component.is-info { - background-color: $color-primary-soft; -} - -.treepickernode-component.is-success { - background-color: $color-success-soft; -} - -.treepickernode-component-metadata { - color: $color-grey-600; -} - -.treepickernode-component-metadata::before { - content: ' '; -} - -.treepickernode-component .grid-component-cell-button { - padding-bottom: 7px; - padding-top: 7px; -} - -.treepickernode-component .grid-component-cell-button .button-xs { - font-size: $font-size-medium; - min-height: 20px; - line-height: 14px; - width: 20px; - padding: 2px 4px; -} - -.treepickernode-component label { - margin-bottom: 0; -} - -.treepickernode-component .spinner-component > .spinner { - margin-bottom: -4px; -} diff --git a/src/components/TreePicker/TreePicker.css b/src/components/TreePicker/TreePicker.css new file mode 100644 index 000000000..cee16a557 --- /dev/null +++ b/src/components/TreePicker/TreePicker.css @@ -0,0 +1,32 @@ +@import url('../../styles/variable.css'); + +.tree-picker { + & .tree-picker-row + .tree-picker-tree { + border-top: 1px solid $color-border-base; + } +} + +.tree-picker-section { + margin-top: 16px; + margin-bottom: 16px; +} + +.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; + } +} + +.tree-picker-row-content { + flex: 1; +} diff --git a/src/components/TreePicker/TreePicker.jsx b/src/components/TreePicker/TreePicker.jsx new file mode 100644 index 000000000..7399e967f --- /dev/null +++ b/src/components/TreePicker/TreePicker.jsx @@ -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) => {renderNode(node, index)}, + [renderNode] + ); + + return ( + +
{children}
+
+ ); +}; + +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; diff --git a/src/components/TreePicker/TreePicker.spec.jsx b/src/components/TreePicker/TreePicker.spec.jsx new file mode 100644 index 000000000..9bcee03f0 --- /dev/null +++ b/src/components/TreePicker/TreePicker.spec.jsx @@ -0,0 +1,326 @@ +import _ from 'lodash'; +import React from 'react'; +import { render, screen, user, within, waitFor } from 'testing'; + +import TreePicker from '.'; + +const rootNodes = [ + { id: 'audience', label: 'Audience', type: 'root', header: 'Segment' }, + { id: 'site', label: 'Site', type: 'root', header: 'Segment' }, + { id: 'geo', label: 'Geo', type: 'root', header: 'Segment' }, +]; +const audSegmentNodes = [ + { id: '111', label: 'Fyllo', type: 'segment' }, + { id: '222', label: 'Acxiom', type: 'segment' }, +]; + +const audSegmentValuesNodes = [{ id: '1111', label: 'MRI', type: 'value' }]; + +it('should display empty state when there is no root nodes', async () => { + render( + ( + + + {node.label} ({node.id}) + + + )} + > + + Label + []} /> + + ); + + await waitFor(() => { + expect(screen.getByText('Label')).toBeInTheDocument(); + }); + + expect(screen.getByClass('tree-picker-empty')).toBeInTheDocument(); + expect(screen.getByClass('tree-picker-empty-title')).toHaveTextContent('No Results.'); +}); + +it('should display with root nodes', async () => { + render( + ( + + + {node.label} ({node.id}) + + + )} + > + + Label + rootNodes} /> + + ); + + await waitFor(() => { + expect(screen.getByText('Audience (audience)')).toBeInTheDocument(); + }); + + expect(screen.getByClass('tree-picker')).toBeInTheDocument(); + + const treePickerTree = screen.getByClass('tree-picker-tree'); + + const nodes = within(treePickerTree).getAllByClass('tree-picker-row-content'); + expect(nodes).toHaveLength(3); + + expect(_.map(nodes, (n) => n.textContent).sort()).toEqual(['Audience (audience)', 'Site (site)', 'Geo (geo)'].sort()); +}); + +it('should hide all hidden nodes', async () => { + render( + ( + + {node.label} + + )} + > + + Label + rootNodes} hiddenNodeIds={['geo', 'site']} /> + + ); + + await waitFor(() => { + expect(screen.getByText('Label')).toBeInTheDocument(); + }); + + expect(screen.getByText('Audience')).toBeInTheDocument(); + expect(screen.queryByText('Site')).not.toBeInTheDocument(); + expect(screen.queryByText('Geo')).not.toBeInTheDocument(); +}); + +it('should display nodes and header correctly when clicking expandable node', async () => { + render( + ( + + {node.label} + _.noop} /> + audSegmentNodes} /> + + )} + > + + Name + rootNodes} /> + + ); + + await waitFor(() => { + expect(screen.getByText('Audience')).toBeInTheDocument(); + }); + + let includeAllBtn = screen.queryByClass('tree-picker-header-include-all'); + let headerContent = screen.getByClass('tree-picker-header'); + let nodes = within(screen.getByClass('tree-picker-tree')).getAllByClass('tree-picker-row-content'); + + expect(nodes).toHaveLength(3); + expect(includeAllBtn).not.toBeInTheDocument(); + expect(headerContent).toHaveTextContent('Name'); + + const nodeExpands = screen.getAllByClass('tree-picker-node-expand'); + expect(nodeExpands).toHaveLength(3); + expect(nodeExpands[1]).toHaveClass('disabled'); + + await user.click(nodeExpands[1]); // disabled + includeAllBtn = screen.queryByClass('tree-picker-header-include-all'); + expect(within(screen.getByClass('tree-picker-tree')).getAllByClass('tree-picker-row-content')).toHaveLength(3); // still render root nodes + expect(includeAllBtn).not.toBeInTheDocument(); + + await user.click(nodeExpands[0]); + + nodes = within(screen.getByClass('tree-picker-tree')).getAllByClass('tree-picker-row-content'); + expect(nodes).toHaveLength(2); + expect(_.map(nodes, (n) => n.textContent).sort()).toEqual(['Fyllo', 'Acxiom'].sort()); + + headerContent = screen.getByClass('tree-picker-header'); + expect(headerContent).toHaveTextContent('Segment'); + + includeAllBtn = screen.getByClass('tree-picker-header-include-all'); + expect(includeAllBtn).toBeInTheDocument(); // include all button +}); + +it('should display nodes correctly when expanding and navigating', async () => { + render( + ( + + {node.label} + { + if (node.id === 'audience') return audSegmentNodes; + if (node.id === '111') return audSegmentValuesNodes; + if (node.id === '1111') return []; + }} + /> + + )} + > + + rootNodes} /> + + ); + + await waitFor(() => { + expect(screen.getByText('Audience')).toBeInTheDocument(); + }); + + expect(screen.queryByClass('tree-picker-header')).not.toBeInTheDocument(); + + const nodeExpands = screen.getAllByClass('tree-picker-node-expand'); + + // expanding audience node + await user.click(nodeExpands[0]); + + let navNodes = screen.getAllByClass('aui--breadcrumb-node'); + + expect(navNodes).toHaveLength(2); + expect(navNodes.map((node) => node.textContent)).toEqual(['All', 'Audience']); + + // expanding Fyllo node + await user.click(screen.getAllByClass('tree-picker-node-expand')[0]); + + navNodes = screen.getAllByClass('aui--breadcrumb-node'); + expect(navNodes).toHaveLength(3); + expect(navNodes.map((node) => node.textContent)).toEqual(['All', 'Audience', 'Fyllo']); + + expect(screen.getAllByClass('tree-picker-row-content')).toHaveLength(1); //1 audSegmentValuesNodes + + // expanding MRI node + await user.click(screen.getAllByClass('tree-picker-node-expand')[0]); // 0 node + const emptyTreePicker = screen.getByClass('tree-picker-empty'); + expect(emptyTreePicker).toBeInTheDocument(); + expect(within(emptyTreePicker).getByClass('tree-picker-empty-title')).toHaveTextContent('No Results.'); + + await user.click(within(emptyTreePicker).getByText('Go Back')); + expect(screen.getAllByClass('tree-picker-row-content')).toHaveLength(1); //1 audSegmentValuesNodes + + // clicking Audience nav node + await user.click(navNodes[1]); + let nodes = screen.getAllByClass('tree-picker-row-content'); + expect(nodes).toHaveLength(2); // 2 audSegmentNodes + + // clicking All nav node + await user.click(navNodes[0]); + nodes = screen.getAllByClass('tree-picker-row-content'); + expect(nodes).toHaveLength(3); // 3 rootNodes +}); + +it('should trigger props.onAdd when add node and include all nodes', async () => { + const onAdd = jest.fn(); + render( + ( + + {node.label} + audSegmentNodes} /> + ; + + )} + > + + Name + rootNodes} /> + + ); + + await waitFor(() => { + expect(screen.getByText('Audience')).toBeInTheDocument(); + }); + + const nodeAddButtons = screen.getAllByClass('tree-picker-node-add'); + expect(nodeAddButtons).toHaveLength(3); + + const includeAllButton = screen.getByClass('tree-picker-header-include-all'); + expect(includeAllButton).toBeInTheDocument(); + + await user.click(nodeAddButtons[0]); // add first root node + + expect(onAdd).toHaveBeenCalledTimes(1); + expect(onAdd).toHaveBeenCalledWith(rootNodes[0], false); + + await user.click(includeAllButton); // include all root nodes + expect(onAdd).toHaveBeenCalledTimes(4); + expect(onAdd).toHaveBeenNthCalledWith(2, rootNodes[0], true); + expect(onAdd).toHaveBeenNthCalledWith(3, rootNodes[1], true); + expect(onAdd).toHaveBeenNthCalledWith(4, rootNodes[2], true); +}); + +it('should trigger resolveNodes when searching', async () => { + const resolveNodes = jest.fn().mockReturnValue([{ id: 1, label: 'test' }]); + render( + ( + + {node.label} + audSegmentNodes} /> + + )} + > + + rootNodes} /> + + ); + + await waitFor(() => { + expect(screen.getByText('Audience')).toBeInTheDocument(); + }); + + const treePickerSearch = screen.getByClass('tree-picker-search'); + expect(treePickerSearch).toBeInTheDocument(); + + await user.click(screen.getByPlaceholderText('Search')); + await user.paste('test'); + + let nodes = screen.getAllByClass('tree-picker-row-content'); + expect(nodes).toHaveLength(1); + expect(nodes[0]).toHaveTextContent('test'); + + expect(resolveNodes).toHaveBeenCalledTimes(1); + expect(resolveNodes).toHaveBeenLastCalledWith('test', null); + + await user.clear(screen.getByTestId('search-input')); + nodes = screen.getAllByClass('tree-picker-row-content'); + expect(nodes).toHaveLength(3); // clear search, clear search nodes +}); + +it('should be able to reset to root nodes when no search results', async () => { + const resolveNodes = jest.fn().mockReturnValue([]); + render( + ( + + {node.label} + audSegmentNodes} /> + + )} + > + + rootNodes} /> + + ); + + await waitFor(() => { + expect(screen.getByText('Audience')).toBeInTheDocument(); + }); + + const treePickerSearch = screen.getByClass('tree-picker-search'); + expect(treePickerSearch).toBeInTheDocument(); + + await user.click(screen.getByPlaceholderText('Search')); + await user.paste('test'); + + expect(screen.queryByClass('tree-picker-row-content')).not.toBeInTheDocument(); + + expect(screen.getByClass('tree-picker-empty')).toBeInTheDocument(); + await user.click(screen.getByText('Reset Search')); + + expect(screen.getAllByClass('tree-picker-row-content')).toHaveLength(3); // reset to root nodes +}); diff --git a/src/components/TreePicker/TreePicker.stories.mdx b/src/components/TreePicker/TreePicker.stories.mdx new file mode 100644 index 000000000..c35168dcc --- /dev/null +++ b/src/components/TreePicker/TreePicker.stories.mdx @@ -0,0 +1,5 @@ +import { Meta } from '@storybook/blocks'; + + + +ok diff --git a/src/components/TreePicker/TreePicker.stories.tsx b/src/components/TreePicker/TreePicker.stories.tsx index da949dcec..005f0abb4 100644 --- a/src/components/TreePicker/TreePicker.stories.tsx +++ b/src/components/TreePicker/TreePicker.stories.tsx @@ -1,10 +1,7 @@ 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', @@ -13,102 +10,79 @@ const meta = { } satisfies Meta; export default meta; - type Story = StoryObj; -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 }) => ( + + {node.label} + { + // eslint-disable-next-line no-console + console.log(`Added ${node.id}: ${node.label}`); + }} + /> + {node.type === 'comment' ? null : ( + { + 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', + })); + }} + /> + )} + +); - return true; - }); - } +export const Default: Story = { + args: { + renderNode: (node) => , - return treePickerPureSubtree; - }; + children: ( + <> + + Name + { + const res = await axios.get(`https://dummyjson.com/posts/search?q=${searchText}`); - return ( -
- - } - emptySelectedListText={ -
- Choose items of interest -
- } - initialStateNode={ -
- Start by searching for items -
- } - 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={ -
- -
- } - /> + return res.data.posts.map((post) => ({ + ...post, + label: post.title, + type: 'post', + })); + }} + /> + { + 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) => ( +
+
- ); -}; - -export const Default: Story = { - args: {}, - render: () => DefaultComponent(), + ), }; diff --git a/src/components/TreePicker/TreePickerContext.jsx b/src/components/TreePicker/TreePickerContext.jsx new file mode 100644 index 000000000..317575009 --- /dev/null +++ b/src/components/TreePicker/TreePickerContext.jsx @@ -0,0 +1,215 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createStoreContext } from 'ryze'; +import _ from 'lodash'; + +const { StoreProvider, useSlice, useStore } = createStoreContext(); +const RenderNodeCtx = React.createContext(); +export const HiddenByIdCtx = React.createContext({}); + +export const useRenderNode = () => React.useContext(RenderNodeCtx); +export const useHiddenById = () => React.useContext(HiddenByIdCtx); + +export const TreePickerProvider = ({ children, renderNode }) => { + return ( + + {children} + + ); +}; + +TreePickerProvider.propTypes = { + children: PropTypes.node.isRequired, + renderNode: PropTypes.func.isRequired, +}; + +export const useTreePickerSlice = useSlice; + +const getCurrentNode = (state) => state.paths[state.paths.length - 1]; +export const useTreePickerCurrentNode = () => useTreePickerSlice(getCurrentNode); +export const useTreePickerPaths = () => useTreePickerSlice('paths'); + +const getNodesForRendering = (state) => (!!state.search ? state.searchNodes : state.nodes); +export const useTreePickerNodes = () => useTreePickerSlice(getNodesForRendering); + +export const useTreePickerSearch = () => useTreePickerSlice('search'); + +export const useTreePickerGetState = () => { + const store = useStore(); + return store.getState; +}; + +export const useInternalActions = () => { + const { setState } = useStore(); + // breadcrumb navigate back to a node + const backTo = React.useCallback( + (nodeId) => { + setState((prev) => { + // nodeId: null -> go back to root + // nodeId: undefined -> go back 1 level + // nodeId: string -> go back to the node + const newPaths = + nodeId === null + ? [] + : // if nodeId === undefined, index = -1, will end up slice without last item, if nodeId has a valid item, then index is positive + prev.paths.slice( + 0, + prev.paths.findIndex((entry) => entry.id === nodeId) + (nodeId === undefined ? 0 : 1) + ); + + const nodes = prev.treeMapRef.current.get(nodeId || newPaths[newPaths.length - 1]?.id || ''); + if (!nodes) { + // eslint-disable-next-line no-console + console.error('unexpected node id: ', nodeId); + return prev; + } + + return { + ...prev, + paths: newPaths, + nodes, + search: '', + }; + }); + }, + [setState] + ); + + // expand new node & init root + const goTo = React.useCallback( + (nodes, node) => { + setState((prev) => { + if (!node) { + // init root + prev.treeMapRef.current.set('', nodes); + + return { + ...prev, + nodes: nodes, + search: '', + }; + } + + const currentNode = prev.paths[prev.paths.length - 1]; + // when node is the same as currentNode, do no change paths + const newPaths = currentNode && currentNode.id === node.id ? prev.paths : [...prev.paths, node]; + + prev.treeMapRef.current.set(node.id, nodes); + + return { + ...prev, + paths: newPaths, + nodes: nodes, + search: '', + }; + }); + }, + [setState] + ); + + const searchTo = React.useCallback( + (nodes, search) => { + setState((prev) => { + if (search) { + return { + ...prev, + search: search, + searchNodes: nodes, + }; + } + + // clear search + // const node = prev.paths[prev.paths.length - 1]; + // if node is falsy, assume it is searching in root + // const newNodes = prev.treeMapRef.current.get(node ? node.id : ''); + return { + ...prev, + search: '', + searchNodes: [], + }; + }); + }, + [setState] + ); + + const addAddable = React.useCallback( + (nodeId, addNode) => { + setState((prev) => { + if (!nodeId || !addNode) return prev; + + return prev.addable[nodeId] + ? prev + : { + ...prev, + addable: { ...prev.addable, [nodeId]: addNode }, + }; + }); + }, + [setState] + ); + + const removeAddable = React.useCallback( + (nodeId) => { + setState((prev) => { + if (!nodeId) return prev; + + return !prev.addable[nodeId] + ? prev + : { + ...prev, + addable: _.pickBy(prev.addable, (_value, id) => id !== nodeId), + }; + }); + }, + [setState] + ); + + const setIsResolvingRoot = React.useCallback( + (newValue) => { + setState((prev) => ({ + ...prev, + isResolvingRoot: newValue, + })); + }, + [setState] + ); + + const setExpandingNodeId = React.useCallback( + (nodeId, expanding) => { + setState((prev) => ({ + ...prev, + expandingNodeId: expanding ? nodeId : prev.expandingNodeId === nodeId ? '' : prev.expandingNodeId, + })); + }, + [setState] + ); + + return { + backTo, + goTo, + searchTo, + addAddable, + removeAddable, + setIsResolvingRoot, + setExpandingNodeId, + }; +}; + +export const useTreePickerActions = () => { + const { backTo, goTo, searchTo } = useInternalActions(); + return { backTo, goTo, searchTo }; +}; diff --git a/src/components/TreePicker/TreePickerHeader.css b/src/components/TreePicker/TreePickerHeader.css new file mode 100644 index 000000000..30b7c136f --- /dev/null +++ b/src/components/TreePicker/TreePickerHeader.css @@ -0,0 +1,18 @@ +@import url('../../styles/variable.css'); + +.tree-picker-header { + color: $color-text-description; +} + +.tree-picker-header-action-holder { + /* 24 + 12 + 24 */ + width: 60px; +} + +.tree-picker-header-include-all.aui--button.aui-link { + width: 60px; + + & .aui-children-container { + font-weight: $font-weight-bold; + } +} diff --git a/src/components/TreePicker/TreePickerHeader.jsx b/src/components/TreePicker/TreePickerHeader.jsx new file mode 100644 index 000000000..9d68db79c --- /dev/null +++ b/src/components/TreePicker/TreePickerHeader.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cc from 'classnames'; +import _ from 'lodash'; +import Button from '../Button'; +import { useTreePickerGetState, useTreePickerSlice } from './TreePickerContext'; + +import './TreePickerHeader.css'; + +const getNoAddable = (state) => _.isEmpty(state.addable); + +const HeaderIncludeAll = () => { + const noAddable = useTreePickerSlice(getNoAddable); + const getTreeState = useTreePickerGetState(); + + return noAddable ? ( +
+ ) : ( + + ); +}; + +const getCurrentNodeHeader = (state) => { + const currentNode = state.paths[state.paths.length - 1]; + return currentNode?.header; +}; + +const TreePickerHeader = ({ children, className }) => { + const header = useTreePickerSlice(getCurrentNodeHeader); + + const content = header || children; + + return !content ? null : ( +
+
{content}
+ +
+ ); +}; + +TreePickerHeader.propTypes = { + children: PropTypes.node, + className: PropTypes.string, +}; + +export default TreePickerHeader; diff --git a/src/components/TreePicker/TreePickerNav.css b/src/components/TreePicker/TreePickerNav.css new file mode 100644 index 000000000..f97dd124c --- /dev/null +++ b/src/components/TreePicker/TreePickerNav.css @@ -0,0 +1,3 @@ +.tree-picker-nav { + margin-left: 2px; +} diff --git a/src/components/TreePicker/TreePickerNav.jsx b/src/components/TreePicker/TreePickerNav.jsx new file mode 100644 index 000000000..155fc6611 --- /dev/null +++ b/src/components/TreePicker/TreePickerNav.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import Breadcrumb from '../Breadcrumb'; +import { useInternalActions, useTreePickerPaths } from './TreePickerContext'; + +import './TreePickerNav.css'; + +const TreePickerNav = ({ rootLabel = 'All', className, onNavTo }) => { + const paths = useTreePickerPaths(); + const { backTo } = useInternalActions(); + + const rootNode = { id: '__all__', label: rootLabel }; + + return paths.length === 0 ? null : ( + { + const newPath = pathId === rootNode.id ? null : pathId; + backTo(newPath); + onNavTo?.(newPath); + }} + /> + ); +}; + +TreePickerNav.propTypes = { + rootLabel: PropTypes.string, + className: PropTypes.string, + onNavTo: PropTypes.func, +}; + +export default TreePickerNav; diff --git a/src/components/TreePicker/TreePickerNode.css b/src/components/TreePicker/TreePickerNode.css new file mode 100644 index 000000000..9180f38df --- /dev/null +++ b/src/components/TreePicker/TreePickerNode.css @@ -0,0 +1,32 @@ +@import url('../../styles/variable.css'); + +.tree-picker-node-expand { + margin: 0; + + &.aui--button.aui-borderless:hover { + border-color: transparent; + } +} + +.tree-picker-node-add { + margin: 0; +} + +.tree-picker-node-svg { + width: 16px; + height: 16px; +} + +.tree-picker-node-branch { + margin-left: 24px; + border-top: 1px solid $color-border-base; + border-left: 1px solid $color-border-base; + + & .tree-picker-row { + padding-left: 12px; + + &:last-child { + border-bottom: 0; + } + } +} diff --git a/src/components/TreePicker/TreePickerNode.jsx b/src/components/TreePicker/TreePickerNode.jsx new file mode 100644 index 000000000..c733d93a7 --- /dev/null +++ b/src/components/TreePicker/TreePickerNode.jsx @@ -0,0 +1,284 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import _ from 'lodash'; +import useIsUnmounted from '../../hooks/useIsUnmounted'; +import useCallbackRef from '../../hooks/useCallbackRef'; +import PlusIcon from '../../styles/icons/plus.svg'; +import FolderOpenIcon from '../../styles/icons/folder-open.svg'; +import FolderIcon from '../../styles/icons/folder.svg'; +import Button from '../Button'; +import Skeleton from '../Skeleton'; +import { + useTreePickerGetState, + useRenderNode, + useInternalActions, + useHiddenById, + useTreePickerSlice, +} from './TreePickerContext'; + +import './TreePickerNode.css'; + +const TreePickerNodeContext = React.createContext({ level: 0 }); + +export const useTreePickerNode = () => React.useContext(TreePickerNodeContext); + +const TreePickerNode = ({ className, children, node }) => { + const { goTo } = useInternalActions(); + const [state, setState] = React.useState(() => ({ expanded: false, subNodes: [] })); + + const { level } = useTreePickerNode(); + + const inlineExpand = React.useCallback( + (nodes) => { + setState((prev) => { + if (!prev.expanded && nodes) { + return { expanded: true, subNodes: nodes }; + } + return { expanded: false, subNodes: [] }; + }); + }, + [setState] + ); + + const fullExpand = React.useCallback( + (nodes) => { + goTo(nodes, node); + }, + [goTo, node] + ); + + const value = React.useMemo( + () => ({ + level: level + 1, + node, + expanded: state.expanded, + inlineExpand, + fullExpand, + }), + [level, node, state.expanded, inlineExpand, fullExpand] + ); + + return ( + +
{children}
+ {state.subNodes.length === 0 ? null : } +
+ ); +}; +TreePickerNode.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + node: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + label: PropTypes.string.isRequired, + }).isRequired, +}; + +const TreePickerNodeBranch = ({ nodes }) => { + const renderNode = useRenderNode(); + const { level } = useTreePickerNode(); + const byId = useHiddenById(); + + const visibleNodes = nodes.filter(({ id }) => !byId[id]); + + return visibleNodes.length === 0 ? null : ( +
+ {visibleNodes.map(renderNode)} +
+ ); +}; + +const TreePickerNodeContent = ({ children, className, ...rest }) => { + return ( +
+ {children} +
+ ); +}; +TreePickerNodeContent.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, +}; + +const getReverseIcon = (current) => + ({ + 'folder-open': 'folder', + folder: 'folder-open', + }[current]); + +const IconComponents = { + 'folder-open': FolderOpenIcon, + folder: FolderIcon, +}; + +const getHasSearch = (state) => !!state.search; + +const TreePickerNodeExpand = ({ + className, + inline, + onClick, + disabled, + onMouseEnter, + onMouseLeave, + resolveNodes, + ...rest +}) => { + const [isHover, setIsHover] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); + const { setExpandingNodeId } = useInternalActions(); + const expandableRef = useTreePickerSlice('expandableRef'); + const { node, expanded, inlineExpand, fullExpand } = useTreePickerNode(); + const isUnmounted = useIsUnmounted(); + const getTreeState = useTreePickerGetState(); + const hasSearch = useTreePickerSlice(getHasSearch); + + const iconName = expanded ? 'folder-open' : 'folder'; + const ExpandIcon = IconComponents[isHover ? getReverseIcon(iconName) : iconName]; + + const propExpand = inline ? inlineExpand : fullExpand; + const defaultExpand = hasSearch ? inlineExpand : fullExpand; + const shouldDefaultExpand = _.isNil(inline); + + const handleExpand = async () => { + setExpandingNodeId(node.id, true); + const expandFunction = shouldDefaultExpand ? defaultExpand : propExpand; + + setIsLoading(true); + try { + const nodes = await resolveNodes(); + + // ignore falsy nodes + // or when user click 2 different expands, ignore the first click + // or if node is unmounted + if (!nodes || getTreeState().expandingNodeId !== node.id || isUnmounted()) return; + + expandFunction(nodes); + } catch (error) { + // TODO use error boundary + // eslint-disable-next-line no-console + console.error('[TreePickerNodeExpand]', error); + } finally { + setIsLoading(false); + setExpandingNodeId(node.id, false); + } + }; + + // record the current expand node to ctx ref + React.useLayoutEffect(() => { + if (!disabled) { + expandableRef.current[node.id] = handleExpand; + return; + } + delete expandableRef.current[node.id]; + }); + + // when using default expand, on search reset it should go back to collapsed + React.useEffect(() => { + if (!hasSearch && shouldDefaultExpand) { + inlineExpand(); + } + }, [hasSearch, shouldDefaultExpand, inlineExpand]); + + return ( + + ); + return ( +
+
{title}
+ {search ? ( + + ) : ( + backAction + )} +
+ ); +}; +TreePickerTreeEmptyState.propTypes = { + title: PropTypes.node, +}; + +const defaultPlaceholder = ; +const defaultEmptyState = ; + +const TreePickerTree = ({ + className, + resolveRootNodes, + hiddenNodeIds, + placeholder = defaultPlaceholder, + emptyState = defaultEmptyState, +}) => { + const isInRoot = useTreePickerSlice(getIsInRoot); + const nodes = useTreePickerNodes(); + const renderNode = useRenderNode(); + const { goTo, setIsResolvingRoot } = useInternalActions(); + const isResolvingRoot = useTreePickerSlice('isResolvingRoot'); + const isUnmounted = useIsUnmounted(); + + React.useEffect(() => { + let cancelled = false; + const resolve = async () => { + if (!isInRoot) return; + + setIsResolvingRoot(true); + try { + const rootNodes = await resolveRootNodes(); + + // ignore falsy nodes + // or if the tree itself is unmounted + // or if the resolveRootNodes func has changed + if (!rootNodes || isUnmounted() || cancelled) return; + + goTo(rootNodes); + } catch (error) { + // TODO use error boundary + // eslint-disable-next-line no-console + console.error('[TreePickerTree]', error); + } finally { + if (cancelled) return; + setIsResolvingRoot(false); + } + }; + resolve(); + + return () => { + cancelled = true; + }; + }, [goTo, setIsResolvingRoot, isUnmounted, resolveRootNodes, isInRoot]); + + const byId = React.useMemo(() => _.keyBy(hiddenNodeIds), [hiddenNodeIds]); + const visibleNodes = nodes.filter(({ id }) => !byId[id]); + + return ( + +
+ {isResolvingRoot ? placeholder : visibleNodes.length === 0 ? emptyState : visibleNodes.map(renderNode)} +
+
+ ); +}; + +TreePickerTree.propTypes = { + className: PropTypes.string, + hiddenNodeIds: PropTypes.array, + resolveRootNodes: PropTypes.func.isRequired, + placeholder: PropTypes.node, + emptyState: PropTypes.node, +}; + +TreePickerTree.Placeholder = TreePickerTreePlaceholder; +TreePickerTree.EmptyState = TreePickerTreeEmptyState; + +export default TreePickerTree; diff --git a/src/components/TreePicker/index.d.ts b/src/components/TreePicker/index.d.ts index bd2f4b623..ce9699a2c 100644 --- a/src/components/TreePicker/index.d.ts +++ b/src/components/TreePicker/index.d.ts @@ -1,167 +1,98 @@ import * as React from 'react'; -export interface TreePickerSimplePureBreadcrumbNodes { - id?: any; - label: string; +export interface TreePickerNodeContentProps { + className?: string; + children: React.ReactNode; } -export interface TreePickerSimplePureSelectedNodes { - id?: any; - label: string; - isExpandable?: boolean; - path?: { - id?: any; - label: string; - }[]; - ancestors?: { - id?: any; - label: string; - }[]; - type: string; - value?: number; - accent?: 'warning' | 'success' | 'info' | 'error'; +declare const TreePickerNodeContent: React.FC; + +export interface TreePickerNodeExpandProps { + className?: string; + inline?: boolean; + resolveNodes?: (...args: any[]) => any; + onClick?: (...args: any[]) => any; + disabled?: boolean; + onMouseEnter?: (...args: any[]) => any; + onMouseLeave?: (...args: any[]) => any; } -export interface TreePickerSimplePureSubtree { - id?: any; - label: string; - isExpandable?: boolean; - path?: { - id?: any; - label: string; - }[]; - ancestors?: { - id?: any; - label: string; - }[]; - type: string; - value?: number; - accent?: 'warning' | 'success' | 'info' | 'error'; +declare const TreePickerNodeExpand: React.FC; + +export interface TreePickerNodeAddProps { + onAdd: (...args: any[]) => any; + onClick?: (...args: any[]) => any; + disabled?: boolean; + className?: string; } -export interface TreePickerSimplePureBreadcrumbRootNode { - id?: any; +declare const TreePickerNodeAdd: React.FC; + +declare const TreePickerNodePlaceholder: React.FC; + +export interface TreePickerNodeNode { + id: string | number; label: string; } -export interface TreePickerSimplePureProps { - /** - * Class Names for SplitPane component - */ - additionalClassNames?: string[]; - /** - * Returns node id. This prop is not required, but an empty array is not allowed. At least one element is required in the array. - */ - breadcrumbNodes?: TreePickerSimplePureBreadcrumbNodes[]; - /** - * This propType creates a list of breadcrumb node - */ - breadcrumbOnClick?: (...args: any[]) => any; - /** - * Interval time on search - */ - debounceInterval?: number; - /** - * Disables treepicker including search bar - */ +export interface TreePickerNodeProps { + node: TreePickerNodeNode; + children: React.ReactNode; + className?: string; +} + +declare const TreePickerNode: React.FC & { + Content: typeof TreePickerNodeContent; + Expand: typeof TreePickerNodeExpand; + Add: typeof TreePickerNodeAdd; + Placeholder: typeof TreePickerNodePlaceholder; +}; + +export interface TreePickerNavProps { + rootLabel?: string; + className?: string; + onNavTo?: (...args: any[]) => any; +} + +declare const TreePickerNav: React.FC; + +export interface TreePickerHeaderProps { + children?: React.ReactNode; + className?: string; +} + +declare const TreePickerHeader: React.FC; + +export interface TreePickerSearchProps { + resolveNodes: (...args: any[]) => any; + className?: string; disabled?: boolean; - /** - * Disables treepicker's grid item - */ - disableInclude?: boolean; - /** - * The svg symbol used when there will be no item on both left or right Grid - */ - emptySvgSymbol?: React.ReactNode; - /** - * The svg symbol used when there will be no item on right Grid (Selected list) - */ - emptySelectedListSvgSymbol?: React.ReactNode; - /** - * Displays this text when there will be no item on left Grid. Prefer type 'string', but rich text can be used here - */ - emptyText?: React.ReactNode; - /** - * Displays this text when there will be no item on right Grid(Selected list). Prefer type 'string', but rich text can be used here. - */ - emptySelectedListText?: React.ReactNode; - /** - * Triggers when clicking any item in the left Grid - */ - expandNode?: (...args: any[]) => any; - /** - * This function use to transform keys of the list item in the left Grid - */ - groupFormatter?: (...args: any[]) => any; - /** - * Hides the empty icon on right Grid (Selected list). Given emptySvgSymbol and hideIcon together, the empty symbol will be only displayed on the left grid. - */ - hideIcon?: boolean; - /** - * Click event on '+' button of each list Item - */ - includeNode?: (...args: any[]) => any; - /** - * Same as emptyText - */ - initialStateNode?: React.ReactNode; - /** - * Same as emptySymbol - */ - initialStateSymbol?: React.ReactNode; - /** - * Uses for specific className - */ - itemType?: string; - isLoading?: boolean; - /** - * Uses for rendering custom node - */ - nodeRenderer?: (...args: any[]) => any; - removeNode?: (...args: any[]) => any; - /** - * Triggers when search input changes - */ - onChange?: (...args: any[]) => any; - /** - * Triggers when the user clicks the clear button on search input - */ - onClear?: (...args: any[]) => any; - /** - * Please see Search - */ - onSearch?: (...args: any[]) => any; - /** - * Please see Search - */ - searchOnEnter?: boolean; - searchPlaceholder?: string; - searchValue?: string; - selectedNodes: TreePickerSimplePureSelectedNodes[]; - /** - * Show or hide the search field on the selection pane - */ - showSearch?: boolean; - /** - * A list of available unselected nodes. This prop is not required, but an empty array is not allowed. At least one element is required in the array. - */ - subtree?: TreePickerSimplePureSubtree[]; - svgSymbolCancel?: React.ReactNode; - svgSymbolSearch?: React.ReactNode; - /** - * e.g: Default Group - */ - displayGroupHeader?: boolean; - hideSearchOnRoot?: boolean; - /** - * A react node to be rendered at the top of the right hand side pane. Generally we are expecting a search component. - */ - selectedTopSearch?: React.ReactNode; - addNodePopoverInfoProps?: Object; - removeNodePopoverInfoProps?: Object; - breadcrumbRootNode?: TreePickerSimplePureBreadcrumbRootNode; } -declare const TreePickerSimplePure: React.FC; +declare const TreePickerSearch: React.FC; + +export interface TreePickerTreeProps { + resolveRootNodes: (...args: any[]) => any; + className?: string; + hiddenNodeIds?: string[]; + placeholder?: React.ReactNode; + emptyState?: React.ReactNode; +} + +declare const TreePickerTree: React.FC; + +export interface TreePickerProps { + children: React.ReactNode; + renderNode: (...args: any[]) => any; + className?: string; +} + +declare const TreePicker: React.FC & { + Node: typeof TreePickerNode; + Nav: typeof TreePickerNav; + Header: typeof TreePickerHeader; + Search: typeof TreePickerSearch; + Tree: typeof TreePickerTree; +}; -export default TreePickerSimplePure; +export default TreePicker; diff --git a/src/components/TreePicker/index.js b/src/components/TreePicker/index.js new file mode 100644 index 000000000..1545edddd --- /dev/null +++ b/src/components/TreePicker/index.js @@ -0,0 +1,11 @@ +export { + useTreePickerActions, + useTreePickerPaths, + useTreePickerNodes, + useTreePickerSearch, + useTreePickerCurrentNode, + useTreePickerGetState, + useTreePickerSlice, +} from './TreePickerContext'; +export { useTreePickerNode, default as TreePickerNode } from './TreePickerNode'; +export { default } from './TreePicker'; diff --git a/src/components/TreePicker/index.jsx b/src/components/TreePicker/index.jsx deleted file mode 100644 index 9b3300d30..000000000 --- a/src/components/TreePicker/index.jsx +++ /dev/null @@ -1,267 +0,0 @@ -import _ from 'lodash'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import SplitPane from '../SplitPane'; -import TreePickerGrid from './Grid'; -import TreePickerNav from './Nav'; -import FlexibleSpacer from '../FlexibleSpacer'; -import { TreePickerPropTypesNode, TreePickerPropTypesBreadCrumbNode } from '../../prop-types/TreePickerPropTypes'; -import './styles.css'; - -export const removeSelected = ({ subtree, selectedNodes }) => { - if (!subtree) return subtree; - - return _.reject(subtree, ({ id }) => _.some(selectedNodes, { id })); -}; - -const TreePickerSimplePure = ({ - additionalClassNames, - breadcrumbRootNode, - breadcrumbNodes, - breadcrumbOnClick, - debounceInterval, - disabled, - disableInclude, - emptySvgSymbol, - emptySelectedListSvgSymbol, - emptyText, - emptySelectedListText, - expandNode, - groupFormatter, - hideIcon, - includeNode, - initialStateNode, - initialStateSymbol, - isLoading, - itemType, - nodeRenderer, - removeNode, - onChange, - onClear, - onSearch, - searchOnEnter, - searchPlaceholder, - searchValue, - selectedNodes, - showSearch, - subtree, - svgSymbolCancel, - svgSymbolSearch, - displayGroupHeader, - hideSearchOnRoot, - selectedTopSearch, - addNodePopoverInfoProps, - removeNodePopoverInfoProps, -}) => { - const selectableNodes = removeSelected({ subtree, selectedNodes }); - let searchTextNode = emptyText || 'No items to select.'; - searchTextNode = initialStateNode && _.isEmpty(searchValue) ? initialStateNode : searchTextNode; - const emptySymbol = initialStateSymbol && _.isEmpty(searchValue) ? initialStateSymbol : emptySvgSymbol; - const className = classnames('treepickersimplepure-component', { disabled }); - - return ( -
- - {hideSearchOnRoot && _.isEmpty(breadcrumbNodes) ? null : ( - - )} - - - - - - - {selectedTopSearch} - - - -
- ); -}; - -TreePickerSimplePure.propTypes = { - /** - * Class Names for SplitPane component - */ - additionalClassNames: PropTypes.arrayOf(PropTypes.string), - /** - * Optional. This prop allows customization of the Breadcrumb root node. { id: PropTypes.sting | PropTypes.number, label: PropTypes.string} - */ - breadcrumbRootNode: TreePickerPropTypesBreadCrumbNode, - /** - * Returns node id. This prop is not required, but an empty array is not allowed. At least one element is required in the array. - */ - breadcrumbNodes: PropTypes.arrayOf(TreePickerPropTypesBreadCrumbNode.isRequired), - /** - * This propType creates a list of breadcrumb node - */ - breadcrumbOnClick: PropTypes.func, - /** - * Interval time on search - */ - debounceInterval: PropTypes.number, - /** - * Disables treepicker including search bar - */ - disabled: PropTypes.bool, - /** - * Disables treepicker's grid item - */ - disableInclude: PropTypes.bool, - /** - * The svg symbol used when there will be no item on both left or right Grid - */ - emptySvgSymbol: PropTypes.node, - /** - * The svg symbol used when there will be no item on right Grid (Selected list) - */ - emptySelectedListSvgSymbol: PropTypes.node, - /** - * Displays this text when there will be no item on left Grid. Prefer type 'string', but rich text can be used here - */ - emptyText: PropTypes.node, - /** - * Displays this text when there will be no item on right Grid(Selected list). Prefer type 'string', but rich text can be used here. - */ - emptySelectedListText: PropTypes.node, - /** - * Triggers when clicking any item in the left Grid - */ - expandNode: PropTypes.func, - /** - * This function use to transform keys of the list item in the left Grid - */ - groupFormatter: PropTypes.func, - /** - * Hides the empty icon on right Grid (Selected list). Given emptySvgSymbol and hideIcon together, the empty symbol will be only displayed on the left grid. - */ - hideIcon: PropTypes.bool, - /** - * Click event on '+' button of each list Item - */ - includeNode: PropTypes.func, - /** - * Same as emptyText - */ - initialStateNode: PropTypes.node, - /** - * Same as emptySymbol - */ - initialStateSymbol: PropTypes.node, - /** - * Uses for specific className - */ - itemType: PropTypes.string, - isLoading: PropTypes.bool, - /** - * Uses for rendering custom node - */ - nodeRenderer: PropTypes.func, - removeNode: PropTypes.func, - /** - * Triggers when search input changes - */ - onChange: PropTypes.func, - /** - * Triggers when the user clicks the clear button on search input - */ - onClear: PropTypes.func, - /** - * Please see Search - */ - onSearch: PropTypes.func, - /** - * Please see Search - */ - searchOnEnter: PropTypes.bool, - searchPlaceholder: PropTypes.string, - searchValue: PropTypes.string, - selectedNodes: PropTypes.arrayOf(TreePickerPropTypesNode).isRequired, - /** - * Show or hide the search field on the selection pane - */ - showSearch: PropTypes.bool, - /** - * A list of available unselected nodes. This prop is not required, but an empty array is not allowed. At least one element is required in the array. - */ - subtree: PropTypes.arrayOf(TreePickerPropTypesNode.isRequired), - svgSymbolCancel: PropTypes.node, - svgSymbolSearch: PropTypes.node, - /** - * e.g: Default Group - */ - displayGroupHeader: PropTypes.bool, - hideSearchOnRoot: PropTypes.bool, - /** - * A react node to be rendered at the top of the right hand side pane. Generally we are expecting a search component. - */ - selectedTopSearch: PropTypes.node, - addNodePopoverInfoProps: PropTypes.object, - removeNodePopoverInfoProps: PropTypes.object, -}; - -TreePickerSimplePure.defaultProps = { - itemType: 'node', - debounceInterval: 0, - disabled: false, - displayGroupHeader: true, - isLoading: false, - searchOnEnter: false, - showSearch: true, - searchPlaceholder: 'Search', - hideSearchOnRoot: false, -}; - -export default TreePickerSimplePure; diff --git a/src/components/TreePicker/index.spec.jsx b/src/components/TreePicker/index.spec.jsx deleted file mode 100644 index 169fd577b..000000000 --- a/src/components/TreePicker/index.spec.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import { render, screen } from 'testing'; -import TreePickerSimplePure, { removeSelected } from '.'; -import TreePickerMocks from './mocks'; - -const { actNode, initialSelection, itemType, ntNode, qldNode, saNode } = TreePickerMocks; -const svgSymbol =
; - -const props = { - breadcrumbNodes: [saNode], - breadcrumbOnClick: _.noop, - emptySvgSymbol: svgSymbol, - initialStateNode: 'Begin searching.', - initialStateSymbol: svgSymbol, - expandNode: _.noop, - includeNode: _.noop, - itemType, - nodeRenderer: _.noop, - removeNode: _.noop, - onClear: _.noop, - onChange: _.noop, - onSearch: _.noop, - searchOnEnter: false, - searchPlaceholder: 'Search Geometry', - searchValue: '', - selectedNodes: initialSelection, - subtree: [qldNode, saNode, actNode, ntNode], - svgSymbolCancel: svgSymbol, - svgSymbolSearch: svgSymbol, - hideSearchOnRoot: false, -}; - -it('should render with props', () => { - render(); - expect(screen.getByTestId('treepicker-wrapper')).toHaveClass('treepickersimplepure-component'); - - expect(screen.getAllByTestId('split-panel-wrapper')).toHaveLength(2); - const wrapper = screen.getByTestId('treepicker-wrapper'); - const splitPanes = screen.getAllByTestId('split-panel-wrapper'); - - expect(wrapper).toContainElement(splitPanes[0]); - expect(wrapper).toContainElement(splitPanes[1]); - - expect(screen.getByTestId('treepicker-nav-wrapper')).toBeInTheDocument(); - - expect(screen.getAllByTestId('flexible-spacer-wrapper')).toHaveLength(2); - const leftSplitPane = screen.getByDts(`treepicker-splitpane-available-${_.kebabCase(itemType)}`); - const rightSplitPane = screen.getByDts(`treepicker-splitpane-selected-${_.kebabCase(itemType)}`); - expect(leftSplitPane).toContainElement(screen.getAllByTestId('flexible-spacer-wrapper')[0]); - expect(rightSplitPane).toContainElement(screen.getAllByTestId('flexible-spacer-wrapper')[1]); -}); - -it('should render empty text as expected', () => { - const newProps = _.assign({}, props, { subtree: [] }); - - const view = render(); - - expect(screen.getByText('Begin searching.')).toBeInTheDocument(); - expect(screen.getByText('Begin searching.')).toHaveClass('empty-component-text'); - - view.rerender(); - expect(screen.queryByText('Begin searching.')).not.toBeInTheDocument(); - expect(screen.getByText('No items to select.')).toBeInTheDocument(); - expect(screen.getByText('No items to select.')).toHaveClass('empty-component-text'); -}); - -it('should have disabled class included when disabled set to true', () => { - render(); - expect(screen.getByTestId('treepicker-wrapper')).toHaveClass('treepickersimplepure-component disabled'); -}); - -it('should not render TreePickerNav when hideSearchOnRoot is true and on root level', () => { - const hideSearchOnRootProps = _.assign({}, props, { - hideSearchOnRoot: true, - breadcrumbNodes: [], - }); - render(); - expect(screen.queryByTestId('treepicker-nav-wrapper')).not.toBeInTheDocument(); -}); - -it('should render TreePickerNav when hideSearchOnRoot is true and not on root level', () => { - const hideSearchOnRootProps = _.assign({}, props, { - hideSearchOnRoot: true, - }); - render(); - expect(screen.getByTestId('treepicker-nav-wrapper')).toBeInTheDocument(); -}); - -describe('removeSelected', () => { - it('should remove selected nodes from a subtree', () => { - const subtree = [{ id: 1 }, { id: 2 }, { id: 3 }]; - const selectedNodes = [{ id: 1 }, { id: 3 }]; - - expect(removeSelected({ subtree, selectedNodes })).toEqual([{ id: 2 }]); - }); - - it('should return an empty array when passed an empty subtree', () => { - const subtree = []; - const selectedNodes = [{ id: 1 }, { id: 3 }]; - - expect(removeSelected({ subtree, selectedNodes })).toEqual([]); - }); - - it('should return undefined when passed an undefined subtree', () => { - const subtree = undefined; - const selectedNodes = [{ id: 1 }, { id: 3 }]; - - expect(removeSelected({ subtree, selectedNodes })).toEqual(undefined); - }); -}); diff --git a/src/components/TreePicker/mocks.js b/src/components/TreePicker/mocks.js deleted file mode 100644 index a4ae37a8a..000000000 --- a/src/components/TreePicker/mocks.js +++ /dev/null @@ -1,99 +0,0 @@ -import immutable from 'seamless-immutable'; - -const baseItem = { - label: 'Awesome Product', - value: 10000, -}; - -const auPath = { id: 'au', label: 'AU', path: [] }; -const actPath = { id: 'au-act', label: 'ACT', path: [auPath] }; - -const actNode = { - id: 'au-act', - label: 'Australian Capital Territory', - type: 'State', - path: [auPath], - value: 1000, - rootTypeId: 'a', - isSelectable: false, -}; - -const ntNode = { - id: 'au-nt', - label: 'Northern Territory', - type: 'State', - path: [auPath], - value: 500, - rootTypeId: 'a', -}; -const qldNode = { - id: 'au-qld', - label: 'Queensland', - type: 'State', - path: [auPath], - value: 500, - rootTypeId: 'a', -}; -const saNode = { - id: 'au-sa', - label: 'South Australia', - type: 'State', - path: [auPath], - value: 500, - rootTypeId: 'a', -}; - -const cbrNode = { - id: 'au-act-cbr', - label: 'Canberra', - type: 'City', - path: [auPath, actPath], - value: 2000, - rootTypeId: 'a', - isExpandable: true, -}; - -const cbrNodeAlreadySelected = { - id: 'au-act-cbr', - label: 'Canberra', - type: 'City', - ancestors: [actPath, auPath], - path: [], - value: 2000, - rootTypeId: 'a', - isExpandable: true, -}; - -const maleNode = { - id: 4, - label: 'Males', - type: '', - path: [], - value: 500, - rootTypeId: 'b', -}; - -const valueFormatter = (value) => value; - -const nodeRenderer = (value) => `Test value: ${value.label}`; - -const initialSelection = [actNode, ntNode]; - -const itemType = 'example item type'; - -const TreePickerMocks = immutable({ - actNode, - ntNode, - baseItem, - cbrNode, - cbrNodeAlreadySelected, - initialSelection, - itemType, - maleNode, - nodeRenderer, - qldNode, - saNode, - valueFormatter, -}); - -export default TreePickerMocks; diff --git a/src/components/TreePicker/styles.css b/src/components/TreePicker/styles.css deleted file mode 100644 index 9c55ebf17..000000000 --- a/src/components/TreePicker/styles.css +++ /dev/null @@ -1,34 +0,0 @@ -@import url('../../styles/variable.css'); - -.treepickersimplepure-component { - display: flex; - height: 500px; -} - -.treepickersimplepure-component .loading-nodes-container { - text-align: center; - padding: 60px; -} - -.treepickersimplepure-component .flexible-spacer-component { - border-top: 0; -} - -.treepickersimplepure-component .treepickernav-component { - background-color: $color-white; - border-top: 1px solid $color-border-base; -} - -.treepickersimplepure-component .splitpane-component + .splitpane-component .empty-component { - margin-top: 71px; -} - -.treepickersimplepure-component.disabled { - pointer-events: none; - cursor: default; - opacity: 0.65; -} - -.background-highlighted { - background-color: $color-grey-200; -} diff --git a/src/hooks/index.js b/src/hooks/index.js deleted file mode 100644 index 03e1ec0b4..000000000 --- a/src/hooks/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as useArrowFocus } from './useArrowFocus'; diff --git a/src/hooks/useCallbackRef.js b/src/hooks/useCallbackRef.js new file mode 100644 index 000000000..7852648a5 --- /dev/null +++ b/src/hooks/useCallbackRef.js @@ -0,0 +1,13 @@ +import React from 'react'; + +const useCallbackRef = (callback) => { + const ref = React.useRef(callback); + + React.useLayoutEffect(() => { + ref.current = callback; + }); + + return ref; +}; + +export default useCallbackRef; diff --git a/src/hooks/useIsUnmounted.js b/src/hooks/useIsUnmounted.js new file mode 100644 index 000000000..2b8b21868 --- /dev/null +++ b/src/hooks/useIsUnmounted.js @@ -0,0 +1,16 @@ +import React from 'react'; + +const useIsUnmounted = () => { + const isUnmounted = React.useRef(false); + + React.useEffect(() => { + isUnmounted.current = false; + return () => { + isUnmounted.current = true; + }; + }, []); + + return React.useCallback(() => isUnmounted.current, []); +}; + +export default useIsUnmounted; diff --git a/src/index.js b/src/index.js index bea4c3f34..9691117ad 100644 --- a/src/index.js +++ b/src/index.js @@ -64,10 +64,7 @@ import Tile from './components/Tile'; import TileGrid from './components/TileGrid'; import Toast from './components/Toast'; import Totals from './components/Totals'; -import TreePickerSimplePure from './components/TreePicker'; -import TreePickerGrid from './components/TreePicker/Grid'; -import TreePickerNav from './components/TreePicker/Nav'; -import TreePickerNode from './components/TreePicker/Node'; +import TreePicker from './components/TreePicker'; import UserListPicker from './components/UserListPicker'; import VerticalNav from './components/VerticalNav'; @@ -128,10 +125,7 @@ export { TextEllipsis, TileGrid, Totals, - TreePickerGrid, - TreePickerNav, - TreePickerNode, - TreePickerSimplePure, + TreePicker, UserListPicker, InformationBox, ImageCropper, diff --git a/src/styles/icons/folder-open.svg b/src/styles/icons/folder-open.svg new file mode 100644 index 000000000..f8f785080 --- /dev/null +++ b/src/styles/icons/folder-open.svg @@ -0,0 +1 @@ + diff --git a/src/styles/icons/folder.svg b/src/styles/icons/folder.svg new file mode 100644 index 000000000..62de78080 --- /dev/null +++ b/src/styles/icons/folder.svg @@ -0,0 +1 @@ + diff --git a/src/styles/icons/plus.svg b/src/styles/icons/plus.svg new file mode 100644 index 000000000..66c3fac50 --- /dev/null +++ b/src/styles/icons/plus.svg @@ -0,0 +1 @@ +