From ca0f48d2f993e777d3f0b137a18d72865861a45b Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 26 Sep 2024 12:52:07 +0930 Subject: [PATCH 1/4] refactor!: replace LIBRARY_MODE env config with Studio API waffle flags --- .env | 1 - .env.development | 1 - .env.test | 1 - README.rst | 10 ++-- src/index.jsx | 1 - src/setupTest.js | 1 - src/studio-home/StudioHome.test.jsx | 28 +++------- src/studio-home/StudioHome.tsx | 11 ++-- src/studio-home/__mocks__/studioHomeMock.js | 2 + .../factories/mockApiResponses.jsx | 2 + src/studio-home/hooks.jsx | 4 ++ .../tabs-section/TabsSection.test.tsx | 52 +++++++------------ src/studio-home/tabs-section/index.tsx | 18 ++++--- src/studio-home/tabs-section/utils.js | 10 +--- 14 files changed, 56 insertions(+), 86 deletions(-) diff --git a/.env b/.env index b0374e5409..14a6e4b1bb 100644 --- a/.env +++ b/.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" diff --git a/.env.development b/.env.development index 7d197a3b88..f6fb467b96 100644 --- a/.env.development +++ b/.env.development @@ -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" diff --git a/.env.test b/.env.test index e34ce6f1b1..335edbef5f 100644 --- a/.env.test +++ b/.env.test @@ -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" diff --git a/README.rst b/README.rst index 61015ad949..626406c5ea 100644 --- a/README.rst +++ b/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: + + * ``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 - * ``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. +.. _meilisearch plugin: https://github.com/open-craft/tutor-contrib-meilisearch Developing ********** diff --git a/src/index.jsx b/src/index.jsx index 29d0a9ac5e..84c4c4b606 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -133,7 +133,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'); }, diff --git a/src/setupTest.js b/src/setupTest.js index 06aa8e74aa..4cc847c713 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -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'); diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx index 08d4789479..bc7ce8fbf4 100644 --- a/src/studio-home/StudioHome.test.jsx +++ b/src/studio-home/StudioHome.test.jsx @@ -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('', () => { }); 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('', () => { it('should navigate to the library authoring page in course authoring', () => { useSelector.mockReturnValue({ ...studioHomeMock, - LIBRARY_MODE: 'v2 only', + librariesV1Enabled: false, }); const { getByTestId } = render(); const createNewLibraryButton = getByTestId('new-library-button'); @@ -208,26 +198,20 @@ describe('', () => { }); 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(); 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(); expect(queryByTestId('new-library-button')).toBeInTheDocument(); diff --git a/src/studio-home/StudioHome.tsx b/src/studio-home/StudioHome.tsx index 43b8caa703..56e6b00cf1 100644 --- a/src/studio-home/StudioHome.tsx +++ b/src/studio-home/StudioHome.tsx @@ -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} /> diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index a811c40518..bf48fb448c 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -62,6 +62,8 @@ module.exports = { }, ], librariesEnabled: true, + librariesV1Enabled: true, + librariesV2Enabled: true, optimizationEnabled: false, requestCourseCreatorUrl: '/request_course_creator', rerunCreatorStatus: true, diff --git a/src/studio-home/factories/mockApiResponses.jsx b/src/studio-home/factories/mockApiResponses.jsx index 7f1d2b4e5f..b8780d1728 100644 --- a/src/studio-home/factories/mockApiResponses.jsx +++ b/src/studio-home/factories/mockApiResponses.jsx @@ -32,6 +32,8 @@ export const generateGetStudioHomeDataApiResponse = () => ({ inProcessCourseActions: [], libraries: [], librariesEnabled: true, + librariesV1Enabled: true, + librariesV2Enabled: true, optimizationEnabled: false, requestCourseCreatorUrl: '/request_course_creator', rerunCreatorStatus: true, diff --git a/src/studio-home/hooks.jsx b/src/studio-home/hooks.jsx index 0e65cb31ee..584d69d842 100644 --- a/src/studio-home/hooks.jsx +++ b/src/studio-home/hooks.jsx @@ -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, }; }; diff --git a/src/studio-home/tabs-section/TabsSection.test.tsx b/src/studio-home/tabs-section/TabsSection.test.tsx index 0591f8c7cd..61eb28dea5 100644 --- a/src/studio-home/tabs-section/TabsSection.test.tsx +++ b/src/studio-home/tabs-section/TabsSection.test.tsx @@ -39,6 +39,8 @@ const tabSectionComponent = (overrideProps) => ( showNewCourseContainer={false} onClickNewCourse={() => {}} isShowProcessing + librariesV1Enabled + librariesV2Enabled {...overrideProps} /> ); @@ -66,10 +68,6 @@ describe('', () => { const newMocks = initializeMocks({ initialState }); store = newMocks.reduxStore; axiosMock = newMocks.axiosMock; - setConfig({ - ...getConfig(), - LIBRARY_MODE: 'mixed', - }); axiosMock.onGet(getContentLibraryV2ListApiUrl()).reply(200, contentLibrariesListV2); }); @@ -99,16 +97,9 @@ describe('', () => { expect(screen.getByText(tabMessages.archivedTabTitle.defaultMessage)).toBeInTheDocument(); }); - it('should render only 1 library tab when "v1 only" lib mode', async () => { - setConfig({ - ...getConfig(), - LIBRARY_MODE: 'v1 only', - }); - - const data = generateGetStudioHomeDataApiResponse(); - - render(); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data); + it('should render only 1 library tab when libraries-v2 disabled', async () => { + render({ librariesV2Enabled: false }); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); await executeThunk(fetchStudioHomeData(), store.dispatch); expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); @@ -120,16 +111,9 @@ describe('', () => { 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', - }); - - const data = generateGetStudioHomeDataApiResponse(); - - render(); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data); + it('should render only 1 library tab when libraries-v1 disabled', async () => { + render({ librariesV1Enabled: false }); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); await executeThunk(fetchStudioHomeData(), store.dispatch); expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); @@ -367,13 +351,13 @@ describe('', () => { }); it('should switch to Libraries tab and render specific v1 library details ("v1 only" mode)', async () => { - setConfig({ - ...getConfig(), - LIBRARY_MODE: 'v1 only', - }); + const data = { + ...generateGetStudioHomeDataApiResponse(), + librariesV2Enabled: false, + }; render(); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data); axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); await executeThunk(fetchStudioHomeData(), store.dispatch); await executeThunk(fetchLibraryData(), store.dispatch); @@ -389,13 +373,13 @@ describe('', () => { }); it('should switch to Libraries tab and render specific v2 library details ("v2 only" mode)', async () => { - setConfig({ - ...getConfig(), - LIBRARY_MODE: 'v2 only', - }); + const data = { + ...generateGetStudioHomeDataApiResponse(), + librariesV1Enabled: false, + }; render(); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data); await executeThunk(fetchStudioHomeData(), store.dispatch); const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); diff --git a/src/studio-home/tabs-section/index.tsx b/src/studio-home/tabs-section/index.tsx index ab83009a0e..c4b8315ea2 100644 --- a/src/studio-home/tabs-section/index.tsx +++ b/src/studio-home/tabs-section/index.tsx @@ -14,19 +14,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 +41,7 @@ const TabsSection = ({ } if (pname.includes('/libraries')) { - return isMixedOrV2LibrariesMode(libMode) + return librariesV2Enabled ? TABS_LIST.libraries : TABS_LIST.legacyLibraries; } @@ -116,7 +116,7 @@ const TabsSection = ({ } if (librariesEnabled) { - if (isMixedOrV2LibrariesMode(libMode)) { + if (librariesV2Enabled) { tabs.push( [...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 }; From 6487ef215f44df1e44693482b34a785a9c80bfed Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 3 Oct 2024 14:45:06 +0930 Subject: [PATCH 2/4] feat: adds Libraries "beta" badge, explanatory text, and tutorial link to the Libraries v2 tab page, and updates the tab tests accordingly. --- .../tabs-section/TabsSection.test.tsx | 59 +++---- src/studio-home/tabs-section/index.tsx | 14 +- .../tabs-section/libraries-v2-tab/index.tsx | 148 ++++++++++-------- src/studio-home/tabs-section/messages.ts | 18 +++ 4 files changed, 143 insertions(+), 96 deletions(-) diff --git a/src/studio-home/tabs-section/TabsSection.test.tsx b/src/studio-home/tabs-section/TabsSection.test.tsx index 61eb28dea5..a35192a059 100644 --- a/src/studio-home/tabs-section/TabsSection.test.tsx +++ b/src/studio-home/tabs-section/TabsSection.test.tsx @@ -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) => ( ', () => { 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(); @@ -112,12 +117,14 @@ describe('', () => { }); 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'); @@ -296,13 +303,13 @@ describe('', () => { 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(); }); }); @@ -332,7 +339,7 @@ describe('', () => { 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'); @@ -351,18 +358,16 @@ describe('', () => { }); 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'); @@ -373,16 +378,14 @@ describe('', () => { }); 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'); diff --git a/src/studio-home/tabs-section/index.tsx b/src/studio-home/tabs-section/index.tsx index c4b8315ea2..13f286da9a 100644 --- a/src/studio-home/tabs-section/index.tsx +++ b/src/studio-home/tabs-section/index.tsx @@ -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'; @@ -121,7 +126,12 @@ const TabsSection = ({ + {intl.formatMessage(messages.librariesTabTitle)} + {intl.formatMessage(messages.librariesV2TabBetaBadge)} + + )} > , diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index 0ef4e6d772..e159f00bef 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -51,79 +51,95 @@ const LibrariesV2Tab: React.FC = () => { const hasV2Libraries = !isLoading && ((data!.results.length || 0) > 0); + // TODO: update this link when tutorial is ready. + const librariesTutorialLink = ( + + {intl.formatMessage(messages.librariesV2TabBetaTutorialLinkText)} + + ); + return ( - isError ? ( - - - {intl.formatMessage(messages.librariesTabErrorMessage)} - + <> + + {intl.formatMessage( + messages.librariesV2TabBetaText, + { link: librariesTutorialLink }, )} - /> - ) : ( -
-
- - { !isLoading - && ( -

- {intl.formatMessage(messages.coursesPaginationInfo, { - length: data!.results.length, - total: data!.count, - })} -

- )} -
+ - { hasV2Libraries - ? data!.results.map(({ - id, org, slug, title, - }) => ( - + + {intl.formatMessage(messages.librariesTabErrorMessage)} + + )} + /> + ) : ( +
+
+ - )) : isFiltered && !isLoading && ( - - - {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)} - + { !isLoading + && (

- {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)} + {intl.formatMessage(messages.coursesPaginationInfo, { + length: data!.results.length, + total: data!.count, + })}

- -
- )} + )} +
- { - hasV2Libraries && (data!.numPages || 0) > 1 - && ( - - ) - } -
- ) + { hasV2Libraries + ? data!.results.map(({ + id, org, slug, title, + }) => ( + + )) : isFiltered && !isLoading && ( + + + {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)} + +

+ {intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)} +

