Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/library-authoring/LibraryLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ROUTES } from './routes';
import { LibrarySectionPage, LibrarySubsectionPage } from './section-subsections';
import { LibraryUnitPage } from './units';
import { LibraryTeamModal } from './library-team';
import { ImportStepperPage } from './import-course/stepper/ImportStepperPage';

const LibraryLayoutWrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
const {
Expand Down Expand Up @@ -97,6 +98,10 @@ const LibraryLayout = () => (
path={ROUTES.IMPORT}
Component={CourseImportHomePage}
/>
<Route
path={ROUTES.IMPORT_COURSE}
Component={ImportStepperPage}
/>
</Route>
</Routes>
);
Expand Down
14 changes: 14 additions & 0 deletions src/library-authoring/data/api.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,3 +1133,17 @@ mockGetCourseImports.applyMock = () => jest.spyOn(
api,
'getCourseImports',
).mockImplementation(mockGetCourseImports);

export const mockGetMigrationInfo = {
applyMock: () => jest.spyOn(api, 'getMigrationInfo').mockResolvedValue(
camelCaseObject({
'course-v1:HarvardX+123+2023': [{
sourceKey: 'course-v1:HarvardX+123+2023',
targetCollectionKey: 'ltc:org:coll-1',
targetCollectionTitle: 'Collection 1',
targetKey: mockContentLibrary.libraryId,
targetTitle: 'Library 1',
}],
}),
),
};
21 changes: 21 additions & 0 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,3 +809,24 @@ export async function getCourseImports(libraryId: string): Promise<CourseImport[
const { data } = await getAuthenticatedHttpClient().get(getCourseImportsApiUrl(libraryId));
return camelCaseObject(data);
}

export interface MigrationInfo {
sourceKey: string;
targetCollectionKey: string;
targetCollectionTitle: string;
targetKey: string;
targetTitle: string;
}

/**
* Get the migration info data for a list of source keys
*/
export async function getMigrationInfo(sourceKeys: string[]): Promise<Record<string, MigrationInfo[]>> {
const client = getAuthenticatedHttpClient();

const params = new URLSearchParams();
sourceKeys.forEach(key => params.append('source_keys', key));

const { data } = await client.get(`${getApiBaseUrl()}/api/modulestore_migrator/v1/migration_info/`, { params });
return camelCaseObject(data);
}
15 changes: 15 additions & 0 deletions src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ export const libraryAuthoringQueryKeys = {
...libraryAuthoringQueryKeys.contentLibrary(libraryId),
'courseImports',
],
migrationInfo: (sourceKeys: string[]) => [
...libraryAuthoringQueryKeys.all,
'migrationInfo',
...sourceKeys,
],
};

export const xblockQueryKeys = {
Expand Down Expand Up @@ -965,3 +970,13 @@ export const useCourseImports = (libraryId: string) => (
queryFn: () => api.getCourseImports(libraryId),
})
);

