feat: Let Studio Home REST API determine if libraries v1 and/or v2 are enabled (#1329)
This commit is contained in:
1
.env
1
.env
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
10
README.rst
10
README.rst
@@ -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
|
||||
**********
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ module.exports = {
|
||||
next: null,
|
||||
previous: null,
|
||||
count: 2,
|
||||
num_pages: 1,
|
||||
num_pages: 2,
|
||||
current_page: 1,
|
||||
start: 0,
|
||||
results: [
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -61,7 +61,8 @@ module.exports = {
|
||||
canEdit: true,
|
||||
},
|
||||
],
|
||||
librariesEnabled: true,
|
||||
librariesV1Enabled: true,
|
||||
librariesV2Enabled: true,
|
||||
optimizationEnabled: false,
|
||||
requestCourseCreatorUrl: '/request_course_creator',
|
||||
rerunCreatorStatus: true,
|
||||
|
||||
@@ -31,7 +31,8 @@ export const generateGetStudioHomeDataApiResponse = () => ({
|
||||
courses: [],
|
||||
inProcessCourseActions: [],
|
||||
libraries: [],
|
||||
librariesEnabled: true,
|
||||
librariesV1Enabled: true,
|
||||
librariesV2Enabled: true,
|
||||
optimizationEnabled: false,
|
||||
requestCourseCreatorUrl: '/request_course_creator',
|
||||
rerunCreatorStatus: true,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user