CreateImageWizard: update packages to use custom callbacks
PF4 now allows more control over the DualListSelector. This component is rewritten to use custom callbacks which allow us to display a more customized version of the DualListSelector. Currently, the component is visually identical to the existing implemention except for the addition of a search button to filter the chosen packages.
This commit is contained in:
parent
4bcbd0adbc
commit
58f866088e
2 changed files with 192 additions and 63 deletions
|
|
@ -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) => (
|
||||
<TextContent key={ `${pack.name}-${key}` }>
|
||||
<span className="pf-c-dual-list-selector__item-text">{ pack.name }</span>
|
||||
<small>{ pack.summary }</small>
|
||||
</TextContent>
|
||||
));
|
||||
|
||||
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 <DualListSelector
|
||||
className="pf-u-mt-sm"
|
||||
isSearchable
|
||||
availableOptionsActions={ [
|
||||
<Button
|
||||
aria-label="Search button for available packages"
|
||||
key="availableSearchButton"
|
||||
data-testid="search-pkgs-button"
|
||||
onClick={ handlePackagesSearch }>
|
||||
Search
|
||||
</Button>
|
||||
] }
|
||||
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 (
|
||||
<DualListSelector>
|
||||
<DualListSelectorPane
|
||||
title="Available packages"
|
||||
searchInput={ <SearchInput
|
||||
data-testid="search-available-pkgs-input"
|
||||
value={ packagesSearchName.current }
|
||||
onChange={ (val) => {
|
||||
packagesSearchName.current = val;
|
||||
} } /> }
|
||||
actions={ [
|
||||
<Button
|
||||
aria-label="Search button for available packages"
|
||||
key="availableSearchButton"
|
||||
data-testid="search-available-pkgs-button"
|
||||
onClick={ handlePackagesAvailableSearch }>
|
||||
Search
|
||||
</Button>
|
||||
] }>
|
||||
<DualListSelectorList>
|
||||
{packagesAvailable.map((pack, index) => {
|
||||
return !pack.isHidden ? (
|
||||
<DualListSelectorListItem
|
||||
key={ index }
|
||||
isSelected={ pack.selected }
|
||||
onOptionSelect={ (e) => onOptionSelect(e, index, false) }>
|
||||
<TextContent key={ `${pack.name}-${index}` }>
|
||||
<span className="pf-c-dual-list-selector__item-text">{ pack.name }</span>
|
||||
<small>{ pack.summary }</small>
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
) : null;
|
||||
})}
|
||||
</DualListSelectorList>
|
||||
</DualListSelectorPane>
|
||||
<DualListSelectorControlsWrapper
|
||||
aria-label="Selector controls">
|
||||
<DualListSelectorControl
|
||||
isDisabled={ !packagesAvailable.some(option => option.selected) }
|
||||
onClick={ () => moveSelected(true) }
|
||||
aria-label="Add selected"
|
||||
tooltipContent="Add selected">
|
||||
<AngleRightIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
isDisabled={ packagesAvailable.length === 0 }
|
||||
onClick={ () => moveAll(true) }
|
||||
aria-label="Add all"
|
||||
tooltipContent="Add all">
|
||||
<AngleDoubleRightIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
isDisabled={ packagesChosen.length === 0 }
|
||||
onClick={ () => moveAll(false) }
|
||||
aria-label="Remove all"
|
||||
tooltipContent="Remove all">
|
||||
<AngleDoubleLeftIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
onClick={ () => moveSelected(false) }
|
||||
isDisabled={ !packagesChosen.some(option => option.selected) }
|
||||
aria-label="Remove selected"
|
||||
tooltipContent="Remove selected">
|
||||
<AngleLeftIcon />
|
||||
</DualListSelectorControl>
|
||||
</DualListSelectorControlsWrapper>
|
||||
<DualListSelectorPane
|
||||
title="Chosen packages"
|
||||
searchInput={ <SearchInput
|
||||
value={ filterChosen }
|
||||
onChange={ (val) => setFilterChosen(val) } /> }
|
||||
actions={ [
|
||||
<Button
|
||||
aria-label="Search button for selected packages"
|
||||
key="selectedSearchButton"
|
||||
data-testid="search-selected-pkgs-button"
|
||||
onClick={ handlePackagesChosenSearch }>
|
||||
Search
|
||||
</Button>
|
||||
] }
|
||||
isChosen>
|
||||
<DualListSelectorList>
|
||||
{packagesChosen.map((pack, index) => {
|
||||
return !pack.isHidden ? (
|
||||
<DualListSelectorListItem
|
||||
key={ index }
|
||||
isSelected={ pack.selected }
|
||||
onOptionSelect={ (e) => onOptionSelect(e, index, true) }>
|
||||
<TextContent key={ `${pack.name}-${index}` }>
|
||||
<span className="pf-c-dual-list-selector__item-text">{ pack.name }</span>
|
||||
<small>{ pack.summary }</small>
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
) : null;
|
||||
})}
|
||||
</DualListSelectorList>
|
||||
</DualListSelectorPane>
|
||||
</DualListSelector>
|
||||
);
|
||||
};
|
||||
|
||||
Packages.propTypes = {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue