diff --git a/src/content-tags-drawer/ContentTagsDrawer.test.jsx b/src/content-tags-drawer/ContentTagsDrawer.test.jsx index 5035b2fe43..90fb145683 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.test.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.test.jsx @@ -34,6 +34,7 @@ const { languageWithoutTagsId, largeTagsId, emptyTagsId, + containerTagsId, } = mockContentTaxonomyTagsData; jest.mock('react-router-dom', () => ({ @@ -46,14 +47,15 @@ jest.mock('../library-authoring/common/context/SidebarContext', () => ({ useSidebarContext: () => ({ sidebarAction: mockSidebarAction() }), })); -const renderDrawer = (contentId, drawerParams = {}) => ( - render( +const renderDrawer = (contentId, drawerParams = {}, renderPath = path, containerId = '') => { + const params = { contentId, containerId }; + return render( , - { path, params: { contentId } }, - ) -); + { path: renderPath, params }, + ); +}; describe('', () => { beforeEach(async () => { @@ -692,6 +694,42 @@ describe('', () => { await waitFor(() => expect(axiosMock.history.put[0].url).toEqual(url)); }); + [ + 'lct:org:lib:unit:1', + 'lib-collection:org:lib:1', + 'lb:org:lib:html:1', + ].forEach((containerId) => { + it(`should invalidate children query when update child tag when containerId is ${containerId}`, async () => { + const newPath = '/container/:containerId/'; + const { axiosMock, queryClient } = initializeMocks(); + const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); + const url = getContentTaxonomyTagsApiUrl(containerTagsId); + axiosMock.onPut(url).reply(200); + renderDrawer(containerTagsId, { id: containerTagsId }, newPath, containerId); + expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument(); + const editTagsButton = screen.getByRole('button', { + name: /edit tags/i, + }); + fireEvent.click(editTagsButton); + + const saveButton = screen.getByRole('button', { + name: /save/i, + }); + fireEvent.click(saveButton); + + await waitFor(() => expect(axiosMock.history.put[0].url).toEqual(url)); + expect(mockInvalidateQueries).toHaveBeenCalledTimes(5); + expect(mockInvalidateQueries).toHaveBeenNthCalledWith(5, [ + 'contentLibrary', + 'lib:org:lib', + 'content', + 'container', + containerId, + 'children', + ]); + }); + }); + it('should taxonomies must be ordered', async () => { renderDrawer(largeTagsId); expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument(); diff --git a/src/content-tags-drawer/data/api.mocks.ts b/src/content-tags-drawer/data/api.mocks.ts index 2f7360be8f..932ae7311e 100644 --- a/src/content-tags-drawer/data/api.mocks.ts +++ b/src/content-tags-drawer/data/api.mocks.ts @@ -205,7 +205,7 @@ mockContentTaxonomyTagsData.emptyTagsId = 'block-v1:EmptyTagsOrg+STC1+2023_1+typ mockContentTaxonomyTagsData.emptyTags = { taxonomies: [], }; -mockContentTaxonomyTagsData.containerTagsId = 'lct:org:lib:unit:container_tags'; +mockContentTaxonomyTagsData.containerTagsId = 'lct:StagedTagsOrg:lib:unit:container_tags'; mockContentTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getContentTaxonomyTagsData').mockImplementation(mockContentTaxonomyTagsData); /** diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx index 01ef3a3a7b..fcd8da92b2 100644 --- a/src/content-tags-drawer/data/apiHooks.jsx +++ b/src/content-tags-drawer/data/apiHooks.jsx @@ -112,7 +112,7 @@ export const useContentTaxonomyTagsData = (contentId) => ( /** * Builds the query to get meta data about the content object - * @param {string} contentId The id of the content object (unit/component) + * @param {string} contentId The id of the content object * @param {boolean} enabled Flag to enable/disable the query */ export const useContentData = (contentId, enabled) => ( @@ -130,7 +130,7 @@ export const useContentData = (contentId, enabled) => ( export const useContentTaxonomyTagsUpdater = (contentId) => { const queryClient = useQueryClient(); const unitIframe = window.frames['xblock-iframe']; - const { unitId } = useParams(); + const { containerId } = useParams(); return useMutation({ /** @@ -143,7 +143,7 @@ export const useContentTaxonomyTagsUpdater = (contentId) => { * >} */ mutationFn: ({ tagsData }) => updateContentTaxonomyTags(contentId, tagsData), - onSettled: /* istanbul ignore next */ () => { + onSettled: () => { queryClient.invalidateQueries({ queryKey: ['contentTaxonomyTags', contentId] }); /// Invalidate query with pattern on course outline let contentPattern; @@ -160,9 +160,10 @@ export const useContentTaxonomyTagsUpdater = (contentId) => { queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId)); // Invalidate content search to update tags count queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) }); - // If the tags for a compoent were edited from Unit page, invalidate children query to fetch count again. - if (unitId) { - queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(unitId)); + // If the tags for an item were edited from a container page (Unit, Subsection, Section), + // invalidate children query to fetch count again. + if (containerId) { + queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(containerId)); } } }, diff --git a/src/generic/tag-count/index.tsx b/src/generic/tag-count/index.tsx index d8787bb2bf..9d31639186 100644 --- a/src/generic/tag-count/index.tsx +++ b/src/generic/tag-count/index.tsx @@ -9,7 +9,7 @@ type TagCountProps = { }; // eslint-disable-next-line react/prop-types -const TagCount: React.FC = ({ count, onClick, size }) => { +const TagCount: React.FC = ({ count, onClick, size }: TagCountProps) => { const renderContent = () => ( diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 72db994e64..168117672d 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -703,7 +703,10 @@ export const useContainerChildren = (containerId?: string, published: boolean = enabled: !!containerId, queryKey: libraryAuthoringQueryKeys.containerChildren(containerId!), queryFn: () => api.getLibraryContainerChildren(containerId!, published), - structuralSharing: (oldData: api.LibraryBlockMetadata[], newData: api.LibraryBlockMetadata[]) => { + structuralSharing: ( + oldData: api.LibraryBlockMetadata[] | api.Container[], + newData: api.LibraryBlockMetadata[] | api.Container[], + ) => { // This just sets `isNew` flag to new children components if (oldData) { const oldDataIds = oldData.map((obj) => obj.id); diff --git a/src/library-authoring/section-subsections/LibraryContainerChildren.tsx b/src/library-authoring/section-subsections/LibraryContainerChildren.tsx index cf5a62c888..c0b2010ec5 100644 --- a/src/library-authoring/section-subsections/LibraryContainerChildren.tsx +++ b/src/library-authoring/section-subsections/LibraryContainerChildren.tsx @@ -24,7 +24,8 @@ import { ToastContext } from '../../generic/toast-context'; import TagCount from '../../generic/tag-count'; import { ContainerMenu } from '../components/ContainerCard'; import { useLibraryRoutes } from '../routes'; -import { useSidebarContext } from '../common/context/SidebarContext'; +import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext'; +import { useRunOnNextRender } from '../../utils'; interface LibraryContainerChildrenProps { containerKey: string; @@ -45,6 +46,8 @@ const ContainerRow = ({ containerKey, container, readOnly }: ContainerRowProps) const { showToast } = useContext(ToastContext); const updateMutation = useUpdateContainer(container.originalId, containerKey); const { showOnlyPublished } = useLibraryContext(); + const { navigateTo } = useLibraryRoutes(); + const { setSidebarAction } = useSidebarContext(); const handleSaveDisplayName = async (newDisplayName: string) => { try { @@ -57,6 +60,18 @@ const ContainerRow = ({ containerKey, container, readOnly }: ContainerRowProps) } }; + /* istanbul ignore next */ + const scheduleJumpToTags = useRunOnNextRender(() => { + // TODO: Ugly hack to make sure sidebar shows manage tags section + // This needs to run after all changes to url takes place to avoid conflicts. + setTimeout(() => setSidebarAction(SidebarActions.JumpToManageTags), 250); + }); + + const jumpToManageTags = () => { + navigateTo({ selectedItemId: container.originalId }); + scheduleJumpToTags(); + }; + return ( <> {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} @@ -90,7 +105,11 @@ const ContainerRow = ({ containerKey, container, readOnly }: ContainerRowProps) )} - + {!readOnly && ( ', () => { expect((await screen.findAllByText(new RegExp(`${childType} block 0`, 'i')))[0]).toBeInTheDocument(); expect(await screen.findByRole('button', { name: new RegExp(`${childType} Info`, 'i') })).toBeInTheDocument(); }); + + it(`should open manage tags on click tag count in ${cType} page`, async () => { + const cId = cType === ContainerType.Section + ? mockGetContainerMetadata.sectionId + : mockGetContainerMetadata.subsectionId; + renderLibrarySectionPage(cId, undefined, cType); + // check all children components are rendered. + expect((await screen.findAllByText(`${childType} block 0`))[0]).toBeInTheDocument(); + expect((await screen.findAllByText(`${childType} block 1`))[0]).toBeInTheDocument(); + expect((await screen.findAllByText(`${childType} block 2`))[0]).toBeInTheDocument(); + + const tagCountButton = screen.getAllByRole('button', { name: '0' })[0]; + fireEvent.click(tagCountButton); + + expect(await screen.findByTestId('library-sidebar')).toBeInTheDocument(); + await waitFor( + () => expect(screen.getByRole('tab', { name: /manage/i })).toHaveClass('active'), + { timeout: 300 }, + ); + }); }); });