feat: Let Studio Home REST API determine if libraries v1 and/or v2 are enabled (#1329)

This commit is contained in:
Jillian
2024-10-19 05:33:26 +10:30
committed by GitHub
parent 40a6ee9ca5
commit cfe19894d1
17 changed files with 242 additions and 198 deletions

1
.env
View File

@@ -44,5 +44,4 @@ INVITE_STUDENTS_EMAIL_TO=''
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=''
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
LIBRARY_MODE="v1 only"
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"

View File

@@ -47,5 +47,4 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
LIBRARY_MODE="mixed"
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"

View File

@@ -39,5 +39,4 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
LIBRARY_MODE="mixed"
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"

View File

@@ -299,11 +299,13 @@ Configuration
In additional to the standard settings, the following local configurations can be set to switch between different library modes:
* ``LIBRARY_MODE``: can be set to ``mixed`` (default for development), ``v1 only`` (default for production) and ``v2 only``.
* ``MEILISEARCH_ENABLED``: Studio setting which is enabled when the `meilisearch plugin`_ is installed.
* ``edx-platform`` Waffle flags:
* ``mixed``: Shows 2 tabs, "Libraries" that lists the v2 libraries and "Legacy Libraries" that lists the v1 libraries. When creating a new library in this mode it will create a new v2 library.
* ``v1 only``: Shows only 1 tab, "Libraries" that lists v1 libraries only. When creating a new library in this mode it will create a new v1 library.
* ``v2 only``: Shows only 1 tab, "Libraries" that lists v2 libraries only. When creating a new library in this mode it will create a new v2 library.
* ``contentstore.new_studio_mfe.disable_legacy_libraries``: this feature flag must be OFF to show legacy Libraries V1
* ``contentstore.new_studio_mfe.disable_new_libraries``: this feature flag must be OFF to show Content Libraries V2
.. _meilisearch plugin: https://github.com/open-craft/tutor-contrib-meilisearch
Developing
**********

View File

@@ -139,7 +139,6 @@ initialize({
ENABLE_HOME_PAGE_COURSE_API_V2: process.env.ENABLE_HOME_PAGE_COURSE_API_V2 === 'true',
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
ENABLE_GRADING_METHOD_IN_PROBLEMS: process.env.ENABLE_GRADING_METHOD_IN_PROBLEMS === 'true',
LIBRARY_MODE: process.env.LIBRARY_MODE || 'v1 only',
LIBRARY_SUPPORTED_BLOCKS: (process.env.LIBRARY_SUPPORTED_BLOCKS || 'problem,video,html').split(','),
}, 'CourseAuthoringConfig');
},

View File

