diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx index 4be07b139d..1ec7b56fa5 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx @@ -704,4 +704,79 @@ describe('', () => { 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; + + catalogApi.getEntities.mockResolvedValue({ items: testEntities }); + }); + + it('User selects item', async () => { + await renderInTestApp( + + + , + ); + + 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'); + }); + }); + }); }); diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx index d800c9b372..f620c75d07 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx @@ -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 => ( diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts index 2621157d02..0c7989e8bf 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts @@ -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'), }), );