CreateImageWizard: sort packages by relevance

Sort the package results in the CreateImageWizard
first by exact matches and then by pacakge results
that start with the same letters as the search term,
otherwise sort alphabetically.
Fixes #256
This commit is contained in:
Gianluca Zuccarelli 2021-11-02 15:46:20 +00:00 committed by jkozol
parent fe891eb56f
commit 49ac292ca0
2 changed files with 149 additions and 25 deletions

View file

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, } from 'react';
import React, { useState, useRef, useEffect, useCallback } 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 api from '../../../api';
@ -31,6 +31,45 @@ const Packages = ({ defaultArch, ...props }) => {
const [ filterChosen, setFilterChosen ] = useState('');
const [ focus, setFocus ] = useState('');
const searchResultsComparator = useCallback((searchTerm) => {
return (a, b) => {
// check exact match first
if (a.name === searchTerm) {
return -1;
}
if (b.name === searchTerm) {
return 1;
}
// check for packages that start with the search term
if (a.name.startsWith(searchTerm) && !b.name.startsWith(searchTerm)) {
return -1;
}
if (b.name.startsWith(searchTerm) && !a.name.startsWith(searchTerm)) {
return 1;
}
// if both (or neither) start with the search term
// sort alphabetically
if (a.name < b.name) {
return -1;
}
if (b.name < a.name) {
return 1;
}
return 0;
};
});
const sortPackages = useCallback((packageList) => {
const sortResults = packageList.sort(searchResultsComparator(packagesSearchName.current));
setPackagesAvailable(sortResults);
});
// call api to list available packages
const handlePackagesAvailableSearch = async () => {
const { data } = await api.getPackages(
@ -38,7 +77,7 @@ const Packages = ({ defaultArch, ...props }) => {
getState()?.values?.architecture || defaultArch,
packagesSearchName.current
);
setPackagesAvailable(data);
sortPackages(data);
};
// filter displayed selected packages
@ -91,11 +130,11 @@ const Packages = ({ defaultArch, ...props }) => {
});
if (fromAvailable) {
setPackagesAvailable(updatedSourcePackages);
sortPackages(updatedSourcePackages);
setPackagesChosen([ ...destinationPackages ]);
} else {
setPackagesChosen(updatedSourcePackages);
setPackagesAvailable([ ...destinationPackages ]);
sortPackages([ ...destinationPackages ]);
}
// set the steps field to the current chosen packages list
@ -106,9 +145,9 @@ const Packages = ({ defaultArch, ...props }) => {
const moveAll = (fromAvailable) => {
if (fromAvailable) {
setPackagesChosen([ ...packagesAvailable.filter(pack => !pack.isHidden), ...packagesChosen ]);
setPackagesAvailable([ ...packagesAvailable.filter(pack => pack.isHidden) ]);
sortPackages([ ...packagesAvailable.filter(pack => pack.isHidden) ]);
} else {
setPackagesAvailable([ ...packagesChosen.filter(pack => !pack.isHidden), ...packagesAvailable ]);
sortPackages([ ...packagesChosen.filter(pack => !pack.isHidden), ...packagesAvailable ]);
setPackagesChosen([ ...packagesChosen.filter(pack => pack.isHidden) ]);
}
@ -124,7 +163,7 @@ const Packages = ({ defaultArch, ...props }) => {
} else {
const newAvailable = [ ...packagesAvailable ];
newAvailable[index].selected = !packagesAvailable[index].selected;
setPackagesAvailable(newAvailable);
sortPackages(newAvailable);
}
};
@ -150,7 +189,7 @@ const Packages = ({ defaultArch, ...props }) => {
Search
</Button>
] }>
<DualListSelectorList>
<DualListSelectorList data-testid="available-pkgs-list">
{packagesAvailable.map((pack, index) => {
return !pack.isHidden ? (
<DualListSelectorListItem

View file

@ -28,6 +28,36 @@ function verifyCancelButton(cancel, history) {
expect(history).toHaveBeenCalledWith('/landing');
}
// packages
const mockPkgResult = {
meta: { count: 100 },
links: { first: '', last: '' },
data: [
{
name: 'testPkg',
summary: 'test package summary',
version: '1.0',
},
{
name: 'lib-test',
summary: 'lib-test package summary',
version: '1.0',
},
{
name: 'test',
summary: 'summary for test package',
version: '1.0',
}
]
};
const searchForPackages = async (searchbox, searchTerm) => {
userEvent.type(searchbox, searchTerm);
await act(async() => {
screen.getByTestId('search-available-pkgs-button').click();
});
};
// mock the insights dependency
beforeAll(() => {
global.insights = {
@ -431,6 +461,75 @@ describe('Step Packages', () => {
name: 'Search button for available packages'
});
});
test('search results should be sorted with most relevant results first', async () => {
const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref
searchbox.click();
const getPackages = jest
.spyOn(api, 'getPackages')
.mockImplementation(() => Promise.resolve(mockPkgResult));
await searchForPackages(searchbox, 'test');
expect(getPackages).toHaveBeenCalledTimes(1);
const availablePackages = screen.getByTestId('available-pkgs-list');
expect(availablePackages.children.length).toEqual(3);
const [ firstItem, secondItem, thirdItem ] = availablePackages.children;
expect(firstItem).toHaveTextContent('testsummary for test package');
expect(secondItem).toHaveTextContent('testPkgtest package summary');
expect(thirdItem).toHaveTextContent('lib-testlib-test package summary');
});
test('search results should be sorted after selecting them and then deselecting them', async () => {
const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref
searchbox.click();
const getPackages = jest
.spyOn(api, 'getPackages')
.mockImplementation(() => Promise.resolve(mockPkgResult));
await searchForPackages(searchbox, 'test');
expect(getPackages).toHaveBeenCalledTimes(1);
screen.getByRole('option', { name: /testPkg test package summary/ }).click();
screen.getByRole('button', { name: /Add selected/ }).click();
screen.getByRole('option', { name: /testPkg test package summary/ }).click();
screen.getByRole('button', { name: /Remove selected/ }).click();
const availablePackages = screen.getByTestId('available-pkgs-list');
expect(availablePackages.children.length).toEqual(3);
const [ firstItem, secondItem, thirdItem ] = availablePackages.children;
expect(firstItem).toHaveTextContent('testsummary for test package');
expect(secondItem).toHaveTextContent('testPkgtest package summary');
expect(thirdItem).toHaveTextContent('lib-testlib-test package summary');
});
test('search results should be sorted after adding and then removing all packages', async () => {
const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref
searchbox.click();
const getPackages = jest
.spyOn(api, 'getPackages')
.mockImplementation(() => Promise.resolve(mockPkgResult));
await searchForPackages(searchbox, 'test');
expect(getPackages).toHaveBeenCalledTimes(1);
screen.getByRole('button', { name: /Add all/ }).click();
screen.getByRole('button', { name: /Remove all/ }).click();
const availablePackages = screen.getByTestId('available-pkgs-list');
expect(availablePackages.children.length).toEqual(3);
const [ firstItem, secondItem, thirdItem ] = availablePackages.children;
expect(firstItem).toHaveTextContent('testsummary for test package');
expect(secondItem).toHaveTextContent('testPkgtest package summary');
expect(thirdItem).toHaveTextContent('lib-testlib-test package summary');
});
});
describe('Step Review', () => {
@ -527,24 +626,11 @@ describe('Click through all steps', () => {
// packages
const getPackages = jest
.spyOn(api, 'getPackages')
.mockImplementation(() => {
return Promise.resolve({
meta: { count: 100 },
links: { first: '', last: '' },
data: [
{
name: 'testPkg',
summary: 'test package summary',
version: '1.0',
}
],
});
});
.mockImplementation(() => Promise.resolve(mockPkgResult));
screen.getByText('Add optional additional packages to your image by searching available packages.');
userEvent.type(screen.getByTestId('search-available-pkgs-input'), 'test');
screen.getByTestId('search-available-pkgs-button').click();
await expect(getPackages).toHaveBeenCalledTimes(1);
await searchForPackages(screen.getByTestId('search-available-pkgs-input'), 'test');
expect(getPackages).toHaveBeenCalledTimes(1);
screen.getByRole('option', { name: /testPkg test package summary/ }).click();
screen.getByRole('button', { name: /Add selected/ }).click();
next.click();
@ -561,7 +647,6 @@ describe('Click through all steps', () => {
const composeImage = jest
.spyOn(api, 'composeImage')
.mockImplementation(body => {
console.log(body, 'huuuh!');
let id;
if (body.image_requests[0].upload_request.type === 'aws') {
expect(body).toEqual({