@@ -2,7 +2,7 @@ module.exports = {
next: null,
previous: null,
count: 2,
num_pages: 1,
num_pages: 2,
current_page: 1,
start: 0,
results: [

View File

@@ -44,7 +44,6 @@ mergeConfig({
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null,
LMS_BASE_URL: process.env.LMS_BASE_URL || null,
LIBRARY_MODE: process.env.LIBRARY_MODE || 'v1 only',
LIBRARY_SUPPORTED_BLOCKS: (process.env.LIBRARY_SUPPORTED_BLOCKS || 'problem,video,html').split(','),
}, 'CourseAuthoringConfig');

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { initializeMockApp, getConfig, setConfig } from '@edx/frontend-platform';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
@@ -165,21 +165,11 @@ describe('<StudioHome />', () => {
});
describe('render new library button', () => {
beforeEach(() => {
setConfig({
...getConfig(),
LIBRARY_MODE: 'mixed',
});
});
it('should navigate to home_library when in "v1 only" lib mode', () => {
setConfig({
...getConfig(),
LIBRARY_MODE: 'v1 only',
});
it('should navigate to home_library when libraries-v2 disabled', () => {
useSelector.mockReturnValue({
...studioHomeMock,
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
librariesV2Enabled: false,
});
const studioBaseUrl = 'http://localhost:18010';
@@ -196,7 +186,7 @@ describe('<StudioHome />', () => {
it('should navigate to the library authoring page in course authoring', () => {
useSelector.mockReturnValue({
...studioHomeMock,
LIBRARY_MODE: 'v2 only',
librariesV1Enabled: false,
});
const { getByTestId } = render(<RootWrapper />);
const createNewLibraryButton = getByTestId('new-library-button');
@@ -208,26 +198,20 @@ describe('<StudioHome />', () => {
});
it('do not render new library button for "v1 only" mode if showNewLibraryButton is False', () => {
setConfig({
...getConfig(),
LIBRARY_MODE: 'v1 only',
});
useSelector.mockReturnValue({
...studioHomeMock,
showNewLibraryButton: false,
librariesV2Enabled: false,
});
const { queryByTestId } = render(<RootWrapper />);
expect(queryByTestId('new-library-button')).not.toBeInTheDocument();
});
it('render new library button for "v2 only" mode even if showNewLibraryButton is False', () => {
setConfig({
...getConfig(),
LIBRARY_MODE: 'v2 only',
});
useSelector.mockReturnValue({
...studioHomeMock,
showNewLibraryButton: false,
librariesV1Enabled: false,
});
const { queryByTestId } = render(<RootWrapper />);
expect(queryByTestId('new-library-button')).toBeInTheDocument();

View File

@@ -19,7 +19,6 @@ import Header from '../header';
import SubHeader from '../generic/sub-header/SubHeader';
import HomeSidebar from './home-sidebar';
import TabsSection from './tabs-section';
import { isMixedOrV1LibrariesMode, isMixedOrV2LibrariesMode } from './tabs-section/utils';
import OrganizationSection from './organization-section';
import VerifyEmailLayout from './verify-email-layout';
import CreateNewCourseForm from './create-new-course-form';
@@ -46,12 +45,12 @@ const StudioHome = () => {
hasAbilityToCreateNewCourse,
isFiltered,
setShowNewCourseContainer,
librariesV1Enabled,
librariesV2Enabled,
} = useStudioHome(isPaginationCoursesEnabled);
const libMode = getConfig().LIBRARY_MODE;
const v1LibraryTab = isMixedOrV1LibrariesMode(libMode) && location?.pathname.split('/').pop() === 'libraries-v1';
const showV2LibraryURL = isMixedOrV2LibrariesMode(libMode) && !v1LibraryTab;
const v1LibraryTab = librariesV1Enabled && location?.pathname.split('/').pop() === 'libraries-v1';
const showV2LibraryURL = librariesV2Enabled && !v1LibraryTab;
const {
userIsActive,
@@ -155,6 +154,8 @@ const StudioHome = () => {
onClickNewCourse={() => setShowNewCourseContainer(true)}
isShowProcessing={isShowProcessing && !isFiltered}
isPaginationCoursesEnabled={isPaginationCoursesEnabled}
librariesV1Enabled={librariesV1Enabled}
librariesV2Enabled={librariesV2Enabled}
/>
</section>
</Layout.Element>

View File

@@ -61,7 +61,8 @@ module.exports = {
canEdit: true,
},
],
librariesEnabled: true,
librariesV1Enabled: true,
librariesV2Enabled: true,
optimizationEnabled: false,
requestCourseCreatorUrl: '/request_course_creator',
rerunCreatorStatus: true,

View File

@@ -31,7 +31,8 @@ export const generateGetStudioHomeDataApiResponse = () => ({
courses: [],
inProcessCourseActions: [],
libraries: [],
librariesEnabled: true,
librariesV1Enabled: true,
librariesV2Enabled: true,
optimizationEnabled: false,
requestCourseCreatorUrl: '/request_course_creator',
rerunCreatorStatus: true,

View File

@@ -68,6 +68,8 @@ const useStudioHome = (isPaginated = false) => {
studioRequestEmail,
inProcessCourseActions,
courseCreatorStatus,
librariesV1Enabled,
librariesV2Enabled,
} = studioHomeData;
const isShowOrganizationDropdown = optimizationEnabled && courseCreatorStatus === COURSE_CREATOR_STATES.granted;
@@ -94,6 +96,8 @@ const useStudioHome = (isPaginated = false) => {
hasAbilityToCreateNewCourse,
isFiltered,
setShowNewCourseContainer,
librariesV1Enabled,
librariesV2Enabled,
};
};

View File

@@ -15,7 +15,7 @@ import {
import { getApiBaseUrl, getStudioHomeApiUrl } from '../data/api';
import { executeThunk } from '../../utils';
import { fetchLibraryData, fetchStudioHomeData } from '../data/thunks';
import { getContentLibraryV2ListApiUrl } from '../../library-authoring/data/api';
import { mockGetContentLibraryV2List } from '../../library-authoring/data/api.mocks';
import contentLibrariesListV2 from '../../library-authoring/__mocks__/contentLibrariesListV2';
import {
initializeMocks,
@@ -33,12 +33,17 @@ 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}
showNewCourseContainer={false}
onClickNewCourse={() => {}}
isShowProcessing
librariesV1Enabled
librariesV2Enabled
{...overrideProps}
/>
);
@@ -66,11 +71,7 @@ describe('<TabsSection />', () => {
const newMocks = initializeMocks({ initialState });
store = newMocks.reduxStore;
axiosMock = newMocks.axiosMock;
setConfig({
...getConfig(),
LIBRARY_MODE: 'mixed',
});
axiosMock.onGet(getContentLibraryV2ListApiUrl()).reply(200, contentLibrariesListV2);
mockGetContentLibraryV2List.applyMock();
});
it('should render all tabs correctly', async () => {
@@ -90,24 +91,19 @@ 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 "v1 only" lib mode', async () => {
setConfig({
...getConfig(),
LIBRARY_MODE: 'v1 only',
});
it('should render only 1 library tab when libraries-v2 disabled', async () => {
const data = generateGetStudioHomeDataApiResponse();
render();
render({ librariesV2Enabled: false });
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -120,20 +116,15 @@ describe('<TabsSection />', () => {
expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).not.toBeInTheDocument();
});
it('should render only 1 library tab when "v2 only" lib mode', async () => {
setConfig({
...getConfig(),
LIBRARY_MODE: 'v2 only',
});
it('should render only 1 library tab when libraries-v1 disabled', async () => {
const data = generateGetStudioHomeDataApiResponse();
render();
render({ librariesV1Enabled: false });
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');
@@ -312,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();
});
});
@@ -348,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');
@@ -367,18 +358,16 @@ describe('<TabsSection />', () => {
});
it('should switch to Libraries tab and render specific v1 library details ("v1 only" mode)', async () => {
setConfig({
...getConfig(),
LIBRARY_MODE: 'v1 only',
});
render();
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');
@@ -389,21 +378,20 @@ describe('<TabsSection />', () => {
});
it('should switch to Libraries tab and render specific v2 library details ("v2 only" mode)', async () => {
setConfig({
...getConfig(),
LIBRARY_MODE: 'v2 only',
});
render();
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');
expect(screen.getByText('Showing 2 of 2')).toBeVisible();
expect(screen.getByText('Page 1, Current Page, of 2')).toBeVisible();
expect(screen.getByText(contentLibrariesListV2.results[0].title)).toBeVisible();
expect(screen.getByText(
@@ -416,11 +404,26 @@ describe('<TabsSection />', () => {
)).toBeVisible();
});
it('should show a "not found" message if no v2 libraries were loaded', async () => {
mockGetContentLibraryV2List.applyMockEmpty();
render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
const librariesTab = screen.getByRole('tab', { name: librariesBetaTabTitle });
fireEvent.click(librariesTab);
expect(librariesTab).toHaveClass('active');
expect(await screen.findByText(
tabMessages.librariesV2TabLibraryNotFoundAlertMessage.defaultMessage,
)).toBeVisible();
});
it('should hide Libraries tab when libraries are disabled', async () => {
const data = generateGetStudioHomeDataApiResponse();
data.librariesEnabled = false;
render();
render({ librariesV1Enabled: false });
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -428,7 +431,7 @@ describe('<TabsSection />', () => {
expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeNull();
});
it('should render libraries fetch failure alert', async () => {
it('should render legacy libraries fetch failure alert', async () => {
render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(libraryApiLink).reply(404);
@@ -442,5 +445,21 @@ describe('<TabsSection />', () => {
expect(await screen.findByText(tabMessages.librariesTabErrorMessage.defaultMessage)).toBeVisible();
});
it('should render v2 libraries fetch failure alert', async () => {
mockGetContentLibraryV2List.applyMockError();
render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
const librariesTab = screen.getByRole('tab', { name: librariesBetaTabTitle });
fireEvent.click(librariesTab);
expect(librariesTab).toHaveClass('active');
expect(await screen.findByText(
tabMessages.librariesTabErrorMessage.defaultMessage,
)).toBeVisible();
});
});
});

View File

@@ -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';
@@ -14,19 +19,19 @@ import ArchivedTab from './archived-tab';
import CoursesTab from './courses-tab';
import { RequestStatus } from '../../data/constants';
import { fetchLibraryData } from '../data/thunks';
import { isMixedOrV1LibrariesMode, isMixedOrV2LibrariesMode } from './utils';
const TabsSection = ({
showNewCourseContainer,
onClickNewCourse,
isShowProcessing,
isPaginationCoursesEnabled,
librariesV1Enabled,
librariesV2Enabled,
}) => {
const dispatch = useDispatch();
const intl = useIntl();
const navigate = useNavigate();
const { pathname } = useLocation();
const libMode = getConfig().LIBRARY_MODE;
const TABS_LIST = {
courses: 'courses',
libraries: 'libraries',
@@ -41,7 +46,7 @@ const TabsSection = ({
}
if (pname.includes('/libraries')) {
return isMixedOrV2LibrariesMode(libMode)
return librariesV2Enabled
? TABS_LIST.libraries
: TABS_LIST.legacyLibraries;
}
@@ -63,7 +68,7 @@ const TabsSection = ({
}, [pathname]);
const {
courses, librariesEnabled, libraries, archivedCourses,
courses, libraries, archivedCourses,
numPages, coursesCount,
} = useSelector(getStudioHomeData);
const {
@@ -115,38 +120,41 @@ const TabsSection = ({
);
}
if (librariesEnabled) {
if (isMixedOrV2LibrariesMode(libMode)) {
tabs.push(
<Tab
key={TABS_LIST.libraries}
eventKey={TABS_LIST.libraries}
title={intl.formatMessage(messages.librariesTabTitle)}
>
<LibrariesV2Tab />
</Tab>,
);
}
if (librariesV2Enabled) {
tabs.push(
<Tab
key={TABS_LIST.libraries}
eventKey={TABS_LIST.libraries}
title={(
<Stack gap={2} direction="horizontal">
{intl.formatMessage(messages.librariesTabTitle)}
<Badge variant="info">{intl.formatMessage(messages.librariesV2TabBetaBadge)}</Badge>
</Stack>
)}
>
<LibrariesV2Tab />
</Tab>,
);
}
if (isMixedOrV1LibrariesMode(libMode)) {
tabs.push(
<Tab
key={TABS_LIST.legacyLibraries}
eventKey={TABS_LIST.legacyLibraries}
title={intl.formatMessage(
libMode === 'v1 only'
? messages.librariesTabTitle
: messages.legacyLibrariesTabTitle,
)}
>
<LibrariesTab
libraries={libraries}
isLoading={isLoadingLibraries}
isFailed={isFailedLibrariesPage}
/>
</Tab>,
);
}
if (librariesV1Enabled) {
tabs.push(
<Tab
key={TABS_LIST.legacyLibraries}
eventKey={TABS_LIST.legacyLibraries}
title={intl.formatMessage(
librariesV2Enabled
? messages.legacyLibrariesTabTitle
: messages.librariesTabTitle,
)}
>
<LibrariesTab
libraries={libraries}
isLoading={isLoadingLibraries}
isFailed={isFailedLibrariesPage}
/>
</Tab>,
);
}
if (getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true') {
@@ -160,7 +168,7 @@ const TabsSection = ({
}
return tabs;
}, [archivedCourses, librariesEnabled, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]);
}, [archivedCourses, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]);
const handleSelectTab = (tab) => {
if (tab === TABS_LIST.courses) {
@@ -197,6 +205,8 @@ TabsSection.propTypes = {
onClickNewCourse: PropTypes.func.isRequired,
isShowProcessing: PropTypes.bool.isRequired,
isPaginationCoursesEnabled: PropTypes.bool,
librariesV1Enabled: PropTypes.bool,
librariesV2Enabled: PropTypes.bool,
};
export default TabsSection;

View File

@@ -49,81 +49,96 @@ const LibrariesV2Tab: React.FC<Props> = () => {
);
}
const hasV2Libraries = !isLoading && ((data!.results.length || 0) > 0);
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>
);
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>
<>
<Alert variant="info">
{intl.formatMessage(
messages.librariesV2TabBetaText,
{ link: librariesTutorialLink },
)}
/>
) : (
<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>
{ hasV2Libraries
? data!.results.map(({
id, org, slug, title,
}) => (
<CardItem
key={`${org}+${slug}`}
isLibraries
displayName={title}
org={org}
number={slug}
path={`/library/${id}`}
{isError ? (
<AlertMessage
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 && !isError
&& (
<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>
)}
</>
);
};

View File

@@ -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',

View File

@@ -11,11 +11,5 @@ const sortAlphabeticallyArray = (arr) => [...arr]
return firstDisplayName.localeCompare(secondDisplayName);
});
const isMixedOrV1LibrariesMode = (libMode) => ['mixed', 'v1 only'].includes(libMode);
const isMixedOrV2LibrariesMode = (libMode) => ['mixed', 'v2 only'].includes(libMode);
export {
sortAlphabeticallyArray,
isMixedOrV1LibrariesMode,
isMixedOrV2LibrariesMode,
};
// eslint-disable-next-line import/prefer-default-export
export { sortAlphabeticallyArray };