diff --git a/src/Components/CreateImageWizard/formComponents/Packages.js b/src/Components/CreateImageWizard/formComponents/Packages.js index 8d1af899..1cb57e32 100644 --- a/src/Components/CreateImageWizard/formComponents/Packages.js +++ b/src/Components/CreateImageWizard/formComponents/Packages.js @@ -1,79 +1,211 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, } from 'react'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api'; -import { DualListSelector, Button, TextContent } from '@patternfly/react-core'; import api from '../../../api'; import PropTypes from 'prop-types'; +import { + Button, + DualListSelector, + DualListSelectorPane, + DualListSelectorList, + DualListSelectorListItem, + DualListSelectorControlsWrapper, + DualListSelectorControl, + SearchInput, + TextContent +} from '@patternfly/react-core'; +import { AngleDoubleLeftIcon, AngleLeftIcon, AngleDoubleRightIcon, AngleRightIcon } from '@patternfly/react-icons'; -const mapPackagesToComponent = (packages) => packages.map((pack, key) => ( - - { pack.name } - { pack.summary } - -)); - -const mapComponentToPackage = (component) => ({ - name: component.props.children[0].props.children, - summary: component.props.children[1].props.children -}); +// the fields isHidden and isSelected should not be included in the package list sent for image creation +const removePackagesDisplayFields = (packages) => packages.map((pack) => ({ + name: pack.name, + summary: pack.summary, +})); const Packages = ({ defaultArch, ...props }) => { const { change, getState } = useFormApi(); const { input } = useFieldApi(props); const packagesSearchName = useRef(); const [ packagesAvailable, setPackagesAvailable ] = useState([]); - const [ packagesSelected, setPackagesSelected ] = useState([]); - const [ filterSelected, setFilterSelected ] = useState(''); + const [ packagesChosen, setPackagesChosen ] = useState([]); + const [ filterChosen, setFilterChosen ] = useState(''); - useEffect(() => { - setPackagesSelected(mapPackagesToComponent(getState()?.values?.[input.name] || [])); - }, []); - - const packageListChange = (newAvailablePackages, newChosenPackages) => { - const chosenPkgs = newChosenPackages.map(mapComponentToPackage); - setPackagesAvailable(newAvailablePackages); - setPackagesSelected(newChosenPackages); - change(input.name, chosenPkgs); - }; - - const handlePackagesSearch = async () => { + // call api to list available packages + const handlePackagesAvailableSearch = async () => { const { data } = await api.getPackages( getState()?.values?.release, getState()?.values?.architecture || defaultArch, packagesSearchName.current ); - setPackagesAvailable(mapPackagesToComponent(data || [])); + setPackagesAvailable(data); }; - return - Search - - ] } - availableOptions={ packagesAvailable } - availableOptionsTitle="Available packages" - chosenOptions={ packagesSelected.filter((item) => mapComponentToPackage(item)?.name?.includes(filterSelected)) } - chosenOptionsTitle="Chosen packages" - addSelected={ packageListChange } - removeSelected={ packageListChange } - addAll={ (available, chosen) => packageListChange([], chosen.concat(available)) } - removeAll= { (newAvailablePackages) => packageListChange( - newAvailablePackages, - packagesSelected.filter((item) => !mapComponentToPackage(item)?.name?.includes(filterSelected)) - ) } - onAvailableOptionsSearchInputChanged={ (val) => { - packagesSearchName.current = val; - } } - onChosenOptionsSearchInputChanged={ (val) => setFilterSelected(val) } - filterOption={ () => true } - id="basicSelectorWithSearch" />; + // filter displayed selected packages + const handlePackagesChosenSearch = () => { + const filteredPackagesChosen = packagesChosen.map((pack) => { + if (!pack.name.includes(filterChosen)) { + pack.isHidden = true; + } else { + pack.isHidden = false; + } + + return pack; + }); + setPackagesChosen(filteredPackagesChosen); + }; + + // move selected packages + const moveSelected = (fromAvailable) => { + const sourcePackages = fromAvailable ? packagesAvailable : packagesChosen; + const destinationPackages = fromAvailable ? packagesChosen : packagesAvailable; + + const updatedSourcePackages = sourcePackages.filter((pack) => { + if (pack.selected) { + pack.selected = false; + destinationPackages.push(pack); + return false; + } + + return true; + }); + + if (fromAvailable) { + setPackagesAvailable(updatedSourcePackages); + setPackagesChosen([ ...destinationPackages ]); + } else { + setPackagesChosen(updatedSourcePackages); + setPackagesAvailable([ ...destinationPackages ]); + } + + // set the steps field to the current chosen packages list + change(input.name, removePackagesDisplayFields(packagesChosen)); + }; + + // move all packages + const moveAll = (fromAvailable) => { + if (fromAvailable) { + setPackagesChosen([ ...packagesAvailable.filter(pack => !pack.isHidden), ...packagesChosen ]); + setPackagesAvailable([ ...packagesAvailable.filter(pack => pack.isHidden) ]); + } else { + setPackagesAvailable([ ...packagesChosen.filter(pack => !pack.isHidden), ...packagesAvailable ]); + setPackagesChosen([ ...packagesChosen.filter(pack => pack.isHidden) ]); + } + + // set the steps field to the current chosen packages list + change(input.name, removePackagesDisplayFields(packagesChosen)); + }; + + const onOptionSelect = (event, index, isChosen) => { + if (isChosen) { + const newChosen = [ ...packagesChosen ]; + newChosen[index].selected = !packagesChosen[index].selected; + setPackagesChosen(newChosen); + } else { + const newAvailable = [ ...packagesAvailable ]; + newAvailable[index].selected = !packagesAvailable[index].selected; + setPackagesAvailable(newAvailable); + } + }; + + return ( + + { + packagesSearchName.current = val; + } } /> } + actions={ [ + + ] }> + + {packagesAvailable.map((pack, index) => { + return !pack.isHidden ? ( + onOptionSelect(e, index, false) }> + + { pack.name } + { pack.summary } + + + ) : null; + })} + + + + option.selected) } + onClick={ () => moveSelected(true) } + aria-label="Add selected" + tooltipContent="Add selected"> + + + moveAll(true) } + aria-label="Add all" + tooltipContent="Add all"> + + + moveAll(false) } + aria-label="Remove all" + tooltipContent="Remove all"> + + + moveSelected(false) } + isDisabled={ !packagesChosen.some(option => option.selected) } + aria-label="Remove selected" + tooltipContent="Remove selected"> + + + + setFilterChosen(val) } /> } + actions={ [ + + ] } + isChosen> + + {packagesChosen.map((pack, index) => { + return !pack.isHidden ? ( + onOptionSelect(e, index, true) }> + + { pack.name } + { pack.summary } + + + ) : null; + })} + + + + ); }; Packages.propTypes = { diff --git a/src/test/Components/CreateImageWizard/CreateImageWizard.test.js b/src/test/Components/CreateImageWizard/CreateImageWizard.test.js index efc7e915..ac933da1 100644 --- a/src/test/Components/CreateImageWizard/CreateImageWizard.test.js +++ b/src/test/Components/CreateImageWizard/CreateImageWizard.test.js @@ -425,10 +425,7 @@ describe('Step Packages', () => { }); test('should display search bar and button', () => { - const search = screen.getByRole('searchbox', { name: 'Available search input' }); - search.click(); - - userEvent.type(search, 'test'); + userEvent.type(screen.getByTestId('search-available-pkgs-input'), 'test'); screen.getByRole('button', { name: 'Search button for available packages' @@ -545,8 +542,8 @@ describe('Click through all steps', () => { }); screen.getByText('Add optional additional packages to your image by searching available packages.'); - userEvent.type(screen.getByRole('searchbox', { name: /Available search input/ }), 'test'); - screen.getByTestId('search-pkgs-button').click(); + userEvent.type(screen.getByTestId('search-available-pkgs-input'), 'test'); + screen.getByTestId('search-available-pkgs-button').click(); await expect(getPackages).toHaveBeenCalledTimes(1); screen.getByRole('option', { name: /testPkg test package summary/ }).click(); screen.getByRole('button', { name: /Add selected/ }).click();