Fixes issues related to component libraries' review/sync flow * Inconsistent sync pane title versions * Library content shown in preview warning only appears in review changes modal when that modal is opened from the review tab * Some new changes only appear within library review tab on scroll at top of list * Vertically misaligned sync icon in review changes message on course outline * Show available updates whenever content is updated, regardless of number of updates available
This commit is contained in:
@@ -118,6 +118,46 @@ describe('<CourseLibraries />', () => {
|
||||
userEvent.click(reviewActionBtn);
|
||||
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('show alert if max lastPublishedDate is greated than the local storage value', async () => {
|
||||
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
|
||||
localStorage.setItem(
|
||||
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
|
||||
String(lastPublishedDate.getTime() - 1000),
|
||||
);
|
||||
|
||||
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
|
||||
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
|
||||
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
|
||||
// review tab should be open by default as outOfSyncCount is greater than 0
|
||||
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
userEvent.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',
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesnt show alert if max lastPublishedDate is less than the local storage value', async () => {
|
||||
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
|
||||
localStorage.setItem(
|
||||
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
|
||||
String(lastPublishedDate.getTime() + 1000),
|
||||
);
|
||||
|
||||
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
|
||||
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
|
||||
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
|
||||
// review tab should be open by default as outOfSyncCount is greater than 0
|
||||
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
|
||||
userEvent.click(allTab);
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
screen.logTestingPlaygroundURL();
|
||||
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<CourseLibraries ReviewTab />', () => {
|
||||
@@ -160,7 +200,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
it('update changes works', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
||||
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
|
||||
@@ -176,7 +216,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
it('update changes works in preview modal', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
||||
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
|
||||
@@ -195,7 +235,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
it('ignore change works', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
||||
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
|
||||
@@ -218,7 +258,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
it('ignore change works in preview', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
||||
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
|
||||
|
||||
@@ -164,7 +164,7 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
if (tabKey !== CourseLibraryTabs.review) {
|
||||
return null;
|
||||
}
|
||||
if (!outOfSyncCount || outOfSyncCount === 0) {
|
||||
if (!outOfSyncCount) {
|
||||
return (
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<Icon src={CheckCircle} size="xs" />
|
||||
|
||||
@@ -18,12 +18,11 @@ interface OutOfSyncAlertProps {
|
||||
* in course can be updated. Following are the conditions for displaying the alert.
|
||||
*
|
||||
* * The alert is displayed if components are out of sync.
|
||||
* * If the user clicks on dismiss button, the state is stored in localstorage of user
|
||||
* in this format: outOfSyncCountAlert-${courseId} = <number of out of sync components>.
|
||||
* * If the number of sync components don't change for the course and the user opens outline
|
||||
* * If the user clicks on dismiss button, the state of dismiss is stored in localstorage of user
|
||||
* in this format: outOfSyncCountAlert-${courseId} = <datetime value in milliseconds>.
|
||||
* * If there are not new published components for the course and the user opens outline
|
||||
* in the same browser, they don't see the alert again.
|
||||
* * If the number changes, i.e., if a new component is out of sync or the user updates or ignores
|
||||
* a component, the alert is displayed again.
|
||||
* * If there is a new published component upstream, the alert is displayed again.
|
||||
*/
|
||||
export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
||||
showAlert,
|
||||
@@ -35,6 +34,8 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
||||
const intl = useIntl();
|
||||
const { data, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
|
||||
const outOfSyncCount = data?.reduce((count, lib) => count + (lib.readyToSyncCount || 0), 0);
|
||||
const lastPublishedDate = data?.map(lib => new Date(lib.lastPublishedAt || 0).getTime())
|
||||
.reduce((acc, lastPublished) => Math.max(lastPublished, acc), 0);
|
||||
const alertKey = `outOfSyncCountAlert-${courseId}`;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -46,13 +47,14 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
||||
setShowAlert(false);
|
||||
return;
|
||||
}
|
||||
const dismissedAlert = localStorage.getItem(alertKey);
|
||||
setShowAlert(parseInt(dismissedAlert || '', 10) !== outOfSyncCount);
|
||||
}, [outOfSyncCount, isLoading, data]);
|
||||
const dismissedAlertDate = parseInt(localStorage.getItem(alertKey) ?? '0', 10);
|
||||
|
||||
setShowAlert((lastPublishedDate ?? 0) > dismissedAlertDate);
|
||||
}, [outOfSyncCount, lastPublishedDate, isLoading, data]);
|
||||
|
||||
const dismissAlert = () => {
|
||||
setShowAlert(false);
|
||||
localStorage.setItem(alertKey, String(outOfSyncCount));
|
||||
localStorage.setItem(alertKey, Date.now().toString());
|
||||
onDismiss?.();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useMemo, useState,
|
||||
useCallback, useContext, useMemo, useState,
|
||||
} from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
import { tail, keyBy } from 'lodash';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Loop, Warning } from '@openedx/paragon/icons';
|
||||
import { Loop } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
import previewChangesMessages from '../course-unit/preview-changes/messages';
|
||||
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
|
||||
@@ -35,7 +35,6 @@ import { useLoadOnScroll } from '../hooks';
|
||||
import DeleteModal from '../generic/delete-modal/DeleteModal';
|
||||
import { PublishableEntityLink } from './data/api';
|
||||
import AlertError from '../generic/alert-error';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
@@ -100,10 +99,8 @@ const BlockCard: React.FC<BlockCardProps> = ({ info, actions }) => {
|
||||
|
||||
const ComponentReviewList = ({
|
||||
outOfSyncComponents,
|
||||
onSearchUpdate,
|
||||
}: {
|
||||
outOfSyncComponents: PublishableEntityLink[];
|
||||
onSearchUpdate: () => void;
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
@@ -111,23 +108,15 @@ const ComponentReviewList = ({
|
||||
// ignore changes confirmation modal toggle.
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
||||
const {
|
||||
hits: downstreamInfo,
|
||||
hits,
|
||||
isLoading: isIndexDataLoading,
|
||||
searchKeywords,
|
||||
hasError,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useSearchContext() as {
|
||||
hits: ContentHit[];
|
||||
isLoading: boolean;
|
||||
searchKeywords: string;
|
||||
searchSortOrder: SearchSortOption;
|
||||
hasError: boolean;
|
||||
hasNextPage: boolean | undefined,
|
||||
isFetchingNextPage: boolean;
|
||||
fetchNextPage: () => void;
|
||||
};
|
||||
} = useSearchContext();
|
||||
|
||||
const downstreamInfo = hits as ContentHit[];
|
||||
|
||||
useLoadOnScroll(
|
||||
hasNextPage,
|
||||
@@ -142,18 +131,12 @@ const ComponentReviewList = ({
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchKeywords) {
|
||||
onSearchUpdate();
|
||||
}
|
||||
}, [searchKeywords]);
|
||||
|
||||
// Toggle preview changes modal
|
||||
const [isModalOpen, openModal, closeModal] = useToggle(false);
|
||||
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
||||
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
||||
|
||||
const setSeletecdBlockData = (info: ContentHit) => {
|
||||
const setSelectedBlockData = useCallback((info: ContentHit) => {
|
||||
setBlockData({
|
||||
displayName: info.displayName,
|
||||
downstreamBlockId: info.usageKey,
|
||||
@@ -161,17 +144,18 @@ const ComponentReviewList = ({
|
||||
upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced,
|
||||
isVertical: info.blockType === 'vertical',
|
||||
});
|
||||
};
|
||||
}, [outOfSyncComponentsByKey]);
|
||||
|
||||
// Show preview changes on review
|
||||
const onReview = useCallback((info: ContentHit) => {
|
||||
setSeletecdBlockData(info);
|
||||
setSelectedBlockData(info);
|
||||
openModal();
|
||||
}, [setSeletecdBlockData, openModal]);
|
||||
}, [setSelectedBlockData, openModal]);
|
||||
|
||||
const onIgnoreClick = useCallback((info: ContentHit) => {
|
||||
setSeletecdBlockData(info);
|
||||
setSelectedBlockData(info);
|
||||
openConfirmModal();
|
||||
}, [setSeletecdBlockData, openConfirmModal]);
|
||||
}, [setSelectedBlockData, openConfirmModal]);
|
||||
|
||||
const reloadLinks = useCallback((usageKey: string) => {
|
||||
const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey;
|
||||
@@ -273,20 +257,14 @@ const ComponentReviewList = ({
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockData}
|
||||
isModalOpen={isModalOpen}
|
||||
closeModal={closeModal}
|
||||
postChange={postChange}
|
||||
alertNode={(
|
||||
<AlertMessage
|
||||
show
|
||||
variant="warning"
|
||||
icon={Warning}
|
||||
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{blockData && (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockData}
|
||||
isModalOpen={isModalOpen}
|
||||
closeModal={closeModal}
|
||||
postChange={postChange}
|
||||
/>
|
||||
)}
|
||||
<DeleteModal
|
||||
isOpen={isConfirmModalOpen}
|
||||
close={closeConfirmModal}
|
||||
@@ -303,37 +281,17 @@ const ComponentReviewList = ({
|
||||
const ReviewTabContent = ({ courseId }: Props) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
data: linkPages,
|
||||
data: outOfSyncComponents,
|
||||
isLoading: isSyncComponentsLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
} = useEntityLinks({ courseId, readyToSync: true });
|
||||
|
||||
const outOfSyncComponents = useMemo(
|
||||
() => linkPages?.pages?.reduce((links, page) => [...links, ...page.results], []) ?? [],
|
||||
[linkPages],
|
||||
);
|
||||
const downstreamKeys = useMemo(
|
||||
() => outOfSyncComponents?.map(link => link.downstreamUsageKey),
|
||||
[outOfSyncComponents],
|
||||
);
|
||||
|
||||
useLoadOnScroll(
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
true,
|
||||
);
|
||||
|
||||
const onSearchUpdate = () => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const disableSortOptions = [
|
||||
SearchSortOption.RELEVANCE,
|
||||
SearchSortOption.OLDEST,
|
||||
@@ -364,7 +322,6 @@ const ReviewTabContent = ({ courseId }: Props) => {
|
||||
</ActionRow>
|
||||
<ComponentReviewList
|
||||
outOfSyncComponents={outOfSyncComponents}
|
||||
onSearchUpdate={onSearchUpdate}
|
||||
/>
|
||||
</SearchContextProvider>
|
||||
);
|
||||
|
||||
@@ -3,17 +3,20 @@
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"readyToSyncCount": 5,
|
||||
"totalCount": 14
|
||||
"totalCount": 14,
|
||||
"lastPublishedAt": "2025-05-01T20:20:44.989042Z"
|
||||
},
|
||||
{
|
||||
"upstreamContextTitle": "CS problems 2",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||
"readyToSyncCount": 0,
|
||||
"totalCount": 21
|
||||
"totalCount": 21,
|
||||
"lastPublishedAt": "2025-05-01T21:20:44.989042Z"
|
||||
},
|
||||
{
|
||||
"upstreamContextTitle": "CS problems",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB",
|
||||
"totalCount": 3
|
||||
"totalCount": 3,
|
||||
"lastPublishedAt": "2025-05-01T22:20:44.989042Z"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,79 +1,72 @@
|
||||
{
|
||||
"count": 7,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"num_pages": 1,
|
||||
"current_page": 1,
|
||||
"results": [
|
||||
{
|
||||
"id": 875,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 876,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 884,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 26,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 16,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 889,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 890,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
[
|
||||
{
|
||||
"id": 875,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 876,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 884,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 26,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 16,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 889,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 890,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -28,27 +28,17 @@ export async function mockGetEntityLinks(
|
||||
case mockGetEntityLinks.courseKeyLoading:
|
||||
return new Promise(() => {});
|
||||
case mockGetEntityLinks.courseKeyEmpty:
|
||||
return Promise.resolve({
|
||||
next: null,
|
||||
previous: null,
|
||||
nextPageNum: null,
|
||||
previousPageNum: null,
|
||||
count: 0,
|
||||
numPages: 0,
|
||||
currentPage: 0,
|
||||
results: [],
|
||||
});
|
||||
return Promise.resolve([]);
|
||||
default: {
|
||||
const { response } = mockGetEntityLinks;
|
||||
let { response } = mockGetEntityLinks;
|
||||
if (readyToSync !== undefined) {
|
||||
response.results = response.results.filter((o) => o.readyToSync === readyToSync);
|
||||
response.count = response.results.length;
|
||||
response = response.filter((o) => o.readyToSync === readyToSync);
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
mockGetEntityLinks.courseKey = mockLinksResult.results[0].downstreamContextKey;
|
||||
mockGetEntityLinks.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||
mockGetEntityLinks.invalidCourseKey = 'course_key_error';
|
||||
mockGetEntityLinks.courseKeyLoading = 'courseKeyLoading';
|
||||
mockGetEntityLinks.courseKeyEmpty = 'courseKeyEmpty';
|
||||
@@ -85,7 +75,7 @@ export async function mockGetEntityLinksSummaryByDownstreamContext(
|
||||
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response);
|
||||
}
|
||||
}
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult.results[0].downstreamContextKey;
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||
mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error';
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading';
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
|
||||
|
||||
@@ -38,32 +38,13 @@ export interface PublishableEntityLinkSummary {
|
||||
upstreamContextTitle: string;
|
||||
readyToSyncCount: number;
|
||||
totalCount: number;
|
||||
lastPublishedAt: string;
|
||||
}
|
||||
|
||||
export const getEntityLinks = async (
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
pageParam?: number,
|
||||
pageSize?: number,
|
||||
): Promise<PaginatedData<PublishableEntityLink[]>> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getEntityLinksByDownstreamContextUrl(), {
|
||||
params: {
|
||||
course_id: downstreamContextKey,
|
||||
ready_to_sync: readyToSync,
|
||||
upstream_usage_key: upstreamUsageKey,
|
||||
page_size: pageSize,
|
||||
page: pageParam,
|
||||
},
|
||||
});
|
||||
return camelCaseObject(data);
|
||||
};
|
||||
|
||||
export const getUnpaginatedEntityLinks = async (
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
): Promise<PublishableEntityLink[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getEntityLinksByDownstreamContextUrl(), {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { getEntityLinksByDownstreamContextUrl } from './api';
|
||||
import { useEntityLinks, useUnpaginatedEntityLinks } from './apiHooks';
|
||||
import { useEntityLinks } from './apiHooks';
|
||||
|
||||
let axiosMock: MockAdapter;
|
||||
|
||||
@@ -39,26 +39,11 @@ describe('course libraries api hooks', () => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it('should return paginated links for course', async () => {
|
||||
const courseId = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl();
|
||||
const expectedResult = {
|
||||
next: null, results: [], previous: null, total: 0,
|
||||
};
|
||||
axiosMock.onGet(url).reply(200, expectedResult);
|
||||
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper });
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBeFalsy();
|
||||
});
|
||||
expect(result.current.data?.pages).toEqual([expectedResult]);
|
||||
expect(axiosMock.history.get[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should return links for course', async () => {
|
||||
const courseId = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl();
|
||||
axiosMock.onGet(url).reply(200, []);
|
||||
const { result } = renderHook(() => useUnpaginatedEntityLinks({ courseId }), { wrapper });
|
||||
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper });
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext, getUnpaginatedEntityLinks } from './api';
|
||||
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext } from './api';
|
||||
|
||||
export const courseLibrariesQueryKeys = {
|
||||
all: ['courseLibraries'],
|
||||
@@ -29,39 +28,10 @@ export const courseLibrariesQueryKeys = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch publishable entity links by course key.
|
||||
* Hook to fetch list of publishable entity links by course key.
|
||||
* (That is, get a list of the library components used in the given course.)
|
||||
*/
|
||||
export const useEntityLinks = ({
|
||||
courseId, readyToSync, upstreamUsageKey, pageSize,
|
||||
}: {
|
||||
courseId?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
pageSize?: number
|
||||
}) => (
|
||||
useInfiniteQuery({
|
||||
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
}),
|
||||
queryFn: ({ pageParam }) => getEntityLinks(
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
pageParam,
|
||||
pageSize,
|
||||
),
|
||||
getNextPageParam: (lastPage) => lastPage.nextPageNum,
|
||||
enabled: courseId !== undefined || upstreamUsageKey !== undefined || readyToSync !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Hook to fetch unpaginated list of publishable entity links by course key.
|
||||
*/
|
||||
export const useUnpaginatedEntityLinks = ({
|
||||
courseId, readyToSync, upstreamUsageKey,
|
||||
}: {
|
||||
courseId?: string,
|
||||
@@ -74,7 +44,7 @@ export const useUnpaginatedEntityLinks = ({
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
}),
|
||||
queryFn: () => getUnpaginatedEntityLinks(
|
||||
queryFn: () => getEntityLinks(
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
|
||||
@@ -116,11 +116,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Something went wrong! Could not fetch results.',
|
||||
description: 'Generic error message displayed when fetching link data fails.',
|
||||
},
|
||||
olderVersionPreviewAlert: {
|
||||
id: 'course-authoring.course-libraries.reviw-tab.preview.old-version-alert',
|
||||
defaultMessage: 'The old version preview is the previous library version',
|
||||
description: 'Alert message stating that older version in preview is of library block',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -229,12 +229,14 @@ const UnitCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</SortableItem>
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockSyncData}
|
||||
isModalOpen={isSyncModalOpen}
|
||||
closeModal={closeSyncModal}
|
||||
postChange={handleOnPostChangeSync}
|
||||
/>
|
||||
{blockSyncData && (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockSyncData}
|
||||
isModalOpen={isSyncModalOpen}
|
||||
closeModal={closeSyncModal}
|
||||
postChange={handleOnPostChangeSync}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import IframePreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.'
|
||||
import { messageTypes } from '../constants';
|
||||
import { libraryBlockChangesUrl } from '../data/api';
|
||||
import { ToastActionData } from '../../generic/toast-context';
|
||||
import { getLibraryBlockMetadataUrl, getLibraryContainerApiUrl } from '../../library-authoring/data/api';
|
||||
|
||||
const usageKey = 'some-id';
|
||||
const defaultEventData: LibraryChangesMessageData = {
|
||||
@@ -66,7 +65,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
expect(await screen.findByRole('tab', { name: 'Old version' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders displayName for units', async () => {
|
||||
it('renders default displayName for units with no displayName', async () => {
|
||||
render({ ...defaultEventData, isVertical: true, displayName: '' });
|
||||
|
||||
expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument();
|
||||
@@ -78,24 +77,6 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
expect(await screen.findByText('Preview changes: Component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders both new and old title if they are different', async () => {
|
||||
axiosMock.onGet(getLibraryBlockMetadataUrl(defaultEventData.upstreamBlockId)).reply(200, {
|
||||
displayName: 'New test block',
|
||||
});
|
||||
render();
|
||||
|
||||
expect(await screen.findByText('Preview changes: Test block -> New test block')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders both new and old title if they are different on units', async () => {
|
||||
axiosMock.onGet(getLibraryContainerApiUrl(defaultEventData.upstreamBlockId)).reply(200, {
|
||||
displayName: 'New test Unit',
|
||||
});
|
||||
render({ ...defaultEventData, isVertical: true, displayName: 'Test Unit' });
|
||||
|
||||
expect(await screen.findByText('Preview changes: Test Unit -> New test Unit')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accept changes works', async () => {
|
||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||
render();
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import {
|
||||
ActionRow, Button, ModalDialog, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { Warning } from '@openedx/paragon/icons';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useEventListener } from '../../generic/hooks';
|
||||
import { messageTypes } from '../constants';
|
||||
import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget';
|
||||
import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks';
|
||||
import AlertMessage from '../../generic/alert-message';
|
||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||
import messages from './messages';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import LoadingButton from '../../generic/loading-button';
|
||||
import Loading from '../../generic/Loading';
|
||||
import { useContainer, useLibraryBlockMetadata } from '../../library-authoring/data/apiHooks';
|
||||
|
||||
export interface LibraryChangesMessageData {
|
||||
displayName: string,
|
||||
@@ -25,11 +26,10 @@ export interface LibraryChangesMessageData {
|
||||
}
|
||||
|
||||
export interface PreviewLibraryXBlockChangesProps {
|
||||
blockData?: LibraryChangesMessageData,
|
||||
blockData: LibraryChangesMessageData,
|
||||
isModalOpen: boolean,
|
||||
closeModal: () => void,
|
||||
postChange: (accept: boolean) => void,
|
||||
alertNode?: React.ReactNode,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +41,6 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
isModalOpen,
|
||||
closeModal,
|
||||
postChange,
|
||||
alertNode,
|
||||
}: PreviewLibraryXBlockChangesProps) => {
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const intl = useIntl();
|
||||
@@ -49,32 +48,9 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
// ignore changes confirmation modal toggle.
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
||||
|
||||
// TODO: Split into two different components to avoid making these two calls in which
|
||||
// one will definitely fail
|
||||
const { data: componentMetadata } = useLibraryBlockMetadata(blockData?.upstreamBlockId);
|
||||
const { data: unitMetadata } = useContainer(blockData?.upstreamBlockId);
|
||||
|
||||
const metadata = blockData?.isVertical ? unitMetadata : componentMetadata;
|
||||
|
||||
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
||||
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
const oldName = blockData?.displayName;
|
||||
const newName = metadata?.displayName;
|
||||
|
||||
if (!oldName) {
|
||||
if (blockData?.isVertical) {
|
||||
return intl.formatMessage(messages.defaultUnitTitle);
|
||||
}
|
||||
return intl.formatMessage(messages.defaultComponentTitle);
|
||||
}
|
||||
if (oldName === newName || !newName) {
|
||||
return intl.formatMessage(messages.title, { blockTitle: oldName });
|
||||
}
|
||||
return intl.formatMessage(messages.diffTitle, { oldName, newName });
|
||||
}, [blockData, metadata]);
|
||||
|
||||
const getBody = useCallback(() => {
|
||||
if (!blockData) {
|
||||
return <Loading />;
|
||||
@@ -108,12 +84,21 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
}
|
||||
}, [blockData]);
|
||||
|
||||
const defaultTitle = intl.formatMessage(
|
||||
blockData.isVertical
|
||||
? messages.defaultUnitTitle
|
||||
: messages.defaultComponentTitle,
|
||||
);
|
||||
const title = blockData.displayName
|
||||
? intl.formatMessage(messages.title, { blockTitle: blockData?.displayName })
|
||||
: defaultTitle;
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
size="xl"
|
||||
title={getTitle()}
|
||||
title={title}
|
||||
className="lib-preview-xblock-changes-modal"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
@@ -121,11 +106,16 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{getTitle()}
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
{alertNode}
|
||||
<AlertMessage
|
||||
show
|
||||
variant="warning"
|
||||
icon={Warning}
|
||||
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
|
||||
/>
|
||||
{getBody()}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
@@ -186,6 +176,10 @@ const IframePreviewLibraryXBlockChanges = () => {
|
||||
|
||||
useEventListener('message', receiveMessage);
|
||||
|
||||
if (!blockData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockData}
|
||||
|
||||
@@ -6,11 +6,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Preview changes: {blockTitle}',
|
||||
description: 'Preview changes modal title text',
|
||||
},
|
||||
diffTitle: {
|
||||
id: 'authoring.course-unit.preview-changes.modal-diff-title',
|
||||
defaultMessage: 'Preview changes: {oldName} -> {newName}',
|
||||
description: 'Preview changes modal title text',
|
||||
},
|
||||
defaultUnitTitle: {
|
||||
id: 'authoring.course-unit.preview-changes.modal-default-unit-title',
|
||||
defaultMessage: 'Preview changes: Unit',
|
||||
@@ -61,6 +56,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Ignore',
|
||||
description: 'Preview changes confirmation dialog confirm button text when user clicks on ignore changes.',
|
||||
},
|
||||
olderVersionPreviewAlert: {
|
||||
id: 'course-authoring.review-tab.preview.old-version-alert',
|
||||
defaultMessage: 'The old version preview is the previous library version',
|
||||
description: 'Alert message stating that older version in preview is of library block',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
6
src/generic/alert-message/index.scss
Normal file
6
src/generic/alert-message/index.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
// TODO: remove this after upstream fix merging: https://github.com/openedx/paragon/pull/3562
|
||||
.alert {
|
||||
.alert-message-content {
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,5 @@
|
||||
@import "./modal-dropzone/ModalDropzone";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
@import "./block-type-utils";
|
||||
@import "./modal-iframe"
|
||||
@import "./modal-iframe";
|
||||
@import "./alert-message";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { mockFetchIndexDocuments, mockContentSearchConfig } from '../../search-manager/data/api.mock';
|
||||
import {
|
||||
mockContentLibrary,
|
||||
mockGetUnpaginatedEntityLinks,
|
||||
mockGetEntityLinks,
|
||||
mockLibraryBlockMetadata,
|
||||
mockXBlockAssets,
|
||||
mockXBlockOLX,
|
||||
@@ -21,7 +21,7 @@ mockContentLibrary.applyMock();
|
||||
mockLibraryBlockMetadata.applyMock();
|
||||
mockXBlockAssets.applyMock();
|
||||
mockXBlockOLX.applyMock();
|
||||
mockGetUnpaginatedEntityLinks.applyMock();
|
||||
mockGetEntityLinks.applyMock();
|
||||
mockFetchIndexDocuments.applyMock();
|
||||
|
||||
const render = (usageKey: string) => baseRender(<ComponentDetails />, {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import {
|
||||
mockContentLibrary,
|
||||
mockLibraryBlockMetadata,
|
||||
mockGetUnpaginatedEntityLinks,
|
||||
mockGetEntityLinks,
|
||||
} from '../data/api.mocks';
|
||||
import { mockContentSearchConfig, mockFetchIndexDocuments } from '../../search-manager/data/api.mock';
|
||||
import { LibraryProvider } from '../common/context/LibraryContext';
|
||||
@@ -18,7 +18,7 @@ import { getXBlockPublishApiUrl } from '../data/api';
|
||||
mockContentSearchConfig.applyMock();
|
||||
mockContentLibrary.applyMock();
|
||||
mockLibraryBlockMetadata.applyMock();
|
||||
mockGetUnpaginatedEntityLinks.applyMock();
|
||||
mockGetEntityLinks.applyMock();
|
||||
mockFetchIndexDocuments.applyMock();
|
||||
jest.mock('./ComponentPreview', () => ({
|
||||
__esModule: true, // Required when mocking 'default' export
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible, Hyperlink, Stack } from '@openedx/paragon';
|
||||
import { useMemo } from 'react';
|
||||
import { useUnpaginatedEntityLinks } from '../../course-libraries/data/apiHooks';
|
||||
import { useEntityLinks } from '../../course-libraries/data/apiHooks';
|
||||
|
||||
import AlertError from '../../generic/alert-error';
|
||||
import Loading from '../../generic/Loading';
|
||||
@@ -34,7 +34,7 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => {
|
||||
isError: isErrorDownstreamLinks,
|
||||
error: errorDownstreamLinks,
|
||||
isLoading: isLoadingDownstreamLinks,
|
||||
} = useUnpaginatedEntityLinks({ upstreamUsageKey: usageKey });
|
||||
} = useEntityLinks({ upstreamUsageKey: usageKey });
|
||||
|
||||
const downstreamKeys = useMemo(
|
||||
() => dataDownstreamLinks?.map(link => link.downstreamUsageKey) || [],
|
||||
|
||||
@@ -5,7 +5,7 @@ import BaseModal from '../../editors/sharedComponents/BaseModal';
|
||||
import messages from './messages';
|
||||
import infoMessages from '../component-info/messages';
|
||||
import { ComponentUsage } from '../component-info/ComponentUsage';
|
||||
import { useUnpaginatedEntityLinks } from '../../course-libraries/data/apiHooks';
|
||||
import { useEntityLinks } from '../../course-libraries/data/apiHooks';
|
||||
|
||||
interface PublishConfirmationModalProps {
|
||||
isOpen: boolean,
|
||||
@@ -29,7 +29,7 @@ const PublishConfirmationModal = ({
|
||||
const {
|
||||
data: dataDownstreamLinks,
|
||||
isLoading: isLoadingDownstreamLinks,
|
||||
} = useUnpaginatedEntityLinks({ upstreamUsageKey: usageKey });
|
||||
} = useEntityLinks({ upstreamUsageKey: usageKey });
|
||||
|
||||
const hasDownstreamUsages = !isLoadingDownstreamLinks && dataDownstreamLinks?.length !== 0;
|
||||
|
||||
|
||||
@@ -675,12 +675,12 @@ mockBlockTypesMetadata.blockTypesMetadata = [
|
||||
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
|
||||
mockBlockTypesMetadata.applyMock = () => jest.spyOn(api, 'getBlockTypes').mockImplementation(mockBlockTypesMetadata);
|
||||
|
||||
export async function mockGetUnpaginatedEntityLinks(
|
||||
export async function mockGetEntityLinks(
|
||||
_downstreamContextKey?: string,
|
||||
_readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
): ReturnType<typeof courseLibApi.getUnpaginatedEntityLinks> {
|
||||
const thisMock = mockGetUnpaginatedEntityLinks;
|
||||
): ReturnType<typeof courseLibApi.getEntityLinks> {
|
||||
const thisMock = mockGetEntityLinks;
|
||||
switch (upstreamUsageKey) {
|
||||
case thisMock.upstreamUsageKey: return thisMock.response;
|
||||
case mockLibraryBlockMetadata.usageKeyPublishedWithChanges: return thisMock.response;
|
||||
@@ -688,8 +688,8 @@ export async function mockGetUnpaginatedEntityLinks(
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
mockGetUnpaginatedEntityLinks.upstreamUsageKey = mockLibraryBlockMetadata.usageKeyPublished;
|
||||
mockGetUnpaginatedEntityLinks.response = downstreamLinkInfo.results[0].hits.map((obj: { usageKey: any; }) => ({
|
||||
mockGetEntityLinks.upstreamUsageKey = mockLibraryBlockMetadata.usageKeyPublished;
|
||||
mockGetEntityLinks.response = downstreamLinkInfo.results[0].hits.map((obj: { usageKey: any; }) => ({
|
||||
id: 875,
|
||||
upstreamContextTitle: 'CS problems 3',
|
||||
upstreamVersion: 10,
|
||||
@@ -703,10 +703,10 @@ mockGetUnpaginatedEntityLinks.response = downstreamLinkInfo.results[0].hits.map(
|
||||
created: '2025-02-08T14:07:05.588484Z',
|
||||
updated: '2025-02-08T14:07:05.588484Z',
|
||||
}));
|
||||
mockGetUnpaginatedEntityLinks.emptyUsageKey = 'lb:Axim:TEST1:html:empty';
|
||||
mockGetUnpaginatedEntityLinks.emptyComponentUsage = [] as courseLibApi.PublishableEntityLink[];
|
||||
mockGetEntityLinks.emptyUsageKey = 'lb:Axim:TEST1:html:empty';
|
||||
mockGetEntityLinks.emptyComponentUsage = [] as courseLibApi.PublishableEntityLink[];
|
||||
|
||||
mockGetUnpaginatedEntityLinks.applyMock = () => jest.spyOn(
|
||||
mockGetEntityLinks.applyMock = () => jest.spyOn(
|
||||
courseLibApi,
|
||||
'getUnpaginatedEntityLinks',
|
||||
).mockImplementation(mockGetUnpaginatedEntityLinks);
|
||||
'getEntityLinks',
|
||||
).mockImplementation(mockGetEntityLinks);
|
||||
|
||||
Reference in New Issue
Block a user