Merge pull request #10329 from keithjgrant/6853-select-all-quirks

Clear list selections on pagination/search/sort

SUMMARY
Updates nearly every list* so that URL param changes (pagination, search, or sort) clear the selection. This prevents the list of selected items in state from including items that may no longer appear on screen — preventing the user from accidentally deleting or otherwise altering an item they may not realize they still have selected.
This also updates the useSelected hook to provide selectAll and clearSelected functions. Any lists that weren't yet already using this hook have been updated to do so.
Addresses #6853 and #7509
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME

UI

ADDITIONAL INFORMATION
*Lists that do not include this change are modals where the user is expected to paginate through screens and make several selections along the way (e.g. Multi Credential select modal), and lists that still use PaginatedDataList and are yet to be converted to PaginatedTable
Note: I originally wanted to make the clearSelected prop on PaginatedTable required, so any lists that don't have this fix applied would fail loudly. Unfortunately that wasn't possible, as there were a few lists that should not have this behavior, so I had to leave it as an optional prop.

Reviewed-by: Alex Corey <Alex.swansboro@gmail.com>
Reviewed-by: Jake McDermott <yo@jakemcdermott.me>
This commit is contained in:
softwarefactory-project-zuul[bot]
2021-06-02 23:11:41 +00:00
committed by GitHub
28 changed files with 353 additions and 284 deletions

View File

