Merge pull request #10623 from AlexSCorey/4208-ExpandWholeList

Adds functionality for expand all 

SUMMARY
Closes #4208.  Adds basically the same work as useSelected, in a useExpanded hook.
ISSUE TYPE

Feature Pull Request

COMPONENT NAME

UI

AWX VERSION
ADDITIONAL INFORMATION

Reviewed-by: Keith Grant <keithjgrant@gmail.com>
Reviewed-by: Sarah Akus <sakus@redhat.com>
This commit is contained in:
softwarefactory-project-zuul[bot] 2021-07-15 19:24:54 +00:00 committed by GitHub
commit 9bb7d918eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 278 additions and 67 deletions

View File

@ -4,6 +4,7 @@ import styled from 'styled-components';
import { t } from '@lingui/macro';
import {
Button,
Checkbox,
Toolbar,
ToolbarContent as PFToolbarContent,
@ -14,7 +15,11 @@ import {
DropdownPosition,
KebabToggle,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
import {
AngleDownIcon,
AngleRightIcon,
SearchIcon,
} from '@patternfly/react-icons';
import ExpandCollapse from '../ExpandCollapse';
import Search from '../Search';
import Sort from '../Sort';
@ -28,14 +33,16 @@ const ToolbarContent = styled(PFToolbarContent)`
`;
function DataListToolbar({
isAllExpanded,
onExpandAll,
itemCount,
clearAllFilters,
searchColumns,
searchableKeys,
relatedSearchableKeys,
sortColumns,
showSelectAll,
isAllSelected,
onSelectAll,
isCompact,
onSort,
onSearch,
@ -43,7 +50,6 @@ function DataListToolbar({
onRemove,
onCompact,
onExpand,
onSelectAll,
additionalControls,
qsConfig,
pagination,
@ -70,7 +76,6 @@ function DataListToolbar({
setIsKebabOpen(false);
}
}, [isKebabModalOpen]);
return (
<Toolbar
id={`${qsConfig.namespace}-list-toolbar`}
@ -79,7 +84,27 @@ function DataListToolbar({
clearFiltersButtonText={t`Clear all filters`}
>
<ToolbarContent>
{showSelectAll && (
{onExpandAll && (
<ToolbarGroup>
<ToolbarItem>
<Button
onClick={() => {
onExpandAll(!isAllExpanded);
}}
aria-label={t`Expand all rows`}
ouiaId="expand-all-rows"
variant="plain"
>
{isAllExpanded ? (
<AngleDownIcon aria-label={t`Is expanded`} />
) : (
<AngleRightIcon aria-label={t`Is not expanded`} />
)}
</Button>
</ToolbarItem>
</ToolbarGroup>
)}
{onSelectAll && (
<ToolbarGroup>
<ToolbarItem>
<Checkbox
@ -178,7 +203,6 @@ DataListToolbar.propTypes = {
searchableKeys: PropTypes.arrayOf(PropTypes.string),
relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
sortColumns: SortColumns,
showSelectAll: PropTypes.bool,
isAllSelected: PropTypes.bool,
isCompact: PropTypes.bool,
onCompact: PropTypes.func,
@ -198,7 +222,6 @@ DataListToolbar.defaultProps = {
relatedSearchableKeys: [],
sortColumns: null,
clearAllFilters: null,
showSelectAll: false,
isAllSelected: false,
isCompact: false,
onCompact: null,

View File

@ -19,6 +19,7 @@ describe('<DataListToolbar />', () => {
const onReplaceSearch = jest.fn();
const onSort = jest.fn();
const onSelectAll = jest.fn();
const onExpandAll = jest.fn();
test('it triggers the expected callbacks', () => {
const searchColumns = [
@ -285,4 +286,61 @@ describe('<DataListToolbar />', () => {
1
);
});
test('should handle expanded rows', async () => {
const searchColumns = [
{ name: 'Name', key: 'name__icontains', isDefault: true },
];
const sortColumns = [{ name: 'Name', key: 'name' }];
const newtoolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
isAllSelected={false}
searchColumns={searchColumns}
sortColumns={sortColumns}
onSearch={onSearch}
onReplaceSearch={onReplaceSearch}
onSort={onSort}
onSelectAll={onSelectAll}
showSelectAll
showExpandAll
isAllExpanded={false}
onExpandAll={onExpandAll}
/>
);
await act(async () =>
newtoolbar.find('Button[aria-label="Expand all rows"]').prop('onClick')()
);
expect(newtoolbar.find('AngleRightIcon')).toHaveLength(1);
expect(newtoolbar.find('AngleDownIcon')).toHaveLength(0);
expect(onExpandAll).toBeCalledWith(true);
});
test('should render angle down icon', async () => {
const searchColumns = [
{ name: 'Name', key: 'name__icontains', isDefault: true },
];
const sortColumns = [{ name: 'Name', key: 'name' }];
const newtoolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
isAllSelected={false}
searchColumns={searchColumns}
sortColumns={sortColumns}
onSearch={onSearch}
onReplaceSearch={onReplaceSearch}
onSort={onSort}
onSelectAll={onSelectAll}
showSelectAll
showExpandAll
isAllExpanded
onExpandAll={onExpandAll}
/>
);
expect(newtoolbar.find('AngleDownIcon')).toHaveLength(1);
expect(newtoolbar.find('AngleRightIcon')).toHaveLength(0);
});
});

View File

@ -17,6 +17,7 @@ import useRequest, {
} from '../../util/useRequest';
import { useConfig } from '../../contexts/Config';
import useSelected from '../../util/useSelected';
import useExpanded from '../../util/useExpanded';
import { isJobRunning, getJobModel } from '../../util/jobs';
import { getQSConfig, parseQueryString } from '../../util/qs';
import JobListItem from './JobListItem';
@ -97,6 +98,10 @@ function JobList({ defaultParams, showTypeColumn = false }) {
clearSelected,
} = useSelected(jobs);
const { expanded, isAllExpanded, handleExpand, expandAll } = useExpanded(
jobs
);
const {
error: cancelJobsError,
isLoading: isCancelLoading,
@ -227,7 +232,8 @@ function JobList({ defaultParams, showTypeColumn = false }) {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllExpanded={isAllExpanded}
onExpandAll={expandAll}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={qsConfig}
@ -264,6 +270,8 @@ function JobList({ defaultParams, showTypeColumn = false }) {
<JobListItem
key={job.id}
job={job}
isExpanded={expanded.some(row => row.id === job.id)}
onExpand={() => handleExpand(job)}
isSuperUser={me?.is_superuser}
showTypeColumn={showTypeColumn}
onSelect={() => handleSelect(job)}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import { t } from '@lingui/macro';
@ -20,6 +20,8 @@ import JobCancelButton from '../JobCancelButton';
const Dash = styled.span``;
function JobListItem({
isExpanded,
onExpand,
job,
rowIndex,
isSelected,
@ -28,7 +30,6 @@ function JobListItem({
isSuperUser = false,
}) {
const labelId = `check-action-${job.id}`;
const [isExpanded, setIsExpanded] = useState(false);
const jobTypes = {
project_update: t`Source Control Update`,
@ -63,7 +64,7 @@ function JobListItem({
expand={{
rowIndex: job.id,
isExpanded,
onToggle: () => setIsExpanded(!isExpanded),
onToggle: onExpand,
}}
/>
<Td

View File

@ -204,7 +204,6 @@ function ScheduleList({
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -17,6 +17,7 @@ import PaginatedTable, {
} from '../PaginatedTable';
import useRequest, { useDeleteItems } from '../../util/useRequest';
import useSelected from '../../util/useSelected';
import useExpanded from '../../util/useExpanded';
import { getQSConfig, parseQueryString } from '../../util/qs';
import useWsTemplates from '../../util/useWsTemplates';
import AddDropDownButton from '../AddDropDownButton';
@ -97,6 +98,10 @@ function TemplateList({ defaultParams }) {
clearSelected,
} = useSelected(templates);
const { expanded, isAllExpanded, handleExpand, expandAll } = useExpanded(
templates
);
const {
isLoading: isDeleteLoading,
deleteItems: deleteTemplates,
@ -229,9 +234,10 @@ function TemplateList({ defaultParams }) {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
isAllExpanded={isAllExpanded}
onExpandAll={expandAll}
qsConfig={qsConfig}
additionalControls={[
...(canAddJT || canAddWFJT ? [addButton] : []),
@ -259,6 +265,8 @@ function TemplateList({ defaultParams }) {
template={template}
detailUrl={`/templates/${template.type}/${template.id}`}
onSelect={() => handleSelect(template)}
isExpanded={expanded.some(row => row.id === template.id)}
onExpand={() => handleExpand(template)}
isSelected={selected.some(row => row.id === template.id)}
fetchTemplates={fetchTemplates}
rowIndex={index}

View File

@ -34,6 +34,8 @@ const ExclamationTriangleIconWarning = styled(ExclamationTriangleIcon)`
ExclamationTriangleIconWarning.displayName = 'ExclamationTriangleIconWarning';
function TemplateListItem({
isExpanded,
onExpand,
template,
isSelected,
onSelect,
@ -42,7 +44,6 @@ function TemplateListItem({
rowIndex,
}) {
const config = useConfig();
const [isExpanded, setIsExpanded] = useState(false);
const [isDisabled, setIsDisabled] = useState(false);
const labelId = `check-action-${template.id}`;
@ -119,7 +120,7 @@ function TemplateListItem({
expand={{
rowIndex,
isExpanded,
onToggle: () => setIsExpanded(!isExpanded),
onToggle: onExpand,
}}
/>
<Td

View File

@ -381,6 +381,7 @@ describe('<TemplateListItem />', () => {
<tbody>
<TemplateListItem
isSelected={false}
isExpanded
detailUrl="/templates/job_template/1/details"
template={{
...mockJobTemplateData,
@ -390,15 +391,7 @@ describe('<TemplateListItem />', () => {
</tbody>
</table>
);
expect(
wrapper
.find('Tr')
.last()
.prop('isExpanded')
).toBe(false);
await act(async () =>
wrapper.find('button[aria-label="Details"]').simulate('click')
);
wrapper.update();
expect(
wrapper

View File

@ -121,7 +121,6 @@ function ApplicationTokenList() {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -134,7 +134,6 @@ function ApplicationsList() {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -161,7 +161,6 @@ function CredentialList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -148,7 +148,6 @@ function CredentialTypeList() {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -164,7 +164,6 @@ function ExecutionEnvironmentList() {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -196,7 +196,6 @@ function HostGroupsList({ host }) {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -171,7 +171,6 @@ function HostList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -258,7 +258,6 @@ function InstanceGroupList({
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -174,7 +174,6 @@ function InstanceList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -218,7 +218,6 @@ function InventoryGroupHostList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={isSelected =>
setSelected(isSelected ? [...hosts] : [])

View File

@ -159,7 +159,6 @@ function InventoryGroupsList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -197,7 +197,6 @@ function InventoryHostGroupsList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -140,7 +140,6 @@ function InventoryHostList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -214,7 +214,6 @@ function InventoryList() {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -192,7 +192,6 @@ function InventoryRelatedGroupList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={isSelected =>
setSelected(isSelected ? [...groups] : [])

View File

@ -169,7 +169,6 @@ function InventorySourceList() {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -87,7 +87,6 @@ function SmartInventoryHostList({ inventory }) {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -91,11 +91,7 @@ function ManagementJobList() {
},
]}
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll={false}
qsConfig={QS_CONFIG}
/>
<DatalistToolbar {...props} qsConfig={QS_CONFIG} />
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>

View File

@ -176,7 +176,6 @@ function NotificationTemplatesList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -156,7 +156,6 @@ function OrganizationsList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -141,7 +141,6 @@ function ProjectJobTemplatesList() {
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -18,6 +18,7 @@ import PaginatedTable, {
} from '../../../components/PaginatedTable';
import useWsProjects from './useWsProjects';
import useSelected from '../../../util/useSelected';
import useExpanded from '../../../util/useExpanded';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import { getQSConfig, parseQueryString } from '../../../util/qs';
@ -103,6 +104,10 @@ function ProjectList() {
clearSelected,
} = useSelected(projects);
const { expanded, isAllExpanded, handleExpand, expandAll } = useExpanded(
projects
);
const {
isLoading: isDeleteLoading,
deleteItems: deleteProjects,
@ -212,7 +217,8 @@ function ProjectList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllExpanded={isAllExpanded}
onExpandAll={expandAll}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}
@ -244,6 +250,8 @@ function ProjectList() {
)}
renderRow={(project, index) => (
<ProjectListItem
isExpanded={expanded.some(row => row.id === project.id)}
onExpand={() => handleExpand(project)}
fetchProjects={fetchProjects}
key={project.id}
project={project}

View File

@ -38,6 +38,8 @@ const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)`
`;
function ProjectListItem({
isExpanded,
onExpand,
project,
isSelected,
onSelect,
@ -46,7 +48,6 @@ function ProjectListItem({
rowIndex,
onRefreshRow,
}) {
const [isExpanded, setIsExpanded] = useState(false);
const [isDisabled, setIsDisabled] = useState(false);
ProjectListItem.propTypes = {
project: Project.isRequired,
@ -165,7 +166,7 @@ function ProjectListItem({
expand={{
rowIndex,
isExpanded,
onToggle: () => setIsExpanded(!isExpanded),
onToggle: onExpand,
}}
/>
<Td

View File

@ -393,6 +393,7 @@ describe('<ProjectsListItem />', () => {
<tbody>
<ProjectsListItem
rowIndex={1}
isExpanded
isSelected={false}
detailUrl="/project/1"
onSelect={() => {}}
@ -431,16 +432,7 @@ describe('<ProjectsListItem />', () => {
</tbody>
</table>
);
expect(
wrapper
.find('Tr')
.last()
.prop('isExpanded')
).toBe(false);
await act(async () =>
wrapper.find('button[aria-label="Details"]').simulate('click')
);
wrapper.update();
expect(
wrapper
.find('Tr')

View File

@ -152,7 +152,6 @@ function TeamList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -135,7 +135,6 @@ function UserList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -202,7 +202,6 @@ function UserTeamList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -150,7 +150,6 @@ function UserTokenList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
qsConfig={QS_CONFIG}
onSelectAll={selectAll}

View File

@ -189,7 +189,6 @@ function WorkflowApprovalsList() {
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}

View File

@ -0,0 +1,32 @@
import { useState, useCallback } from 'react';
export default function useExpanded(list = []) {
const [expanded, setExpanded] = useState([]);
const isAllExpanded = expanded.length > 0 && expanded.length === list.length;
const handleExpand = row => {
if (!row.id) {
throw new Error(`Selected row does not have an id`);
}
if (expanded.some(s => s.id === row.id)) {
setExpanded(prevState => [...prevState.filter(i => i.id !== row.id)]);
} else {
setExpanded(prevState => [...prevState, row]);
}
};
const expandAll = useCallback(
isExpanded => {
setExpanded(isExpanded ? [...list] : []);
},
[list]
);
return {
expanded,
isAllExpanded,
handleExpand,
setExpanded,
expandAll,
};
}

View File

@ -0,0 +1,116 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import useExpanded from './useExpanded';
const array = [{ id: '1' }, { id: '2' }, { id: '3' }];
const TestHook = ({ callback }) => {
callback();
return null;
};
const testHook = callback => {
mount(<TestHook callback={callback} />);
};
describe('useSelected hook', () => {
let expanded;
let isAllExpanded;
let handleExpand;
let setExpanded;
let expandAll;
test('should return expected initial values', () => {
testHook(() => {
({
expanded,
isAllExpanded,
handleExpand,
setExpanded,
expandAll,
} = useExpanded());
});
expect(expanded).toEqual([]);
expect(isAllExpanded).toEqual(false);
expect(handleExpand).toBeInstanceOf(Function);
expect(setExpanded).toBeInstanceOf(Function);
});
test('handleSelect should update and filter selected items', () => {
testHook(() => {
({
expanded,
isAllExpanded,
handleExpand,
setExpanded,
expandAll,
} = useExpanded());
});
act(() => {
handleExpand(array[0]);
});
expect(expanded).toEqual([array[0]]);
act(() => {
handleExpand(array[0]);
});
expect(expanded).toEqual([]);
});
test('should return expected isAllSelected value', () => {
testHook(() => {
({
expanded,
isAllExpanded,
handleExpand,
setExpanded,
expandAll,
} = useExpanded(array));
});
act(() => {
handleExpand(array[0]);
});
expect(expanded).toEqual([array[0]]);
expect(isAllExpanded).toEqual(false);
act(() => {
handleExpand(array[1]);
handleExpand(array[2]);
});
expect(expanded).toEqual(array);
expect(isAllExpanded).toEqual(true);
act(() => {
setExpanded([]);
});
expect(expanded).toEqual([]);
expect(isAllExpanded).toEqual(false);
});
test('should return selectAll', () => {
testHook(() => {
({
expanded,
isAllExpanded,
handleExpand,
setExpanded,
expandAll,
} = useExpanded(array));
});
act(() => {
expandAll(true);
});
expect(isAllExpanded).toEqual(true);
expect(expanded).toEqual(array);
act(() => {
expandAll(false);
});
expect(isAllExpanded).toEqual(false);
expect(expanded).toEqual([]);
});
});