Skip to content

Commit e2f1aed

Browse files
authored
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.
1 parent 5fadcca commit e2f1aed

21 files changed

+796
-209
lines changed

.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ INVITE_STUDENTS_EMAIL_TO="[email protected]"
4848
ENABLE_CHECKLIST_QUALITY=true
4949
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
5050
# "Multi-level" blocks are unsupported in libraries
51-
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
51+
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content"
5252
# Fallback in local style files
5353
PARAGON_THEME_URLS={}
5454
COURSE_TEAM_SUPPORT_EMAIL=''

src/course-outline/data/apiHooks.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,13 @@ export const useCreateCourseBlock = (
2929
export const useCourseItemData = (itemId?: string, enabled: boolean = true) => (
3030
useQuery({
3131
queryKey: courseOutlineQueryKeys.courseItemId(itemId),
32-
queryFn: () => getCourseItem(itemId!),
33-
enabled: enabled && itemId !== undefined,
32+
queryFn: enabled && itemId !== undefined ? () => getCourseItem(itemId!) : skipToken,
3433
})
3534
);
3635

37-
export const useCourseDetails = (courseId?: string) => (
36+
export const useCourseDetails = (courseId?: string, enabled: boolean = true) => (
3837
useQuery({
3938
queryKey: courseOutlineQueryKeys.courseDetails(courseId),
40-
queryFn: courseId ? () => getCourseDetails(courseId) : skipToken,
39+
queryFn: enabled && courseId ? () => getCourseDetails(courseId) : skipToken,
4140
})
4241
);

src/legacy-libraries-migration/data/api.mocks.ts renamed to src/data/api.mocks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,4 @@ mockGetMigrationStatus.migrationStatusFailedOneLibraryData = {
132132
},
133133
],
134134
} as api.MigrateTaskStatusData;
135-
mockGetMigrationStatus.applyMock = () => jest.spyOn(api, 'getMigrationStatus').mockImplementation(mockGetMigrationStatus);
135+
mockGetMigrationStatus.applyMock = () => jest.spyOn(api, 'getModulestoreMigrationStatus').mockImplementation(mockGetMigrationStatus);

src/legacy-libraries-migration/data/api.test.ts renamed to src/data/api.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { initializeMocks } from '../../testUtils';
1+
import { initializeMocks } from '../testUtils';
22
import * as api from './api';
33

44
let axiosMock;
@@ -8,22 +8,22 @@ describe('legacy libraries migration API', () => {
88
({ axiosMock } = initializeMocks());
99
});
1010

