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
46 changes: 46 additions & 0 deletions src/data/api.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export async function mockGetMigrationStatus(migrationId: string): Promise<api.M
return mockGetMigrationStatus.migrationStatusFailedMultipleData;
case mockGetMigrationStatus.migrationIdOneLibrary:
return mockGetMigrationStatus.migrationStatusFailedOneLibraryData;
case mockGetMigrationStatus.migrationIdLoading:
return new Promise(() => {});
case mockGetMigrationStatus.migrationIdInProgress:
return mockGetMigrationStatus.migrationStatusInProgressData;
default:
/* istanbul ignore next */
throw new Error(`mockGetMigrationStatus: unknown migration ID "${migrationId}"`);
Expand All @@ -29,6 +33,7 @@ mockGetMigrationStatus.migrationStatusData = {
artifacts: [],
parameters: [
{
id: 1,
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
Expand All @@ -37,6 +42,10 @@ mockGetMigrationStatus.migrationStatusData = {
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: false,
targetCollection: {
key: 'coll',
title: 'Test Collection',
},
},
],
} as api.MigrateTaskStatusData;
Expand All @@ -53,6 +62,7 @@ mockGetMigrationStatus.migrationStatusFailedData = {
artifacts: [],
parameters: [
{
id: 1,
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
Expand All @@ -61,6 +71,7 @@ mockGetMigrationStatus.migrationStatusFailedData = {
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: true,
targetCollection: null,
},
],
} as api.MigrateTaskStatusData;
Expand All @@ -77,6 +88,7 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
artifacts: [],
parameters: [
{
id: 1,
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
Expand All @@ -85,8 +97,10 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: true,
targetCollection: null,
},
{
id: 2,
source: 'legacy-lib-2',
target: 'lib',
compositionLevel: 'component',
Expand All @@ -95,6 +109,7 @@ mockGetMigrationStatus.migrationStatusFailedMultipleData = {
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: true,
targetCollection: null,
},
],
} as api.MigrateTaskStatusData;
Expand All @@ -111,6 +126,7 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
artifacts: [],
parameters: [
{
id: 1,
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
Expand All @@ -119,8 +135,10 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: true,
targetCollection: null,
},
{
id: 2,
source: 'legacy-lib-2',
target: 'lib',
compositionLevel: 'component',
Expand All @@ -129,6 +147,34 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: false,
targetCollection: null,
},
],
} as api.MigrateTaskStatusData;
mockGetMigrationStatus.migrationIdLoading = '5';
mockGetMigrationStatus.migrationIdInProgress = '6';
mockGetMigrationStatus.migrationStatusInProgressData = {
uuid: mockGetMigrationStatus.migrationIdInProgress,
state: 'In Progress',
stateText: 'In Progress',
completedSteps: 3,
totalSteps: 9,
attempts: 1,
created: '',
modified: '',
artifacts: [],
parameters: [
{
id: 1,
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
repeatHandlingStrategy: 'update',
preserveUrlSlugs: false,
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: false,
targetCollection: null,
},
],
} as api.MigrateTaskStatusData;
Expand Down
9 changes: 7 additions & 2 deletions src/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export async function getWaffleFlags(courseId?: string): Promise<WaffleFlagsStat
return normalizeCourseDetail(data);
}

