Add max no of entities option to MultiEntityPicker
Signed-off-by: Raghunandan Balachandran <raghunandan@spotify.com>
This commit is contained in:
@@ -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'),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user