feat: Show sections/subsections/units available for sync in library sync page [FC-0097] (#2271)
- Adds Units, Subsection, and section cards in the libraries sync page. - Rename of `mockGetEntityLinks` to `mockGetComponentEntityLinks` - Use the top-level parent logic - Which user roles will this change impact? "Course Author".
This commit is contained in:
@@ -78,14 +78,14 @@ describe('<CourseLibraries />', () => {
|
||||
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('<CourseLibraries />', () => {
|
||||
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('<CourseLibraries />', () => {
|
||||
|
||||
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('<CourseLibraries />', () => {
|
||||
// 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('<CourseLibraries />', () => {
|
||||
|
||||
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('<CourseLibraries />', () => {
|
||||
|
||||
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('<CourseLibraries ReviewTab />', () => {
|
||||
});
|
||||
|
||||
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('<CourseLibraries ReviewTab />', () => {
|
||||
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('<CourseLibraries ReviewTab />', () => {
|
||||
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('<CourseLibraries ReviewTab />', () => {
|
||||
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'] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<BlockCardProps> = ({ info, actions, libraryName }) => {
|
||||
const ItemCard: React.FC<ItemCardProps> = ({
|
||||
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<BlockCardProps> = ({ info, actions, libraryName }) =>
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<Stack direction="vertical" gap={1}>
|
||||
<Stack direction="horizontal" gap={1} className="micro text-gray-500">
|
||||
<Icon src={componentIcon} size="xs" />
|
||||
<Icon src={itemIcon} size="xs" />
|
||||
<BlockTypeLabel blockType={info.blockType} />
|
||||
</Stack>
|
||||
<Stack direction="horizontal" className="small" gap={1}>
|
||||
@@ -87,14 +103,20 @@ const BlockCard: React.FC<BlockCardProps> = ({ info, actions, libraryName }) =>
|
||||
</Stack>
|
||||
)}
|
||||
{intl.formatMessage(messages.breadcrumbLabel)}
|
||||
<Hyperlink showLaunchIcon={false} destination={getBlockLink()} target="_blank">
|
||||
<Breadcrumb
|
||||
className="micro text-gray-700 border-bottom"
|
||||
ariaLabel={intl.formatMessage(messages.breadcrumbLabel)}
|
||||
links={breadcrumbs.map((breadcrumb) => ({ label: breadcrumb.displayName }))}
|
||||
spacer={<span className="custom-spacer">/</span>}
|
||||
linkAs="span"
|
||||
/>
|
||||
<Hyperlink showLaunchIcon={false} destination={getItemLink()} target="_blank">
|
||||
{info.blockType === 'chapter' ? (
|
||||
<div className="micro text-gray-700 border-bottom">
|
||||
{intl.formatMessage(messages.viewSectionInCourseLabel)}
|
||||
</div>
|
||||
) : (
|
||||
<Breadcrumb
|
||||
className="micro text-gray-700 border-bottom"
|
||||
ariaLabel={intl.formatMessage(messages.breadcrumbLabel)}
|
||||
links={breadcrumbs.map((breadcrumb) => ({ label: breadcrumb.displayName }))}
|
||||
spacer={<span className="custom-spacer">/</span>}
|
||||
linkAs="span"
|
||||
/>
|
||||
)}
|
||||
</Hyperlink>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -105,16 +127,21 @@ const BlockCard: React.FC<BlockCardProps> = ({ info, actions, libraryName }) =>
|
||||
);
|
||||
};
|
||||
|
||||
const ComponentReviewList = ({
|
||||
outOfSyncComponents,
|
||||
const ItemReviewList = ({
|
||||
outOfSyncItems,
|
||||
}: {
|
||||
outOfSyncComponents: PublishableEntityLink[];
|
||||
outOfSyncItems: PublishableEntityLink[];
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const [blockData, setBlockData] = useState<LibraryChangesMessageData | undefined>(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) => (
|
||||
<BlockCard
|
||||
<ItemCard
|
||||
key={info.usageKey}
|
||||
info={info}
|
||||
libraryName={outOfSyncComponentsByKey[info.usageKey]?.upstreamContextTitle}
|
||||
itemType={outOfSyncItemsByKey[info.usageKey]?.upstreamType}
|
||||
libraryName={outOfSyncItemsByKey[info.usageKey]?.upstreamContextTitle}
|
||||
actions={(
|
||||
<ActionRow>
|
||||
<Button
|
||||
@@ -269,8 +292,8 @@ const ComponentReviewList = ({
|
||||
{blockData && (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockData}
|
||||
isModalOpen={isModalOpen}
|
||||
closeModal={closeModal}
|
||||
isModalOpen={isPreviewModalOpen}
|
||||
closeModal={closePreviewModal}
|
||||
postChange={postChange}
|
||||
/>
|
||||
)}
|
||||
@@ -290,15 +313,19 @@ const ComponentReviewList = ({
|
||||
const ReviewTabContent = ({ courseId }: Props) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
data: outOfSyncComponents,
|
||||
isLoading: isSyncComponentsLoading,
|
||||
data: outOfSyncItems,
|
||||
isLoading: isSyncItemsLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useEntityLinks({ courseId, readyToSync: true });
|
||||
} = useEntityLinks({
|
||||
courseId,
|
||||
readyToSync: true,
|
||||
useTopLevelParents: true,
|
||||
});
|
||||
|
||||
const downstreamKeys = useMemo(
|
||||
() => outOfSyncComponents?.map(link => link.downstreamUsageKey),
|
||||
[outOfSyncComponents],
|
||||
() => outOfSyncItems?.map(link => link.downstreamUsageKey),
|
||||
[outOfSyncItems],
|
||||
);
|
||||
|
||||
const disableSortOptions = [
|
||||
@@ -308,7 +335,7 @@ const ReviewTabContent = ({ courseId }: Props) => {
|
||||
SearchSortOption.RECENTLY_PUBLISHED,
|
||||
];
|
||||
|
||||
if (isSyncComponentsLoading) {
|
||||
if (isSyncItemsLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
@@ -329,8 +356,8 @@ const ReviewTabContent = ({ courseId }: Props) => {
|
||||
<SearchSortWidget disableOptions={disableSortOptions} />
|
||||
<ActionRow.Spacer />
|
||||
</ActionRow>
|
||||
<ComponentReviewList
|
||||
outOfSyncComponents={outOfSyncComponents}
|
||||
<ItemReviewList
|
||||
outOfSyncItems={outOfSyncItems}
|
||||
/>
|
||||
</SearchContextProvider>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"readyToSyncCount": 5,
|
||||
"readyToSyncCount": 7,
|
||||
"totalCount": 14,
|
||||
"lastPublishedAt": "2025-05-01T20:20:44.989042Z"
|
||||
},
|
||||
|
||||
@@ -364,13 +364,125 @@
|
||||
"org": "OpenEdx",
|
||||
"access_id": "4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Unit 1",
|
||||
"block_id": "20f2c12b8b7f4b4bab7a900576c78ad8",
|
||||
"content": {
|
||||
"problem_types": [
|
||||
"optionresponse"
|
||||
],
|
||||
"capa_content": "This is a super unit"
|
||||
},
|
||||
"description": "This is a super unit",
|
||||
"tags": {},
|
||||
"id": "block-v1itcracydemoxcoursextypeverticalblocka20f2c12b8b7f4b4bab7a900576c78ad8-efa48aff",
|
||||
"type": "course_block",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "OpenedX Demo Course"
|
||||
},
|
||||
{
|
||||
"display_name": "Module 1: Dive into the Open edX® platform!",
|
||||
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@20f2c12b8b7f4b4bab7a900576c78ad8",
|
||||
"block_type": "vertical",
|
||||
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"org": "OpenEdx",
|
||||
"access_id": 4,
|
||||
"_formatted": {
|
||||
"display_name": "Unit 1",
|
||||
"block_id": "20f2c12b8b7f4b4bab7a900576c78ad8",
|
||||
"content": {
|
||||
"problem_types": [
|
||||
"optionresponse"
|
||||
],
|
||||
"capa_content": "This is a super unit"
|
||||
},
|
||||
"description": "This is a super unit",
|
||||
"tags": {},
|
||||
"id": "block-v1itcracydemoxcoursextypeverticalblocka20f2c12b8b7f4b4bab7a900576c78ad8-efa48aff",
|
||||
"type": "course_block",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "OpenedX Demo Course"
|
||||
},
|
||||
{
|
||||
"display_name": "Module 1: Dive into the Open edX® platform!",
|
||||
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@20f2c12b8b7f4b4bab7a900576c78ad8",
|
||||
"block_type": "vertical",
|
||||
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"org": "OpenEdx",
|
||||
"access_id": "4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Section 1",
|
||||
"block_id": "20f2c12b8b7e4b4cab7a900576c78cv5",
|
||||
"content": {
|
||||
"problem_types": [
|
||||
"optionresponse"
|
||||
],
|
||||
"capa_content": "This is a super section"
|
||||
},
|
||||
"description": "This is a super section",
|
||||
"tags": {},
|
||||
"id": "block-v1itcracydemoxcoursextypechapterblocka20f2c12b8b7e4b4cab7a900576c78cv5",
|
||||
"type": "course_block",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "OpenedX Demo Course"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@20f2c12b8b7e4b4cab7a900576c78cv5",
|
||||
"block_type": "chapter",
|
||||
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"org": "OpenEdx",
|
||||
"access_id": 4,
|
||||
"_formatted": {
|
||||
"display_name": "Section 1",
|
||||
"block_id": "20f2c12b8b7e4b4cab7a900576c78cv5",
|
||||
"content": {
|
||||
"problem_types": [
|
||||
"optionresponse"
|
||||
],
|
||||
"capa_content": "This is a super section"
|
||||
},
|
||||
"description": "This is a super section",
|
||||
"tags": {},
|
||||
"id": "block-v1itcracydemoxcoursextypechapterblocka20f2c12b8b7e4b4cab7a900576c78cv5",
|
||||
"type": "course_block",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "OpenedX Demo Course"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@20f2c12b8b7e4b4cab7a900576c78cv5",
|
||||
"block_type": "chapter",
|
||||
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"org": "OpenEdx",
|
||||
"access_id": "4"
|
||||
}
|
||||
}
|
||||
],
|
||||
"query": "",
|
||||
"processingTimeMs": 8,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 5
|
||||
"estimatedTotalHits": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamType": "component",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
@@ -18,7 +19,8 @@
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamType": "component",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
@@ -32,7 +34,8 @@
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 26,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamType": "component",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
@@ -46,7 +49,8 @@
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamType": "component",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
@@ -60,7 +64,8 @@
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamType": "component",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
@@ -68,5 +73,35 @@
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 891,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 17,
|
||||
"readyToSync": true,
|
||||
"upstreamKey": "lct:OpenedX:CSPROB3:unit:cb367f92-bf7d-4d08-86cd-aae9efa48aff",
|
||||
"upstreamType": "container",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@20f2c12b8b7f4b4bab7a900576c78ad8",
|
||||
"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": 892,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 3,
|
||||
"readyToSync": true,
|
||||
"upstreamKey": "lct:OpenedX:CSPROB3:section:9a90e4e4-cdf9-48ce-89b5-a8f9f07ccbfc",
|
||||
"upstreamType": "container",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@20f2c12b8b7e4b4cab7a900576c78cv5",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
/* istanbul ignore file */
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import fetchMock from 'fetch-mock-jest';
|
||||
import * as libApi from '@src/library-authoring/data/api';
|
||||
import { createAxiosError } from '@src/testUtils';
|
||||
|
||||
import mockLinksResult from '../__mocks__/publishableEntityLinks.json';
|
||||
import mockSummaryResult from '../__mocks__/linkCourseSummary.json';
|
||||
import mockLinkDetailsFromIndex from '../__mocks__/linkDetailsFromIndex.json';
|
||||
import mockLibBlockMetadata from '../__mocks__/libBlockMetadata.json';
|
||||
import { createAxiosError } from '../../testUtils';
|
||||
import * as api from './api';
|
||||
import * as libApi from '../../library-authoring/data/api';
|
||||
|
||||
/**
|
||||
* Mock for `getEntityLinks()`
|
||||
*
|
||||
* This mock returns a fixed response for the downstreamContextKey.
|
||||
*/
|
||||
export async function mockGetEntityLinks(
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
@@ -61,7 +57,7 @@ export async function mockGetEntityLinksSummaryByDownstreamContext(
|
||||
throw createAxiosError({
|
||||
code: 404,
|
||||
message: 'Not found.',
|
||||
path: api.getEntityLinksByDownstreamContextUrl(),
|
||||
path: api.getEntityLinksSummaryByDownstreamContextUrl(courseId),
|
||||
});
|
||||
case mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading:
|
||||
return new Promise(() => {});
|
||||
|
||||
@@ -3,9 +3,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
|
||||
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/`;
|
||||
export const getContainerEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstream-containers/`;
|
||||
|
||||
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams-all/`;
|
||||
export const getEntityLinksSummaryByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/${downstreamContextKey}/summary`;
|
||||
|
||||
export interface PaginatedData<T> {
|
||||
@@ -33,7 +31,7 @@ export interface BasePublishableEntityLink {
|
||||
readyToSync: boolean;
|
||||
}
|
||||
|
||||
export interface PublishableEntityLink extends BasePublishableEntityLink {
|
||||
export interface ComponentPublishableEntityLink extends BasePublishableEntityLink {
|
||||
upstreamUsageKey: string;
|
||||
}
|
||||
|
||||
@@ -41,6 +39,11 @@ export interface ContainerPublishableEntityLink extends BasePublishableEntityLin
|
||||
upstreamContainerKey: string;
|
||||
}
|
||||
|
||||
export interface PublishableEntityLink extends BasePublishableEntityLink {
|
||||
upstreamKey: string;
|
||||
upstreamType: 'component' | 'container';
|
||||
}
|
||||
|
||||
export interface PublishableEntityLinkSummary {
|
||||
upstreamContextKey: string;
|
||||
upstreamContextTitle: string;
|
||||
@@ -52,31 +55,18 @@ export interface PublishableEntityLinkSummary {
|
||||
export const getEntityLinks = async (
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
useTopLevelParents?: boolean,
|
||||
upstreamKey?: string,
|
||||
contentType?: 'all' | 'components' | 'containers',
|
||||
): Promise<PublishableEntityLink[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getEntityLinksByDownstreamContextUrl(), {
|
||||
params: {
|
||||
course_id: downstreamContextKey,
|
||||
ready_to_sync: readyToSync,
|
||||
upstream_usage_key: upstreamUsageKey,
|
||||
no_page: true,
|
||||
},
|
||||
});
|
||||
return camelCaseObject(data);
|
||||
};
|
||||
|
||||
export const getContainerEntityLinks = async (
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamContainerKey?: string,
|
||||
): Promise<ContainerPublishableEntityLink[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getContainerEntityLinksByDownstreamContextUrl(), {
|
||||
params: {
|
||||
course_id: downstreamContextKey,
|
||||
ready_to_sync: readyToSync,
|
||||
upstream_container_key: upstreamContainerKey,
|
||||
upstream_key: upstreamKey,
|
||||
use_top_level_parents: useTopLevelParents,
|
||||
item_type: contentType,
|
||||
no_page: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -39,6 +39,24 @@ describe('course libraries api hooks', () => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it('should return component links for course', async () => {
|
||||
const courseId = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl();
|
||||
axiosMock.onGet(url).reply(200, []);
|
||||
const { result } = renderHook(() => useEntityLinks({ courseId, contentType: 'components' }), { wrapper });
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBeFalsy();
|
||||
});
|
||||
expect(axiosMock.history.get[0].url).toEqual(url);
|
||||
expect(axiosMock.history.get[0].params).toEqual({
|
||||
course_id: courseId,
|
||||
ready_to_sync: undefined,
|
||||
upstream_key: undefined,
|
||||
no_page: true,
|
||||
item_type: 'components',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return links for course', async () => {
|
||||
const courseId = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl();
|
||||
@@ -53,6 +71,7 @@ describe('course libraries api hooks', () => {
|
||||
ready_to_sync: undefined,
|
||||
upstream_usage_key: undefined,
|
||||
no_page: true,
|
||||
content_type: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,61 +1,62 @@
|
||||
import {
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { getContainerEntityLinks, getEntityLinks, getEntityLinksSummaryByDownstreamContext } from './api';
|
||||
import { getEntityLinksSummaryByDownstreamContext, getEntityLinks } from './api';
|
||||
|
||||
export const courseLibrariesQueryKeys = {
|
||||
all: ['courseLibraries'],
|
||||
courseLibraries: (courseId?: string) => [...courseLibrariesQueryKeys.all, courseId],
|
||||
courseReadyToSyncLibraries: ({
|
||||
courseId, readyToSync, upstreamUsageKey, upstreamContainerKey,
|
||||
contentType, courseId, readyToSync, upstreamKey,
|
||||
}: {
|
||||
contentType?: 'all' | 'components' | 'containers',
|
||||
courseId?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
upstreamContainerKey?: string,
|
||||
upstreamKey?: string,
|
||||
pageSize?: number,
|
||||
}) => {
|
||||
const key: Array<string | boolean | number> = [...courseLibrariesQueryKeys.all];
|
||||
if (courseId !== undefined) {
|
||||
key.push(courseId);
|
||||
}
|
||||
if (contentType !== undefined) {
|
||||
key.push(contentType);
|
||||
}
|
||||
if (readyToSync !== undefined) {
|
||||
key.push(readyToSync);
|
||||
}
|
||||
if (upstreamUsageKey !== undefined) {
|
||||
key.push(upstreamUsageKey);
|
||||
}
|
||||
if (upstreamContainerKey !== undefined) {
|
||||
key.push(upstreamContainerKey);
|
||||
if (upstreamKey !== undefined) {
|
||||
key.push(upstreamKey);
|
||||
}
|
||||
return key;
|
||||
},
|
||||
courseLibrariesSummary: (courseId?: string) => [...courseLibrariesQueryKeys.courseLibraries(courseId), 'summary'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
courseId, readyToSync, useTopLevelParents, upstreamKey, contentType,
|
||||
}: {
|
||||
courseId?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
useTopLevelParents?: boolean,
|
||||
upstreamKey?: string,
|
||||
contentType?: 'all' | 'components' | 'containers',
|
||||
}) => (
|
||||
useQuery({
|
||||
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({
|
||||
contentType: contentType ?? 'all',
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
upstreamKey,
|
||||
}),
|
||||
queryFn: () => getEntityLinks(
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
useTopLevelParents,
|
||||
upstreamKey,
|
||||
contentType,
|
||||
),
|
||||
enabled: courseId !== undefined || upstreamUsageKey !== undefined || readyToSync !== undefined,
|
||||
enabled: courseId !== undefined || upstreamKey !== undefined || readyToSync !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -69,29 +70,3 @@ export const useEntityLinksSummaryByDownstreamContext = (courseId?: string) => (
|
||||
enabled: courseId !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Hook to fetch list of publishable entity links for containers by course key.
|
||||
* (That is, get a list of the library containers used in the given course.)
|
||||
*/
|
||||
export const useContainerEntityLinks = ({
|
||||
courseId, readyToSync, upstreamContainerKey,
|
||||
}: {
|
||||
courseId?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamContainerKey?: string,
|
||||
}) => (
|
||||
useQuery({
|
||||
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamContainerKey,
|
||||
}),
|
||||
queryFn: () => getContainerEntityLinks(
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamContainerKey,
|
||||
),
|
||||
enabled: courseId !== undefined || upstreamContainerKey !== undefined || readyToSync !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ const messages = defineMessages({
|
||||
breadcrumbLabel: {
|
||||
id: 'course-authoring.course-libraries.downstream-block.breadcrumb.label',
|
||||
defaultMessage: 'Location:',
|
||||
description: 'label for breadcrumb in component cards in course libraries page.',
|
||||
description: 'Label for breadcrumb in component cards in course libraries page.',
|
||||
},
|
||||
totalComponentLabel: {
|
||||
id: 'course-authoring.course-libraries.libcard.total-component.label',
|
||||
@@ -79,17 +79,17 @@ const messages = defineMessages({
|
||||
cardReviewContentBtn: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.review-btn-text',
|
||||
defaultMessage: 'Review Updates',
|
||||
description: 'Card review button for component in review tab',
|
||||
description: 'Card review button for component/container in review tab',
|
||||
},
|
||||
cardUpdateContentBtn: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.update-btn-text',
|
||||
defaultMessage: 'Update',
|
||||
description: 'Card update button for component in review tab',
|
||||
description: 'Card update button for component/container in review tab',
|
||||
},
|
||||
cardIgnoreContentBtn: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.ignore-btn-text',
|
||||
defaultMessage: 'Ignore',
|
||||
description: 'Card ignore button for component in review tab',
|
||||
description: 'Card ignore button for component/container in review tab',
|
||||
},
|
||||
updateSingleBlockSuccess: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.update-success-toast',
|
||||
@@ -116,6 +116,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Something went wrong! Could not fetch results.',
|
||||
description: 'Generic error message displayed when fetching link data fails.',
|
||||
},
|
||||
viewSectionInCourseLabel: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.view-section.label',
|
||||
defaultMessage: 'View Section in Course',
|
||||
description: 'Label of the button to see the section in the course',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -98,7 +98,7 @@ const UnitCard = ({
|
||||
downstreamBlockId: id,
|
||||
upstreamBlockId: upstreamInfo.upstreamRef,
|
||||
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
|
||||
isVertical: true,
|
||||
isContainer: true,
|
||||
};
|
||||
}, [upstreamInfo]);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const defaultEventData: LibraryChangesMessageData = {
|
||||
downstreamBlockId: usageKey,
|
||||
upstreamBlockId: 'lct:org:lib1:unit:1',
|
||||
upstreamBlockVersionSynced: 1,
|
||||
isVertical: false,
|
||||
isContainer: false,
|
||||
};
|
||||
|
||||
const mockSendMessageToIframe = jest.fn();
|
||||
@@ -66,9 +66,9 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
});
|
||||
|
||||
it('renders default displayName for units with no displayName', async () => {
|
||||
render({ ...defaultEventData, isVertical: true, displayName: '' });
|
||||
render({ ...defaultEventData, isContainer: true, displayName: '' });
|
||||
|
||||
expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Preview changes: Container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default displayName for components with no displayName', async () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface LibraryChangesMessageData {
|
||||
downstreamBlockId: string,
|
||||
upstreamBlockId: string,
|
||||
upstreamBlockVersionSynced: number,
|
||||
isVertical: boolean,
|
||||
isContainer: boolean,
|
||||
}
|
||||
|
||||
export interface PreviewLibraryXBlockChangesProps {
|
||||
@@ -60,7 +60,7 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
usageKey={blockData.upstreamBlockId}
|
||||
oldVersion={blockData.upstreamBlockVersionSynced || 'published'}
|
||||
newVersion="published"
|
||||
isContainer={blockData.isVertical}
|
||||
isContainer={blockData.isContainer}
|
||||
/>
|
||||
);
|
||||
}, [blockData]);
|
||||
@@ -85,8 +85,8 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
}, [blockData]);
|
||||
|
||||
const defaultTitle = intl.formatMessage(
|
||||
blockData.isVertical
|
||||
? messages.defaultUnitTitle
|
||||
blockData.isContainer
|
||||
? messages.defaultContainerTitle
|
||||
: messages.defaultComponentTitle,
|
||||
);
|
||||
const title = blockData.displayName
|
||||
|
||||
@@ -6,10 +6,10 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Preview changes: {blockTitle}',
|
||||
description: 'Preview changes modal title text',
|
||||
},
|
||||
defaultUnitTitle: {
|
||||
defaultContainerTitle: {
|
||||
id: 'authoring.course-unit.preview-changes.modal-default-unit-title',
|
||||
defaultMessage: 'Preview changes: Unit',
|
||||
description: 'Preview changes modal default title text for units',
|
||||
defaultMessage: 'Preview changes: Container',
|
||||
description: 'Preview changes modal default title text for containers',
|
||||
},
|
||||
defaultComponentTitle: {
|
||||
id: 'authoring.course-unit.preview-changes.modal-default-component-title',
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
render as baseRender,
|
||||
screen,
|
||||
fireEvent,
|
||||
} from '../../testUtils';
|
||||
import { mockFetchIndexDocuments, mockContentSearchConfig } from '../../search-manager/data/api.mock';
|
||||
} from '@src/testUtils';
|
||||
import { mockFetchIndexDocuments, mockContentSearchConfig } from '@src/search-manager/data/api.mock';
|
||||
|
||||
import {
|
||||
mockContentLibrary,
|
||||
mockGetEntityLinks,
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '../../testUtils';
|
||||
} from '@src/testUtils';
|
||||
import { mockContentSearchConfig, mockFetchIndexDocuments } from '@src/search-manager/data/api.mock';
|
||||
|
||||
import {
|
||||
mockContentLibrary,
|
||||
mockLibraryBlockMetadata,
|
||||
mockGetEntityLinks,
|
||||
} from '../data/api.mocks';
|
||||
import { mockContentSearchConfig, mockFetchIndexDocuments } from '../../search-manager/data/api.mock';
|
||||
import { LibraryProvider } from '../common/context/LibraryContext';
|
||||
import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext';
|
||||
import ComponentInfo from './ComponentInfo';
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 { useEntityLinks } from '../../course-libraries/data/apiHooks';
|
||||
|
||||
import AlertError from '../../generic/alert-error';
|
||||
import Loading from '../../generic/Loading';
|
||||
import { useEntityLinks } from '@src/course-libraries/data/apiHooks';
|
||||
import AlertError from '@src/generic/alert-error';
|
||||
import Loading from '@src/generic/Loading';
|
||||
|
||||
import messages from './messages';
|
||||
import { useContentFromSearchIndex } from '../data/apiHooks';
|
||||
|
||||
@@ -34,7 +36,7 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => {
|
||||
isError: isErrorDownstreamLinks,
|
||||
error: errorDownstreamLinks,
|
||||
isLoading: isLoadingDownstreamLinks,
|
||||
} = useEntityLinks({ upstreamUsageKey: usageKey });
|
||||
} = useEntityLinks({ upstreamKey: usageKey, contentType: 'components' });
|
||||
|
||||
const downstreamKeys = useMemo(
|
||||
() => dataDownstreamLinks?.map(link => link.downstreamUsageKey) || [],
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
|
||||
import BaseModal from '../../editors/sharedComponents/BaseModal';
|
||||
import BaseModal from '@src/editors/sharedComponents/BaseModal';
|
||||
import { useEntityLinks } from '@src/course-libraries/data/apiHooks';
|
||||
|
||||
import messages from './messages';
|
||||
import infoMessages from '../component-info/messages';
|
||||
import { ComponentUsage } from '../component-info/ComponentUsage';
|
||||
import { useEntityLinks } from '../../course-libraries/data/apiHooks';
|
||||
|
||||
interface PublishConfirmationModalProps {
|
||||
isOpen: boolean,
|
||||
@@ -29,7 +30,7 @@ const PublishConfirmationModal = ({
|
||||
const {
|
||||
data: dataDownstreamLinks,
|
||||
isLoading: isLoadingDownstreamLinks,
|
||||
} = useEntityLinks({ upstreamUsageKey: usageKey });
|
||||
} = useEntityLinks({ upstreamKey: usageKey, contentType: 'components' });
|
||||
|
||||
const hasDownstreamUsages = !isLoadingDownstreamLinks && dataDownstreamLinks?.length !== 0;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { ToastActionData } from '../../generic/toast-context';
|
||||
import type { ToastActionData } from '@src/generic/toast-context';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
initializeMocks,
|
||||
waitFor,
|
||||
} from '../../testUtils';
|
||||
} from '@src/testUtils';
|
||||
import { mockContentSearchConfig, mockSearchResult } from '@src/search-manager/data/api.mock';
|
||||
|
||||
import { LibraryProvider } from '../common/context/LibraryContext';
|
||||
import { SidebarProvider } from '../common/context/SidebarContext';
|
||||
import {
|
||||
@@ -13,14 +15,13 @@ import {
|
||||
mockGetContainerMetadata,
|
||||
mockDeleteContainer,
|
||||
mockRestoreContainer,
|
||||
mockGetContainerEntityLinks,
|
||||
mockGetEntityLinks,
|
||||
} from '../data/api.mocks';
|
||||
import { mockContentSearchConfig, mockSearchResult } from '../../search-manager/data/api.mock';
|
||||
import ContainerDeleter from './ContainerDeleter';
|
||||
|
||||
mockContentLibrary.applyMock(); // Not required, but avoids 404 errors in the logs when <LibraryProvider> loads data
|
||||
mockContentSearchConfig.applyMock();
|
||||
mockGetContainerEntityLinks.applyMock();
|
||||
mockGetEntityLinks.applyMock();
|
||||
const mockDelete = mockDeleteContainer.applyMock();
|
||||
const mockRestore = mockRestoreContainer.applyMock();
|
||||
|
||||
|
||||
@@ -3,15 +3,16 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { Error, Warning, School } from '@openedx/paragon/icons';
|
||||
|
||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||
import DeleteModal from '@src/generic/delete-modal/DeleteModal';
|
||||
import { ToastContext } from '@src/generic/toast-context';
|
||||
import { ContainerType } from '@src/generic/key-utils';
|
||||
import { ContainerHit } from '@src/search-manager';
|
||||
import { useEntityLinks } from '@src/course-libraries/data/apiHooks';
|
||||
import { LoadingSpinner } from '@src/generic/Loading';
|
||||
|
||||
import { useSidebarContext } from '../common/context/SidebarContext';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { useContentFromSearchIndex, useDeleteContainer, useRestoreContainer } from '../data/apiHooks';
|
||||
import messages from './messages';
|
||||
import { ContainerType } from '../../generic/key-utils';
|
||||
import { ContainerHit } from '../../search-manager';
|
||||
import { useContainerEntityLinks } from '../../course-libraries/data/apiHooks';
|
||||
import { LoadingSpinner } from '../../generic/Loading';
|
||||
|
||||
type ContainerDeleterProps = {
|
||||
isOpen: boolean,
|
||||
@@ -37,7 +38,7 @@ const ContainerDeleter = ({
|
||||
const {
|
||||
data: dataDownstreamLinks,
|
||||
isLoading: linksIsLoading,
|
||||
} = useContainerEntityLinks({ upstreamContainerKey: containerId });
|
||||
} = useEntityLinks({ upstreamKey: containerId, contentType: 'containers' });
|
||||
const downstreamCount = dataDownstreamLinks?.length ?? 0;
|
||||
|
||||
const messageMap = useMemo(() => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/* istanbul ignore file */
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { mockContentTaxonomyTagsData } from '../../content-tags-drawer/data/api.mocks';
|
||||
import { ContainerType, getBlockType } from '../../generic/key-utils';
|
||||
import { createAxiosError } from '../../testUtils';
|
||||
|
||||
import { mockContentTaxonomyTagsData } from '@src/content-tags-drawer/data/api.mocks';
|
||||
import { ContainerType, getBlockType } from '@src/generic/key-utils';
|
||||
import { createAxiosError } from '@src/testUtils';
|
||||
import downstreamLinkInfo from '@src/search-manager/data/__mocks__/downstream-links.json';
|
||||
import * as courseLibApi from '@src/course-libraries/data/api';
|
||||
|
||||
import contentLibrariesListV2 from '../__mocks__/contentLibrariesListV2';
|
||||
import downstreamLinkInfo from '../../search-manager/data/__mocks__/downstream-links.json';
|
||||
import * as api from './api';
|
||||
import * as courseLibApi from '../../course-libraries/data/api';
|
||||
|
||||
/**
|
||||
* Mock for `getContentLibraryV2List()`
|
||||
@@ -769,23 +771,36 @@ mockBlockTypesMetadata.applyMock = () => jest.spyOn(api, 'getBlockTypes').mockIm
|
||||
export async function mockGetEntityLinks(
|
||||
_downstreamContextKey?: string,
|
||||
_readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
_useTopLevelParents?: boolean,
|
||||
upstreamKey?: string,
|
||||
contentType?: 'all' | 'components' | 'containers',
|
||||
): ReturnType<typeof courseLibApi.getEntityLinks> {
|
||||
const thisMock = mockGetEntityLinks;
|
||||
switch (upstreamUsageKey) {
|
||||
case thisMock.upstreamContainerKey: return thisMock.response;
|
||||
case mockLibraryBlockMetadata.usageKeyPublishedWithChanges: return thisMock.response;
|
||||
case thisMock.emptyUsageKey: return thisMock.emptyComponentUsage;
|
||||
default: return [];
|
||||
if (contentType === 'components') {
|
||||
switch (upstreamKey) {
|
||||
case thisMock.upstreamContainerKey: return thisMock.componentResponse;
|
||||
case mockLibraryBlockMetadata.usageKeyPublishedWithChanges: return thisMock.componentResponse;
|
||||
case thisMock.emptyUsageKey: return thisMock.emptyComponentUsage;
|
||||
default: return [];
|
||||
}
|
||||
} else if (contentType === 'containers') {
|
||||
switch (upstreamKey) {
|
||||
case thisMock.unitKey: return thisMock.unitResponse;
|
||||
case thisMock.subsectionKey: return thisMock.subsectionResponse;
|
||||
case thisMock.sectionKey: return thisMock.sectionResponse;
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
return thisMock.allResponse;
|
||||
}
|
||||
|
||||
mockGetEntityLinks.upstreamContainerKey = mockLibraryBlockMetadata.usageKeyPublished;
|
||||
mockGetEntityLinks.response = downstreamLinkInfo.results[0].hits.map((obj: { usageKey: any; }) => ({
|
||||
mockGetEntityLinks.componentResponse = downstreamLinkInfo.results[0].hits.map((obj: { usageKey: any; }) => ({
|
||||
id: 875,
|
||||
upstreamContextTitle: 'CS problems 3',
|
||||
upstreamVersion: 10,
|
||||
readyToSync: true,
|
||||
upstreamUsageKey: mockLibraryBlockMetadata.usageKeyPublished,
|
||||
upstreamKey: mockLibraryBlockMetadata.usageKeyPublished,
|
||||
upstreamContextKey: 'lib:Axim:TEST2',
|
||||
downstreamUsageKey: obj.usageKey,
|
||||
downstreamContextKey: 'course-v1:OpenEdx+DemoX+CourseX',
|
||||
@@ -793,36 +808,18 @@ mockGetEntityLinks.response = downstreamLinkInfo.results[0].hits.map((obj: { usa
|
||||
versionDeclined: null,
|
||||
created: '2025-02-08T14:07:05.588484Z',
|
||||
updated: '2025-02-08T14:07:05.588484Z',
|
||||
upstreamType: 'component',
|
||||
}));
|
||||
mockGetEntityLinks.emptyUsageKey = 'lb:Axim:TEST1:html:empty';
|
||||
mockGetEntityLinks.emptyComponentUsage = [] as courseLibApi.PublishableEntityLink[];
|
||||
|
||||
mockGetEntityLinks.applyMock = () => jest.spyOn(
|
||||
courseLibApi,
|
||||
'getEntityLinks',
|
||||
).mockImplementation(mockGetEntityLinks);
|
||||
|
||||
export async function mockGetContainerEntityLinks(
|
||||
_downstreamContextKey?: string,
|
||||
_readyToSync?: boolean,
|
||||
upstreamContainerKey?: string,
|
||||
): ReturnType<typeof courseLibApi.getContainerEntityLinks> {
|
||||
const thisMock = mockGetContainerEntityLinks;
|
||||
switch (upstreamContainerKey) {
|
||||
case thisMock.unitKey: return thisMock.unitResponse;
|
||||
case thisMock.subsectionKey: return thisMock.subsectionResponse;
|
||||
case thisMock.sectionKey: return thisMock.sectionResponse;
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
mockGetContainerEntityLinks.unitKey = mockGetContainerMetadata.unitId;
|
||||
mockGetContainerEntityLinks.unitResponse = [
|
||||
mockGetEntityLinks.unitKey = mockGetContainerMetadata.unitId;
|
||||
mockGetEntityLinks.unitResponse = [
|
||||
{
|
||||
id: 1,
|
||||
upstreamContextTitle: 'CS problems 3',
|
||||
upstreamVersion: 1,
|
||||
readyToSync: false,
|
||||
upstreamContainerKey: mockGetContainerEntityLinks.unitKey,
|
||||
upstreamKey: mockGetEntityLinks.unitKey,
|
||||
upstreamContextKey: 'lib:Axim:TEST2',
|
||||
downstreamUsageKey: 'some-key',
|
||||
downstreamContextKey: 'course-v1:OpenEdx+DemoX+CourseX',
|
||||
@@ -830,13 +827,14 @@ mockGetContainerEntityLinks.unitResponse = [
|
||||
versionDeclined: null,
|
||||
created: '2025-02-08T14:07:05.588484Z',
|
||||
updated: '2025-02-08T14:07:05.588484Z',
|
||||
upstreamType: 'container',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
upstreamContextTitle: 'CS problems 3',
|
||||
upstreamVersion: 1,
|
||||
readyToSync: false,
|
||||
upstreamContainerKey: mockGetContainerEntityLinks.unitKey,
|
||||
upstreamKey: mockGetEntityLinks.unitKey,
|
||||
upstreamContextKey: 'lib:Axim:TEST2',
|
||||
downstreamUsageKey: 'some-key-1',
|
||||
downstreamContextKey: 'course-v1:OpenEdx+DemoX+CourseX',
|
||||
@@ -844,16 +842,17 @@ mockGetContainerEntityLinks.unitResponse = [
|
||||
versionDeclined: null,
|
||||
created: '2025-02-08T14:07:05.588484Z',
|
||||
updated: '2025-02-08T14:07:05.588484Z',
|
||||
upstreamType: 'container',
|
||||
},
|
||||
];
|
||||
mockGetContainerEntityLinks.subsectionKey = mockGetContainerMetadata.subsectionId;
|
||||
mockGetContainerEntityLinks.subsectionResponse = [
|
||||
] as courseLibApi.PublishableEntityLink[];
|
||||
mockGetEntityLinks.subsectionKey = mockGetContainerMetadata.subsectionId;
|
||||
mockGetEntityLinks.subsectionResponse = [
|
||||
{
|
||||
id: 1,
|
||||
upstreamContextTitle: 'CS problems 3',
|
||||
upstreamVersion: 1,
|
||||
readyToSync: false,
|
||||
upstreamContainerKey: mockGetContainerEntityLinks.subsectionKey,
|
||||
upstreamKey: mockGetEntityLinks.subsectionKey,
|
||||
upstreamContextKey: 'lib:Axim:TEST2',
|
||||
downstreamUsageKey: 'some-subsection-key',
|
||||
downstreamContextKey: 'course-v1:OpenEdx+DemoX+CourseX',
|
||||
@@ -861,13 +860,14 @@ mockGetContainerEntityLinks.subsectionResponse = [
|
||||
versionDeclined: null,
|
||||
created: '2025-02-08T14:07:05.588484Z',
|
||||
updated: '2025-02-08T14:07:05.588484Z',
|
||||
upstreamType: 'container',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
upstreamContextTitle: 'CS problems 3',
|
||||
upstreamVersion: 1,
|
||||
readyToSync: false,
|
||||
upstreamContainerKey: mockGetContainerEntityLinks.subsectionKey,
|
||||
upstreamKey: mockGetEntityLinks.subsectionKey,
|
||||
upstreamContextKey: 'lib:Axim:TEST2',
|
||||
downstreamUsageKey: 'some-subsection-key-1',
|
||||
downstreamContextKey: 'course-v1:OpenEdx+DemoX+CourseX',
|
||||
@@ -875,16 +875,17 @@ mockGetContainerEntityLinks.subsectionResponse = [
|
||||
versionDeclined: null,
|
||||
created: '2025-02-08T14:07:05.588484Z',
|
||||
updated: '2025-02-08T14:07:05.588484Z',
|
||||
upstreamType: 'container',
|
||||
},
|
||||
];
|
||||
mockGetContainerEntityLinks.sectionKey = mockGetContainerMetadata.sectionId;
|
||||
mockGetContainerEntityLinks.sectionResponse = [
|
||||
] as courseLibApi.PublishableEntityLink[];
|
||||
mockGetEntityLinks.sectionKey = mockGetContainerMetadata.sectionId;
|
||||
mockGetEntityLinks.sectionResponse = [
|
||||
{
|
||||
id: 1,
|
||||
upstreamContextTitle: 'CS problems 3',
|
||||
upstreamVersion: 1,
|
||||
readyToSync: false,
|
||||
upstreamContainerKey: mockGetContainerEntityLinks.sectionKey,
|
||||
upstreamKey: mockGetEntityLinks.sectionKey,
|
||||
upstreamContextKey: 'lib:Axim:TEST2',
|
||||
downstreamUsageKey: 'some-section-key',
|
||||
downstreamContextKey: 'course-v1:OpenEdx+DemoX+CourseX',
|
||||
@@ -892,13 +893,14 @@ mockGetContainerEntityLinks.sectionResponse = [
|
||||
versionDeclined: null,
|
||||
created: '2025-02-08T14:07:05.588484Z',
|
||||
updated: '2025-02-08T14:07:05.588484Z',
|
||||
upstreamType: 'container',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
upstreamContextTitle: 'CS problems 3',
|
||||
upstreamVersion: 1,
|
||||
readyToSync: false,
|
||||
upstreamContainerKey: mockGetContainerEntityLinks.sectionKey,
|
||||
upstreamKey: mockGetEntityLinks.sectionKey,
|
||||
upstreamContextKey: 'lib:Axim:TEST2',
|
||||
downstreamUsageKey: 'some-section-key-1',
|
||||
downstreamContextKey: 'course-v1:OpenEdx+DemoX+CourseX',
|
||||
@@ -906,10 +908,16 @@ mockGetContainerEntityLinks.sectionResponse = [
|
||||
versionDeclined: null,
|
||||
created: '2025-02-08T14:07:05.588484Z',
|
||||
updated: '2025-02-08T14:07:05.588484Z',
|
||||
upstreamType: 'container',
|
||||
},
|
||||
] as courseLibApi.PublishableEntityLink[];
|
||||
mockGetEntityLinks.allResponse = [
|
||||
...mockGetEntityLinks.componentResponse,
|
||||
...mockGetEntityLinks.unitResponse,
|
||||
...mockGetEntityLinks.subsectionResponse,
|
||||
...mockGetEntityLinks.sectionResponse,
|
||||
];
|
||||
|
||||
mockGetContainerEntityLinks.applyMock = () => jest.spyOn(
|
||||
mockGetEntityLinks.applyMock = () => jest.spyOn(
|
||||
courseLibApi,
|
||||
'getContainerEntityLinks',
|
||||
).mockImplementation(mockGetContainerEntityLinks);
|
||||
'getEntityLinks',
|
||||
).mockImplementation(mockGetEntityLinks);
|
||||
|
||||
Reference in New Issue
Block a user