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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
update EntityAutocompletePicker selected options when filter value is changed externally
|
||||
+30
-30
@@ -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 () => {
|
||||
|
||||
+19
-2
@@ -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)) ||
|
||||
|
||||
Reference in New Issue
Block a user