diff --git a/.changeset/long-hairs-throw.md b/.changeset/long-hairs-throw.md new file mode 100644 index 0000000000..a00b24fda3 --- /dev/null +++ b/.changeset/long-hairs-throw.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Added a new `ui:autoSelect` option to the EntityPicker field that controls whether an entity is automatically selected when the field loses focus. When set to `false`, the field will remain empty if the user closes it without explicitly selecting an entity, preventing unintentional selections. Defaults to `true` for backward compatibility. diff --git a/docs/features/software-templates/ui-options-examples.md b/docs/features/software-templates/ui-options-examples.md index 845da8e60b..fd4b20d466 100644 --- a/docs/features/software-templates/ui-options-examples.md +++ b/docs/features/software-templates/ui-options-examples.md @@ -119,6 +119,36 @@ entity: defaultNamespace: payment ``` +### `autoSelect` + +Whether to automatically select the highlighted option when the input loses focus. Defaults to `true`. + +When set to `false`, users must explicitly select an option from the dropdown by clicking or pressing Enter. This prevents accidental selections when typing to filter options. + +- Default behavior with `autoSelect` as `true` (auto-selects on blur) + +```yaml +entity: + title: Entity + type: string + description: Entity of the component + ui:field: EntityPicker + ui:options: + autoSelect: true +``` + +- Require explicit selection with `autoSelect` as `false` + +```yaml +entity: + title: Entity + type: string + description: Entity of the component + ui:field: EntityPicker + ui:options: + autoSelect: false +``` + ## MultiEntityPicker The input props that can be specified under `ui:options` for the `MultiEntityPicker` field extension. diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index e624b157fa..5c0fffbcc1 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -74,6 +74,7 @@ export const EntityNamePickerFieldExtension: FieldExtensionComponent_2< export const EntityPickerFieldExtension: FieldExtensionComponent_2< string, { + autoSelect?: boolean | undefined; defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; catalogFilter?: @@ -103,6 +104,7 @@ export const EntityPickerFieldExtension: FieldExtensionComponent_2< export const EntityPickerFieldSchema: FieldSchema_2< string, { + autoSelect?: boolean | undefined; defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; catalogFilter?: @@ -249,6 +251,7 @@ export type MyGroupsPickerUiOptions = NonNullable< export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< string, { + autoSelect?: boolean | undefined; defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; catalogFilter?: @@ -278,6 +281,7 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< export const OwnedEntityPickerFieldSchema: FieldSchema_2< string, { + autoSelect?: boolean | undefined; defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; catalogFilter?: diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index 97c90b2aa5..4b272f1aa0 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -378,6 +378,100 @@ describe('', () => { }); }); + describe('ui:autoSelect behavior', () => { + beforeEach(() => { + uiSchema = { + 'ui:options': { + defaultKind: 'Group', + }, + }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + catalogApi.getEntities.mockResolvedValue({ items: entities }); + }); + + it('default behavior', async () => { + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + // Type partial match and blur + fireEvent.change(input, { target: { value: 'team' } }); + fireEvent.blur(input); + + // Default behavior with freeSolo enabled processes the typed value + expect(onChange).toHaveBeenCalledWith('group:default/team'); + }); + + it('does not autoSelect value onBlur', async () => { + uiSchema = { + 'ui:options': { + defaultKind: 'Group', + autoSelect: false, + }, + }; + props = { + ...props, + uiSchema, + } as unknown as FieldProps; + + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + // Type and blur - with autoSelect=false, the autocomplete won't auto-select on blur + fireEvent.change(input, { target: { value: 'team' } }); + fireEvent.blur(input); + + // With autoSelect=false, onChange should not be called on blur + // This is the key difference - users must explicitly select an option + expect(onChange).not.toHaveBeenCalled(); + }); + + it('autoSelects entity onBlur', async () => { + uiSchema = { + 'ui:options': { + defaultKind: 'Group', + autoSelect: true, + }, + }; + props = { + ...props, + uiSchema, + } as unknown as FieldProps; + + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + // Type and blur + fireEvent.change(input, { target: { value: 'squad' } }); + fireEvent.blur(input); + + // With autoSelect=true and freeSolo, processes the typed value + expect(onChange).toHaveBeenCalledWith('group:default/squad'); + }); + }); + describe('uses full entity ref', () => { beforeEach(() => { uiSchema = { diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index d2197d4426..9a3dbd0009 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -74,6 +74,7 @@ export const EntityPicker = (props: EntityPickerProps) => { const defaultKind = uiSchema['ui:options']?.defaultKind; const defaultNamespace = uiSchema['ui:options']?.defaultNamespace || undefined; + const autoSelect = uiSchema?.['ui:options']?.autoSelect ?? true; const isDisabled = uiSchema?.['ui:disabled'] ?? false; const catalogApi = useApi(catalogApiRef); @@ -209,7 +210,7 @@ export const EntityPicker = (props: EntityPickerProps) => { : entities?.entityRefToPresentation.get(stringifyEntityRef(option)) ?.entityRef! } - autoSelect + autoSelect={autoSelect} freeSolo={allowArbitraryValues} renderInput={params => (