Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
ENABLE_TAGGING_TAXONOMY_PAGES=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_COURSE_IMPORT_IN_LIBRARY=false
BBB_LEARN_MORE_URL=''
HOTJAR_APP_ID=''
HOTJAR_VERSION=6
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ENABLE_UNIT_PAGE=false
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_COURSE_IMPORT_IN_LIBRARY=true
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
Expand Down
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ ENABLE_UNIT_PAGE=true
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_COURSE_IMPORT_IN_LIBRARY=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
INVITE_STUDENTS_EMAIL_TO="[email protected]"
Expand Down
18 changes: 16 additions & 2 deletions src/course-outline/data/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { XBlock } from '@src/data/types';
import { CourseOutline } from './types';
import { CourseOutline, CourseDetails } from './types';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;

export const getCourseOutlineIndexApiUrl = (
courseId: string,
) => `${getApiBaseUrl()}/api/contentstore/v1/course_index/${courseId}`;

export const getCourseDetailsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/course_details/${courseId}`;

export const getCourseBestPracticesApiUrl = ({
courseId,
excludeGraded,
Expand Down Expand Up @@ -46,7 +48,7 @@ export const createDiscussionsTopicsUrl = (courseId: string) => `${getApiBaseUrl
/**
* Get course outline index.
* @param {string} courseId
* @returns {Promise<courseOutline>}
* @returns {Promise<CourseOutline>}
*/
export async function getCourseOutlineIndex(courseId: string): Promise<CourseOutline> {
const { data } = await getAuthenticatedHttpClient()
Expand All @@ -55,6 +57,18 @@ export async function getCourseOutlineIndex(courseId: string): Promise<CourseOut
return camelCaseObject(data);
}

/**
* Get course details.
* @param {string} courseId
* @returns {Promise<CourseDetails>}
*/
export async function getCourseDetails(courseId: string): Promise<CourseDetails> {
const { data } = await getAuthenticatedHttpClient()
.get(getCourseDetailsApiUrl(courseId));

return camelCaseObject(data);
}

/**
*
* @param courseId
Expand Down
13 changes: 10 additions & 3 deletions src/course-outline/data/apiHooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { skipToken, useMutation, useQuery } from '@tanstack/react-query';
import { createCourseXblock } from '@src/course-unit/data/api';
import { getCourseItem } from './api';
import { getCourseDetails, getCourseItem } from './api';

export const courseOutlineQueryKeys = {
all: ['courseOutline'],
Expand All @@ -9,7 +9,7 @@ export const courseOutlineQueryKeys = {
*/
contentLibrary: (courseId?: string) => [...courseOutlineQueryKeys.all, courseId],
courseItemId: (itemId?: string) => [...courseOutlineQueryKeys.all, itemId],

courseDetails: (courseId?: string) => [...courseOutlineQueryKeys.all, courseId, 'details'],
};