export interface MigrateArtifacts {
export interface MigrateParameters {
id: number;
source: string;
target: string;
compositionLevel: string;
Expand All @@ -92,6 +93,10 @@ export interface MigrateArtifacts {
targetCollectionSlug: string;
forwardSourceToTarget: boolean;
isFailed: boolean;
targetCollection: {
key: string;
title: string;
} | null;
}

export interface MigrateTaskStatusData {
Expand All @@ -104,7 +109,7 @@ export interface MigrateTaskStatusData {
modified: string;
artifacts: string[];
uuid: string;
parameters: MigrateArtifacts[];
parameters: MigrateParameters[];
}

export interface BulkMigrateRequestData {
Expand Down
4 changes: 2 additions & 2 deletions src/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ export const useBulkModulestoreMigrate = () => {
/**
* Get the migration status
*/
export const useModulestoreMigrationStatus = (migrationId: string | null) => (
export const useModulestoreMigrationStatus = (migrationId: string | null, refetchInterval: number | false = 1000) => (
useQuery({
queryKey: migrationQueryKeys.migrationTask(migrationId),
queryFn: migrationId ? () => getModulestoreMigrationStatus(migrationId!) : skipToken,
refetchInterval: 1000, // Refresh every second
refetchInterval,
})
);
22 changes: 22 additions & 0 deletions src/generic/key-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
buildCollectionUsageKey,
ContainerType,
getBlockType,
getBlockTypeBlockV1,
getLibraryId,
isLibraryKey,
isLibraryV1Key,
Expand Down Expand Up @@ -142,4 +143,25 @@ describe('component utils', () => {
});
}
});

describe('getBlockTypeBlockV1', () => {
for (const [input, expected] of [
['block-v1:org+type@html+block@1', 'html'],
['block-v1:OpenCraftX+type@html+block@1571fe018-f3ce-45c9-8f53-5dafcb422fdd', 'html'],
['block-v1:Axim+type@problem+block@571fe018-f3ce-45c9-8f53-5dafcb422fdd', 'problem'],
['block-v1:org+type@unit+block@1', 'unit'],
['block-v1:org+type@section+block@1', 'section'],
['block-v1:org+type@subsection+block@1', 'subsection'],
]) {
it(`returns '${expected}' for usage key '${input}'`, () => {
expect(getBlockTypeBlockV1(input)).toStrictEqual(expected);
});
}

for (const input of ['', undefined, null, 'not a key', 'block-v1:foo']) {
it(`throws an exception for usage key '${input}'`, () => {
expect(() => getBlockTypeBlockV1(input as any)).toThrow(`Invalid usageKey: ${input}`);
});
}
});
});
15 changes: 15 additions & 0 deletions src/generic/key-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,18 @@ export function normalizeContainerType(containerType: ContainerType | string) {
return containerType;
}
}

/**
* Given a usage key of V1 block like `block-v1:org+type@html+block@1`, get the type (e.g. `html`)
* @param usageKey e.g. `block-v1:org+type@html+block@1`
* @returns The block type as a string
*/
export function getBlockTypeBlockV1(usageKey: string): string {
if (usageKey && usageKey.startsWith('block-v1:')) {
const blockType = usageKey.match(/type@([^+]+)/);
if (blockType) {
return blockType[1];
}
}
throw new Error(`Invalid usageKey: ${usageKey}`);
}
1 change: 1 addition & 0 deletions src/library-authoring/LibraryContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const LibraryContent = ({ contentType = ContentType.home }: LibraryContentProps)
libraryId,
collectionId,
true,
undefined,
showPlaceholderBlocks,
);
// Fetch unsupported blocks usage_key information from meilisearch index.
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 @@ -21,6 +21,7 @@ import { LibrarySectionPage, LibrarySubsectionPage } from './section-subsections
import { LibraryUnitPage } from './units';
import { LibraryTeamModal } from './library-team';
import { ImportStepperPage } from './import-course/stepper/ImportStepperPage';
import { ImportDetailsPage } from './import-course/ImportDetailsPage';

const LibraryLayoutWrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
const {
Expand Down Expand Up @@ -102,6 +103,10 @@ const LibraryLayout = () => (
path={ROUTES.IMPORT_COURSE}
Component={ImportStepperPage}
/>
<Route
path={ROUTES.IMPORT_COURSE_DETAILS}
Component={ImportDetailsPage}
/>
</Route>
</Routes>
);
Expand Down
50 changes: 50 additions & 0 deletions src/library-authoring/data/api.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,52 @@ export const mockGetContentLibraryV2List = {
}),
};

