Merge pull request #134 from mabashian/toolbar-refactor-v2

Refactor of DataListToolbar v2
This commit is contained in:
Michael Abashian 2019-03-19 14:16:04 -04:00 committed by GitHub
commit c288c5fcbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 801 additions and 334 deletions

View File

@ -0,0 +1,23 @@
import React from 'react';
import { mount } from 'enzyme';
import { I18nProvider } from '@lingui/react';
import ExpandCollapse from '../../src/components/ExpandCollapse';
describe('<ExpandCollapse />', () => {
const onCompact = jest.fn();
const onExpand = jest.fn();
const isCompact = false;
test('initially renders without crashing', () => {
const wrapper = mount(
<I18nProvider>
<ExpandCollapse
onCompact={onCompact}
onExpand={onExpand}
isCompact={isCompact}
/>
</I18nProvider>
);
expect(wrapper.length).toBe(1);
wrapper.unmount();
});
});

View File

@ -0,0 +1,78 @@
import React from 'react';
import { mount } from 'enzyme';
import { I18nProvider } from '@lingui/react';
import Search from '../../src/components/Search';
describe('<Search />', () => {
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(
<I18nProvider>
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
</I18nProvider>
);
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('handleDropdownToggle properly updates state', async () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
const onSearch = jest.fn();
const wrapper = mount(
<I18nProvider>
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
</I18nProvider>
).find('Search');
expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
wrapper.instance().handleDropdownToggle(true);
expect(wrapper.state('isSearchDropdownOpen')).toEqual(true);
});
test('handleDropdownSelect 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(
<I18nProvider>
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
</I18nProvider>
).find('Search');
expect(wrapper.state('searchKey')).toEqual('name');
wrapper.instance().handleDropdownSelect({ target: { innerText: 'Description' } });
expect(wrapper.state('searchKey')).toEqual('description');
});
});

View File

