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();