From 3596d776fcc34f4518251c2635f9ff40f7910c7e Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 13 Mar 2019 15:40:27 -0400 Subject: [PATCH 1/2] Refactor of DataListToolbar. Creates a number of smaller components used by the toolbar. Adds support for passing in an add button node to the toolbar. --- __tests__/components/ExpandCollapse.test.jsx | 23 ++ __tests__/components/Search.test.jsx | 78 ++++++ __tests__/components/Sort.test.jsx | 208 ++++++++++++++ package-lock.json | 12 +- .../DataListToolbar/DataListToolbar.jsx | 265 ++++-------------- .../ExpandCollapse/ExpandCollapse.jsx | 67 +++++ src/components/ExpandCollapse/index.js | 3 + src/components/Search/Search.jsx | 126 +++++++++ src/components/Search/index.js | 3 + src/components/Sort/Sort.jsx | 130 +++++++++ src/components/Sort/index.js | 3 + .../components/OrganizationAccessList.jsx | 217 +++++++------- 12 files changed, 801 insertions(+), 334 deletions(-) create mode 100644 __tests__/components/ExpandCollapse.test.jsx create mode 100644 __tests__/components/Search.test.jsx create mode 100644 __tests__/components/Sort.test.jsx create mode 100644 src/components/ExpandCollapse/ExpandCollapse.jsx create mode 100644 src/components/ExpandCollapse/index.js create mode 100644 src/components/Search/Search.jsx create mode 100644 src/components/Search/index.js create mode 100644 src/components/Sort/Sort.jsx create mode 100644 src/components/Sort/index.js diff --git a/__tests__/components/ExpandCollapse.test.jsx b/__tests__/components/ExpandCollapse.test.jsx new file mode 100644 index 0000000000..f2bf3e27aa --- /dev/null +++ b/__tests__/components/ExpandCollapse.test.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@lingui/react'; +import ExpandCollapse from '../../src/components/ExpandCollapse'; + +describe('', () => { + const onCompact = jest.fn(); + const onExpand = jest.fn(); + const isCompact = false; + test('initially renders without crashing', () => { + const wrapper = mount( + + + + ); + expect(wrapper.length).toBe(1); + wrapper.unmount(); + }); +}); diff --git a/__tests__/components/Search.test.jsx b/__tests__/components/Search.test.jsx new file mode 100644 index 0000000000..acb3722381 --- /dev/null +++ b/__tests__/components/Search.test.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@lingui/react'; +import Search from '../../src/components/Search'; + +describe('', () => { + let search; + + afterEach(() => { + if (search) { + search.unmount(); + search = null; + } + }); + + test('it triggers the expected callbacks', () => { + const columns = [{ name: 'Name', key: 'name', isSortable: true }]; + + const searchBtn = 'button[aria-label="Search"]'; + const searchTextInput = 'input[aria-label="Search text input"]'; + + const onSearch = jest.fn(); + + search = mount( + + + + ); + + search.find(searchTextInput).instance().value = 'test-321'; + search.find(searchTextInput).simulate('change'); + search.find(searchBtn).simulate('click'); + + expect(onSearch).toHaveBeenCalledTimes(1); + expect(onSearch).toBeCalledWith('test-321'); + }); + + test('onSearchDropdownToggle properly updates state', async () => { + const columns = [{ name: 'Name', key: 'name', isSortable: true }]; + const onSearch = jest.fn(); + const wrapper = mount( + + + + ).find('Search'); + expect(wrapper.state('isSearchDropdownOpen')).toEqual(false); + wrapper.instance().onSearchDropdownToggle(true); + expect(wrapper.state('isSearchDropdownOpen')).toEqual(true); + }); + + test('onSearchDropdownSelect properly updates state', async () => { + const columns = [ + { name: 'Name', key: 'name', isSortable: true }, + { name: 'Description', key: 'description', isSortable: true } + ]; + const onSearch = jest.fn(); + const wrapper = mount( + + + + ).find('Search'); + expect(wrapper.state('searchKey')).toEqual('name'); + wrapper.instance().onSearchDropdownSelect({ target: { innerText: 'Description' } }); + expect(wrapper.state('searchKey')).toEqual('description'); + }); +}); diff --git a/__tests__/components/Sort.test.jsx b/__tests__/components/Sort.test.jsx new file mode 100644 index 0000000000..b58e368118 --- /dev/null +++ b/__tests__/components/Sort.test.jsx @@ -0,0 +1,208 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@lingui/react'; +import Sort from '../../src/components/Sort'; + +describe('', () => { + let sort; + + afterEach(() => { + if (sort) { + sort.unmount(); + sort = null; + } + }); + + test('it triggers the expected callbacks', () => { + const columns = [{ name: 'Name', key: 'name', isSortable: true }]; + + const sortBtn = 'button[aria-label="Sort"]'; + + const onSort = jest.fn(); + + const wrapper = mount( + + + + ).find('Sort'); + + wrapper.find(sortBtn).simulate('click'); + + expect(onSort).toHaveBeenCalledTimes(1); + expect(onSort).toBeCalledWith('name', 'descending'); + }); + + test('onSort properly passes back descending when ascending was passed as prop', () => { + const multipleColumns = [ + { name: 'Foo', key: 'foo', isSortable: true }, + { name: 'Bar', key: 'bar', isSortable: true }, + { name: 'Bakery', key: 'bakery', isSortable: true }, + { name: 'Baz', key: 'baz' } + ]; + + const onSort = jest.fn(); + + const wrapper = mount( + + + + ).find('Sort'); + const sortDropdownToggle = wrapper.find('Button'); + expect(sortDropdownToggle.length).toBe(1); + sortDropdownToggle.simulate('click'); + expect(onSort).toHaveBeenCalledWith('foo', 'descending'); + }); + + test('onSort properly passes back ascending when descending was passed as prop', () => { + const multipleColumns = [ + { name: 'Foo', key: 'foo', isSortable: true }, + { name: 'Bar', key: 'bar', isSortable: true }, + { name: 'Bakery', key: 'bakery', isSortable: true }, + { name: 'Baz', key: 'baz' } + ]; + + const onSort = jest.fn(); + + const wrapper = mount( + + + + ).find('Sort'); + const sortDropdownToggle = wrapper.find('Button'); + expect(sortDropdownToggle.length).toBe(1); + sortDropdownToggle.simulate('click'); + expect(onSort).toHaveBeenCalledWith('foo', 'ascending'); + }); + + test('Changing dropdown correctly passes back new sort key', () => { + const multipleColumns = [ + { name: 'Foo', key: 'foo', isSortable: true }, + { name: 'Bar', key: 'bar', isSortable: true }, + { name: 'Bakery', key: 'bakery', isSortable: true }, + { name: 'Baz', key: 'baz' } + ]; + + const onSort = jest.fn(); + + const wrapper = mount( + + + + ).find('Sort'); + + wrapper.instance().onSortDropdownSelect({ target: { innerText: 'Bar' } }); + expect(onSort).toBeCalledWith('bar', 'ascending'); + }); + + test('Opening dropdown correctly updates state', () => { + const multipleColumns = [ + { name: 'Foo', key: 'foo', isSortable: true }, + { name: 'Bar', key: 'bar', isSortable: true }, + { name: 'Bakery', key: 'bakery', isSortable: true }, + { name: 'Baz', key: 'baz' } + ]; + + const onSort = jest.fn(); + + const wrapper = mount( + + + + ).find('Sort'); + expect(wrapper.state('isSortDropdownOpen')).toEqual(false); + wrapper.instance().onSortDropdownToggle(true); + expect(wrapper.state('isSortDropdownOpen')).toEqual(true); + }); + + test('It displays correct sort icon', () => { + const downNumericIconSelector = 'SortNumericDownIcon'; + const upNumericIconSelector = 'SortNumericUpIcon'; + const downAlphaIconSelector = 'SortAlphaDownIcon'; + const upAlphaIconSelector = 'SortAlphaUpIcon'; + + const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }]; + const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }]; + const onSort = jest.fn(); + + sort = mount( + + + + ); + + const downNumericIcon = sort.find(downNumericIconSelector); + expect(downNumericIcon.length).toBe(1); + + sort = mount( + + + + ); + + const upNumericIcon = sort.find(upNumericIconSelector); + expect(upNumericIcon.length).toBe(1); + + sort = mount( + + + + ); + + const downAlphaIcon = sort.find(downAlphaIconSelector); + expect(downAlphaIcon.length).toBe(1); + + sort = mount( + + + + ); + + const upAlphaIcon = sort.find(upAlphaIconSelector); + expect(upAlphaIcon.length).toBe(1); + }); +}); diff --git a/package-lock.json b/package-lock.json index 5502f818c4..22c3270b8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1647,7 +1647,7 @@ }, "ansi-colors": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "requires": { "ansi-wrap": "^0.1.0" @@ -2557,12 +2557,12 @@ }, "babel-plugin-syntax-class-properties": { "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" }, "babel-plugin-syntax-flow": { "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=" }, "babel-plugin-syntax-jsx": { @@ -4701,7 +4701,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -5726,7 +5726,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -9254,7 +9254,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=" }, "kleur": { diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx index efa66c1a70..00f3381458 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -1,29 +1,18 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { I18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Button, Checkbox, - Dropdown, - DropdownPosition, - DropdownToggle, - DropdownItem, Level, LevelItem, - TextInput, Toolbar, ToolbarGroup, ToolbarItem, Tooltip, } from '@patternfly/react-core'; import { - BarsIcon, - EqualsIcon, - SortAlphaDownIcon, - SortAlphaUpIcon, - SortNumericDownIcon, - SortNumericUpIcon, TrashAltIcon, PlusIcon, } from '@patternfly/react-icons'; @@ -31,97 +20,13 @@ import { Link } from 'react-router-dom'; +import ExpandCollapse from '../ExpandCollapse'; +import Search from '../Search'; +import Sort from '../Sort'; import VerticalSeparator from '../VerticalSeparator'; -const flexGrowStyling = { - flexGrow: '1' -}; - -const ToolbarActiveStyle = { - backgroundColor: '#007bba', - color: 'white', - padding: '0 5px', -}; - class DataListToolbar extends React.Component { - constructor (props) { - super(props); - - const { sortedColumnKey } = this.props; - this.state = { - isSearchDropdownOpen: false, - isSortDropdownOpen: false, - searchKey: sortedColumnKey, - searchValue: '', - }; - - this.handleSearchInputChange = this.handleSearchInputChange.bind(this); - this.onSortDropdownToggle = this.onSortDropdownToggle.bind(this); - this.onSortDropdownSelect = this.onSortDropdownSelect.bind(this); - this.onSearchDropdownToggle = this.onSearchDropdownToggle.bind(this); - this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this); - this.onSearch = this.onSearch.bind(this); - this.onSort = this.onSort.bind(this); - this.onExpand = this.onExpand.bind(this); - this.onCompact = this.onCompact.bind(this); - } - - onExpand () { - const { onExpand } = this.props; - onExpand(); - } - - onCompact () { - const { onCompact } = this.props; - onCompact(); - } - - onSortDropdownToggle (isSortDropdownOpen) { - this.setState({ isSortDropdownOpen }); - } - - onSortDropdownSelect ({ target }) { - const { columns, onSort, sortOrder } = this.props; - const { innerText } = target; - - const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText); - - this.setState({ isSortDropdownOpen: false }); - onSort(searchKey, sortOrder); - } - - onSearchDropdownToggle (isSearchDropdownOpen) { - this.setState({ isSearchDropdownOpen }); - } - - onSearchDropdownSelect ({ target }) { - const { columns } = this.props; - const { innerText } = target; - - const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText); - this.setState({ isSearchDropdownOpen: false, searchKey }); - } - - onSearch () { - const { searchValue } = this.state; - const { onSearch } = this.props; - - onSearch(searchValue); - } - - onSort () { - const { onSort, sortedColumnKey, sortOrder } = this.props; - const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending'; - - onSort(sortedColumnKey, newSortOrder); - } - - handleSearchInputChange (searchValue) { - this.setState({ searchValue }); - } - render () { - const { up } = DropdownPosition; const { columns, isAllSelected, @@ -129,45 +34,18 @@ class DataListToolbar extends React.Component { sortedColumnKey, sortOrder, addUrl, - showExpandCollapse, showDelete, showSelectAll, isLookup, isCompact, + onSort, + onSearch, + onCompact, + onExpand, + add } = this.props; - const { - isSearchDropdownOpen, - isSortDropdownOpen, - searchKey, - searchValue, - } = this.state; - const [{ name: searchColumnName }] = columns.filter(({ key }) => key === searchKey); - const [{ name: sortedColumnName, isNumeric }] = columns - .filter(({ key }) => key === sortedColumnKey); - - const searchDropdownItems = columns - .filter(({ key }) => key !== searchKey) - .map(({ key, name }) => ( - - {name} - - )); - - const sortDropdownItems = columns - .filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey) - .map(({ key, name }) => ( - - {name} - - )); - - let SortIcon; - if (isNumeric) { - SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon; - } else { - SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon; - } + const showExpandCollapse = (onCompact && onExpand); return ( @@ -189,102 +67,44 @@ class DataListToolbar extends React.Component { )} - - -
- - {searchColumnName} - - )} - dropdownItems={searchDropdownItems} - /> - -