/**
Expand All @@ -33,3 +33,10 @@ export const useCourseItemData = (itemId?: string, enabled: boolean = true) => (
enabled: enabled && itemId !== undefined,
})
);

export const useCourseDetails = (courseId?: string) => (
useQuery({
queryKey: courseOutlineQueryKeys.courseDetails(courseId),
queryFn: courseId ? () => getCourseDetails(courseId) : skipToken,
})
);
9 changes: 9 additions & 0 deletions src/course-outline/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export interface CourseOutline {
rerunNotificationId: null;
}

// TODO: This interface has only basic data, all the rest needs to be added.
export interface CourseDetails {
courseId: string;
title: string;
subtitle?: string;
org: string;
description?: string;
}

export interface CourseOutlineState {
loadingStatus: {
outlineIndexLoadingStatus: string;
Expand Down
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ initialize({
ENABLE_ASSETS_PAGE: process.env.ENABLE_ASSETS_PAGE || 'false',
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN || 'false',
ENABLE_CERTIFICATE_PAGE: process.env.ENABLE_CERTIFICATE_PAGE || 'false',
ENABLE_COURSE_IMPORT_IN_LIBRARY: process.env.ENABLE_COURSE_IMPORT_IN_LIBRARY || 'false',
ENABLE_TAGGING_TAXONOMY_PAGES: process.env.ENABLE_TAGGING_TAXONOMY_PAGES || 'false',
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
ENABLE_GRADING_METHOD_IN_PROBLEMS: process.env.ENABLE_GRADING_METHOD_IN_PROBLEMS === 'true',
Expand Down
4 changes: 0 additions & 4 deletions src/legacy-libraries-migration/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@

.card-item {
margin: 0 0 16px !important;

&.selected {
box-shadow: 0 0 0 2px var(--pgn-color-primary-700);
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from 'react';
import { Helmet } from 'react-helmet';
import classNames from 'classnames';
import { getConfig } from '@edx/frontend-platform';
import { StudioFooterSlot } from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Expand All @@ -19,6 +20,7 @@ import {
Stack,
Tab,
Tabs,
useToggle,
} from '@openedx/paragon';
import { Add, InfoOutline } from '@openedx/paragon/icons';
import { Link, useLocation, useNavigate } from 'react-router-dom';
Expand Down Expand Up @@ -49,8 +51,10 @@ import { useLibraryContext } from './common/context/LibraryContext';
import { SidebarBodyItemId, useSidebarContext } from './common/context/SidebarContext';
import { allLibraryPageTabs, ContentType, useLibraryRoutes } from './routes';
import messages from './messages';
import tempMessages from './course-import/messages';
import LibraryFilterByPublished from './generic/filter-by-published';
import { libraryQueryPredicate } from './data/apiHooks';
import { ImportStepperModal } from './course-import/ImportStepperModal';

const HeaderActions = () => {
const intl = useIntl();
Expand Down Expand Up @@ -147,6 +151,7 @@ const LibraryAuthoringPage = ({
const params = new URLSearchParams(location.search);
const { showToast } = useContext(ToastContext);
const queryClient = useQueryClient();
const [importModalIsOpen, openImportModal, closeImportModal] = useToggle(false);

// Get migration status every second if applicable
const migrationId = params.get('migration_task');
Expand Down Expand Up @@ -353,6 +358,11 @@ const LibraryAuthoringPage = ({
extraFilter={extraFilter}
overrideTypesFilter={overrideTypesFilter}
>
{getConfig().ENABLE_COURSE_IMPORT_IN_LIBRARY === 'true' && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go for the route, I think we don't need this flag, as we can test it directly using the url.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The env flag is a requirement in the ticket #2524

<Button onClick={openImportModal}>
{intl.formatMessage(tempMessages.importCourseButton)}
</Button>
)}
<SubHeader
title={<SubHeaderTitle title={libraryData.title} />}
subtitle={!componentPickerMode ? intl.formatMessage(messages.headingSubtitle) : undefined}
Expand Down Expand Up @@ -396,6 +406,11 @@ const LibraryAuthoringPage = ({
<LibrarySidebar />
</div>
)}
<ImportStepperModal
isOpen={importModalIsOpen}
onClose={closeImportModal}
libraryKey={libraryId}
/>
</div>
);
};
Expand Down
144 changes: 144 additions & 0 deletions src/library-authoring/course-import/ImportStepperModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import userEvent from '@testing-library/user-event';
import {
initializeMocks,
render,
screen,
fireEvent,
waitFor,
} from '@src/testUtils';
import { initialState } from '@src/studio-home/factories/mockApiResponses';
import { RequestStatus } from '@src/data/constants';
import { type DeprecatedReduxState } from '@src/store';
import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock';
import { mockGetMigrationInfo } from '@src/studio-home/data/api.mocks';
import { getCourseDetailsApiUrl } from '@src/course-outline/data/api';
import { ImportStepperModal } from './ImportStepperModal';

let axiosMock;
mockGetMigrationInfo.applyMock();
type StudioHomeState = DeprecatedReduxState['studioHome'];

const libraryKey = 'lib:org:lib1';
const mockOnClose = jest.fn();
const numPages = 1;
const coursesCount = studioHomeMock.courses.length;

const renderComponent = (studioHomeState: Partial<StudioHomeState> = {}) => {
// Generate a custom initial state based on studioHomeCoursesRequestParams
const customInitialState: Partial<DeprecatedReduxState> = {
...initialState,
studioHome: {
...initialState.studioHome,
studioHomeData: {
courses: studioHomeMock.courses,
numPages,
coursesCount,
},
loadingStatuses: {
...initialState.studioHome.loadingStatuses,
courseLoadingStatus: RequestStatus.SUCCESSFUL,
},
...studioHomeState,
},
};

// Initialize the store with the custom initial state
const newMocks = initializeMocks({ initialState: customInitialState });
const store = newMocks.reduxStore;
axiosMock = newMocks.axiosMock;

return {
...render(
<ImportStepperModal
libraryKey={libraryKey}
onClose={mockOnClose}
isOpen
/>,
),
store,
};
};

describe('<ImportStepperModal />', () => {
it('should render correctly', async () => {
renderComponent();
// Renders the stepper header
expect(await screen.findByText('Select Course')).toBeInTheDocument();
expect(await screen.findByText('Review Import Details')).toBeInTheDocument();

// Renders the course list and previously imported chip
expect(screen.getByText(/managing risk in the information age/i)).toBeInTheDocument();
expect(screen.getByText(/run 0/i)).toBeInTheDocument();
expect(await screen.findByText('Previously Imported')).toBeInTheDocument();

// Renders cancel and next step buttons
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /next step/i })).toBeInTheDocument();
});

it('should cancel the import', async () => {
const user = userEvent.setup();
renderComponent();

const cancelButon = await screen.findByRole('button', { name: /cancel/i });
await user.click(cancelButon);

expect(mockOnClose).toHaveBeenCalled();
});

it('should go to review import details step', async () => {
const user = userEvent.setup();
renderComponent();
axiosMock.onGet(getCourseDetailsApiUrl('course-v1:HarvardX+123+2023')).reply(200, {
courseId: 'course-v1:HarvardX+123+2023',
title: 'Managing Risk in the Information Age',
subtitle: '',
org: 'HarvardX',
description: 'This is a test course',
});

const nextButton = await screen.findByRole('button', { name: /next step/i });
expect(nextButton).toBeDisabled();

// Select a course
const courseCard = screen.getAllByRole('radio')[0];
await fireEvent.click(courseCard);
expect(courseCard).toBeChecked();

// Click next
expect(nextButton).toBeEnabled();
await user.click(nextButton);

await waitFor(async () => expect(await screen.findByText(
/managing risk in the information age is being analyzed for review prior to import/i,
)).toBeInTheDocument());

expect(screen.getByText('Analysis Summary')).toBeInTheDocument();
expect(screen.getByText('Import Details')).toBeInTheDocument();
// The import details is loading
expect(screen.getByText('The selected course is being analyzed for import and review')).toBeInTheDocument();
});

it('the course should remain selected on back', async () => {
const user = userEvent.setup();
renderComponent();

const nextButton = await screen.findByRole('button', { name: /next step/i });
expect(nextButton).toBeDisabled();

// Select a course
const courseCard = screen.getAllByRole('radio')[0];
await fireEvent.click(courseCard);
expect(courseCard).toBeChecked();

// Click next
expect(nextButton).toBeEnabled();
await user.click(nextButton);

const backButton = await screen.getByRole('button', { name: /back/i });
await user.click(backButton);

expect(screen.getByText(/managing risk in the information age/i)).toBeInTheDocument();
expect(courseCard).toBeChecked();
});
});
Loading