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'),
}),
);