Skip to content
Merged
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
69 changes: 67 additions & 2 deletions src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import { getContentLibraryV2CreateApiUrl } from '@src/library-authoring/create-l
import { getStudioHomeApiUrl } from '@src/studio-home/data/api';

import { LegacyLibMigrationPage } from './LegacyLibMigrationPage';
import { bulkMigrateLegacyLibrariesUrl } from './data/api';

const path = '/libraries-v1/migrate/*';
let axiosMock: MockAdapter;
let mockShowToast;

mockGetStudioHomeLibraries.applyMock();
mockGetContentLibraryV2List.applyMock();
Expand All @@ -41,7 +43,9 @@ const renderPage = () => (

describe('<LegacyLibMigrationPage />', () => {
beforeEach(() => {
axiosMock = initializeMocks().axiosMock;
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
mockShowToast = mocks.mockShowToast;
});

it('should render legacy library migration page', async () => {
Expand Down Expand Up @@ -292,6 +296,7 @@ describe('<LegacyLibMigrationPage />', () => {

it('should confirm migration', async () => {
const user = userEvent.setup();
axiosMock.onPost(bulkMigrateLegacyLibrariesUrl()).reply(200);
renderPage();
expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument();
expect(await screen.findByText('MBA')).toBeInTheDocument();
Expand Down Expand Up @@ -334,6 +339,66 @@ describe('<LegacyLibMigrationPage />', () => {
const confirmButton = screen.getByRole('button', { name: /confirm/i });
confirmButton.click();

// TODO: expect call migrate API
await waitFor(() => {
expect(axiosMock.history.post.length).toBe(1);
});
expect(axiosMock.history.post[0].data).toBe(
'{"sources":["library-v1:MBA+123","library-v1:UNIX+LG1","library-v1:MBA+1234"],"target":"lib:SampleTaxonomyOrg1:TL1","create_collections":true,"repeat_handling_strategy":"fork"}',
);
expect(mockShowToast).toHaveBeenCalledWith('3 legacy libraries are being migrated.');
});

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

// The filter is 'unmigrated' by default.
// Clear the filter to select all libraries
const filterButton = screen.getByRole('button', { name: /unmigrated/i });
await user.click(filterButton);
const clearButton = await screen.findByRole('button', { name: /clear filter/i });
await user.click(clearButton);

const legacyLibrary1 = screen.getByRole('checkbox', { name: 'MBA' });
const legacyLibrary2 = screen.getByRole('checkbox', { name: /legacy library 1 imported library/i });
const legacyLibrary3 = screen.getByRole('checkbox', { name: 'MBA 1' });

legacyLibrary1.click();
legacyLibrary2.click();
legacyLibrary3.click();

const nextButton = screen.getByRole('button', { name: /next/i });
nextButton.click();

// Should show alert of SelectDestinationView
expect(await screen.findByText(/any legacy libraries that are used/i)).toBeInTheDocument();
expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
const radioButton = screen.getByRole('radio', { name: /test library 1/i });
radioButton.click();

nextButton.click();

// Should show alert of ConfirmationView
expect(await screen.findByText(/these 3 legacy libraries will be migrated to/i)).toBeInTheDocument();
expect(screen.getByText('MBA')).toBeInTheDocument();
expect(screen.getByText('Legacy library 1')).toBeInTheDocument();
expect(screen.getByText('MBA 1')).toBeInTheDocument();
expect(screen.getByText(
/Previously migrated library. Any problem bank links were already moved will be migrated to/i,
)).toBeInTheDocument();

const confirmButton = screen.getByRole('button', { name: /confirm/i });
confirmButton.click();

await waitFor(() => {
expect(axiosMock.history.post.length).toBe(1);
});
expect(axiosMock.history.post[0].data).toBe(
'{"sources":["library-v1:MBA+123","library-v1:UNIX+LG1","library-v1:MBA+1234"],"target":"lib:SampleTaxonomyOrg1:TL1","create_collections":true,"repeat_handling_strategy":"fork"}',
);
expect(mockShowToast).toHaveBeenCalledWith('Legacy libraries migration failed.');
});
});
35 changes: 32 additions & 3 deletions src/legacy-libraries-migration/LegacyLibMigrationPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { useCallback, useMemo, useState } from 'react';
import {
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom';

Expand All @@ -16,11 +21,13 @@ import Header from '@src/header';
import SubHeader from '@src/generic/sub-header/SubHeader';
import type { ContentLibrary } from '@src/library-authoring/data/api';
import type { LibraryV1Data } from '@src/studio-home/data/api';
import { ToastContext } from '@src/generic/toast-context';
import { Filter, LibrariesList } from '@src/studio-home/tabs-section/libraries-tab';

import messages from './messages';
import { SelectDestinationView } from './SelectDestinationView';
import { ConfirmationView } from './ConfirmationView';
import { useUpdateContainerCollections } from './data/apiHooks';

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

Expand Down Expand Up @@ -66,11 +73,33 @@ const ExitModal = ({

export const LegacyLibMigrationPage = () => {
const intl = useIntl();
const navigate = useNavigate();
const { showToast } = useContext(ToastContext);
const [currentStep, setCurrentStep] = useState<MigrationStep>('select-libraries');
const [isExitModalOpen, openExitModal, closeExitModal] = useToggle(false);
const [legacyLibraries, setLegacyLibraries] = useState<LibraryV1Data[]>([]);
const [destinationLibrary, setDestination] = useState<ContentLibrary>();
const [confirmationButtonState, setConfirmationButtonState] = useState('default');
const migrate = useUpdateContainerCollections();

const handleMigrate = useCallback(async () => {
if (destinationLibrary) {
try {
const migrationTask = await migrate.mutateAsync({
sources: legacyLibraries.map((lib) => lib.libraryKey),
target: destinationLibrary.id,
createCollections: true,
repeatHandlingStrategy: 'fork',
});
showToast(intl.formatMessage(messages.migrationInProgress, {
count: legacyLibraries.length,
}));
navigate(`/library/${destinationLibrary.id}?migration_task=${migrationTask.uuid}`);
} catch (error) {
showToast(intl.formatMessage(messages.migrationFailed));
}
}
}, [migrate, legacyLibraries, destinationLibrary]);

const handleNext = useCallback(() => {
switch (currentStep) {
Expand All @@ -82,13 +111,13 @@ export const LegacyLibMigrationPage = () => {
break;
case 'confirmation-view':
setConfirmationButtonState('pending');
// TODO Call migration API
handleMigrate();
break;
default:
/* istanbul ignore next */
break;
}
}, [currentStep, setCurrentStep]);
}, [currentStep, setCurrentStep, handleMigrate]);

const handleBack = useCallback(() => {
switch (currentStep) {
Expand Down
61 changes: 61 additions & 0 deletions src/legacy-libraries-migration/data/api.mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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;
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,
},
],
} 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,
},
],
} as api.MigrateTaskStatusData;
mockGetMigrationStatus.applyMock = () => jest.spyOn(api, 'getMigrationStatus').mockImplementation(mockGetMigrationStatus);
34 changes: 34 additions & 0 deletions src/legacy-libraries-migration/data/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { initializeMocks } from '../../testUtils';
import * as api from './api';

let axiosMock;

describe('legacy libraries migration API', () => {
beforeEach(() => {
({ axiosMock } = initializeMocks());
});

describe('getMigrationStatus', () => {
it('should get migration status', async () => {
const migrationId = '1';
const url = api.getMigrationStatusUrl(migrationId);
axiosMock.onGet(url).reply(200);
await api.getMigrationStatus(migrationId);

expect(axiosMock.history.get[0].url).toEqual(url);
});
});

describe('bulkMigrateLegacyLibraries', () => {
it('should call bulk migrate legacy libraries', async () => {
const url = api.bulkMigrateLegacyLibrariesUrl();
axiosMock.onPost(url).reply(200);
await api.bulkMigrateLegacyLibraries({
sources: [],
target: '1',
});

expect(axiosMock.history.post[0].url).toEqual(url);
});
});
});
70 changes: 70 additions & 0 deletions src/legacy-libraries-migration/data/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;

/**
* Get the URL to check the migration task status
*/
export const getMigrationStatusUrl = (migrationId: string) => `${getApiBaseUrl()}/api/modulestore_migrator/v1/migrations/${migrationId}/`;

/**
* Get the URL for bulk migrate legacy libraries
*/
export const bulkMigrateLegacyLibrariesUrl = () => `${getApiBaseUrl()}/api/modulestore_migrator/v1/bulk_migration/`;

export interface MigrateArtifacts {
source: string;
target: string;
compositionLevel: string;
repeatHandlingStrategy: 'update' | 'skip' | 'fork';
preserveUrlSlugs: boolean;
targetCollectionSlug: string;
forwardSourceToTarget: 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 getMigrationStatus(
migrationId: string,
): Promise<MigrateTaskStatusData> {
const client = getAuthenticatedHttpClient();
const { data } = await client.get(getMigrationStatusUrl(migrationId));
return camelCaseObject(data);
}

/**
* Bulk migrate legacy libraries
*/
export async function bulkMigrateLegacyLibraries(
requestData: BulkMigrateRequestData,
): Promise<MigrateTaskStatusData> {
const client = getAuthenticatedHttpClient();
const { data } = await client.post(bulkMigrateLegacyLibrariesUrl(), snakeCaseObject(requestData));
return camelCaseObject(data);
}
31 changes: 31 additions & 0 deletions src/legacy-libraries-migration/data/apiHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { skipToken, useMutation, useQuery } from '@tanstack/react-query';

import * as api from './api';

export const legacyMigrationQueryKeys = {
all: ['contentLibrary'],
/**
* Base key for data specific to a migration task
*/
migrationTask: (migrationId?: string | null) => [...legacyMigrationQueryKeys.all, migrationId],
};

/**
* Use this mutation to update container collections
*/
export const useUpdateContainerCollections = () => (
useMutation({
mutationFn: async (requestData: api.BulkMigrateRequestData) => api.bulkMigrateLegacyLibraries(requestData),
})
);

/**
* Get the migration status
*/
export const useMigrationStatus = (migrationId: string | null) => (
useQuery({
queryKey: legacyMigrationQueryKeys.migrationTask(migrationId),
queryFn: migrationId ? () => api.getMigrationStatus(migrationId!) : skipToken,
refetchInterval: 1000, // Refresh every second
})
);
15 changes: 15 additions & 0 deletions src/legacy-libraries-migration/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ const messages = defineMessages({
+ ' moved will be migrated to <b>{libraryName}</b>',
description: 'Alert text when the legacy library is already migrated.',
},
migrationInProgress: {
id: 'legacy-libraries-migration.confirmation-step.toast.migration-in-progress',
defaultMessage: '{count, plural, one {{count} legacy library is} other {{count} legacy libraries are}} being migrated.',
description: 'Toast message that indicates the legacy libraries are being migrated',
},
migrationFailed: {
id: 'legacy-libraries-migration.confirmation-step.toast.migration-failed',
defaultMessage: 'Legacy libraries migration failed.',
description: 'Toast message that indicates the migration of legacy libraries is failed',
},
migrationSuccess: {
id: 'legacy-libraries-migration.confirmation-step.toast.migration-success',
defaultMessage: 'The migration of legacy libraries has been completed successfully.',
description: 'Toast message that indicates the migration of legacy libraries is finished',
},
});

export default messages;
Loading