Add max no of entities option to MultiEntityPicker

Signed-off-by: Raghunandan Balachandran <raghunandan@spotify.com>
This commit is contained in:
Raghunandan Balachandran
2024-09-15 23:28:21 +02:00
parent 42c13f1530
commit be99085951
3 changed files with 88 additions and 1 deletions
@@ -704,4 +704,79 @@ describe('<MultiEntityPicker />', () => {
expect(onChange).toHaveBeenCalledWith([]);
});
});
describe('Multiselect maxNoOfEntities option', () => {
beforeEach(() => {
const testEntities = [
makeEntity('Group', 'default', 'team-a'),
makeEntity('Group', 'default', 'squad-b'),
makeEntity('User', 'default', 'user-a'),
makeEntity('User', 'default', 'user-b'),
];
uiSchema = {
'ui:options': {
maxNoOfEntities: 2,
catalogFilter: [
{
kind: ['Group'],
'metadata.name': 'test-entity',
},
{
kind: ['User'],
'metadata.name': 'test-entity',
},
],
},
allowArbitraryValues: true,
};
props = {
onChange,
schema,
required: false,
uiSchema,
rawErrors,
formData,
} as unknown as FieldProps<any>;
catalogApi.getEntities.mockResolvedValue({ items: testEntities });
});
it('User selects item', async () => {
await renderInTestApp(
<Wrapper>
<MultiEntityPicker {...props} />
</Wrapper>,
);
const input = screen.getByRole('textbox');
fireEvent.mouseDown(input);
const optionsBefore = screen.getAllByRole('option');
// Check that all options are enabled
optionsBefore.forEach(option => {
expect(option).toHaveAttribute('aria-disabled', 'false');
});
fireEvent.mouseDown(input);
// Select two options from the dropdown
fireEvent.change(input, { target: { value: 'team-a' } });
fireEvent.blur(input);
fireEvent.change(input, { target: { value: 'user-a' } });
fireEvent.blur(input);
expect(onChange).toHaveBeenCalledWith(['team-a']);
fireEvent.mouseDown(input);
const optionsAfter = screen.getAllByRole('option');
// Check that all options are disabled when macNoOfEntities is reached
optionsAfter.forEach(option => {
expect(option).toHaveAttribute('aria-disabled', 'true');
});
});
});
});
@@ -34,7 +34,7 @@ import FormControl from '@material-ui/core/FormControl';
import Autocomplete, {
AutocompleteChangeReason,
} from '@material-ui/lab/Autocomplete';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import useAsync from 'react-use/esm/useAsync';
import { FieldValidation } from '@rjsf/utils';
import {
@@ -65,6 +65,7 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
const defaultKind = uiSchema['ui:options']?.defaultKind;
const defaultNamespace =
uiSchema['ui:options']?.defaultNamespace || undefined;
const [noOfItemsSelected, setNoOfItemsSelected] = useState(0);
const catalogApi = useApi(catalogApiRef);
const entityPresentationApi = useApi(entityPresentationApiRef);
@@ -92,6 +93,9 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
const allowArbitraryValues =
uiSchema['ui:options']?.allowArbitraryValues ?? true;
// if not specified, default to undefined
const maxNoOfEntities = uiSchema['ui:options']?.maxNoOfEntities ?? undefined;
const onSelect = useCallback(
(_: any, refs: (string | Entity)[], reason: AutocompleteChangeReason) => {
const values = refs
@@ -125,6 +129,7 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
})
.filter(ref => ref !== undefined) as string[];
setNoOfItemsSelected(values.length);
onChange(values);
},
[onChange, formData, defaultKind, defaultNamespace, allowArbitraryValues],
@@ -158,6 +163,9 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
: entities?.entityRefToPresentation.get(stringifyEntityRef(option))
?.entityRef!
}
getOptionDisabled={_options =>
maxNoOfEntities ? noOfItemsSelected >= maxNoOfEntities : false
}
autoSelect
freeSolo={allowArbitraryValues}
renderInput={params => (
@@ -47,6 +47,10 @@ export const MultiEntityPickerFieldSchema = makeFieldSchemaFromZod(
.or(entityQueryFilterExpressionSchema)
.optional()
.describe('List of key-value filter expression for entities'),
maxNoOfEntities: z
.number()
.optional()
.describe('The maximum number of entities that can be selected'),
}),
);