@ -0,0 +1,208 @@
import React from 'react';
import { mount } from 'enzyme';
import { I18nProvider } from '@lingui/react';
import Sort from '../../src/components/Sort';
describe('<Sort />', () => {
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(
<I18nProvider>
<Sort
sortedColumnKey="name"
sortOrder="ascending"
columns={columns}
onSort={onSort}
/>
</I18nProvider>
).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(
<I18nProvider>
<Sort
sortedColumnKey="foo"
sortOrder="ascending"
columns={multipleColumns}
onSort={onSort}
/>
</I18nProvider>
).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(
<I18nProvider>
<Sort
sortedColumnKey="foo"
sortOrder="descending"
columns={multipleColumns}
onSort={onSort}
/>
</I18nProvider>
).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(
<I18nProvider>
<Sort
sortedColumnKey="foo"
sortOrder="ascending"
columns={multipleColumns}
onSort={onSort}
/>
</I18nProvider>
).find('Sort');
wrapper.instance().handleDropdownSelect({ 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(
<I18nProvider>
<Sort
sortedColumnKey="foo"
sortOrder="ascending"
columns={multipleColumns}
onSort={onSort}
/>
</I18nProvider>
).find('Sort');
expect(wrapper.state('isSortDropdownOpen')).toEqual(false);
wrapper.instance().handleDropdownToggle(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(
<I18nProvider>
<Sort
sortedColumnKey="id"
sortOrder="descending"
columns={numericColumns}
onSort={onSort}
/>
</I18nProvider>
);
const downNumericIcon = sort.find(downNumericIconSelector);
expect(downNumericIcon.length).toBe(1);
sort = mount(
<I18nProvider>
<Sort
sortedColumnKey="id"
sortOrder="ascending"
columns={numericColumns}
onSort={onSort}
/>
</I18nProvider>
);
const upNumericIcon = sort.find(upNumericIconSelector);
expect(upNumericIcon.length).toBe(1);
sort = mount(
<I18nProvider>
<Sort
sortedColumnKey="name"
sortOrder="descending"
columns={alphaColumns}
onSort={onSort}
/>
</I18nProvider>
);
const downAlphaIcon = sort.find(downAlphaIconSelector);
expect(downAlphaIcon.length).toBe(1);
sort = mount(
<I18nProvider>
<Sort
sortedColumnKey="name"
sortOrder="ascending"
columns={alphaColumns}
onSort={onSort}
/>
</I18nProvider>
);
const upAlphaIcon = sort.find(upAlphaIconSelector);
expect(upAlphaIcon.length).toBe(1);
});
});

12
package-lock.json generated
View File

@ -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": {

View File

@ -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 }) => (
<DropdownItem key={key} component="button">
{name}
</DropdownItem>
));
const sortDropdownItems = columns
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
.map(({ key, name }) => (
<DropdownItem key={key} component="button">
{name}
</DropdownItem>
));
let SortIcon;
if (isNumeric) {
SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
} else {
SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
}
const showExpandCollapse = (onCompact && onExpand);
return (
<I18n>
@ -189,102 +67,44 @@ class DataListToolbar extends React.Component {
<VerticalSeparator />
</ToolbarGroup>
)}
<ToolbarGroup style={flexGrowStyling}>
<ToolbarItem style={flexGrowStyling}>
<div className="pf-c-input-group">
<Dropdown
className="searchKeyDropdown"
onToggle={this.onSearchDropdownToggle}
onSelect={this.onSearchDropdownSelect}
direction={up}
isOpen={isSearchDropdownOpen}
toggle={(
<DropdownToggle
onToggle={this.onSearchDropdownToggle}
>
{searchColumnName}
</DropdownToggle>
)}
dropdownItems={searchDropdownItems}
/>
<TextInput
type="search"
aria-label={i18n._(t`Search text input`)}
value={searchValue}
onChange={this.handleSearchInputChange}
style={{ height: '30px' }}
/>
<Button
variant="tertiary"
aria-label={i18n._(t`Search`)}
onClick={this.onSearch}
>
<i className="fas fa-search" aria-hidden="true" />
</Button>
</div>
<ToolbarGroup style={{ flexGrow: '1' }}>
<ToolbarItem style={{ flexGrow: '1' }}>
<Search
columns={columns}
onSearch={onSearch}
sortedColumnKey={sortedColumnKey}
/>
</ToolbarItem>
<VerticalSeparator />
</ToolbarGroup>
<ToolbarGroup
className="sortDropdownGroup"
>
{ sortDropdownItems.length > 1 && (
<ToolbarItem>
<Dropdown
onToggle={this.onSortDropdownToggle}
onSelect={this.onSortDropdownSelect}
direction={up}
isOpen={isSortDropdownOpen}
toggle={(
<DropdownToggle
onToggle={this.onSortDropdownToggle}
>
{sortedColumnName}
</DropdownToggle>
)}
dropdownItems={sortDropdownItems}
/>
</ToolbarItem>
)}
<ToolbarItem>
<Button
onClick={this.onSort}
variant="plain"
aria-label={i18n._(t`Sort`)}
>
<SortIcon />
</Button>
<Sort
columns={columns}
onSort={onSort}
sortOrder={sortOrder}
sortedColumnKey={sortedColumnKey}
/>
</ToolbarItem>
{ (showExpandCollapse || showDelete || addUrl) && (
<VerticalSeparator />
)}
</ToolbarGroup>
{ (showExpandCollapse || showDelete || addUrl || add) && (
<VerticalSeparator />
)}
{showExpandCollapse && (
<ToolbarGroup>
<ToolbarItem>
<Button
variant="plain"
aria-label={i18n._(t`Collapse`)}
onClick={this.onCompact}
style={isCompact ? ToolbarActiveStyle : null}
>
<BarsIcon />
</Button>
</ToolbarItem>
<ToolbarItem>
<Button
variant="plain"
aria-label={i18n._(t`Expand`)}
onClick={this.onExpand}
style={!isCompact ? ToolbarActiveStyle : null}
>
<EqualsIcon />
</Button>
</ToolbarItem>
{ (showDelete || addUrl) && (
<Fragment>
<ToolbarGroup>
<ExpandCollapse
isCompact={isCompact}
onCompact={onCompact}
onExpand={onExpand}
/>
</ToolbarGroup>
{ (showDelete || addUrl || add) && (
<VerticalSeparator />
)}
</ToolbarGroup>
</Fragment>
)}
</Toolbar>
</LevelItem>
@ -312,6 +132,9 @@ class DataListToolbar extends React.Component {
</Button>
</Link>
)}
{add && (
<Fragment>{add}</Fragment>
)}
</LevelItem>
</Level>
</div>
@ -329,10 +152,13 @@ DataListToolbar.propTypes = {
onSelectAll: PropTypes.func,
onSort: PropTypes.func,
showDelete: PropTypes.bool,
showExpandCollapse: PropTypes.bool,
showSelectAll: PropTypes.bool,
sortOrder: PropTypes.string,
sortedColumnKey: PropTypes.string,
onCompact: PropTypes.func,
onExpand: PropTypes.func,
isCompact: PropTypes.bool,
add: PropTypes.node
};
DataListToolbar.defaultProps = {
@ -341,11 +167,14 @@ DataListToolbar.defaultProps = {
onSelectAll: null,
onSort: null,
showDelete: false,
showExpandCollapse: false,
showSelectAll: false,
sortOrder: 'ascending',
sortedColumnKey: 'name',
isAllSelected: false,
onCompact: null,
onExpand: null,
isCompact: false,
add: null
};
export default DataListToolbar;

View File

@ -0,0 +1,67 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
ToolbarItem
} from '@patternfly/react-core';
import {
BarsIcon,
EqualsIcon,
} from '@patternfly/react-icons';
const ToolbarActiveStyle = {
backgroundColor: '#007bba',
color: 'white',
padding: '0 5px',
};
class ExpandCollapse extends React.Component {
render () {
const {
onCompact,
onExpand,
isCompact
} = this.props;
return (
<I18n>
{({ i18n }) => (
<Fragment>
<ToolbarItem>
<Button
variant="plain"
aria-label={i18n._(t`Collapse`)}
onClick={onCompact}
style={isCompact ? ToolbarActiveStyle : null}
>
<BarsIcon />
</Button>
</ToolbarItem>
<ToolbarItem>
<Button
variant="plain"
aria-label={i18n._(t`Expand`)}
onClick={onExpand}
style={!isCompact ? ToolbarActiveStyle : null}
>
<EqualsIcon />
</Button>
</ToolbarItem>
</Fragment>
)}
</I18n>
);
}
}
ExpandCollapse.propTypes = {
onCompact: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
isCompact: PropTypes.bool.isRequired
};
ExpandCollapse.defaultProps = {};
export default ExpandCollapse;

View File

@ -0,0 +1,3 @@
import ExpandCollapse from './ExpandCollapse';
export default ExpandCollapse;

View File

@ -0,0 +1,126 @@
import React from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
Dropdown,
DropdownPosition,
DropdownToggle,
DropdownItem,
TextInput
} from '@patternfly/react-core';
class Search extends React.Component {
constructor (props) {
super(props);
const { sortedColumnKey } = this.props;
this.state = {
isSearchDropdownOpen: false,
searchKey: sortedColumnKey,
searchValue: '',
};
this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
this.handleDropdownSelect = this.handleDropdownSelect.bind(this);
this.handleSearch = this.handleSearch.bind(this);
}
handleDropdownToggle (isSearchDropdownOpen) {
this.setState({ isSearchDropdownOpen });
}
handleDropdownSelect ({ target }) {
const { columns } = this.props;
const { innerText } = target;
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
this.setState({ isSearchDropdownOpen: false, searchKey });
}
handleSearch () {
const { searchValue } = this.state;
const { onSearch } = this.props;
onSearch(searchValue);
}
handleSearchInputChange (searchValue) {
this.setState({ searchValue });
}
render () {
const { up } = DropdownPosition;
const {
columns
} = this.props;
const {
isSearchDropdownOpen,
searchKey,
searchValue,
} = this.state;
const [{ name: searchColumnName }] = columns.filter(({ key }) => key === searchKey);
const searchDropdownItems = columns
.filter(({ key }) => key !== searchKey)
.map(({ key, name }) => (
<DropdownItem key={key} component="button">
{name}
</DropdownItem>
));
return (
<I18n>
{({ i18n }) => (
<div className="pf-c-input-group">
<Dropdown
className="searchKeyDropdown"
onToggle={this.handleDropdownToggle}
onSelect={this.handleDropdownSelect}
direction={up}
isOpen={isSearchDropdownOpen}
toggle={(
<DropdownToggle
onToggle={this.handleDropdownToggle}
>
{searchColumnName}
</DropdownToggle>
)}
dropdownItems={searchDropdownItems}
/>
<TextInput
type="search"
aria-label="Search text input"
value={searchValue}
onChange={this.handleSearchInputChange}
style={{ height: '30px' }}
/>
<Button
variant="tertiary"
aria-label={i18n._(t`Search`)}
onClick={this.handleSearch}
>
<i className="fas fa-search" aria-hidden="true" />
</Button>
</div>
)}
</I18n>
);
}
}
Search.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSearch: PropTypes.func,
sortedColumnKey: PropTypes.string,
};
Search.defaultProps = {
onSearch: null,
sortedColumnKey: 'name'
};
export default Search;

