From 49c28de286d3e1544548148477c6d8f98d5ddb9b Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 2 Sep 2025 14:19:37 -0500 Subject: [PATCH] feat: Base page for Migrate Legacy Libraries --- src/index.jsx | 2 + .../ConfirmationView.tsx | 7 + .../LegacyLibMigrationPage.test.tsx | 63 ++++++++ .../LegacyLibMigrationPage.tsx | 153 ++++++++++++++++++ .../MigrationStepsViewer.tsx | 80 +++++++++ .../SelectDestinationView.tsx | 7 + .../SelectLegacyLibraryView.tsx | 7 + src/legacy-libraries-migration/messages.ts | 66 ++++++++ 8 files changed, 385 insertions(+) create mode 100644 src/legacy-libraries-migration/ConfirmationView.tsx create mode 100644 src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx create mode 100644 src/legacy-libraries-migration/LegacyLibMigrationPage.tsx create mode 100644 src/legacy-libraries-migration/MigrationStepsViewer.tsx create mode 100644 src/legacy-libraries-migration/SelectDestinationView.tsx create mode 100644 src/legacy-libraries-migration/SelectLegacyLibraryView.tsx create mode 100644 src/legacy-libraries-migration/messages.ts diff --git a/src/index.jsx b/src/index.jsx index 620de1d47..db72d045a 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -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 = () => { } /> } /> } /> + } /> } /> } /> ( + + Confirmation View + +); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx new file mode 100644 index 000000000..6863bff99 --- /dev/null +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -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(, { path }) +); + +describe('', () => { + 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'); + }); + }); +}); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx new file mode 100644 index 000000000..d92b54dde --- /dev/null +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -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 ( + + + + {intl.formatMessage(messages.exitModalTitle)} + + + + {intl.formatMessage(messages.exitModalBodyText)} + + + + + {intl.formatMessage(messages.exitModalCancelText)} + + + + + + ); +}; + +export const LegacyLibMigrationPage = () => { + const intl = useIntl(); + const [currentStep, setCurrentStep] = useState('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 ( + <> +
+
+ + + {intl.formatMessage(messages.siteTitle)} + + +
+ + + + + + + + + + + + + + +
+ + +
+
+
+
+ + + ); +}; diff --git a/src/legacy-libraries-migration/MigrationStepsViewer.tsx b/src/legacy-libraries-migration/MigrationStepsViewer.tsx new file mode 100644 index 000000000..910a5bc39 --- /dev/null +++ b/src/legacy-libraries-migration/MigrationStepsViewer.tsx @@ -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 = { + 'select-libraries': 1, + 'select-destination': 2, + 'confirmation-view': 3, + }; + const stepNames: Record = { + '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 ( + + + {stepStatus === 'done' ? ( + + ) : ( + stepNumbers[step] + )} + +
+ + {intl.formatMessage(stepNames[step])} + +
+
+ ); + }; + + return ( + + {buildStep('select-libraries')} +
+ {buildStep('select-destination')} +
+ {buildStep('confirmation-view')} +
+ ); +}; diff --git a/src/legacy-libraries-migration/SelectDestinationView.tsx b/src/legacy-libraries-migration/SelectDestinationView.tsx new file mode 100644 index 000000000..e696db3aa --- /dev/null +++ b/src/legacy-libraries-migration/SelectDestinationView.tsx @@ -0,0 +1,7 @@ +import { Container } from '@openedx/paragon'; + +export const SelectDestinationView = () => ( + + SelectDestinationView + +); diff --git a/src/legacy-libraries-migration/SelectLegacyLibraryView.tsx b/src/legacy-libraries-migration/SelectLegacyLibraryView.tsx new file mode 100644 index 000000000..cc474d19c --- /dev/null +++ b/src/legacy-libraries-migration/SelectLegacyLibraryView.tsx @@ -0,0 +1,7 @@ +import { Container } from '@openedx/paragon'; + +export const SelectLegacyLibraryView = () => ( + + Select Legacy LibraryStep + +); diff --git a/src/legacy-libraries-migration/messages.ts b/src/legacy-libraries-migration/messages.ts new file mode 100644 index 000000000..6935379c3 --- /dev/null +++ b/src/legacy-libraries-migration/messages.ts @@ -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;