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; +}