11-
describe('getMigrationStatus', () => {
11+
describe('getModulestoreMigrationStatus', () => {
1212
it('should get migration status', async () => {
1313
const migrationId = '1';
14-
const url = api.getMigrationStatusUrl(migrationId);
14+
const url = api.getModulestoreMigrationStatusUrl(migrationId);
1515
axiosMock.onGet(url).reply(200);
16-
await api.getMigrationStatus(migrationId);
16+
await api.getModulestoreMigrationStatus(migrationId);
1717

1818
expect(axiosMock.history.get[0].url).toEqual(url);
1919
});
2020
});
2121

2222
describe('bulkMigrateLegacyLibraries', () => {
2323
it('should call bulk migrate legacy libraries', async () => {
24-
const url = api.bulkMigrateLegacyLibrariesUrl();
24+
const url = api.bulkModulestoreMigrateUrl();
2525
axiosMock.onPost(url).reply(200);
26-
await api.bulkMigrateLegacyLibraries({
26+
await api.bulkModulestoreMigrate({
2727
sources: [],
2828
target: '1',
2929
});

src/data/api.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
1+
import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform';
22
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
33

44
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL as string;
55

6+
/**
7+
* Get the URL to check the migration task status
8+
*/
9+
export const getModulestoreMigrationStatusUrl = (migrationId: string) => `${getStudioBaseUrl()}/api/modulestore_migrator/v1/migrations/${migrationId}/`;
10+
11+
/**
12+
* Get the URL for bulk migrate content to libraries
13+
*/
14+
export const bulkModulestoreMigrateUrl = () => `${getStudioBaseUrl()}/api/modulestore_migrator/v1/bulk_migration/`;
15+
616
export const getApiWaffleFlagsUrl = (courseId?: string): string => {
717
const baseUrl = getStudioBaseUrl();
818
const apiPath = '/api/contentstore/v1/course_waffle_flags';
@@ -72,3 +82,60 @@ export async function getWaffleFlags(courseId?: string): Promise<WaffleFlagsStat
7282
.get(getApiWaffleFlagsUrl(courseId));
7383
return normalizeCourseDetail(data);
7484
}
85+
86+
export interface MigrateArtifacts {
87+
source: string;
88+
target: string;
89+
compositionLevel: string;
90+
repeatHandlingStrategy: 'update' | 'skip' | 'fork';
91+
preserveUrlSlugs: boolean;
92+
targetCollectionSlug: string;
93+
forwardSourceToTarget: boolean;
94+
isFailed: boolean;
95+
}
96+
97+
export interface MigrateTaskStatusData {
98+
state: string;
99+
stateText: string;
100+
completedSteps: number;
101+
totalSteps: number;
102+
attempts: number;
103+
created: string;
104+
modified: string;
105+
artifacts: string[];
106+
uuid: string;
107+
parameters: MigrateArtifacts[];
108+
}
109+
110+
export interface BulkMigrateRequestData {
111+
sources: string[];
112+
target: string;
113+
targetCollectionSlugList?: string[];
114+
createCollections?: boolean;
115+
compositionLevel?: string;
116+
repeatHandlingStrategy?: string;
117+
preserveUrlSlugs?: boolean;
118+
forwardSourceToTarget?: boolean;
119+
}
120+
121+
/**
122+
* Get migration task status
123+
*/
124+
export async function getModulestoreMigrationStatus(
125+
migrationId: string,
126+
): Promise<MigrateTaskStatusData> {
127+
const client = getAuthenticatedHttpClient();
128+
const { data } = await client.get(getModulestoreMigrationStatusUrl(migrationId));
129+
return camelCaseObject(data);
130+
}
131+
132+
/**
133+
* Bulk migrate content to libraries
134+
*/
135+
export async function bulkModulestoreMigrate(
136+
requestData: BulkMigrateRequestData,
137+
): Promise<MigrateTaskStatusData> {
138+
const client = getAuthenticatedHttpClient();
139+
const { data } = await client.post(bulkModulestoreMigrateUrl(), snakeCaseObject(requestData));
140+
return camelCaseObject(data);
141+
}

src/data/apiHooks.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1-
import { useQuery, useQueryClient } from '@tanstack/react-query';
2-
import { getWaffleFlags, waffleFlagDefaults } from './api';
1+
import {
2+
skipToken, useMutation, useQuery, useQueryClient,
3+
} from '@tanstack/react-query';
4+
import { libraryAuthoringQueryKeys } from '@src/library-authoring/data/apiHooks';
5+
import {
6+
getWaffleFlags,
7+
waffleFlagDefaults,
8+
bulkModulestoreMigrate,
9+
getModulestoreMigrationStatus,
10+
BulkMigrateRequestData,
11+
} from './api';
12+
13+
export const migrationQueryKeys = {
14+
all: ['contentLibrary'],
15+
/**
16+
* Base key for data specific to a migration task
17+
*/
18+
migrationTask: (migrationId?: string | null) => [...migrationQueryKeys.all, migrationId],
19+
};
320

421
/**
522
* Get the waffle flags (which enable/disable specific features). They may
@@ -30,3 +47,28 @@ export const useWaffleFlags = (courseId?: string) => {
3047
isError,
3148
};
3249
};
50+
51+
/**
52+
* Use this mutation to migrate multiple sources to a library
53+
*/
54+
export const useBulkModulestoreMigrate = () => {
55+
const queryClient = useQueryClient();
56+
return useMutation({
57+
mutationFn: async (requestData: BulkMigrateRequestData) => bulkModulestoreMigrate(requestData),
58+
onSettled: (_data, _err, variables) => {
59+
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.courseImports(variables.target) });
60+
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.allMigrationInfo() });
61+
},
62+
});
63+
};
64+
65+
/**
66+
* Get the migration status
67+
*/
68+
export const useModulestoreMigrationStatus = (migrationId: string | null) => (
69+
useQuery({
70+
queryKey: migrationQueryKeys.migrationTask(migrationId),
71+
queryFn: migrationId ? () => getModulestoreMigrationStatus(migrationId!) : skipToken,
72+
refetchInterval: 1000, // Refresh every second
73+
})
74+
);

src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { mockGetStudioHomeLibraries } from '@src/studio-home/data/api.mocks';
1414
import { getContentLibraryV2CreateApiUrl } from '@src/library-authoring/create-library/data/api';
1515
import { getStudioHomeApiUrl } from '@src/studio-home/data/api';
1616

17+
import { bulkModulestoreMigrateUrl } from '@src/data/api';
1718
import { LegacyLibMigrationPage } from './LegacyLibMigrationPage';
18-
import { bulkMigrateLegacyLibrariesUrl } from './data/api';
1919

2020
const path = '/libraries-v1/migrate/*';
2121
let axiosMock: MockAdapter;
@@ -320,7 +320,7 @@ describe('<LegacyLibMigrationPage />', () => {
320320

321321
it('should confirm migration', async () => {
322322
const user = userEvent.setup();
323-
axiosMock.onPost(bulkMigrateLegacyLibrariesUrl()).reply(200);
323+
axiosMock.onPost(bulkModulestoreMigrateUrl()).reply(200);
324324
renderPage();
325325
expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument();
326326
expect(await screen.findByText('MBA')).toBeInTheDocument();
@@ -377,7 +377,7 @@ describe('<LegacyLibMigrationPage />', () => {
377377

378378
it('should show error when confirm migration', async () => {
379379
const user = userEvent.setup();
380-
axiosMock.onPost(bulkMigrateLegacyLibrariesUrl()).reply(400);
380+
axiosMock.onPost(bulkModulestoreMigrateUrl()).reply(400);
381381
renderPage();
382382
expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument();
383383
expect(await screen.findByText('MBA')).toBeInTheDocument();

src/legacy-libraries-migration/LegacyLibMigrationPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ import type { LibraryV1Data } from '@src/studio-home/data/api';
2525
import { ToastContext } from '@src/generic/toast-context';
2626
import { Filter, LibrariesList } from '@src/studio-home/tabs-section/libraries-tab';
2727

28+
import { useBulkModulestoreMigrate } from '@src/data/apiHooks';
2829
import messages from './messages';
2930
import { SelectDestinationView } from './SelectDestinationView';
3031
import { ConfirmationView } from './ConfirmationView';
3132
import { LegacyMigrationHelpSidebar } from './LegacyMigrationHelpSidebar';
32-
import { useUpdateContainerCollections } from './data/apiHooks';
3333

3434
export type MigrationStep = 'select-libraries' | 'select-destination' | 'confirmation-view';
3535

@@ -83,7 +83,7 @@ export const LegacyLibMigrationPage = () => {
8383
const [migrationFilter, setMigrationFilter] = useState<Filter[]>([Filter.unmigrated]);
8484
const [destinationLibrary, setDestination] = useState<ContentLibrary>();
8585
const [confirmationButtonState, setConfirmationButtonState] = useState('default');
86-
const migrate = useUpdateContainerCollections();
86+
const migrate = useBulkModulestoreMigrate();
8787

8888
const handleMigrate = useCallback(async () => {
8989
if (destinationLibrary) {

src/legacy-libraries-migration/data/api.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.

src/legacy-libraries-migration/data/apiHooks.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)