Skip to content

Commit

Permalink
feat: adds Libraries "beta" badge, explanatory text, and tutorial link
Browse files Browse the repository at this point in the history
to the Libraries v2 tab page, and updates the tab tests accordingly.
  • Loading branch information
pomegranited committed Oct 3, 2024
1 parent 31b1fc5 commit d1986ad
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 98 deletions.
59 changes: 31 additions & 28 deletions src/studio-home/tabs-section/TabsSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
const courseApiLinkV2 = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;

// The Libraries v2 tab title contains a badge, so we need to use regex to match its tab text.
const librariesBetaTabTitle = /Libraries Beta/;

const tabSectionComponent = (overrideProps) => (
<TabsSection
isPaginationCoursesEnabled={false}
Expand Down Expand Up @@ -88,18 +91,20 @@ describe('<TabsSection />', () => {
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);

expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByRole('tab', { name: tabMessages.coursesTabTitle.defaultMessage })).toBeInTheDocument();

expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByRole('tab', { name: librariesBetaTabTitle })).toBeInTheDocument();

expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByRole('tab', { name: tabMessages.legacyLibrariesTabTitle.defaultMessage })).toBeInTheDocument();

expect(screen.getByText(tabMessages.archivedTabTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByRole('tab', { name: tabMessages.archivedTabTitle.defaultMessage })).toBeInTheDocument();
});

it('should render only 1 library tab when libraries-v2 disabled', async () => {
const data = generateGetStudioHomeDataApiResponse();

render({ librariesV2Enabled: false });
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);

expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
Expand All @@ -112,12 +117,14 @@ describe('<TabsSection />', () => {
});

it('should render only 1 library tab when libraries-v1 disabled', async () => {
const data = generateGetStudioHomeDataApiResponse();

render({ librariesV1Enabled: false });
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);

expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
const librariesTab = screen.getByRole('tab', { name: tabMessages.librariesTabTitle.defaultMessage });
const librariesTab = screen.getByRole('tab', { name: librariesBetaTabTitle });
expect(librariesTab).toBeInTheDocument();
// Check Tab.eventKey
expect(librariesTab).toHaveAttribute('data-rb-event-key', 'libraries');
Expand Down Expand Up @@ -296,13 +303,13 @@ describe('<TabsSection />', () => {
axiosMock.onGet(courseApiLink).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);

expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByRole('tab', { name: tabMessages.coursesTabTitle.defaultMessage })).toBeInTheDocument();

expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByRole('tab', { name: librariesBetaTabTitle })).toBeInTheDocument();

expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByRole('tab', { name: tabMessages.legacyLibrariesTabTitle.defaultMessage })).toBeInTheDocument();

expect(screen.queryByText(tabMessages.archivedTabTitle.defaultMessage)).toBeNull();
expect(screen.queryByRole('tab', { name: tabMessages.archivedTabTitle.defaultMessage })).toBeNull();
});
});

