Compare commits

...

1 Commits

Author SHA1 Message Date
XnpioChV
49c28de286 feat: Base page for Migrate Legacy Libraries 2025-09-02 14:19:37 -05:00
8 changed files with 385 additions and 0 deletions

View File

@@ -35,6 +35,7 @@ import { ContentType } from './library-authoring/routes';
import 'react-datepicker/dist/react-datepicker.css';
import './index.scss';
import { LegacyLibMigrationPage } from './legacy-libraries-migration/LegacyLibMigrationPage';
const queryClient = new QueryClient({
defaultOptions: {
@@ -65,6 +66,7 @@ const App = () => {
<Route path="/home" element={<StudioHome />} />
<Route path="/libraries" element={<StudioHome />} />
<Route path="/libraries-v1" element={<StudioHome />} />
<Route path="/libraries-v1/migrate" element={<LegacyLibMigrationPage />} />
<Route path="/library/create" element={<CreateLibrary />} />
<Route path="/library/:libraryId/*" element={<LibraryLayout />} />
<Route

View File

@@ -0,0 +1,7 @@
import { Container } from '@openedx/paragon';
export const ConfirmatiobView = () => (
<Container>
Confirmation View
</Container>
);

View File

@@ -0,0 +1,63 @@
import {
initializeMocks,
render,
screen,
waitFor,
} from '@src/testUtils';
import { LegacyLibMigrationPage } from './LegacyLibMigrationPage';
const path = '/libraries-v1/migrate/*';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
const renderPage = () => (
render(<LegacyLibMigrationPage />, { path })
);
describe('<LegacyLibMigrationPage />', () => {
beforeEach(() => {
initializeMocks();
});
it('should render legacy library migration page', async () => {
renderPage();
// Should render the title
expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument();
// Should render the Migration Steps Viewer
expect(screen.getByText(/select legacy libraries/i)).toBeInTheDocument();
expect(screen.getByText(/select destination/i)).toBeInTheDocument();
expect(screen.getByText(/confirm/i)).toBeInTheDocument();
});
it('should cancel the migration', async () => {
renderPage();
expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument();
const cancelButton = screen.getByRole('button', { name: /cancel/i });
cancelButton.click();
// Should show exit confirmation modal
expect(await screen.findByText('Exit Migration?')).toBeInTheDocument();
// Close exit confirmation modal
const continueButton = screen.getByRole('button', { name: /continue migrating/i });
continueButton.click();
expect(mockNavigate).not.toHaveBeenCalled();
cancelButton.click();
// Should navigate to legacy libraries tab on studio home
expect(await screen.findByText('Exit Migration?')).toBeInTheDocument();
const exitButton = screen.getByRole('button', { name: /exit/i });
exitButton.click();
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/libraries-v1');
});
});
});

View File

@@ -0,0 +1,153 @@
import { useCallback, useState } from 'react';
import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Button,
Container,
ModalDialog,
Stepper,
useToggle,
} from '@openedx/paragon';
import Header from '@src/header';
import SubHeader from '@src/generic/sub-header/SubHeader';
import messages from './messages';
import { SelectLegacyLibraryView } from './SelectLegacyLibraryView';
import { SelectDestinationView } from './SelectDestinationView';
import { ConfirmatiobView } from './ConfirmationView';
import { MigrationStepsViewer } from './MigrationStepsViewer';
export type MigrationStep = 'select-libraries' | 'select-destination' | 'confirmation-view';
const ExitModal = ({
isExitModalOpen,
closeExitModal,
}: {
isExitModalOpen: boolean,
closeExitModal: () => void,
}) => {
const intl = useIntl();
const navigate = useNavigate();
const handleExit = useCallback(() => {
navigate('/libraries-v1');
}, []);
return (
<ModalDialog
title={intl.formatMessage(messages.exitModalTitle)}
isOpen={isExitModalOpen}
onClose={closeExitModal}
isOverflowVisible={false}
hasCloseButton={false}
>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages.exitModalTitle)}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{intl.formatMessage(messages.exitModalBodyText)}
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages.exitModalCancelText)}
</ModalDialog.CloseButton>
<Button onClick={handleExit}>
{intl.formatMessage(messages.exitModalConfirmText)}
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};
export const LegacyLibMigrationPage = () => {
const intl = useIntl();
const [currentStep, setCurrentStep] = useState<MigrationStep>('select-libraries');
const [isExitModalOpen, openExitModal, closeExitModal] = useToggle(false);
const handleNext = useCallback(() => {
switch (currentStep) {
case 'select-libraries':
setCurrentStep('select-destination');
break;
case 'select-destination':
setCurrentStep('confirmation-view');
break;
case 'confirmation-view':
// Handle confirm
break;
default:
break;
}
}, [currentStep, setCurrentStep]);
const handleBack = useCallback(() => {
switch (currentStep) {
case 'select-libraries':
openExitModal();
break;
case 'select-destination':
setCurrentStep('select-libraries');
break;
case 'confirmation-view':
setCurrentStep('select-destination');
break;
default:
break;
}
}, [currentStep, setCurrentStep]);
return (
<>
<div className="d-flex">
<div className="flex-grow-1">
<Helmet>
<title>
{intl.formatMessage(messages.siteTitle)}
</title>
</Helmet>
<Header isHiddenMainMenu />
<Container className="px-6 mt-5 mb-5">
<SubHeader
title={intl.formatMessage(messages.siteTitle)}
/>
<MigrationStepsViewer currentStep={currentStep} />
<Stepper activeKey={currentStep}>
<Stepper.Step eventKey="select-libraries" title="Select Legacy Libraries">
<SelectLegacyLibraryView />
</Stepper.Step>
<Stepper.Step eventKey="select-destination" title="Select Destination">
<SelectDestinationView />
</Stepper.Step>
<Stepper.Step eventKey="confirmation-view" title="Confirmation">
<ConfirmatiobView />
</Stepper.Step>
</Stepper>
<div className="d-flex justify-content-between">
<Button variant="outline-primary" onClick={handleBack}>
{currentStep === 'select-libraries'
? intl.formatMessage(messages.cancel)
: intl.formatMessage(messages.back)}
</Button>
<Button onClick={handleNext}>
{currentStep === 'confirmation-view'
? intl.formatMessage(messages.confirm)
: intl.formatMessage(messages.next)}
</Button>
</div>
</Container>
</div>
</div>
<ExitModal
isExitModalOpen={isExitModalOpen}
closeExitModal={closeExitModal}
/>
</>
);
};

