diff --git a/.changeset/seven-apes-shave.md b/.changeset/seven-apes-shave.md new file mode 100644 index 0000000000..356fd377dd --- /dev/null +++ b/.changeset/seven-apes-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Updated `useEntityListProvider` and catalog pickers to respond to external changes to query parameters in the URL, such as two sidebar links that apply different catalog filters. diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index 630b1700a0..e4e42eb7ba 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -51,15 +51,19 @@ export const EntityLifecyclePicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamLifecycles = [queryParameters.lifecycles] - .flat() - .filter(Boolean) as string[]; const [selectedLifecycles, setSelectedLifecycles] = useState( - queryParamLifecycles.length - ? queryParamLifecycles - : filters.lifecycles?.values ?? [], + filters.lifecycles?.values ?? [], ); + // Set selected lifecycles on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamLifecycles = [queryParameters.lifecycles] + .flat() + .filter(Boolean) as string[]; + setSelectedLifecycles(queryParamLifecycles); + }, [queryParameters]); + useEffect(() => { updateFilters({ lifecycles: selectedLifecycles.length diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index ae88858666..95e09ed434 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -53,13 +53,19 @@ export const EntityOwnerPicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamOwners = [queryParameters.owners] - .flat() - .filter(Boolean) as string[]; const [selectedOwners, setSelectedOwners] = useState( - queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [], + filters.owners?.values ?? [], ); + // Set selected owners on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamOwners = [queryParameters.owners] + .flat() + .filter(Boolean) as string[]; + setSelectedOwners(queryParamOwners); + }, [queryParameters]); + useEffect(() => { updateFilters({ owners: selectedOwners.length diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index a25c50c0a9..9753ae14f4 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -51,12 +51,16 @@ export const EntityTagPicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamTags = [queryParameters.tags] - .flat() - .filter(Boolean) as string[]; - const [selectedTags, setSelectedTags] = useState( - queryParamTags.length ? queryParamTags : filters.tags?.values ?? [], - ); + const [selectedTags, setSelectedTags] = useState(filters.tags?.values ?? []); + + // Set selected tags on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamTags = [queryParameters.tags] + .flat() + .filter(Boolean) as string[]; + setSelectedTags(queryParamTags); + }, [queryParameters]); useEffect(() => { updateFilters({ diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index d731a185fc..4820a4f0f9 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -161,9 +161,7 @@ export const UserListPicker = ({ [isOwnedEntity, isStarredEntity], ); - const [selectedUserFilter, setSelectedUserFilter] = useState( - [queryParameters.user].flat()[0] ?? initialFilter, - ); + const [selectedUserFilter, setSelectedUserFilter] = useState(initialFilter); // To show proper counts for each section, apply all other frontend filters _except_ the user // filter that's controlled by this picker. @@ -190,6 +188,13 @@ export const UserListPicker = ({ [entitiesWithoutUserFilter, starredFilter, ownedFilter], ); + // Set selected user filter on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamUserFilter = [queryParameters.user].flat()[0]; + setSelectedUserFilter(queryParamUserFilter as UserListFilterKind); + }, [queryParameters]); + useEffect(() => { if ( !loading && diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index fb8eb7279f..9b4d850208 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -25,6 +25,7 @@ import React, { useMemo, useState, } from 'react'; +import { useLocation } from 'react-router'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import useDebounce from 'react-use/lib/useDebounce'; import useMountedState from 'react-use/lib/useMountedState'; @@ -98,7 +99,6 @@ type OutputState = { appliedFilters: EntityFilters; entities: Entity[]; backendEntities: Entity[]; - queryParameters: Record; }; export const EntityListProvider = ({ @@ -109,19 +109,26 @@ export const EntityListProvider = ({ const [requestedFilters, setRequestedFilters] = useState( {} as EntityFilters, ); + + // We use react-router's useLocation hook so updates from external sources trigger an update to + // the queryParameters in outputState. Updates from this hook use replaceState below and won't + // trigger a useLocation change; this would instead come from an external source, such as a manual + // update of the URL or two catalog sidebar links with different catalog filters. + const location = useLocation(); + const queryParameters = useMemo( + () => + (qs.parse(location.search, { + ignoreQueryPrefix: true, + }).filters ?? {}) as Record, + [location], + ); + const [outputState, setOutputState] = useState>( () => { - const query = qs.parse(window.location.search, { - ignoreQueryPrefix: true, - }); return { appliedFilters: {} as EntityFilters, entities: [], backendEntities: [], - queryParameters: (query.filters ?? {}) as Record< - string, - string | string[] - >, }; }, ); @@ -163,19 +170,17 @@ export const EntityListProvider = ({ appliedFilters: requestedFilters, backendEntities: response.items, entities: response.items.filter(entityFilter), - queryParameters: queryParams, }); } else { setOutputState({ appliedFilters: requestedFilters, backendEntities: outputState.backendEntities, entities: outputState.backendEntities.filter(entityFilter), - queryParameters: queryParams, }); } if (isMounted()) { - const oldParams = qs.parse(window.location.search, { + const oldParams = qs.parse(location.search, { ignoreQueryPrefix: true, }); const newParams = qs.stringify( @@ -191,7 +196,7 @@ export const EntityListProvider = ({ window.history?.replaceState(null, document.title, newUrl); } }, - [catalogApi, requestedFilters, outputState], + [catalogApi, queryParameters, requestedFilters, outputState], { loading: true }, ); @@ -220,11 +225,11 @@ export const EntityListProvider = ({ entities: outputState.entities, backendEntities: outputState.backendEntities, updateFilters, - queryParameters: outputState.queryParameters, + queryParameters, loading, error, }), - [outputState, updateFilters, loading, error], + [outputState, updateFilters, queryParameters, loading, error], ); return ( diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index 5c0139dd26..cbc9d4ed45 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -42,13 +42,19 @@ export function useEntityTypeFilter(): EntityTypeReturn { updateFilters, } = useEntityListProvider(); - const queryParamTypes = [queryParameters.type] - .flat() - .filter(Boolean) as string[]; const [selectedTypes, setSelectedTypes] = useState( - queryParamTypes.length ? queryParamTypes : typeFilter?.getTypes() ?? [], + typeFilter?.getTypes() ?? [], ); + // Set selected types on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamTypes = [queryParameters.type] + .flat() + .filter(Boolean) as string[]; + setSelectedTypes(queryParamTypes); + }, [queryParameters]); + const [availableTypes, setAvailableTypes] = useState([]); const kind = useMemo(() => kindFilter?.value, [kindFilter]);