@@ -37,7 +37,6 @@ function DataListToolbar({
onExpand, onExpand,
onSelectAll, onSelectAll,
additionalControls, additionalControls,
qsConfig, qsConfig,
pagination, pagination,
}) { }) {

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { t, Plural } from '@lingui/macro'; import { t, Plural } from '@lingui/macro';
@@ -13,7 +13,7 @@ import useRequest, {
useDismissableError, useDismissableError,
} from '../../util/useRequest'; } from '../../util/useRequest';
import { useConfig } from '../../contexts/Config'; import { useConfig } from '../../contexts/Config';
import useSelected from '../../util/useSelected';
import { isJobRunning, getJobModel } from '../../util/jobs'; import { isJobRunning, getJobModel } from '../../util/jobs';
import { getQSConfig, parseQueryString } from '../../util/qs'; import { getQSConfig, parseQueryString } from '../../util/qs';
import JobListItem from './JobListItem'; import JobListItem from './JobListItem';
@@ -35,8 +35,6 @@ function JobList({ defaultParams, showTypeColumn = false }) {
); );
const { me } = useConfig(); const { me } = useConfig();
const [selected, setSelected] = useState([]);
const location = useLocation(); const location = useLocation();
const { const {
result: { results, count, relatedSearchableKeys, searchableKeys }, result: { results, count, relatedSearchableKeys, searchableKeys },
@@ -88,7 +86,13 @@ function JobList({ defaultParams, showTypeColumn = false }) {
const jobs = useWsJobs(results, fetchJobsById, qsConfig); const jobs = useWsJobs(results, fetchJobsById, qsConfig);
const isAllSelected = selected.length === jobs.length && selected.length > 0; const {
selected,
isAllSelected,
handleSelect,
selectAll,
clearSelected,
} = useSelected(jobs);
const { const {
error: cancelJobsError, error: cancelJobsError,
@@ -135,24 +139,12 @@ function JobList({ defaultParams, showTypeColumn = false }) {
const handleJobCancel = async () => { const handleJobCancel = async () => {
await cancelJobs(); await cancelJobs();
setSelected([]); clearSelected();
}; };
const handleJobDelete = async () => { const handleJobDelete = async () => {
await deleteJobs(); await deleteJobs();
setSelected([]); clearSelected();
};
const handleSelectAll = isSelected => {
setSelected(isSelected ? [...jobs] : []);
};
const handleSelect = item => {
if (selected.some(s => s.id === item.id)) {
setSelected(selected.filter(s => s.id !== item.id));
} else {
setSelected(selected.concat(item));
}
}; };
const cannotDeleteItems = selected.filter(job => isJobRunning(job.status)); const cannotDeleteItems = selected.filter(job => isJobRunning(job.status));
@@ -226,6 +218,7 @@ function JobList({ defaultParams, showTypeColumn = false }) {
<HeaderCell>{t`Actions`}</HeaderCell> <HeaderCell>{t`Actions`}</HeaderCell>
</HeaderRow> </HeaderRow>
} }
clearSelected={clearSelected}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
renderToolbar={props => ( renderToolbar={props => (
@@ -233,7 +226,7 @@ function JobList({ defaultParams, showTypeColumn = false }) {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={qsConfig} qsConfig={qsConfig}
additionalControls={[ additionalControls={[
<ToolbarDeleteButton <ToolbarDeleteButton

View File

@@ -1,5 +1,5 @@
import 'styled-components/macro'; import 'styled-components/macro';
import React, { Fragment } from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TableComposable, Tbody } from '@patternfly/react-table'; import { TableComposable, Tbody } from '@patternfly/react-table';
@@ -33,10 +33,16 @@ function PaginatedTable({
showPageSizeOptions, showPageSizeOptions,
renderToolbar, renderToolbar,
emptyContentMessage, emptyContentMessage,
clearSelected,
ouiaId, ouiaId,
}) { }) {
const { search, pathname } = useLocation(); const { search, pathname } = useLocation();
const history = useHistory(); const history = useHistory();
const location = useLocation();
useEffect(() => {
clearSelected();
}, [location.search, clearSelected]);
const pushHistoryState = qs => { const pushHistoryState = qs => {
history.push(qs ? `${pathname}?${qs}` : pathname); history.push(qs ? `${pathname}?${qs}` : pathname);
@@ -127,7 +133,7 @@ function PaginatedTable({
); );
return ( return (
<Fragment> <>
<ListHeader <ListHeader
itemCount={itemCount} itemCount={itemCount}
renderToolbar={renderToolbar} renderToolbar={renderToolbar}
@@ -159,7 +165,7 @@ function PaginatedTable({
onPerPageSelect={handleSetPageSize} onPerPageSelect={handleSetPageSize}
/> />
) : null} ) : null}
</Fragment> </>
); );
} }
@@ -182,6 +188,7 @@ PaginatedTable.propTypes = {
renderToolbar: PropTypes.func, renderToolbar: PropTypes.func,
hasContentLoading: PropTypes.bool, hasContentLoading: PropTypes.bool,
contentError: PropTypes.shape(), contentError: PropTypes.shape(),
clearSelected: PropTypes.func,
ouiaId: PropTypes.string, ouiaId: PropTypes.string,
}; };
@@ -195,6 +202,7 @@ PaginatedTable.defaultProps = {
showPageSizeOptions: true, showPageSizeOptions: true,
renderToolbar: props => <DataListToolbar {...props} />, renderToolbar: props => <DataListToolbar {...props} />,
ouiaId: null, ouiaId: null,
clearSelected: () => {},
}; };
export { PaginatedTable as _PaginatedTable }; export { PaginatedTable as _PaginatedTable };

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { bool, func } from 'prop-types'; import { bool, func } from 'prop-types';
@@ -10,6 +10,7 @@ import PaginatedTable, { HeaderRow, HeaderCell } from '../../PaginatedTable';
import DataListToolbar from '../../DataListToolbar'; import DataListToolbar from '../../DataListToolbar';
import { ToolbarAddButton, ToolbarDeleteButton } from '../../PaginatedDataList'; import { ToolbarAddButton, ToolbarDeleteButton } from '../../PaginatedDataList';
import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useRequest, { useDeleteItems } from '../../../util/useRequest';
import useSelected from '../../../util/useSelected';
import { getQSConfig, parseQueryString } from '../../../util/qs'; import { getQSConfig, parseQueryString } from '../../../util/qs';
import ScheduleListItem from './ScheduleListItem'; import ScheduleListItem from './ScheduleListItem';
@@ -27,8 +28,6 @@ function ScheduleList({
launchConfig, launchConfig,
surveyConfig, surveyConfig,
}) { }) {
const [selected, setSelected] = useState([]);
const location = useLocation(); const location = useLocation();
const { const {
@@ -76,8 +75,13 @@ function ScheduleList({
fetchSchedules(); fetchSchedules();
}, [fetchSchedules]); }, [fetchSchedules]);
const isAllSelected = const {
selected.length === schedules.length && selected.length > 0; selected,
isAllSelected,
handleSelect,
selectAll,
clearSelected,
} = useSelected(schedules);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
@@ -95,21 +99,9 @@ function ScheduleList({
} }
); );
const handleSelectAll = isSelected => {
setSelected(isSelected ? [...schedules] : []);
};
const handleSelect = row => {
if (selected.some(s => s.id === row.id)) {
setSelected(selected.filter(s => s.id !== row.id));
} else {
setSelected(selected.concat(row));
}
};
const handleDelete = async () => { const handleDelete = async () => {
await deleteJobs(); await deleteJobs();
setSelected([]); clearSelected();
}; };
const canAdd = const canAdd =
@@ -183,6 +175,7 @@ function ScheduleList({
isMissingSurvey={isTemplate && hasMissingSurveyValue(item)} isMissingSurvey={isTemplate && hasMissingSurveyValue(item)}
/> />
)} )}
clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -209,7 +202,7 @@ function ScheduleList({
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useEffect, useState, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useLocation, Link } from 'react-router-dom'; import { useLocation, Link } from 'react-router-dom';
import { t, Plural } from '@lingui/macro'; import { t, Plural } from '@lingui/macro';
import { Card, DropdownItem } from '@patternfly/react-core'; import { Card, DropdownItem } from '@patternfly/react-core';
@@ -13,6 +13,7 @@ import ErrorDetail from '../ErrorDetail';
import { ToolbarDeleteButton } from '../PaginatedDataList'; import { ToolbarDeleteButton } from '../PaginatedDataList';
import PaginatedTable, { HeaderRow, HeaderCell } from '../PaginatedTable'; import PaginatedTable, { HeaderRow, HeaderCell } from '../PaginatedTable';
import useRequest, { useDeleteItems } from '../../util/useRequest'; import useRequest, { useDeleteItems } from '../../util/useRequest';
import useSelected from '../../util/useSelected';
import { getQSConfig, parseQueryString } from '../../util/qs'; import { getQSConfig, parseQueryString } from '../../util/qs';
import useWsTemplates from '../../util/useWsTemplates'; import useWsTemplates from '../../util/useWsTemplates';
import AddDropDownButton from '../AddDropDownButton'; import AddDropDownButton from '../AddDropDownButton';
@@ -35,8 +36,6 @@ function TemplateList({ defaultParams }) {
); );
const location = useLocation(); const location = useLocation();
const [selected, setSelected] = useState([]);
const { const {
result: { result: {
results, results,
@@ -87,8 +86,14 @@ function TemplateList({ defaultParams }) {
const templates = useWsTemplates(results); const templates = useWsTemplates(results);
const isAllSelected = const {
selected.length === templates.length && selected.length > 0; selected,
isAllSelected,
handleSelect,
selectAll,
clearSelected,
} = useSelected(templates);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
deleteItems: deleteTemplates, deleteItems: deleteTemplates,
@@ -117,19 +122,7 @@ function TemplateList({ defaultParams }) {
const handleTemplateDelete = async () => { const handleTemplateDelete = async () => {
await deleteTemplates(); await deleteTemplates();
setSelected([]); clearSelected();
};
const handleSelectAll = isSelected => {
setSelected(isSelected ? [...templates] : []);
};
const handleSelect = template => {
if (selected.some(s => s.id === template.id)) {
setSelected(selected.filter(s => s.id !== template.id));
} else {
setSelected(selected.concat(template));
}
}; };
const canAddJT = const canAddJT =
@@ -179,7 +172,7 @@ function TemplateList({ defaultParams }) {
); );
return ( return (
<Fragment> <>
<Card> <Card>
<PaginatedTable <PaginatedTable
contentError={contentError} contentError={contentError}
@@ -188,7 +181,7 @@ function TemplateList({ defaultParams }) {
itemCount={count} itemCount={count}
pluralizedItemName={t`Templates`} pluralizedItemName={t`Templates`}
qsConfig={qsConfig} qsConfig={qsConfig}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -235,7 +228,7 @@ function TemplateList({ defaultParams }) {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={qsConfig} qsConfig={qsConfig}
additionalControls={[ additionalControls={[
...(canAddJT || canAddWFJT ? [addButton] : []), ...(canAddJT || canAddWFJT ? [addButton] : []),
@@ -281,7 +274,7 @@ function TemplateList({ defaultParams }) {
{t`Failed to delete one or more templates.`} {t`Failed to delete one or more templates.`}
<ErrorDetail error={deletionError} /> <ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
</Fragment> </>
); );
} }

View File

@@ -77,9 +77,13 @@ function ApplicationsList() {
fetchApplications(); fetchApplications();
}, [fetchApplications]); }, [fetchApplications]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
applications selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(applications);
const { const {
isLoading: deleteLoading, isLoading: deleteLoading,
@@ -99,7 +103,7 @@ function ApplicationsList() {
const handleDeleteApplications = async () => { const handleDeleteApplications = async () => {
await deleteApplications(); await deleteApplications();
setSelected([]); clearSelected();
}; };
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
@@ -115,7 +119,7 @@ function ApplicationsList() {
itemCount={itemCount} itemCount={itemCount}
pluralizedItemName={t`Applications`} pluralizedItemName={t`Applications`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -134,9 +138,7 @@ function ApplicationsList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...applications] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -76,9 +76,14 @@ function CredentialList() {
fetchCredentials(); fetchCredentials();
}, [fetchCredentials]); }, [fetchCredentials]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
credentials selected,
); isAllSelected,
handleSelect,
setSelected,
selectAll,
clearSelected,
} = useSelected(credentials);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
@@ -115,7 +120,7 @@ function CredentialList() {
items={credentials} items={credentials}
itemCount={credentialCount} itemCount={credentialCount}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
toolbarSearchColumns={[ toolbarSearchColumns={[
@@ -160,9 +165,7 @@ function CredentialList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...credentials] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -77,9 +77,13 @@ function CredentialTypeList() {
fetchCredentialTypes(); fetchCredentialTypes();
}, [fetchCredentialTypes]); }, [fetchCredentialTypes]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
credentialTypes selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(credentialTypes);
const { const {
isLoading: deleteLoading, isLoading: deleteLoading,
@@ -101,7 +105,7 @@ function CredentialTypeList() {
const handleDelete = async () => { const handleDelete = async () => {
await deleteCredentialTypes(); await deleteCredentialTypes();
setSelected([]); clearSelected();
}; };
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
@@ -121,7 +125,7 @@ function CredentialTypeList() {
itemCount={credentialTypesCount} itemCount={credentialTypesCount}
pluralizedItemName={t`Credential Types`} pluralizedItemName={t`Credential Types`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -148,9 +152,7 @@ function CredentialTypeList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...credentialTypes] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -76,9 +76,13 @@ function ExecutionEnvironmentList() {
fetchExecutionEnvironments(); fetchExecutionEnvironments();
}, [fetchExecutionEnvironments]); }, [fetchExecutionEnvironments]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
executionEnvironments selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(executionEnvironments);
const { const {
isLoading: deleteLoading, isLoading: deleteLoading,
@@ -100,7 +104,7 @@ function ExecutionEnvironmentList() {
const handleDelete = async () => { const handleDelete = async () => {
await deleteExecutionEnvironments(); await deleteExecutionEnvironments();
setSelected([]); clearSelected();
}; };
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
@@ -119,7 +123,7 @@ function ExecutionEnvironmentList() {
itemCount={executionEnvironmentsCount} itemCount={executionEnvironmentsCount}
pluralizedItemName={t`Execution Environments`} pluralizedItemName={t`Execution Environments`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
toolbarSearchColumns={[ toolbarSearchColumns={[
@@ -164,9 +168,7 @@ function ExecutionEnvironmentList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...executionEnvironments] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'; import { useHistory, useLocation, useRouteMatch } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -17,6 +17,7 @@ import PaginatedTable, {
HeaderCell, HeaderCell,
} from '../../../components/PaginatedTable'; } from '../../../components/PaginatedTable';
import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useRequest, { useDeleteItems } from '../../../util/useRequest';
import useSelected from '../../../util/useSelected';
import { import {
encodeQueryString, encodeQueryString,
getQSConfig, getQSConfig,
@@ -36,7 +37,6 @@ function HostList() {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const match = useRouteMatch(); const match = useRouteMatch();
const [selected, setSelected] = useState([]);
const parsedQueryStrings = parseQueryString(QS_CONFIG, location.search); const parsedQueryStrings = parseQueryString(QS_CONFIG, location.search);
const nonDefaultSearchParams = {}; const nonDefaultSearchParams = {};
@@ -86,7 +86,14 @@ function HostList() {
fetchHosts(); fetchHosts();
}, [fetchHosts]); }, [fetchHosts]);
const isAllSelected = selected.length === hosts.length && selected.length > 0; const {
selected,
isAllSelected,
handleSelect,
selectAll,
clearSelected,
} = useSelected(hosts);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
deleteItems: deleteHosts, deleteItems: deleteHosts,
@@ -105,19 +112,7 @@ function HostList() {
const handleHostDelete = async () => { const handleHostDelete = async () => {
await deleteHosts(); await deleteHosts();
setSelected([]); clearSelected();
};
const handleSelectAll = isSelected => {
setSelected(isSelected ? [...hosts] : []);
};
const handleSelect = host => {
if (selected.some(h => h.id === host.id)) {
setSelected(selected.filter(h => h.id !== host.id));
} else {
setSelected(selected.concat(host));
}
}; };
const handleSmartInventoryClick = () => { const handleSmartInventoryClick = () => {
@@ -141,7 +136,7 @@ function HostList() {
itemCount={count} itemCount={count}
pluralizedItemName={t`Hosts`} pluralizedItemName={t`Hosts`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -175,7 +170,7 @@ function HostList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -92,9 +92,13 @@ function InstanceGroupList() {
fetchInstanceGroups(); fetchInstanceGroups();
}, [fetchInstanceGroups]); }, [fetchInstanceGroups]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
instanceGroups selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(instanceGroups);
const modifiedSelected = modifyInstanceGroups(selected); const modifiedSelected = modifyInstanceGroups(selected);
@@ -118,7 +122,7 @@ function InstanceGroupList() {
const handleDelete = async () => { const handleDelete = async () => {
await deleteInstanceGroups(); await deleteInstanceGroups();
setSelected([]); clearSelected();
}; };
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
@@ -201,7 +205,7 @@ function InstanceGroupList() {
itemCount={instanceGroupsCount} itemCount={instanceGroupsCount}
pluralizedItemName={pluralizedItemName} pluralizedItemName={pluralizedItemName}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
renderToolbar={props => ( renderToolbar={props => (
@@ -209,9 +213,7 @@ function InstanceGroupList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...instanceGroups] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ? [addButton] : []), ...(canAdd ? [addButton] : []),

View File

@@ -77,9 +77,13 @@ function InventoryGroupsList() {
fetchData(); fetchData();
}, [fetchData]); }, [fetchData]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
groups selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(groups);
const renderTooltip = () => { const renderTooltip = () => {
const itemsUnableToDelete = selected const itemsUnableToDelete = selected
@@ -111,7 +115,7 @@ function InventoryGroupsList() {
items={groups} items={groups}
itemCount={groupCount} itemCount={groupCount}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -159,9 +163,7 @@ function InventoryGroupsList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...groups] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd
@@ -185,7 +187,7 @@ function InventoryGroupsList() {
} }
onAfterDelete={() => { onAfterDelete={() => {
fetchData(); fetchData();
setSelected([]); clearSelected();
}} }}
/> />
</Tooltip>, </Tooltip>,

View File

@@ -84,9 +84,13 @@ function InventoryHostGroupsList() {
fetchGroups(); fetchGroups();
}, [fetchGroups]); }, [fetchGroups]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
groups selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(groups);
const { const {
isLoading: isDisassociateLoading, isLoading: isDisassociateLoading,
@@ -107,7 +111,7 @@ function InventoryHostGroupsList() {
const handleDisassociate = async () => { const handleDisassociate = async () => {
await disassociateHosts(); await disassociateHosts();
setSelected([]); clearSelected();
}; };
const fetchGroupsToAssociate = useCallback( const fetchGroupsToAssociate = useCallback(
@@ -156,7 +160,7 @@ function InventoryHostGroupsList() {
items={groups} items={groups}
itemCount={itemCount} itemCount={itemCount}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -195,9 +199,7 @@ function InventoryHostGroupsList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...groups] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -15,6 +15,7 @@ import {
ToolbarAddButton, ToolbarAddButton,
ToolbarDeleteButton, ToolbarDeleteButton,
} from '../../../components/PaginatedDataList'; } from '../../../components/PaginatedDataList';
import useSelected from '../../../util/useSelected';
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands'; import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
import InventoryHostItem from './InventoryHostItem'; import InventoryHostItem from './InventoryHostItem';
@@ -25,7 +26,6 @@ const QS_CONFIG = getQSConfig('host', {
}); });
function InventoryHostList() { function InventoryHostList() {
const [selected, setSelected] = useState([]);
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false); const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
const { id } = useParams(); const { id } = useParams();
const { search } = useLocation(); const { search } = useLocation();
@@ -74,17 +74,14 @@ function InventoryHostList() {
fetchData(); fetchData();
}, [fetchData]); }, [fetchData]);
const handleSelectAll = isSelected => { const {
setSelected(isSelected ? [...hosts] : []); selected,
}; isAllSelected,
handleSelect,
selectAll,
clearSelected,
} = useSelected(hosts);
const handleSelect = row => {
if (selected.some(s => s.id === row.id)) {
setSelected(selected.filter(s => s.id !== row.id));
} else {
setSelected(selected.concat(row));
}
};
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
deleteItems: deleteHosts, deleteItems: deleteHosts,
@@ -99,12 +96,11 @@ function InventoryHostList() {
const handleDeleteHosts = async () => { const handleDeleteHosts = async () => {
await deleteHosts(); await deleteHosts();
setSelected([]); clearSelected();
}; };
const canAdd = const canAdd =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const isAllSelected = selected.length > 0 && selected.length === hosts.length;
return ( return (
<> <>
@@ -115,6 +111,7 @@ function InventoryHostList() {
itemCount={hostCount} itemCount={hostCount}
pluralizedItemName={t`Hosts`} pluralizedItemName={t`Hosts`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
clearSelected={clearSelected}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
headerRow={ headerRow={
@@ -128,7 +125,7 @@ function InventoryHostList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -1,9 +1,10 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useLocation, useRouteMatch, Link } from 'react-router-dom'; import { useLocation, useRouteMatch, Link } from 'react-router-dom';
import { t, Plural } from '@lingui/macro'; import { t, Plural } from '@lingui/macro';
import { Card, PageSection, DropdownItem } from '@patternfly/react-core'; import { Card, PageSection, DropdownItem } from '@patternfly/react-core';
import { InventoriesAPI } from '../../../api'; import { InventoriesAPI } from '../../../api';
import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useRequest, { useDeleteItems } from '../../../util/useRequest';
import useSelected from '../../../util/useSelected';
import AlertModal from '../../../components/AlertModal'; import AlertModal from '../../../components/AlertModal';
import DatalistToolbar from '../../../components/DataListToolbar'; import DatalistToolbar from '../../../components/DataListToolbar';
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
@@ -27,7 +28,6 @@ const QS_CONFIG = getQSConfig('inventory', {
function InventoryList() { function InventoryList() {
const location = useLocation(); const location = useLocation();
const match = useRouteMatch(); const match = useRouteMatch();
const [selected, setSelected] = useState([]);
const { const {
result: { result: {
@@ -89,8 +89,14 @@ function InventoryList() {
QS_CONFIG QS_CONFIG
); );
const isAllSelected = const {
selected.length === inventories.length && selected.length > 0; selected,
isAllSelected,
handleSelect,
selectAll,
clearSelected,
} = useSelected(inventories);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
deleteItems: deleteInventories, deleteItems: deleteInventories,
@@ -107,26 +113,12 @@ function InventoryList() {
const handleInventoryDelete = async () => { const handleInventoryDelete = async () => {
await deleteInventories(); await deleteInventories();
setSelected([]); clearSelected();
}; };
const hasContentLoading = isDeleteLoading || isLoading; const hasContentLoading = isDeleteLoading || isLoading;
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
const handleSelectAll = isSelected => {
setSelected(isSelected ? [...inventories] : []);
};
const handleSelect = row => {
if (!row.pending_deletion) {
if (selected.some(s => s.id === row.id)) {
setSelected(selected.filter(s => s.id !== row.id));
} else {
setSelected(selected.concat(row));
}
}
};
const deleteDetailsRequests = relatedResourceDeleteRequests.inventory( const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
selected[0] selected[0]
); );
@@ -197,6 +189,7 @@ function InventoryList() {
]} ]}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
clearSelected={clearSelected}
headerRow={ headerRow={
<HeaderRow qsConfig={QS_CONFIG}> <HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell> <HeaderCell sortKey="name">{t`Name`}</HeaderCell>
@@ -211,7 +204,7 @@ function InventoryList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ? [addButton] : []), ...(canAdd ? [addButton] : []),
@@ -251,7 +244,11 @@ function InventoryList() {
? `${match.url}/smart_inventory/${inventory.id}/details` ? `${match.url}/smart_inventory/${inventory.id}/details`
: `${match.url}/inventory/${inventory.id}/details` : `${match.url}/inventory/${inventory.id}/details`
} }
onSelect={() => handleSelect(inventory)} onSelect={() => {
if (!inventory.pending_deletion) {
handleSelect(inventory);
}
}}
isSelected={selected.some(row => row.id === inventory.id)} isSelected={selected.some(row => row.id === inventory.id)}
/> />
)} )}

View File

@@ -83,9 +83,13 @@ function InventorySourceList() {
fetchSources(); fetchSources();
}, [fetchSources]); }, [fetchSources]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
sources selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(sources);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
@@ -140,7 +144,7 @@ function InventorySourceList() {
if (!deleteRelatedResourcesError) { if (!deleteRelatedResourcesError) {
await handleDeleteSources(); await handleDeleteSources();
} }
setSelected([]); clearSelected();
}; };
const canAdd = const canAdd =
sourceChoicesOptions && sourceChoicesOptions &&
@@ -164,14 +168,13 @@ function InventorySourceList() {
itemCount={sourceCount} itemCount={sourceCount}
pluralizedItemName={t`Inventory Sources`} pluralizedItemName={t`Inventory Sources`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
clearSelected={clearSelected}
renderToolbar={props => ( renderToolbar={props => (
<DatalistToolbar <DatalistToolbar
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...sources] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -306,7 +306,7 @@ function NotificationTemplateDetail({ template, defaultMessages }) {
label={t`HTTP Headers`} label={t`HTTP Headers`}
value={JSON.stringify(configuration.headers)} value={JSON.stringify(configuration.headers)}
mode="json" mode="json"
rows="6" rows={6}
dataCy="nt-detail-webhook-headers" dataCy="nt-detail-webhook-headers"
/> />
</> </>

View File

@@ -82,9 +82,13 @@ function NotificationTemplatesList() {
fetchTemplates(); fetchTemplates();
}, [fetchTemplates]); }, [fetchTemplates]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
templates selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(templates);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
@@ -106,7 +110,7 @@ function NotificationTemplatesList() {
const handleDelete = async () => { const handleDelete = async () => {
await deleteTemplates(); await deleteTemplates();
setSelected([]); clearSelected();
}; };
const addTestToast = useCallback(notification => { const addTestToast = useCallback(notification => {
@@ -133,7 +137,7 @@ function NotificationTemplatesList() {
itemCount={count} itemCount={count}
pluralizedItemName={t`Notification Templates`} pluralizedItemName={t`Notification Templates`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -176,7 +180,7 @@ function NotificationTemplatesList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={set => setSelected(set ? [...templates] : [])} onSelectAll={selectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom'; import { useLocation, useRouteMatch } from 'react-router-dom';
import { t, Plural } from '@lingui/macro'; import { t, Plural } from '@lingui/macro';
import { Card, PageSection } from '@patternfly/react-core'; import { Card, PageSection } from '@patternfly/react-core';
@@ -17,6 +17,7 @@ import PaginatedTable, {
HeaderCell, HeaderCell,
} from '../../../components/PaginatedTable'; } from '../../../components/PaginatedTable';
import { getQSConfig, parseQueryString } from '../../../util/qs'; import { getQSConfig, parseQueryString } from '../../../util/qs';
import useSelected from '../../../util/useSelected';
import OrganizationListItem from './OrganizationListItem'; import OrganizationListItem from './OrganizationListItem';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails'; import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
@@ -30,8 +31,6 @@ function OrganizationsList() {
const location = useLocation(); const location = useLocation();
const match = useRouteMatch(); const match = useRouteMatch();
const [selected, setSelected] = useState([]);
const addUrl = `${match.url}/add`; const addUrl = `${match.url}/add`;
const { const {
@@ -77,8 +76,14 @@ function OrganizationsList() {
fetchOrganizations(); fetchOrganizations();
}, [fetchOrganizations]); }, [fetchOrganizations]);
const isAllSelected = const {
selected.length === organizations.length && selected.length > 0; selected,
isAllSelected,
handleSelect,
selectAll,
clearSelected,
} = useSelected(organizations);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
deleteItems: deleteOrganizations, deleteItems: deleteOrganizations,
@@ -99,23 +104,12 @@ function OrganizationsList() {
const handleOrgDelete = async () => { const handleOrgDelete = async () => {
await deleteOrganizations(); await deleteOrganizations();
setSelected([]); clearSelected();
}; };
const hasContentLoading = isDeleteLoading || isOrgsLoading; const hasContentLoading = isDeleteLoading || isOrgsLoading;
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
const handleSelectAll = isSelected => {
setSelected(isSelected ? [...organizations] : []);
};
const handleSelect = row => {
if (selected.some(s => s.id === row.id)) {
setSelected(selected.filter(s => s.id !== row.id));
} else {
setSelected(selected.concat(row));
}
};
const deleteDetailsRequests = relatedResourceDeleteRequests.organization( const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
selected[0] selected[0]
); );
@@ -131,6 +125,7 @@ function OrganizationsList() {
itemCount={organizationCount} itemCount={organizationCount}
pluralizedItemName={t`Organizations`} pluralizedItemName={t`Organizations`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -165,7 +160,7 @@ function OrganizationsList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useState, useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom'; import { useLocation, useRouteMatch } from 'react-router-dom';
import { t, Plural } from '@lingui/macro'; import { t, Plural } from '@lingui/macro';
import { Card, PageSection } from '@patternfly/react-core'; import { Card, PageSection } from '@patternfly/react-core';
@@ -17,6 +17,7 @@ import PaginatedTable, {
HeaderCell, HeaderCell,
} from '../../../components/PaginatedTable'; } from '../../../components/PaginatedTable';
import useWsProjects from './useWsProjects'; import useWsProjects from './useWsProjects';
import useSelected from '../../../util/useSelected';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails'; import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import { getQSConfig, parseQueryString } from '../../../util/qs'; import { getQSConfig, parseQueryString } from '../../../util/qs';
@@ -31,7 +32,6 @@ const QS_CONFIG = getQSConfig('project', {
function ProjectList() { function ProjectList() {
const location = useLocation(); const location = useLocation();
const match = useRouteMatch(); const match = useRouteMatch();
const [selected, setSelected] = useState([]);
const { const {
result: { result: {
@@ -78,8 +78,15 @@ function ProjectList() {
const projects = useWsProjects(results); const projects = useWsProjects(results);
const isAllSelected = const {
selected.length === projects.length && selected.length > 0; selected,
isAllSelected,
handleSelect,
setSelected,
selectAll,
clearSelected,
} = useSelected(projects);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
deleteItems: deleteProjects, deleteItems: deleteProjects,
@@ -104,24 +111,12 @@ function ProjectList() {
const hasContentLoading = isDeleteLoading || isLoading; const hasContentLoading = isDeleteLoading || isLoading;
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
const handleSelectAll = isSelected => {
setSelected(isSelected ? [...projects] : []);
};
const handleSelect = row => {
if (selected.some(s => s.id === row.id)) {
setSelected(selected.filter(s => s.id !== row.id));
} else {
setSelected(selected.concat(row));
}
};
const deleteDetailsRequests = relatedResourceDeleteRequests.project( const deleteDetailsRequests = relatedResourceDeleteRequests.project(
selected[0] selected[0]
); );
return ( return (
<Fragment> <>
<PageSection> <PageSection>
<Card> <Card>
<PaginatedTable <PaginatedTable
@@ -131,7 +126,7 @@ function ProjectList() {
itemCount={itemCount} itemCount={itemCount}
pluralizedItemName={t`Projects`} pluralizedItemName={t`Projects`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -182,7 +177,7 @@ function ProjectList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd
@@ -239,7 +234,7 @@ function ProjectList() {
{t`Failed to delete one or more projects.`} {t`Failed to delete one or more projects.`}
<ErrorDetail error={deletionError} /> <ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
</Fragment> </>
); );
} }

View File

@@ -52,7 +52,7 @@ function SubscriptionModal({
[] []
); );
const { selected, handleSelect } = useSelected(subscriptions); const { selected, setSelected } = useSelected(subscriptions);
function handleConfirm() { function handleConfirm() {
const [subscription] = selected; const [subscription] = selected;
@@ -64,6 +64,14 @@ function SubscriptionModal({
fetchSubscriptions(); fetchSubscriptions();
}, [fetchSubscriptions]); }, [fetchSubscriptions]);
const handleSelect = item => {
if (selected.some(s => s.pool_id === item.pool_id)) {
setSelected(selected.filter(s => s.pool_id !== item.pool_id));
} else {
setSelected(selected.concat(item));
}
};
useEffect(() => { useEffect(() => {
if (selectedSubscription?.pool_id) { if (selectedSubscription?.pool_id) {
handleSelect({ pool_id: selectedSubscription.pool_id }); handleSelect({ pool_id: selectedSubscription.pool_id });

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useState, useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom'; import { useLocation, useRouteMatch } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -17,6 +17,7 @@ import {
ToolbarAddButton, ToolbarAddButton,
ToolbarDeleteButton, ToolbarDeleteButton,
} from '../../../components/PaginatedDataList'; } from '../../../components/PaginatedDataList';
import useSelected from '../../../util/useSelected';
import { getQSConfig, parseQueryString } from '../../../util/qs'; import { getQSConfig, parseQueryString } from '../../../util/qs';
import TeamListItem from './TeamListItem'; import TeamListItem from './TeamListItem';
@@ -30,7 +31,6 @@ const QS_CONFIG = getQSConfig('team', {
function TeamList() { function TeamList() {
const location = useLocation(); const location = useLocation();
const match = useRouteMatch(); const match = useRouteMatch();
const [selected, setSelected] = useState([]);
const { const {
result: { result: {
@@ -75,7 +75,14 @@ function TeamList() {
fetchTeams(); fetchTeams();
}, [fetchTeams]); }, [fetchTeams]);
const isAllSelected = selected.length === teams.length && selected.length > 0; const {
selected,
isAllSelected,
handleSelect,
selectAll,
clearSelected,
} = useSelected(teams);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
deleteItems: deleteTeams, deleteItems: deleteTeams,
@@ -94,26 +101,14 @@ function TeamList() {
const handleTeamDelete = async () => { const handleTeamDelete = async () => {
await deleteTeams(); await deleteTeams();
setSelected([]); clearSelected();
}; };
const hasContentLoading = isDeleteLoading || isLoading; const hasContentLoading = isDeleteLoading || isLoading;
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
const handleSelectAll = isSelected => {
setSelected(isSelected ? [...teams] : []);
};
const handleSelect = row => {
if (selected.some(s => s.id === row.id)) {
setSelected(selected.filter(s => s.id !== row.id));
} else {
setSelected(selected.concat(row));
}
};
return ( return (
<Fragment> <>
<PageSection> <PageSection>
<Card> <Card>
<PaginatedTable <PaginatedTable
@@ -123,7 +118,7 @@ function TeamList() {
itemCount={itemCount} itemCount={itemCount}
pluralizedItemName={t`Teams`} pluralizedItemName={t`Teams`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -161,7 +156,7 @@ function TeamList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd
@@ -208,7 +203,7 @@ function TeamList() {
{t`Failed to delete one or more teams.`} {t`Failed to delete one or more teams.`}
<ErrorDetail error={deletionError} /> <ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
</Fragment> </>
); );
} }

View File

@@ -19,6 +19,7 @@ import { ToolbarAddButton } from '../../../components/PaginatedDataList';
import SurveyListItem from './SurveyListItem'; import SurveyListItem from './SurveyListItem';
import SurveyToolbar from './SurveyToolbar'; import SurveyToolbar from './SurveyToolbar';
import SurveyPreviewModal from './SurveyPreviewModal'; import SurveyPreviewModal from './SurveyPreviewModal';
import useSelected from '../../../util/useSelected';
const Button = styled(_Button)` const Button = styled(_Button)`
margin: 20px; margin: 20px;
@@ -36,15 +37,16 @@ function SurveyList({
const match = useRouteMatch(); const match = useRouteMatch();
const questions = survey?.spec || []; const questions = survey?.spec || [];
const [selected, setSelected] = useState([]);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
const isAllSelected =
selected.length === questions?.length && selected.length > 0;
const handleSelectAll = isSelected => { const {
setSelected(isSelected ? [...questions] : []); selected,
}; isAllSelected,
setSelected,
selectAll,
clearSelected,
} = useSelected(questions);
const handleSelect = item => { const handleSelect = item => {
if (selected.some(q => q.variable === item.variable)) { if (selected.some(q => q.variable === item.variable)) {
@@ -61,7 +63,7 @@ function SurveyList({
await updateSurvey(questions.filter(q => !selected.includes(q))); await updateSurvey(questions.filter(q => !selected.includes(q)));
} }
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
setSelected([]); clearSelected();
}; };
const moveUp = question => { const moveUp = question => {
@@ -91,7 +93,7 @@ function SurveyList({
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
onClose={() => { onClose={() => {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
setSelected([]); clearSelected();
}} }}
actions={[ actions={[
<Button <Button
@@ -110,7 +112,7 @@ function SurveyList({
aria-label={t`cancel delete`} aria-label={t`cancel delete`}
onClick={() => { onClick={() => {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
setSelected([]); clearSelected();
}} }}
> >
{t`Cancel`} {t`Cancel`}
@@ -181,7 +183,7 @@ function SurveyList({
<> <>
<SurveyToolbar <SurveyToolbar
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={handleSelectAll} onSelectAll={selectAll}
surveyEnabled={surveyEnabled} surveyEnabled={surveyEnabled}
onToggleSurvey={toggleSurvey} onToggleSurvey={toggleSurvey}
isDeleteDisabled={selected?.length === 0} isDeleteDisabled={selected?.length === 0}

View File

@@ -73,9 +73,13 @@ function UserList() {
fetchUsers(); fetchUsers();
}, [fetchUsers]); }, [fetchUsers]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
users selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(users);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
@@ -95,7 +99,7 @@ function UserList() {
const handleUserDelete = async () => { const handleUserDelete = async () => {
await deleteUsers(); await deleteUsers();
setSelected([]); clearSelected();
}; };
const hasContentLoading = isDeleteLoading || isLoading; const hasContentLoading = isDeleteLoading || isLoading;
@@ -112,7 +116,7 @@ function UserList() {
itemCount={itemCount} itemCount={itemCount}
pluralizedItemName={t`Users`} pluralizedItemName={t`Users`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Username`, name: t`Username`,
@@ -135,9 +139,7 @@ function UserList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...users] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -85,9 +85,13 @@ function UserTeamList() {
fetchTeams(); fetchTeams();
}, [fetchTeams]); }, [fetchTeams]);
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
teams selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(teams);
const disassociateUserRoles = team => { const disassociateUserRoles = team => {
return [ return [
@@ -141,7 +145,7 @@ function UserTeamList() {
const handleDisassociate = async () => { const handleDisassociate = async () => {
await disassociateTeams(); await disassociateTeams();
setSelected([]); clearSelected();
}; };
const { error, dismissError } = useDismissableError( const { error, dismissError } = useDismissableError(
@@ -176,7 +180,7 @@ function UserTeamList() {
itemCount={count} itemCount={count}
pluralizedItemName={t`Teams`} pluralizedItemName={t`Teams`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
headerRow={ headerRow={
<HeaderRow qsConfig={QS_CONFIG}> <HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell> <HeaderCell sortKey="name">{t`Name`}</HeaderCell>
@@ -200,9 +204,7 @@ function UserTeamList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...teams] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -84,9 +84,13 @@ function WorkflowApprovalsList() {
QS_CONFIG QS_CONFIG
); );
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
workflowApprovals selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(workflowApprovals);
const { const {
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
@@ -108,7 +112,7 @@ function WorkflowApprovalsList() {
const handleDelete = async () => { const handleDelete = async () => {
await deleteWorkflowApprovals(); await deleteWorkflowApprovals();
setSelected([]); clearSelected();
}; };
const { const {
@@ -126,7 +130,7 @@ function WorkflowApprovalsList() {
const handleApprove = async () => { const handleApprove = async () => {
await approveWorkflowApprovals(); await approveWorkflowApprovals();
setSelected([]); clearSelected();
}; };
const { const {
@@ -144,7 +148,7 @@ function WorkflowApprovalsList() {
const handleDeny = async () => { const handleDeny = async () => {
await denyWorkflowApprovals(); await denyWorkflowApprovals();
setSelected([]); clearSelected();
}; };
const { const {
@@ -168,7 +172,7 @@ function WorkflowApprovalsList() {
itemCount={count} itemCount={count}
pluralizedItemName={t`Workflow Approvals`} pluralizedItemName={t`Workflow Approvals`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -187,9 +191,7 @@ function WorkflowApprovalsList() {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={set => onSelectAll={selectAll}
setSelected(set ? [...workflowApprovals] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
<WorkflowApprovalListApproveButton <WorkflowApprovalListApproveButton

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useCallback } from 'react';
/** /**
* useSelected hook provides a way to read and update a selected list * useSelected hook provides a way to read and update a selected list
@@ -8,6 +8,7 @@ import { useState } from 'react';
* isAllSelected: boolean that indicates if all items are selected * isAllSelected: boolean that indicates if all items are selected
* handleSelect: function that adds and removes items from selected list * handleSelect: function that adds and removes items from selected list
* setSelected: setter function * setSelected: setter function
* clearSelected: de-select all items
* } * }
*/ */
@@ -16,6 +17,9 @@ export default function useSelected(list = []) {
const isAllSelected = selected.length > 0 && selected.length === list.length; const isAllSelected = selected.length > 0 && selected.length === list.length;
const handleSelect = row => { const handleSelect = row => {
if (!row.id) {
throw new Error(`Selected row does not have an id`);
}
if (selected.some(s => s.id === row.id)) { if (selected.some(s => s.id === row.id)) {
setSelected(prevState => [...prevState.filter(i => i.id !== row.id)]); setSelected(prevState => [...prevState.filter(i => i.id !== row.id)]);
} else { } else {
@@ -23,5 +27,23 @@ export default function useSelected(list = []) {
} }
}; };
return { selected, isAllSelected, handleSelect, setSelected }; const selectAll = useCallback(
isSelected => {
setSelected(isSelected ? [...list] : []);
},
[list]
);
const clearSelected = useCallback(() => {
setSelected([]);
}, []);
return {
selected,
isAllSelected,
handleSelect,
setSelected,
selectAll,
clearSelected,
};
} }

View File

@@ -19,6 +19,8 @@ describe('useSelected hook', () => {
let isAllSelected; let isAllSelected;
let handleSelect; let handleSelect;
let setSelected; let setSelected;
let selectAll;
let clearSelected;
test('should return expected initial values', () => { test('should return expected initial values', () => {
testHook(() => { testHook(() => {
@@ -72,4 +74,51 @@ describe('useSelected hook', () => {
expect(selected).toEqual([]); expect(selected).toEqual([]);
expect(isAllSelected).toEqual(false); expect(isAllSelected).toEqual(false);
}); });
test('should return selectAll', () => {
testHook(() => {
({
selected,
isAllSelected,
handleSelect,
setSelected,
selectAll,
} = useSelected(array));
});
act(() => {
selectAll(true);
});
expect(isAllSelected).toEqual(true);
expect(selected).toEqual(array);
act(() => {
selectAll(false);
});
expect(isAllSelected).toEqual(false);
expect(selected).toEqual([]);
});
test('should return clearSelected', () => {
testHook(() => {
({
selected,
isAllSelected,
handleSelect,
setSelected,
selectAll,
clearSelected,
} = useSelected(array));
});
act(() => {
selectAll(true);
});
act(() => {
clearSelected();
});
expect(isAllSelected).toEqual(false);
expect(selected).toEqual([]);
});
}); });