View File

@@ -0,0 +1,80 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import type { MessageDescriptor } from 'react-intl';
import {
Bubble,
Container,
Icon,
Stack,
} from '@openedx/paragon';
import { Check } from '@openedx/paragon/icons';
import type { MigrationStep } from './LegacyLibMigrationPage';
import messages from './messages';
export const MigrationStepsViewer = ({ currentStep }: { currentStep: MigrationStep }) => {
const intl = useIntl();
const stepNumbers: Record<MigrationStep, number> = {
'select-libraries': 1,
'select-destination': 2,
'confirmation-view': 3,
};
const stepNames: Record<MigrationStep, MessageDescriptor> = {
'select-libraries': messages.selectLegacyLibrariesStepTitle,
'select-destination': messages.selectDestinationStepTitle,
'confirmation-view': messages.confirmStepTitle,
};
const checkStep = (step: MigrationStep) => {
if (currentStep === step) {
return 'current';
}
switch (step) {
case 'select-libraries':
// If is not current, then is done.
return 'done';
case 'select-destination':
if (currentStep === 'select-libraries') {
return 'disabled';
}
return 'done';
case 'confirmation-view':
// If is not current, then is disabled.
return 'disabled';
default:
return 'disabled';
}
return 'disabled';
};
const buildStep = (step: MigrationStep) => {
const stepStatus = checkStep(step);
return (
<Stack direction="horizontal">
<Bubble className="mr-2" disabled={stepStatus === 'disabled'}>
{stepStatus === 'done' ? (
<Icon src={Check} />
) : (
stepNumbers[step]
)}
</Bubble>
<div>
<span>
{intl.formatMessage(stepNames[step])}
</span>
</div>
</Stack>
);
};
return (
<Container className="d-flex justify-content-center migration-steps-viewer">
{buildStep('select-libraries')}
<hr className="ml-3 mr-3" style={{ width: '80px' }} />
{buildStep('select-destination')}
<hr className="ml-3 mr-3" style={{ width: '80px' }} />
{buildStep('confirmation-view')}
</Container>
);
};

View File

@@ -0,0 +1,7 @@
import { Container } from '@openedx/paragon';
export const SelectDestinationView = () => (
<Container>
SelectDestinationView
</Container>
);

View File

@@ -0,0 +1,7 @@
import { Container } from '@openedx/paragon';
export const SelectLegacyLibraryView = () => (
<Container>
Select Legacy LibraryStep
</Container>
);

View File

@@ -0,0 +1,66 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
siteTitle: {
id: 'legacy-libraries-migration.site-title',
defaultMessage: 'Migrate Legacy Libraries',
description: 'Title for the page to migrate legacy libraries.',
},
cancel: {
id: 'legacy-libraries-migration.button.cancel',
defaultMessage: 'Cancel',
description: 'Text of the button to cancel the migration.',
},
next: {
id: 'legacy-libraries-migration.button.next',
defaultMessage: 'Next',
description: 'Text of the button to go to the next step of the migration.',
},
back: {
id: 'legacy-libraries-migration.button.back',
defaultMessage: 'Back',
description: 'Text of the button to go back to the previous step of the migration.',
},
confirm: {
id: 'legacy-libraries-migration.button.confirm',
defaultMessage: 'Confirm',
description: 'Text of the button to confirm the migration.',
},
selectLegacyLibrariesStepTitle: {
id: 'legacy-libraries-migration.select-legacy-libraries-step.title',
defaultMessage: 'Select Legacy Libraries',
description: 'Title of the Select Legacy Libraries step',
},
selectDestinationStepTitle: {
id: 'legacy-libraries-migration.select-destination-step.title',
defaultMessage: 'Select Destination',
description: 'Title of the Select Destination step',
},
confirmStepTitle: {
id: 'legacy-libraries-migration.confirm-step.title',
defaultMessage: 'Confirm',
description: 'Title of the Confirm step',
},
exitModalTitle: {
id: 'legacy-libraries-migration.exit-modal.title',
defaultMessage: 'Exit Migration?',
description: 'Title of the modal to confirm exit the migration.',
},
exitModalBodyText: {
id: 'legacy-libraries-migration.exit-modal.body',
defaultMessage: 'By exiting, all changes will be lost and no libraries will be migrated.',
description: 'Body text of the modal to confirm exit the migration.',
},
exitModalCancelText: {
id: 'legacy-libraries-migration.exit-modal.button.cancel.text',
defaultMessage: 'Continue Migrating',
description: 'Text for the button to close the modal to confirm exit the migration.',
},
exitModalConfirmText: {
id: 'legacy-libraries-migration.exit-modal.button.confirm.text',
defaultMessage: 'Exit',
description: 'Text for the button to confirm exit the migration.',
},
});
export default messages;