diff --git a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx
index 78655e44d9..ca4c4a40b6 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 { useKebabifiedMenu } from '../../contexts/Kebabified';
-function AddDropDownButton({ dropdownItems }) {
+function AddDropDownButton({ dropdownItems, i18n }) {
+ const { isKebabified } = useKebabifiedMenu();
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..4c5c295976 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 { useKebabifiedMenu } from '../../contexts/Kebabified';
function ToolbarAddButton({ linkTo, onClick, i18n, isDisabled }) {
+ const { isKebabified } = useKebabifiedMenu();
+
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`)}
- ,
- ,
+
+ {i18n._(t`Cancel`)}
+ ,
+ ]}
>
- {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..6c1badfa56 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"
>
-
+
);
@@ -83,6 +93,50 @@ describe('', () => {
expect(onSearch).toBeCalledWith('description__icontains', 'test-321');
});
+ test('changing key select to and from advanced causes onShowAdvancedSearch callback to be invoked', () => {
+ const columns = [
+ { name: 'Name', key: 'name__icontains', isDefault: true },
+ { name: 'Description', key: 'description__icontains' },
+ { name: 'Advanced', key: 'advanced' },
+ ];
+ const onSearch = jest.fn();
+ const onShowAdvancedSearch = jest.fn();
+ const wrapper = mountWithContexts(
+ {}}
+ collapseListedFiltersBreakpoint="lg"
+ >
+
+
+
+
+ );
+
+ act(() => {
+ wrapper
+ .find('Select[aria-label="Simple key select"]')
+ .invoke('onSelect')({ target: { innerText: 'Advanced' } });
+ });
+ wrapper.update();
+ expect(onShowAdvancedSearch).toHaveBeenCalledTimes(1);
+ expect(onShowAdvancedSearch).toBeCalledWith(true);
+ jest.clearAllMocks();
+ act(() => {
+ wrapper
+ .find('Select[aria-label="Simple key select"]')
+ .invoke('onSelect')({ target: { innerText: 'Description' } });
+ });
+ wrapper.update();
+ expect(onShowAdvancedSearch).toHaveBeenCalledTimes(1);
+ expect(onShowAdvancedSearch).toBeCalledWith(false);
+ });
+
test('attempt to search with empty string', () => {
const searchButton = 'button[aria-label="Search submit button"]';
const searchTextInput = 'input[aria-label="Search text input"]';
@@ -95,7 +149,12 @@ describe('', () => {
collapseListedFiltersBreakpoint="lg"
>
-
+
);
@@ -119,7 +178,12 @@ describe('', () => {
collapseListedFiltersBreakpoint="lg"
>
-
+
);
@@ -150,7 +214,11 @@ describe('', () => {
collapseListedFiltersBreakpoint="lg"
>
-
+
,
{ context: { router: { history } } }
@@ -197,6 +265,7 @@ describe('', () => {
qsConfig={qsConfigNew}
columns={columns}
onRemove={onRemove}
+ onShowAdvancedSearch={jest.fn}
/>
,
@@ -243,6 +312,7 @@ describe('', () => {
qsConfig={qsConfigNew}
columns={columns}
onRemove={onRemove}
+ onShowAdvancedSearch={jest.fn}
/>
,
@@ -277,7 +347,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..c50431c73f
--- /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 useKebabifiedMenu = () => useContext(KebabifiedContext);