feat: add library v2 alert (#2413)

Adds an Alert to the Legacy Library Page to notify the user of the process of deprecating Legacy Libraries and a Button to open the Migrate Library interface.
This commit is contained in:
Rômulo Penido
2025-09-25 18:01:57 -03:00
committed by GitHub
parent cffc4d77c9
commit 523dd1f389
11 changed files with 139 additions and 59 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import AlertError from '.';
@@ -33,7 +33,6 @@ describe('<AlertMessage />', () => {
},
};
const { getByText } = render(<RootWrapper error={error} />);
screen.logTestingPlaygroundURL();
expect(getByText(/this is an error message/i)).toBeInTheDocument();
expect(getByText(/\{ "message": "this is a response body" \}/i)).toBeInTheDocument();
});

View File

@@ -121,7 +121,6 @@ describe('<ManageCollections />', () => {
collections={[]}
useUpdateCollectionsHook={useUpdateComponentCollections}
/>);
screen.logTestingPlaygroundURL();
const manageBtn = await screen.findByRole('button', { name: 'Add to Collection' });
await user.click(manageBtn);
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });

View File

@@ -177,7 +177,6 @@ describe('<LibraryUnitPage />', () => {
const textBox = screen.getByRole('textbox', { name: /text input/i });
expect(textBox).toBeInTheDocument();
expect(textBox).toHaveValue('Test Unit');
screen.logTestingPlaygroundURL();
fireEvent.change(textBox, { target: { value: 'New Unit Title' } });
fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });

View File

@@ -12,7 +12,7 @@ export const studioHomeQueryKeys = {
export const useLibrariesV1Data = (enabled: boolean = true) => (
useQuery({
queryKey: studioHomeQueryKeys.librariesV1(),
queryFn: () => getStudioHomeLibraries(),
queryFn: getStudioHomeLibraries,
enabled,
})
);

View File

@@ -478,5 +478,42 @@ describe('<TabsSection />', () => {
tabMessages.librariesTabErrorMessage.defaultMessage,
)).toBeVisible();
});
[true, false].forEach((isMigrated) => {
it(`should render v2 libraries migration alert when the libraries have isMigrated=${isMigrated}`, async () => {
setConfig({
...getConfig(),
ENABLE_LEGACY_LIBRARY_MIGRATOR: 'true',
});
const libraries = generateGetStudioHomeLibrariesApiResponse().libraries.map(
library => ({
...library,
isMigrated,
}),
);
const user = userEvent.setup();
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeLibrariesApiResponse());
await axiosMock.onGet(libraryApiLink).reply(200, { libraries });
render();
await executeThunk(fetchStudioHomeData(), store.dispatch);
const librariesTab = await screen.findByRole('tab', { name: librariesBetaTabTitle });
await user.click(librariesTab);
expect(librariesTab).toHaveClass('active');
expect(await screen.findByText(/welcome to the new content libraries/i)).toBeVisible();
const migrationPendingText = /legacy libraries can be migrated using the migration tool/i;
if (isMigrated) {
expect(screen.queryByText(migrationPendingText)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Review Legacy Libraries' })).not.toBeInTheDocument();
} else {
expect(screen.getByText(migrationPendingText)).toBeVisible();
expect(screen.getByRole('button', { name: 'Review Legacy Libraries' })).toBeVisible();
}
});
});
});
});

View File

