fix: update EntityAutocompletePicker selected options when value changes

When the filter value is changed externally, the selectedOptions weren't
updated within EntityAutocompletePicker. This commit adds an effect that
fixes this. It checks that the value is actually different from the
current value to prevent infinite loops.

Fixes #28743

Signed-off-by: Anner Visser <anner.visser@alliander.com>
This commit is contained in:
Anner Visser
2025-02-12 14:15:52 +01:00
parent 9c3eb928ef
commit bec1e1517b
3 changed files with 54 additions and 32 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
update EntityAutocompletePicker selected options when filter value is changed externally
@@ -14,16 +14,16 @@
* limitations under the License.
*/
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import {
MockEntityListContextProvider,
catalogApiMock,
MockEntityListContextProvider,
} from '@backstage/plugin-catalog-react/testUtils';
import { EntityAutocompletePicker } from './EntityAutocompletePicker';
import { TestApiProvider } from '@backstage/test-utils';
import { catalogApiRef } from '../../api';
import { DefaultEntityFilters } from '../../hooks';
import { DefaultEntityFilters, useEntityList } from '../../hooks';
import { Entity } from '@backstage/catalog-model';
import { EntityFilter } from '../../types';
import { EntityKindFilter, EntityTypeFilter } from '../../filters';
@@ -281,15 +281,28 @@ describe('<EntityAutocompletePicker/>', () => {
});
});
it('responds to external queryParameters changes', async () => {
it('responds to external filter changes', async () => {
const mockCatalogApi = makeMockCatalogApi();
const updateFilters = jest.fn();
const rendered = render(
const ChangeFilterButton = () => {
const { updateFilters } = useEntityList<EntityFilters>();
return (
<button
data-testid="external-filter-change-button"
onClick={() =>
updateFilters({ options: new EntityOptionFilter(['option3']) })
}
>
Trigger external filter change
</button>
);
};
render(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}>
<MockEntityListContextProvider<EntityFilters>
value={{
updateFilters,
queryParameters: { options: ['option1'] },
filters: { options: new EntityOptionFilter(['option2']) },
}}
>
<EntityAutocompletePicker<EntityFilters>
@@ -298,34 +311,21 @@ describe('<EntityAutocompletePicker/>', () => {
name="options"
Filter={EntityOptionFilter}
/>
<ChangeFilterButton />
</MockEntityListContextProvider>
</TestApiProvider>,
);
await waitFor(() =>
expect(updateFilters).toHaveBeenLastCalledWith({
options: new EntityOptionFilter(['option1']),
}),
expect(screen.queryByText('Options')).toBeInTheDocument(),
);
rendered.rerender(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}>
<MockEntityListContextProvider<EntityFilters>
value={{
updateFilters,
queryParameters: { options: ['option2'] },
}}
>
<EntityAutocompletePicker<EntityFilters>
label="Options"
path="spec.options"
name="options"
Filter={EntityOptionFilter}
/>
</MockEntityListContextProvider>
</TestApiProvider>,
expect(screen.queryByText('option2')).toBeInTheDocument();
screen.getByTestId('external-filter-change-button').click();
await waitFor(() =>
expect(screen.queryByText('option3')).toBeInTheDocument(),
);
expect(updateFilters).toHaveBeenLastCalledWith({
options: new EntityOptionFilter(['option2']),
});
expect(screen.queryByText('option2')).not.toBeInTheDocument();
});
it('filters available values by kind as default', async () => {
@@ -29,6 +29,7 @@ import {
import { EntityFilter } from '../../types';
import { reduceBackendCatalogFilters } from '../../utils/filters';
import { CatalogAutocomplete } from '../CatalogAutocomplete';
import { isEqual } from 'lodash';
/** @public */
export type AllowedEntityFilters<T extends DefaultEntityFilters> = {
@@ -114,11 +115,13 @@ export function EntityAutocompletePicker<
[queryParameter],
);
const filteredOptions = (filters[name] as unknown as { values: string[] })
?.values;
const [selectedOptions, setSelectedOptions] = useState(
queryParameters.length
? queryParameters
: (filters[name] as unknown as { values: string[] })?.values ??
initialSelectedOptions,
: filteredOptions ?? initialSelectedOptions,
);
// Set selected options on query parameter updates; this happens at initial page load and from
@@ -132,12 +135,26 @@ export function EntityAutocompletePicker<
const availableOptions = Object.keys(availableValues ?? {});
const shouldAddFilter = selectedOptions.length && availableOptions.length;
// Update filter value when selectedOptions change
useEffect(() => {
updateFilters({
[name]: shouldAddFilter ? new Filter(selectedOptions) : undefined,
} as Partial<T>);
}, [name, shouldAddFilter, selectedOptions, Filter, updateFilters]);
// Update selected options when filter value changes
useEffect(() => {
if (!shouldAddFilter) return;
const newSelectedOptions = filteredOptions ?? [];
// Check value is actually different (not just a different reference) to prevent selectedOptions <> filters loop
if (!isEqual(newSelectedOptions, selectedOptions)) {
setSelectedOptions(newSelectedOptions);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- Don't re-set filter value when selectedOptions changes
}, [filteredOptions]);
const filter = filters[name];
if (
(filter && typeof filter === 'object' && !('values' in filter)) ||