Skip to content

Commit 60e1e60

Browse files
committed
feat: Basics for Success/Failed import details page
1 parent 650bb62 commit 60e1e60

10 files changed

Lines changed: 303 additions & 15 deletions

File tree

src/data/api.mocks.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mockGetMigrationStatus.migrationStatusData = {
2929
artifacts: [],
3030
parameters: [
3131
{
32+
id: 1,
3233
source: 'legacy-lib-1',
3334
target: 'lib',
3435
compositionLevel: 'component',
@@ -37,6 +38,7 @@ mockGetMigrationStatus.migrationStatusData = {
3738
targetCollectionSlug: 'coll-1',
3839
forwardSourceToTarget: true,
3940
isFailed: false,
41+
targetCollection: null,
4042
},
4143
],
4244
} as api.MigrateTaskStatusData;
@@ -53,6 +55,7 @@ mockGetMigrationStatus.migrationStatusFailedData = {
5355
artifacts: [],
5456
parameters: [
5557
{
58+
id: 1,
5659
source: 'legacy-lib-1',
5760
target: 'lib',
5861
compositionLevel: 'component',
@@ -61,6 +64,7 @@ mockGetMigrationStatus.migrationStatusFailedData = {
6164
targetCollectionSlug: 'coll-1',
6265
forwardSourceToTarget: true,
6366
isFailed: true,
67+
targetCollection: null,
6468
},
6569
],
6670
} as api.MigrateTaskStatusData;
@@ -77,6 +81,7 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
7781
artifacts: [],
7882
parameters: [
7983
{
84+
id: 1,
8085
source: 'legacy-lib-1',
8186
target: 'lib',
8287
compositionLevel: 'component',
@@ -85,8 +90,10 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
8590
targetCollectionSlug: 'coll-1',
8691
forwardSourceToTarget: true,
8792
isFailed: true,
93+
targetCollection: null,
8894
},
8995
{
96+
id: 2,
9097
source: 'legacy-lib-2',
9198
target: 'lib',
9299
compositionLevel: 'component',
@@ -95,6 +102,7 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
95102
targetCollectionSlug: 'coll-1',
96103
forwardSourceToTarget: true,
97104
isFailed: true,
105+
targetCollection: null,
98106
},
99107
],
100108
} as api.MigrateTaskStatusData;
@@ -111,6 +119,7 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
111119
artifacts: [],
112120
parameters: [
113121
{
122+
id: 1,
114123
source: 'legacy-lib-1',
115124
target: 'lib',
116125
compositionLevel: 'component',
@@ -119,8 +128,10 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
119128
targetCollectionSlug: 'coll-1',
120129
forwardSourceToTarget: true,
121130
isFailed: true,
131+
targetCollection: null,
122132
},
123133
{
134+
id: 2,
124135
source: 'legacy-lib-2',
125136
target: 'lib',
126137
compositionLevel: 'component',
@@ -129,6 +140,7 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
129140
targetCollectionSlug: 'coll-1',
130141
forwardSourceToTarget: true,
131142
isFailed: false,
143+
targetCollection: null,
132144
},
133145
],
134146
} as api.MigrateTaskStatusData;

src/data/api.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ export async function getWaffleFlags(courseId?: string): Promise<WaffleFlagsStat
8383
return normalizeCourseDetail(data);
8484
}
8585

