From 8f04026404da9d35060cfa9ecd1aa82113713b68 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 11 Aug 2020 14:20:38 -0400 Subject: [PATCH] kebabify additional controls when advanced search is displayed --- .../AddDropDownButton/AddDropDownButton.jsx | 43 +++- .../DataListToolbar/DataListToolbar.jsx | 189 ++++++++++-------- .../PaginatedDataList/ToolbarAddButton.jsx | 18 +- .../PaginatedDataList/ToolbarDeleteButton.jsx | 106 +++++----- awx/ui_next/src/components/Search/Search.jsx | 4 +- .../src/components/Search/Search.test.jsx | 42 +++- awx/ui_next/src/contexts/Kebabified.jsx | 8 + 7 files changed, 264 insertions(+), 146 deletions(-) create mode 100644 awx/ui_next/src/contexts/Kebabified.jsx diff --git a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx index 78655e44d9..3bf1963bce 100644 --- a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx +++ b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx @@ -1,25 +1,46 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, Fragment } from 'react'; import { Link } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; import PropTypes from 'prop-types'; -import { Dropdown, DropdownPosition } from '@patternfly/react-core'; +import { + Dropdown, + DropdownPosition, + DropdownItem, +} from '@patternfly/react-core'; import { ToolbarAddButton } from '../PaginatedDataList'; +import { toTitleCase } from '../../util/strings'; +import { useKebabified } from '../../contexts/Kebabified'; -function AddDropDownButton({ dropdownItems }) { +function AddDropDownButton({ dropdownItems, i18n }) { + const { isKebabified } = useKebabified(); const [isOpen, setIsOpen] = useState(false); const element = useRef(null); - const toggle = e => { - if (!element || !element.current.contains(e.target)) { - setIsOpen(false); - } - }; - useEffect(() => { + const toggle = e => { + if (!isKebabified && (!element || !element.current.contains(e.target))) { + setIsOpen(false); + } + }; + document.addEventListener('click', toggle, false); return () => { document.removeEventListener('click', toggle); }; - }, []); + }, [isKebabified]); + + if (isKebabified) { + return ( + + {dropdownItems.map(item => ( + + {toTitleCase(`${i18n._(t`Add`)} ${item.label}`)} + + ))} + + ); + } return (
@@ -52,4 +73,4 @@ AddDropDownButton.propTypes = { }; export { AddDropDownButton as _AddDropDownButton }; -export default AddDropDownButton; +export default withI18n()(AddDropDownButton); diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx index 9e02017fdf..1ddfb57df6 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -9,103 +9,128 @@ import { ToolbarGroup, ToolbarItem, ToolbarToggleGroup, + Dropdown, + KebabToggle, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import ExpandCollapse from '../ExpandCollapse'; import Search from '../Search'; import Sort from '../Sort'; - import { SearchColumns, SortColumns, QSConfig } from '../../types'; +import { KebabifiedProvider } from '../../contexts/Kebabified'; -class DataListToolbar extends React.Component { - render() { - const { - itemCount, - clearAllFilters, - searchColumns, - searchableKeys, - relatedSearchableKeys, - sortColumns, - showSelectAll, - isAllSelected, - isCompact, - onSort, - onSearch, - onReplaceSearch, - onRemove, - onCompact, - onExpand, - onSelectAll, - additionalControls, - i18n, - qsConfig, - pagination, - } = this.props; +function DataListToolbar({ + itemCount, + clearAllFilters, + searchColumns, + searchableKeys, + relatedSearchableKeys, + sortColumns, + showSelectAll, + isAllSelected, + isCompact, + onSort, + onSearch, + onReplaceSearch, + onRemove, + onCompact, + onExpand, + onSelectAll, + additionalControls, + i18n, + qsConfig, + pagination, +}) { + const showExpandCollapse = onCompact && onExpand; + const [kebabIsOpen, setKebabIsOpen] = useState(false); + const [advancedSearchShown, setAdvancedSearchShown] = useState(false); - const showExpandCollapse = onCompact && onExpand; - return ( - - - {showSelectAll && ( - - - - - - )} - } breakpoint="lg"> + const onShowAdvancedSearch = shown => { + setAdvancedSearchShown(shown); + setKebabIsOpen(false); + }; + + return ( + + + {showSelectAll && ( + - - - - - - {showExpandCollapse && ( - - - - - - - - )} + + )} + } breakpoint="lg"> + + + + + + + + {showExpandCollapse && ( + + + + + + + + )} + {advancedSearchShown && ( + + } + isOpen={kebabIsOpen} + isPlain + dropdownItems={additionalControls.map(control => { + return ( + + {control} + + ); + })} + /> + + )} + {!advancedSearchShown && ( {additionalControls.map(control => ( {control} ))} - {pagination && itemCount > 0 && ( - {pagination} - )} - - - ); - } + )} + {!advancedSearchShown && pagination && itemCount > 0 && ( + {pagination} + )} + + + ); } DataListToolbar.propTypes = { diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx index 19ae9a68c9..589a09a64e 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx @@ -1,16 +1,32 @@ import React from 'react'; import { string, func } from 'prop-types'; import { Link } from 'react-router-dom'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { useKebabified } from '../../contexts/Kebabified'; function ToolbarAddButton({ linkTo, onClick, i18n, isDisabled }) { + const { isKebabified } = useKebabified(); + if (!linkTo && !onClick) { throw new Error( 'ToolbarAddButton requires either `linkTo` or `onClick` prop' ); } + if (isKebabified) { + return ( + + {i18n._(t`Add`)} + + ); + } if (linkTo) { return ( diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx index 2d5625a95c..7be476dfc1 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx @@ -8,10 +8,11 @@ import { shape, checkPropTypes, } from 'prop-types'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import AlertModal from '../AlertModal'; +import { Kebabified } from '../../contexts/Kebabified'; const requireNameOrUsername = props => { const { name, username } = props; @@ -138,54 +139,69 @@ class ToolbarDeleteButton extends React.Component { // we can delete the extra
around the below. // See: https://github.com/patternfly/patternfly-react/issues/1894 return ( - - -
- -
-
- {isModalOpen && ( - + {({ isKebabified }) => ( + + {isKebabified ? ( + {i18n._(t`Delete`)} - , - +
+
+ )} + {isModalOpen && ( + + {i18n._(t`Delete`)} + , + , + ]} > - {i18n._(t`Cancel`)} - , - ]} - > -
{i18n._(t`This action will delete the following:`)}
- {itemsToDelete.map(item => ( - - {item.name || item.username} -
-
- ))} -
+
{i18n._(t`This action will delete the following:`)}
+ {itemsToDelete.map(item => ( + + {item.name || item.username} +
+
+ ))} + + )} + )} - + ); } } diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx index 8049a326e7..e92f5c2d16 100644 --- a/awx/ui_next/src/components/Search/Search.jsx +++ b/awx/ui_next/src/components/Search/Search.jsx @@ -40,6 +40,7 @@ function Search({ location, searchableKeys, relatedSearchableKeys, + onShowAdvancedSearch, }) { const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false); const [searchKey, setSearchKey] = useState( @@ -62,7 +63,7 @@ function Search({ const { key: actualSearchKey } = columns.find( ({ name }) => name === target.innerText ); - + onShowAdvancedSearch(actualSearchKey === 'advanced'); setIsFilterDropdownOpen(false); setSearchKey(actualSearchKey); }; @@ -301,6 +302,7 @@ Search.propTypes = { columns: SearchColumns.isRequired, onSearch: PropTypes.func, onRemove: PropTypes.func, + onShowAdvancedSearch: PropTypes.func.isRequired, }; Search.defaultProps = { diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index a34b6bcc09..e896fcdb37 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -36,7 +36,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -64,7 +69,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -95,7 +105,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -119,7 +134,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -150,7 +170,11 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + , { context: { router: { history } } } @@ -197,6 +221,7 @@ describe('', () => { qsConfig={qsConfigNew} columns={columns} onRemove={onRemove} + onShowAdvancedSearch={jest.fn} /> , @@ -243,6 +268,7 @@ describe('', () => { qsConfig={qsConfigNew} columns={columns} onRemove={onRemove} + onShowAdvancedSearch={jest.fn} /> , @@ -277,7 +303,11 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + , { context: { router: { history } } } diff --git a/awx/ui_next/src/contexts/Kebabified.jsx b/awx/ui_next/src/contexts/Kebabified.jsx new file mode 100644 index 0000000000..34007631b0 --- /dev/null +++ b/awx/ui_next/src/contexts/Kebabified.jsx @@ -0,0 +1,8 @@ +import React, { useContext } from 'react'; + +// eslint-disable-next-line import/prefer-default-export +export const KebabifiedContext = React.createContext({}); + +export const KebabifiedProvider = KebabifiedContext.Provider; +export const Kebabified = KebabifiedContext.Consumer; +export const useKebabified = () => useContext(KebabifiedContext);