diff --git a/src/course-libraries/CourseLibraries.test.tsx b/src/course-libraries/CourseLibraries.test.tsx index 06cf098e45..4753d0d52f 100644 --- a/src/course-libraries/CourseLibraries.test.tsx +++ b/src/course-libraries/CourseLibraries.test.tsx @@ -78,14 +78,14 @@ describe('', () => { const user = userEvent.setup(); await renderCourseLibrariesPage(mockGetEntityLinks.courseKey); const allTab = await screen.findByRole('tab', { name: 'Libraries' }); - const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' }); + const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 7' }); // review tab should be open by default as outOfSyncCount is greater than 0 expect(reviewTab).toHaveAttribute('aria-selected', 'true'); await user.click(allTab); const alert = await screen.findByRole('alert'); expect(await within(alert).findByText( - '5 library components are out of sync. Review updates to accept or ignore changes', + '7 library components are out of sync. Review updates to accept or ignore changes', )).toBeInTheDocument(); expect(allTab).toHaveAttribute('aria-selected', 'true'); @@ -93,14 +93,14 @@ describe('', () => { await user.click(reviewBtn); expect(allTab).toHaveAttribute('aria-selected', 'false'); - expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true'); + expect(await screen.findByRole('tab', { name: 'Review Content Updates 7' })).toHaveAttribute('aria-selected', 'true'); expect(alert).not.toBeInTheDocument(); }); it('hide alert on dismiss', async () => { const user = userEvent.setup(); await renderCourseLibrariesPage(mockGetEntityLinks.courseKey); - const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' }); + const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 7' }); // review tab should be open by default as outOfSyncCount is greater than 0 expect(reviewTab).toHaveAttribute('aria-selected', 'true'); const allTab = await screen.findByRole('tab', { name: 'Libraries' }); @@ -109,7 +109,7 @@ describe('', () => { const alert = await screen.findByRole('alert'); expect(await within(alert).findByText( - '5 library components are out of sync. Review updates to accept or ignore changes', + '7 library components are out of sync. Review updates to accept or ignore changes', )).toBeInTheDocument(); const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' }); await user.click(dismissBtn); @@ -118,7 +118,7 @@ describe('', () => { // review updates button const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' }); await user.click(reviewActionBtn); - expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true'); + expect(await screen.findByRole('tab', { name: 'Review Content Updates 7' })).toHaveAttribute('aria-selected', 'true'); }); it('show alert if max lastPublishedDate is greated than the local storage value', async () => { @@ -131,14 +131,14 @@ describe('', () => { await renderCourseLibrariesPage(mockGetEntityLinks.courseKey); const allTab = await screen.findByRole('tab', { name: 'Libraries' }); - const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' }); + const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 7' }); // review tab should be open by default as outOfSyncCount is greater than 0 expect(reviewTab).toHaveAttribute('aria-selected', 'true'); await user.click(allTab); const alert = await screen.findByRole('alert'); expect(await within(alert).findByText( - '5 library components are out of sync. Review updates to accept or ignore changes', + '7 library components are out of sync. Review updates to accept or ignore changes', )).toBeInTheDocument(); }); @@ -152,14 +152,12 @@ describe('', () => { await renderCourseLibrariesPage(mockGetEntityLinks.courseKey); const allTab = await screen.findByRole('tab', { name: 'Libraries' }); - const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' }); + const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 7' }); // review tab should be open by default as outOfSyncCount is greater than 0 expect(reviewTab).toHaveAttribute('aria-selected', 'true'); await user.click(allTab); expect(allTab).toHaveAttribute('aria-selected', 'true'); - screen.logTestingPlaygroundURL(); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); }); }); @@ -195,39 +193,61 @@ describe('', () => { }); it('shows all readyToSync links', async () => { - await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); + await renderCourseLibrariesReviewPage(); const updateBtns = await screen.findAllByRole('button', { name: 'Update' }); - expect(updateBtns.length).toEqual(5); + expect(updateBtns.length).toEqual(7); const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' }); - expect(ignoreBtns.length).toEqual(5); + expect(ignoreBtns.length).toEqual(7); }); - it('update changes works', async () => { + test.each([ + { + label: 'update changes works with components', + itemIndex: 0, + expectedToastMsg: 'Success! "Dropdown" is updated', + }, + { + label: 'update changes works with containers', + itemIndex: 5, + expectedToastMsg: 'Success! "Unit 1" is updated', + }, + ])('$label', async ({ itemIndex, expectedToastMsg }) => { const user = userEvent.setup(); const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); - const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey; + const usageKey = mockGetEntityLinks.response[itemIndex].downstreamUsageKey; axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {}); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); const updateBtns = await screen.findAllByRole('button', { name: 'Update' }); - expect(updateBtns.length).toEqual(5); - await user.click(updateBtns[0]); + expect(updateBtns.length).toEqual(7); + await user.click(updateBtns[itemIndex]); await waitFor(() => { expect(axiosMock.history.post.length).toEqual(1); }); expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey)); - expect(mockShowToast).toHaveBeenCalledWith('Success! "Dropdown" is updated'); - expect(mockInvalidateQueries).toHaveBeenCalledWith(['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX']); + expect(mockShowToast).toHaveBeenCalledWith(expectedToastMsg); + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX'] }); }); - it('update changes works in preview modal', async () => { + test.each([ + { + label: 'update changes works in preview modal with components', + itemIndex: 0, + expectedToastMsg: 'Success! "Dropdown" is updated', + }, + { + label: 'update changes works in preview modal with containers', + itemIndex: 5, + expectedToastMsg: 'Success! "Unit 1" is updated', + }, + ])('$label', async ({ itemIndex, expectedToastMsg }) => { const user = userEvent.setup(); const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); - const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey; + const usageKey = mockGetEntityLinks.response[itemIndex].downstreamUsageKey; axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {}); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' }); - expect(previewBtns.length).toEqual(5); - await user.click(previewBtns[0]); + expect(previewBtns.length).toEqual(7); + await user.click(previewBtns[itemIndex]); const dialog = await screen.findByRole('dialog'); const confirmBtn = await within(dialog).findByRole('button', { name: 'Accept changes' }); await user.click(confirmBtn); @@ -235,20 +255,31 @@ describe('', () => { expect(axiosMock.history.post.length).toEqual(1); }); expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey)); - expect(mockShowToast).toHaveBeenCalledWith('Success! "Dropdown" is updated'); - expect(mockInvalidateQueries).toHaveBeenCalledWith(['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX']); + expect(mockShowToast).toHaveBeenCalledWith(expectedToastMsg); + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX'] }); }); - it('ignore change works', async () => { + test.each([ + { + label: 'ignore change works with components', + itemIndex: 0, + expectedToastMsg: '"Dropdown" will remain out of sync with library content. You will be notified when this component is updated again.', + }, + { + label: 'ignore change works with containers', + itemIndex: 5, + expectedToastMsg: '"Unit 1" will remain out of sync with library content. You will be notified when this component is updated again.', + }, + ])('$label', async ({ itemIndex, expectedToastMsg }) => { const user = userEvent.setup(); const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); - const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey; + const usageKey = mockGetEntityLinks.response[itemIndex].downstreamUsageKey; axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {}); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' }); - expect(ignoreBtns.length).toEqual(5); + expect(ignoreBtns.length).toEqual(7); // Show confirmation modal on clicking ignore. - await user.click(ignoreBtns[0]); + await user.click(ignoreBtns[itemIndex]); const dialog = await screen.findByRole('dialog', { name: 'Ignore these changes?' }); expect(dialog).toBeInTheDocument(); const confirmBtn = await within(dialog).findByRole('button', { name: 'Ignore' }); @@ -257,21 +288,30 @@ describe('', () => { expect(axiosMock.history.delete.length).toEqual(1); }); expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey)); - expect(mockShowToast).toHaveBeenCalledWith( - '"Dropdown" will remain out of sync with library content. You will be notified when this component is updated again.', - ); - expect(mockInvalidateQueries).toHaveBeenCalledWith(['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX']); + expect(mockShowToast).toHaveBeenCalledWith(expectedToastMsg); + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX'] }); }); - it('ignore change works in preview', async () => { + test.each([ + { + label: 'ignore change works with components', + itemIndex: 0, + expectedToastMsg: '"Dropdown" will remain out of sync with library content. You will be notified when this component is updated again.', + }, + { + label: 'ignore change works with containers', + itemIndex: 5, + expectedToastMsg: '"Unit 1" will remain out of sync with library content. You will be notified when this component is updated again.', + }, + ])('$label', async ({ itemIndex, expectedToastMsg }) => { const user = userEvent.setup(); const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); - const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey; + const usageKey = mockGetEntityLinks.response[itemIndex].downstreamUsageKey; axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {}); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' }); - expect(previewBtns.length).toEqual(5); - await user.click(previewBtns[0]); + expect(previewBtns.length).toEqual(7); + await user.click(previewBtns[itemIndex]); const previewDialog = await screen.findByRole('dialog'); const ignoreBtn = await within(previewDialog).findByRole('button', { name: 'Ignore changes' }); await user.click(ignoreBtn); @@ -284,9 +324,7 @@ describe('', () => { expect(axiosMock.history.delete.length).toEqual(1); }); expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey)); - expect(mockShowToast).toHaveBeenCalledWith( - '"Dropdown" will remain out of sync with library content. You will be notified when this component is updated again.', - ); - expect(mockInvalidateQueries).toHaveBeenCalledWith(['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX']); + expect(mockShowToast).toHaveBeenCalledWith(expectedToastMsg); + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX'] }); }); }); diff --git a/src/course-libraries/ReviewTabContent.tsx b/src/course-libraries/ReviewTabContent.tsx index ba3fa2c0e8..4e8440e4d7 100644 --- a/src/course-libraries/ReviewTabContent.tsx +++ b/src/course-libraries/ReviewTabContent.tsx @@ -41,23 +41,39 @@ interface Props { courseId: string; } -interface BlockCardProps { +interface ItemCardProps { info: ContentHit; + itemType: 'component' | 'container'; actions?: React.ReactNode; libraryName?: string; } -const BlockCard: React.FC = ({ info, actions, libraryName }) => { +const ItemCard: React.FC = ({ + info, + itemType, + actions, + libraryName, +}) => { const intl = useIntl(); - const componentIcon = getItemIcon(info.blockType); + const itemIcon = getItemIcon(info.blockType); const breadcrumbs = tail(info.breadcrumbs) as Array<{ displayName: string, usageKey: string }>; - const getBlockLink = useCallback(() => { + const getItemLink = useCallback(() => { let key = info.usageKey; if (breadcrumbs?.length > 1) { key = breadcrumbs[breadcrumbs.length - 1].usageKey || key; } - return `${getConfig().STUDIO_BASE_URL}/container/${key}`; + + if (itemType === 'component') { + return `${getConfig().STUDIO_BASE_URL}/container/${key}`; + } + if (itemType === 'container') { + const encodedKey = encodeURIComponent(key); + return `${getConfig().STUDIO_BASE_URL}/course/${info.contextKey}?show=${encodedKey}`; + } + + // istanbul ignore next + return ''; }, [info]); return ( @@ -71,7 +87,7 @@ const BlockCard: React.FC = ({ info, actions, libraryName }) => - + @@ -87,14 +103,20 @@ const BlockCard: React.FC = ({ info, actions, libraryName }) => )} {intl.formatMessage(messages.breadcrumbLabel)} - - ({ label: breadcrumb.displayName }))} - spacer={/} - linkAs="span" - /> + + {info.blockType === 'chapter' ? ( +
+ {intl.formatMessage(messages.viewSectionInCourseLabel)} +
+ ) : ( + ({ label: breadcrumb.displayName }))} + spacer={/} + linkAs="span" + /> + )}
@@ -105,16 +127,21 @@ const BlockCard: React.FC = ({ info, actions, libraryName }) => ); }; -const ComponentReviewList = ({ - outOfSyncComponents, +const ItemReviewList = ({ + outOfSyncItems, }: { - outOfSyncComponents: PublishableEntityLink[]; + outOfSyncItems: PublishableEntityLink[]; }) => { const intl = useIntl(); const { showToast } = useContext(ToastContext); const [blockData, setBlockData] = useState(undefined); // ignore changes confirmation modal toggle. const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false); + // Toggle preview changes modal + const [isPreviewModalOpen, openPreviewModal, closePreviewModal] = useToggle(false); + const acceptChangesMutation = useAcceptLibraryBlockChanges(); + const ignoreChangesMutation = useIgnoreLibraryBlockChanges(); + const { hits, isLoading: isIndexDataLoading, @@ -133,32 +160,27 @@ const ComponentReviewList = ({ true, ); - const outOfSyncComponentsByKey = useMemo( - () => keyBy(outOfSyncComponents, 'downstreamUsageKey'), - [outOfSyncComponents], + const outOfSyncItemsByKey = useMemo( + () => keyBy(outOfSyncItems, 'downstreamUsageKey'), + [outOfSyncItems], ); const queryClient = useQueryClient(); - // Toggle preview changes modal - const [isModalOpen, openModal, closeModal] = useToggle(false); - const acceptChangesMutation = useAcceptLibraryBlockChanges(); - const ignoreChangesMutation = useIgnoreLibraryBlockChanges(); - const setSelectedBlockData = useCallback((info: ContentHit) => { setBlockData({ displayName: info.displayName, downstreamBlockId: info.usageKey, - upstreamBlockId: outOfSyncComponentsByKey[info.usageKey].upstreamUsageKey, - upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced, - isVertical: info.blockType === 'vertical', + upstreamBlockId: outOfSyncItemsByKey[info.usageKey].upstreamKey, + upstreamBlockVersionSynced: outOfSyncItemsByKey[info.usageKey].versionSynced, + isContainer: info.blockType === 'vertical' || info.blockType === 'sequencial' || info.blockType === 'chapter', }); - }, [outOfSyncComponentsByKey]); + }, [outOfSyncItemsByKey]); // Show preview changes on review const onReview = useCallback((info: ContentHit) => { setSelectedBlockData(info); - openModal(); - }, [setSelectedBlockData, openModal]); + openPreviewModal(); + }, [setSelectedBlockData, openPreviewModal]); const onIgnoreClick = useCallback((info: ContentHit) => { setSelectedBlockData(info); @@ -166,9 +188,9 @@ const ComponentReviewList = ({ }, [setSelectedBlockData, openConfirmModal]); const reloadLinks = useCallback((usageKey: string) => { - const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey; - queryClient.invalidateQueries(courseLibrariesQueryKeys.courseLibraries(courseKey)); - }, [outOfSyncComponentsByKey]); + const courseKey = outOfSyncItemsByKey[usageKey].downstreamContextKey; + queryClient.invalidateQueries({ queryKey: courseLibrariesQueryKeys.courseLibraries(courseKey) }); + }, [outOfSyncItemsByKey]); const postChange = (accept: boolean) => { // istanbul ignore if: this should never happen @@ -232,10 +254,11 @@ const ComponentReviewList = ({ return ( <> {downstreamInfo?.map((info) => ( -