View File

@ -0,0 +1,3 @@
import Search from './Search';
export default Search;

View File

@ -0,0 +1,130 @@
import React from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
Dropdown,
DropdownPosition,
DropdownToggle,
DropdownItem
} from '@patternfly/react-core';
import {
SortAlphaDownIcon,
SortAlphaUpIcon,
SortNumericDownIcon,
SortNumericUpIcon
} from '@patternfly/react-icons';
class Sort extends React.Component {
constructor (props) {
super(props);
this.state = {
isSortDropdownOpen: false,
};
this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
this.handleDropdownSelect = this.handleDropdownSelect.bind(this);
this.handleSort = this.handleSort.bind(this);
}
handleDropdownToggle (isSortDropdownOpen) {
this.setState({ isSortDropdownOpen });
}
handleDropdownSelect ({ 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);
}
handleSort () {
const { onSort, sortedColumnKey, sortOrder } = this.props;
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
onSort(sortedColumnKey, newSortOrder);
}
render () {
const { up } = DropdownPosition;
const {
columns,
sortedColumnKey,
sortOrder
} = this.props;
const {
isSortDropdownOpen
} = this.state;
const [{ name: sortedColumnName, isNumeric }] = columns
.filter(({ key }) => key === sortedColumnKey);
const sortDropdownItems = columns
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
.map(({ key, name }) => (
<DropdownItem key={key} component="button">
{name}
</DropdownItem>
));
let SortIcon;
if (isNumeric) {
SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
} else {
SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
}
return (
<I18n>
{({ i18n }) => (
<React.Fragment>
{ sortDropdownItems.length > 1 && (
<Dropdown
style={{ marginRight: '20px' }}
onToggle={this.handleDropdownToggle}
onSelect={this.handleDropdownSelect}
direction={up}
isOpen={isSortDropdownOpen}
toggle={(
<DropdownToggle
onToggle={this.handleDropdownToggle}
>
{sortedColumnName}
</DropdownToggle>
)}
dropdownItems={sortDropdownItems}
/>
)}
<Button
onClick={this.handleSort}
variant="plain"
aria-label={i18n._(t`Sort`)}
>
<SortIcon />
</Button>
</React.Fragment>
)}
</I18n>
);
}
}
Sort.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSort: PropTypes.func,
sortOrder: PropTypes.string,
sortedColumnKey: PropTypes.string
};
Sort.defaultProps = {
onSort: null,
sortOrder: 'ascending',
sortedColumnKey: 'name'
};
export default Sort;

