Wizard: Add "refine search" warning to Packages step

Fixes #913.

This adds status bars to the `DualListSelectorPane` on the Packages step. The status bar indicates how many packages were found and how many of those have been selected.

Warning for too many returned results was also added. When an exact match is found during a search with over 100 results, it is shown together with the warning.
This commit is contained in:
regexowl 2023-04-06 09:57:27 +02:00 committed by Lucas Garfield
parent c3c8a687a0
commit e5a9f0eaf9
4 changed files with 138 additions and 25 deletions

View file

@ -9,8 +9,8 @@
}
.pf-c-dual-list-selector {
--pf-c-dual-list-selector__menu--MinHeight: 18.5rem;
--pf-c-dual-list-selector__menu--MaxHeight: 18.5rem;
--pf-c-dual-list-selector__menu--MinHeight: 15.5rem;
--pf-c-dual-list-selector__menu--MaxHeight: 15.5rem;
}
.pf-c-form {

View file

@ -10,6 +10,8 @@ import React, {
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
import {
Alert,
Divider,
DualListSelector,
DualListSelectorControl,
DualListSelectorControlsWrapper,
@ -31,6 +33,31 @@ import api from '../../../api';
import { useGetArchitecturesByDistributionQuery } from '../../../store/apiSlice';
import isBeta from '../../../Utilities/isBeta';
const ExactMatch = ({
pkgList,
search,
chosenPackages,
selectedAvailablePackages,
handleSelectAvailableFunc,
}) => {
const match = pkgList.find((pkg) => pkg.name === search);
return (
<DualListSelectorListItem
data-testid={`exact-match-${match.name}`}
isDisabled={chosenPackages[match.name] ? true : false}
isSelected={selectedAvailablePackages.has(match.name)}
onOptionSelect={(e) => handleSelectAvailableFunc(e, match.name)}
>
<TextContent key={`${match.name}`}>
<small className="pf-u-mb-sm">Exact match</small>
<span className="pf-c-dual-list-selector__item-text">{match.name}</span>
<small>{match.summary}</small>
<Divider />
</TextContent>
</DualListSelectorListItem>
);
};
export const RedHatPackages = ({ defaultArch }) => {
const { getState } = useFormApi();
const distribution = getState()?.values?.release;
@ -269,20 +296,36 @@ const Packages = ({ getAllPackages, isSuccess }) => {
<DualListSelectorPane
title="Available packages"
searchInput={
<SearchInput
placeholder="Search for a package"
data-testid="search-available-pkgs-input"
value={packagesSearchName}
ref={firstInputElement}
onFocus={() => setFocus('available')}
onBlur={() => setFocus('')}
onChange={(_, val) => setPackagesSearchName(val)}
submitSearchButtonLabel="Search button for available packages"
onSearch={handleAvailablePackagesSearch}
resetButtonLabel="Clear available packages search"
onClear={handleClearAvailableSearch}
isDisabled={currentStep.name === 'packages' ? !isSuccess : false}
/>
<>
<SearchInput
placeholder="Search for a package"
data-testid="search-available-pkgs-input"
value={packagesSearchName}
ref={firstInputElement}
onFocus={() => setFocus('available')}
onBlur={() => setFocus('')}
onChange={(_, val) => setPackagesSearchName(val)}
submitSearchButtonLabel="Search button for available packages"
onSearch={handleAvailablePackagesSearch}
resetButtonLabel="Clear available packages search"
onClear={handleClearAvailableSearch}
isDisabled={currentStep.name === 'packages' ? !isSuccess : false}
/>
{availablePackagesDisplayList.length >= 100 && (
<Alert
title="Over 100 results found. Refine your search."
variant="warning"
isPlain
isInline
/>
)}
</>
}
status={
selectedAvailablePackages.size > 0
? `${selectedAvailablePackages.size}
of ${availablePackagesDisplayList.length} items`
: `${availablePackagesDisplayList.length} items`
}
>
<DualListSelectorList data-testid="available-pkgs-list">
@ -293,9 +336,38 @@ const Packages = ({ getAllPackages, isSuccess }) => {
packages to your image
</p>
) : availablePackagesDisplayList.length === 0 ? (
<p className="pf-u-text-align-center pf-u-mt-md">
No packages found
</p>
<>
<p className="pf-u-text-align-center pf-u-mt-md pf-u-font-size-lg pf-u-font-weight-bold">
No results found
</p>
<br />
<p className="pf-u-text-align-center pf-u-mt-md">
Adjust your search and try again
</p>
</>
) : availablePackagesDisplayList.length >= 100 ? (
<>
{availablePackagesDisplayList.some(
(pkg) => pkg.name === packagesSearchName
) && (
<ExactMatch
pkgList={availablePackagesDisplayList}
search={packagesSearchName}
chosenPackages={chosenPackages}
selectedAvailablePackages={selectedAvailablePackages}
handleSelectAvailableFunc={handleSelectAvailable}
/>
)}
<p className="pf-u-text-align-center pf-u-mt-md pf-u-font-size-lg pf-u-font-weight-bold">
Too many results to display
</p>
<br />
<p className="pf-u-text-align-center pf-u-mt-md">
Please make the search more specific
<br />
and try again
</p>
</>
) : (
availablePackagesDisplayList.map((pkg) => {
return (
@ -366,6 +438,12 @@ const Packages = ({ getAllPackages, isSuccess }) => {
onClear={handleClearChosenSearch}
/>
}
status={
selectedChosenPackages.size > 0
? `${selectedChosenPackages.size}
of ${chosenPackagesDisplayList.length} items`
: `${chosenPackagesDisplayList.length} items`
}
isChosen
>
<DualListSelectorList data-testid="chosen-pkgs-list">
@ -402,6 +480,14 @@ const Packages = ({ getAllPackages, isSuccess }) => {
);
};
ExactMatch.propTypes = {
pkgList: PropTypes.arrayOf(PropTypes.object),
search: PropTypes.string,
chosenPackages: PropTypes.object,
selectedAvailablePackages: PropTypes.arrayOf(PropTypes.string),
handleSelectAvailableFunc: PropTypes.func,
};
RedHatPackages.propTypes = {
defaultArch: PropTypes.string,
};

View file

@ -486,7 +486,7 @@ describe('Step Packages', () => {
await searchForAvailablePackages(searchbox, 'asdf');
await screen.findByText('No packages found');
await screen.findByText('No results found');
});
test('should display empty chosen state on failed search', async () => {

View file

@ -935,7 +935,7 @@ describe('Step Packages', () => {
searchbox.click();
await searchForAvailablePackages(searchbox, 'asdf');
screen.getByText('No packages found');
screen.getByText('No results found');
});
test('should display empty available state on failed search after a successful search', async () => {
@ -955,7 +955,7 @@ describe('Step Packages', () => {
await searchForAvailablePackages(searchbox, 'asdf');
screen.getByText('No packages found');
screen.getByText('No results found');
});
test('should display empty chosen state on failed search', async () => {
@ -979,7 +979,7 @@ describe('Step Packages', () => {
await searchForChosenPackages(searchboxChosen, '');
});
test('should get all packages, regardless of api default limit', async () => {
test('should display warning when over hundred results were found', async () => {
await setUp();
const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref
@ -999,11 +999,38 @@ describe('Step Packages', () => {
await searchForAvailablePackages(searchbox, 'testPkg');
expect(getPackages).toHaveBeenCalledTimes(2);
screen.getByText('Over 100 results found. Refine your search.');
screen.getByText('Too many results to display');
});
test('should display an exact match if found regardless of too many results', async () => {
await setUp();
const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref
expect(searchbox).toBeDisabled();
await waitFor(() => expect(searchbox).toBeEnabled());
searchbox.click();
const getPackages = jest
.spyOn(api, 'getPackages')
.mockImplementation((distribution, architecture, search, limit) => {
return limit
? Promise.resolve(mockPkgResultAll)
: Promise.resolve(mockPkgResultPartial);
});
await searchForAvailablePackages(searchbox, 'testPkg-128');
expect(getPackages).toHaveBeenCalledTimes(2);
const availablePackagesList = screen.getByTestId('available-pkgs-list');
const availablePackagesItems = within(availablePackagesList).getAllByRole(
const availablePackagesItems = within(availablePackagesList).getByRole(
'option'
);
expect(availablePackagesItems).toHaveLength(132);
expect(availablePackagesItems).toBeInTheDocument();
screen.getByText('Exact match');
screen.getByText('testPkg-128');
screen.getByText('Too many results to display');
});
test('search results should be sorted alphabetically', async () => {