From 4adf2ff087d221178283120b73af8e16b847373b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Wed, 11 Jun 2025 19:57:25 -0500 Subject: [PATCH] fix: Refresh section list on subsection page (#2103) Invalidates the query in the subsection page used to get the list of sections that contains the subsection --- src/library-authoring/data/apiHooks.test.tsx | 20 +++++++++ src/library-authoring/data/apiHooks.ts | 41 +++++++++++++++-- src/search-manager/data/apiHooks.ts | 46 +++++++++++++++++--- src/search-manager/index.ts | 7 ++- 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/src/library-authoring/data/apiHooks.test.tsx b/src/library-authoring/data/apiHooks.test.tsx index af996b46e..e0e19cd01 100644 --- a/src/library-authoring/data/apiHooks.test.tsx +++ b/src/library-authoring/data/apiHooks.test.tsx @@ -311,6 +311,26 @@ describe('library api hooks', () => { }); }); + it('should invalidate subsection when added to section', async () => { + const spy = jest.spyOn(queryClient, 'invalidateQueries'); + const subsectionId1 = 'lct:org:lib:subsection:1'; + const subsectionId2 = 'lct:org:lib:subsection:2'; + const sectionId = 'lct:org:lib:section:1'; + const url = getLibraryContainerChildrenApiUrl(sectionId); + + axiosMock.onPost(url).reply(200); + + const { result } = renderHook(() => useAddItemsToContainer(sectionId), { wrapper }); + + await result.current.mutateAsync([subsectionId1, subsectionId2]); + + expect(axiosMock.history.post[0].url).toEqual(url); + + // Two call for `containerChildren` and library predicate + // and two more calls to invalidate the subsections. + expect(spy).toHaveBeenCalledTimes(4); + }); + describe('publishContainer', () => { it('should publish a container', async () => { const containerId = 'lct:org:lib:unit:1'; diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 3098e3756..6d583fa3c 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -8,11 +8,12 @@ import { replaceEqualDeep, } from '@tanstack/react-query'; import { useCallback } from 'react'; +import { type MeiliSearch } from 'meilisearch'; -import { getLibraryId } from '../../generic/key-utils'; +import { getBlockType, getLibraryId } from '../../generic/key-utils'; import * as api from './api'; import { VersionSpec } from '../LibraryBlock'; -import { useContentSearchConnection, useContentSearchResults } from '../../search-manager'; +import { useContentSearchConnection, useContentSearchResults, buildSearchQueryKey } from '../../search-manager'; export const libraryQueryPredicate = (query: Query, libraryId: string): boolean => { // Invalidate all content queries related to this library. @@ -697,11 +698,34 @@ export const useContainerChildren = (containerId?: string, published: boolean = }) ); +/** + * If you work with `useContentFromSearchIndex`, you can use this + * function to get the query key, usually to invalidate the query. + */ +const getSearchQueryKeyFromContent = ( + contentIds: string[], + client?: MeiliSearch, + indexName?: string, +) => ( + buildSearchQueryKey({ + client, + indexName, + extraFilter: [`usage_key IN ["${contentIds.join('","')}"]`], + searchKeywords: '', + blockTypesFilter: [], + problemTypesFilter: [], + publishStatusFilter: [], + tagsFilter: [], + sort: [], + }) +); + /** * Use this mutation to add items to a container */ export const useAddItemsToContainer = (containerId?: string) => { const queryClient = useQueryClient(); + const { client, indexName } = useContentSearchConnection(); return useMutation({ mutationFn: async (itemIds: string[]) => { // istanbul ignore if: this should never happen @@ -710,7 +734,7 @@ export const useAddItemsToContainer = (containerId?: string) => { } return api.addComponentsToContainer(containerId, itemIds); }, - onSettled: () => { + onSettled: (_data, _error, variables) => { // istanbul ignore if: this should never happen if (!containerId) { return; @@ -720,6 +744,17 @@ export const useAddItemsToContainer = (containerId?: string) => { const libraryId = getLibraryId(containerId); queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerChildren(containerId) }); queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); + + const containerType = getBlockType(containerId); + if (containerType === 'section') { + // We invalidate the search query of the each itemId if the container is a section. + // This because the subsection page calls this query individually. + variables.forEach((itemId) => { + queryClient.invalidateQueries({ + queryKey: getSearchQueryKeyFromContent([itemId], client, indexName), + }); + }); + } }, }); }; diff --git a/src/search-manager/data/apiHooks.ts b/src/search-manager/data/apiHooks.ts index a5ead78dc..a2ee0b6c3 100644 --- a/src/search-manager/data/apiHooks.ts +++ b/src/search-manager/data/apiHooks.ts @@ -44,6 +44,43 @@ export const useContentSearchConnection = (): { return { client, indexName, hasConnectionError }; }; +export const buildSearchQueryKey = ({ + client, + indexName, + extraFilter, + searchKeywords, + blockTypesFilter, + problemTypesFilter, + publishStatusFilter, + tagsFilter, + sort, +}: { + client?: MeiliSearch; + indexName?: string; + extraFilter?: Filter; + searchKeywords: string; + blockTypesFilter: string[]; + problemTypesFilter: string[]; + publishStatusFilter: PublishStatus[]; + tagsFilter: string[]; + sort: SearchSortOption[]; +}) => ( + [ + 'content_search', + 'results', + client?.config.apiKey, + client?.config.host, + indexName, + extraFilter, + searchKeywords, + blockTypesFilter, + problemTypesFilter, + publishStatusFilter, + tagsFilter, + sort, + ] +); + /** * Get the results of a search */ @@ -87,11 +124,8 @@ export const useContentSearchResults = ({ }) => { const query = useInfiniteQuery({ enabled: enabled && client !== undefined && indexName !== undefined, - queryKey: [ - 'content_search', - 'results', - client?.config.apiKey, - client?.config.host, + queryKey: buildSearchQueryKey({ + client, indexName, extraFilter, searchKeywords, @@ -100,7 +134,7 @@ export const useContentSearchResults = ({ publishStatusFilter, tagsFilter, sort, - ], + }), queryFn: ({ pageParam = 0 }) => { // istanbul ignore if: this should never happen if (client === undefined || indexName === undefined) { diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index bb4fed411..08c090f0a 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -9,7 +9,12 @@ export { default as SearchKeywordsField } from './SearchKeywordsField'; export { default as SearchSortWidget } from './SearchSortWidget'; export { default as Stats } from './Stats'; export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG, PublishStatus } from './data/api'; -export { useContentSearchConnection, useContentSearchResults, useGetBlockTypes } from './data/apiHooks'; +export { + useContentSearchConnection, + useContentSearchResults, + useGetBlockTypes, + buildSearchQueryKey, +} from './data/apiHooks'; export { TypesFilterData } from './hooks'; export type {