+ +
+ )} + + { + hasV2Libraries && (data!.numPages || 0) > 1 + && ( + + ) + } +
+ )} + ); }; diff --git a/src/studio-home/tabs-section/messages.ts b/src/studio-home/tabs-section/messages.ts index 076f9c8eb7..cecac598e1 100644 --- a/src/studio-home/tabs-section/messages.ts +++ b/src/studio-home/tabs-section/messages.ts @@ -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', From 850588e1995c27b43a50e7f72253faf0a48b75b5 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 17 Oct 2024 15:14:55 +1030 Subject: [PATCH 3/4] refactor: use librariesV1Enabled and librariesV2Enabled instead of librariesEnabled flag --- src/studio-home/__mocks__/studioHomeMock.js | 1 - .../factories/mockApiResponses.jsx | 1 - .../tabs-section/TabsSection.test.tsx | 3 +- src/studio-home/tabs-section/index.tsx | 76 +++++++++---------- 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index bf48fb448c..b9aedf3549 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -61,7 +61,6 @@ module.exports = { canEdit: true, }, ], - librariesEnabled: true, librariesV1Enabled: true, librariesV2Enabled: true, optimizationEnabled: false, diff --git a/src/studio-home/factories/mockApiResponses.jsx b/src/studio-home/factories/mockApiResponses.jsx index b8780d1728..8d86e22d4a 100644 --- a/src/studio-home/factories/mockApiResponses.jsx +++ b/src/studio-home/factories/mockApiResponses.jsx @@ -31,7 +31,6 @@ export const generateGetStudioHomeDataApiResponse = () => ({ courses: [], inProcessCourseActions: [], libraries: [], - librariesEnabled: true, librariesV1Enabled: true, librariesV2Enabled: true, optimizationEnabled: false, diff --git a/src/studio-home/tabs-section/TabsSection.test.tsx b/src/studio-home/tabs-section/TabsSection.test.tsx index a35192a059..44abef4449 100644 --- a/src/studio-home/tabs-section/TabsSection.test.tsx +++ b/src/studio-home/tabs-section/TabsSection.test.tsx @@ -405,9 +405,8 @@ describe('', () => { 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); diff --git a/src/studio-home/tabs-section/index.tsx b/src/studio-home/tabs-section/index.tsx index 13f286da9a..50f2905939 100644 --- a/src/studio-home/tabs-section/index.tsx +++ b/src/studio-home/tabs-section/index.tsx @@ -68,7 +68,7 @@ const TabsSection = ({ }, [pathname]); const { - courses, librariesEnabled, libraries, archivedCourses, + courses, libraries, archivedCourses, numPages, coursesCount, } = useSelector(getStudioHomeData); const { @@ -120,43 +120,41 @@ const TabsSection = ({ ); } - if (librariesEnabled) { - if (librariesV2Enabled) { - tabs.push( - - {intl.formatMessage(messages.librariesTabTitle)} - {intl.formatMessage(messages.librariesV2TabBetaBadge)} - - )} - > - - , - ); - } - - if (librariesV1Enabled) { - tabs.push( - - - , - ); - } + if (librariesV2Enabled) { + tabs.push( + + {intl.formatMessage(messages.librariesTabTitle)} + {intl.formatMessage(messages.librariesV2TabBetaBadge)} + + )} + > + + , + ); + } + + if (librariesV1Enabled) { + tabs.push( + + + , + ); } if (getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true') { @@ -170,7 +168,7 @@ const TabsSection = ({ } return tabs; - }, [archivedCourses, librariesEnabled, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]); + }, [archivedCourses, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]); const handleSelectTab = (tab) => { if (tab === TABS_LIST.courses) { From d4d7671457acce60a917ed8b085056ab843189ce Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 17 Oct 2024 17:44:28 +1030 Subject: [PATCH 4/4] test: fix test coverage --- .../__mocks__/contentLibrariesListV2.js | 2 +- .../tabs-section/TabsSection.test.tsx | 39 +++++++++++++++++-- .../tabs-section/libraries-v2-tab/index.tsx | 5 +-- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/library-authoring/__mocks__/contentLibrariesListV2.js b/src/library-authoring/__mocks__/contentLibrariesListV2.js index 02257a9744..931a2f8150 100644 --- a/src/library-authoring/__mocks__/contentLibrariesListV2.js +++ b/src/library-authoring/__mocks__/contentLibrariesListV2.js @@ -2,7 +2,7 @@ module.exports = { next: null, previous: null, count: 2, - num_pages: 1, + num_pages: 2, current_page: 1, start: 0, results: [ diff --git a/src/studio-home/tabs-section/TabsSection.test.tsx b/src/studio-home/tabs-section/TabsSection.test.tsx index 44abef4449..e35c470109 100644 --- a/src/studio-home/tabs-section/TabsSection.test.tsx +++ b/src/studio-home/tabs-section/TabsSection.test.tsx @@ -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, @@ -71,7 +71,7 @@ describe('', () => { const newMocks = initializeMocks({ initialState }); store = newMocks.reduxStore; axiosMock = newMocks.axiosMock; - axiosMock.onGet(getContentLibraryV2ListApiUrl()).reply(200, contentLibrariesListV2); + mockGetContentLibraryV2List.applyMock(); }); it('should render all tabs correctly', async () => { @@ -391,6 +391,7 @@ describe('', () => { 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( @@ -403,6 +404,22 @@ describe('', () => { )).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(); @@ -414,7 +431,7 @@ describe('', () => { 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); @@ -428,5 +445,21 @@ describe('', () => { 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(); + }); }); }); diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index e159f00bef..605ec71416 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -49,7 +49,7 @@ const LibrariesV2Tab: React.FC = () => { ); } - 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 = ( @@ -69,7 +69,6 @@ const LibrariesV2Tab: React.FC = () => { {isError ? ( @@ -88,7 +87,7 @@ const LibrariesV2Tab: React.FC = () => { setFilterParams={setFilterParams} setCurrentPage={setCurrentPage} /> - { !isLoading + {!isLoading && !isError && (

{intl.formatMessage(messages.coursesPaginationInfo, {