diff --git a/package.json b/package.json
index 976e34c5a98..98a59097288 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
},
"dependencies": {
"@module-federation/utilities": "^1.7.0",
- "@theforeman/vendor": "^13.0.1",
+ "@theforeman/vendor": "^13.1.0",
"graphql-tag": "^2.11.0",
"intl": "~1.2.5",
"jed": "^1.1.1",
@@ -30,11 +30,11 @@
},
"devDependencies": {
"@babel/core": "^7.7.0",
- "@theforeman/builder": "^13.0.1",
- "@theforeman/eslint-plugin-foreman": "^13.0.1",
- "@theforeman/eslint-plugin-rules": "^13.0.1",
- "@theforeman/test": "^13.0.1",
- "@theforeman/vendor-dev": "^13.0.1",
+ "@theforeman/builder": "^13.1.0",
+ "@theforeman/eslint-plugin-foreman": "^13.1.0",
+ "@theforeman/eslint-plugin-rules": "^13.1.0",
+ "@theforeman/test": "^13.1.0",
+ "@theforeman/vendor-dev": "^13.1.0",
"@types/jest": "<27.0.0",
"argv-parse": "^1.0.1",
"babel-eslint": "^10.0.0",
diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/RowSelectTd.js b/webpack/assets/javascripts/react_app/components/HostsIndex/RowSelectTd.js
index 8fff3593e6b..2bd64346dd1 100644
--- a/webpack/assets/javascripts/react_app/components/HostsIndex/RowSelectTd.js
+++ b/webpack/assets/javascripts/react_app/components/HostsIndex/RowSelectTd.js
@@ -2,14 +2,19 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Td } from '@patternfly/react-table';
-export const RowSelectTd = ({ rowData, selectOne, isSelected }) => (
+export const RowSelectTd = ({
+ rowData,
+ selectOne,
+ isSelected,
+ idColumnName = 'id',
+}) => (
{
- selectOne(isSelecting, rowData.id, rowData);
+ selectOne(isSelecting, rowData[idColumnName], rowData);
},
- isSelected: isSelected(rowData.id),
+ isSelected: isSelected(rowData[idColumnName]),
disable: false,
}}
/>
@@ -19,4 +24,9 @@ RowSelectTd.propTypes = {
rowData: PropTypes.object.isRequired,
selectOne: PropTypes.func.isRequired,
isSelected: PropTypes.func.isRequired,
+ idColumnName: PropTypes.string,
+};
+
+RowSelectTd.defaultProps = {
+ idColumnName: 'id',
};
diff --git a/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/Bookmarks.js b/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/Bookmarks.js
index 19dbff71f15..b720da01563 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/Bookmarks.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/Bookmarks.js
@@ -28,6 +28,7 @@ const Bookmarks = ({
setModalOpen,
setModalClosed,
searchQuery,
+ bookmarksPosition,
}) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@@ -73,6 +74,7 @@ const Bookmarks = ({
/>
setIsDropdownOpen(false)}
toggle={
@@ -107,6 +109,7 @@ Bookmarks.propTypes = {
setModalOpen: PropTypes.func.isRequired,
setModalClosed: PropTypes.func.isRequired,
searchQuery: PropTypes.string.isRequired,
+ bookmarksPosition: PropTypes.string,
};
Bookmarks.defaultProps = {
@@ -116,6 +119,7 @@ Bookmarks.defaultProps = {
status: null,
documentationUrl: '',
getBookmarks: noop,
+ bookmarksPosition: 'left',
};
export default Bookmarks;
diff --git a/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/index.js b/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/index.js
index ff3996f54e8..665231708c8 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/index.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/index.js
@@ -23,6 +23,7 @@ const ConnectedBookmarks = ({
canCreate,
documentationUrl,
searchQuery,
+ bookmarksPosition,
}) => {
const key = `${BOOKMARKS}_${controller.toUpperCase()}`;
const modalID = getBookmarksModalId(id);
@@ -53,6 +54,7 @@ const ConnectedBookmarks = ({
setModalClosed={setModalClosed}
isModalOpen={isModalOpen}
searchQuery={searchQuery}
+ bookmarksPosition={bookmarksPosition}
/>
);
};
@@ -65,6 +67,7 @@ ConnectedBookmarks.propTypes = {
canCreate: PropTypes.bool,
documentationUrl: PropTypes.string,
searchQuery: PropTypes.string,
+ bookmarksPosition: PropTypes.string,
};
ConnectedBookmarks.defaultProps = {
@@ -72,6 +75,7 @@ ConnectedBookmarks.defaultProps = {
canCreate: false,
documentationUrl: '',
searchQuery: '',
+ bookmarksPosition: 'left',
};
export const reducers = { bookmarksPF4: reducer };
diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js
index 4954550162c..f93621b9808 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js
@@ -23,6 +23,8 @@ export const Table = ({
getActions,
isDeleteable,
itemCount,
+ selectOne,
+ isSelected,
params,
refreshData,
results,
@@ -32,8 +34,21 @@ export const Table = ({
isEmbedded,
showCheckboxes,
rowSelectTd,
+ idColumn,
children,
+ bottomPagination,
}) => {
+ if (!bottomPagination)
+ bottomPagination = (
+
+ );
const columnsToSortParams = {};
Object.keys(columns).forEach(key => {
if (columns[key].isSorted) {
@@ -129,7 +144,14 @@ export const Table = ({
const rowActions = actions(result);
return (
- {showCheckboxes && }
+ {showCheckboxes && (
+
+ )}
{columnNamesKeys.map(k => (
{columns[k].wrapper
@@ -147,15 +169,7 @@ export const Table = ({
})}
- {results.length > 0 && !errorMessage && (
-
- )}
+ {results.length > 0 && !errorMessage && bottomPagination}
>
);
};
@@ -179,7 +193,11 @@ Table.propTypes = {
isPending: PropTypes.bool.isRequired,
isEmbedded: PropTypes.bool,
rowSelectTd: PropTypes.func,
+ idColumn: PropTypes.string,
+ selectOne: PropTypes.func,
+ isSelected: PropTypes.func,
showCheckboxes: PropTypes.bool,
+ bottomPagination: PropTypes.node,
};
Table.defaultProps = {
@@ -191,5 +209,9 @@ Table.defaultProps = {
results: [],
isEmbedded: false,
rowSelectTd: noop,
+ idColumn: 'id',
+ selectOne: noop,
+ isSelected: noop,
showCheckboxes: false,
+ bottomPagination: null,
};
diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableHooks.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableHooks.js
index 799c2589336..74f0541d94c 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableHooks.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableHooks.js
@@ -179,11 +179,12 @@ export const useBulkSelect = ({
idColumn,
isSelectable,
});
+ const [hasInteracted, setHasInteracted] = useState(false);
const exclusionSet = useSet(initialExclusionArry);
const [searchQuery, updateSearchQuery] = useState(initialSearchQuery);
const [selectAllMode, setSelectAllMode] = useState(initialSelectAllMode);
const selectedCount = selectAllMode
- ? Number(metadata.selectable || metadata.total) - exclusionSet.size
+ ? Number(metadata.selectable ?? metadata.total) - exclusionSet.size
: selectOptions.selectedCount;
const areAllRowsOnPageSelected = () =>
@@ -209,6 +210,7 @@ export const useBulkSelect = ({
const selectPage = () => {
setSelectAllMode(false);
selectOptions.selectPage();
+ setHasInteracted(true);
};
const selectNone = useCallback(() => {
@@ -216,6 +218,7 @@ export const useBulkSelect = ({
exclusionSet.clear();
inclusionSet.clear();
selectOptions.clearSelectedResults();
+ setHasInteracted(true);
}, [exclusionSet, inclusionSet, selectOptions]);
const selectOne = (isRowSelected, id, data) => {
@@ -228,20 +231,26 @@ export const useBulkSelect = ({
} else {
selectOptions.selectOne(isRowSelected, id, data);
}
+ setHasInteracted(true);
};
- const selectAll = checked => {
- setSelectAllMode(checked);
- if (checked) {
- exclusionSet.clear();
- } else {
- inclusionSet.clear();
- }
- };
+ const selectAll = useCallback(
+ checked => {
+ setSelectAllMode(checked);
+ if (checked) {
+ exclusionSet.clear();
+ } else {
+ inclusionSet.clear();
+ }
+ setHasInteracted(true);
+ },
+ [exclusionSet, inclusionSet]
+ );
const selectDefault = () => {
selectNone();
selectOptions.selectDefault();
+ setHasInteracted(true);
};
const fetchBulkParams = ({
@@ -301,6 +310,8 @@ export const useBulkSelect = ({
areAllRowsSelected,
inclusionSet,
exclusionSet,
+ hasInteracted,
+ setHasInteracted,
};
};
diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableIndexHooks.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableIndexHooks.js
index 2d03dedf2b9..6fb1075db3f 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableIndexHooks.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableIndexHooks.js
@@ -44,30 +44,39 @@ A hook that stores the 'params' state and returns the setParamsAndAPI and setSea
@param {Object}{apiOptions} - options object. Should include { key: HOSTS_API_KEY }; see APIRequest.js for more details
@param {Function}{setAPIOptions} - Pass in the setAPIOptions function returned from useAPI.
@param {Function}{updateSearchQuery} - Pass in the updateSearchQuery function returned from useBulkSelect.
+@param {Boolean}{pushToHistory} - If true, keep the browser url params in sync with search and pagination
@return {Object} - returns the setParamsAndAPI and setSearch functions, and current params
+@return {Function}{setParamsAndAPI} - function to set the params and API options
+@return {Function}{setSearch} - function to set the search query
+@return {Object}{params} - current params state
*/
export const useSetParamsAndApiAndSearch = ({
defaultParams,
apiOptions,
setAPIOptions,
updateSearchQuery,
+ pushToHistory = true,
}) => {
const [params, setParams] = useState(defaultParams);
const history = useHistory();
const setParamsAndAPI = newParams => {
// add url edit params to the new params
- const uri = new URI();
- uri.setSearch(newParams);
- history.push({ search: uri.search() });
+ if (pushToHistory) {
+ const uri = new URI();
+ uri.setSearch(newParams);
+ history.push({ search: uri.search() });
+ }
setParams(newParams);
setAPIOptions({ ...apiOptions, params: newParams });
};
const setSearch = newSearch => {
- const uri = new URI();
- uri.setSearch(newSearch);
+ if (pushToHistory) {
+ const uri = new URI();
+ uri.setSearch(newSearch);
+ history.push({ search: uri.search() });
+ }
updateSearchQuery(newSearch.search);
- history.push({ search: uri.search() });
setParamsAndAPI({ ...params, ...newSearch });
};
diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js
index 34df0423b76..0a5c04e3d4f 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js
@@ -66,8 +66,14 @@ A page component that displays a table with data fetched from the API. It provid
@param {Object} {replacementResponse} - If included, skip the API request and use this response instead
@param {boolean} {showCheckboxes} - Not needed when passing children. Whether or not to show selection checkboxes in the first column.
@param {function} {rowSelectTd} - Not needed when passing children. A function that takes a single result object and returns a React component to be rendered in the first column.
+@param {function} {selectOne} - Not needed when passing children. Pass in the selectOne function from useBulkSelect, to use within rowSelectTd.
+@param {function} {isSelected} - Not needed when passing children. Pass in the isSelected function from useBulkSelect, to use within rowSelectTd.
+@param {string} {idColumn} - Not needed when passing children. The column name to use for RowSelectTd to pass to its selectOne function
@param {function} {rowKebabItems} - Not needed when passing children. A function that takes a single result object and returns an array of kebab items to be displayed in the last column
@param {function} {updateSearchQuery} - Pass in the updateSearchQuery function returned from useBulkSelect.
+@param {function} {restrictedSearchQuery} - If included, normalize the search query to add this to all search queries to restrict search results without altering the search input value. Useful for limiting results to an initial selection.
+@param {boolean} {updateParamsByUrl} - If true, update pagination props from URL params. Default is true.
+@param {string} {bookmarksPosition} - The position of the bookmarks dropdown. Default is 'left', which means the menu will take up space to its right.
*/
const TableIndexPage = ({
@@ -95,8 +101,14 @@ const TableIndexPage = ({
replacementResponse,
showCheckboxes,
rowSelectTd,
+ selectOne,
+ isSelected,
+ idColumn,
rowKebabItems,
updateSearchQuery,
+ restrictedSearchQuery,
+ updateParamsByUrl,
+ bookmarksPosition,
}) => {
const history = useHistory();
const { location: { search: historySearch } = {} } = history || {};
@@ -104,13 +116,15 @@ const TableIndexPage = ({
const urlParamsSearch = urlParams.get('search') || '';
const search = urlParamsSearch || getURIsearch();
const defaultParams = { search: search || '' };
- const urlPage = urlParams.get('page');
- const urlPerPage = urlParams.get('per_page');
- if (urlPage) {
- defaultParams.page = parseInt(urlPage, 10);
- }
- if (urlPerPage) {
- defaultParams.per_page = parseInt(urlPerPage, 10);
+ if (updateParamsByUrl) {
+ const urlPage = urlParams.get('page');
+ const urlPerPage = urlParams.get('per_page');
+ if (urlPage) {
+ defaultParams.page = parseInt(urlPage, 10);
+ }
+ if (urlPerPage) {
+ defaultParams.per_page = parseInt(urlPerPage, 10);
+ }
}
const response = useTableIndexAPIResponse({
replacementResponse,
@@ -134,10 +148,6 @@ const TableIndexPage = ({
setAPIOptions,
} = response;
- const onPagination = newPagination => {
- setParamsAndAPI({ ...params, ...newPagination });
- };
-
const memoDefaultSearchProps = useMemo(
() => getControllerSearchProps(controller),
[controller]
@@ -150,11 +160,19 @@ const TableIndexPage = ({
apiOptions,
setAPIOptions,
updateSearchQuery,
+ pushToHistory: updateParamsByUrl,
});
+ const onPagination = newPagination => {
+ setParamsAndAPI({ ...params, ...newPagination });
+ };
+
const onSearch = newSearch => {
if (newSearch !== apiSearchQuery) {
- setSearch({ search: newSearch, page: 1 });
+ setSearch({
+ search: newSearch,
+ page: 1,
+ });
}
};
@@ -209,8 +227,10 @@ const TableIndexPage = ({
{status === STATUS.PENDING && (
@@ -237,6 +257,8 @@ const TableIndexPage = ({
{total > 0 && (
{children || (
+ }
getActions={rowKebabItems}
itemCount={subtotal}
results={results}
@@ -271,8 +308,11 @@ const TableIndexPage = ({
status === STATUS.ERROR && errorMessage ? errorMessage : null
}
isPending={status === STATUS.PENDING}
+ selectOne={selectOne}
+ isSelected={isSelected}
showCheckboxes={showCheckboxes}
rowSelectTd={rowSelectTd}
+ idColumn={idColumn}
/>
)}
@@ -326,10 +366,16 @@ TableIndexPage.propTypes = {
searchable: PropTypes.bool,
children: PropTypes.node,
selectionToolbar: PropTypes.node,
+ idColumn: PropTypes.string,
rowSelectTd: PropTypes.func,
+ selectOne: PropTypes.func,
+ isSelected: PropTypes.func,
showCheckboxes: PropTypes.bool,
rowKebabItems: PropTypes.func,
updateSearchQuery: PropTypes.func,
+ restrictedSearchQuery: PropTypes.func,
+ updateParamsByUrl: PropTypes.bool,
+ bookmarksPosition: PropTypes.string,
};
TableIndexPage.defaultProps = {
@@ -354,10 +400,16 @@ TableIndexPage.defaultProps = {
searchable: true,
selectionToolbar: null,
rowSelectTd: noop,
+ selectOne: noop,
+ isSelected: noop,
showCheckboxes: false,
+ idColumn: 'id',
replacementResponse: null,
rowKebabItems: noop,
updateSearchQuery: noop,
+ restrictedSearchQuery: noop,
+ updateParamsByUrl: true,
+ bookmarksPosition: 'left',
};
export default TableIndexPage;
diff --git a/webpack/assets/javascripts/react_app/components/SearchBar/index.js b/webpack/assets/javascripts/react_app/components/SearchBar/index.js
index a3e831cdcf1..5de11e4f7c7 100644
--- a/webpack/assets/javascripts/react_app/components/SearchBar/index.js
+++ b/webpack/assets/javascripts/react_app/components/SearchBar/index.js
@@ -16,9 +16,11 @@ const SearchBar = ({
disabled,
},
initialQuery,
+ restrictedSearchQuery,
onSearch,
onSearchChange,
name,
+ bookmarksPosition,
}) => {
const [search, setSearch] = useState(initialQuery || searchQuery || '');
const { response, status, setAPIOptions } = useAPI('get', url, {
@@ -28,8 +30,13 @@ const SearchBar = ({
if (searchQuery !== prevSearch) {
setPrevSearch(searchQuery);
if (searchQuery !== search) {
- setSearch(searchQuery || '');
- setAPIOptions({ params: { ...apiParams, search: searchQuery || '' } });
+ setSearch(restrictedSearchQuery(searchQuery) ?? (searchQuery || ''));
+ setAPIOptions({
+ params: {
+ ...apiParams,
+ search: restrictedSearchQuery(searchQuery) ?? (searchQuery || ''),
+ },
+ });
}
}
const _onSearchChange = newValue => {
@@ -37,6 +44,12 @@ const SearchBar = ({
setSearch(newValue);
setAPIOptions({ params: { ...apiParams, search: newValue } });
};
+ const _onSearch = searchValue => {
+ if (restrictedSearchQuery(searchValue)) {
+ return onSearch(restrictedSearchQuery(searchValue));
+ }
+ return onSearch(searchValue);
+ };
const error =
status === STATUS.ERROR || response?.[0]?.error
? response?.[0]?.error || response.message
@@ -49,7 +62,7 @@ const SearchBar = ({
}
onSearchChange={_onSearchChange}
value={search}
- onSearch={onSearch}
+ onSearch={_onSearch}
disabled={disabled}
error={error}
name={name}
@@ -62,6 +75,7 @@ const SearchBar = ({
}}
controller={controller}
searchQuery={search || ''}
+ bookmarksPosition={bookmarksPosition}
{...bookmarks}
/>
)}
@@ -86,15 +100,19 @@ SearchBar.propTypes = {
}).isRequired,
initialQuery: PropTypes.string,
onSearch: PropTypes.func,
+ restrictedSearchQuery: PropTypes.func,
onSearchChange: PropTypes.func,
name: PropTypes.string,
+ bookmarksPosition: PropTypes.string,
};
SearchBar.defaultProps = {
initialQuery: '',
onSearch: searchQuery => changeQuery({ search: searchQuery.trim(), page: 1 }),
onSearchChange: noop,
+ restrictedSearchQuery: noop,
name: null,
+ bookmarksPosition: 'left',
};
export default SearchBar;
| |