86-
export interface MigrateArtifacts {
86+
export interface MigrateParameters {
87+
id: number;
8788
source: string;
8889
target: string;
8990
compositionLevel: string;
@@ -92,6 +93,10 @@ export interface MigrateArtifacts {
9293
targetCollectionSlug: string;
9394
forwardSourceToTarget: boolean;
9495
isFailed: boolean;
96+
targetCollection: {
97+
key: string;
98+
title: string;
99+
} | null;
95100
}
96101

97102
export interface MigrateTaskStatusData {
@@ -104,7 +109,7 @@ export interface MigrateTaskStatusData {
104109
modified: string;
105110
artifacts: string[];
106111
uuid: string;
107-
parameters: MigrateArtifacts[];
112+
parameters: MigrateParameters[];
108113
}
109114

110115
export interface BulkMigrateRequestData {

src/library-authoring/LibraryLayout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { LibrarySectionPage, LibrarySubsectionPage } from './section-subsections
2121
import { LibraryUnitPage } from './units';
2222
import { LibraryTeamModal } from './library-team';
2323
import { ImportStepperPage } from './import-course/stepper/ImportStepperPage';
24+
import { ImportDetailsPage } from './import-course/ImportDetailsPage';
2425

2526
const LibraryLayoutWrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
2627
const {
@@ -102,6 +103,10 @@ const LibraryLayout = () => (
102103
path={ROUTES.IMPORT_COURSE}
103104
Component={ImportStepperPage}
104105
/>
106+
<Route
107+
path={ROUTES.IMPORT_COURSE_DETAILS}
108+
Component={ImportDetailsPage}
109+
/>
105110
</Route>
106111
</Routes>
107112
);

src/library-authoring/data/api.mocks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,7 @@ export async function mockGetCourseImports(libraryId: string): ReturnType<typeof
10911091
mockGetCourseImports.libraryId = mockContentLibrary.libraryId;
10921092
mockGetCourseImports.emptyLibraryId = mockContentLibrary.libraryId2;
10931093
mockGetCourseImports.succeedImport = {
1094+
taskUuid: '1',
10941095
source: {
10951096
key: 'course-v1:edX+DemoX+2025_T1',
10961097
displayName: 'DemoX 2025 T1',
@@ -1100,6 +1101,7 @@ mockGetCourseImports.succeedImport = {
11001101
progress: 1,
11011102
} satisfies api.CourseImport;
11021103
mockGetCourseImports.succeedImportWithCollection = {
1104+
taskUuid: '2',
11031105
source: {
11041106
key: 'course-v1:edX+DemoX+2025_T2',
11051107
displayName: 'DemoX 2025 T2',
@@ -1112,6 +1114,7 @@ mockGetCourseImports.succeedImportWithCollection = {
11121114
progress: 1,
11131115
} satisfies api.CourseImport;
11141116
mockGetCourseImports.failImport = {
1117+
taskUuid: '3',
11151118
source: {
11161119
key: 'course-v1:edX+DemoX+2025_T3',
11171120
displayName: 'DemoX 2025 T3',
@@ -1121,6 +1124,7 @@ mockGetCourseImports.failImport = {
11211124
progress: 0.30,
11221125
} satisfies api.CourseImport;
11231126
mockGetCourseImports.inProgressImport = {
1127+
taskUuid: '4',
11241128
source: {
11251129
key: 'course-v1:edX+DemoX+2025_T4',
11261130
displayName: 'DemoX 2025 T4',

src/library-authoring/data/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ export async function publishContainer(containerId: string) {
790790
}
791791

792792
export interface CourseImport {
793+
taskUuid: string;
793794
source: {
794795
key: string;
795796
displayName: string;
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { useContext } from 'react';
2+
import { Helmet } from 'react-helmet';
3+
import { useNavigate, useParams } from 'react-router';
4+
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
5+
import {
6+
Stack, Container, Alert, Layout, Button,
7+
} from '@openedx/paragon';
8+
9+
import Header from '@src/header';
10+
import { useCourseDetails } from '@src/course-outline/data/apiHooks';
11+
import SubHeader from '@src/generic/sub-header/SubHeader';
12+
import { ArrowForward, CheckCircle, Info } from '@openedx/paragon/icons';
13+
import Loading from '@src/generic/Loading';
14+
import { ToastContext } from '@src/generic/toast-context';
15+
16+
import { useBulkModulestoreMigrate, useModulestoreMigrationStatus } from '@src/data/apiHooks';
17+
import messages from './messages';
18+
import { SummaryCard } from './stepper/SummaryCard';
19+
import { HelpSidebar } from './HelpSidebar';
20+
import { useLibraryContext } from '../common/context/LibraryContext';
21+
22+
export const ImportDetailsPage = () => {
23+
const intl = useIntl();
24+
const navigate = useNavigate();
25+
const { libraryId, libraryData, readOnly } = useLibraryContext();
26+
const { courseId, migrationTaskId } = useParams();
27+
const { showToast } = useContext(ToastContext);
28+
// Using bulk migrate as it allows us to create collection automatically
29+
// TODO: Modify single migration API to allow create collection
30+
const migrate = useBulkModulestoreMigrate();
31+
32+
if (libraryId === undefined) {
33+
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
34+
throw new Error('Error: route is missing libraryId.');
35+
}
36+
if (migrationTaskId === undefined) {
37+
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
38+
throw new Error('Error: route is missing migrationId.');
39+
}
40+
41+
const {
42+
data: courseDetails,
43+
isPending: isPendingCourseDetails,
44+
} = useCourseDetails(courseId);
45+
const {
46+
data: migrationStatusData,
47+
isPaused: isPendingMigrationStatusData,
48+
} = useModulestoreMigrationStatus(migrationTaskId);
49+
// Get the first migration, because the courses are imported one by one
50+
const courseImportDetails = migrationStatusData?.parameters?.[0];
51+
52+
const isPending = isPendingCourseDetails || isPendingMigrationStatusData;
53+
54+
const collectionLink = () => {
55+
let libUrl = `/library/${libraryId}`;
56+
if (courseImportDetails?.targetCollection?.key) {
57+
libUrl += `/collection/${courseImportDetails.targetCollection.key}`;
58+
}
59+
return libUrl;
60+
};
61+
62+
const handleImportCourse = async () => {
63+
if (!courseId || !courseImportDetails || !courseDetails || !migrationStatusData) {
64+
return;
65+
}
66+
67+
try {
68+
await migrate.mutateAsync({
69+
sources: [courseId!],
70+
target: libraryId,
71+
createCollections: true,
72+
repeatHandlingStrategy: 'fork',
73+
compositionLevel: 'section',
74+
});
75+
showToast(intl.formatMessage(messages.importCourseCompleteToastMessage, {
76+
courseName: courseDetails.title,
77+
}));
78+
navigate(`${courseImportDetails.source}/${migrationStatusData.uuid}`);
79+
} catch (error) {
80+
showToast(intl.formatMessage(messages.importCourseCompleteFailedToastMessage, {
81+
courseName: courseDetails.title,
82+
}));
83+
}
84+
};
85+
86+
const renderBody = () => {
87+
if (isPending) {
88+
return <Loading />;
89+
}
90+
91+
if (migrationStatusData?.state === 'Succeeded') {
92+
return (
93+
<Stack gap={3}>
94+
<Alert variant="success" icon={CheckCircle}>
95+
<Alert.Heading>
96+
<FormattedMessage {...messages.importSuccessfulAlertTitle} />
97+
</Alert.Heading>
98+
<p>
99+
<FormattedMessage
100+
{...messages.importSuccessfulAlertBody}
101+
values={{
102+
courseName: courseDetails?.title,
103+
collectionName: courseImportDetails?.targetCollection?.title,
104+
}}
105+
/>
106+
</p>
107+
</Alert>
108+
<h4><FormattedMessage {...messages.importSummaryTitle} /></h4>
109+
<SummaryCard isPending />
110+
<p>
111+
<FormattedMessage
112+
{...messages.importSuccessfulBody}
113+
values={{
114+
courseName: courseDetails?.title,
115+
}}
116+
/>
117+
</p>
118+
<div className="w-100 d-flex justify-content-end">
119+
<Button
120+
variant="outline-primary"
121+
iconAfter={ArrowForward}
122+
onClick={() => navigate(collectionLink())}
123+
>
124+
<FormattedMessage {...messages.viewImportedContentButton} />
125+
</Button>
126+
</div>
127+
</Stack>
128+
);
129+
} if (migrationStatusData?.state === 'Failed') {
130+
return (
131+
<Stack gap={3}>
132+
<Alert variant="danger" icon={Info}>
133+
<Alert.Heading>
134+
<FormattedMessage {...messages.importFailedAlertTitle} />
135+
</Alert.Heading>
136+
<p>
137+
<FormattedMessage
138+
{...messages.importFailedAlertBody}
139+
values={{
140+
courseName: courseDetails?.title,
141+
}}
142+
/>
143+
</p>
144+
</Alert>
145+
<h4><FormattedMessage {...messages.importFailedDetailsSectionTitle} /></h4>
146+
<p>
147+
<FormattedMessage {...messages.importFailedDetailsSectionBody} />
148+
</p>
149+
<div className="w-100 d-flex justify-content-end">
150+
<Button
151+
variant="outline-primary"
152+
iconAfter={ArrowForward}
153+
onClick={handleImportCourse}
154+
>
155+
<FormattedMessage {...messages.importFailedRetryImportButton} />
156+
</Button>
157+
</div>
158+
</Stack>
159+
);
160+
}
161+
return (
162+
// In Progress
163+
<Stack gap={2}>
164+
In progress
165+
</Stack>
166+
);
167+
};
168+
169+
return (
170+
<div className="d-flex">
171+
<div className="flex-grow-1">
172+
<Helmet>
173+
<title>{courseDetails?.title ?? ''} | {process.env.SITE_NAME}</title>
174+
</Helmet>
175+
<Header
176+
number={libraryData?.slug}
177+
title={libraryData?.title}
178+
org={libraryData?.org}
179+
contextId={libraryId}
180+
isLibrary
181+
readOnly={readOnly}
182+
containerProps={{
183+
size: undefined,
184+
}}
185+
/>
186+
<Container className="mt-4 mb-5">
187+
<div className="px-4 bg-light-200 border-bottom">
188+
<SubHeader
189+
title={intl.formatMessage(messages.importDetailsTitle)}
190+
hideBorder
191+
/>
192+
</div>
193+
<Layout xs={[{ span: 9 }, { span: 3 }]}>
194+
<Layout.Element>
195+
<div className="mt-4 px-4">
196+
{renderBody()}
197+
</div>
198+
</Layout.Element>
199+
<Layout.Element>
200+
<HelpSidebar />
201+
</Layout.Element>
202+
</Layout>
203+
</Container>
204+
</div>
205+
</div>
206+
);
207+
};

0 commit comments

Comments
 (0)