From 49c28de286d3e1544548148477c6d8f98d5ddb9b Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 2 Sep 2025 14:19:37 -0500 Subject: [PATCH 01/20] 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 620de1d470..db72d045a7 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 0000000000..6863bff992 --- /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 0000000000..d92b54dde2 --- /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 0000000000..910a5bc39d --- /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 0000000000..e696db3aa7 --- /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 0000000000..cc474d19c9 --- /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 0000000000..6935379c3e --- /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; From 308f876e233ce43ec44735e4f7151088ff6e02d7 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 2 Sep 2025 14:39:46 -0500 Subject: [PATCH 02/20] feat: Add link in the Migrate Legacy Libraries alert --- .../MigrateLegacyLibrariesAlert.tsx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/studio-home/tabs-section/libraries-tab/MigrateLegacyLibrariesAlert.tsx b/src/studio-home/tabs-section/libraries-tab/MigrateLegacyLibrariesAlert.tsx index d4a35c95af..61e42ed5a2 100644 --- a/src/studio-home/tabs-section/libraries-tab/MigrateLegacyLibrariesAlert.tsx +++ b/src/studio-home/tabs-section/libraries-tab/MigrateLegacyLibrariesAlert.tsx @@ -1,23 +1,33 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Alert, Button } from '@openedx/paragon'; import { Warning } from '@openedx/paragon/icons'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from '../messages'; -export const MigrateLegacyLibrariesAlert = () => ( - - - - -
-
- -
-
- +export const MigrateLegacyLibrariesAlert = () => { + const navigate = useNavigate(); + + const handleClick = useCallback(() => { + navigate('migrate'); + }, []); + + return ( + + + + +
+
+ +
+
+ +
-
- -); + + ); +}; From 6453f8434debae533f46ba17adb4cf7c18755935 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 2 Sep 2025 19:24:18 -0500 Subject: [PATCH 03/20] feat: Build SelectDestinationView * Refactor LibrariesV2List to use in this new view * Refactor CardItem to use in this new view --- .../LegacyLibMigrationPage.test.tsx | 17 ++ .../LegacyLibMigrationPage.tsx | 21 +- .../MigrationStepsViewer.tsx | 2 +- .../SelectDestinationView.tsx | 32 ++- src/legacy-libraries-migration/messages.ts | 8 + src/studio-home/card-item/index.tsx | 20 +- src/studio-home/tabs-section/index.tsx | 22 +- .../tabs-section/libraries-v2-tab/index.tsx | 221 +++++++++++------- 8 files changed, 240 insertions(+), 103 deletions(-) diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 6863bff992..0ef3f41d2a 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -60,4 +60,21 @@ describe('', () => { expect(mockNavigate).toHaveBeenCalledWith('/libraries-v1'); }); }); + + it('should select a library destination', async () => { + renderPage(); + expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + + // TODO Missing select legacy libraries + 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(); + + // The next button is disabled + expect(nextButton).toBeDisabled(); + + // TODO select library destination + }); }); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index d92b54dde2..0baf1bdd4b 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -70,6 +70,7 @@ export const LegacyLibMigrationPage = () => { const intl = useIntl(); const [currentStep, setCurrentStep] = useState('select-libraries'); const [isExitModalOpen, openExitModal, closeExitModal] = useToggle(false); + const [destinationLibraryId, setDestination] = useState(); const handleNext = useCallback(() => { switch (currentStep) { @@ -93,6 +94,7 @@ export const LegacyLibMigrationPage = () => { openExitModal(); break; case 'select-destination': + setDestination(undefined); setCurrentStep('select-libraries'); break; case 'confirmation-view': @@ -103,6 +105,21 @@ export const LegacyLibMigrationPage = () => { } }, [currentStep, setCurrentStep]); + const isNextDisabled = useCallback(() => { + switch (currentStep) { + case 'select-libraries': + // TODO + return false; + case 'select-destination': + return destinationLibraryId === undefined; + case 'confirmation-view': + // TODO + return false; + default: + return true; + } + }, [currentStep, destinationLibraryId]); + return ( <>
@@ -123,7 +140,7 @@ export const LegacyLibMigrationPage = () => { - + @@ -135,7 +152,7 @@ export const LegacyLibMigrationPage = () => { ? intl.formatMessage(messages.cancel) : intl.formatMessage(messages.back)} - + + ) + ); +}; + +interface Props { + selectedLibraryId?: string | null; + handleSelect?: ((libraryId: string) => void) | null; +} -const LibrariesV2Tab: React.FC = () => { +const LibrariesV2List: React.FC = ({ + selectedLibraryId = null, + handleSelect = null, +}) => { const intl = useIntl(); const [currentPage, setCurrentPage] = useState(1); const [filterParams, setFilterParams] = useState({}); const isFiltered = Object.keys(filterParams).length > 0; + const inSelectMode = handleSelect !== null; const handlePageSelect = (page: number) => { setCurrentPage(page); @@ -51,95 +110,79 @@ const LibrariesV2Tab: React.FC = () => { const hasV2Libraries = !isLoading && !isError && ((data!.results.length || 0) > 0); - // TODO: update this link when tutorial is ready. - const librariesTutorialLink = ( - - {intl.formatMessage(messages.librariesV2TabBetaTutorialLinkText)} - - ); - return ( - <> - - {intl.formatMessage( - messages.librariesV2TabBetaText, - { link: librariesTutorialLink }, + isError ? ( + + + {intl.formatMessage(messages.librariesTabErrorMessage)} + )} - - - {isError ? ( - - - {intl.formatMessage(messages.librariesTabErrorMessage)} - + /> + ) : ( +
+
+ + {!isLoading && !isError + && ( +

+ {intl.formatMessage(messages.coursesPaginationInfo, { + length: data!.results.length, + total: data!.count, + })} +

)} - /> - ) : ( -
-
- + + {inSelectMode ? ( + handleSelect?.(e.target.value)} + > + - {!isLoading && !isError - && ( -

- {intl.formatMessage(messages.coursesPaginationInfo, { - length: data!.results.length, - total: data!.count, - })} -

- )} -
- - { hasV2Libraries - ? data!.results.map(({ - id, org, slug, title, - }) => ( - - )) : isFiltered && !isLoading && ( - - - {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)} - -

- {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)} -

- -
- )} - - { - hasV2Libraries && (data!.numPages || 0) > 1 - && ( - - ) - } -
- )} - + + ) : ( + + )} + + { + hasV2Libraries && (data!.numPages || 0) > 1 + && ( + + ) + } +
+ ) ); }; -export default LibrariesV2Tab; +export default LibrariesV2List; From 5d5e6a321894d9a4380be8ffe7593f66cc915d35 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 2 Sep 2025 19:43:42 -0500 Subject: [PATCH 04/20] style: Fix broken lints --- src/studio-home/card-item/index.tsx | 74 +++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/src/studio-home/card-item/index.tsx b/src/studio-home/card-item/index.tsx index 48d769fbc3..8c2e2e6eb2 100644 --- a/src/studio-home/card-item/index.tsx +++ b/src/studio-home/card-item/index.tsx @@ -16,6 +16,51 @@ import { COURSE_CREATOR_STATES } from '../../constants'; import { getStudioHomeData } from '../data/selectors'; import messages from '../messages'; +interface CardTitleProps { + readOnlyItem: boolean; + inSelectMode: boolean; + destinationUrl: string; + hasDisplayName: string; + displayName: string; + itemId?: string | null; +} + +const CardTitle: React.FC = ({ + readOnlyItem, + inSelectMode, + destinationUrl, + hasDisplayName, + displayName, + itemId, +}) => { + if (!readOnlyItem && !inSelectMode) { + return ( + + {hasDisplayName} + + ); + } + if (inSelectMode) { + return ( + + + {displayName} + + + ); + } + + return ( + {displayName} + ); +}; + interface BaseProps { displayName: string; org: string; @@ -80,26 +125,15 @@ const CardItem: React.FC = ({ - {hasDisplayName} - - ) : ( - inSelectMode ? ( - - - {displayName} - - - ) : ( - {displayName} - ) + title={( + )} subtitle={subtitle} actions={showActions && ( From f80bd59de87d487c7bc5b38b04b5a71c6e45d0ec Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 3 Sep 2025 16:24:51 -0500 Subject: [PATCH 05/20] feat: Add create library modal in migrate legacy libraries page * Refactor CreateLibrary.tsx to enable to put it in a modal * Create CreateLibraryModal.tsx to use CreateLibrary.tsx as a modal --- .../SelectDestinationView.tsx | 1 + .../create-library/CreateLibrary.tsx | 64 +++++++++++++------ .../create-library/CreateLibraryModal.tsx | 41 ++++++++++++ src/library-authoring/create-library/index.ts | 3 +- src/library-authoring/index.tsx | 2 +- .../tabs-section/libraries-v2-tab/index.tsx | 49 +++++++++++--- src/studio-home/tabs-section/messages.ts | 5 ++ 7 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 src/library-authoring/create-library/CreateLibraryModal.tsx diff --git a/src/legacy-libraries-migration/SelectDestinationView.tsx b/src/legacy-libraries-migration/SelectDestinationView.tsx index f08dc4a14c..2b7e127965 100644 --- a/src/legacy-libraries-migration/SelectDestinationView.tsx +++ b/src/legacy-libraries-migration/SelectDestinationView.tsx @@ -21,6 +21,7 @@ export const SelectDestinationView = ({ ); diff --git a/src/library-authoring/create-library/CreateLibrary.tsx b/src/library-authoring/create-library/CreateLibrary.tsx index e88c4ffa07..8066c3d767 100644 --- a/src/library-authoring/create-library/CreateLibrary.tsx +++ b/src/library-authoring/create-library/CreateLibrary.tsx @@ -10,19 +10,29 @@ import { import { Formik } from 'formik'; import { useNavigate } from 'react-router-dom'; import * as Yup from 'yup'; +import classNames from 'classnames'; + +import { REGEX_RULES } from '@src/constants'; +import { useOrganizationListData } from '@src/generic/data/apiHooks'; +import { useStudioHome } from '@src/studio-home/hooks'; +import Header from '@src/header'; +import SubHeader from '@src/generic/sub-header/SubHeader'; +import FormikControl from '@src/generic/FormikControl'; +import FormikErrorFeedback from '@src/generic/FormikErrorFeedback'; +import AlertError from '@src/generic/alert-error'; -import { REGEX_RULES } from '../../constants'; -import Header from '../../header'; -import FormikControl from '../../generic/FormikControl'; -import FormikErrorFeedback from '../../generic/FormikErrorFeedback'; -import AlertError from '../../generic/alert-error'; -import { useOrganizationListData } from '../../generic/data/apiHooks'; -import SubHeader from '../../generic/sub-header/SubHeader'; -import { useStudioHome } from '../../studio-home/hooks'; import { useCreateLibraryV2 } from './data/apiHooks'; import messages from './messages'; -const CreateLibrary = () => { +export const CreateLibrary = ({ + showInModal = false, + handleCancel = null, + handlePostCreate = null, +}: { + showInModal?: boolean, + handleCancel?: (() => void) | null, + handlePostCreate?: ((libraryId: string) => void) | null, +}) => { const intl = useIntl(); const navigate = useNavigate(); @@ -56,20 +66,30 @@ const CreateLibrary = () => { ) || []; const handleOnClickCancel = () => { - navigate('/libraries'); + if (handleCancel) { + handleCancel(); + } else { + navigate('/libraries'); + } }; if (data) { - navigate(`/library/${data.id}`); + if (handlePostCreate) { + handlePostCreate(data.id); + } else { + navigate(`/library/${data.id}`); + } } return ( <> -
+ {!showInModal && (
)} - + {!showInModal && ( + + )} { className="" controlClasses="pb-2" /> - + + )} + + {!isLoading && !isError && (

@@ -180,6 +204,11 @@ const LibrariesV2List: React.FC = ({ /> ) } +

) ); diff --git a/src/studio-home/tabs-section/messages.ts b/src/studio-home/tabs-section/messages.ts index db60d65367..38fe63855a 100644 --- a/src/studio-home/tabs-section/messages.ts +++ b/src/studio-home/tabs-section/messages.ts @@ -106,6 +106,11 @@ const messages = defineMessages({ defaultMessage: 'Review Legacy Libraries', description: 'Label for the button to review legacy libraries', }, + createLibraryButton: { + id: 'studio-home.legacy-libraries.migrate.create-button', + defaultMessage: 'Create New Library', + description: 'Label for the button to create a new library in the library list view.', + }, }); export default messages; From c5ab851de6c75b6c2c14048a6ee53cdfa26ff665 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 3 Sep 2025 17:42:36 -0500 Subject: [PATCH 06/20] test: Adding test for select destination view --- .../LegacyLibMigrationPage.test.tsx | 83 ++++++++++++++++++- .../create-library/CreateLibrary.test.tsx | 4 +- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 0ef3f41d2a..37d5e21114 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -1,13 +1,23 @@ +import type MockAdapter from 'axios-mock-adapter'; +import userEvent from '@testing-library/user-event'; + import { initializeMocks, render, screen, waitFor, } from '@src/testUtils'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; +import { mockGetContentLibraryV2List } from '@src/library-authoring/data/api.mocks'; +import { getContentLibraryV2CreateApiUrl } from '@src/library-authoring/create-library/data/api'; +import { getStudioHomeApiUrl } from '@src/studio-home/data/api'; import { LegacyLibMigrationPage } from './LegacyLibMigrationPage'; const path = '/libraries-v1/migrate/*'; +let axiosMock: MockAdapter; + +mockGetContentLibraryV2List.applyMock(); const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -15,13 +25,21 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate, })); +jest.mock('@src/generic/data/apiHooks', () => ({ + ...jest.requireActual('@src/generic/data/apiHooks'), + useOrganizationListData: () => ({ + data: ['org1', 'org2', 'org3', 'org4', 'org5'], + isLoading: false, + }), +})); + const renderPage = () => ( render(, { path }) ); describe('', () => { beforeEach(() => { - initializeMocks(); + axiosMock = initializeMocks().axiosMock; }); it('should render legacy library migration page', async () => { @@ -75,6 +93,67 @@ describe('', () => { // The next button is disabled expect(nextButton).toBeDisabled(); - // TODO select library destination + expect(await screen.findByText('Test Library 1')).toBeInTheDocument(); + const radioButton = screen.getByRole('radio', { name: /test library 1/i }); + radioButton.click(); + + expect(radioButton).toBeChecked(); + expect(nextButton).not.toBeDisabled(); + }); + + it('should open the create new library modal', async () => { + const user = userEvent.setup(); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); + axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { + id: 'library-id', + }); + + renderPage(); + expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + + // TODO Missing select legacy libraries + 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(); + + const createButton = screen.getByRole('button', { name: /create new library/i }); + expect(createButton).toBeInTheDocument(); + createButton.click(); + + // Should open the create library modal + expect(await screen.findByText('Create new library')).toBeInTheDocument(); + + // Cancel and close the create library modal + const cancelButton = screen.getByRole('button', { name: /cancel/i }); + cancelButton.click(); + await waitFor(() => { + expect(screen.queryByText('Create new library')).not.toBeInTheDocument(); + }); + + // Open the modal again and create a new library + createButton.click(); + const titleInput = await screen.findByRole('textbox', { name: /library name/i }); + await user.click(titleInput); + await user.type(titleInput, 'Test Library Name'); + + const orgInput = await screen.findByRole('combobox', { name: /organization/i }); + await user.click(orgInput); + await user.type(orgInput, 'org1'); + await user.tab(); + + const slugInput = await screen.findByRole('textbox', { name: /library id/i }); + await user.click(slugInput); + await user.type(slugInput, 'test_library_slug'); + + const confirmButton = await screen.findByRole('button', { name: 'Create' }); + confirmButton.click(); + await waitFor(() => { + expect(axiosMock.history.post.length).toBe(1); + }); + expect(axiosMock.history.post[0].data).toBe( + '{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}', + ); }); }); diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx index eb45d80073..cf73083982 100644 --- a/src/library-authoring/create-library/CreateLibrary.test.tsx +++ b/src/library-authoring/create-library/CreateLibrary.test.tsx @@ -23,8 +23,8 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate, })); -jest.mock('../../generic/data/apiHooks', () => ({ - ...jest.requireActual('../../generic/data/apiHooks'), +jest.mock('@src/generic/data/apiHooks', () => ({ + ...jest.requireActual('@src/generic/data/apiHooks'), useOrganizationListData: () => ({ data: ['org1', 'org2', 'org3', 'org4', 'org5'], isLoading: false, From f2ef6805c67a1d5c0a2e6b3013a2771af631ff8b Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 4 Sep 2025 20:42:17 -0500 Subject: [PATCH 07/20] feat: ConfirmationView step created --- src/index.scss | 1 + .../ConfirmationView.tsx | 90 ++++++++++++++++++- .../LegacyLibMigrationPage.test.tsx | 36 +++++++- .../LegacyLibMigrationPage.tsx | 57 +++++++++--- .../MigrationStepsViewer.tsx | 6 +- .../SelectDestinationView.tsx | 15 ++-- src/legacy-libraries-migration/index.scss | 13 +++ src/legacy-libraries-migration/messages.ts | 13 +++ .../create-library/CreateLibrary.tsx | 5 +- .../create-library/CreateLibraryModal.tsx | 3 +- .../tabs-section/libraries-v2-tab/index.tsx | 36 +++++--- 11 files changed, 235 insertions(+), 40 deletions(-) create mode 100644 src/legacy-libraries-migration/index.scss diff --git a/src/index.scss b/src/index.scss index cd18b8836d..8fb7da5be2 100644 --- a/src/index.scss +++ b/src/index.scss @@ -30,6 +30,7 @@ @import "certificates/scss/Certificates"; @import "group-configurations/GroupConfigurations"; @import "optimizer-page/scan-results/ScanResults"; +@import "legacy-libraries-migration/"; // To apply the glow effect to the selected Section/Subsection, in the Course Outline div.row:has(> div > div.highlight) { diff --git a/src/legacy-libraries-migration/ConfirmationView.tsx b/src/legacy-libraries-migration/ConfirmationView.tsx index 4c7073d3fe..95f7d7beb6 100644 --- a/src/legacy-libraries-migration/ConfirmationView.tsx +++ b/src/legacy-libraries-migration/ConfirmationView.tsx @@ -1,7 +1,89 @@ -import { Container } from '@openedx/paragon'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + Alert, + Card, + Container, + Icon, + Stack, +} from '@openedx/paragon'; +import { + AccessTime, + Folder, + SubdirectoryArrowRight, +} from '@openedx/paragon/icons'; -export const ConfirmatiobView = () => ( - - Confirmation View +import type { ContentLibrary } from '@src/library-authoring/data/api'; + +import messages from './messages'; + +const BoldText = (chunk: string[]) => {chunk}; + +interface ConfirmationCardProps { + legacyLib: any; + destinationName: string; +} + +const ConfirmationCard = ({ + legacyLib, + destinationName, +}: ConfirmationCardProps) => ( + + + + {legacyLib.title} + + )} + subtitle={( + + + {destinationName} + + )} + /> + {legacyLib.migratedIn && ( + + + + + + + )} + +); + +interface ConfirmationViewProps { + destination: ContentLibrary | undefined; + legacyLibraries: any[]; +} + +export const ConfirmationView = ({ + destination, + legacyLibraries, +}: ConfirmationViewProps) => ( + + + + + {legacyLibraries.map((legacyLib) => ( + + ))} ); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 37d5e21114..264388625f 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -105,7 +105,7 @@ describe('', () => { const user = userEvent.setup(); axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { - id: 'library-id', + id: 'lib:SampleTaxonomyOrg1:TL1', }); renderPage(); @@ -155,5 +155,39 @@ describe('', () => { expect(axiosMock.history.post[0].data).toBe( '{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}', ); + + // The library should be checked + expect(screen.getByRole('radio', { name: /test library 1/i })).toBeChecked(); + }); + + it('should confirm migration', async () => { + renderPage(); + expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + + // TODO Missing select legacy libraries + 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 4 legacy libraries will be migrated to/i)).toBeInTheDocument(); + // TODO update with legacy libraries names + expect(screen.getByText('Legacy Lib 1')).toBeInTheDocument(); + expect(screen.getByText('Legacy Lib 3')).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(); + + // TODO: expect call migrate API }); }); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index 0baf1bdd4b..0f2c7e1563 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -8,16 +8,18 @@ import { Button, Container, ModalDialog, + StatefulButton, Stepper, useToggle, } from '@openedx/paragon'; import Header from '@src/header'; import SubHeader from '@src/generic/sub-header/SubHeader'; +import type { ContentLibrary } from '@src/library-authoring/data/api'; import messages from './messages'; import { SelectLegacyLibraryView } from './SelectLegacyLibraryView'; import { SelectDestinationView } from './SelectDestinationView'; -import { ConfirmatiobView } from './ConfirmationView'; +import { ConfirmationView } from './ConfirmationView'; import { MigrationStepsViewer } from './MigrationStepsViewer'; export type MigrationStep = 'select-libraries' | 'select-destination' | 'confirmation-view'; @@ -70,7 +72,8 @@ export const LegacyLibMigrationPage = () => { const intl = useIntl(); const [currentStep, setCurrentStep] = useState('select-libraries'); const [isExitModalOpen, openExitModal, closeExitModal] = useToggle(false); - const [destinationLibraryId, setDestination] = useState(); + const [destinationLibrary, setDestination] = useState(); + const [confirmationButtonState, setConfirmationButtonState] = useState('default'); const handleNext = useCallback(() => { switch (currentStep) { @@ -81,7 +84,8 @@ export const LegacyLibMigrationPage = () => { setCurrentStep('confirmation-view'); break; case 'confirmation-view': - // Handle confirm + setConfirmationButtonState('pending'); + // TODO Call migration API break; default: break; @@ -111,18 +115,26 @@ export const LegacyLibMigrationPage = () => { // TODO return false; case 'select-destination': - return destinationLibraryId === undefined; + return destinationLibrary === undefined; case 'confirmation-view': - // TODO return false; default: return true; } - }, [currentStep, destinationLibraryId]); + }, [currentStep, destinationLibrary]); + + // TODO Set this after implement SelectLegacyLibraryView + const legacyLibCount = 1; + const legacyLibs = [ + { title: 'Legacy Lib 1', migratedIn: undefined }, + { title: 'Legacy Lib 2', migratedIn: undefined }, + { title: 'Legacy Lib 3', migratedIn: 'Lib1 Large' }, + { title: 'Legacy Lib 4', migratedIn: undefined }, + ]; return ( <> -
+
@@ -140,10 +152,17 @@ export const LegacyLibMigrationPage = () => { <SelectLegacyLibraryView /> </Stepper.Step> <Stepper.Step eventKey="select-destination" title="Select Destination"> - <SelectDestinationView destinationId={destinationLibraryId} setDestinationId={setDestination} /> + <SelectDestinationView + destinationId={destinationLibrary?.id} + setDestinationId={setDestination} + legacyLibCount={legacyLibCount} + /> </Stepper.Step> <Stepper.Step eventKey="confirmation-view" title="Confirmation"> - <ConfirmatiobView /> + <ConfirmationView + destination={destinationLibrary} + legacyLibraries={legacyLibs} + /> </Stepper.Step> </Stepper> <div className="d-flex justify-content-between"> @@ -152,11 +171,21 @@ export const LegacyLibMigrationPage = () => { ? intl.formatMessage(messages.cancel) : intl.formatMessage(messages.back)} </Button> - <Button onClick={handleNext} disabled={isNextDisabled()}> - {currentStep === 'confirmation-view' - ? intl.formatMessage(messages.confirm) - : intl.formatMessage(messages.next)} - </Button> + {currentStep !== 'confirmation-view' ? ( + <Button onClick={handleNext} disabled={isNextDisabled()}> + {intl.formatMessage(messages.next)} + </Button> + ) : ( + <StatefulButton + state={confirmationButtonState} + disabledStates={['pending']} + labels={{ + default: intl.formatMessage(messages.confirm), + pending: intl.formatMessage(messages.confirm), + }} + onClick={handleNext} + /> + )} </div> </Container> </div> diff --git a/src/legacy-libraries-migration/MigrationStepsViewer.tsx b/src/legacy-libraries-migration/MigrationStepsViewer.tsx index b5fc58776e..cc4483a419 100644 --- a/src/legacy-libraries-migration/MigrationStepsViewer.tsx +++ b/src/legacy-libraries-migration/MigrationStepsViewer.tsx @@ -69,11 +69,11 @@ export const MigrationStepsViewer = ({ currentStep }: { currentStep: MigrationSt }; return ( - <Container className="d-flex justify-content-center migration-steps-viewer mt-4 mb-4"> + <Container className="migration-steps-viewer d-flex justify-content-center mt-4 mb-4"> {buildStep('select-libraries')} - <hr className="ml-3 mr-3" style={{ width: '80px' }} /> + <hr className="ml-3 mr-3" /> {buildStep('select-destination')} - <hr className="ml-3 mr-3" style={{ width: '80px' }} /> + <hr className="ml-3 mr-3" /> {buildStep('confirmation-view')} </Container> ); diff --git a/src/legacy-libraries-migration/SelectDestinationView.tsx b/src/legacy-libraries-migration/SelectDestinationView.tsx index 2b7e127965..f5cb9fdccb 100644 --- a/src/legacy-libraries-migration/SelectDestinationView.tsx +++ b/src/legacy-libraries-migration/SelectDestinationView.tsx @@ -1,22 +1,27 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Alert, Container } from '@openedx/paragon'; import LibrariesV2List from '@src/studio-home/tabs-section/libraries-v2-tab'; +import type { ContentLibrary } from '@src/library-authoring/data/api'; import messages from './messages'; +interface SelectDestinationViewProps { + destinationId: string | undefined; + setDestinationId: (library: ContentLibrary) => void; + legacyLibCount: number; +} + export const SelectDestinationView = ({ destinationId, setDestinationId, -} : { - destinationId?: string | null, - setDestinationId: (libraryId: string) => void, -}) => { + legacyLibCount, +}: SelectDestinationViewProps) => { const intl = useIntl(); return ( <Container> <Alert variant="info"> - {intl.formatMessage(messages.selectDestinationAlert, { count: 1 })} + {intl.formatMessage(messages.selectDestinationAlert, { count: legacyLibCount })} </Alert> <LibrariesV2List selectedLibraryId={destinationId} diff --git a/src/legacy-libraries-migration/index.scss b/src/legacy-libraries-migration/index.scss new file mode 100644 index 0000000000..95288f6903 --- /dev/null +++ b/src/legacy-libraries-migration/index.scss @@ -0,0 +1,13 @@ +.legacy-library-migration-page { + .confirmation-view { + .pgn__card-header-content { + margin-top: calc(var(--pgn-spacing-spacer-base)) !important;; + } + } + + .migration-steps-viewer { + hr { + width: 80px; + } + } +} \ No newline at end of file diff --git a/src/legacy-libraries-migration/messages.ts b/src/legacy-libraries-migration/messages.ts index a4ac90b35b..b0d5b4e8b9 100644 --- a/src/legacy-libraries-migration/messages.ts +++ b/src/legacy-libraries-migration/messages.ts @@ -69,6 +69,19 @@ const messages = defineMessages({ + ' problem banks will maintain their link with migrated content the first time they are migrated.', description: 'Alert text in the select destination step of the legacy libraries migration page.', }, + confirmationViewAlert: { + id: 'legacy-libraries-migration.select-destination.alert.text', + defaultMessage: 'These {count, plural, one {{count} legacy library} other {{count} legacy libraries}}' + + ' will be migrated to <b>{libraryName}</b> and organized as collections. Any legacy libraries that are used in' + + ' problem banks will maintain their link with migrated content the first time they are migrated.', + description: 'Alert text in the confirmation step of the legacy libraries migration page.', + }, + previouslyMigratedAlert: { + id: 'legacy-libraries-migration.confirmation-step.card.previously-migrated.text', + defaultMessage: 'Previously migrated library. Any problem bank links were already' + + ' moved will be migrated to <b>{libraryName}</b>', + description: 'Alert text when the legacy library is already migrated.', + }, }); export default messages; diff --git a/src/library-authoring/create-library/CreateLibrary.tsx b/src/library-authoring/create-library/CreateLibrary.tsx index 8066c3d767..1a94773bee 100644 --- a/src/library-authoring/create-library/CreateLibrary.tsx +++ b/src/library-authoring/create-library/CreateLibrary.tsx @@ -23,6 +23,7 @@ import AlertError from '@src/generic/alert-error'; import { useCreateLibraryV2 } from './data/apiHooks'; import messages from './messages'; +import type { ContentLibrary } from '../data/api'; export const CreateLibrary = ({ showInModal = false, @@ -31,7 +32,7 @@ export const CreateLibrary = ({ }: { showInModal?: boolean, handleCancel?: (() => void) | null, - handlePostCreate?: ((libraryId: string) => void) | null, + handlePostCreate?: ((library: ContentLibrary) => void) | null, }) => { const intl = useIntl(); const navigate = useNavigate(); @@ -75,7 +76,7 @@ export const CreateLibrary = ({ if (data) { if (handlePostCreate) { - handlePostCreate(data.id); + handlePostCreate(data); } else { navigate(`/library/${data.id}`); } diff --git a/src/library-authoring/create-library/CreateLibraryModal.tsx b/src/library-authoring/create-library/CreateLibraryModal.tsx index 6e720251f4..2abcb3833a 100644 --- a/src/library-authoring/create-library/CreateLibraryModal.tsx +++ b/src/library-authoring/create-library/CreateLibraryModal.tsx @@ -3,11 +3,12 @@ import { ModalDialog } from '@openedx/paragon'; import messages from './messages'; import { CreateLibrary } from './CreateLibrary'; +import type { ContentLibrary } from '../data/api'; interface CreateLibraryModalProps { isOpen: boolean; onClose: () => void; - handlePostCreate?: ((libraryId: string) => void) | null, + handlePostCreate?: ((library: ContentLibrary) => void) | null, } export const CreateLibraryModal = ({ diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index 186a4968ea..36d3b68909 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -15,7 +15,7 @@ import { Add, Error } from '@openedx/paragon/icons'; import { CreateLibraryModal, useContentLibraryV2List } from '@src/library-authoring'; import { LoadingSpinner } from '@src/generic/Loading'; import AlertMessage from '@src/generic/alert-message'; -import type { LibrariesV2Response } from '@src/library-authoring/data/api'; +import type { ContentLibrary, LibrariesV2Response } from '@src/library-authoring/data/api'; import CardItem from '../../card-item'; import messages from '../messages'; @@ -72,7 +72,7 @@ const CardList: React.FC<CardListProps> = ({ interface Props { selectedLibraryId?: string | null; - handleSelect?: ((libraryId: string) => void) | null; + handleSelect?: ((library: ContentLibrary) => void) | null; showCreateLibrary?: boolean; } @@ -99,19 +99,35 @@ const LibrariesV2List: React.FC<Props> = ({ setCurrentPage(1); }; - const handlePostCreateLibrary = useCallback((libraryId: string) => { - if (handleSelect) { - handleSelect(libraryId); - closeCreateLibrary(); - } - }, [handleSelect, closeCreateLibrary]); - const { data, isLoading, isError, } = useContentLibraryV2List({ page: currentPage, ...filterParams }); + const findLibrary = useCallback((libraryId: string) => { + if (data) { + return data.results.find((library) => library.id === libraryId); + } + return undefined; + }, [data]); + + const handlePostCreateLibrary = useCallback((library: ContentLibrary) => { + if (handleSelect) { + handleSelect(library); + closeCreateLibrary(); + } + }, [handleSelect, closeCreateLibrary]); + + const handleOnChangeRadioSet = useCallback((libraryId: string) => { + if (handleSelect) { + const library = findLibrary(libraryId); + if (library) { + handleSelect(library); + } + } + }, [findLibrary, handleSelect]); + if (isLoading && !isFiltered) { return ( <Row className="m-0 mt-4 justify-content-center"> @@ -170,7 +186,7 @@ const LibrariesV2List: React.FC<Props> = ({ <Form.RadioSet name="select-libraries-v2-list" value={selectedLibraryId} - onChange={(e) => handleSelect?.(e.target.value)} + onChange={(e) => handleOnChangeRadioSet(e.target.value)} > <CardList hasV2Libraries={hasV2Libraries} From 930554f264eb330959d59a0652c873dffa586782 Mon Sep 17 00:00:00 2001 From: XnpioChV <xnpiochv@gmail.com> Date: Fri, 5 Sep 2025 13:02:22 -0500 Subject: [PATCH 08/20] style: Fix types --- .../ConfirmationView.tsx | 2 +- .../tabs-section/libraries-v2-tab/index.tsx | 70 +++++++++++-------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/legacy-libraries-migration/ConfirmationView.tsx b/src/legacy-libraries-migration/ConfirmationView.tsx index 95f7d7beb6..4335965868 100644 --- a/src/legacy-libraries-migration/ConfirmationView.tsx +++ b/src/legacy-libraries-migration/ConfirmationView.tsx @@ -82,7 +82,7 @@ export const ConfirmationView = ({ {legacyLibraries.map((legacyLib) => ( <ConfirmationCard legacyLib={legacyLib} - destinationName={destination?.title} + destinationName={destination?.title ?? ''} /> ))} </Container> diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index 36d3b68909..9c18aa5bba 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -39,35 +39,47 @@ const CardList: React.FC<CardListProps> = ({ handleClearFilters, }) => { const intl = useIntl(); - return ( - hasV2Libraries - ? data!.results.map(({ - id, org, slug, title, - }) => ( - <CardItem - key={`${org}+${slug}`} - isLibraries - displayName={title} - org={org} - number={slug} - path={`/library/${id}`} - inSelectMode={inSelectMode} - itemId={id} - /> - )) : isFiltered && !isLoading && ( - <Alert className="mt-4"> - <Alert.Heading> - {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)} - </Alert.Heading> - <p> - {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)} - </p> - <Button variant="primary" onClick={handleClearFilters}> - {intl.formatMessage(messages.coursesTabCourseNotFoundAlertCleanFiltersButton)} - </Button> - </Alert> - ) - ); + + if (hasV2Libraries) { + return ( + <> + { + data!.results.map(({ + id, org, slug, title, + }) => ( + <CardItem + key={`${org}+${slug}`} + isLibraries + displayName={title} + org={org} + number={slug} + path={`/library/${id}`} + inSelectMode={inSelectMode} + itemId={id} + /> + )) + } + </> + ); + } + + // Empty alert + if (isFiltered && !isLoading) { + return ( + <Alert className="mt-4"> + <Alert.Heading> + {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)} + </Alert.Heading> + <p> + {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)} + </p> + <Button variant="primary" onClick={handleClearFilters}> + {intl.formatMessage(messages.coursesTabCourseNotFoundAlertCleanFiltersButton)} + </Button> + </Alert> + ); + } + return null; }; interface Props { From 225925a05806aecb4bc1cc202f768b4dde428102 Mon Sep 17 00:00:00 2001 From: XnpioChV <xnpiochv@gmail.com> Date: Fri, 5 Sep 2025 17:53:20 -0500 Subject: [PATCH 09/20] refactor: Update CardItem to support slectMode --- src/studio-home/card-item/index.tsx | 183 ++++++++++-------- .../tabs-section/libraries-v2-tab/index.tsx | 10 +- 2 files changed, 103 insertions(+), 90 deletions(-) diff --git a/src/studio-home/card-item/index.tsx b/src/studio-home/card-item/index.tsx index 1a63fac0fa..7af0171902 100644 --- a/src/studio-home/card-item/index.tsx +++ b/src/studio-home/card-item/index.tsx @@ -19,49 +19,103 @@ import { parseLibraryKey } from '@src/generic/key-utils'; import { getStudioHomeData } from '../data/selectors'; import messages from '../messages'; +const PrevToNextName = ({ from, to }: { from: React.ReactNode, to?: React.ReactNode }) => ( + <Stack direction="horizontal" gap={2}> + <span>{from}</span> + {to + && ( + <> + <Icon src={ArrowForward} size="xs" className="mb-1" /> + <span>{to}</span> + </> + )} + </Stack> +); + +const MakeLinkOrSpan = ({ + when, to, children, className, +}: { + when: boolean, + to: string, + children: React.ReactNode; + className?: string, +}) => { + if (when) { + return <Link className={className} to={to}>{children}</Link>; + } + return <span className={className}>{children}</span>; +}; + interface CardTitleProps { readOnlyItem: boolean; - inSelectMode: boolean; + selectMode?: 'single' | 'multiple' | null; destinationUrl: string; - hasDisplayName: string; - displayName: string; + title: string; itemId?: string | null; + isMigrated?: boolean; + migratedToKey?: string; + migratedToTitle?: string; } const CardTitle: React.FC<CardTitleProps> = ({ readOnlyItem, - inSelectMode, + selectMode, destinationUrl, - hasDisplayName, - displayName, + title, itemId, + isMigrated, + migratedToTitle, + migratedToKey, }) => { - if (!readOnlyItem && !inSelectMode) { - return ( - <Link - className="card-item-title" - to={destinationUrl} - > - {hasDisplayName} - </Link> - ); - } - if (inSelectMode) { - return ( - <Form.Radio className="mt-1 ml-1" value={itemId} name={`select-card-item-${itemId}`}> - <span - className="card-item-title" - style={{ marginTop: '-0.2rem' }} + const getTitle = useCallback(() => ( + <div style={{ marginTop: selectMode ? '-3px' : '' }}> + <PrevToNextName + from={( + <MakeLinkOrSpan + when={!readOnlyItem} + to={destinationUrl} + className="card-item-title" + > + {title} + </MakeLinkOrSpan> + )} + to={ + isMigrated && migratedToTitle && ( + <MakeLinkOrSpan + when={!readOnlyItem} + to={`/library/${migratedToKey}`} + className="card-item-title" + > + {migratedToTitle} + </MakeLinkOrSpan> + ) + } + /> + </div> + ), [ + readOnlyItem, + isMigrated, + destinationUrl, + migratedToTitle, + title, + selectMode, + ]); + + if (selectMode) { + if (selectMode === 'single') { + return ( + <Form.Radio + className="mt-1 ml-1" + value={itemId} + name={`select-card-item-${itemId}`} + style={{ marginBottom: '-20px' }} > - {displayName} - </span> - </Form.Radio> - ); + {getTitle()} + </Form.Radio> + ); + } } - - return ( - <span className="card-item-title">{displayName}</span> - ); + return getTitle(); }; interface BaseProps { @@ -77,7 +131,7 @@ interface BaseProps { migratedToKey?: string; migratedToTitle?: string; migratedToCollectionKey?: string | null; - inSelectMode?: boolean; + selectMode?: 'single' | 'multiple' | null; itemId?: string | null; } @@ -91,33 +145,6 @@ type Props = BaseProps & ( { url: string, path?: never } ); -const PrevToNextName = ({ from, to }: { from: React.ReactNode, to?: React.ReactNode }) => ( - <Stack direction="horizontal" gap={2}> - <span>{from}</span> - {to - && ( - <> - <Icon src={ArrowForward} size="xs" className="mb-1" /> - <span>{to}</span> - </> - )} - </Stack> -); - -const MakeLinkOrSpan = ({ - when, to, children, className, -}: { - when: boolean, - to: string, - children: React.ReactNode; - className?: string, -}) => { - if (when) { - return <Link className={className} to={to}>{children}</Link>; - } - return <span className={className}>{children}</span>; -}; - /** * A card on the Studio home page that represents a Course or a Library */ @@ -130,7 +157,7 @@ const CardItem: React.FC<Props> = ({ run = '', isLibraries = false, courseKey = '', - inSelectMode = false, + selectMode, itemId = '', path, url, @@ -181,36 +208,22 @@ const CardItem: React.FC<Props> = ({ return libUrl; }; - const getTitle = useCallback(() => ( - <PrevToNextName - from={( - <MakeLinkOrSpan - when={!readOnlyItem} - to={destinationUrl} - className="card-item-title" - > - {title} - </MakeLinkOrSpan> - )} - to={ - isMigrated && migratedToTitle && ( - <MakeLinkOrSpan - when={!readOnlyItem} - to={`/library/${migratedToKey}`} - className="card-item-title" - > - {migratedToTitle} - </MakeLinkOrSpan> - ) - } - /> - ), [readOnlyItem, isMigrated, destinationUrl, migratedToTitle, title]); - return ( <Card className="card-item"> <Card.Header size="sm" - title={getTitle()} + title={( + <CardTitle + readOnlyItem={readOnlyItem} + selectMode={selectMode} + destinationUrl={destinationUrl} + title={title} + itemId={itemId} + isMigrated={isMigrated} + migratedToTitle={migratedToTitle} + migratedToKey={migratedToKey} + /> + )} subtitle={getSubtitle()} actions={showActions && ( <Dropdown> diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index 9c18aa5bba..439b51a27a 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -23,7 +23,7 @@ import LibrariesV2Filters from './libraries-v2-filters'; interface CardListProps { hasV2Libraries: boolean; - inSelectMode: boolean; + selectMode?: 'single' | 'multiple' | null; isFiltered: boolean; isLoading: boolean; data: LibrariesV2Response; @@ -32,7 +32,7 @@ interface CardListProps { const CardList: React.FC<CardListProps> = ({ hasV2Libraries, - inSelectMode, + selectMode, isFiltered, isLoading, data, @@ -54,7 +54,7 @@ const CardList: React.FC<CardListProps> = ({ org={org} number={slug} path={`/library/${id}`} - inSelectMode={inSelectMode} + selectMode={selectMode} itemId={id} /> )) @@ -202,7 +202,7 @@ const LibrariesV2List: React.FC<Props> = ({ > <CardList hasV2Libraries={hasV2Libraries} - inSelectMode={inSelectMode} + selectMode={inSelectMode ? 'single' : null} isFiltered={isFiltered} isLoading={isLoading} data={data!} @@ -212,7 +212,7 @@ const LibrariesV2List: React.FC<Props> = ({ ) : ( <CardList hasV2Libraries={hasV2Libraries} - inSelectMode={inSelectMode} + selectMode={inSelectMode ? 'single' : null} isFiltered={isFiltered} isLoading={isLoading} data={data!} From 224f09d1491bae523b5276464aec89c3c1d1115e Mon Sep 17 00:00:00 2001 From: XnpioChV <xnpiochv@gmail.com> Date: Tue, 9 Sep 2025 17:49:11 -0500 Subject: [PATCH 10/20] feat: Select legacy libraries in Migrate legacy libraries --- .../ConfirmationView.tsx | 13 ++- .../LegacyLibMigrationPage.test.tsx | 62 ++++++++-- .../LegacyLibMigrationPage.tsx | 102 +++++++++-------- .../SelectDestinationView.tsx | 32 +++--- .../SelectLegacyLibraryView.tsx | 7 -- src/legacy-libraries-migration/index.scss | 9 ++ src/studio-home/card-item/index.tsx | 13 ++- src/studio-home/data/api.mocks.ts | 23 ++++ src/studio-home/tabs-section/index.tsx | 4 +- .../tabs-section/libraries-tab/index.tsx | 106 ++++++++++++++---- .../tabs-section/libraries-v2-tab/index.tsx | 13 +-- 11 files changed, 267 insertions(+), 117 deletions(-) delete mode 100644 src/legacy-libraries-migration/SelectLegacyLibraryView.tsx create mode 100644 src/studio-home/data/api.mocks.ts diff --git a/src/legacy-libraries-migration/ConfirmationView.tsx b/src/legacy-libraries-migration/ConfirmationView.tsx index 4335965868..8fa8357653 100644 --- a/src/legacy-libraries-migration/ConfirmationView.tsx +++ b/src/legacy-libraries-migration/ConfirmationView.tsx @@ -13,13 +13,14 @@ import { } from '@openedx/paragon/icons'; import type { ContentLibrary } from '@src/library-authoring/data/api'; +import { LibraryV1Data } from '@src/studio-home/data/api'; import messages from './messages'; const BoldText = (chunk: string[]) => <b>{chunk}</b>; interface ConfirmationCardProps { - legacyLib: any; + legacyLib: LibraryV1Data; destinationName: string; } @@ -27,12 +28,12 @@ const ConfirmationCard = ({ legacyLib, destinationName, }: ConfirmationCardProps) => ( - <Card className="mb-3"> + <Card className="mb-3.5"> <Card.Header title={( <Stack className="h4" direction="horizontal"> <Icon className="mr-1" src={Folder} /> - <span>{legacyLib.title}</span> + <span>{legacyLib.displayName}</span> </Stack> )} subtitle={( @@ -42,14 +43,14 @@ const ConfirmationCard = ({ </Stack> )} /> - {legacyLib.migratedIn && ( + {legacyLib.isMigrated && ( <Stack className="ml-3.5 mt-1 mb-2 text-gray-500" direction="horizontal"> <Icon className="mr-1.5" src={AccessTime} /> <span className="x-small"> <FormattedMessage {...messages.previouslyMigratedAlert} values={{ - libraryName: legacyLib.migratedIn, + libraryName: legacyLib.migratedToTitle, b: BoldText, }} /> @@ -61,7 +62,7 @@ const ConfirmationCard = ({ interface ConfirmationViewProps { destination: ContentLibrary | undefined; - legacyLibraries: any[]; + legacyLibraries: LibraryV1Data[]; } export const ConfirmationView = ({ diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 264388625f..f149800c1b 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -9,6 +9,7 @@ import { } from '@src/testUtils'; import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import { mockGetContentLibraryV2List } from '@src/library-authoring/data/api.mocks'; +import { mockGetStudioHomeLibraries } from '@src/studio-home/data/api.mocks'; import { getContentLibraryV2CreateApiUrl } from '@src/library-authoring/create-library/data/api'; import { getStudioHomeApiUrl } from '@src/studio-home/data/api'; @@ -17,6 +18,7 @@ import { LegacyLibMigrationPage } from './LegacyLibMigrationPage'; const path = '/libraries-v1/migrate/*'; let axiosMock: MockAdapter; +mockGetStudioHomeLibraries.applyMock(); mockGetContentLibraryV2List.applyMock(); const mockNavigate = jest.fn(); @@ -79,11 +81,46 @@ describe('<LegacyLibMigrationPage />', () => { }); }); + it('should select legacy libraries', async () => { + renderPage(); + expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + + const nextButton = screen.getByRole('button', { name: /next/i }); + // The next button is disabled + expect(nextButton).toBeDisabled(); + + expect(screen.getByText('MBA')).toBeInTheDocument(); + expect(screen.getByText('Legacy library 1')).toBeInTheDocument(); + expect(screen.getByText('MBA 1')).toBeInTheDocument(); + + screen.logTestingPlaygroundURL(); + + const library1 = screen.getByRole('checkbox', { name: 'MBA' }); + const library2 = screen.getByRole('checkbox', { name: /legacy library 1 imported library/i }); + + expect(library1).not.toBeChecked(); + expect(library2).not.toBeChecked(); + + library1.click(); + + expect(library1).toBeChecked(); + expect(library2).not.toBeChecked(); + expect(nextButton).not.toBeDisabled(); + + library2.click(); + expect(library1).toBeChecked(); + expect(library2).toBeChecked(); + expect(nextButton).not.toBeDisabled(); + }); + it('should select a library destination', async () => { renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + expect(await screen.findByText('MBA')).toBeInTheDocument(); + + const legacyLibrary = screen.getByRole('checkbox', { name: 'MBA' }); + legacyLibrary.click(); - // TODO Missing select legacy libraries const nextButton = screen.getByRole('button', { name: /next/i }); nextButton.click(); @@ -110,8 +147,11 @@ describe('<LegacyLibMigrationPage />', () => { renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + expect(await screen.findByText('MBA')).toBeInTheDocument(); + + const legacyLibrary = screen.getByRole('checkbox', { name: 'MBA' }); + legacyLibrary.click(); - // TODO Missing select legacy libraries const nextButton = screen.getByRole('button', { name: /next/i }); nextButton.click(); @@ -163,8 +203,16 @@ describe('<LegacyLibMigrationPage />', () => { it('should confirm migration', async () => { renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + expect(await screen.findByText('MBA')).toBeInTheDocument(); + + 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(); - // TODO Missing select legacy libraries const nextButton = screen.getByRole('button', { name: /next/i }); nextButton.click(); @@ -177,10 +225,10 @@ describe('<LegacyLibMigrationPage />', () => { nextButton.click(); // Should show alert of ConfirmationView - expect(await screen.findByText(/these 4 legacy libraries will be migrated to/i)).toBeInTheDocument(); - // TODO update with legacy libraries names - expect(screen.getByText('Legacy Lib 1')).toBeInTheDocument(); - expect(screen.getByText('Legacy Lib 3')).toBeInTheDocument(); + 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(); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index 0f2c7e1563..023213dd19 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet'; import { useNavigate } from 'react-router-dom'; @@ -15,9 +15,10 @@ import { 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 LibrariesList from '@src/studio-home/tabs-section/libraries-tab'; import messages from './messages'; -import { SelectLegacyLibraryView } from './SelectLegacyLibraryView'; import { SelectDestinationView } from './SelectDestinationView'; import { ConfirmationView } from './ConfirmationView'; import { MigrationStepsViewer } from './MigrationStepsViewer'; @@ -72,6 +73,7 @@ export const LegacyLibMigrationPage = () => { const intl = useIntl(); 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'); @@ -112,8 +114,7 @@ export const LegacyLibMigrationPage = () => { const isNextDisabled = useCallback(() => { switch (currentStep) { case 'select-libraries': - // TODO - return false; + return legacyLibraries.length === 0; case 'select-destination': return destinationLibrary === undefined; case 'confirmation-view': @@ -121,74 +122,79 @@ export const LegacyLibMigrationPage = () => { default: return true; } - }, [currentStep, destinationLibrary]); + }, [legacyLibraries, currentStep, destinationLibrary]); + + const handleUpdateLegacyLibraries = useCallback((library: LibraryV1Data, action: 'add' | 'remove') => { + if (action === 'add') { + setLegacyLibraries([...legacyLibraries, library]); + } else { + setLegacyLibraries(legacyLibraries.filter(item => item.libraryKey !== library.libraryKey)); + } + }, [legacyLibraries, setLegacyLibraries]); - // TODO Set this after implement SelectLegacyLibraryView - const legacyLibCount = 1; - const legacyLibs = [ - { title: 'Legacy Lib 1', migratedIn: undefined }, - { title: 'Legacy Lib 2', migratedIn: undefined }, - { title: 'Legacy Lib 3', migratedIn: 'Lib1 Large' }, - { title: 'Legacy Lib 4', migratedIn: undefined }, - ]; + const legacyLibrariesIds = useMemo(() => legacyLibraries.map(item => item.libraryKey), [legacyLibraries]); return ( <> - <div className="d-flex legacy-library-migration-page"> - <div className="flex-grow-1"> - <Helmet> - <title> - {intl.formatMessage(messages.siteTitle)} - - -
- +
+ + + {intl.formatMessage(messages.siteTitle)} + + +
+ +
- + -
-
+
+ + {currentStep !== 'confirmation-view' ? ( + - {currentStep !== 'confirmation-view' ? ( - - ) : ( - - )} -
- -
+ ) : ( + + )} +
+
{ - const intl = useIntl(); - - return ( - - - {intl.formatMessage(messages.selectDestinationAlert, { count: legacyLibCount })} - - ( + + + - - ); -}; + + + +); diff --git a/src/legacy-libraries-migration/SelectLegacyLibraryView.tsx b/src/legacy-libraries-migration/SelectLegacyLibraryView.tsx deleted file mode 100644 index cc474d19c9..0000000000 --- a/src/legacy-libraries-migration/SelectLegacyLibraryView.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Container } from '@openedx/paragon'; - -export const SelectLegacyLibraryView = () => ( - - Select Legacy LibraryStep - -); diff --git a/src/legacy-libraries-migration/index.scss b/src/legacy-libraries-migration/index.scss index 95288f6903..7213a95d13 100644 --- a/src/legacy-libraries-migration/index.scss +++ b/src/legacy-libraries-migration/index.scss @@ -1,4 +1,13 @@ .legacy-library-migration-page { + .migration-container { + // Calculate all the screen size subtracting the height of the header and top/bottom margins + min-height: calc(calc(100vh - 60px) - calc(var(--pgn-spacing-spacer-base) * 6)); + + .migration-content { + flex: 1; + } + } + .confirmation-view { .pgn__card-header-content { margin-top: calc(var(--pgn-spacing-spacer-base)) !important;; diff --git a/src/studio-home/card-item/index.tsx b/src/studio-home/card-item/index.tsx index 7af0171902..f6157163b9 100644 --- a/src/studio-home/card-item/index.tsx +++ b/src/studio-home/card-item/index.tsx @@ -72,7 +72,7 @@ const CardTitle: React.FC = ({ @@ -82,7 +82,7 @@ const CardTitle: React.FC = ({ to={ isMigrated && migratedToTitle && ( @@ -114,6 +114,15 @@ const CardTitle: React.FC = ({ ); } + // Multiple + return ( + + {getTitle()} + + ); } return getTitle(); }; diff --git a/src/studio-home/data/api.mocks.ts b/src/studio-home/data/api.mocks.ts new file mode 100644 index 0000000000..3bb7822945 --- /dev/null +++ b/src/studio-home/data/api.mocks.ts @@ -0,0 +1,23 @@ +import { camelCaseObject } from '@edx/frontend-platform'; + +import { createAxiosError } from '@src/testUtils'; +import * as api from './api'; +import { generateGetStudioHomeLibrariesApiResponse } from '../factories/mockApiResponses'; + +/** + * Mock for `getContentLibraryV2List()` + */ +export const mockGetStudioHomeLibraries = { + applyMock: () => jest.spyOn(api, 'getStudioHomeLibraries').mockResolvedValue( + camelCaseObject(generateGetStudioHomeLibrariesApiResponse()), + ), + applyMockError: () => jest.spyOn(api, 'getStudioHomeLibraries').mockRejectedValue( + createAxiosError({ code: 500, message: 'Internal Error.', path: `${api.getStudioHomeApiUrl()}/libraries` }), + ), + applyMockLoading: () => jest.spyOn(api, 'getStudioHomeLibraries').mockResolvedValue( + new Promise(() => {}), + ), + applyMockEmpty: () => jest.spyOn(api, 'getStudioHomeLibraries').mockResolvedValue({ + libraries: [], + }), +}; diff --git a/src/studio-home/tabs-section/index.tsx b/src/studio-home/tabs-section/index.tsx index 3dd7f3064d..71e3655963 100644 --- a/src/studio-home/tabs-section/index.tsx +++ b/src/studio-home/tabs-section/index.tsx @@ -15,7 +15,7 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { RequestStatus } from '@src/data/constants'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; -import LibrariesTab from './libraries-tab'; +import LibrariesList from './libraries-tab'; import LibrariesV2List from './libraries-v2-tab/index'; import CoursesTab from './courses-tab'; @@ -133,7 +133,7 @@ const TabsSection = ({ : messages.librariesTabTitle, )} > - + , ); } diff --git a/src/studio-home/tabs-section/libraries-tab/index.tsx b/src/studio-home/tabs-section/libraries-tab/index.tsx index 5ac5c9d26b..32dec15a84 100644 --- a/src/studio-home/tabs-section/libraries-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-tab/index.tsx @@ -1,3 +1,4 @@ +import { useCallback, useState } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, Form, Icon, Menu, MenuItem, Pagination, Row, SearchField, @@ -9,11 +10,52 @@ import { LoadingSpinner } from '@src/generic/Loading'; import AlertMessage from '@src/generic/alert-message'; import { useLibrariesV1Data } from '@src/studio-home/data/apiHooks'; import CardItem from '@src/studio-home/card-item'; -import { useCallback, useState } from 'react'; import SearchFilterWidget from '@src/search-manager/SearchFilterWidget'; +import type { LibraryV1Data } from '@src/studio-home/data/api'; + import messages from '../messages'; import { MigrateLegacyLibrariesAlert } from './MigrateLegacyLibrariesAlert'; +const CardList = ({ + data, + inSelectMode, +}: { + data: LibraryV1Data[], + inSelectMode: boolean, +}) => ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + { + data?.map(({ + displayName, + org, + number, + url, + isMigrated, + migratedToKey, + migratedToTitle, + migratedToCollectionKey, + libraryKey, + }) => ( + + )) + } + +); + function findInValues(arr: T[] | undefined, searchValue: string) { return arr?.filter(o => Object.values(o).some(value => String(value).toLowerCase().includes( String(searchValue).toLowerCase().trim(), @@ -96,7 +138,17 @@ const MigrationFilter = ({ filters, setFilters }: MigrationFilterProps) => { ); }; -const LibrariesTab = () => { +interface LibrariesListProps { + selectedIds?: string[] | null; + handleCheck?: ((library: LibraryV1Data, action: 'add' | 'remove') => void) | null; + hideMigationAlert?: boolean; +} + +const LibrariesList = ({ + selectedIds = null, + handleCheck = null, + hideMigationAlert = false, +}: LibrariesListProps) => { const intl = useIntl(); const { isPending, data, isError } = useLibrariesV1Data(); const [currentPage, setCurrentPage] = useState(1); @@ -111,6 +163,21 @@ const LibrariesTab = () => { const perPage = 10; const totalPages = Math.ceil(filteredData.length / perPage); const currentPageData = filteredData.slice((currentPage - 1) * perPage, currentPage * perPage); + const inSelectMode = handleCheck !== null; + + const handleChangeCheckboxSet = useCallback((event) => { + if (handleCheck) { + const libraryId = event.target.value; + const library = currentPageData.find((item) => item.libraryKey === libraryId); + if (library) { + if (event.target.checked) { + handleCheck(library, 'add'); + } else { + handleCheck(library, 'remove'); + } + } + } + }, [handleCheck, currentPageData]); if (isPending) { return ( @@ -136,7 +203,7 @@ const LibrariesTab = () => { return ( <> - {getConfig().ENABLE_LEGACY_LIBRARY_MIGRATOR === 'true' && ()} + {!hideMigationAlert && getConfig().ENABLE_LEGACY_LIBRARY_MIGRATOR === 'true' && ()}
{ )} - {currentPageData?.map(({ - displayName, org, number, url, isMigrated, migratedToKey, migratedToTitle, migratedToCollectionKey, - }) => ( - + + + ) : ( + - ))} + )} { totalPages > 1 && ( @@ -192,4 +260,4 @@ const LibrariesTab = () => { ); }; -export default LibrariesTab; +export default LibrariesList; diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index 439b51a27a..ee6b4c21ed 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -117,13 +117,6 @@ const LibrariesV2List: React.FC = ({ isError, } = useContentLibraryV2List({ page: currentPage, ...filterParams }); - const findLibrary = useCallback((libraryId: string) => { - if (data) { - return data.results.find((library) => library.id === libraryId); - } - return undefined; - }, [data]); - const handlePostCreateLibrary = useCallback((library: ContentLibrary) => { if (handleSelect) { handleSelect(library); @@ -132,13 +125,13 @@ const LibrariesV2List: React.FC = ({ }, [handleSelect, closeCreateLibrary]); const handleOnChangeRadioSet = useCallback((libraryId: string) => { - if (handleSelect) { - const library = findLibrary(libraryId); + if (handleSelect && data) { + const library = data.results.find((item) => item.id === libraryId); if (library) { handleSelect(library); } } - }, [findLibrary, handleSelect]); + }, [data, handleSelect]); if (isLoading && !isFiltered) { return ( From 3e300ee022de2de7e5f9eeb5aaafd10ffcef266c Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 25 Sep 2025 14:57:19 -0500 Subject: [PATCH 11/20] test: Fix broken tests --- .../LegacyLibMigrationPage.test.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index f149800c1b..85361e88e5 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -89,11 +89,9 @@ describe('', () => { // The next button is disabled expect(nextButton).toBeDisabled(); - expect(screen.getByText('MBA')).toBeInTheDocument(); - expect(screen.getByText('Legacy library 1')).toBeInTheDocument(); - expect(screen.getByText('MBA 1')).toBeInTheDocument(); - - screen.logTestingPlaygroundURL(); + expect(await screen.findByText('MBA')).toBeInTheDocument(); + expect(await screen.findByText('Legacy library 1')).toBeInTheDocument(); + expect(await screen.findByText('MBA 1')).toBeInTheDocument(); const library1 = screen.getByRole('checkbox', { name: 'MBA' }); const library2 = screen.getByRole('checkbox', { name: /legacy library 1 imported library/i }); @@ -158,7 +156,7 @@ describe('', () => { // Should show alert of SelectDestinationView expect(await screen.findByText(/any legacy libraries that are used/i)).toBeInTheDocument(); - const createButton = screen.getByRole('button', { name: /create new library/i }); + const createButton = await screen.findByRole('button', { name: /create new library/i }); expect(createButton).toBeInTheDocument(); createButton.click(); From 2343d92ca0380f40311a65d618825ca331cb9a11 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 25 Sep 2025 15:37:14 -0500 Subject: [PATCH 12/20] test: Fix coverage --- .../tabs-section/TabsSection.test.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/studio-home/tabs-section/TabsSection.test.tsx b/src/studio-home/tabs-section/TabsSection.test.tsx index c63aac248f..ca046dc802 100644 --- a/src/studio-home/tabs-section/TabsSection.test.tsx +++ b/src/studio-home/tabs-section/TabsSection.test.tsx @@ -392,6 +392,35 @@ describe('', () => { ).toBeVisible(); }); + it('should open migration library page', async () => { + setConfig({ + ...getConfig(), + ENABLE_LEGACY_LIBRARY_MIGRATOR: 'true', + }); + await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + await axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); + render({ librariesV2Enabled: false }); + const user = userEvent.setup(); + await act(async () => executeThunk(fetchStudioHomeData(), store.dispatch)); + + // Libraries v2 tab should not be shown + expect(screen.queryByRole('tab', { name: librariesBetaTabTitle })).toBeNull(); + + const librariesTab = await screen.findByRole('tab', { name: tabMessages.librariesTabTitle.defaultMessage }); + await user.click(librariesTab); + + expect(librariesTab).toHaveClass('active'); + + expect(await screen.findByText(studioHomeMock.libraries[0].displayName)).toBeVisible(); + + const migratorButton = screen.getByRole('button', { name: /review legacy libraries/i }); + expect(migratorButton).toBeInTheDocument(); + await user.click(migratorButton); + + const locationDisplay = await screen.findByTestId('location-display'); + expect(locationDisplay).toHaveTextContent('/migrate'); + }); + it('should switch to Libraries tab and render specific v2 library details ("v2 only" mode)', async () => { render({ librariesV1Enabled: false }); await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); From 290946fda8bbb2acadb4f4fc30fb356fdf3d5679 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 25 Sep 2025 17:18:26 -0500 Subject: [PATCH 13/20] test: Fix coverage --- .../LegacyLibMigrationPage.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 85361e88e5..4b20cfbb1b 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -109,6 +109,11 @@ describe('', () => { expect(library1).toBeChecked(); expect(library2).toBeChecked(); expect(nextButton).not.toBeDisabled(); + + library2.click(); + expect(library1).toBeChecked(); + expect(library2).not.toBeChecked(); + expect(nextButton).not.toBeDisabled(); }); it('should select a library destination', async () => { From 74f5a896abfbab8642d96642c67b4212821f7c3e Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 26 Sep 2025 11:14:57 -0500 Subject: [PATCH 14/20] refactor: Delete MigrationStepsViewer an use Stepper.Header --- .../LegacyLibMigrationPage.tsx | 18 +++-- .../MigrationStepsViewer.tsx | 80 ------------------- 2 files changed, 13 insertions(+), 85 deletions(-) delete mode 100644 src/legacy-libraries-migration/MigrationStepsViewer.tsx diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index 023213dd19..78a2d63453 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -21,7 +21,6 @@ import LibrariesList from '@src/studio-home/tabs-section/libraries-tab'; import messages from './messages'; import { SelectDestinationView } from './SelectDestinationView'; import { ConfirmationView } from './ConfirmationView'; -import { MigrationStepsViewer } from './MigrationStepsViewer'; export type MigrationStep = 'select-libraries' | 'select-destination' | 'confirmation-view'; @@ -148,23 +147,32 @@ export const LegacyLibMigrationPage = () => { - - + + - + - + { - 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')} -
- ); -}; From 414d74a11e4e1997b55eda8fbc49ba91cf26a3c6 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 26 Sep 2025 11:56:42 -0500 Subject: [PATCH 15/20] style: Update the code with review feedback --- src/generic/unlink-modal/UnlinkModal.tsx | 3 +-- .../ConfirmationView.tsx | 9 ++++----- .../LegacyLibMigrationPage.tsx | 10 ++++++---- src/legacy-libraries-migration/index.scss | 10 ++++------ .../create-library/CreateLibrary.tsx | 8 ++++---- .../create-library/CreateLibraryModal.tsx | 2 +- src/studio-home/card-item/index.tsx | 8 ++++---- .../tabs-section/libraries-tab/index.tsx | 10 +++++----- .../tabs-section/libraries-v2-tab/index.tsx | 14 +++++++------- src/{utils.test.js => utils.test.tsx} | 0 src/{utils.ts => utils.tsx} | 2 ++ 11 files changed, 38 insertions(+), 38 deletions(-) rename src/{utils.test.js => utils.test.tsx} (100%) rename src/{utils.ts => utils.tsx} (99%) diff --git a/src/generic/unlink-modal/UnlinkModal.tsx b/src/generic/unlink-modal/UnlinkModal.tsx index 55a513064f..ca66e738f4 100644 --- a/src/generic/unlink-modal/UnlinkModal.tsx +++ b/src/generic/unlink-modal/UnlinkModal.tsx @@ -5,12 +5,11 @@ import { } from '@openedx/paragon'; import { Warning } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { BoldText } from '@src/utils'; import messages from './messages'; import LoadingButton from '../loading-button'; -const BoldText = (chunk: string[]) => {chunk}; - type UnlinkModalPropsContainer = { displayName?: string; category?: string; diff --git a/src/legacy-libraries-migration/ConfirmationView.tsx b/src/legacy-libraries-migration/ConfirmationView.tsx index 8fa8357653..6cb8e025ec 100644 --- a/src/legacy-libraries-migration/ConfirmationView.tsx +++ b/src/legacy-libraries-migration/ConfirmationView.tsx @@ -14,11 +14,10 @@ import { import type { ContentLibrary } from '@src/library-authoring/data/api'; import { LibraryV1Data } from '@src/studio-home/data/api'; +import { BoldText } from '@src/utils'; import messages from './messages'; -const BoldText = (chunk: string[]) => {chunk}; - interface ConfirmationCardProps { legacyLib: LibraryV1Data; destinationName: string; @@ -61,7 +60,7 @@ const ConfirmationCard = ({ ); interface ConfirmationViewProps { - destination: ContentLibrary | undefined; + destination: ContentLibrary; legacyLibraries: LibraryV1Data[]; } @@ -75,7 +74,7 @@ export const ConfirmationView = ({ {...messages.confirmationViewAlert} values={{ count: legacyLibraries.length, - libraryName: destination?.title, + libraryName: destination.title, b: BoldText, }} /> @@ -83,7 +82,7 @@ export const ConfirmationView = ({ {legacyLibraries.map((legacyLib) => ( ))} diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index 78a2d63453..b2fd9ba2b3 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -173,10 +173,12 @@ export const LegacyLibMigrationPage = () => { eventKey="confirmation-view" title={intl.formatMessage(messages.confirmStepTitle)} > - + {destinationLibrary && ( + + )}
diff --git a/src/legacy-libraries-migration/index.scss b/src/legacy-libraries-migration/index.scss index 7213a95d13..01e7010bf8 100644 --- a/src/legacy-libraries-migration/index.scss +++ b/src/legacy-libraries-migration/index.scss @@ -3,6 +3,10 @@ // Calculate all the screen size subtracting the height of the header and top/bottom margins min-height: calc(calc(100vh - 60px) - calc(var(--pgn-spacing-spacer-base) * 6)); + .courses-tab-container { + min-height: auto; + } + .migration-content { flex: 1; } @@ -13,10 +17,4 @@ margin-top: calc(var(--pgn-spacing-spacer-base)) !important;; } } - - .migration-steps-viewer { - hr { - width: 80px; - } - } } \ No newline at end of file diff --git a/src/library-authoring/create-library/CreateLibrary.tsx b/src/library-authoring/create-library/CreateLibrary.tsx index a8c4c5cecc..82431e597a 100644 --- a/src/library-authoring/create-library/CreateLibrary.tsx +++ b/src/library-authoring/create-library/CreateLibrary.tsx @@ -27,12 +27,12 @@ import type { ContentLibrary } from '../data/api'; export const CreateLibrary = ({ showInModal = false, - handleCancel = null, - handlePostCreate = null, + handleCancel, + handlePostCreate, }: { showInModal?: boolean, - handleCancel?: (() => void) | null, - handlePostCreate?: ((library: ContentLibrary) => void) | null, + handleCancel?: () => void, + handlePostCreate?: (library: ContentLibrary) => void, }) => { const intl = useIntl(); const navigate = useNavigate(); diff --git a/src/library-authoring/create-library/CreateLibraryModal.tsx b/src/library-authoring/create-library/CreateLibraryModal.tsx index 2abcb3833a..3f81e171c8 100644 --- a/src/library-authoring/create-library/CreateLibraryModal.tsx +++ b/src/library-authoring/create-library/CreateLibraryModal.tsx @@ -8,7 +8,7 @@ import type { ContentLibrary } from '../data/api'; interface CreateLibraryModalProps { isOpen: boolean; onClose: () => void; - handlePostCreate?: ((library: ContentLibrary) => void) | null, + handlePostCreate: (library: ContentLibrary) => void, } export const CreateLibraryModal = ({ diff --git a/src/studio-home/card-item/index.tsx b/src/studio-home/card-item/index.tsx index f6157163b9..f65256ef81 100644 --- a/src/studio-home/card-item/index.tsx +++ b/src/studio-home/card-item/index.tsx @@ -48,10 +48,10 @@ const MakeLinkOrSpan = ({ interface CardTitleProps { readOnlyItem: boolean; - selectMode?: 'single' | 'multiple' | null; + selectMode?: 'single' | 'multiple'; destinationUrl: string; title: string; - itemId?: string | null; + itemId?: string; isMigrated?: boolean; migratedToKey?: string; migratedToTitle?: string; @@ -140,8 +140,8 @@ interface BaseProps { migratedToKey?: string; migratedToTitle?: string; migratedToCollectionKey?: string | null; - selectMode?: 'single' | 'multiple' | null; - itemId?: string | null; + selectMode?: 'single' | 'multiple'; + itemId?: string; } type Props = BaseProps & ( diff --git a/src/studio-home/tabs-section/libraries-tab/index.tsx b/src/studio-home/tabs-section/libraries-tab/index.tsx index 32dec15a84..486c7bcc22 100644 --- a/src/studio-home/tabs-section/libraries-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-tab/index.tsx @@ -45,7 +45,7 @@ const CardList = ({ number={number} url={url} itemId={libraryKey} - selectMode={inSelectMode ? 'multiple' : null} + selectMode={inSelectMode ? 'multiple' : undefined} isMigrated={isMigrated} migratedToKey={migratedToKey} migratedToTitle={migratedToTitle} @@ -139,14 +139,14 @@ const MigrationFilter = ({ filters, setFilters }: MigrationFilterProps) => { }; interface LibrariesListProps { - selectedIds?: string[] | null; - handleCheck?: ((library: LibraryV1Data, action: 'add' | 'remove') => void) | null; + selectedIds?: string[]; + handleCheck?: (library: LibraryV1Data, action: 'add' | 'remove') => void; hideMigationAlert?: boolean; } const LibrariesList = ({ - selectedIds = null, - handleCheck = null, + selectedIds, + handleCheck, hideMigationAlert = false, }: LibrariesListProps) => { const intl = useIntl(); diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index d59e63f98c..82b5f01bf4 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -23,7 +23,7 @@ import LibrariesV2Filters from './libraries-v2-filters'; interface CardListProps { hasV2Libraries: boolean; - selectMode?: 'single' | 'multiple' | null; + selectMode?: 'single' | 'multiple'; isFiltered: boolean; isLoading: boolean; data: LibrariesV2Response; @@ -83,14 +83,14 @@ const CardList: React.FC = ({ }; interface Props { - selectedLibraryId?: string | null; - handleSelect?: ((library: ContentLibrary) => void) | null; + selectedLibraryId?: string; + handleSelect?: (library: ContentLibrary) => void; showCreateLibrary?: boolean; } const LibrariesV2List: React.FC = ({ - selectedLibraryId = null, - handleSelect = null, + selectedLibraryId, + handleSelect, showCreateLibrary = false, }) => { const intl = useIntl(); @@ -195,7 +195,7 @@ const LibrariesV2List: React.FC = ({ > = ({ ) : ( {chunk}; From 6bdb95e849b0fb75fb979350754ced189b5c63e3 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 26 Sep 2025 12:12:22 -0500 Subject: [PATCH 16/20] style: Fix broken lint --- src/utils.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.test.tsx b/src/utils.test.tsx index d4fa59d373..9b59013edf 100644 --- a/src/utils.test.tsx +++ b/src/utils.test.tsx @@ -120,6 +120,7 @@ describe('FilesAndUploads utils', () => { const dateStr = '2023-10-01T12:00:00Z'; const date = convertToDateFromString(dateStr); expect(date).toBeInstanceOf(Date); + // @ts-ignore expect(date.toISOString()).toBe('2023-10-01T12:00:00.000Z'); }); From 3b3fece9bb46c0dd45857e298e815c3f17babbe7 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 26 Sep 2025 12:42:43 -0500 Subject: [PATCH 17/20] style: fix broken tests --- src/studio-home/tabs-section/libraries-tab/index.tsx | 2 +- src/studio-home/tabs-section/libraries-v2-tab/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/studio-home/tabs-section/libraries-tab/index.tsx b/src/studio-home/tabs-section/libraries-tab/index.tsx index 486c7bcc22..952946bb90 100644 --- a/src/studio-home/tabs-section/libraries-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-tab/index.tsx @@ -163,7 +163,7 @@ const LibrariesList = ({ const perPage = 10; const totalPages = Math.ceil(filteredData.length / perPage); const currentPageData = filteredData.slice((currentPage - 1) * perPage, currentPage * perPage); - const inSelectMode = handleCheck !== null; + const inSelectMode = handleCheck !== undefined; const handleChangeCheckboxSet = useCallback((event) => { if (handleCheck) { diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index 82b5f01bf4..e1f5d32b64 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -100,7 +100,7 @@ const LibrariesV2List: React.FC = ({ const [isCreateLibraryOpen, openCreateLibrary, closeCreateLibrary] = useToggle(false); const isFiltered = Object.keys(filterParams).length > 0; - const inSelectMode = handleSelect !== null; + const inSelectMode = handleSelect !== undefined; const handlePageSelect = (page: number) => { setCurrentPage(page); From 4a4b18cf75d437c9e70fa4f4e0debdba6d63dc4f Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 29 Sep 2025 12:33:56 -0500 Subject: [PATCH 18/20] style: Use FormattedMessage --- .../LegacyLibMigrationPage.tsx | 18 +++++++----------- src/legacy-libraries-migration/index.scss | 2 +- .../create-library/CreateLibraryModal.tsx | 4 ++-- .../tabs-section/libraries-v2-tab/index.tsx | 12 +++++------- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index b2fd9ba2b3..9b5237995e 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet'; import { useNavigate } from 'react-router-dom'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, Button, @@ -34,10 +34,6 @@ const ExitModal = ({ 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)} + - @@ -138,7 +134,7 @@ export const LegacyLibMigrationPage = () => {
- {intl.formatMessage(messages.siteTitle)} + <FormattedMessage {...messages.siteTitle} />
diff --git a/src/legacy-libraries-migration/index.scss b/src/legacy-libraries-migration/index.scss index 01e7010bf8..9af18213ab 100644 --- a/src/legacy-libraries-migration/index.scss +++ b/src/legacy-libraries-migration/index.scss @@ -17,4 +17,4 @@ margin-top: calc(var(--pgn-spacing-spacer-base)) !important;; } } -} \ No newline at end of file +} diff --git a/src/library-authoring/create-library/CreateLibraryModal.tsx b/src/library-authoring/create-library/CreateLibraryModal.tsx index 3f81e171c8..436941049d 100644 --- a/src/library-authoring/create-library/CreateLibraryModal.tsx +++ b/src/library-authoring/create-library/CreateLibraryModal.tsx @@ -1,4 +1,4 @@ -import { useIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { ModalDialog } from '@openedx/paragon'; import messages from './messages'; @@ -27,7 +27,7 @@ export const CreateLibraryModal = ({ > - {intl.formatMessage(messages.createLibrary)} + diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index e1f5d32b64..dd348139be 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -9,7 +9,7 @@ import { Stack, useToggle, } from '@openedx/paragon'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Add, Error } from '@openedx/paragon/icons'; import { CreateLibraryModal, useContentLibraryV2List } from '@src/library-authoring'; @@ -38,8 +38,6 @@ const CardList: React.FC = ({ data, handleClearFilters, }) => { - const intl = useIntl(); - if (hasV2Libraries) { return ( <> @@ -68,13 +66,13 @@ const CardList: React.FC = ({ return ( - {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)} +

- {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)} +

); @@ -165,7 +163,7 @@ const LibrariesV2List: React.FC = ({ iconBefore={Add} className="mr-3" > - {intl.formatMessage(messages.createLibraryButton)} + )} Date: Mon, 29 Sep 2025 12:48:03 -0500 Subject: [PATCH 19/20] style: Add comments in CreateLibrary --- src/library-authoring/create-library/CreateLibrary.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/library-authoring/create-library/CreateLibrary.tsx b/src/library-authoring/create-library/CreateLibrary.tsx index 82431e597a..676d02d9a4 100644 --- a/src/library-authoring/create-library/CreateLibrary.tsx +++ b/src/library-authoring/create-library/CreateLibrary.tsx @@ -25,6 +25,13 @@ import { useCreateLibraryV2 } from './data/apiHooks'; import messages from './messages'; import type { ContentLibrary } from '../data/api'; +/** + * Renders the form and logic to create a new library. + * + * Use `showInModal` to render this component in a way that can be + * used in a modal. Currently this component is used in a modal in the + * legacy libraries migration flow. + */ export const CreateLibrary = ({ showInModal = false, handleCancel, From 7260b87557fd949fcc2149cb2f125d3b419516b4 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 29 Sep 2025 13:12:25 -0500 Subject: [PATCH 20/20] test: Add more test to fix coverage --- .../LegacyLibMigrationPage.test.tsx | 46 +++++++++++++++++++ .../LegacyLibMigrationPage.tsx | 6 ++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 4b20cfbb1b..17d4cbbbb0 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -116,6 +116,26 @@ describe('', () => { expect(nextButton).not.toBeDisabled(); }); + it('should back to select legacy libraries', async () => { + renderPage(); + expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + expect(await screen.findByText('MBA')).toBeInTheDocument(); + + const legacyLibrary = screen.getByRole('checkbox', { name: 'MBA' }); + legacyLibrary.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(); + + const backButton = screen.getByRole('button', { name: /back/i }); + backButton.click(); + + expect(await screen.findByText('MBA')).toBeInTheDocument(); + }); + it('should select a library destination', async () => { renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); @@ -141,6 +161,32 @@ describe('', () => { expect(nextButton).not.toBeDisabled(); }); + it('should back to select library destination', async () => { + renderPage(); + expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + expect(await screen.findByText('MBA')).toBeInTheDocument(); + + const legacyLibrary = screen.getByRole('checkbox', { name: 'MBA' }); + legacyLibrary.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(); + expect(await screen.findByText(/these 1 legacy library will be migrated to/i)).toBeInTheDocument(); + + const backButton = screen.getByRole('button', { name: /back/i }); + backButton.click(); + + expect(await screen.findByText('Test Library 1')).toBeInTheDocument(); + }); + it('should open the create new library modal', async () => { const user = userEvent.setup(); axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index 9b5237995e..c24504d5f2 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -85,6 +85,7 @@ export const LegacyLibMigrationPage = () => { // TODO Call migration API break; default: + /* istanbul ignore next */ break; } }, [currentStep, setCurrentStep]); @@ -102,6 +103,7 @@ export const LegacyLibMigrationPage = () => { setCurrentStep('select-destination'); break; default: + /* istanbul ignore next */ break; } }, [currentStep, setCurrentStep]); @@ -113,8 +115,10 @@ export const LegacyLibMigrationPage = () => { case 'select-destination': return destinationLibrary === undefined; case 'confirmation-view': + /* istanbul ignore next */ return false; default: + /* istanbul ignore next */ return true; } }, [legacyLibraries, currentStep, destinationLibrary]); @@ -134,7 +138,7 @@ export const LegacyLibMigrationPage = () => {
- <FormattedMessage {...messages.siteTitle} /> + {intl.formatMessage(messages.siteTitle)}