feat: import analysis step (#2657)

Shows course analysis information in review import details step in course import stepper page. Also handles alerts based on the import status, like, reimport or unsupported number of blocks.
This commit is contained in:
Navin Karkera
2025-11-18 22:11:27 +05:30
committed by GitHub
parent 5fadccabe2
commit e2f1aedf9a
21 changed files with 800 additions and 213 deletions

135
src/data/api.mocks.ts Normal file
View File

@@ -0,0 +1,135 @@
import * as api from './api';
export async function mockGetMigrationStatus(migrationId: string): Promise<api.MigrateTaskStatusData> {
switch (migrationId) {
case mockGetMigrationStatus.migrationId:
return mockGetMigrationStatus.migrationStatusData;
case mockGetMigrationStatus.migrationIdFailed:
return mockGetMigrationStatus.migrationStatusFailedData;
case mockGetMigrationStatus.migrationIdMultiple:
return mockGetMigrationStatus.migrationStatusFailedMultipleData;
case mockGetMigrationStatus.migrationIdOneLibrary:
return mockGetMigrationStatus.migrationStatusFailedOneLibraryData;
default:
/* istanbul ignore next */
throw new Error(`mockGetMigrationStatus: unknown migration ID "${migrationId}"`);
}
}
mockGetMigrationStatus.migrationId = '1';
mockGetMigrationStatus.migrationStatusData = {
uuid: mockGetMigrationStatus.migrationId,
state: 'Succeeded',
stateText: 'Succeeded',
completedSteps: 9,
totalSteps: 9,
attempts: 1,
created: '',
modified: '',
artifacts: [],
parameters: [
{
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
repeatHandlingStrategy: 'update',
preserveUrlSlugs: false,
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: false,
},
],
} as api.MigrateTaskStatusData;
mockGetMigrationStatus.migrationIdFailed = '2';
mockGetMigrationStatus.migrationStatusFailedData = {
uuid: mockGetMigrationStatus.migrationId,
state: 'Failed',
stateText: 'Failed',
completedSteps: 9,
totalSteps: 9,
attempts: 1,
created: '',
modified: '',
artifacts: [],
parameters: [
{
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
repeatHandlingStrategy: 'update',
preserveUrlSlugs: false,
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: true,
},
],
} as api.MigrateTaskStatusData;
mockGetMigrationStatus.migrationIdMultiple = '3';
mockGetMigrationStatus.migrationStatusFailedMultipleData = {
uuid: mockGetMigrationStatus.migrationId,
state: 'Succeeded',
stateText: 'Succeeded',
completedSteps: 9,
totalSteps: 9,
attempts: 1,
created: '',
modified: '',
artifacts: [],
parameters: [
{
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
repeatHandlingStrategy: 'update',
preserveUrlSlugs: false,
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: true,
},
{
source: 'legacy-lib-2',
target: 'lib',
compositionLevel: 'component',
repeatHandlingStrategy: 'update',
preserveUrlSlugs: false,
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: true,
},
],
} as api.MigrateTaskStatusData;
mockGetMigrationStatus.migrationIdOneLibrary = '4';
mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
uuid: mockGetMigrationStatus.migrationId,
state: 'Succeeded',
stateText: 'Succeeded',
completedSteps: 9,
totalSteps: 9,
attempts: 1,
created: '',
modified: '',
artifacts: [],
parameters: [
{
source: 'legacy-lib-1',
target: 'lib',
compositionLevel: 'component',
repeatHandlingStrategy: 'update',
preserveUrlSlugs: false,
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: true,
},
{
source: 'legacy-lib-2',
target: 'lib',
compositionLevel: 'component',
repeatHandlingStrategy: 'update',
preserveUrlSlugs: false,
targetCollectionSlug: 'coll-1',
forwardSourceToTarget: true,
isFailed: false,
},
],
} as api.MigrateTaskStatusData;
mockGetMigrationStatus.applyMock = () => jest.spyOn(api, 'getModulestoreMigrationStatus').mockImplementation(mockGetMigrationStatus);

34
src/data/api.test.ts Normal file
View File

