From 6a717a8f3c126e7e4bd36aa539145358a06918c8 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 8 Jul 2021 15:42:21 -0400 Subject: [PATCH 1/2] Adds functionality for expand all in lists that have list items that can be expanded --- .../DataListToolbar/DataListToolbar.jsx | 37 ++++-- .../DataListToolbar/DataListToolbar.test.jsx | 58 +++++++++ .../src/components/JobList/JobList.jsx | 10 +- .../src/components/JobList/JobListItem.jsx | 7 +- .../components/TemplateList/TemplateList.jsx | 10 +- .../TemplateList/TemplateListItem.jsx | 5 +- .../TemplateList/TemplateListItem.test.jsx | 11 +- .../Project/ProjectList/ProjectList.jsx | 10 +- .../Project/ProjectList/ProjectListItem.jsx | 5 +- .../ProjectList/ProjectListItem.test.jsx | 12 +- awx/ui_next/src/util/useExpanded.jsx | 32 +++++ awx/ui_next/src/util/useExpanded.test.jsx | 116 ++++++++++++++++++ 12 files changed, 277 insertions(+), 36 deletions(-) create mode 100644 awx/ui_next/src/util/useExpanded.jsx create mode 100644 awx/ui_next/src/util/useExpanded.test.jsx diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx index 268fd3d3e9..57f135d05c 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx @@ -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 ( - {showSelectAll && ( + {onExpandAll && ( + + + + + + )} + {onSelectAll && ( ', () => { const onReplaceSearch = jest.fn(); const onSort = jest.fn(); const onSelectAll = jest.fn(); + const onExpandAll = jest.fn(); test('it triggers the expected callbacks', () => { const searchColumns = [ @@ -353,4 +354,61 @@ describe('', () => { 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( + + ); + 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( + + ); + + expect(newtoolbar.find('AngleDownIcon')).toHaveLength(1); + expect(newtoolbar.find('AngleRightIcon')).toHaveLength(0); + }); }); diff --git a/awx/ui_next/src/components/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx index 07702628c3..83e8e8f851 100644 --- a/awx/ui_next/src/components/JobList/JobList.jsx +++ b/awx/ui_next/src/components/JobList/JobList.jsx @@ -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 => ( row.id === job.id)} + onExpand={() => handleExpand(job)} isSuperUser={me?.is_superuser} showTypeColumn={showTypeColumn} onSelect={() => handleSelect(job)} diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx index 85d2ade5bd..fa0f138820 100644 --- a/awx/ui_next/src/components/JobList/JobListItem.jsx +++ b/awx/ui_next/src/components/JobList/JobListItem.jsx @@ -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`, @@ -57,7 +58,7 @@ function JobListItem({ expand={{ rowIndex: job.id, isExpanded, - onToggle: () => setIsExpanded(!isExpanded), + onToggle: onExpand, }} /> ( 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} diff --git a/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx index 57c25073da..a17dd7a7aa 100644 --- a/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx @@ -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, }} /> ', () => { ', () => { ); - expect( - wrapper - .find('Tr') - .last() - .prop('isExpanded') - ).toBe(false); - await act(async () => - wrapper.find('button[aria-label="Details"]').simulate('click') - ); + wrapper.update(); expect( wrapper diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx index 4170ba2933..ffef685fc0 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx @@ -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 => ( ( row.id === project.id)} + onExpand={() => handleExpand(project)} fetchProjects={fetchProjects} key={project.id} project={project} diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index 894bb09947..349bd86ee6 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -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, }} /> ', () => { {}} @@ -431,16 +432,7 @@ describe('', () => { ); - 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') diff --git a/awx/ui_next/src/util/useExpanded.jsx b/awx/ui_next/src/util/useExpanded.jsx new file mode 100644 index 0000000000..0b410ea8f5 --- /dev/null +++ b/awx/ui_next/src/util/useExpanded.jsx @@ -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, + }; +} diff --git a/awx/ui_next/src/util/useExpanded.test.jsx b/awx/ui_next/src/util/useExpanded.test.jsx new file mode 100644 index 0000000000..9fe7773d0d --- /dev/null +++ b/awx/ui_next/src/util/useExpanded.test.jsx @@ -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(); +}; + +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([]); + }); +}); From 8198f045f9b0633f280333e88e38e7a80909dce3 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Mon, 12 Jul 2021 14:42:38 -0400 Subject: [PATCH 2/2] removes unnecessary prop --- .../src/components/Schedule/ScheduleList/ScheduleList.jsx | 1 - .../Application/ApplicationTokens/ApplicationTokenList.jsx | 1 - .../Application/ApplicationsList/ApplicationsList.jsx | 1 - .../screens/Credential/CredentialList/CredentialList.jsx | 1 - .../CredentialTypeList/CredentialTypeList.jsx | 1 - .../ExecutionEnvironmentList/ExecutionEnvironmentList.jsx | 1 - awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx | 1 - awx/ui_next/src/screens/Host/HostList/HostList.jsx | 1 - .../InstanceGroup/InstanceGroupList/InstanceGroupList.jsx | 1 - .../src/screens/InstanceGroup/Instances/InstanceList.jsx | 1 - .../InventoryGroupHosts/InventoryGroupHostList.jsx | 1 - .../Inventory/InventoryGroups/InventoryGroupsList.jsx | 1 - .../InventoryHostGroups/InventoryHostGroupsList.jsx | 1 - .../screens/Inventory/InventoryHosts/InventoryHostList.jsx | 1 - .../src/screens/Inventory/InventoryList/InventoryList.jsx | 1 - .../InventoryRelatedGroups/InventoryRelatedGroupList.jsx | 1 - .../Inventory/InventorySources/InventorySourceList.jsx | 1 - .../SmartInventoryHosts/SmartInventoryHostList.jsx | 1 - .../ManagementJob/ManagementJobList/ManagementJobList.jsx | 6 +----- .../NotificationTemplateList/NotificationTemplateList.jsx | 1 - .../Organization/OrganizationList/OrganizationList.jsx | 1 - .../ProjectJobTemplatesList/ProjectJobTemplatesList.jsx | 1 - awx/ui_next/src/screens/Team/TeamList/TeamList.jsx | 1 - awx/ui_next/src/screens/User/UserList/UserList.jsx | 1 - awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx | 1 - .../src/screens/User/UserTokenList/UserTokenList.jsx | 1 - .../WorkflowApprovalList/WorkflowApprovalList.jsx | 1 - 27 files changed, 1 insertion(+), 31 deletions(-) diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx index 3ed3db2a44..28e09840ae 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx @@ -204,7 +204,6 @@ function ScheduleList({ renderToolbar={props => ( ( ( ( ( ( ( ( ( ( ( setSelected(isSelected ? [...hosts] : []) diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx index 28981c9be9..2d759d3fb0 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx @@ -159,7 +159,6 @@ function InventoryGroupsList() { renderToolbar={props => ( ( ( ( ( setSelected(isSelected ? [...groups] : []) diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx index 14787eb602..c998869c9d 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx @@ -169,7 +169,6 @@ function InventorySourceList() { renderToolbar={props => ( ( ( - + )} headerRow={ diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx index b3abf09f1e..be14f2f779 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx @@ -176,7 +176,6 @@ function NotificationTemplatesList() { renderToolbar={props => ( ( ( ( ( ( ( (