/**
* Returns the migration info of a given source list
*/
export const useMigrationInfo = (sourcesKeys: string[]) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.migrationInfo(sourcesKeys),
queryFn: () => api.getMigrationInfo(sourcesKeys),
})
);
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const render = (libraryId: string) => (
{children}
</LibraryProvider>
),
path: '/libraries/:libraryId/import-course',
path: '/libraries/:libraryId/import',
params: { libraryId },
},
)
Expand Down
24 changes: 19 additions & 5 deletions src/library-authoring/import-course/CourseImportHomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import {
Button,
Card,
Expand All @@ -6,8 +8,7 @@ import {
Stack,
} from '@openedx/paragon';
import { Add } from '@openedx/paragon/icons';
import { Helmet } from 'react-helmet';

import { getConfig } from '@edx/frontend-platform';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import Loading from '@src/generic/Loading';
import SubHeader from '@src/generic/sub-header/SubHeader';
Expand All @@ -19,14 +20,26 @@ import { HelpSidebar } from './HelpSidebar';
import { ImportedCourseCard } from './ImportedCourseCard';
import messages from './messages';

const ImportCourseButton = () => {
const navigate = useNavigate();

if (getConfig().ENABLE_COURSE_IMPORT_IN_LIBRARY === 'true') {
return (
<Button iconBefore={Add} onClick={() => navigate('courses')}>
<FormattedMessage {...messages.importCourseButton} />
</Button>
);
}

return null;
};

const EmptyState = () => (
<Container size="md" className="py-6">
<Card>
<Stack direction="horizontal" gap={3} className="my-6 justify-content-center">
<FormattedMessage {...messages.emptyStateText} />
<Button iconBefore={Add} disabled>
<FormattedMessage {...messages.emptyStateButtonText} />
</Button>
<ImportCourseButton />
</Stack>
</Card>
</Container>
Expand Down Expand Up @@ -64,6 +77,7 @@ export const CourseImportHomePage = () => {
title={intl.formatMessage(messages.pageTitle)}
subtitle={intl.formatMessage(messages.pageSubtitle)}
hideBorder
headerActions={<ImportCourseButton />}
/>
</div>
<Layout xs={[{ span: 9 }, { span: 3 }]}>
Expand Down
91 changes: 91 additions & 0 deletions src/library-authoring/import-course/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,97 @@ const messages = defineMessages({
+ '<p>For additional details you can review the Library Import documentation.</p>',
description: 'Body of the second question in the Help & Support sidebar',
},
importCourseStepperTitle: {
id: 'course-authoring.library-authoring.import-course.stepper.title',
defaultMessage: 'Import Course to Library',
description: 'Title for the modal to import a course into a library.',
},
importCourseButton: {
id: 'course-authoring.library-authoring.import-course.button.text',
defaultMessage: 'Import Course',
description: 'Label of the button to open the modal to import a course into a library.',
},
importCourseSelectCourseStep: {
id: 'course-authoring.library-authoring.import-course.stepper.select-course.title',
defaultMessage: 'Select Course',
description: 'Title for the step to select course in the modal to import a course into a library.',
},
importCourseReviewDetailsStep: {
id: 'course-authoring.library-authoring.import-course.stepper.review-details.title',
defaultMessage: 'Review Import Details',
description: 'Title for the step to review import details in the modal to import a course into a library.',
},
importCourseCalcel: {
id: 'course-authoring.library-authoring.import-course.stepper.cancel.text',
defaultMessage: 'Cancel',
description: 'Label of the button to cancel the course import.',
},
importCourseNext: {
id: 'course-authoring.library-authoring.import-course.stepper.next.text',
defaultMessage: 'Next step',
description: 'Label of the button go to the next step in the course import modal.',
},
importCourseBack: {
id: 'course-authoring.library-authoring.import-course.stepper.back.text',
defaultMessage: 'Back',
description: 'Label of the button to go to the previous step in the course import modal.',
},
importCourseInProgressStatusTitle: {
id: 'course-authoring.library-authoring.import-course.review-details.in-progress.title',
defaultMessage: 'Import Analysis in Progress',
description: 'Titile for the info card with the in-progress status in the course import modal.',
},
importCourseInProgressStatusBody: {
id: 'course-authoring.library-authoring.import-course.review-details.in-progress.body',
defaultMessage: '{courseName} is being analyzed for review prior to import. For large courses, this may take some time.'
+ ' Please remain on this page.',
description: 'Body of the info card with the in-progress status in the course import modal.',
},
importCourseAnalysisSummary: {
id: 'course-authoring.library-authoring.import-course.review-details.analysis-symmary.title',
defaultMessage: 'Analysis Summary',
description: 'Title of the card for the analysis summary of a imported course.',
},
importCourseTotalBlocks: {
id: 'course-authoring.library-authoring.import-course.review-details.analysis-symmary.total-blocks',
defaultMessage: 'Total Blocks',
description: 'Label title for the total blocks in the analysis summary of a imported course.',
},
importCourseSections: {
id: 'course-authoring.library-authoring.import-course.review-details.analysis-symmary.sections',
defaultMessage: 'Sections',
description: 'Label title for the number of sections in the analysis summary of a imported course.',
},
importCourseSubsections: {
id: 'course-authoring.library-authoring.import-course.review-details.analysis-symmary.subsections',
defaultMessage: 'Subsections',
description: 'Label title for the number of subsections in the analysis summary of a imported course.',
},
importCourseUnits: {
id: 'course-authoring.library-authoring.import-course.review-details.analysis-symmary.units',
defaultMessage: 'Units',
description: 'Label title for the number of units in the analysis summary of a imported course.',
},
importCourseComponents: {
id: 'course-authoring.library-authoring.import-course.review-details.analysis-symmary.components',
defaultMessage: 'Components',
description: 'Label title for the number of components in the analysis summary of a imported course.',
},
importCourseDetailsTitle: {
id: 'course-authoring.library-authoring.import-course.review-details.import-details.title',
defaultMessage: 'Import Details',
description: 'Title of the card for the import details of a imported course.',
},
importCourseDetailsLoadingBody: {
id: 'course-authoring.library-authoring.import-course.review-details.import-details.loading.body',
defaultMessage: 'The selected course is being analyzed for import and review',
description: 'Body of the card in loading state for the import details of a imported course.',
},
previouslyImported: {
id: 'course-authoring.library-authoring.import-course.course-list.card.previously-imported.text',
defaultMessage: 'Previously Imported',
description: 'Chip that indicates that the course has been previously imported.',
},
});

export default messages;
Loading