Expand Down Expand Up @@ -332,7 +339,7 @@ describe('<TabsSection />', () => {
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);

const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
const librariesTab = screen.getByRole('tab', { name: librariesBetaTabTitle });
fireEvent.click(librariesTab);

expect(librariesTab).toHaveClass('active');
Expand All @@ -351,18 +358,16 @@ describe('<TabsSection />', () => {
});

it('should switch to Libraries tab and render specific v1 library details ("v1 only" mode)', async () => {
const data = {
...generateGetStudioHomeDataApiResponse(),
librariesV2Enabled: false,
};

render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
render({ librariesV2Enabled: false });
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
await executeThunk(fetchLibraryData(), store.dispatch);

const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
// Libraries v2 tab should not be shown
expect(screen.queryByRole('tab', { name: librariesBetaTabTitle })).toBeNull();

const librariesTab = screen.getByRole('tab', { name: tabMessages.librariesTabTitle.defaultMessage });
fireEvent.click(librariesTab);

expect(librariesTab).toHaveClass('active');
Expand All @@ -373,16 +378,14 @@ describe('<TabsSection />', () => {
});

it('should switch to Libraries tab and render specific v2 library details ("v2 only" mode)', async () => {
const data = {
...generateGetStudioHomeDataApiResponse(),
librariesV1Enabled: false,
};

render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
render({ librariesV1Enabled: false });
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);

const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
// Libraries v1 tab should not be shown
expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeNull();

const librariesTab = screen.getByRole('tab', { name: librariesBetaTabTitle });
fireEvent.click(librariesTab);

expect(librariesTab).toHaveClass('active');
Expand Down
14 changes: 12 additions & 2 deletions src/studio-home/tabs-section/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React, { useMemo, useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Tab, Tabs } from '@openedx/paragon';
import {
Badge,
Stack,
Tab,
Tabs,
} from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNavigate, useLocation } from 'react-router-dom';
Expand Down Expand Up @@ -121,7 +126,12 @@ const TabsSection = ({
<Tab
key={TABS_LIST.libraries}
eventKey={TABS_LIST.libraries}
title={intl.formatMessage(messages.librariesTabTitle)}
title={(
<Stack gap={2} direction="horizontal">
{intl.formatMessage(messages.librariesTabTitle)}
<Badge variant="info">{intl.formatMessage(messages.librariesV2TabBetaBadge)}</Badge>
</Stack>
)}
>
<LibrariesV2Tab />
</Tab>,
Expand Down
154 changes: 86 additions & 68 deletions src/studio-home/tabs-section/libraries-v2-tab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Alert,
Button,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Error } from '@openedx/paragon/icons';

import { useContentLibraryV2List } from '../../../library-authoring';
Expand Down Expand Up @@ -51,79 +51,97 @@ const LibrariesV2Tab: React.FC<Props> = () => {

const hasV2Libraries = !isLoading && ((data!.results.length || 0) > 0);

return (
isError ? (
<AlertMessage
title={intl.formatMessage(messages.librariesTabErrorMessage)}
variant="danger"
description={(
<Row className="m-0 align-items-center">
<Icon src={Error} className="text-danger-500 mr-1" />
<span>{intl.formatMessage(messages.librariesTabErrorMessage)}</span>
</Row>
)}
// TODO: update this link when tutorial is ready.
const librariesTutorialLink = (
<Alert.Link href="https://docs.openedx.org">
<FormattedMessage
{...messages.librariesV2TabBetaTutorialLinkText}
/>
) : (
<div className="courses-tab-container">
<div className="d-flex flex-row justify-content-between my-4">
<LibrariesV2Filters
isLoading={isLoading}
isFiltered={isFiltered}
filterParams={filterParams}
setFilterParams={setFilterParams}
setCurrentPage={setCurrentPage}
/>
{ !isLoading
&& (
<p>
{intl.formatMessage(messages.coursesPaginationInfo, {
length: data!.results.length,
total: data!.count,
})}
</p>
)}
</div>
</Alert.Link>
);

return (
<>
<Alert variant="info">
<FormattedMessage
{...messages.librariesV2TabBetaText}
values={{ link: librariesTutorialLink }}
/>
</Alert>

{ hasV2Libraries
? data!.results.map(({
id, org, slug, title,
}) => (
<CardItem
key={`${org}+${slug}`}
isLibraries
displayName={title}
org={org}
number={slug}
path={`/library/${id}`}
{isError ? (
<AlertMessage
title={intl.formatMessage(messages.librariesTabErrorMessage)}
variant="danger"
description={(
<Row className="m-0 align-items-center">
<Icon src={Error} className="text-danger-500 mr-1" />
<span>{intl.formatMessage(messages.librariesTabErrorMessage)}</span>
</Row>
)}
/>
) : (
<div className="courses-tab-container">
<div className="d-flex flex-row justify-content-between my-4">
<LibrariesV2Filters
isLoading={isLoading}
isFiltered={isFiltered}
filterParams={filterParams}
setFilterParams={setFilterParams}
setCurrentPage={setCurrentPage}
/>
)) : isFiltered && !isLoading && (
<Alert className="mt-4">
<Alert.Heading>
{intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)}
</Alert.Heading>
{ !isLoading
&& (
<p>
{intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)}
{intl.formatMessage(messages.coursesPaginationInfo, {
length: data!.results.length,
total: data!.count,
})}
</p>
<Button variant="primary" onClick={handleClearFilters}>
{intl.formatMessage(messages.coursesTabCourseNotFoundAlertCleanFiltersButton)}
</Button>
</Alert>
)}
)}
</div>

{
hasV2Libraries && (data!.numPages || 0) > 1
&& (
<Pagination
className="d-flex justify-content-center"
paginationLabel="pagination navigation"
pageCount={data!.numPages}
currentPage={currentPage}
onPageSelect={handlePageSelect}
/>
)
}
</div>
)
{ hasV2Libraries
? data!.results.map(({
id, org, slug, title,
}) => (
<CardItem
key={`${org}+${slug}`}
isLibraries
displayName={title}
org={org}
number={slug}
path={`/library/${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>
)}

{
hasV2Libraries && (data!.numPages || 0) > 1
&& (
<Pagination
className="d-flex justify-content-center"
paginationLabel="pagination navigation"
pageCount={data!.numPages}
currentPage={currentPage}
onPageSelect={handlePageSelect}
/>
)
}
</div>
)}
</>
);
};

Expand Down
18 changes: 18 additions & 0 deletions src/studio-home/tabs-section/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ const messages = defineMessages({
id: 'course-authoring.studio-home.libraries.placeholder.body',
defaultMessage: 'This is a placeholder page, as the Library Authoring MFE is not enabled.',
},
librariesV2TabBetaBadge: {
id: 'course-authoring.studio-home.libraries.tab.library.beta-badge',
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',
Expand Down

0 comments on commit d1986ad

Please sign in to comment.