From c4a09a2b430598e8260be974845d40c576bfc02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Thu, 26 Feb 2026 14:34:13 -0500 Subject: [PATCH] refactor: Migrate `courseImport` from redux store to React query (#2902) --- src/CourseAuthoringRoutes.tsx | 9 +- ...tepper.test.jsx => CourseStepper.test.tsx} | 68 +++---- .../course-stepper/{index.jsx => index.tsx} | 50 +++--- src/import-page/CourseImportContext.tsx | 167 ++++++++++++++++++ src/import-page/CourseImportPage.test.tsx | 78 ++++---- src/import-page/CourseImportPage.tsx | 44 ++--- .../data/{api.test.jsx => api.test.tsx} | 16 +- src/import-page/data/{api.js => api.ts} | 25 ++- src/import-page/data/apiHooks.ts | 51 ++++++ src/import-page/data/selectors.js | 8 - src/import-page/data/slice.ts | 63 ------- src/import-page/data/thunks.ts | 68 ------- src/import-page/file-section/FileSection.jsx | 56 ------ .../file-section/FileSection.test.jsx | 61 ------- .../file-section/FileSection.test.tsx | 58 ++++++ src/import-page/file-section/FileSection.tsx | 43 +++++ .../import-sidebar/ImportSidebar.test.jsx | 17 -- .../import-sidebar/ImportSidebar.test.tsx | 22 +++ .../{ImportSidebar.jsx => ImportSidebar.tsx} | 18 +- .../import-stepper/ImportStepper.test.jsx | 38 ---- .../import-stepper/ImportStepper.test.tsx | 24 +++ .../{ImportStepper.jsx => ImportStepper.tsx} | 71 +++----- src/import-page/import-stepper/messages.ts | 4 - src/import-page/messages.ts | 4 + src/import-page/utils.test.ts | 5 +- src/import-page/utils.ts | 5 +- src/store.ts | 3 - 27 files changed, 536 insertions(+), 540 deletions(-) rename src/generic/course-stepper/{CourseStepper.test.jsx => CourseStepper.test.tsx} (53%) rename src/generic/course-stepper/{index.jsx => index.tsx} (76%) create mode 100644 src/import-page/CourseImportContext.tsx rename src/import-page/data/{api.test.jsx => api.test.tsx} (73%) rename src/import-page/data/{api.js => api.ts} (82%) create mode 100644 src/import-page/data/apiHooks.ts delete mode 100644 src/import-page/data/selectors.js delete mode 100644 src/import-page/data/slice.ts delete mode 100644 src/import-page/data/thunks.ts delete mode 100644 src/import-page/file-section/FileSection.jsx delete mode 100644 src/import-page/file-section/FileSection.test.jsx create mode 100644 src/import-page/file-section/FileSection.test.tsx create mode 100644 src/import-page/file-section/FileSection.tsx delete mode 100644 src/import-page/import-sidebar/ImportSidebar.test.jsx create mode 100644 src/import-page/import-sidebar/ImportSidebar.test.tsx rename src/import-page/import-sidebar/{ImportSidebar.jsx => ImportSidebar.tsx} (86%) delete mode 100644 src/import-page/import-stepper/ImportStepper.test.jsx create mode 100644 src/import-page/import-stepper/ImportStepper.test.tsx rename src/import-page/import-stepper/{ImportStepper.jsx => ImportStepper.tsx} (54%) diff --git a/src/CourseAuthoringRoutes.tsx b/src/CourseAuthoringRoutes.tsx index 3a330377b..fbd6e6a5a 100644 --- a/src/CourseAuthoringRoutes.tsx +++ b/src/CourseAuthoringRoutes.tsx @@ -31,6 +31,7 @@ import GroupConfigurations from './group-configurations'; import { CourseLibraries } from './course-libraries'; import { IframeProvider } from './generic/hooks/context/iFrameContext'; import { CourseAuthoringProvider } from './CourseAuthoringContext'; +import { CourseImportProvider } from './import-page/CourseImportContext'; /** * As of this writing, these routes are mounted at a path prefixed with the following: @@ -141,7 +142,13 @@ const CourseAuthoringRoutes = () => { /> } + element={( + + + + + + )} /> render( - - - , + , ); describe('', () => { - it('renders CourseStepper correctly', () => { - const { - getByText, getByTestId, getAllByTestId, queryByTestId, - } = renderComponent({ activeKey: 0 }); + beforeEach(() => { + initializeMocks(); + }); - const steps = getAllByTestId('course-stepper__step'); + it('renders CourseStepper correctly', () => { + renderComponent({ activeKey: 0 }); + + const steps = screen.getAllByTestId('course-stepper__step'); expect(steps.length).toBe(stepsMock.length); stepsMock.forEach((step) => { - expect(getByText(step.title)).toBeInTheDocument(); - expect(getByText(step.description)).toBeInTheDocument(); - expect(getByTestId(`${step.title}-icon`)).toBeInTheDocument(); + expect(screen.getByText(step.title)).toBeInTheDocument(); + expect(screen.getByText(step.description)).toBeInTheDocument(); + expect(screen.getByTestId(`${step.title}-icon`)).toBeInTheDocument(); }); - const percentElement = queryByTestId('course-stepper__step-percent'); - expect(percentElement).toBeNull(); + expect(screen.queryByTestId('course-stepper__step-percent')).toBeNull(); }); it('marks the active and done steps correctly', () => { const activeKey = 1; - const { getAllByTestId } = renderComponent({ activeKey }); + renderComponent({ activeKey }); - const steps = getAllByTestId('course-stepper__step'); + const steps = screen.getAllByTestId('course-stepper__step'); stepsMock.forEach((_, index) => { const stepElement = steps[index]; if (index === activeKey) { @@ -71,37 +68,46 @@ describe('', () => { }); it('mark the error step correctly', () => { - const { getAllByTestId } = renderComponent({ activeKey: 1, hasError: true }); + renderComponent({ activeKey: 1, hasError: true }); - const errorStep = getAllByTestId('course-stepper__step')[1]; + const errorStep = screen.getAllByTestId('course-stepper__step')[1]; expect(errorStep).toHaveClass('error'); }); it('shows error message for error step', () => { const errorMessage = 'Some error text'; - const { getAllByTestId } = renderComponent({ activeKey: 1, hasError: true, errorMessage }); + renderComponent({ activeKey: 1, hasError: true, errorMessage }); - const errorStep = getAllByTestId('course-stepper__step')[1]; + const errorStep = screen.getAllByTestId('course-stepper__step')[1]; expect(errorStep).toHaveClass('error'); }); it('shows percentage for active step', () => { const percent = 50; - const { getByTestId } = renderComponent({ activeKey: 1, percent }); + renderComponent({ activeKey: 1, percent }); - const percentElement = getByTestId('course-stepper__step-percent'); + const percentElement = screen.getByTestId('course-stepper__step-percent'); expect(percentElement).toBeInTheDocument(); expect(percentElement).toHaveTextContent(`${percent}%`); }); - it('shows null when steps length equal to zero', () => { - const { queryByTestId } = render( - - - , - ); + it('renders titleComponent instead of title when provided', () => { + const customTitle = Custom Title Component; + const stepsWithTitleComponent = [ + { ...stepsMock[0], titleComponent: customTitle }, + ...stepsMock.slice(1), + ]; - const steps = queryByTestId('[data-testid="course-stepper__step"]'); + renderComponent({ steps: stepsWithTitleComponent, activeKey: 0 }); + + expect(screen.getByTestId('custom-title')).toBeInTheDocument(); + expect(screen.queryByText(stepsMock[0].title)).not.toBeInTheDocument(); + }); + + it('shows null when steps length equal to zero', () => { + renderComponent({ steps: [], activeKey: 0 }); + + const steps = screen.queryByTestId('[data-testid="course-stepper__step"]'); expect(steps).toBe(null); }); }); diff --git a/src/generic/course-stepper/index.jsx b/src/generic/course-stepper/index.tsx similarity index 76% rename from src/generic/course-stepper/index.jsx rename to src/generic/course-stepper/index.tsx index a682f297a..ef3cefe1e 100644 --- a/src/generic/course-stepper/index.jsx +++ b/src/generic/course-stepper/index.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import { ReactElement } from 'react'; +import classNames from 'classnames'; import { Settings as SettingsIcon, ManageHistory as SuccessIcon, @@ -6,16 +7,26 @@ import { CheckCircle, } from '@openedx/paragon/icons'; import { Icon } from '@openedx/paragon'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; + +export interface CourseStepperProps { + steps: { + title: string; + description: string; + titleComponent?: ReactElement; + }[]; + activeKey: number; + percent?: number | boolean; + errorMessage?: string | null; + hasError?: boolean; +} const CourseStepper = ({ steps, activeKey, - percent, - hasError, - errorMessage, -}) => { + percent = false, + hasError = false, + errorMessage = '', +}: CourseStepperProps) => { const getStepperSettings = (index) => { const lastStepIndex = steps.length - 1; const isActiveStep = index === activeKey; @@ -42,7 +53,7 @@ const CourseStepper = ({ }; return { - stepIcon: getStepIcon(index), + stepIcon: getStepIcon(), isPercentShow: Boolean(percent) && percent !== 100 && isActiveStep && !hasError, isErrorMessageShow: isErrorStep && errorMessage, isActiveClass: isActiveStep && !isLastStep && !hasError, @@ -53,7 +64,7 @@ const CourseStepper = ({ return (
- {steps.length ? steps.map(({ title, description }, index) => { + {steps.length ? steps.map(({ title, description, titleComponent }, index) => { const { stepIcon, isPercentShow, @@ -74,10 +85,10 @@ const CourseStepper = ({ data-testid="course-stepper__step" >
- +
-

{title}

+

{titleComponent ?? title}

{isPercentShow && (

Promise; + formattedErrorMessage: string; + successDate?: number; +}; + +/** + * Course Import Context. + * Always available when we're in the context of the Course Import Page. + * + * Get this using `useCourseImportContext()` + */ +const CourseImportContext = createContext(undefined); + +type CourseImportProviderProps = { + children?: React.ReactNode; +}; + +type OnProcessUploadProps = { + fileData: any; + requestConfig: Record; + handleError: (error: any) => void; +}; + +export const CourseImportProvider = ({ children }: CourseImportProviderProps) => { + const intl = useIntl(); + const { courseId } = useCourseAuthoringContext(); + const [isStopFetching, setStopFetching] = useState(false); + const [importTriggered, setImportTriggered] = useState(false); + const [currentStage, setCurrentStage] = useState(0); + const [fileName, setFileName] = useState(); + const importMutation = useStartCourseImporting(courseId); + const [progress, updateProgress] = useState(0); + const [successDate, setSuccessDate] = useState(); + + const cookies = new Cookies(); + + useEffect(() => { + const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME); + if (cookieData) { + setImportTriggered(true); + setFileName(cookieData.fileName); + setSuccessDate(cookieData.date); + } + }, []); + + const reset = () => { + setCurrentStage(0); + updateProgress(0); + setImportTriggered(false); + setStopFetching(false); + setFileName(undefined); + }; + + const handleOnProcessUpload = async ({ + fileData, + requestConfig, + handleError, + }: OnProcessUploadProps) => { + reset(); + const file = fileData.get('file'); + setFileName(file.name); + setImportTriggered(true); + importMutation.mutateAsync({ + fileData: file, + requestConfig, + handleError, + updateProgress, + }).then(() => { + const momentData = moment().valueOf(); + setImportCookie(momentData, file.name); + setSuccessDate(momentData); + }).catch((error) => { + handleError(error); + }); + }; + + const { + data: importStatusData, + isError: isErrorImportStatus, + isPending: isPendingImportStatus, + failureReason: importStatusError, + } = useImportStatus(courseId, isStopFetching, fileName); + + const errorMessage = importStatusData?.message; + const anyRequestFailed = isErrorImportStatus || importMutation.isError || Boolean(errorMessage); + const anyRequestInProgress = isPendingImportStatus || importMutation.isPending; + const formattedErrorMessage = anyRequestFailed ? errorMessage || intl.formatMessage(messages.defaultErrorMessage) : ''; + const isLoadingDenied = importStatusError?.response?.status === 403; + + useEffect(() => { + const polledStage = importStatusData?.importStatus; + if (polledStage !== undefined && polledStage >= 0) { + setCurrentStage(polledStage); + } + }, [importStatusData?.importStatus]); + + useEffect(() => { + if (currentStage === IMPORT_STAGES.SUCCESS || anyRequestFailed) { + setStopFetching(true); + } + }, [currentStage, anyRequestFailed]); + + const context = useMemo(() => { + const contextValue = { + importTriggered, + progress, + fileName, + currentStage, + anyRequestFailed, + anyRequestInProgress, + isLoadingDenied, + handleOnProcessUpload, + formattedErrorMessage, + successDate, + }; + + return contextValue; + }, [ + importTriggered, + progress, + fileName, + currentStage, + anyRequestFailed, + anyRequestInProgress, + isLoadingDenied, + handleOnProcessUpload, + formattedErrorMessage, + successDate, + ]); + + return ( + + {children} + + ); +}; + +export function useCourseImportContext(): CourseImportContextData { + const ctx = useContext(CourseImportContext); + if (ctx === undefined) { + /* istanbul ignore next */ + throw new Error('useCourseImportContext() was used in a component without a ancestor.'); + } + return ctx; +} diff --git a/src/import-page/CourseImportPage.test.tsx b/src/import-page/CourseImportPage.test.tsx index dab7c0bf0..481d0e51b 100644 --- a/src/import-page/CourseImportPage.test.tsx +++ b/src/import-page/CourseImportPage.test.tsx @@ -1,17 +1,17 @@ +import { screen } from '@testing-library/react'; import { Helmet } from 'react-helmet'; import Cookies from 'universal-cookie'; import { CourseAuthoringProvider } from '@src/CourseAuthoringContext'; import { getCourseDetailsUrl } from '@src/data/api'; -import { initializeMocks, render, waitFor } from '../testUtils'; -import { RequestStatus } from '../data/constants'; +import { initializeMocks, render, waitFor } from '@src/testUtils'; import messages from './messages'; import CourseImportPage from './CourseImportPage'; import { getImportStatusApiUrl } from './data/api'; import { IMPORT_STAGES } from './data/constants'; import stepperMessages from './import-stepper/messages'; +import { CourseImportProvider } from './CourseImportContext'; -let store; let axiosMock; let cookies; const courseId = '123'; @@ -27,7 +27,9 @@ jest.mock('universal-cookie', () => { const renderComponent = () => render( - + + + , ); @@ -38,7 +40,6 @@ describe('', () => { username: 'username', }; const mocks = initializeMocks({ user }); - store = mocks.reduxStore; axiosMock = mocks.axiosMock; axiosMock .onGet(getImportStatusApiUrl(courseId, 'testFileName.test')) @@ -49,6 +50,7 @@ describe('', () => { cookies = new Cookies(); cookies.get.mockReturnValue(null); }); + it('should render page title correctly', async () => { renderComponent(); await waitFor(() => { @@ -58,67 +60,59 @@ describe('', () => { ); }); }); + it('should render without errors', async () => { - const { getByText } = renderComponent(); - await waitFor(() => { - expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); - const importPageElement = getByText(messages.headingTitle.defaultMessage, { - selector: 'h2.sub-header-title', - }); - expect(importPageElement).toBeInTheDocument(); - expect(getByText(messages.description1.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument(); + renderComponent(); + expect(await screen.findByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); + const importPageElement = screen.getByText(messages.headingTitle.defaultMessage, { + selector: 'h2.sub-header-title', }); + expect(importPageElement).toBeInTheDocument(); + expect(screen.getByText(messages.description1.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.description2.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.description3.defaultMessage)).toBeInTheDocument(); }); + it('should fetch status without clicking when cookies has', async () => { - cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.test' }); - const { getByText } = renderComponent(); - expect(getByText(stepperMessages.stepperUnpackingDescription.defaultMessage)).toBeInTheDocument(); + cookies.get.mockReturnValue({ date: 1679787000, fileName: 'testFileName.test' }); + renderComponent(); + expect(screen.getByText(stepperMessages.stepperUnpackingDescription.defaultMessage)).toBeInTheDocument(); }); + it('should show error', async () => { + const errorMessage = 'This is a test error message'; axiosMock .onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz')) - .reply(200, { importStatus: -IMPORT_STAGES.UPDATING, message: '' }); - cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' }); - const { getByText } = renderComponent(); - await waitFor(() => { - expect(getByText(stepperMessages.defaultErrorMessage.defaultMessage)).toBeInTheDocument(); - }, { timeout: 4000 }); + .reply(200, { importStatus: -IMPORT_STAGES.UPDATING, message: errorMessage }); + cookies.get.mockReturnValue({ date: 1679787000, fileName: 'testFileName.tar.gz' }); + renderComponent(); + expect(await screen.findByText(errorMessage)).toBeInTheDocument(); }); + it('should show success button', async () => { axiosMock .onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz')) .reply(200, { importStatus: IMPORT_STAGES.SUCCESS, message: '' }); - cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' }); - const { getByText } = renderComponent(); - await waitFor(() => { - expect(getByText(stepperMessages.viewOutlineButton.defaultMessage)).toBeInTheDocument(); - }, { timeout: 4000 }); + cookies.get.mockReturnValue({ date: 1679787000, fileName: 'testFileName.tar.gz' }); + renderComponent(); + expect(await screen.findByText(stepperMessages.viewOutlineButton.defaultMessage)).toBeInTheDocument(); }); it('displays an alert and sets status to DENIED when API responds with 403', async () => { axiosMock .onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz')) .reply(403); - cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' }); - const { getByRole } = renderComponent(); - await waitFor(() => { - expect(getByRole('alert')).toBeInTheDocument(); - }, { timeout: 4000 }); - const { loadingStatus } = store.getState().courseImport; - expect(loadingStatus).toEqual(RequestStatus.DENIED); + cookies.get.mockReturnValue({ date: 1679787000, fileName: 'testFileName.tar.gz' }); + renderComponent(); + expect(await screen.findByRole('alert')).toBeInTheDocument(); }); - it('sets loading status to FAILED upon receiving a 404 response from the API', async () => { + it('shows an error message upon receiving a 404 response from the API', async () => { axiosMock .onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz')) .reply(404); - cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' }); + cookies.get.mockReturnValue({ date: 1679787000, fileName: 'testFileName.tar.gz' }); renderComponent(); - await waitFor(() => { - const { loadingStatus } = store.getState().courseImport; - expect(loadingStatus).toEqual(RequestStatus.FAILED); - }, { timeout: 4000 }); + expect(await screen.findByText(messages.defaultErrorMessage.defaultMessage)).toBeInTheDocument(); }); }); diff --git a/src/import-page/CourseImportPage.tsx b/src/import-page/CourseImportPage.tsx index 1c8f2adb0..0f9b75a36 100644 --- a/src/import-page/CourseImportPage.tsx +++ b/src/import-page/CourseImportPage.tsx @@ -1,50 +1,30 @@ -/* eslint-disable max-len */ -import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Container, Layout, } from '@openedx/paragon'; -import Cookies from 'universal-cookie'; + import { Helmet } from 'react-helmet'; import SubHeader from '@src/generic/sub-header/SubHeader'; import InternetConnectionAlert from '@src/generic/internet-connection-alert'; -import { RequestStatus } from '@src/data/constants'; import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert'; import { useCourseAuthoringContext } from '@src/CourseAuthoringContext'; -import { - updateFileName, updateImportTriggered, updateSavingStatus, updateSuccessDate, -} from './data/slice'; import ImportStepper from './import-stepper/ImportStepper'; -import { getImportTriggered, getLoadingStatus, getSavingStatus } from './data/selectors'; -import { LAST_IMPORT_COOKIE_NAME } from './data/constants'; import ImportSidebar from './import-sidebar/ImportSidebar'; import FileSection from './file-section/FileSection'; import messages from './messages'; +import { useCourseImportContext } from './CourseImportContext'; const CourseImportPage = () => { const intl = useIntl(); - const dispatch = useDispatch(); - const cookies = new Cookies(); - const { courseId, courseDetails } = useCourseAuthoringContext(); - const importTriggered = useSelector(getImportTriggered); - const savingStatus = useSelector(getSavingStatus); - const loadingStatus = useSelector(getLoadingStatus); - const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED; - const isLoadingDenied = loadingStatus === RequestStatus.DENIED; - const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS; - - useEffect(() => { - const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME); - if (cookieData) { - dispatch(updateSavingStatus(RequestStatus.SUCCESSFUL)); - dispatch(updateImportTriggered(true)); - dispatch(updateFileName(cookieData.fileName)); - dispatch(updateSuccessDate(cookieData.date)); - } - }, []); + const { courseDetails } = useCourseAuthoringContext(); + const { + importTriggered, + anyRequestFailed, + anyRequestInProgress, + isLoadingDenied, + } = useCourseImportContext(); if (isLoadingDenied) { return ( @@ -83,12 +63,12 @@ const CourseImportPage = () => {

{intl.formatMessage(messages.description1)}

{intl.formatMessage(messages.description2)}

{intl.formatMessage(messages.description3)}

- - {importTriggered && } + + {importTriggered && } - + diff --git a/src/import-page/data/api.test.jsx b/src/import-page/data/api.test.tsx similarity index 73% rename from src/import-page/data/api.test.jsx rename to src/import-page/data/api.test.tsx index 5f0e7b43b..5e5dbe88f 100644 --- a/src/import-page/data/api.test.jsx +++ b/src/import-page/data/api.test.tsx @@ -1,7 +1,6 @@ -import MockAdapter from 'axios-mock-adapter'; -import { initializeMockApp, getConfig } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getConfig } from '@edx/frontend-platform'; +import { initializeMocks } from '@src/testUtils'; import { getImportStatus, postImportCourseApiUrl, startCourseImporting } from './api'; let axiosMock; @@ -9,15 +8,7 @@ const courseId = 'course-123'; describe('API Functions', () => { beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + ({ axiosMock } = initializeMocks()); }); afterEach(() => { @@ -25,6 +16,7 @@ describe('API Functions', () => { }); it('should fetch status on start importing', async () => { + // @ts-ignore const file = new File(['(⌐□_□)'], 'download.tar.gz', { size: 20 }); const data = { importStatus: 1 }; axiosMock.onPost(postImportCourseApiUrl(courseId)).reply(200, data); diff --git a/src/import-page/data/api.js b/src/import-page/data/api.ts similarity index 82% rename from src/import-page/data/api.js rename to src/import-page/data/api.ts index 9d7bf74e8..c2f24513b 100644 --- a/src/import-page/data/api.js +++ b/src/import-page/data/api.ts @@ -5,14 +5,20 @@ const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; export const postImportCourseApiUrl = (courseId) => `${getApiBaseUrl()}/import/${courseId}`; export const getImportStatusApiUrl = (courseId, fileName) => `${getApiBaseUrl()}/import_status/${courseId}/${fileName}`; +export interface ImportStatusData { + importStatus: number, + message?: string, +} + /** * Start import course. - * @param {string} courseId - * @param {File} fileData - * @param {Record} requestConfig - * @returns {Promise>} */ -export async function startCourseImporting(courseId, fileData, requestConfig, updateProgress) { +export async function startCourseImporting( + courseId: string, + fileData: File, + requestConfig: Record, + updateProgress: (percent: number) => void, +): Promise { const chunkSize = 20 * 1000000; // 20 MB const fileSize = fileData.size || 0; const chunkLength = Math.ceil(fileSize / chunkSize); @@ -54,12 +60,13 @@ export async function startCourseImporting(courseId, fileData, requestConfig, up /** * Get import status. - * @param {string} courseId - * @param {string} fileName - * @returns {Promise} */ -export async function getImportStatus(courseId, fileName) { +export async function getImportStatus( + courseId: string, + fileName: string, +): Promise { const { data } = await getAuthenticatedHttpClient() .get(getImportStatusApiUrl(courseId, fileName)); + return camelCaseObject(data); } diff --git a/src/import-page/data/apiHooks.ts b/src/import-page/data/apiHooks.ts new file mode 100644 index 000000000..320074c77 --- /dev/null +++ b/src/import-page/data/apiHooks.ts @@ -0,0 +1,51 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { AxiosError } from 'axios'; +import { skipToken, useMutation, useQuery } from '@tanstack/react-query'; + +import { getImportStatus, startCourseImporting, type ImportStatusData } from './api'; + +export const importQueryKeys = { + all: ['courseImport'], + /** Key for the import status of a specific file in a course */ + importStatus: (courseId: string, fileName: string) => [ + ...importQueryKeys.all, + courseId, + fileName, + ], +}; + +interface StartCourseImportingProps { + updateProgress: (percent: number) => void; + fileData: File; + requestConfig: Record; + handleError: (error: any) => void; +} + +/** + * Returns a mutation to start uploading and importing a course file. + * Handles chunked file uploads and reports upload progress via `updateProgress`. + */ +export const useStartCourseImporting = (courseId: string) => ( + useMutation({ + mutationFn: ({ fileData, requestConfig, updateProgress }: StartCourseImportingProps) => ( + startCourseImporting(courseId, fileData, requestConfig, updateProgress) + ), + onError: (error, { handleError }) => handleError(error), + }) +); + +/** + * Polls the import status for a given file being imported into a course. + * Only enabled when `fileName` is provided. + */ +export const useImportStatus = ( + courseId: string, + stopRefetch: boolean, + fileName?: string, +) => ( + useQuery({ + queryKey: importQueryKeys.importStatus(courseId, fileName ?? ''), + queryFn: fileName ? () => getImportStatus(courseId, fileName!) : skipToken, + refetchInterval: (fileName && !stopRefetch) ? 3000 : false, + }) +); diff --git a/src/import-page/data/selectors.js b/src/import-page/data/selectors.js deleted file mode 100644 index 807566daa..000000000 --- a/src/import-page/data/selectors.js +++ /dev/null @@ -1,8 +0,0 @@ -export const getProgress = (state) => state.courseImport.progress; -export const getCurrentStage = (state) => state.courseImport.currentStage; -export const getImportTriggered = (state) => state.courseImport.importTriggered; -export const getFileName = (state) => state.courseImport.fileName; -export const getError = (state) => state.courseImport.error; -export const getLoadingStatus = (state) => state.courseImport.loadingStatus; -export const getSavingStatus = (state) => state.courseImport.savingStatus; -export const getSuccessDate = (state) => state.courseImport.successDate; diff --git a/src/import-page/data/slice.ts b/src/import-page/data/slice.ts deleted file mode 100644 index 82550e705..000000000 --- a/src/import-page/data/slice.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { createSlice } from '@reduxjs/toolkit'; - -const initialState = { - currentStage: 0, - error: { hasError: false, message: '' }, - progress: 0, - importTriggered: false, - fileName: null, - loadingStatus: '', - savingStatus: '', - successDate: null, -}; - -const slice = createSlice({ - name: 'importPage', - initialState, - reducers: { - updateCurrentStage: (state, { payload }) => { - if (payload >= state.currentStage) { - state.currentStage = payload; - } - }, - updateError: (state, { payload }) => { - state.error = { ...state.error, ...payload }; - }, - updateProgress: (state, { payload }) => { - state.progress = payload; - }, - updateImportTriggered: (state, { payload }) => { - state.importTriggered = payload; - }, - updateFileName: (state, { payload }) => { - state.fileName = payload; - }, - reset: () => initialState, - updateLoadingStatus: (state, { payload }) => { - state.loadingStatus = payload; - }, - updateSavingStatus: (state, { payload }) => { - state.savingStatus = payload; - }, - updateSuccessDate: (state, { payload }) => { - state.successDate = payload; - }, - }, -}); - -export const { - updateCurrentStage, - updateError, - updateProgress, - updateImportTriggered, - updateFileName, - reset, - updateLoadingStatus, - updateSavingStatus, - updateSuccessDate, -} = slice.actions; - -export const { - reducer, -} = slice; diff --git a/src/import-page/data/thunks.ts b/src/import-page/data/thunks.ts deleted file mode 100644 index 183dabbcb..000000000 --- a/src/import-page/data/thunks.ts +++ /dev/null @@ -1,68 +0,0 @@ -import Cookies from 'universal-cookie'; -import moment from 'moment'; - -import { RequestStatus } from '../../data/constants'; -import { setImportCookie } from '../utils'; -import { getImportStatus, startCourseImporting } from './api'; -import { - reset, updateCurrentStage, updateError, updateFileName, updateProgress, - updateImportTriggered, updateLoadingStatus, updateSavingStatus, updateSuccessDate, -} from './slice'; -import { IMPORT_STAGES, LAST_IMPORT_COOKIE_NAME } from './constants'; - -export function fetchImportStatus(courseId, fileName) { - return async (dispatch) => { - try { - dispatch(updateLoadingStatus(RequestStatus.IN_PROGRESS)); - const { importStatus, message } = await getImportStatus(courseId, fileName); - dispatch(updateCurrentStage(Math.abs(importStatus))); - const cookies = new Cookies(); - const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME); - - if (importStatus < 0) { - dispatch(updateError({ hasError: true, message })); - } else if (importStatus === IMPORT_STAGES.SUCCESS && !cookieData?.completed) { - dispatch(updateSuccessDate(moment().valueOf())); - } - - if (!cookieData?.completed) { - setImportCookie(moment().valueOf(), importStatus === IMPORT_STAGES.SUCCESS, fileName); - } - dispatch(updateLoadingStatus(RequestStatus.SUCCESSFUL)); - return true; - } catch (error: any) { - if (error.response && error.response.status === 403) { - dispatch(updateLoadingStatus(RequestStatus.DENIED)); - } else { - dispatch(updateLoadingStatus(RequestStatus.FAILED)); - } - return false; - } - }; -} - -export function handleProcessUpload(courseId, fileData, requestConfig, handleError) { - return async (dispatch) => { - try { - const file = fileData.get('file'); - dispatch(reset()); - dispatch(updateSavingStatus(RequestStatus.PENDING)); - dispatch(updateFileName(file.name)); - dispatch(updateImportTriggered(true)); - const { importStatus } = await startCourseImporting( - courseId, - file, - requestConfig, - (percent) => dispatch(updateProgress(percent)), - ); - dispatch(updateCurrentStage(importStatus)); - setImportCookie(moment().valueOf(), importStatus === IMPORT_STAGES.SUCCESS, file.name); - dispatch(updateSavingStatus(RequestStatus.SUCCESSFUL)); - return true; - } catch (error) { - handleError(error); - dispatch(updateSavingStatus(RequestStatus.FAILED)); - return false; - } - }; -} diff --git a/src/import-page/file-section/FileSection.jsx b/src/import-page/file-section/FileSection.jsx deleted file mode 100644 index bc42b1114..000000000 --- a/src/import-page/file-section/FileSection.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; -import { Card, Dropzone } from '@openedx/paragon'; - -import { IMPORT_STAGES } from '../data/constants'; -import { - getCurrentStage, getError, getFileName, getImportTriggered, -} from '../data/selectors'; -import messages from './messages'; -import { handleProcessUpload } from '../data/thunks'; - -const FileSection = ({ courseId }) => { - const intl = useIntl(); - const dispatch = useDispatch(); - const importTriggered = useSelector(getImportTriggered); - const currentStage = useSelector(getCurrentStage); - const fileName = useSelector(getFileName); - const { hasError } = useSelector(getError); - const isShowedDropzone = !importTriggered || currentStage === IMPORT_STAGES.SUCCESS || hasError; - - return ( - - - - {isShowedDropzone - && ( - dispatch(handleProcessUpload( - courseId, - fileData, - requestConfig, - handleError, - )) - } - accept={{ 'application/x-tar.gz': ['.tar.gz'] }} - data-testid="dropzone" - style={{ height: '200px' }} - /> - )} - - - ); -}; - -FileSection.propTypes = { - courseId: PropTypes.string.isRequired, -}; - -export default FileSection; diff --git a/src/import-page/file-section/FileSection.test.jsx b/src/import-page/file-section/FileSection.test.jsx deleted file mode 100644 index d5b27dc45..000000000 --- a/src/import-page/file-section/FileSection.test.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { initializeMockApp } from '@edx/frontend-platform'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { AppProvider } from '@edx/frontend-platform/react'; -import { fireEvent, render, waitFor } from '@testing-library/react'; - -import initializeStore from '../../store'; -import messages from './messages'; -import FileSection from './FileSection'; - -let store; -const courseId = '123'; - -const RootWrapper = () => ( - - - - - -); - -describe('', () => { - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - store = initializeStore(); - }); - it('should render without errors', async () => { - const { getByText } = render(); - await waitFor(() => { - expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); - }); - }); - it('should displays Dropzone when import is not triggered or in success stage or has an error', async () => { - const { getByTestId } = render(); - await waitFor(() => { - expect(getByTestId('dropzone')).toBeInTheDocument(); - }); - }); - it('should work Dropzone', async () => { - const { - getByText, getByTestId, queryByTestId, container, - } = render(); - - const dropzoneElement = getByTestId('dropzone'); - - const file = new File(['file contents'], 'example.tar.gz', { type: 'application/gzip' }); - fireEvent.drop(dropzoneElement, { dataTransfer: { files: [file], types: ['Files'] } }); - - await waitFor(() => { - expect(getByText('File chosen: example.tar.gz')).toBeInTheDocument(); - expect(queryByTestId(container, 'dropzone')).not.toBeInTheDocument(); - }); - }); -}); diff --git a/src/import-page/file-section/FileSection.test.tsx b/src/import-page/file-section/FileSection.test.tsx new file mode 100644 index 000000000..acacd512d --- /dev/null +++ b/src/import-page/file-section/FileSection.test.tsx @@ -0,0 +1,58 @@ +import { + fireEvent, + render, + initializeMocks, + screen, + waitFor, +} from '@src/testUtils'; + +import { CourseAuthoringProvider } from '@src/CourseAuthoringContext'; +import messages from './messages'; +import FileSection from './FileSection'; +import { CourseImportProvider } from '../CourseImportContext'; +import { getImportStatusApiUrl, postImportCourseApiUrl } from '../data/api'; + +const courseId = '123'; + +let axiosMock; + +const renderComponent = () => render( + + + + + , +); + +describe('', () => { + beforeEach(() => { + ({ axiosMock } = initializeMocks()); + }); + + it('should render without errors', async () => { + renderComponent(); + expect(await screen.findByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); + }); + + it('should displays Dropzone when import is not triggered or in success stage or has an error', async () => { + renderComponent(); + expect(await screen.findByTestId('dropzone')).toBeInTheDocument(); + }); + + it('should work Dropzone', async () => { + axiosMock.onPost(postImportCourseApiUrl(courseId)).reply(200, { importStatus: 1 }); + axiosMock.onGet(getImportStatusApiUrl(courseId, 'example.tar.gz')).reply(200, { importStatus: 1, message: '' }); + renderComponent(); + + const dropzoneElement = screen.getByTestId('dropzone'); + + const file = new File(['file contents'], 'example.tar.gz', { type: 'application/gzip' }); + fireEvent.drop(dropzoneElement, { dataTransfer: { files: [file], types: ['Files'] } }); + + expect(await screen.findByText('File chosen: example.tar.gz')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.queryByTestId('dropzone')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/import-page/file-section/FileSection.tsx b/src/import-page/file-section/FileSection.tsx new file mode 100644 index 000000000..ac1a51a4a --- /dev/null +++ b/src/import-page/file-section/FileSection.tsx @@ -0,0 +1,43 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Card, Dropzone } from '@openedx/paragon'; + +import { IMPORT_STAGES } from '../data/constants'; +import messages from './messages'; +import { useCourseImportContext } from '../CourseImportContext'; + +const FileSection = () => { + const intl = useIntl(); + + const { + importTriggered, + currentStage, + fileName, + anyRequestFailed, + handleOnProcessUpload, + } = useCourseImportContext(); + + const isShowedDropzone = !importTriggered || currentStage === IMPORT_STAGES.SUCCESS || anyRequestFailed; + + return ( + + + + {isShowedDropzone + && ( + + )} + + + ); +}; + +export default FileSection; diff --git a/src/import-page/import-sidebar/ImportSidebar.test.jsx b/src/import-page/import-sidebar/ImportSidebar.test.jsx deleted file mode 100644 index d80357f2b..000000000 --- a/src/import-page/import-sidebar/ImportSidebar.test.jsx +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-check -import { initializeMocks, render } from '../../testUtils'; -import messages from './messages'; -import ImportSidebar from './ImportSidebar'; - -const courseId = 'course-123'; - -describe('', () => { - beforeEach(() => { - initializeMocks(); - }); - it('render sidebar correctly', () => { - const { getByText } = render(); - expect(getByText(messages.title1.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.importedContentHeading.defaultMessage)).toBeInTheDocument(); - }); -}); diff --git a/src/import-page/import-sidebar/ImportSidebar.test.tsx b/src/import-page/import-sidebar/ImportSidebar.test.tsx new file mode 100644 index 000000000..0e2904633 --- /dev/null +++ b/src/import-page/import-sidebar/ImportSidebar.test.tsx @@ -0,0 +1,22 @@ +import { initializeMocks, render, screen } from '@src/testUtils'; +import { CourseAuthoringProvider } from '@src/CourseAuthoringContext'; +import messages from './messages'; +import ImportSidebar from './ImportSidebar'; + +const courseId = 'course-123'; + +describe('', () => { + beforeEach(() => { + initializeMocks(); + }); + + it('render sidebar correctly', () => { + render( + + + , + ); + expect(screen.getByText(messages.title1.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.importedContentHeading.defaultMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/import-page/import-sidebar/ImportSidebar.jsx b/src/import-page/import-sidebar/ImportSidebar.tsx similarity index 86% rename from src/import-page/import-sidebar/ImportSidebar.jsx rename to src/import-page/import-sidebar/ImportSidebar.tsx index 4450ff2bd..5ec129d44 100644 --- a/src/import-page/import-sidebar/ImportSidebar.jsx +++ b/src/import-page/import-sidebar/ImportSidebar.tsx @@ -1,17 +1,15 @@ -import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; -import PropTypes from 'prop-types'; import { Hyperlink } from '@openedx/paragon'; import { getConfig } from '@edx/frontend-platform'; +import { HelpSidebar } from '@src/generic/help-sidebar'; +import { useHelpUrls } from '@src/help-urls/hooks'; +import { useCourseAuthoringContext } from '@src/CourseAuthoringContext'; -import { HelpSidebar } from '../../generic/help-sidebar'; -import { useHelpUrls } from '../../help-urls/hooks'; import messages from './messages'; -const ImportSidebar = ({ - courseId, -}) => { +const ImportSidebar = () => { const intl = useIntl(); + const { courseId } = useCourseAuthoringContext(); const { importCourse: importLearnMoreUrl } = useHelpUrls(['importCourse']); return ( @@ -40,7 +38,7 @@ const ImportSidebar = ({
{intl.formatMessage(messages.learnMoreButtonTitle)} @@ -49,8 +47,4 @@ const ImportSidebar = ({ ); }; -ImportSidebar.propTypes = { - courseId: PropTypes.string.isRequired, -}; - export default ImportSidebar; diff --git a/src/import-page/import-stepper/ImportStepper.test.jsx b/src/import-page/import-stepper/ImportStepper.test.jsx deleted file mode 100644 index eaf466da4..000000000 --- a/src/import-page/import-stepper/ImportStepper.test.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { initializeMockApp } from '@edx/frontend-platform'; -import { AppProvider } from '@edx/frontend-platform/react'; - -import initializeStore from '../../store'; -import messages from './messages'; -import ImportStepper from './ImportStepper'; - -const courseId = 'course-123'; -let store; - -const RootWrapper = () => ( - - - - - -); - -describe('', () => { - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - store = initializeStore(); - }); - it('render stepper correctly', () => { - const { getByText } = render(); - expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument(); - }); -}); diff --git a/src/import-page/import-stepper/ImportStepper.test.tsx b/src/import-page/import-stepper/ImportStepper.test.tsx new file mode 100644 index 000000000..a41926902 --- /dev/null +++ b/src/import-page/import-stepper/ImportStepper.test.tsx @@ -0,0 +1,24 @@ +import { render, initializeMocks, screen } from '@src/testUtils'; +import { CourseAuthoringProvider } from '@src/CourseAuthoringContext'; +import messages from './messages'; +import ImportStepper from './ImportStepper'; +import { CourseImportProvider } from '../CourseImportContext'; + +const renderComponent = () => render( + + + + + , +); + +describe('', () => { + beforeEach(() => { + initializeMocks(); + }); + + it('render stepper correctly', () => { + renderComponent(); + expect(screen.getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/import-page/import-stepper/ImportStepper.jsx b/src/import-page/import-stepper/ImportStepper.tsx similarity index 54% rename from src/import-page/import-stepper/ImportStepper.jsx rename to src/import-page/import-stepper/ImportStepper.tsx index 3587f3559..fe72a3a36 100644 --- a/src/import-page/import-stepper/ImportStepper.jsx +++ b/src/import-page/import-stepper/ImportStepper.tsx @@ -1,50 +1,30 @@ -import React, { useEffect } from 'react'; -import { - FormattedDate, - useIntl, -} from '@edx/frontend-platform/i18n'; -import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; +import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n'; import { Button } from '@openedx/paragon'; import { getConfig } from '@edx/frontend-platform'; -import { RequestStatus } from '../../data/constants'; -import CourseStepper from '../../generic/course-stepper'; +import CourseStepper from '@src/generic/course-stepper'; +import { useCourseAuthoringContext } from '@src/CourseAuthoringContext'; + import { IMPORT_STAGES } from '../data/constants'; -import { fetchImportStatus } from '../data/thunks'; -import { - getCurrentStage, getError, getFileName, getLoadingStatus, getProgress, getSavingStatus, getSuccessDate, -} from '../data/selectors'; import messages from './messages'; +import { useCourseImportContext } from '../CourseImportContext'; -const ImportStepper = ({ courseId }) => { +const ImportStepper = () => { const intl = useIntl(); - const currentStage = useSelector(getCurrentStage); - const fileName = useSelector(getFileName); - const { hasError, message: errorMessage } = useSelector(getError); - const progress = useSelector(getProgress); - const dispatch = useDispatch(); - const loadingStatus = useSelector(getLoadingStatus); - const savingStatus = useSelector(getSavingStatus); - const successDate = useSelector(getSuccessDate); - const isStopFetching = currentStage === IMPORT_STAGES.SUCCESS - || loadingStatus === RequestStatus.FAILED - || savingStatus === RequestStatus.FAILED - || hasError; - const formattedErrorMessage = hasError ? errorMessage || intl.formatMessage(messages.defaultErrorMessage) : ''; - useEffect(() => { - const id = setInterval(() => { - if (isStopFetching) { - clearInterval(id); - } else if (fileName) { - dispatch(fetchImportStatus(courseId, fileName)); - } - }, 3000); - return () => clearInterval(id); - }); + const { courseId } = useCourseAuthoringContext(); + const { + progress, + currentStage, + formattedErrorMessage, + anyRequestFailed, + successDate, + } = useCourseImportContext(); - let successTitle = intl.formatMessage(messages.stepperSuccessTitle); + const handleRedirectCourseOutline = () => window.location.replace(`${getConfig().STUDIO_BASE_URL}/course/${courseId}`); + + const successTitle = intl.formatMessage(messages.stepperSuccessTitle); + let successTitleComponent; const localizedSuccessDate = successDate ? ( { /> ) : null; if (localizedSuccessDate && currentStage === IMPORT_STAGES.SUCCESS) { - const successWithDate = ( + successTitleComponent = ( <> {successTitle} ({localizedSuccessDate}) ); - successTitle = successWithDate; } - const handleRedirectCourseOutline = () => window.location.replace(`${getConfig().STUDIO_BASE_URL}/course/${courseId}`); - const steps = [ { title: intl.formatMessage(messages.stepperUploadingTitle), @@ -87,6 +64,7 @@ const ImportStepper = ({ courseId }) => { title: successTitle, description: intl.formatMessage(messages.stepperSuccessDescription), key: IMPORT_STAGES.SUCCESS, + titleComponent: successTitleComponent, }, ]; @@ -94,11 +72,10 @@ const ImportStepper = ({ courseId }) => {

{intl.formatMessage(messages.stepperHeaderTitle)}

{currentStage === IMPORT_STAGES.SUCCESS && ( @@ -108,8 +85,4 @@ const ImportStepper = ({ courseId }) => { ); }; -ImportStepper.propTypes = { - courseId: PropTypes.string.isRequired, -}; - export default ImportStepper; diff --git a/src/import-page/import-stepper/messages.ts b/src/import-page/import-stepper/messages.ts index c31b66823..a1aef725d 100644 --- a/src/import-page/import-stepper/messages.ts +++ b/src/import-page/import-stepper/messages.ts @@ -45,10 +45,6 @@ const messages = defineMessages({ id: 'course-authoring.import.stepper.button.outline', defaultMessage: 'View updated outline', }, - defaultErrorMessage: { - id: 'course-authoring.import.stepper.error.default', - defaultMessage: 'Error importing course', - }, stepperHeaderTitle: { id: 'course-authoring.import.stepper.header.title', defaultMessage: 'Course import status', diff --git a/src/import-page/messages.ts b/src/import-page/messages.ts index 7b7c21371..87693e23b 100644 --- a/src/import-page/messages.ts +++ b/src/import-page/messages.ts @@ -25,6 +25,10 @@ const messages = defineMessages({ id: 'course-authoring.import.description3', defaultMessage: 'The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don\'t make important changes to your course until the import operation has completed.', }, + defaultErrorMessage: { + id: 'course-authoring.import.stepper.error.default', + defaultMessage: 'Error importing course', + }, }); export default messages; diff --git a/src/import-page/utils.test.ts b/src/import-page/utils.test.ts index 48ed6e108..603fb851b 100644 --- a/src/import-page/utils.test.ts +++ b/src/import-page/utils.test.ts @@ -15,13 +15,12 @@ describe('setImportCookie', () => { it('should set the import cookie with the provided data', () => { const cookiesSetMock = jest.spyOn(Cookies.prototype, 'set'); const date = moment('2023-07-24').valueOf(); - const completed = true; const fileName = 'testFileName.test'; - setImportCookie(date, completed, fileName); + setImportCookie(date, fileName); expect(cookiesSetMock).toHaveBeenCalledWith( LAST_IMPORT_COOKIE_NAME, - { date, completed, fileName }, + { date, fileName }, { path: '/some-path' }, ); diff --git a/src/import-page/utils.ts b/src/import-page/utils.ts index ee0d55a52..b29d32caf 100644 --- a/src/import-page/utils.ts +++ b/src/import-page/utils.ts @@ -6,10 +6,9 @@ import { LAST_IMPORT_COOKIE_NAME } from './data/constants'; * Sets an import-related cookie with the provided information. * * @param date - Date of import (unix timestamp). - * @param {boolean} completed - Indicates if import was completed successfully. * @param {string} fileName - File name. */ -export const setImportCookie = (date: number, completed: boolean, fileName: string): void => { +export const setImportCookie = (date: number, fileName: string): void => { const cookies = new Cookies(); - cookies.set(LAST_IMPORT_COOKIE_NAME, { date, completed, fileName }, { path: window.location.pathname }); + cookies.set(LAST_IMPORT_COOKIE_NAME, { date, fileName }, { path: window.location.pathname }); }; diff --git a/src/store.ts b/src/store.ts index f3b265baa..fdb2c11cb 100644 --- a/src/store.ts +++ b/src/store.ts @@ -20,7 +20,6 @@ import { reducer as processingNotificationReducer } from './generic/processing-n import { reducer as courseExportReducer } from './export-page/data/slice'; import { reducer as courseOptimizerReducer } from './optimizer-page/data/slice'; import { reducer as genericReducer } from './generic/data/slice'; -import { reducer as courseImportReducer } from './import-page/data/slice'; import { reducer as videosReducer } from './files-and-videos/videos-page/data/slice'; import { reducer as courseOutlineReducer } from './course-outline/data/slice'; import { reducer as courseUnitReducer } from './course-unit/data/slice'; @@ -49,7 +48,6 @@ export interface DeprecatedReduxState { courseExport: Record; courseOptimizer: Record; generic: Record; - courseImport: Record; videos: Record; courseOutline: Record; courseUnit: Record; @@ -82,7 +80,6 @@ export default function initializeStore(preloadedState: Partial