@@ -0,0 +1,34 @@
import { initializeMocks } from '../testUtils';
import * as api from './api';
let axiosMock;
describe('legacy libraries migration API', () => {
beforeEach(() => {
({ axiosMock } = initializeMocks());
});
describe('getModulestoreMigrationStatus', () => {
it('should get migration status', async () => {
const migrationId = '1';
const url = api.getModulestoreMigrationStatusUrl(migrationId);
axiosMock.onGet(url).reply(200);
await api.getModulestoreMigrationStatus(migrationId);
expect(axiosMock.history.get[0].url).toEqual(url);
});
});
describe('bulkMigrateLegacyLibraries', () => {
it('should call bulk migrate legacy libraries', async () => {
const url = api.bulkModulestoreMigrateUrl();
axiosMock.onPost(url).reply(200);
await api.bulkModulestoreMigrate({
sources: [],
target: '1',
});
expect(axiosMock.history.post[0].url).toEqual(url);
});
});
});

View File

@@ -1,8 +1,18 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL as string;
/**
* Get the URL to check the migration task status
*/
export const getModulestoreMigrationStatusUrl = (migrationId: string) => `${getStudioBaseUrl()}/api/modulestore_migrator/v1/migrations/${migrationId}/`;
/**
* Get the URL for bulk migrate content to libraries
*/
export const bulkModulestoreMigrateUrl = () => `${getStudioBaseUrl()}/api/modulestore_migrator/v1/bulk_migration/`;
export const getApiWaffleFlagsUrl = (courseId?: string): string => {
const baseUrl = getStudioBaseUrl();
const apiPath = '/api/contentstore/v1/course_waffle_flags';
@@ -72,3 +82,60 @@ export async function getWaffleFlags(courseId?: string): Promise<WaffleFlagsStat
.get(getApiWaffleFlagsUrl(courseId));
return normalizeCourseDetail(data);
}
export interface MigrateArtifacts {
source: string;
target: string;
compositionLevel: string;
repeatHandlingStrategy: 'update' | 'skip' | 'fork';
preserveUrlSlugs: boolean;
targetCollectionSlug: string;
forwardSourceToTarget: boolean;
isFailed: boolean;
}
export interface MigrateTaskStatusData {
state: string;
stateText: string;
completedSteps: number;
totalSteps: number;
attempts: number;
created: string;
modified: string;
artifacts: string[];
uuid: string;
parameters: MigrateArtifacts[];
}
export interface BulkMigrateRequestData {
sources: string[];
target: string;
targetCollectionSlugList?: string[];
createCollections?: boolean;
compositionLevel?: string;
repeatHandlingStrategy?: string;
preserveUrlSlugs?: boolean;
forwardSourceToTarget?: boolean;
}
/**
* Get migration task status
*/
export async function getModulestoreMigrationStatus(
migrationId: string,
): Promise<MigrateTaskStatusData> {
const client = getAuthenticatedHttpClient();
const { data } = await client.get(getModulestoreMigrationStatusUrl(migrationId));
return camelCaseObject(data);
}
/**
* Bulk migrate content to libraries
*/
export async function bulkModulestoreMigrate(
requestData: BulkMigrateRequestData,
): Promise<MigrateTaskStatusData> {
const client = getAuthenticatedHttpClient();
const { data } = await client.post(bulkModulestoreMigrateUrl(), snakeCaseObject(requestData));
return camelCaseObject(data);
}

View File

@@ -1,5 +1,22 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getWaffleFlags, waffleFlagDefaults } from './api';
import {
skipToken, useMutation, useQuery, useQueryClient,
} from '@tanstack/react-query';
import { libraryAuthoringQueryKeys } from '@src/library-authoring/data/apiHooks';
import {
getWaffleFlags,
waffleFlagDefaults,
bulkModulestoreMigrate,
getModulestoreMigrationStatus,
BulkMigrateRequestData,
} from './api';
export const migrationQueryKeys = {
all: ['contentLibrary'],
/**
* Base key for data specific to a migration task
*/
migrationTask: (migrationId?: string | null) => [...migrationQueryKeys.all, migrationId],
};
/**
* Get the waffle flags (which enable/disable specific features). They may
@@ -30,3 +47,28 @@ export const useWaffleFlags = (courseId?: string) => {
isError,
};
};
/**
* Use this mutation to migrate multiple sources to a library
*/
export const useBulkModulestoreMigrate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (requestData: BulkMigrateRequestData) => bulkModulestoreMigrate(requestData),
onSettled: (_data, _err, variables) => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.courseImports(variables.target) });
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.allMigrationInfo() });
},
});
};
/**
* Get the migration status
*/
export const useModulestoreMigrationStatus = (migrationId: string | null) => (
useQuery({
queryKey: migrationQueryKeys.migrationTask(migrationId),
queryFn: migrationId ? () => getModulestoreMigrationStatus(migrationId!) : skipToken,
refetchInterval: 1000, // Refresh every second
})
);