diff --git a/src/course-libraries/CourseLibraries.tsx b/src/course-libraries/CourseLibraries.tsx
index 7ab6a1bdf..4021303df 100644
--- a/src/course-libraries/CourseLibraries.tsx
+++ b/src/course-libraries/CourseLibraries.tsx
@@ -23,16 +23,17 @@ import {
import sumBy from 'lodash/sumBy';
import { useSearchParams } from 'react-router-dom';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
-import getPageHeadTitle from '../generic/utils';
+import { useStudioHome } from '@src/studio-home/hooks';
+import NewsstandIcon from '@src/generic/NewsstandIcon';
+import getPageHeadTitle from '@src/generic/utils';
+import SubHeader from '@src/generic/sub-header/SubHeader';
+import Loading from '@src/generic/Loading';
import messages from './messages';
-import SubHeader from '../generic/sub-header/SubHeader';
import { useEntityLinksSummaryByDownstreamContext } from './data/apiHooks';
import type { PublishableEntityLinkSummary } from './data/api';
-import Loading from '../generic/Loading';
-import { useStudioHome } from '../studio-home/hooks';
-import NewsstandIcon from '../generic/NewsstandIcon';
import ReviewTabContent from './ReviewTabContent';
import { OutOfSyncAlert } from './OutOfSyncAlert';
+import LegacyLibContentBlockAlert from './LegacyLibContentBlockAlert';
interface LibraryCardProps {
linkSummary: PublishableEntityLinkSummary;
@@ -233,6 +234,7 @@ export const CourseLibraries = () => {
notification={outOfSyncCount}
className="px-2 mt-3"
>
+
{renderReviewTabContent()}
diff --git a/src/course-libraries/LegacyLibContentBlockAlert.test.tsx b/src/course-libraries/LegacyLibContentBlockAlert.test.tsx
new file mode 100644
index 000000000..661546552
--- /dev/null
+++ b/src/course-libraries/LegacyLibContentBlockAlert.test.tsx
@@ -0,0 +1,83 @@
+import { initializeMocks, render, screen } from '@src/testUtils';
+import { userEvent } from '@testing-library/user-event';
+import { waitFor } from '@testing-library/react';
+import LegacyLibContentBlockAlert from './LegacyLibContentBlockAlert';
+import * as apiMocks from './data/api.mocks';
+
+const renderComponent = (courseId: string) => {
+ render(
+ ,
+ );
+};
+
+apiMocks.mockGetReadyToUpdateReferences.applyMock();
+apiMocks.mockMigrateCourseReadyToMigrateLegacyLibContentBlocks.applyMock();
+apiMocks.mockGetCourseLegacyLibRefUpdateTaskStatus.applyMock();
+let mockShowToast;
+
+describe('LegacyLibContentBlockAlert', () => {
+ beforeEach(() => {
+ const mocks = initializeMocks();
+ mockShowToast = mocks.mockShowToast;
+ });
+
+ test('alert is not rendered when data is loading', async () => {
+ renderComponent(apiMocks.mockGetReadyToUpdateReferences.courseKeyLoading);
+ expect(await screen.findByTestId('redux-provider')).toBeEmptyDOMElement();
+ });
+
+ test('alert is not rendered when data is empty', async () => {
+ renderComponent(apiMocks.mockGetReadyToUpdateReferences.courseKeyEmpty);
+ expect(await screen.findByTestId('redux-provider')).toBeEmptyDOMElement();
+ });
+
+ test('alert is rendered when 1 block is present', async () => {
+ renderComponent(apiMocks.mockGetReadyToUpdateReferences.courseKeyWith1Block);
+ expect(await screen.findByText('This course contains 1 legacy library reference')).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: 'Update library references' })).toBeInTheDocument();
+ expect(await screen.findByRole('link', { name: 'Learn more' })).toBeInTheDocument();
+ });
+
+ test('alert is rendered when 2 blocks are present', async () => {
+ renderComponent(apiMocks.mockGetReadyToUpdateReferences.courseKeyWith2Blocks);
+ expect(await screen.findByText('This course contains 2 legacy library references')).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: 'Update library references' })).toBeInTheDocument();
+ expect(await screen.findByRole('link', { name: 'Learn more' })).toBeInTheDocument();
+ });
+
+ test('migrates all blocks successfully', async () => {
+ const user = userEvent.setup();
+ renderComponent(apiMocks.mockGetReadyToUpdateReferences.courseKeyWith1Block);
+ expect(await screen.findByText('This course contains 1 legacy library reference')).toBeInTheDocument();
+ const actionBtn = await screen.findByRole('button', { name: 'Update library references' });
+ expect(actionBtn).toBeInTheDocument();
+ await user.click(actionBtn);
+
+ // mockShowToast will have been called
+ await waitFor(() => {
+ expect(mockShowToast).toHaveBeenCalledWith('Updating library references...');
+ });
+ // mockShowToast will have been called
+ await waitFor(() => {
+ expect(mockShowToast).toHaveBeenCalledWith('Successfully updated all legacy library references');
+ });
+ });
+
+ test('migrates all blocks: failed', async () => {
+ const user = userEvent.setup();
+ renderComponent(apiMocks.mockGetReadyToUpdateReferences.courseKeyWith3Blocks);
+ expect(await screen.findByText('This course contains 3 legacy library references')).toBeInTheDocument();
+ const actionBtn = await screen.findByRole('button', { name: 'Update library references' });
+ expect(actionBtn).toBeInTheDocument();
+ await user.click(actionBtn);
+
+ // mockShowToast will have been called
+ await waitFor(() => {
+ expect(mockShowToast).toHaveBeenCalledWith('Updating library references...');
+ });
+ // mockShowToast will have been called
+ await waitFor(() => {
+ expect(mockShowToast).toHaveBeenCalledWith('Failed to update legacy library references');
+ });
+ });
+});
diff --git a/src/course-libraries/LegacyLibContentBlockAlert.tsx b/src/course-libraries/LegacyLibContentBlockAlert.tsx
new file mode 100644
index 000000000..a5092ff0a
--- /dev/null
+++ b/src/course-libraries/LegacyLibContentBlockAlert.tsx
@@ -0,0 +1,87 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Button, Hyperlink } from '@openedx/paragon';
+import {
+ useContext, useEffect, useMemo, useState,
+} from 'react';
+import { UserTaskStatus } from '@src/data/constants';
+import AlertMessage from '@src/generic/alert-message';
+import LoadingButton from '@src/generic/loading-button';
+import { ToastContext } from '@src/generic/toast-context';
+import {
+ useCheckMigrateCourseLegacyLibReadyToMigrateBlocksOptions,
+ useCourseLegacyLibReadyToMigrateBlocks,
+ useMigrateCourseLegacyLibReadyToMigrateBlocks,
+} from './data/apiHooks';
+import messages from './messages';
+
+interface Props {
+ courseId: string,
+}
+
+const LegacyLibContentBlockAlert = ({ courseId }: Props) => {
+ const intl = useIntl();
+ const { showToast } = useContext(ToastContext);
+ const [taskId, setTaskId] = useState(undefined);
+ const { data, isPending, refetch } = useCourseLegacyLibReadyToMigrateBlocks(courseId);
+ const { mutateAsync } = useMigrateCourseLegacyLibReadyToMigrateBlocks(courseId);
+ const taskStatus = useCheckMigrateCourseLegacyLibReadyToMigrateBlocksOptions(courseId, taskId);
+ const learnMoreUrl = 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/migrate_legacy_libraries.html#id8';
+
+ useEffect(() => {
+ if (taskStatus.data?.state === UserTaskStatus.Succeeded) {
+ showToast(intl.formatMessage(messages.legacyLibReadyToMigrateTaskCompleted));
+ setTaskId(undefined);
+ refetch();
+ } else if (taskStatus.data?.state === UserTaskStatus.Failed
+ || taskStatus.data?.state === UserTaskStatus.Cancelled) {
+ showToast(intl.formatMessage(messages.legacyLibReadyToMigrateTaskFailed));
+ setTaskId(undefined);
+ refetch();
+ } else if (taskId) {
+ showToast(intl.formatMessage(messages.legacyLibReadyToMigrateTaskInProgress));
+ }
+ }, [taskStatus, taskId, refetch]);
+
+ const migrateFn = async () => {
+ await mutateAsync(undefined, {
+ onSuccess: (result) => {
+ setTaskId(result.uuid);
+ },
+ onError: () => {
+ setTaskId(undefined);
+ },
+ });
+ };
+
+ const alertCount = useMemo(() => data?.length || 0, [data]);
+
+ if (isPending || taskId) {
+ return null;
+ }
+
+ return (
+ 0}
+ variant="info"
+ actions={[
+ ,
+ ,
+ ]}
+ />
+ );
+};
+
+export default LegacyLibContentBlockAlert;
diff --git a/src/course-libraries/data/api.mocks.ts b/src/course-libraries/data/api.mocks.ts
index b4d2fc9a1..588886d10 100644
--- a/src/course-libraries/data/api.mocks.ts
+++ b/src/course-libraries/data/api.mocks.ts
@@ -4,6 +4,7 @@ import fetchMock from 'fetch-mock-jest';
import * as libApi from '@src/library-authoring/data/api';
import { createAxiosError } from '@src/testUtils';
+import { UserTaskStatus } from '@src/data/constants';
import mockLinksResult from '../__mocks__/publishableEntityLinks.json';
import mockSummaryResult from '../__mocks__/linkCourseSummary.json';
import mockLinkDetailsFromIndex from '../__mocks__/linkDetailsFromIndex.json';
@@ -105,3 +106,106 @@ export async function mockUseLibBlockMetadata() {
mockUseLibBlockMetadata.applyMock = () => {
jest.spyOn(libApi, 'getLibraryBlockMetadata').mockImplementation(mockUseLibBlockMetadata);
};
+
+/**
+ * Mock getCourseReadyToMigrateLegacyLibContentBlocks
+*/
+export async function mockGetReadyToUpdateReferences(
+ courseId?: string,
+): ReturnType {
+ switch (courseId) {
+ case mockGetReadyToUpdateReferences.courseKeyLoading:
+ return new Promise(() => {});
+ case mockGetReadyToUpdateReferences.courseKeyEmpty:
+ return Promise.resolve([]);
+ case mockGetReadyToUpdateReferences.courseKeyWith2Blocks:
+ return Promise.resolve([{ usageKey: 'some-key-1' }, { usageKey: 'some-key-2' }]);
+ case mockGetReadyToUpdateReferences.courseKeyWith3Blocks:
+ return Promise.resolve([{ usageKey: 'some-key-1' }, { usageKey: 'some-key-2' }, { usageKey: 'some-key-3' }]);
+ case mockGetReadyToUpdateReferences.courseKeyWith1Block:
+ return Promise.resolve([{ usageKey: 'some-key-1' }]);
+ default:
+ throw Error();
+ }
+}
+mockGetReadyToUpdateReferences.courseKeyLoading = 'course-v1:loading+1+1';
+mockGetReadyToUpdateReferences.courseKeyEmpty = 'course-v1:empty+1+1';
+mockGetReadyToUpdateReferences.courseKeyWith2Blocks = 'course-v1:normal+2+2';
+mockGetReadyToUpdateReferences.courseKeyWith1Block = 'course-v1:normal+1+1';
+mockGetReadyToUpdateReferences.courseKeyWith3Blocks = 'course-v1:normal+3+3';
+mockGetReadyToUpdateReferences.applyMock = () => {
+ jest.spyOn(api, 'getCourseReadyToMigrateLegacyLibContentBlocks').mockImplementation(mockGetReadyToUpdateReferences);
+};
+
+/**
+ * Mock getCourseLegacyLibRefUpdateTaskStatus
+*/
+export async function mockGetCourseLegacyLibRefUpdateTaskStatus(
+ _courseId?: string,
+ taskId?: string,
+): ReturnType {
+ switch (taskId) {
+ case mockGetCourseLegacyLibRefUpdateTaskStatus.taskInProgress:
+ return Promise.resolve({
+ state: UserTaskStatus.InProgress,
+ stateText: 'In Progress',
+ uuid: 'task-pending',
+ } as unknown as ReturnType);
+ case mockGetCourseLegacyLibRefUpdateTaskStatus.taskComplete:
+ return Promise.resolve({
+ state: UserTaskStatus.Succeeded,
+ stateText: 'Succeeded',
+ uuid: 'task-complete',
+ } as unknown as ReturnType);
+ case mockGetCourseLegacyLibRefUpdateTaskStatus.taskFailed:
+ return Promise.resolve({
+ state: UserTaskStatus.Failed,
+ stateText: 'Failed',
+ uuid: 'task-failed',
+ } as unknown as ReturnType);
+ default:
+ throw Error();
+ }
+}
+mockGetCourseLegacyLibRefUpdateTaskStatus.taskInProgress = 'task-pending';
+mockGetCourseLegacyLibRefUpdateTaskStatus.taskComplete = 'task-complete';
+mockGetCourseLegacyLibRefUpdateTaskStatus.taskFailed = 'task-failed';
+mockGetCourseLegacyLibRefUpdateTaskStatus.applyMock = () => {
+ jest.spyOn(api, 'getCourseLegacyLibRefUpdateTaskStatus').mockImplementation(mockGetCourseLegacyLibRefUpdateTaskStatus);
+};
+
+/**
+ * Mock getCourseReadyToMigrateLegacyLibContentBlocks
+*/
+export async function mockMigrateCourseReadyToMigrateLegacyLibContentBlocks(
+ courseId?: string,
+): ReturnType {
+ switch (courseId) {
+ case mockGetReadyToUpdateReferences.courseKeyWith2Blocks:
+ return Promise.resolve({
+ state: UserTaskStatus.InProgress,
+ stateText: 'In Progress',
+ uuid: 'task-pending',
+ } as unknown as ReturnType);
+ case mockGetReadyToUpdateReferences.courseKeyWith1Block:
+ return Promise.resolve({
+ state: UserTaskStatus.InProgress,
+ stateText: 'In Progress',
+ uuid: 'task-complete',
+ } as unknown as ReturnType);
+ case mockGetReadyToUpdateReferences.courseKeyWith3Blocks:
+ return Promise.resolve({
+ state: UserTaskStatus.InProgress,
+ stateText: 'In Progress',
+ uuid: 'task-failed',
+ } as unknown as ReturnType);
+ default:
+ throw Error();
+ }
+}
+mockMigrateCourseReadyToMigrateLegacyLibContentBlocks.applyMock = () => {
+ jest.spyOn(
+ api,
+ 'migrateCourseReadyToMigrateLegacyLibContentBlocks',
+ ).mockImplementation(mockMigrateCourseReadyToMigrateLegacyLibContentBlocks);
+};
diff --git a/src/course-libraries/data/api.ts b/src/course-libraries/data/api.ts
index 03b01b2c8..5806ec47e 100644
--- a/src/course-libraries/data/api.ts
+++ b/src/course-libraries/data/api.ts
@@ -1,10 +1,13 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import type { UsageKeyBlock, UserTaskStatusWithUuid } from '@src/data/types';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/`;
export const getEntityLinksSummaryByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/${downstreamContextKey}/summary`;
+export const courseLegacyLibraryContentBlocks = (courseId: string) => `${getApiBaseUrl()}/api/courses/v1/migrate_legacy_content_blocks/${courseId}/`;
+export const courseLegacyLibraryContentTaskStatus = (courseId: string, taskId: string) => `${courseLegacyLibraryContentBlocks(courseId)}${taskId}/`;
export interface PaginatedData {
next: string | null;
@@ -81,3 +84,39 @@ export const getEntityLinksSummaryByDownstreamContext = async (
.get(getEntityLinksSummaryByDownstreamContextUrl(downstreamContextKey));
return camelCaseObject(data);
};
+
+/**
+ * Get all legacy library blocks that ready to migrate to library v2 item bank in given course
+ */
+export async function getCourseReadyToMigrateLegacyLibContentBlocks(courseId: string): Promise {
+ const { data } = await getAuthenticatedHttpClient()
+ .get(courseLegacyLibraryContentBlocks(courseId));
+
+ return camelCaseObject(data);
+}
+
+// istanbul ignore next
+/**
+ * Migrate legacy library blocks that ready to migrate to library v2 item bank in given course
+ */
+export async function migrateCourseReadyToMigrateLegacyLibContentBlocks(
+ courseId: string,
+): Promise {
+ const { data } = await getAuthenticatedHttpClient()
+ .post(courseLegacyLibraryContentBlocks(courseId));
+
+ return camelCaseObject(data);
+}
+
+/**
+ * Get task status of legacy library blocks reference update task.
+ */
+export async function getCourseLegacyLibRefUpdateTaskStatus(
+ courseId: string,
+ taskId: string,
+): Promise {
+ const { data } = await getAuthenticatedHttpClient()
+ .get(courseLegacyLibraryContentTaskStatus(courseId, taskId));
+
+ return camelCaseObject(data);
+}
diff --git a/src/course-libraries/data/apiHooks.test.tsx b/src/course-libraries/data/apiHooks.test.tsx
index 4f8c5f9a9..418ff3d5b 100644
--- a/src/course-libraries/data/apiHooks.test.tsx
+++ b/src/course-libraries/data/apiHooks.test.tsx
@@ -3,8 +3,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
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 } from './apiHooks';
+import { courseLegacyLibraryContentBlocks, courseLegacyLibraryContentTaskStatus, getEntityLinksByDownstreamContextUrl } from './api';
+import { useCheckMigrateCourseLegacyLibReadyToMigrateBlocksOptions, useCourseLegacyLibReadyToMigrateBlocks, useEntityLinks } from './apiHooks';
let axiosMock: MockAdapter;
@@ -74,4 +74,36 @@ describe('course libraries api hooks', () => {
content_type: undefined,
});
});
+
+ it('should return ready to migrate blocks', async () => {
+ const courseId = 'course-v1:some+key';
+ const url = courseLegacyLibraryContentBlocks(courseId);
+ axiosMock.onGet(url).reply(200, []);
+ const { result } = renderHook(() => useCourseLegacyLibReadyToMigrateBlocks(courseId), { wrapper });
+ await waitFor(() => {
+ expect(result.current.isLoading).toBeFalsy();
+ });
+ expect(axiosMock.history.get[0].url).toEqual(url);
+ });
+
+ it('should check tasks status', async () => {
+ const courseId = 'course-v1:some+key';
+ const taskId = 'some-id';
+ const uuid = '1f8831dd-6f90-48df-a503-c0d0e957a331';
+ const url = courseLegacyLibraryContentTaskStatus(courseId, taskId);
+ axiosMock.onGet(url).reply(200, {
+ task_id: 'some-id',
+ status: 'Succeeded',
+ status_text: 'Succeeded',
+ uuid,
+ });
+ const { result } = renderHook(() => useCheckMigrateCourseLegacyLibReadyToMigrateBlocksOptions(courseId, taskId), {
+ wrapper,
+ });
+ await waitFor(() => {
+ expect(result.current.isLoading).toBeFalsy();
+ });
+ expect(axiosMock.history.get[0].url).toEqual(url);
+ expect(result.current.data?.uuid).toEqual(uuid);
+ });
});
diff --git a/src/course-libraries/data/apiHooks.ts b/src/course-libraries/data/apiHooks.ts
index 78920339c..51264cf14 100644
--- a/src/course-libraries/data/apiHooks.ts
+++ b/src/course-libraries/data/apiHooks.ts
@@ -1,8 +1,19 @@
import {
type QueryClient,
useQuery,
+ skipToken,
+ useMutation,
+ UseQueryResult,
} from '@tanstack/react-query';
-import { getEntityLinksSummaryByDownstreamContext, getEntityLinks } from './api';
+import { UserTaskStatus } from '@src/data/constants';
+import type { UserTaskStatusWithUuid } from '@src/data/types';
+import {
+ getEntityLinksSummaryByDownstreamContext,
+ getEntityLinks,
+ getCourseReadyToMigrateLegacyLibContentBlocks,
+ migrateCourseReadyToMigrateLegacyLibContentBlocks,
+ getCourseLegacyLibRefUpdateTaskStatus,
+} from './api';
export const courseLibrariesQueryKeys = {
all: ['courseLibraries'],
@@ -32,6 +43,12 @@ export const courseLibrariesQueryKeys = {
return key;
},
courseLibrariesSummary: (courseId?: string) => [...courseLibrariesQueryKeys.courseLibraries(courseId), 'summary'],
+ legacyLibReadyToMigrateBlocks: (courseId: string) => [...courseLibrariesQueryKeys.courseLibraries(courseId), 'legacyLibReadyToMigrateBlocks'],
+ legacyLibReadyToMigrateBlocksStatus: (courseId: string, taskId?: string) => [
+ ...courseLibrariesQueryKeys.legacyLibReadyToMigrateBlocks(courseId),
+ 'status',
+ { taskId },
+ ],
};
export const useEntityLinks = ({
@@ -80,3 +97,28 @@ export const invalidateLinksQuery = (queryClient: QueryClient, courseId: string)
queryKey: courseLibrariesQueryKeys.courseLibraries(courseId),
});
};
+
+export const useCourseLegacyLibReadyToMigrateBlocks = (courseId: string, enabled: boolean = true) => (
+ useQuery({
+ queryKey: courseLibrariesQueryKeys.legacyLibReadyToMigrateBlocks(courseId),
+ queryFn: enabled && courseId ? () => getCourseReadyToMigrateLegacyLibContentBlocks(courseId) : skipToken,
+ })
+);
+
+export const useMigrateCourseLegacyLibReadyToMigrateBlocks = (courseId: string) => useMutation({
+ mutationFn: () => migrateCourseReadyToMigrateLegacyLibContentBlocks(courseId),
+ gcTime: 60, // Cache for 1 minute to prevent rapid re-run of updating references
+});
+
+export const useCheckMigrateCourseLegacyLibReadyToMigrateBlocksOptions = (
+ courseId: string,
+ taskId?: string,
+): UseQueryResult => useQuery({
+ queryKey: courseLibrariesQueryKeys.legacyLibReadyToMigrateBlocksStatus(courseId, taskId),
+ queryFn: taskId ? () => getCourseLegacyLibRefUpdateTaskStatus(courseId, taskId) : skipToken,
+ refetchInterval: (query) => ([
+ UserTaskStatus.Succeeded,
+ UserTaskStatus.Failed,
+ UserTaskStatus.Cancelled,
+ ].includes(query.state.data?.state || UserTaskStatus.InProgress) ? false : 2000),
+});
diff --git a/src/course-libraries/messages.ts b/src/course-libraries/messages.ts
index efd3bb7a2..25ef757a8 100644
--- a/src/course-libraries/messages.ts
+++ b/src/course-libraries/messages.ts
@@ -121,6 +121,41 @@ const messages = defineMessages({
defaultMessage: 'View Section in Course',
description: 'Label of the button to see the section in the course',
},
+ legacyLibReadyToMigrateAlertTitle: {
+ id: 'course-authoring.course-libraries.legacyLibReadyToMigrate.alert.title',
+ defaultMessage: 'This course contains {count, plural, one {# legacy library reference} other {# legacy library references}}',
+ description: 'Title of alert shown when course contains legacy library content references',
+ },
+ legacyLibReadyToMigrateAlertDescription: {
+ id: 'course-authoring.course-libraries.legacyLibReadyToMigrate.alert.description',
+ defaultMessage: 'Legacy library references will no longer be supported and need to be updated to receive future changes.',
+ description: 'Description of alert shown when course contains legacy library content references',
+ },
+ legacyLibReadyToMigrateAlertLearnMoreBtn: {
+ id: 'course-authoring.course-libraries.legacyLibReadyToMigrate.alert.learnMoreBtn',
+ defaultMessage: 'Learn more',
+ description: 'Learn more button text of alert shown when course contains legacy library content references',
+ },
+ legacyLibReadyToMigrateAlertActionBtn: {
+ id: 'course-authoring.course-libraries.legacyLibReadyToMigrate.alert.actionBtn',
+ defaultMessage: 'Update library references',
+ description: 'Action button text of alert shown when course contains legacy library content references',
+ },
+ legacyLibReadyToMigrateTaskCompleted: {
+ id: 'course-authoring.course-libraries.legacyLibReadyToMigrate.alert.task.completed',
+ defaultMessage: 'Successfully updated all legacy library references',
+ description: 'Toast text when all legacy library references are updated.',
+ },
+ legacyLibReadyToMigrateTaskFailed: {
+ id: 'course-authoring.course-libraries.legacyLibReadyToMigrate.alert.task.failed',
+ defaultMessage: 'Failed to update legacy library references',
+ description: 'Toast text when legacy library references fail to update.',
+ },
+ legacyLibReadyToMigrateTaskInProgress: {
+ id: 'course-authoring.course-libraries.legacyLibReadyToMigrate.alert.task.in-progress',
+ defaultMessage: 'Updating library references...',
+ description: 'Toast text when updating legacy library references is in progress.',
+ },
});
export default messages;
diff --git a/src/course-outline/CourseOutline.tsx b/src/course-outline/CourseOutline.tsx
index e629e289b..0406b95e5 100644
--- a/src/course-outline/CourseOutline.tsx
+++ b/src/course-outline/CourseOutline.tsx
@@ -41,6 +41,7 @@ import { NOTIFICATION_MESSAGES } from '@src/constants';
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
import { XBlock } from '@src/data/types';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
+import LegacyLibContentBlockAlert from '@src/course-libraries/LegacyLibContentBlockAlert';
import {
getCurrentItem,
getProctoredExamsFlag,
@@ -306,6 +307,7 @@ const CourseOutline = () => {
savingStatus={savingStatus}
errors={errors}
/>
+
{showSuccessAlert ? (
`${getXBlockBaseApiUrl()}
export const getXBlockApiUrl = (blockId: string) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
export const exportTags = (courseId: string) => `${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`;
export const createDiscussionsTopicsUrl = (courseId: string) => `${getApiBaseUrl()}/api/discussions/v0/course/${courseId}/sync_discussion_topics`;
+export const courseLegacyLibraryContentBlocks = (courseId: string) => `${getApiBaseUrl()}/api/courses/v1/migrate_legacy_content_blocks/${courseId}/`;
+export const courseLegacyLibraryContentTaskStatus = (courseId: string, taskId: string) => `${courseLegacyLibraryContentBlocks(courseId)}${taskId}/`;
/**
* Get course outline index.
diff --git a/src/course-outline/data/apiHooks.ts b/src/course-outline/data/apiHooks.ts
index fd4b76827..67a20acde 100644
--- a/src/course-outline/data/apiHooks.ts
+++ b/src/course-outline/data/apiHooks.ts
@@ -1,6 +1,11 @@
-import { skipToken, useMutation, useQuery } from '@tanstack/react-query';
+import {
+ skipToken, useMutation, useQuery,
+} from '@tanstack/react-query';
import { createCourseXblock } from '@src/course-unit/data/api';
-import { getCourseDetails, getCourseItem } from './api';
+import {
+ getCourseDetails,
+ getCourseItem,
+} from './api';
export const courseOutlineQueryKeys = {
all: ['courseOutline'],
@@ -10,6 +15,12 @@ export const courseOutlineQueryKeys = {
contentLibrary: (courseId?: string) => [...courseOutlineQueryKeys.all, courseId],
courseItemId: (itemId?: string) => [...courseOutlineQueryKeys.all, itemId],
courseDetails: (courseId?: string) => [...courseOutlineQueryKeys.all, courseId, 'details'],
+ legacyLibReadyToMigrateBlocks: (courseId: string) => [...courseOutlineQueryKeys.all, courseId, 'legacyLibReadyToMigrateBlocks'],
+ legacyLibReadyToMigrateBlocksStatus: (courseId: string, taskId?: string) => [
+ ...courseOutlineQueryKeys.legacyLibReadyToMigrateBlocks(courseId),
+ 'status',
+ { taskId },
+ ],
};
/**
diff --git a/src/data/constants.ts b/src/data/constants.ts
index 2fc511d85..1bd4d70f7 100644
--- a/src/data/constants.ts
+++ b/src/data/constants.ts
@@ -61,3 +61,12 @@ export const VisibilityTypes = {
export const TOTAL_LENGTH_KEY = 'total-length';
export const MAX_TOTAL_LENGTH = 65;
+
+export enum UserTaskStatus {
+ Pending = 'Pending',
+ Succeeded = 'Succeeded',
+ Failed = 'Failed',
+ InProgress = 'In Progress',
+ Cancelled = 'Cancelled',
+ Retrying = 'Retrying',
+}
diff --git a/src/data/types.ts b/src/data/types.ts
index 95fe26471..246f8c8c9 100644
--- a/src/data/types.ts
+++ b/src/data/types.ts
@@ -1,3 +1,5 @@
+import { UserTaskStatus } from './constants';
+
export interface GroupTypes {
id: number;
name: string;
@@ -135,3 +137,19 @@ export interface OutlinePageErrors {
sectionLoadingApi?: OutlineError | null,
courseLaunchApi?: OutlineError | null,
}
+
+export interface UsageKeyBlock {
+ usageKey: string;
+}
+
+export interface UserTaskStatusWithUuid {
+ name: string;
+ state: UserTaskStatus;
+ stateText: string;
+ completedSteps: number;
+ totalSteps: number;
+ attempts: number;
+ created: string;
+ modified: string;
+ uuid: string;
+}