fix(scaffolder): prevent MultiEntityPicker from removing existing options from form data
Signed-off-by: Alec Jacobs <cajacobs5401@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Prevent the MultiEntityPicker from removing options present in form state when new options are selected
|
||||
@@ -262,6 +262,71 @@ describe('<MultiEntityPicker />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with existing form data', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = { 'ui:options': {} };
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData: ['group:default/team-a'],
|
||||
} as unknown as FieldProps;
|
||||
});
|
||||
|
||||
it('preserves existing data on blur', async () => {
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'squ' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['group:default/team-a', 'squ']);
|
||||
});
|
||||
|
||||
it('preserves existing data on value create', async () => {
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'squ' } });
|
||||
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['group:default/team-a', 'squ']);
|
||||
});
|
||||
|
||||
it('preserves existing data on selecting an existing option', async () => {
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = getByRole('textbox');
|
||||
|
||||
fireEvent.mouseDown(input);
|
||||
const optionA = screen.getByText('squad-b');
|
||||
await userEvent.click(optionA as HTMLElement);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([
|
||||
'group:default/team-a',
|
||||
'group:default/squad-b',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses full entity ref', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
|
||||
@@ -50,6 +50,12 @@ import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
export { MultiEntityPickerSchema } from './schema';
|
||||
|
||||
// AutocompleteChangeReason events that can be triggered when a user inputs a freeSolo option
|
||||
const FREE_SOLO_EVENTS: readonly AutocompleteChangeReason[] = [
|
||||
'blur',
|
||||
'create-option',
|
||||
];
|
||||
|
||||
/**
|
||||
* The underlying component that is rendered in the form for the `MultiEntityPicker`
|
||||
* field extension.
|
||||
@@ -110,29 +116,34 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
|
||||
(_: any, refs: (string | Entity)[], reason: AutocompleteChangeReason) => {
|
||||
const values = refs
|
||||
.map(ref => {
|
||||
// If the ref is not a string, then it was a selected option in the picker
|
||||
if (typeof ref !== 'string') {
|
||||
// if ref does not exist: pass 'undefined' to trigger validation for required value
|
||||
return ref ? stringifyEntityRef(ref as Entity) : undefined;
|
||||
}
|
||||
if (reason === 'blur' || reason === 'create-option') {
|
||||
// Add in default namespace, etc.
|
||||
let entityRef = ref;
|
||||
try {
|
||||
// Attempt to parse the entity ref into it's full form.
|
||||
entityRef = stringifyEntityRef(
|
||||
parseEntityRef(ref as string, {
|
||||
defaultKind,
|
||||
defaultNamespace,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
// If the passed in value isn't an entity ref, do nothing.
|
||||
}
|
||||
|
||||
// We need to check against formData here as that's the previous value for this field.
|
||||
if (formData?.includes(ref) || allowArbitraryValues) {
|
||||
return entityRef;
|
||||
}
|
||||
// Add in default namespace, etc.
|
||||
let entityRef = ref;
|
||||
try {
|
||||
// Attempt to parse the entity ref into it's full form.
|
||||
entityRef = stringifyEntityRef(
|
||||
parseEntityRef(ref as string, {
|
||||
defaultKind,
|
||||
defaultNamespace,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
// If the passed in value isn't an entity ref, do nothing.
|
||||
}
|
||||
|
||||
// We need to check against formData here as that's the previous value for this field.
|
||||
if (
|
||||
// If value already matches what exists in form data, allow it
|
||||
formData?.includes(ref) ||
|
||||
// If arbitrary values are allowed and the reason is a free solo event, allow it
|
||||
(allowArbitraryValues && FREE_SOLO_EVENTS.includes(reason))
|
||||
) {
|
||||
return entityRef;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user