Skip to content

Commit 50f4f70

Browse files
authored
feat: Import Course Details Page [FC-0112] (#2664)
Implements all the states for the Import Course Details
1 parent 0796e89 commit 50f4f70

16 files changed

Lines changed: 889 additions & 24 deletions

src/data/api.mocks.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export async function mockGetMigrationStatus(migrationId: string): Promise<api.M
1010
return mockGetMigrationStatus.migrationStatusFailedMultipleData;
1111
case mockGetMigrationStatus.migrationIdOneLibrary:
1212
return mockGetMigrationStatus.migrationStatusFailedOneLibraryData;
13+
case mockGetMigrationStatus.migrationIdLoading:
14+
return new Promise(() => {});
15+
case mockGetMigrationStatus.migrationIdInProgress:
16+
return mockGetMigrationStatus.migrationStatusInProgressData;
1317
default:
1418
/* istanbul ignore next */
1519
throw new Error(`mockGetMigrationStatus: unknown migration ID "${migrationId}"`);
@@ -29,6 +33,7 @@ mockGetMigrationStatus.migrationStatusData = {
2933
artifacts: [],
3034
parameters: [
3135
{
36+
id: 1,
3237
source: 'legacy-lib-1',
3338
target: 'lib',
3439
compositionLevel: 'component',
@@ -37,6 +42,10 @@ mockGetMigrationStatus.migrationStatusData = {
3742
targetCollectionSlug: 'coll-1',
3843
forwardSourceToTarget: true,
3944
isFailed: false,
45+
targetCollection: {
46+
key: 'coll',
47+
title: 'Test Collection',
48+
},
4049
},
4150
],
4251
} as api.MigrateTaskStatusData;
@@ -53,6 +62,7 @@ mockGetMigrationStatus.migrationStatusFailedData = {
5362
artifacts: [],
5463
parameters: [
5564
{
65+
id: 1,
5666
source: 'legacy-lib-1',
5767
target: 'lib',
5868
compositionLevel: 'component',
@@ -61,6 +71,7 @@ mockGetMigrationStatus.migrationStatusFailedData = {
6171
targetCollectionSlug: 'coll-1',
6272
forwardSourceToTarget: true,
6373
isFailed: true,
74+
targetCollection: null,
6475
},
6576
],
6677
} as api.MigrateTaskStatusData;
@@ -77,6 +88,7 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
7788
artifacts: [],
7889
parameters: [
7990
{
91+
id: 1,
8092
source: 'legacy-lib-1',
8193
target: 'lib',
8294
compositionLevel: 'component',
@@ -85,8 +97,10 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
8597
targetCollectionSlug: 'coll-1',
8698
forwardSourceToTarget: true,
8799
isFailed: true,
100+
targetCollection: null,
88101
},
89102
{
103+
id: 2,
90104
source: 'legacy-lib-2',
91105
target: 'lib',
92106
compositionLevel: 'component',
@@ -95,6 +109,7 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
95109
targetCollectionSlug: 'coll-1',
96110
forwardSourceToTarget: true,
97111
isFailed: true,
112+
targetCollection: null,
98113
},
99114
],
100115
} as api.MigrateTaskStatusData;
@@ -111,6 +126,7 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
111126
artifacts: [],
112127
parameters: [
113128
{
129+
id: 1,
114130
source: 'legacy-lib-1',
115131
target: 'lib',
116132
compositionLevel: 'component',
@@ -119,8 +135,10 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
119135
targetCollectionSlug: 'coll-1',
120136
forwardSourceToTarget: true,
121137
isFailed: true,
138+
targetCollection: null,
122139
},
123140
{
141+
id: 2,
124142
source: 'legacy-lib-2',
125143
target: 'lib',
126144
compositionLevel: 'component',
@@ -129,6 +147,34 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
129147
targetCollectionSlug: 'coll-1',
130148
forwardSourceToTarget: true,
131149
isFailed: false,
150+
targetCollection: null,
151+
},
152+
],
153+
} as api.MigrateTaskStatusData;
154+
mockGetMigrationStatus.migrationIdLoading = '5';
155+
mockGetMigrationStatus.migrationIdInProgress = '6';
156+
mockGetMigrationStatus.migrationStatusInProgressData = {
157+
uuid: mockGetMigrationStatus.migrationIdInProgress,
158+
state: 'In Progress',
159+
stateText: 'In Progress',
160+
completedSteps: 3,
161+
totalSteps: 9,
162+
attempts: 1,
163+
created: '',
164+
modified: '',
165+
artifacts: [],
166+
parameters: [
167+
{
168+
id: 1,
169+
source: 'legacy-lib-1',
170+
target: 'lib',
171+
compositionLevel: 'component',
172+
repeatHandlingStrategy: 'update',
173+
preserveUrlSlugs: false,
174+
targetCollectionSlug: 'coll-1',
175+
forwardSourceToTarget: true,
176+
isFailed: false,
177+
targetCollection: null,
132178
},
133179
],
134180
} 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/data/apiHooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ export const useBulkModulestoreMigrate = () => {
6565
/**
6666
* Get the migration status
6767
*/
68-
export const useModulestoreMigrationStatus = (migrationId: string | null) => (
68+
export const useModulestoreMigrationStatus = (migrationId: string | null, refetchInterval: number | false = 1000) => (
6969
useQuery({
7070
queryKey: migrationQueryKeys.migrationTask(migrationId),
7171
queryFn: migrationId ? () => getModulestoreMigrationStatus(migrationId!) : skipToken,
72-
refetchInterval: 1000, // Refresh every second
72+
refetchInterval,
7373
})
7474
);

src/generic/key-utils.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ describe('component utils', () => {
1818
['lct:org:lib:unit:my-unit-9284e2', 'unit'],
1919
['lct:org:lib:section:my-section-9284e2', 'section'],
2020
['lct:org:lib:subsection:my-section-9284e2', 'subsection'],
21+
['block-v1:org+type@html+block@1', 'html'],
22+
['block-v1:OpenCraftX+type@html+block@1571fe018-f3ce-45c9-8f53-5dafcb422fdd', 'html'],
23+
['block-v1:Axim+type@problem+block@571fe018-f3ce-45c9-8f53-5dafcb422fdd', 'problem'],
24+
['block-v1:org+type@unit+block@1', 'unit'],
25+
['block-v1:org+type@section+block@1', 'section'],
26+
['block-v1:org+type@subsection+block@1', 'subsection'],
2127
]) {
2228
it(`returns '${expected}' for usage key '${input}'`, () => {
2329
expect(getBlockType(input)).toStrictEqual(expected);
2430
});
2531
}
2632

27-
for (const input of ['', undefined, null, 'not a key', 'lb:foo']) {
33+
for (const input of ['', undefined, null, 'not a key', 'lb:foo', 'block-v1:foo']) {
2834
it(`throws an exception for usage key '${input}'`, () => {
2935
expect(() => getBlockType(input as any)).toThrow(`Invalid usageKey: ${input}`);
3036
});

src/generic/key-utils.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
/**
2-
* Given a usage key like `lb:org:lib:html:id`, get the type (e.g. `html`)
3-
* @param usageKey e.g. `lb:org:lib:html:id`
2+
* Given a usage key like `lb:org:lib:html:id` or `block-v1:org+type@html+block@1`, get the type (e.g. `html`)
3+
* @param usageKey e.g. `lb:org:lib:html:id`, `block-v1:org+type@html+block@1`
44
* @returns The block type as a string
55
*/
66
export function getBlockType(usageKey: string): string {
7-
if (usageKey && (usageKey.startsWith('lb:') || usageKey.startsWith('lct:'))) {
8-
const blockType = usageKey.split(':')[3];
9-
if (blockType) {
10-
return blockType;
7+
if (usageKey) {
8+
if (usageKey.startsWith('lb:') || usageKey.startsWith('lct:')) {
9+
const blockType = usageKey.split(':')[3];
10+
if (blockType) {
11+
return blockType;
12+
}
13+
} else if (usageKey.startsWith('block-v1:')) {
14+
const blockType = usageKey.match(/type@([^+]+)/);
15+
if (blockType) {
16+
return blockType[1];
17+
}
1118
}
1219
}
1320
throw new Error(`Invalid usageKey: ${usageKey}`);

src/library-authoring/LibraryContent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const LibraryContent = ({ contentType = ContentType.home }: LibraryContentProps)
5656
libraryId,
5757
collectionId,
5858
true,
59+
undefined,
5960
showPlaceholderBlocks,
6061
);
6162
// Fetch unsupported blocks usage_key information from meilisearch index.

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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,52 @@ export const mockGetContentLibraryV2List = {
3434
}),
3535
};
3636

37+
export const mockGetModulestoreMigratedBlocksInfo = {
38+
applyMockSuccess: () => jest.spyOn(api, 'getModulestoreMigrationBlocksInfo').mockResolvedValue(
39+
[
40+
{
41+
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@chapter+block@1',
42+
targetKey: '1',
43+
unsupportedReason: undefined,
44+
},
45+
{
46+
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@sequential+block@2',
47+
targetKey: '2',
48+
unsupportedReason: undefined,
49+
},
50+
{
51+
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@vertical+block@2',
52+
targetKey: '3',
53+
unsupportedReason: undefined,
54+
},
55+
{
56+
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@html+block@3',
57+
targetKey: '4',
58+
unsupportedReason: undefined,
59+
},
60+
],
61+
),
62+
applyMockPartial: () => jest.spyOn(api, 'getModulestoreMigrationBlocksInfo').mockResolvedValue(
63+
[
64+
{
65+
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@library_content+block@test_lib_content',
66+
targetKey: null,
67+
unsupportedReason: 'The "library_content" XBlock (ID: "test_lib_content") has children, so it not supported in content libraries. It has 2 children blocks.',
68+
},
69+
{
70+
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@html+block@1',
71+
targetKey: '1',
72+
unsupportedReason: undefined,
73+
},
74+
{
75+
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@chapter+block@1',
76+
targetKey: '2',
77+
unsupportedReason: undefined,
78+
},
79+
],
80+
),
81+
};
82+
3783
/**
3884
* Mock for `getContentLibrary()`
3985
*
@@ -1091,6 +1137,7 @@ export async function mockGetCourseImports(libraryId: string): ReturnType<typeof
10911137
mockGetCourseImports.libraryId = mockContentLibrary.libraryId;
10921138
mockGetCourseImports.emptyLibraryId = mockContentLibrary.libraryId2;
10931139
mockGetCourseImports.succeedImport = {
1140+
taskUuid: '2d35e36b-1234-1234-1234-123456789000',
10941141
source: {
10951142
key: 'course-v1:edX+DemoX+2025_T1',
10961143
displayName: 'DemoX 2025 T1',
@@ -1100,6 +1147,7 @@ mockGetCourseImports.succeedImport = {
11001147
progress: 1,
11011148
} satisfies api.CourseImport;
11021149
mockGetCourseImports.succeedImportWithCollection = {
1150+
taskUuid: '2',
11031151
source: {
11041152
key: 'course-v1:edX+DemoX+2025_T2',
11051153
displayName: 'DemoX 2025 T2',
@@ -1112,6 +1160,7 @@ mockGetCourseImports.succeedImportWithCollection = {
11121160
progress: 1,
11131161
} satisfies api.CourseImport;
11141162
mockGetCourseImports.failImport = {
1163+
taskUuid: '3',
11151164
source: {
11161165
key: 'course-v1:edX+DemoX+2025_T3',
11171166
displayName: 'DemoX 2025 T3',
@@ -1121,6 +1170,7 @@ mockGetCourseImports.failImport = {
11211170
progress: 0.30,
11221171
} satisfies api.CourseImport;
11231172
mockGetCourseImports.inProgressImport = {
1173+
taskUuid: '4',
11241174
source: {
11251175
key: 'course-v1:edX+DemoX+2025_T4',
11261176
displayName: 'DemoX 2025 T4',

src/library-authoring/data/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,7 @@ export async function publishContainer(containerId: string) {
798798
}
799799

800800
export interface CourseImport {
801+
taskUuid: string;
801802
source: {
802803
key: string;
803804
displayName: string;
@@ -852,6 +853,7 @@ export async function getModulestoreMigrationBlocksInfo(
852853
libraryId: string,
853854
collectionId?: string,
854855
isFailed?: boolean,
856+
taskUuid?: string,
855857
): Promise<BlockMigrationInfo[]> {
856858
const client = getAuthenticatedHttpClient();
857859

@@ -860,6 +862,9 @@ export async function getModulestoreMigrationBlocksInfo(
860862
if (collectionId) {
861863
params.append('target_collection_key', collectionId);
862864
}
865+
if (taskUuid) {
866+
params.append('task_uuid', taskUuid);
867+
}
863868
if (isFailed !== undefined) {
864869
params.append('is_failed', JSON.stringify(isFailed));
865870
}

src/library-authoring/data/apiHooks.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,10 +995,16 @@ export const useMigrationBlocksInfo = (
995995
libraryId: string,
996996
collectionId?: string,
997997
isFailed?: boolean,
998+
taskUuid?: string,
998999
enabled = true,
9991000
) => (
10001001
useQuery({
10011002
queryKey: libraryAuthoringQueryKeys.migrationBlocksInfo(libraryId, collectionId, isFailed),
1002-
queryFn: enabled ? () => api.getModulestoreMigrationBlocksInfo(libraryId, collectionId, isFailed) : skipToken,
1003+
queryFn: enabled ? () => api.getModulestoreMigrationBlocksInfo(
1004+
libraryId,
1005+
collectionId,
1006+
isFailed,
1007+
taskUuid,
1008+
) : skipToken,
10031009
})
10041010
);

0 commit comments

Comments
 (0)