View File

@ -0,0 +1,3 @@
import Sort from './Sort';
export default Sort;

View File

@ -305,125 +305,122 @@ class OrganizationAccessList extends React.Component {
showWarning
} = this.state;
return (
<Fragment>
{!error && results.length <= 0 && (
<h1>Loading...</h1> // TODO: replace with proper loading state
)}
{error && results.length <= 0 && (
<I18n>
{({ i18n }) => (
<Fragment>
<div>{error.message}</div>
{error.response && (
<div>{error.response.data.detail}</div>
{!error && results.length <= 0 && (
<h1>Loading...</h1> // TODO: replace with proper loading state
)}
</Fragment> // TODO: replace with proper error handling
)}
{results.length > 0 && (
<Fragment>
<DataListToolbar
sortedColumnKey={sortedColumnKey}
sortOrder={sortOrder}
columns={this.columns}
onSearch={() => { }}
onSort={this.onSort}
onCompact={this.onCompact}
onExpand={this.onExpand}
isCompact={isCompact}
showExpandCollapse
/>
{showWarning && (
<Alert
variant="danger"
title={warningTitle}
action={<AlertActionCloseButton onClose={this.hideWarning} />}
>
{warningMsg}
<span className="awx-c-form-action-group">
<Button variant="danger" aria-label="confirm-delete" onClick={this.confirmDelete}>Delete</Button>
<Button variant="secondary" onClick={this.hideWarning}>Cancel</Button>
</span>
</Alert>
{error && results.length <= 0 && (
<Fragment>
<div>{error.message}</div>
{error.response && (
<div>{error.response.data.detail}</div>
)}
</Fragment> // TODO: replace with proper error handling
)}
<Fragment>
<I18n>
{({ i18n }) => (
<DataList aria-label={i18n._(t`Access List`)}>
{results.map(result => (
<DataListItem aria-labelledby={i18n._(t`access-list-item`)} key={result.id}>
<DataListCell>
<UserName
value={result.username}
url={result.url}
/>
{result.first_name || result.last_name ? (
<Detail
label={i18n._(t`Name`)}
value={`${result.first_name} ${result.last_name}`}
url={null}
customStyles={isCompact ? hiddenStyle : null}
/>
) : (
null
)}
</DataListCell>
<DataListCell>
{results.length > 0 && (
<Fragment>
<DataListToolbar
sortedColumnKey={sortedColumnKey}
sortOrder={sortOrder}
columns={this.columns}
onSearch={() => { }}
onSort={this.onSort}
onCompact={this.onCompact}
onExpand={this.onExpand}
isCompact={isCompact}
showExpandCollapse
/>
{showWarning && (
<Alert
variant="danger"
title={warningTitle}
action={<AlertActionCloseButton onClose={this.hideWarning} />}
>
{warningMsg}
<span className="awx-c-form-action-group">
<Button variant="danger" aria-label="confirm-delete" onClick={this.confirmDelete}>Delete</Button>
<Button variant="secondary" onClick={this.hideWarning}>Cancel</Button>
</span>
</Alert>
)}
<DataList aria-label={i18n._(t`Access List`)}>
{results.map(result => (
<DataListItem aria-labelledby={i18n._(t`access-list-item`)} key={result.id}>
<DataListCell>
<UserName
value={result.username}
url={result.url}
/>
{result.first_name || result.last_name ? (
<Detail
label=" "
value=" "
label={i18n._(t`Name`)}
value={`${result.first_name} ${result.last_name}`}
url={null}
customStyles={isCompact ? hiddenStyle : null}
/>
{result.userRoles.length > 0 && (
<ul style={isCompact
? { ...userRolesWrapperStyle, ...hiddenStyle }
: userRolesWrapperStyle}
>
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`User Roles`)}</Text>
{result.userRoles.map(role => (
<Chip
key={role.id}
className="awx-c-chip"
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
>
{role.name}
</Chip>
))}
</ul>
)}
{result.teamRoles.length > 0 && (
<ul style={isCompact
? { ...userRolesWrapperStyle, ...hiddenStyle }
: userRolesWrapperStyle}
>
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`Team Roles`)}</Text>
{result.teamRoles.map(role => (
<Chip
key={role.id}
className="awx-c-chip"
onClick={() => this.handleWarning(role.name, role.id, role.team_name, role.team_id, 'teams')}
>
{role.name}
</Chip>
))}
</ul>
)}
</DataListCell>
</DataListItem>
))}
</DataList>
)}
</I18n>
</Fragment>
<Pagination
count={count}
page={page}
pageCount={pageCount}
page_size={page_size}
onSetPage={this.onSetPage}
/>
) : (
null
)}
</DataListCell>
<DataListCell>
<Detail
label=" "
value=" "
url={null}
customStyles={isCompact ? hiddenStyle : null}
/>
{result.userRoles.length > 0 && (
<ul style={isCompact
? { ...userRolesWrapperStyle, ...hiddenStyle }
: userRolesWrapperStyle}
>
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`User Roles`)}</Text>
{result.userRoles.map(role => (
<Chip
key={role.id}
className="awx-c-chip"
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
>
{role.name}
</Chip>
))}
</ul>
)}
{result.teamRoles.length > 0 && (
<ul style={isCompact
? { ...userRolesWrapperStyle, ...hiddenStyle }
: userRolesWrapperStyle}
>
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`Team Roles`)}</Text>
{result.teamRoles.map(role => (
<Chip
key={role.id}
className="awx-c-chip"
onClick={() => this.handleWarning(role.name, role.id, role.team_name, role.team_id, 'teams')}
>
{role.name}
</Chip>
))}
</ul>
)}
</DataListCell>
</DataListItem>
))}
</DataList>
<Pagination
count={count}
page={page}
pageCount={pageCount}
page_size={page_size}
onSetPage={this.onSetPage}
/>
</Fragment>
)}
</Fragment>
)}
</Fragment>
</I18n>
);
}
}