export const mockGetModulestoreMigratedBlocksInfo = {
applyMockSuccess: () => jest.spyOn(api, 'getModulestoreMigrationBlocksInfo').mockResolvedValue(
[
{
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@chapter+block@1',
targetKey: '1',
unsupportedReason: undefined,
},
{
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@sequential+block@2',
targetKey: '2',
unsupportedReason: undefined,
},
{
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@vertical+block@2',
targetKey: '3',
unsupportedReason: undefined,
},
{
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@html+block@3',
targetKey: '4',
unsupportedReason: undefined,
},
],
),
applyMockPartial: () => jest.spyOn(api, 'getModulestoreMigrationBlocksInfo').mockResolvedValue(
[
{
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@library_content+block@test_lib_content',
targetKey: null,
unsupportedReason: 'The "library_content" XBlock (ID: "test_lib_content") has children, so it not supported in content libraries. It has 2 children blocks.',
},
{
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@html+block@1',
targetKey: '1',
unsupportedReason: undefined,
},
{
sourceKey: 'block-v1:UNIX+UX2+2025_T2+type@chapter+block@1',
targetKey: '2',
unsupportedReason: undefined,
},
],
),
};

/**
* Mock for `getContentLibrary()`
*
Expand Down Expand Up @@ -1091,6 +1137,7 @@ export async function mockGetCourseImports(libraryId: string): ReturnType<typeof
mockGetCourseImports.libraryId = mockContentLibrary.libraryId;
mockGetCourseImports.emptyLibraryId = mockContentLibrary.libraryId2;
mockGetCourseImports.succeedImport = {
taskUuid: '1',
source: {
key: 'course-v1:edX+DemoX+2025_T1',
displayName: 'DemoX 2025 T1',
Expand All @@ -1100,6 +1147,7 @@ mockGetCourseImports.succeedImport = {
progress: 1,
} satisfies api.CourseImport;
mockGetCourseImports.succeedImportWithCollection = {
taskUuid: '2',
source: {
key: 'course-v1:edX+DemoX+2025_T2',
displayName: 'DemoX 2025 T2',
Expand All @@ -1112,6 +1160,7 @@ mockGetCourseImports.succeedImportWithCollection = {
progress: 1,
} satisfies api.CourseImport;
mockGetCourseImports.failImport = {
taskUuid: '3',
source: {
key: 'course-v1:edX+DemoX+2025_T3',
displayName: 'DemoX 2025 T3',
Expand All @@ -1121,6 +1170,7 @@ mockGetCourseImports.failImport = {
progress: 0.30,
} satisfies api.CourseImport;
mockGetCourseImports.inProgressImport = {
taskUuid: '4',
source: {
key: 'course-v1:edX+DemoX+2025_T4',
displayName: 'DemoX 2025 T4',
Expand Down
5 changes: 5 additions & 0 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ export async function publishContainer(containerId: string) {
}

export interface CourseImport {
taskUuid: string;
source: {
key: string;
displayName: string;
Expand Down Expand Up @@ -852,6 +853,7 @@ export async function getModulestoreMigrationBlocksInfo(
libraryId: string,
collectionId?: string,
isFailed?: boolean,
taskUuid?: string,
): Promise<BlockMigrationInfo[]> {
const client = getAuthenticatedHttpClient();

Expand All @@ -860,6 +862,9 @@ export async function getModulestoreMigrationBlocksInfo(
if (collectionId) {
params.append('target_collection_key', collectionId);
}
if (taskUuid) {
params.append('task_uuid', taskUuid);
}
if (isFailed !== undefined) {
params.append('is_failed', JSON.stringify(isFailed));
}
Expand Down
8 changes: 7 additions & 1 deletion src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -995,10 +995,16 @@ export const useMigrationBlocksInfo = (
libraryId: string,
collectionId?: string,
isFailed?: boolean,
taskUuid?: string,
enabled = true,
) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.migrationBlocksInfo(libraryId, collectionId, isFailed),
queryFn: enabled ? () => api.getModulestoreMigrationBlocksInfo(libraryId, collectionId, isFailed) : skipToken,
queryFn: enabled ? () => api.getModulestoreMigrationBlocksInfo(
libraryId,
collectionId,
isFailed,
taskUuid,
) : skipToken,
})
);
Loading