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:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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'); });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export const studioHomeQueryKeys = {
|
||||
export const useLibrariesV1Data = (enabled: boolean = true) => (
|
||||
useQuery({
|
||||
queryKey: studioHomeQueryKeys.librariesV1(),
|
||||
queryFn: () => getStudioHomeLibraries(),
|
||||
queryFn: getStudioHomeLibraries,
|
||||
enabled,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user