@@ -11,7 +11,7 @@ export const MigrateLegacyLibrariesAlert = () => (
</Alert.Heading>
<div className="row">
<div className="col-8">
<FormattedMessage {...messages.alertDescription} />
<FormattedMessage {...messages.alertDescriptionV1} />
</div>
<div className="col-4 d-flex justify-content-center align-items-start">
<Button>

View File

@@ -0,0 +1,51 @@
import { Alert, Button, Hyperlink } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { useLibrariesV1Data } from '@src/studio-home/data/apiHooks';
import messages from '../messages';
const libraryDocsLink = (
<Hyperlink
target="_blank"
showLaunchIcon={false}
destination="https://docs.openedx.org/en/latest/educators/how-tos/course_development/create_new_library.html"
>
<FormattedMessage {...messages.alertLibrariesDocLinkText} />
</Hyperlink>
);
export const WelcomeLibrariesV2Alert = () => {
const { data, isPending, isError } = useLibrariesV1Data();
// Does not show the alert if we are still loading or if there was an error fetching libraries
if (isPending || isError) {
return null;
}
const hasPendingV1Migrations = data.libraries.some(library => !library.isMigrated);
return (
<Alert variant="info">
{hasPendingV1Migrations ? (
<>
<Alert.Heading>
<FormattedMessage {...messages.alertTitle} />
</Alert.Heading>
<div className="row">
<div className="col-8">
<FormattedMessage {...messages.alertDescriptionV2} values={{ link: libraryDocsLink }} />
<FormattedMessage {...messages.alertDescriptionV2MigrationPending} />
</div>
<div className="col-4 d-flex justify-content-center align-items-start">
<Button>
<FormattedMessage {...messages.alertReviewButton} />
</Button>
</div>
</div>
</>
) : (
<FormattedMessage {...messages.alertDescriptionV2} values={{ link: libraryDocsLink }} />
)}
</Alert>
);
};

View File

@@ -6,15 +6,17 @@ import {
Alert,
Button,
} from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Error } from '@openedx/paragon/icons';
import { useContentLibraryV2List } from '../../../library-authoring';
import { LoadingSpinner } from '../../../generic/Loading';
import AlertMessage from '../../../generic/alert-message';
import { useContentLibraryV2List } from '@src/library-authoring';
import { LoadingSpinner } from '@src/generic/Loading';
import AlertMessage from '@src/generic/alert-message';
import CardItem from '../../card-item';
import messages from '../messages';
import LibrariesV2Filters from './libraries-v2-filters';
import { WelcomeLibrariesV2Alert } from './WelcomeLibrariesV2Alert';
type Props = Record<never, never>;
@@ -37,11 +39,11 @@ const LibrariesV2Tab: React.FC<Props> = () => {
const {
data,
isLoading,
isPending,
isError,
} = useContentLibraryV2List({ page: currentPage, ...filterParams });
if (isLoading && !isFiltered) {
if (isPending && !isFiltered) {
return (
<Row className="m-0 mt-4 justify-content-center">
<LoadingSpinner />
@@ -49,23 +51,11 @@ const LibrariesV2Tab: React.FC<Props> = () => {
);
}
const hasV2Libraries = !isLoading && !isError && ((data!.results.length || 0) > 0);
// TODO: update this link when tutorial is ready.
const librariesTutorialLink = (
<Alert.Link href="https://docs.openedx.org">
{intl.formatMessage(messages.librariesV2TabBetaTutorialLinkText)}
</Alert.Link>
);
const hasV2Libraries = !isPending && !isError && ((data!.results.length || 0) > 0);
return (
<>
<Alert variant="info">
{intl.formatMessage(
messages.librariesV2TabBetaText,
{ link: librariesTutorialLink },
)}
</Alert>
{getConfig().ENABLE_LEGACY_LIBRARY_MIGRATOR === 'true' && (<WelcomeLibrariesV2Alert />)}
{isError ? (
<AlertMessage
@@ -81,24 +71,24 @@ const LibrariesV2Tab: React.FC<Props> = () => {
<div className="courses-tab-container">
<div className="d-flex flex-row justify-content-between my-4">
<LibrariesV2Filters
isLoading={isLoading}
isPending={isPending}
isFiltered={isFiltered}
filterParams={filterParams}
setFilterParams={setFilterParams}
setCurrentPage={setCurrentPage}
/>
{!isLoading && !isError
&& (
<p>
{intl.formatMessage(messages.coursesPaginationInfo, {
length: data!.results.length,
total: data!.count,
})}
</p>
)}
{!isPending && !isError
&& (
<p>
{intl.formatMessage(messages.coursesPaginationInfo, {
length: data.results.length,
total: data.count,
})}
</p>
)}
</div>
{ hasV2Libraries
{hasV2Libraries
? data!.results.map(({
id, org, slug, title,
}) => (
@@ -110,7 +100,7 @@ const LibrariesV2Tab: React.FC<Props> = () => {
number={slug}
path={`/library/${id}`}
/>
)) : isFiltered && !isLoading && (
)) : isFiltered && !isPending && (
<Alert className="mt-4">
<Alert.Heading>
{intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)}

View File

@@ -115,13 +115,13 @@ describe('LibrariesV2Filters', () => {
});
it('should display the loading spinner when isLoading is true', () => {
renderComponent({ isLoading: true });
renderComponent({ isPending: true });
const spinner = screen.getByText('Loading...');
expect(spinner).toBeInTheDocument();
});
it('should not display the loading spinner when isLoading is false', () => {
renderComponent({ isLoading: false });
renderComponent({ isPending: false });
const spinner = screen.queryByText('Loading...');
expect(spinner).not.toBeInTheDocument();
});

View File

@@ -7,7 +7,7 @@ import LibrariesV2OrderFilterMenu from './libraries-v2-order-filter-menu';
import messages from '../../messages';
export interface LibrariesV2FiltersProps {
isLoading?: boolean;
isPending?: boolean;
isFiltered?: boolean;
filterParams: { search?: string | undefined, order?: string };
setFilterParams: React.Dispatch<React.SetStateAction<{ search: string | undefined, order: string }>>;
@@ -15,7 +15,7 @@ export interface LibrariesV2FiltersProps {
}
const LibrariesV2Filters: React.FC<LibrariesV2FiltersProps> = ({
isLoading = false,
isPending = false,
isFiltered = false,
filterParams,
setFilterParams,
@@ -93,7 +93,7 @@ const LibrariesV2Filters: React.FC<LibrariesV2FiltersProps> = ({
className="mr-4"
placeholder={intl.formatMessage(messages.librariesV2TabLibrarySearchPlaceholder)}
/>
{isLoading && (
{isPending && (
<span className="search-field-loading">
<LoadingSpinner size="sm" />
</span>

View File

@@ -63,19 +63,6 @@ const messages = defineMessages({
defaultMessage: 'Beta',
description: 'Text used to mark the Libraries v2 feature as "in beta"',
},
librariesV2TabBetaText: {
id: 'course-authoring.studio-home.libraries.tab.library.beta-text',
defaultMessage: 'Welcome to the new Beta Libraries experience! Libraries have been redesigned from the ground up,'
+ ' making it much easier to reuse and remix course content. The new Libraries space lets you create, organize and'
+ ' manage new content; reuse your content in as many courses as you\'d like; sync updates centrally; and create'
+ ' and randomize problem sets. See {link} for details.',
description: 'Explanatory text shown on the Libraries v2 tab during the beta release.',
},
librariesV2TabBetaTutorialLinkText: {
id: 'course-authoring.studio-home.libraries.tab.library.beta-link-text',
defaultMessage: 'Libraries v2 tutorial',
description: 'Text to use as the link in the "course-authoring.studio-home.libraries.tab.library.beta-text" message',
},
librariesV2TabLibrarySearchPlaceholder: {
id: 'course-authoring.studio-home.libraries.tab.library.search-placeholder',
defaultMessage: 'Search',
@@ -89,17 +76,17 @@ const messages = defineMessages({
defaultMessage: 'There are no libraries with the current filters.',
},
alertTitle: {
id: 'studio-home.legacy-libraries.migrate-alert.title',
id: 'studio-home.libraries.migrate-alert.title',
defaultMessage: 'Migrate Legacy Libraries',
description: 'Title for the alert message to migrate legacy libraries',
},
alertDescription: {
id: 'studio-home.legacy-libraries.migrate-alert.description',
alertDescriptionV1: {
id: 'studio-home.libraries.migrate-alert.description-v1',
defaultMessage: 'In a future release, legacy libraries will no longer be supported.'
+ ' The new libraries experience allows you to author sections, subsections, units,'
+ ' and components to reuse across your courses. Content from legacy libraries can be'
+ ' migrated to the new experience.',
description: 'Description for the alert message to migrate legacy libraries',
description: 'Description for the alert message to migrate legacy libraries on legacy libraries tab.',
},
alertReviewButton: {
id: 'studio-home.legacy-libraries.migrate-alert.review-button',
@@ -121,6 +108,24 @@ const messages = defineMessages({
description: 'Label text for unmigrated migration filter menu item in legacy libraries tab',
defaultMessage: 'Unmigrated',
},
alertDescriptionV2: {
id: 'studio-home.libraries.migrate-alert.description-v2',
defaultMessage: 'Welcome to the new Content Libraries experience! Libraries have been redesigned'
+ ' from the ground up, making it much easier to reuse content. You can create, organize and manage'
+ ' new content, reuse your content in as many courses as you\'d like, publish updates, and create/randomize'
+ ' problem sets. See {link} for details.',
description: 'Description for the alert message while there are no libraries pending migration on v2 tab.',
},
alertDescriptionV2MigrationPending: {
id: 'studio-home.libraries.migrate-alert.description-v2.migration-pending',
defaultMessage: ' Legacy libraries can be migrated using the migration tool.',
description: 'Complementary description for the alert message while there are libraries pending migration.',
},
alertLibrariesDocLinkText: {
id: 'studio-home.libraries.migrate-alert.docs',
defaultMessage: 'Libraries documentation',
description: 'Link text for the libraries documentation link.',
},
});
export default messages;