From bec1e1517b752773ade180e29c6eff986ec7f946 Mon Sep 17 00:00:00 2001 From: Anner Visser Date: Wed, 12 Feb 2025 14:15:52 +0100 Subject: [PATCH] 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 --- .changeset/ninety-experts-cheat.md | 5 ++ .../EntityAutocompletePicker.test.tsx | 60 +++++++++---------- .../EntityAutocompletePicker.tsx | 21 ++++++- 3 files changed, 54 insertions(+), 32 deletions(-) create mode 100644 .changeset/ninety-experts-cheat.md diff --git a/.changeset/ninety-experts-cheat.md b/.changeset/ninety-experts-cheat.md new file mode 100644 index 0000000000..093145ecc6 --- /dev/null +++ b/.changeset/ninety-experts-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +update EntityAutocompletePicker selected options when filter value is changed externally diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.test.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.test.tsx index f025c14011..5fd8b549ce 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.test.tsx @@ -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('', () => { }); }); - 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(); + + return ( + + ); + }; + + render( value={{ - updateFilters, - queryParameters: { options: ['option1'] }, + filters: { options: new EntityOptionFilter(['option2']) }, }} > @@ -298,34 +311,21 @@ describe('', () => { name="options" Filter={EntityOptionFilter} /> + , ); + await waitFor(() => - expect(updateFilters).toHaveBeenLastCalledWith({ - options: new EntityOptionFilter(['option1']), - }), + expect(screen.queryByText('Options')).toBeInTheDocument(), ); - rendered.rerender( - - - value={{ - updateFilters, - queryParameters: { options: ['option2'] }, - }} - > - - label="Options" - path="spec.options" - name="options" - Filter={EntityOptionFilter} - /> - - , + 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 () => { diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index 09b5c1e908..c134446d27 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -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 = { @@ -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); }, [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)) ||