From 4191ecb5d0047b49e0534bb3bef48b33812887ec Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 27 May 2024 20:13:28 +0300 Subject: [PATCH 001/106] feat: Add lib v2/legacy tabs in studio home When lib mode is set to "mixed", both "Libraries" and "Legacy Libraries" tabs are show in the Studio Home. When "Libraries" is clicked, v2 libraries are fetched, when "Legacy Libraries" is clicked, v1 libraries are fetched. When lib mode is set to "v1 only" or "v2 only", only one tab "Libraries" is show and only the respective libraries are fetched when the tab is clicked. --- src/studio-home/StudioHome.jsx | 9 ++- src/studio-home/data/api.js | 5 ++ src/studio-home/data/apiHooks.ts | 13 +++++ .../tabs-section/TabsSection.test.jsx | 12 ++-- src/studio-home/tabs-section/index.jsx | 47 ++++++++++----- .../tabs-section/libraries-v2-tab/index.tsx | 58 +++++++++++++++++++ src/studio-home/tabs-section/messages.js | 4 ++ src/studio-home/tabs-section/utils.js | 10 +++- 8 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 src/studio-home/data/apiHooks.ts create mode 100644 src/studio-home/tabs-section/libraries-v2-tab/index.tsx diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 8348aaca34..acc5cd1174 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -18,6 +18,7 @@ import Header from '../header'; import SubHeader from '../generic/sub-header/SubHeader'; import HomeSidebar from './home-sidebar'; import TabsSection from './tabs-section'; +import { isMixedOrV2LibrariesMode } from './tabs-section/utils'; import OrganizationSection from './organization-section'; import VerifyEmailLayout from './verify-email-layout'; import CreateNewCourseForm from './create-new-course-form'; @@ -43,12 +44,14 @@ const StudioHome = ({ intl }) => { dispatch, } = useStudioHome(isPaginationCoursesEnabled); + // TODO: this should be a flag in the backend + const LIB_MODE = 'mixed'; + const { userIsActive, studioShortName, studioRequestEmail, libraryAuthoringMfeUrl, - redirectToLibraryAuthoringMfe, } = studioHomeData; function getHeaderButtons() { @@ -79,8 +82,8 @@ const StudioHome = ({ intl }) => { } let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; - if (redirectToLibraryAuthoringMfe) { - libraryHref = `${libraryAuthoringMfeUrl}/create`; + if (isMixedOrV2LibrariesMode(LIB_MODE)) { + libraryHref = `${libraryAuthoringMfeUrl}create`; } headerButtons.push( diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index 1fefe2981a..0c09601d11 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -40,6 +40,11 @@ export async function getStudioHomeLibraries() { return camelCaseObject(data); } +export async function getStudioHomeLibrariesV2() { + const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`); + return camelCaseObject(data); +} + /** * Handle course notification requests. * @param {string} url diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.ts new file mode 100644 index 0000000000..7285874c64 --- /dev/null +++ b/src/studio-home/data/apiHooks.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getStudioHomeLibrariesV2 } from './api'; + +/** + * Builds the query to fetch list of V2 Libraries + */ +export const useListStudioHomeV2Libraries = () => ( + useQuery({ + queryKey: ['listV2Libraries'], + queryFn: () => getStudioHomeLibrariesV2(), + }) +); diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index fdc955d8df..ea5929aeec 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -80,7 +80,7 @@ describe('', () => { expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); - expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument(); expect(screen.getByText(tabMessages.archivedTabTitle.defaultMessage)).toBeInTheDocument(); }); @@ -222,7 +222,7 @@ describe('', () => { expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); - expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument(); expect(screen.queryByText(tabMessages.archivedTabTitle.defaultMessage)).toBeNull(); }); @@ -236,7 +236,7 @@ describe('', () => { await executeThunk(fetchStudioHomeData(), store.dispatch); await executeThunk(fetchLibraryData(), store.dispatch); - const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); await act(async () => { fireEvent.click(librariesTab); }); @@ -257,7 +257,7 @@ describe('', () => { await executeThunk(fetchStudioHomeData(), store.dispatch); expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); - expect(screen.queryByText(tabMessages.librariesTabTitle.defaultMessage)).toBeNull(); + expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeNull(); }); it('should redirect to library authoring mfe', async () => { @@ -268,7 +268,7 @@ describe('', () => { axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data); await executeThunk(fetchStudioHomeData(), store.dispatch); - const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); fireEvent.click(librariesTab); waitFor(() => { @@ -283,7 +283,7 @@ describe('', () => { await executeThunk(fetchStudioHomeData(), store.dispatch); await executeThunk(fetchLibraryData(), store.dispatch); - const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); await act(async () => { fireEvent.click(librariesTab); }); diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 1409766c47..789bb2bea1 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -9,10 +9,12 @@ import { useNavigate } from 'react-router-dom'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; import LibrariesTab from './libraries-tab'; +import LibrariesV2Tab from './libraries-v2-tab/index.tsx'; 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 = ({ intl, @@ -23,9 +25,14 @@ const TabsSection = ({ isPaginationCoursesEnabled, }) => { const navigate = useNavigate(); + + // TODO: this should be a flag in the backend + const LIB_MODE = 'mixed'; + const TABS_LIST = { courses: 'courses', libraries: 'libraries', + legacyLibraries: 'legacyLibraries', archived: 'archived', taxonomies: 'taxonomies', }; @@ -87,21 +94,37 @@ const TabsSection = ({ } if (librariesEnabled) { - tabs.push( - - {!redirectToLibraryAuthoringMfe && ( + if (isMixedOrV2LibrariesMode(LIB_MODE)) { + tabs.push( + + + , + ); + } + + if (isMixedOrV1LibrariesMode(LIB_MODE)) { + tabs.push( + - )} - , - ); + , + ); + } } if (getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true') { @@ -118,9 +141,7 @@ const TabsSection = ({ }, [archivedCourses, librariesEnabled, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]); const handleSelectTab = (tab) => { - if (tab === TABS_LIST.libraries && redirectToLibraryAuthoringMfe) { - window.location.assign(libraryAuthoringMfeUrl); - } else if (tab === TABS_LIST.libraries && !redirectToLibraryAuthoringMfe) { + if (tab === TABS_LIST.legacyLibraries) { dispatch(fetchLibraryData()); } else if (tab === TABS_LIST.taxonomies) { navigate('/taxonomies'); diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx new file mode 100644 index 0000000000..1e14ffef6c --- /dev/null +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Icon, Row } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { useListStudioHomeV2Libraries } from '../../data/apiHooks'; +import { LoadingSpinner } from '../../../generic/Loading'; +import AlertMessage from '../../../generic/alert-message'; +import CardItem from '../../card-item'; +import messages from '../messages'; + +const LibrariesV2Tab = () => { + const intl = useIntl(); + const { + data, + isLoading, + isError, + } = useListStudioHomeV2Libraries(); + + if (isLoading) { + return ( + + + + ); + } + + return ( + isError ? ( + + + {intl.formatMessage(messages.librariesTabErrorMessage)} + + )} + /> + ) : ( +
+ {data.map(({ org, slug, title }) => ( + + ))} +
+ ) + ); +}; + + +export default LibrariesV2Tab; diff --git a/src/studio-home/tabs-section/messages.js b/src/studio-home/tabs-section/messages.js index 5ae2e139b2..e1ad0fd44f 100644 --- a/src/studio-home/tabs-section/messages.js +++ b/src/studio-home/tabs-section/messages.js @@ -21,6 +21,10 @@ const messages = defineMessages({ id: 'course-authoring.studio-home.libraries.tab.title', defaultMessage: 'Libraries', }, + legacyLibrariesTabTitle: { + id: 'course-authoring.studio-home.legacy.libraries.tab.title', + defaultMessage: 'Legacy Libraries', + }, archivedTabTitle: { id: 'course-authoring.studio-home.archived.tab.title', defaultMessage: 'Archived courses', diff --git a/src/studio-home/tabs-section/utils.js b/src/studio-home/tabs-section/utils.js index 5d3822b8ed..e7dea1ad69 100644 --- a/src/studio-home/tabs-section/utils.js +++ b/src/studio-home/tabs-section/utils.js @@ -8,5 +8,11 @@ const sortAlphabeticallyArray = (arr) => [...arr] .sort((firstArrayData, secondArrayData) => firstArrayData .displayName.localeCompare(secondArrayData.displayName)); -// eslint-disable-next-line import/prefer-default-export -export { sortAlphabeticallyArray }; +const isMixedOrV1LibrariesMode = (libMode) => ['mixed', 'v1 only'].includes(libMode); +const isMixedOrV2LibrariesMode = (libMode) => ['mixed', 'v2 only'].includes(libMode); + +export { + sortAlphabeticallyArray, + isMixedOrV1LibrariesMode, + isMixedOrV2LibrariesMode, +}; From 15c678b8fa6248dfba857bca098d3426e3878341 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Tue, 28 May 2024 17:31:08 +0300 Subject: [PATCH 002/106] feat: Add `LIBRARY_MODE` config variable This is to switch between different library modes. --- .env | 1 + .env.development | 1 + .env.test | 1 + README.rst | 16 ++++++++++++++++ .../feature-v2-and-legacy-libs.png | Bin 0 -> 246316 bytes src/index.jsx | 1 + src/studio-home/StudioHome.jsx | 5 ++--- src/studio-home/tabs-section/index.jsx | 11 ++++------- 8 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 docs/readme-images/feature-v2-and-legacy-libs.png diff --git a/.env b/.env index ce17454708..4235461134 100644 --- a/.env +++ b/.env @@ -43,3 +43,4 @@ AI_TRANSLATIONS_BASE_URL='' ENABLE_HOME_PAGE_COURSE_API_V2=false ENABLE_CHECKLIST_QUALITY='' ENABLE_GRADING_METHOD_IN_PROBLEMS=false +LIBRARY_MODE="v1 only" diff --git a/.env.development b/.env.development index 983ce9674f..5547e8ffec 100644 --- a/.env.development +++ b/.env.development @@ -46,3 +46,4 @@ AI_TRANSLATIONS_BASE_URL='http://localhost:18760' ENABLE_HOME_PAGE_COURSE_API_V2=false ENABLE_CHECKLIST_QUALITY=true ENABLE_GRADING_METHOD_IN_PROBLEMS=false +LIBRARY_MODE="mixed" diff --git a/.env.test b/.env.test index 28240ad2ff..0f73517968 100644 --- a/.env.test +++ b/.env.test @@ -37,3 +37,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" diff --git a/README.rst b/README.rst index 3847453ea3..6f1de194b4 100644 --- a/README.rst +++ b/README.rst @@ -264,6 +264,22 @@ In additional to the standard settings, the following local configuration items Tagging/Taxonomy functionality. +Feature: Libraries V2/Legacy Tabs +================================= + +.. image:: ./docs/readme-images/feature-v2-and-legacy-libs.png + +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``. + + * ``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. + Developing ********** diff --git a/docs/readme-images/feature-v2-and-legacy-libs.png b/docs/readme-images/feature-v2-and-legacy-libs.png new file mode 100644 index 0000000000000000000000000000000000000000..c8fd363655f7e4fc0b026bc9c7f9faf0df045e7a GIT binary patch literal 246316 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
-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

wa;w%Q#(cB+nr}wMmuC0|@ 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 { dispatch, } = useStudioHome(isPaginationCoursesEnabled); - // TODO: this should be a flag in the backend - const LIB_MODE = 'mixed'; + const libMode = getConfig().LIBRARY_MODE; const { userIsActive, @@ -82,7 +81,7 @@ const StudioHome = ({ intl }) => { } let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; - if (isMixedOrV2LibrariesMode(LIB_MODE)) { + if (isMixedOrV2LibrariesMode(libMode)) { libraryHref = `${libraryAuthoringMfeUrl}create`; } diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 789bb2bea1..997796913f 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -25,10 +25,7 @@ const TabsSection = ({ isPaginationCoursesEnabled, }) => { const navigate = useNavigate(); - - // TODO: this should be a flag in the backend - const LIB_MODE = 'mixed'; - + const libMode = getConfig().LIBRARY_MODE; const TABS_LIST = { courses: 'courses', libraries: 'libraries', @@ -94,7 +91,7 @@ const TabsSection = ({ } if (librariesEnabled) { - if (isMixedOrV2LibrariesMode(LIB_MODE)) { + if (isMixedOrV2LibrariesMode(libMode)) { tabs.push( Date: Tue, 28 May 2024 21:23:39 +0300 Subject: [PATCH 003/106] feat: Add url paths/navigation for each tab The path updates when selecting tabs, when accessing the url with the path directly it will open its respective tab. Navigating using the browser back/forward buttons is also supported. --- src/index.jsx | 2 ++ src/studio-home/data/api.js | 4 +++ src/studio-home/tabs-section/index.jsx | 48 +++++++++++++++++++++++--- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index e3d21096a3..f17c0563ab 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -52,6 +52,8 @@ const App = () => { createRoutesFromElements( } /> + } /> + } /> } /> } /> {getConfig().ENABLE_ACCESSIBILITY_PAGE === 'true' && ( diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index 0c09601d11..69e0487fff 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -40,6 +40,10 @@ export async function getStudioHomeLibraries() { return camelCaseObject(data); } +/** + * Get's studio home v2 Libraries. + * @returns {Promise} + */ export async function getStudioHomeLibrariesV2() { const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`); return camelCaseObject(data); diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 997796913f..089f9bc842 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -1,10 +1,10 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { Tab, Tabs } from '@openedx/paragon'; import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; @@ -25,6 +25,7 @@ const TabsSection = ({ isPaginationCoursesEnabled, }) => { const navigate = useNavigate(); + const { pathname } = useLocation(); const libMode = getConfig().LIBRARY_MODE; const TABS_LIST = { courses: 'courses', @@ -33,7 +34,37 @@ const TabsSection = ({ archived: 'archived', taxonomies: 'taxonomies', }; - const [tabKey, setTabKey] = useState(TABS_LIST.courses); + + const initTabKeyState = (pname) => { + if (pname.includes('/libraries')) { + return isMixedOrV2LibrariesMode(libMode) + ? TABS_LIST.libraries + : TABS_LIST.legacyLibraries; + } + + if (pname.includes('/legacy-libraries')) { + return TABS_LIST.legacyLibraries; + } + + // Default to courses tab + return TABS_LIST.courses; + }; + + const [tabKey, setTabKey] = useState(initTabKeyState(pathname)); + + // This is needed to handle navigating using the back/forward buttons in the browser + useEffect(() => { + // Handle special case when navigating directly to /legacy-libraries or /libraries in `v1 only` mode + // we need to call dispatch to fetch library data + if ( + (isMixedOrV1LibrariesMode(libMode) && pathname.includes('/libraries')) + || pathname.includes('/legacy-libraries') + ) { + dispatch(fetchLibraryData()); + } + setTabKey(initTabKeyState(pathname)); + }, [pathname]); + const { libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe, @@ -138,8 +169,17 @@ const TabsSection = ({ }, [archivedCourses, librariesEnabled, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]); const handleSelectTab = (tab) => { - if (tab === TABS_LIST.legacyLibraries) { + if (tab === TABS_LIST.courses) { + navigate('/home'); + } else if (tab === TABS_LIST.legacyLibraries) { dispatch(fetchLibraryData()); + navigate( + libMode === 'v1 only' + ? '/libraries' + : '/legacy-libraries', + ); + } else if (tab === TABS_LIST.libraries) { + navigate('/libraries'); } else if (tab === TABS_LIST.taxonomies) { navigate('/taxonomies'); } From 515cc71d5acba5cf36ad4a3f19f568c087233122 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Thu, 30 May 2024 14:52:53 +0300 Subject: [PATCH 004/106] feat: LibraryV2 redirect to lib mfe or placeholder --- src/index.jsx | 2 ++ .../tabs-section/LibraryV2Placeholder.tsx | 36 +++++++++++++++++++ src/studio-home/tabs-section/index.jsx | 5 ++- .../tabs-section/libraries-v2-tab/index.tsx | 23 +++++++++--- src/studio-home/tabs-section/messages.js | 8 +++++ 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/studio-home/tabs-section/LibraryV2Placeholder.tsx diff --git a/src/index.jsx b/src/index.jsx index f17c0563ab..8bd2d4ef06 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -23,6 +23,7 @@ import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; import { StudioHome } from './studio-home'; +import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder.tsx'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; @@ -54,6 +55,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> {getConfig().ENABLE_ACCESSIBILITY_PAGE === 'true' && ( diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.tsx b/src/studio-home/tabs-section/LibraryV2Placeholder.tsx new file mode 100644 index 0000000000..ba47ee8899 --- /dev/null +++ b/src/studio-home/tabs-section/LibraryV2Placeholder.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Container } from '@openedx/paragon'; +import { StudioFooter } from '@edx/frontend-component-footer'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import Header from '../../header'; +import SubHeader from '../../generic/sub-header/SubHeader'; +import messages from './messages'; + + +const LibraryV2Placeholder = () => { + const intl = useIntl(); + + return ( + <> +
+ +
+
+
+ +
+
+
+

{intl.formatMessage(messages.libraryV2PlaceholderBody)}

+
+
+
+ + + ); +}; + +export default LibraryV2Placeholder; diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 089f9bc842..aa9d1aa0e2 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -129,7 +129,10 @@ const TabsSection = ({ eventKey={TABS_LIST.libraries} title={intl.formatMessage(messages.librariesTabTitle)} > - + , ); } 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 1e14ffef6c..98888f79a8 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Icon, Row } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -8,7 +9,10 @@ import AlertMessage from '../../../generic/alert-message'; import CardItem from '../../card-item'; import messages from '../messages'; -const LibrariesV2Tab = () => { +const LibrariesV2Tab = ({ + libraryAuthoringMfeUrl, + redirectToLibraryAuthoringMfe, +}) => { const intl = useIntl(); const { data, @@ -24,6 +28,14 @@ const LibrariesV2Tab = () => { ); } + const libURL = (id: string): string => ( + libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe + ? `${libraryAuthoringMfeUrl}library/${id}` + // Redirection to the placeholder is done in the MFE rather than + // through the backend i.e. redirection from cms, because this this will probably change + : `${window.location.origin}/course-authoring/library/${id}` + ); + return ( isError ? ( { /> ) : (
- {data.map(({ org, slug, title }) => ( + {data.map(({ id, org, slug, title }) => ( ))}
@@ -54,5 +65,9 @@ const LibrariesV2Tab = () => { ); }; +LibrariesV2Tab.propTypes = { + libraryAuthoringMfeUrl: PropTypes.string.isRequired, + redirectToLibraryAuthoringMfe: PropTypes.bool.isRequired, +}; export default LibrariesV2Tab; diff --git a/src/studio-home/tabs-section/messages.js b/src/studio-home/tabs-section/messages.js index e1ad0fd44f..0ed614f55a 100644 --- a/src/studio-home/tabs-section/messages.js +++ b/src/studio-home/tabs-section/messages.js @@ -50,6 +50,14 @@ const messages = defineMessages({ defaultMessage: 'Taxonomies', description: 'Title of Taxonomies tab on the home page', }, + libraryV2PlaceholderTitle: { + id: 'course-authoring.studio-home.libraries.placeholder.title', + defaultMessage: 'Library V2 Placeholder', + }, + libraryV2PlaceholderBody: { + id: 'course-authoring.studio-home.libraries.placeholder.body', + defaultMessage: 'This is a placeholder page, as the Library Authoring MFE is not enabled.', + }, }); export default messages; From 9c6e1e6fc7d4206146993edd042ae846b90be217 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Thu, 30 May 2024 18:56:20 +0300 Subject: [PATCH 005/106] feat: Add pagination support for lib v2s --- src/studio-home/data/api.js | 19 ++++++++-- src/studio-home/data/apiHooks.ts | 14 +++++-- .../tabs-section/libraries-v2-tab/index.tsx | 38 ++++++++++++++++--- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index 69e0487fff..2124f6fed7 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -42,10 +42,23 @@ export async function getStudioHomeLibraries() { /** * Get's studio home v2 Libraries. - * @returns {Promise} + * @param {object} customParams - Additional custom paramaters for the API request. + * @param {string} [customParams.type] - (optional) Library type, default `complex` + * @param {number} [customParams.page] - (optional) Page number of results + * @param {number} [customParams.pageSize] - (optional) The number of results on each page, default `50` + * @param {boolean} [customParams.pagination] - (optional) Whether pagination is supported, default `true` + * @returns {Promise} - A Promise that resolves to the response data container the studio home v2 libraries. */ -export async function getStudioHomeLibrariesV2() { - const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`); +export async function getStudioHomeLibrariesV2(customParams) { + // Set default params if not passed in + const customParamsDefaults = { + type: customParams.type || 'complex', + page: customParams.page || 1, + pageSize: customParams.pageSize || 50, + pagination: customParams.pagination !== undefined ? customParams.pagination : true, + }; + const customParamsFormat = snakeCaseObject(customParamsDefaults); + const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`, { params: customParamsFormat }); return camelCaseObject(data); } diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.ts index 7285874c64..79929f040f 100644 --- a/src/studio-home/data/apiHooks.ts +++ b/src/studio-home/data/apiHooks.ts @@ -2,12 +2,20 @@ import { useQuery } from '@tanstack/react-query'; import { getStudioHomeLibrariesV2 } from './api'; + +interface CustomParams { + type?: string, + page?: number, + pageSize?: number, + pagination?: boolean, +} + /** * Builds the query to fetch list of V2 Libraries */ -export const useListStudioHomeV2Libraries = () => ( +export const useListStudioHomeV2Libraries = (customParams: CustomParams) => ( useQuery({ - queryKey: ['listV2Libraries'], - queryFn: () => getStudioHomeLibrariesV2(), + queryKey: ['listV2Libraries', customParams], + queryFn: () => getStudioHomeLibrariesV2(customParams), }) ); 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 98888f79a8..c26527edd8 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Icon, Row } from '@openedx/paragon'; +import { Icon, Row, Pagination } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useListStudioHomeV2Libraries } from '../../data/apiHooks'; @@ -14,11 +14,18 @@ const LibrariesV2Tab = ({ redirectToLibraryAuthoringMfe, }) => { const intl = useIntl(); + + const [currentPage, setCurrentPage] = useState(1); + + const handlePageSelect = (page) => { + setCurrentPage(page); + }; + const { data, isLoading, isError, - } = useListStudioHomeV2Libraries(); + } = useListStudioHomeV2Libraries({page: currentPage}); if (isLoading) { return ( @@ -49,8 +56,19 @@ const LibrariesV2Tab = ({ )} /> ) : ( -
- {data.map(({ id, org, slug, title }) => ( +
+
+ {/* Temporary div to add spacing. This will be replaced with lib search/filters */} +
+

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

+
+ + {data.results.map(({ id, org, slug, title }) => ( ))} + + {data.numPages > 1 && + + }
) ); From da189c1d6d1687b5a6971ab8604fd852481c4a6b Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 16:50:53 +0300 Subject: [PATCH 006/106] fix: Redirect to placeholder create lib in v2/mixed disabled mfe --- src/studio-home/StudioHome.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 2d68af9c25..52be27a60f 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -51,6 +51,7 @@ const StudioHome = ({ intl }) => { studioShortName, studioRequestEmail, libraryAuthoringMfeUrl, + redirectToLibraryAuthoringMfe, } = studioHomeData; function getHeaderButtons() { @@ -82,7 +83,9 @@ const StudioHome = ({ intl }) => { let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; if (isMixedOrV2LibrariesMode(libMode)) { - libraryHref = `${libraryAuthoringMfeUrl}create`; + libraryHref = libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe + ? `${libraryAuthoringMfeUrl}create` + : `${window.location.origin}/course-authoring/library/create`; } headerButtons.push( From 85d9ff215c9d3980a50aacd4c92ce8092bcd0014 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 17:03:01 +0300 Subject: [PATCH 007/106] temp: This removes TS code to get tests to run This commit is temporary as the current frontend build system in tests doesnt support TS syntax. That should be fixed soon, and this commit should be removed. --- src/studio-home/data/apiHooks.ts | 9 +-------- src/studio-home/tabs-section/libraries-v2-tab/index.tsx | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.ts index 79929f040f..ec163e5732 100644 --- a/src/studio-home/data/apiHooks.ts +++ b/src/studio-home/data/apiHooks.ts @@ -3,17 +3,10 @@ import { useQuery } from '@tanstack/react-query'; import { getStudioHomeLibrariesV2 } from './api'; -interface CustomParams { - type?: string, - page?: number, - pageSize?: number, - pagination?: boolean, -} - /** * Builds the query to fetch list of V2 Libraries */ -export const useListStudioHomeV2Libraries = (customParams: CustomParams) => ( +export const useListStudioHomeV2Libraries = (customParams) => ( useQuery({ queryKey: ['listV2Libraries', customParams], queryFn: () => getStudioHomeLibrariesV2(customParams), 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 c26527edd8..a659dcc1fa 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -35,7 +35,7 @@ const LibrariesV2Tab = ({ ); } - const libURL = (id: string): string => ( + const libURL = (id) => ( libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe ? `${libraryAuthoringMfeUrl}library/${id}` // Redirection to the placeholder is done in the MFE rather than From c6b7bf8380f2623f2dc53f55d2be73341c9b8d61 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 18:35:29 +0300 Subject: [PATCH 008/106] test: Update existing tests to support changes --- src/setupTest.js | 1 + src/studio-home/StudioHome.test.jsx | 46 ++++++++++++++----- src/studio-home/__mocks__/studioHomeMock.js | 2 +- .../tabs-section/TabsSection.test.jsx | 39 +++++++++++++--- 4 files changed, 69 insertions(+), 19 deletions(-) diff --git a/src/setupTest.js b/src/setupTest.js index 35b1c9ebe2..f0f7f6a435 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -48,6 +48,7 @@ mergeConfig({ ENABLE_TEAM_TYPE_SETTING: process.env.ENABLE_TEAM_TYPE_SETTING === 'true', ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true', STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null, + LIBRARY_MODE: process.env.LIBRARY_MODE || 'v1 only', }, 'CourseAuthoringConfig'); class ResizeObserver { diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx index 7286acda0f..49ca600e5d 100644 --- a/src/studio-home/StudioHome.test.jsx +++ b/src/studio-home/StudioHome.test.jsx @@ -1,6 +1,8 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { initializeMockApp } from '@edx/frontend-platform'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { initializeMockApp, getConfig, setConfig } 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'; @@ -23,7 +25,6 @@ import { StudioHome } from '.'; let axiosMock; let store; -const mockPathname = '/foo-bar'; const { studioShortName, studioRequestEmail, @@ -34,17 +35,29 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useLocation: () => ({ - pathname: mockPathname, - }), -})); +const queryClient = new QueryClient(); const RootWrapper = () => ( - + - + + + + } + /> + } + /> + } + /> + + + ); @@ -145,7 +158,18 @@ describe('', async () => { }); describe('render new library button', () => { - it('href should include home_library', async () => { + beforeEach(() => { + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'mixed', + }); + }); + + it('href should include home_library when in "v1 only" lib mode', async () => { + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'v1 only', + }); useSelector.mockReturnValue({ ...studioHomeMock, courseCreatorStatus: COURSE_CREATOR_STATES.granted, diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index 5385201e52..4f66cc116f 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -62,7 +62,7 @@ module.exports = { }, ], librariesEnabled: true, - libraryAuthoringMfeUrl: 'http://localhost:3001', + libraryAuthoringMfeUrl: 'http://localhost:3001/', optimizationEnabled: false, redirectToLibraryAuthoringMfe: false, requestCourseCreatorUrl: '/request_course_creator', diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index ea5929aeec..945322dcd5 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -1,4 +1,6 @@ import React from 'react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { getConfig, initializeMockApp, setConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { @@ -34,15 +36,38 @@ const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`; const mockDispatch = jest.fn(); +const queryClient = new QueryClient(); + +const tabSectionComponent = (overrideProps) => ( + +); + const RootWrapper = (overrideProps) => ( - + - + + + + + + + + + ); From 14933d2ce093c4396469c62b9e307275940fd947 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 19:04:16 +0300 Subject: [PATCH 009/106] temp: Rename .tsx -> .jsx & .ts -> .js for tests This is a temporary commit since there are currently no webpack loaders that support tsx files in the test running. This commit should be removed once that is fixed upstream. --- src/index.jsx | 2 +- src/studio-home/data/{apiHooks.ts => apiHooks.js} | 0 .../{LibraryV2Placeholder.tsx => LibraryV2Placeholder.jsx} | 0 src/studio-home/tabs-section/index.jsx | 2 +- .../tabs-section/libraries-v2-tab/{index.tsx => index.jsx} | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename src/studio-home/data/{apiHooks.ts => apiHooks.js} (100%) rename src/studio-home/tabs-section/{LibraryV2Placeholder.tsx => LibraryV2Placeholder.jsx} (100%) rename src/studio-home/tabs-section/libraries-v2-tab/{index.tsx => index.jsx} (100%) diff --git a/src/index.jsx b/src/index.jsx index 8bd2d4ef06..93fe3c3f4c 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -23,7 +23,7 @@ import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; import { StudioHome } from './studio-home'; -import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder.tsx'; +import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.js similarity index 100% rename from src/studio-home/data/apiHooks.ts rename to src/studio-home/data/apiHooks.js diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.tsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx similarity index 100% rename from src/studio-home/tabs-section/LibraryV2Placeholder.tsx rename to src/studio-home/tabs-section/LibraryV2Placeholder.jsx diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index aa9d1aa0e2..703a4e6f04 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -9,7 +9,7 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; import LibrariesTab from './libraries-tab'; -import LibrariesV2Tab from './libraries-v2-tab/index.tsx'; +import LibrariesV2Tab from './libraries-v2-tab/index'; import ArchivedTab from './archived-tab'; import CoursesTab from './courses-tab'; import { RequestStatus } from '../../data/constants'; diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx similarity index 100% rename from src/studio-home/tabs-section/libraries-v2-tab/index.tsx rename to src/studio-home/tabs-section/libraries-v2-tab/index.jsx From 8b9626887eb13d0df0f77d4b13b9e19c6f875ea1 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 19:24:05 +0300 Subject: [PATCH 010/106] fix: Fix lint issues --- src/studio-home/data/apiHooks.js | 5 +- .../tabs-section/LibraryV2Placeholder.jsx | 1 - .../tabs-section/libraries-v2-tab/index.jsx | 47 +++++++++++-------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/studio-home/data/apiHooks.js b/src/studio-home/data/apiHooks.js index ec163e5732..92575bf717 100644 --- a/src/studio-home/data/apiHooks.js +++ b/src/studio-home/data/apiHooks.js @@ -2,13 +2,14 @@ import { useQuery } from '@tanstack/react-query'; import { getStudioHomeLibrariesV2 } from './api'; - /** * Builds the query to fetch list of V2 Libraries */ -export const useListStudioHomeV2Libraries = (customParams) => ( +const useListStudioHomeV2Libraries = (customParams) => ( useQuery({ queryKey: ['listV2Libraries', customParams], queryFn: () => getStudioHomeLibrariesV2(customParams), }) ); + +export default useListStudioHomeV2Libraries; diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx index ba47ee8899..6844515bd9 100644 --- a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx +++ b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx @@ -7,7 +7,6 @@ import Header from '../../header'; import SubHeader from '../../generic/sub-header/SubHeader'; import messages from './messages'; - const LibraryV2Placeholder = () => { const intl = useIntl(); diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx index a659dcc1fa..9060493dd1 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Icon, Row, Pagination } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { useListStudioHomeV2Libraries } from '../../data/apiHooks'; +import useListStudioHomeV2Libraries from '../../data/apiHooks'; import { LoadingSpinner } from '../../../generic/Loading'; import AlertMessage from '../../../generic/alert-message'; import CardItem from '../../card-item'; @@ -25,7 +25,7 @@ const LibrariesV2Tab = ({ data, isLoading, isError, - } = useListStudioHomeV2Libraries({page: currentPage}); + } = useListStudioHomeV2Libraries({ page: currentPage }); if (isLoading) { return ( @@ -68,25 +68,32 @@ const LibrariesV2Tab = ({

- {data.results.map(({ id, org, slug, title }) => ( - - ))} + { + data.results.map(({ + id, org, slug, title, + }) => ( + + )) + } - {data.numPages > 1 && - + { + data.numPages > 1 + && ( + + ) }
) From f8db85358bd87578bfa4d5c5aaf8cb2632d045dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 4 Jun 2024 14:56:13 -0300 Subject: [PATCH 011/106] feat: library home page bare bones --- src/CourseAuthoringPage.jsx | 33 +---- src/header/Header.jsx | 51 +++---- src/index.jsx | 4 +- src/library-authoring/EmptyStates.jsx | 21 +++ .../LibraryAuthoringPage.jsx | 131 ++++++++++++++++++ src/library-authoring/LibraryCollections.jsx | 19 +++ src/library-authoring/LibraryComponents.jsx | 35 +++++ src/library-authoring/LibraryHome.jsx | 61 ++++++++ src/library-authoring/data/api.ts | 12 ++ src/library-authoring/data/apiHook.ts | 56 ++++++++ src/library-authoring/data/types.ts | 17 +++ src/library-authoring/index.ts | 3 + src/library-authoring/messages.ts | 31 +++++ src/search-modal/data/apiHooks.js | 7 +- src/search-modal/index.ts | 3 + 15 files changed, 428 insertions(+), 56 deletions(-) create mode 100644 src/library-authoring/EmptyStates.jsx create mode 100644 src/library-authoring/LibraryAuthoringPage.jsx create mode 100644 src/library-authoring/LibraryCollections.jsx create mode 100644 src/library-authoring/LibraryComponents.jsx create mode 100644 src/library-authoring/LibraryHome.jsx create mode 100644 src/library-authoring/data/api.ts create mode 100644 src/library-authoring/data/apiHook.ts create mode 100644 src/library-authoring/data/types.ts create mode 100644 src/library-authoring/index.ts create mode 100644 src/library-authoring/messages.ts create mode 100644 src/search-modal/index.ts diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx index c4281a8c13..eaa16c49c2 100644 --- a/src/CourseAuthoringPage.jsx +++ b/src/CourseAuthoringPage.jsx @@ -15,29 +15,6 @@ import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors'; import { RequestStatus } from './data/constants'; import Loading from './generic/Loading'; -const AppHeader = ({ - courseNumber, courseOrg, courseTitle, courseId, -}) => ( -
-); - -AppHeader.propTypes = { - courseId: PropTypes.string.isRequired, - courseNumber: PropTypes.string, - courseOrg: PropTypes.string, - courseTitle: PropTypes.string.isRequired, -}; - -AppHeader.defaultProps = { - courseNumber: null, - courseOrg: null, -}; - const CourseAuthoringPage = ({ courseId, children }) => { const dispatch = useDispatch(); @@ -74,11 +51,11 @@ const CourseAuthoringPage = ({ courseId, children }) => { This functionality will be removed in TNL-9591 */} {inProgress ? !isEditor && : (!isEditor && ( - ) )} diff --git a/src/header/Header.jsx b/src/header/Header.jsx index 7cc1adcb08..8e15d32292 100644 --- a/src/header/Header.jsx +++ b/src/header/Header.jsx @@ -6,16 +6,17 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { StudioHeader } from '@edx/frontend-component-header'; import { useToggle } from '@openedx/paragon'; -import SearchModal from '../search-modal/SearchModal'; +import { SearchModal } from '../search-modal'; import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils'; import messages from './messages'; const Header = ({ - courseId, - courseOrg, - courseNumber, - courseTitle, + contentId, + org, + number, + title, isHiddenMainMenu, + isLibrary, }) => { const intl = useIntl(); @@ -23,40 +24,40 @@ const Header = ({ const studioBaseUrl = getConfig().STUDIO_BASE_URL; const meiliSearchEnabled = [true, 'true'].includes(getConfig().MEILISEARCH_ENABLED); - const mainMenuDropdowns = [ + const mainMenuDropdowns = !isLibrary ? [ { id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`, buttonTitle: intl.formatMessage(messages['header.links.content']), - items: getContentMenuItems({ studioBaseUrl, courseId, intl }), + items: getContentMenuItems({ studioBaseUrl, courseId: contentId, intl }), }, { id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`, buttonTitle: intl.formatMessage(messages['header.links.settings']), - items: getSettingMenuItems({ studioBaseUrl, courseId, intl }), + items: getSettingMenuItems({ studioBaseUrl, courseId: contentId, intl }), }, { id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, buttonTitle: intl.formatMessage(messages['header.links.tools']), - items: getToolsMenuItems({ studioBaseUrl, courseId, intl }), + items: getToolsMenuItems({ studioBaseUrl, courseId: contentId, intl }), }, - ]; - const outlineLink = `${studioBaseUrl}/course/${courseId}`; + ] : []; + const outlineLink = !isLibrary ? `${studioBaseUrl}/course/${contentId}` : `${studioBaseUrl}/library/${contentId}`; return ( <> { meiliSearchEnabled && ( )} @@ -65,19 +66,21 @@ const Header = ({ }; Header.propTypes = { - courseId: PropTypes.string, - courseNumber: PropTypes.string, - courseOrg: PropTypes.string, - courseTitle: PropTypes.string, + contentId: PropTypes.string, + number: PropTypes.string, + org: PropTypes.string, + title: PropTypes.string, isHiddenMainMenu: PropTypes.bool, + isLibrary: PropTypes.bool, }; Header.defaultProps = { - courseId: '', - courseNumber: '', - courseOrg: '', - courseTitle: '', + contentId: '', + number: '', + org: '', + title: '', isHiddenMainMenu: false, + isLibrary: false, }; export default Header; diff --git a/src/index.jsx b/src/index.jsx index 93fe3c3f4c..588689aae7 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -19,11 +19,11 @@ import { initializeHotjar } from '@edx/frontend-enterprise-hotjar'; import { logError } from '@edx/frontend-platform/logging'; import messages from './i18n'; +import { LibraryAuthoringPage } from './library-authoring'; import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; import { StudioHome } from './studio-home'; -import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; @@ -55,7 +55,7 @@ const App = () => { } /> } /> } /> - } /> + } /> } /> } /> {getConfig().ENABLE_ACCESSIBILITY_PAGE === 'true' && ( diff --git a/src/library-authoring/EmptyStates.jsx b/src/library-authoring/EmptyStates.jsx new file mode 100644 index 0000000000..6f54dc810b --- /dev/null +++ b/src/library-authoring/EmptyStates.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + Button, Stack, +} from '@openedx/paragon'; +import { Add } from '@openedx/paragon/icons'; + +import messages from './messages'; + +export const NoComponents = () => ( + +
You have not added any content to this library yet.
+ +
+); + +export const NoSearchResults = () => ( +
+ +
+); diff --git a/src/library-authoring/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.jsx new file mode 100644 index 0000000000..9c075ada88 --- /dev/null +++ b/src/library-authoring/LibraryAuthoringPage.jsx @@ -0,0 +1,131 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React, { useEffect } from 'react'; +import { StudioFooter } from '@edx/frontend-component-footer'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Container, Icon, IconButton, SearchField, Tab, Tabs, +} from '@openedx/paragon'; +import { InfoOutline } from '@openedx/paragon/icons'; +import { + Routes, Route, useLocation, useNavigate, useParams, +} from 'react-router-dom'; + +import Loading from '../generic/Loading'; +import SubHeader from '../generic/sub-header/SubHeader'; +import Header from '../header'; +import NotFoundAlert from '../generic/NotFoundAlert'; +import LibraryComponents from './LibraryComponents'; +import LibraryCollections from './LibraryCollections'; +import LibraryHome from './LibraryHome'; +import { useContentLibrary } from './data/apiHook'; +import messages from './messages'; + +const TAB_LIST = { + home: '', + components: 'components', + collections: 'collections', +}; + +const SubHeaderTitle = ({ title }) => ( + <> + {title} + {}} className="mr-2" /> + +); + +/** + * @type {React.FC} + */ +const LibraryAuthoringPage = () => { + const intl = useIntl(); + const location = useLocation(); + const navigate = useNavigate(); + const [tabKey, setTabKey] = React.useState(TAB_LIST.home); + const [searchKeywords, setSearchKeywords] = React.useState(''); + + const { libraryId } = useParams(); + + const { data: libraryData, isLoading } = useContentLibrary(libraryId); + + useEffect(() => { + const currentPath = location.pathname.split('/').pop(); + if (currentPath && Object.values(TAB_LIST).includes(currentPath)) { + setTabKey(currentPath); + } else { + setTabKey(TAB_LIST.home); + } + }, [location]); + + if (isLoading) { + return ; + } + + if (!libraryId || !libraryData) { + return ; + } + + /** Handle tab change + * @param {string} key + */ + const handleTabChange = (key) => { + setTabKey(key); + navigate(key); + }; + + return ( + <> +
+ + } + subtitle={intl.formatMessage(messages.headingSubtitle)} + /> + setSearchKeywords(value)} + onChange={(value) => setSearchKeywords(value)} + className="w-50" + /> + + + + + + + } + /> + } + /> + } + /> + } + /> + + + + + ); +}; + +export default LibraryAuthoringPage; diff --git a/src/library-authoring/LibraryCollections.jsx b/src/library-authoring/LibraryCollections.jsx new file mode 100644 index 0000000000..5292b10f40 --- /dev/null +++ b/src/library-authoring/LibraryCollections.jsx @@ -0,0 +1,19 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; + +/** + * @type {React.FC} + */ +const LibraryCollections = () => ( +
+ +
+); + +export default LibraryCollections; diff --git a/src/library-authoring/LibraryComponents.jsx b/src/library-authoring/LibraryComponents.jsx new file mode 100644 index 0000000000..a48fbfe645 --- /dev/null +++ b/src/library-authoring/LibraryComponents.jsx @@ -0,0 +1,35 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import { NoComponents, NoSearchResults } from './EmptyStates'; +import { useLibraryComponentCount } from './data/apiHook'; +import messages from './messages'; + +/** + * @type {React.FC<{ + * libraryId: string, + * filter: { + * searchKeywords: string, + * }, + * }>} + */ +const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { + const { componentCount, collectionCount } = useLibraryComponentCount(libraryId, searchKeywords); + + if (componentCount === 0) { + return searchKeywords === '' ? : ; + } + + return ( +
+ +
+ ); +}; + +export default LibraryComponents; diff --git a/src/library-authoring/LibraryHome.jsx b/src/library-authoring/LibraryHome.jsx new file mode 100644 index 0000000000..e2c16862ca --- /dev/null +++ b/src/library-authoring/LibraryHome.jsx @@ -0,0 +1,61 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React from 'react'; +import { + Card, Stack, +} from '@openedx/paragon'; + +import { NoComponents, NoSearchResults } from './EmptyStates'; +import LibraryCollections from './LibraryCollections'; +import LibraryComponents from './LibraryComponents'; +import { useLibraryComponentCount } from './data/apiHook'; + +/** + * @type {React.FC<{ + * title: string, + * children: React.ReactNode, + * }>} + */ +const Section = ({ title, children }) => ( + + + + {children} + + +); + +/** + * @type {React.FC<{ + * libraryId: string, + * filter: { + * searchKeywords: string, + * }, + * }>} + */ +const LibraryHome = ({ libraryId, filter }) => { + const { searchKeywords } = filter; + const { componentCount, collectionCount } = useLibraryComponentCount(libraryId, searchKeywords); + + if (componentCount === 0) { + return searchKeywords === '' ? : ; + } + + return ( + +
+ Recently modified components and collections will be displayed here. +
+
+ +
+
+ +
+
+ ); +}; + +export default LibraryHome; diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts new file mode 100644 index 0000000000..ff0dd3dec5 --- /dev/null +++ b/src/library-authoring/data/api.ts @@ -0,0 +1,12 @@ +// @ts-check +import type { ContentLibrary } from './types'; +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = (): string => getConfig().STUDIO_BASE_URL; +const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; + +export async function getContentLibrary(libraryId: string): Promise { + const { data } = await getAuthenticatedHttpClient().get(getContentLibraryApiUrl(libraryId)); + return camelCaseObject(data); +} diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.ts new file mode 100644 index 0000000000..2b1516ee22 --- /dev/null +++ b/src/library-authoring/data/apiHook.ts @@ -0,0 +1,56 @@ +// @ts-check +import React, { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { MeiliSearch } from 'meilisearch'; + +import { useContentSearchConnection, useContentSearchResults } from '../../search-modal'; +import { getContentLibrary } from './api'; + +/** + * Hook to fetch a content library by its ID. + */ +export const useContentLibrary = (libraryId?: string) => { + if (!libraryId) { + return { + data: undefined, + error: 'No library ID provided', + } + } + + return useQuery({ + queryKey: ['contentLibrary', libraryId], + queryFn: () => getContentLibrary(libraryId), + }); +}; + + +export const useLibraryComponentCount = (libraryId: string, searchKeywords: string) => { + // Meilisearch code to get Collection and Component counts + const { data: connectionDetails } = useContentSearchConnection(); + + const indexName = connectionDetails?.indexName; + const client = React.useMemo(() => { + if (connectionDetails?.apiKey === undefined || connectionDetails?.url === undefined) { + return undefined; + } + return new MeiliSearch({ host: connectionDetails.url, apiKey: connectionDetails.apiKey }); + }, [connectionDetails?.apiKey, connectionDetails?.url]); + + const libFilter = `context_key = "${libraryId}"`; + + const { totalHits: componentCount } = useContentSearchResults({ + client, + indexName, + searchKeywords, + extraFilter: [libFilter], // ToDo: Add filter for components when collection is implemented + }); + + const collectionCount = 0; // ToDo: Implement collections count + + return { + componentCount, + collectionCount, + }; +} + + diff --git a/src/library-authoring/data/types.ts b/src/library-authoring/data/types.ts new file mode 100644 index 0000000000..1af41a8b1f --- /dev/null +++ b/src/library-authoring/data/types.ts @@ -0,0 +1,17 @@ +export type ContentLibrary = { + id: string; + type: string; + org: string; + slug: string; + title: string; + description: string; + numBlocks: number; + version: number; + lastPublished: Date | null; + allowLti: boolean; + allowPublicLearning: boolean; + allowPublicRead: boolean; + hasUnpublishedChanges: boolean; + hasUnpublishedDeletes: boolean; + license: string; +} diff --git a/src/library-authoring/index.ts b/src/library-authoring/index.ts new file mode 100644 index 0000000000..05cd9d1e61 --- /dev/null +++ b/src/library-authoring/index.ts @@ -0,0 +1,3 @@ +// @ts-check +// eslint-disable-next-line import/prefer-default-export +export { default as LibraryAuthoringPage } from './LibraryAuthoringPage'; diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts new file mode 100644 index 0000000000..1a48fdeaf4 --- /dev/null +++ b/src/library-authoring/messages.ts @@ -0,0 +1,31 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + headingSubtitle: { + id: 'course-authoring.library-authoring.heading-subtitle', + defaultMessage: 'Content library', + description: 'The page heading for the library page.', + }, + searchPlaceholder: { + id: 'course-authoring.library-authoring.search', + defaultMessage: 'Search...', + description: 'Placeholder for search field', + }, + noSearchResults: { + id: 'course-authoring.library-authoring.no-search-results', + defaultMessage: 'No matching components found in this library.', + description: 'Message displayed when no search results are found', + }, + componentsTempPlaceholder: { + id: 'course-authoring.library-authoring.components-temp-placeholder', + defaultMessage: 'There are {componentCount} components in this library', + description: 'Temp placeholder for the component container. This will be replaced with the actual component list.', + }, + collectionsTempPlaceholder: { + id: 'course-authoring.library-authoring.collections-temp-placeholder', + defaultMessage: 'Coming soon!', + description: 'Temp placeholder for the collections container. This will be replaced with the actual collection list.', + }, +}); + +export default messages; diff --git a/src/search-modal/data/apiHooks.js b/src/search-modal/data/apiHooks.js index 02488635da..59a07c425a 100644 --- a/src/search-modal/data/apiHooks.js +++ b/src/search-modal/data/apiHooks.js @@ -34,8 +34,8 @@ export const useContentSearchConnection = () => ( * @param {string} [context.indexName] Which search index contains the content data * @param {import('meilisearch').Filter} [context.extraFilter] Other filters to apply to the search, e.g. course ID * @param {string} context.searchKeywords The keywords that the user is searching for, if any - * @param {string[]} context.blockTypesFilter Only search for these block types (e.g. ["html", "problem"]) - * @param {string[]} context.tagsFilter Required tags (all must match), e.g. ["Difficulty > Hard", "Subject > Math"] + * @param {string[]} [context.blockTypesFilter] Only search for these block types (e.g. ["html", "problem"]) + * @param {string[]} [context.tagsFilter] Required tags (all must match), e.g. ["Difficulty > Hard", "Subject > Math"] */ export const useContentSearchResults = ({ client, @@ -45,6 +45,9 @@ export const useContentSearchResults = ({ blockTypesFilter, tagsFilter, }) => { + blockTypesFilter ??= []; // eslint-disable-line no-param-reassign -- default value for optional parameter + tagsFilter ??= []; // eslint-disable-line no-param-reassign -- Default value for optional parameter + const query = useInfiniteQuery({ enabled: client !== undefined && indexName !== undefined, queryKey: [ diff --git a/src/search-modal/index.ts b/src/search-modal/index.ts new file mode 100644 index 0000000000..190635618d --- /dev/null +++ b/src/search-modal/index.ts @@ -0,0 +1,3 @@ +// @ts-check +export { default as SearchModal } from './SearchModal'; +export { useContentSearchConnection, useContentSearchResults } from './data/apiHooks'; From 7a8488d8101509bd797e19db44ff220376dacddd Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Tue, 4 Jun 2024 22:58:51 +0300 Subject: [PATCH 012/106] test: Add tests for new functionality --- src/studio-home/__mocks__/index.js | 2 +- .../listStudioHomeV2LibrariesMock.js | 44 ++++ src/studio-home/data/api.test.js | 24 ++- .../factories/mockApiResponses.jsx | 47 +++- .../tabs-section/LibraryV2Placeholder.jsx | 1 + .../tabs-section/TabsSection.test.jsx | 201 +++++++++++++++++- 6 files changed, 309 insertions(+), 10 deletions(-) create mode 100644 src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js diff --git a/src/studio-home/__mocks__/index.js b/src/studio-home/__mocks__/index.js index 92461eb0bb..af2a85b390 100644 --- a/src/studio-home/__mocks__/index.js +++ b/src/studio-home/__mocks__/index.js @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export { default as studioHomeMock } from './studioHomeMock'; +export { default as listStudioHomeV2LibrariesMock } from './listStudioHomeV2LibrariesMock'; diff --git a/src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js b/src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js new file mode 100644 index 0000000000..02257a9744 --- /dev/null +++ b/src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js @@ -0,0 +1,44 @@ +module.exports = { + next: null, + previous: null, + count: 2, + num_pages: 1, + current_page: 1, + start: 0, + results: [ + { + id: 'lib:SampleTaxonomyOrg1:AL1', + type: 'complex', + org: 'SampleTaxonomyOrg1', + slug: 'AL1', + title: 'Another Library 2', + description: '', + num_blocks: 0, + version: 0, + last_published: null, + allow_lti: false, + allow_public_learning: false, + allow_public_read: false, + has_unpublished_changes: false, + has_unpublished_deletes: false, + license: '', + }, + { + id: 'lib:SampleTaxonomyOrg1:TL1', + type: 'complex', + org: 'SampleTaxonomyOrg1', + slug: 'TL1', + title: 'Test Library 1', + description: '', + num_blocks: 0, + version: 0, + last_published: null, + allow_lti: false, + allow_public_learning: false, + allow_public_read: false, + has_unpublished_changes: false, + has_unpublished_deletes: false, + license: '', + }, + ], +}; diff --git a/src/studio-home/data/api.test.js b/src/studio-home/data/api.test.js index 593a2730de..66f6ee279f 100644 --- a/src/studio-home/data/api.test.js +++ b/src/studio-home/data/api.test.js @@ -13,8 +13,14 @@ import { getStudioHomeCourses, getStudioHomeCoursesV2, getStudioHomeLibraries, + getStudioHomeLibrariesV2, } from './api'; -import { generateGetStudioCoursesApiResponse, generateGetStudioHomeDataApiResponse, generateGetStuioHomeLibrariesApiResponse } from '../factories/mockApiResponses'; +import { + generateGetStudioCoursesApiResponse, + generateGetStudioHomeDataApiResponse, + generateGetStudioHomeLibrariesApiResponse, + generateGetStudioHomeLibrariesV2ApiResponse, +} from '../factories/mockApiResponses'; let axiosMock; @@ -64,11 +70,21 @@ describe('studio-home api calls', () => { expect(result).toEqual(expected); }); - it('should get studio libraries data', async () => { + it('should get studio v1 libraries data', async () => { const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`; - axiosMock.onGet(apiLink).reply(200, generateGetStuioHomeLibrariesApiResponse()); + axiosMock.onGet(apiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); const result = await getStudioHomeLibraries(); - const expected = generateGetStuioHomeLibrariesApiResponse(); + const expected = generateGetStudioHomeLibrariesApiResponse(); + + expect(axiosMock.history.get[0].url).toEqual(apiLink); + expect(result).toEqual(expected); + }); + + it('should get studio v2 libraries data', async () => { + const apiLink = `${getApiBaseUrl()}/api/libraries/v2/`; + axiosMock.onGet(apiLink).reply(200, generateGetStudioHomeLibrariesV2ApiResponse()); + const result = await getStudioHomeLibrariesV2({}); + const expected = generateGetStudioHomeLibrariesV2ApiResponse(); expect(axiosMock.history.get[0].url).toEqual(apiLink); expect(result).toEqual(expected); diff --git a/src/studio-home/factories/mockApiResponses.jsx b/src/studio-home/factories/mockApiResponses.jsx index 30615ba8d5..5d75f9f592 100644 --- a/src/studio-home/factories/mockApiResponses.jsx +++ b/src/studio-home/factories/mockApiResponses.jsx @@ -112,7 +112,7 @@ export const generateGetStudioCoursesApiResponseV2 = () => ({ }, }); -export const generateGetStuioHomeLibrariesApiResponse = () => ({ +export const generateGetStudioHomeLibrariesApiResponse = () => ({ libraries: [ { displayName: 'MBA', @@ -125,6 +125,51 @@ export const generateGetStuioHomeLibrariesApiResponse = () => ({ ], }); +export const generateGetStudioHomeLibrariesV2ApiResponse = () => ({ + next: null, + previous: null, + count: 2, + numPages: 1, + currentPage: 1, + start: 0, + results: [ + { + id: 'lib:SampleTaxonomyOrg1:AL1', + type: 'complex', + org: 'SampleTaxonomyOrg1', + slug: 'AL1', + title: 'Another Library 2', + description: '', + numBlocks: 0, + version: 0, + lastPublished: null, + allowLti: false, + allowPublicLearning: false, + allowpublicRead: false, + hasUnpublishedChanges: false, + hasUnpublishedDeletes: false, + license: '', + }, + { + id: 'lib:SampleTaxonomyOrg1:TL1', + type: 'complex', + org: 'SampleTaxonomyOrg1', + slug: 'TL1', + title: 'Test Library 1', + description: '', + numBlocks: 0, + version: 0, + lastPublished: null, + allowLti: false, + allowPublicLearning: false, + allowPublicRead: false, + hasUnpublishedChanges: false, + hasUnpublishedDeletes: false, + license: '', + }, + ], +}); + export const generateNewVideoApiResponse = () => ({ files: [{ edx_video_id: 'mOckID4', diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx index 6844515bd9..6b13853a2c 100644 --- a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx +++ b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx @@ -7,6 +7,7 @@ import Header from '../../header'; import SubHeader from '../../generic/sub-header/SubHeader'; import messages from './messages'; +/* istanbul ignore next */ const LibraryV2Placeholder = () => { const intl = useIntl(); diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index 945322dcd5..54741ebbb1 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -11,7 +11,7 @@ import { AppProvider } from '@edx/frontend-platform/react'; import MockAdapter from 'axios-mock-adapter'; import initializeStore from '../../store'; -import { studioHomeMock } from '../__mocks__'; +import { studioHomeMock, listStudioHomeV2LibrariesMock } from '../__mocks__'; import messages from '../messages'; import tabMessages from './messages'; import TabsSection from '.'; @@ -20,12 +20,32 @@ import { generateGetStudioHomeDataApiResponse, generateGetStudioCoursesApiResponse, generateGetStudioCoursesApiResponseV2, - generateGetStuioHomeLibrariesApiResponse, + generateGetStudioHomeLibrariesApiResponse, } from '../factories/mockApiResponses'; import { getApiBaseUrl, getStudioHomeApiUrl } from '../data/api'; import { executeThunk } from '../../utils'; import { fetchLibraryData, fetchStudioHomeData } from '../data/thunks'; +import useListStudioHomeV2Libraries from '../data/apiHooks'; + +jest.mock('../data/apiHooks', () => ({ + // Since only useListStudioHomeV2Libraries is exported as default + __esModule: true, + default: jest.fn(() => ({ + data: { + next: null, + previous: null, + count: 2, + num_pages: 1, + current_page: 1, + start: 0, + results: [], + }, + isLoading: false, + isError: false, + })), +})); + const { studioShortName } = studioHomeMock; let axiosMock; @@ -84,6 +104,10 @@ describe('', () => { }); store = initializeStore(initialState); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'mixed', + }); }); it('should render all tabs correctly', async () => { @@ -105,11 +129,47 @@ describe('', () => { expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument(); 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); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + + 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); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + + expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).not.toBeInTheDocument(); + }); + describe('course tab', () => { it('should render specific course details', async () => { render(); @@ -181,6 +241,46 @@ describe('', () => { const pagination = screen.queryByRole('navigation'); expect(pagination).not.toBeInTheDocument(); }); + + it('should set the url path to "/home" when switching away then back to courses tab', async () => { + const data = generateGetStudioCoursesApiResponseV2(); + data.results.courses = []; + render(); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + axiosMock.onGet(courseApiLinkV2).reply(200, data); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + // confirm the url path is initially /home + waitFor(() => { + expect(window.location.href).toContain('/home'); + }); + + // switch to libraries tab + axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); + await executeThunk(fetchLibraryData(), store.dispatch); + const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); + await act(async () => { + fireEvent.click(librariesTab); + }); + + // confirm that the url path has changed + expect(librariesTab).toHaveClass('active'); + waitFor(() => { + expect(window.location.href).toContain('/legacy-libraries'); + }); + + // switch back to courses tab + const coursesTab = screen.getByText(tabMessages.coursesTabTitle.defaultMessage); + await act(async () => { + fireEvent.click(coursesTab); + }); + + // confirm that the url path is /home + expect(coursesTab).toHaveClass('active'); + waitFor(() => { + expect(window.location.href).toContain('/home'); + }); + }); }); describe('taxonomies tab', () => { @@ -247,6 +347,8 @@ describe('', () => { expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument(); expect(screen.queryByText(tabMessages.archivedTabTitle.defaultMessage)).toBeNull(); @@ -254,10 +356,10 @@ describe('', () => { }); describe('library tab', () => { - it('should switch to Libraries tab and render specific library details', async () => { + it('should switch to Legacy Libraries tab and render specific v1 library details', async () => { render(); axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); - axiosMock.onGet(libraryApiLink).reply(200, generateGetStuioHomeLibrariesApiResponse()); + axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); await executeThunk(fetchStudioHomeData(), store.dispatch); await executeThunk(fetchLibraryData(), store.dispatch); @@ -273,6 +375,97 @@ describe('', () => { expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible(); }); + it('should switch to Libraries tab and render specific v2 library details', async () => { + useListStudioHomeV2Libraries.mockReturnValue({ + data: listStudioHomeV2LibrariesMock, + isLoading: false, + isError: false, + }); + + render(); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + await act(async () => { + fireEvent.click(librariesTab); + }); + + expect(librariesTab).toHaveClass('active'); + + expect(screen.getByText('Showing 2 of 2')).toBeVisible(); + + expect(screen.getByText(listStudioHomeV2LibrariesMock.results[0].title)).toBeVisible(); + expect(screen.getByText( + `${listStudioHomeV2LibrariesMock.results[0].org} / ${listStudioHomeV2LibrariesMock.results[0].slug}`, + )).toBeVisible(); + + expect(screen.getByText(listStudioHomeV2LibrariesMock.results[1].title)).toBeVisible(); + expect(screen.getByText( + `${listStudioHomeV2LibrariesMock.results[1].org} / ${listStudioHomeV2LibrariesMock.results[1].slug}`, + )).toBeVisible(); + }); + + it('should switch to Libraries tab and render specific v1 library details ("v1 only" mode)', async () => { + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'v1 only', + }); + + render(); + 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); + await act(async () => { + fireEvent.click(librariesTab); + }); + + expect(librariesTab).toHaveClass('active'); + + expect(screen.getByText(studioHomeMock.libraries[0].displayName)).toBeVisible(); + + expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible(); + }); + + it('should switch to Libraries tab and render specific v2 library details ("v2 only" mode)', async () => { + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'v2 only', + }); + + useListStudioHomeV2Libraries.mockReturnValue({ + data: listStudioHomeV2LibrariesMock, + isLoading: false, + isError: false, + }); + + render(); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + await act(async () => { + fireEvent.click(librariesTab); + }); + + expect(librariesTab).toHaveClass('active'); + + expect(screen.getByText('Showing 2 of 2')).toBeVisible(); + + expect(screen.getByText(listStudioHomeV2LibrariesMock.results[0].title)).toBeVisible(); + expect(screen.getByText( + `${listStudioHomeV2LibrariesMock.results[0].org} / ${listStudioHomeV2LibrariesMock.results[0].slug}`, + )).toBeVisible(); + + expect(screen.getByText(listStudioHomeV2LibrariesMock.results[1].title)).toBeVisible(); + expect(screen.getByText( + `${listStudioHomeV2LibrariesMock.results[1].org} / ${listStudioHomeV2LibrariesMock.results[1].slug}`, + )).toBeVisible(); + }); + it('should hide Libraries tab when libraries are disabled', async () => { const data = generateGetStudioHomeDataApiResponse(); data.librariesEnabled = false; From 7842ce029fa770c5ae4e9f6021a81cc1b8c4c9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 5 Jun 2024 10:57:46 -0300 Subject: [PATCH 013/106] fix: update search modal for new library urls --- src/header/Header.jsx | 2 +- src/search-modal/SearchResult.jsx | 26 +++++++++++++------------- src/search-modal/SearchUI.test.jsx | 18 +++++++++++------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/header/Header.jsx b/src/header/Header.jsx index 8e15d32292..6865a3db96 100644 --- a/src/header/Header.jsx +++ b/src/header/Header.jsx @@ -52,7 +52,7 @@ const Header = ({ isHiddenMainMenu={isHiddenMainMenu} mainMenuDropdowns={mainMenuDropdowns} outlineLink={outlineLink} - searchButtonAction={meiliSearchEnabled && !isLibrary ? openSearchModal : undefined} + searchButtonAction={meiliSearchEnabled ? openSearchModal : undefined} /> { meiliSearchEnabled && ( { const { closeSearchModal } = useSearchContext(); const { libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe } = useSelector(getStudioHomeData); - const { usageKey } = hit; - - const noRedirectUrl = usageKey.startsWith('lb:') && !redirectToLibraryAuthoringMfe; - /** * Returns the URL for the context of the hit */ @@ -149,10 +144,16 @@ const SearchResult = ({ hit }) => { return `/${urlSuffix}`; } - if (usageKey.startsWith('lb:')) { - if (redirectToLibraryAuthoringMfe) { - return getLibraryHitUrl(hit, libraryAuthoringMfeUrl); + if (contextKey.startsWith('lib:')) { + const urlSuffix = getLibraryComponentUrlSuffix(hit); + if (libraryAuthoringMfeUrl) { + return `${libraryAuthoringMfeUrl}${urlSuffix}`; } + + if (newWindow) { + return `${getPath(getConfig().PUBLIC_PATH)}${urlSuffix}`; + } + return `/${urlSuffix}`; } // No context URL for this hit (e.g. a library without library authoring mfe) @@ -206,12 +207,12 @@ const SearchResult = ({ hit }) => { return ( @@ -230,7 +231,6 @@ const SearchResult = ({ hit }) => { diff --git a/src/search-modal/SearchUI.test.jsx b/src/search-modal/SearchUI.test.jsx index c653807dfb..0c46908c08 100644 --- a/src/search-modal/SearchUI.test.jsx +++ b/src/search-modal/SearchUI.test.jsx @@ -344,9 +344,10 @@ describe('', () => { window.location = location; }); - test('click lib component result doesnt navigates to the context withou libraryAuthoringMfe', async () => { + test('click lib component result navigates to course-authoring/library without libraryAuthoringMfe', async () => { const data = generateGetStudioHomeDataApiResponse(); data.redirectToLibraryAuthoringMfe = false; + data.libraryAuthoringMfeUrl = ''; axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data); await executeThunk(fetchStudioHomeData(), store.dispatch); @@ -356,18 +357,21 @@ describe('', () => { const resultItem = await findByRole('button', { name: /Library Content/ }); // Clicking the "Open in new window" button should open the result in a new window: - const { open, location } = window; + const { open } = window; window.open = jest.fn(); fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' })); - expect(window.open).not.toHaveBeenCalled(); + + expect(window.open).toHaveBeenCalledWith( + '/library/lib:org1:libafter1', + '_blank', + ); window.open = open; - // @ts-ignore - window.location = { href: '' }; // Clicking in the result should navigate to the result's URL: fireEvent.click(resultItem); - expect(window.location.href === location.href); - window.location = location; + expect(mockNavigate).toHaveBeenCalledWith( + '/library/lib:org1:libafter1', + ); }); }); From 462cda93f674abe8941f66c3ab1ef7f6a26d5e9e Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 27 May 2024 20:13:28 +0300 Subject: [PATCH 014/106] feat: Add lib v2/legacy tabs in studio home When lib mode is set to "mixed", both "Libraries" and "Legacy Libraries" tabs are show in the Studio Home. When "Libraries" is clicked, v2 libraries are fetched, when "Legacy Libraries" is clicked, v1 libraries are fetched. When lib mode is set to "v1 only" or "v2 only", only one tab "Libraries" is show and only the respective libraries are fetched when the tab is clicked. --- src/studio-home/StudioHome.jsx | 9 ++- src/studio-home/data/api.js | 5 ++ src/studio-home/data/apiHooks.ts | 13 +++++ .../tabs-section/TabsSection.test.jsx | 12 ++-- src/studio-home/tabs-section/index.jsx | 47 ++++++++++----- .../tabs-section/libraries-v2-tab/index.tsx | 58 +++++++++++++++++++ src/studio-home/tabs-section/messages.js | 4 ++ src/studio-home/tabs-section/utils.js | 10 +++- 8 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 src/studio-home/data/apiHooks.ts create mode 100644 src/studio-home/tabs-section/libraries-v2-tab/index.tsx diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 8348aaca34..acc5cd1174 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -18,6 +18,7 @@ import Header from '../header'; import SubHeader from '../generic/sub-header/SubHeader'; import HomeSidebar from './home-sidebar'; import TabsSection from './tabs-section'; +import { isMixedOrV2LibrariesMode } from './tabs-section/utils'; import OrganizationSection from './organization-section'; import VerifyEmailLayout from './verify-email-layout'; import CreateNewCourseForm from './create-new-course-form'; @@ -43,12 +44,14 @@ const StudioHome = ({ intl }) => { dispatch, } = useStudioHome(isPaginationCoursesEnabled); + // TODO: this should be a flag in the backend + const LIB_MODE = 'mixed'; + const { userIsActive, studioShortName, studioRequestEmail, libraryAuthoringMfeUrl, - redirectToLibraryAuthoringMfe, } = studioHomeData; function getHeaderButtons() { @@ -79,8 +82,8 @@ const StudioHome = ({ intl }) => { } let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; - if (redirectToLibraryAuthoringMfe) { - libraryHref = `${libraryAuthoringMfeUrl}/create`; + if (isMixedOrV2LibrariesMode(LIB_MODE)) { + libraryHref = `${libraryAuthoringMfeUrl}create`; } headerButtons.push( diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index 1fefe2981a..0c09601d11 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -40,6 +40,11 @@ export async function getStudioHomeLibraries() { return camelCaseObject(data); } +export async function getStudioHomeLibrariesV2() { + const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`); + return camelCaseObject(data); +} + /** * Handle course notification requests. * @param {string} url diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.ts new file mode 100644 index 0000000000..7285874c64 --- /dev/null +++ b/src/studio-home/data/apiHooks.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getStudioHomeLibrariesV2 } from './api'; + +/** + * Builds the query to fetch list of V2 Libraries + */ +export const useListStudioHomeV2Libraries = () => ( + useQuery({ + queryKey: ['listV2Libraries'], + queryFn: () => getStudioHomeLibrariesV2(), + }) +); diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index fdc955d8df..ea5929aeec 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -80,7 +80,7 @@ describe('', () => { expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); - expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument(); expect(screen.getByText(tabMessages.archivedTabTitle.defaultMessage)).toBeInTheDocument(); }); @@ -222,7 +222,7 @@ describe('', () => { expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); - expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument(); expect(screen.queryByText(tabMessages.archivedTabTitle.defaultMessage)).toBeNull(); }); @@ -236,7 +236,7 @@ describe('', () => { await executeThunk(fetchStudioHomeData(), store.dispatch); await executeThunk(fetchLibraryData(), store.dispatch); - const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); await act(async () => { fireEvent.click(librariesTab); }); @@ -257,7 +257,7 @@ describe('', () => { await executeThunk(fetchStudioHomeData(), store.dispatch); expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); - expect(screen.queryByText(tabMessages.librariesTabTitle.defaultMessage)).toBeNull(); + expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeNull(); }); it('should redirect to library authoring mfe', async () => { @@ -268,7 +268,7 @@ describe('', () => { axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data); await executeThunk(fetchStudioHomeData(), store.dispatch); - const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); fireEvent.click(librariesTab); waitFor(() => { @@ -283,7 +283,7 @@ describe('', () => { await executeThunk(fetchStudioHomeData(), store.dispatch); await executeThunk(fetchLibraryData(), store.dispatch); - const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); await act(async () => { fireEvent.click(librariesTab); }); diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 1409766c47..789bb2bea1 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -9,10 +9,12 @@ import { useNavigate } from 'react-router-dom'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; import LibrariesTab from './libraries-tab'; +import LibrariesV2Tab from './libraries-v2-tab/index.tsx'; 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 = ({ intl, @@ -23,9 +25,14 @@ const TabsSection = ({ isPaginationCoursesEnabled, }) => { const navigate = useNavigate(); + + // TODO: this should be a flag in the backend + const LIB_MODE = 'mixed'; + const TABS_LIST = { courses: 'courses', libraries: 'libraries', + legacyLibraries: 'legacyLibraries', archived: 'archived', taxonomies: 'taxonomies', }; @@ -87,21 +94,37 @@ const TabsSection = ({ } if (librariesEnabled) { - tabs.push( - - {!redirectToLibraryAuthoringMfe && ( + if (isMixedOrV2LibrariesMode(LIB_MODE)) { + tabs.push( + + + , + ); + } + + if (isMixedOrV1LibrariesMode(LIB_MODE)) { + tabs.push( + - )} - , - ); + , + ); + } } if (getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true') { @@ -118,9 +141,7 @@ const TabsSection = ({ }, [archivedCourses, librariesEnabled, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]); const handleSelectTab = (tab) => { - if (tab === TABS_LIST.libraries && redirectToLibraryAuthoringMfe) { - window.location.assign(libraryAuthoringMfeUrl); - } else if (tab === TABS_LIST.libraries && !redirectToLibraryAuthoringMfe) { + if (tab === TABS_LIST.legacyLibraries) { dispatch(fetchLibraryData()); } else if (tab === TABS_LIST.taxonomies) { navigate('/taxonomies'); diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx new file mode 100644 index 0000000000..1e14ffef6c --- /dev/null +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Icon, Row } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { useListStudioHomeV2Libraries } from '../../data/apiHooks'; +import { LoadingSpinner } from '../../../generic/Loading'; +import AlertMessage from '../../../generic/alert-message'; +import CardItem from '../../card-item'; +import messages from '../messages'; + +const LibrariesV2Tab = () => { + const intl = useIntl(); + const { + data, + isLoading, + isError, + } = useListStudioHomeV2Libraries(); + + if (isLoading) { + return ( + + + + ); + } + + return ( + isError ? ( + + + {intl.formatMessage(messages.librariesTabErrorMessage)} + + )} + /> + ) : ( +
+ {data.map(({ org, slug, title }) => ( + + ))} +
+ ) + ); +}; + + +export default LibrariesV2Tab; diff --git a/src/studio-home/tabs-section/messages.js b/src/studio-home/tabs-section/messages.js index 5ae2e139b2..e1ad0fd44f 100644 --- a/src/studio-home/tabs-section/messages.js +++ b/src/studio-home/tabs-section/messages.js @@ -21,6 +21,10 @@ const messages = defineMessages({ id: 'course-authoring.studio-home.libraries.tab.title', defaultMessage: 'Libraries', }, + legacyLibrariesTabTitle: { + id: 'course-authoring.studio-home.legacy.libraries.tab.title', + defaultMessage: 'Legacy Libraries', + }, archivedTabTitle: { id: 'course-authoring.studio-home.archived.tab.title', defaultMessage: 'Archived courses', diff --git a/src/studio-home/tabs-section/utils.js b/src/studio-home/tabs-section/utils.js index 5d3822b8ed..e7dea1ad69 100644 --- a/src/studio-home/tabs-section/utils.js +++ b/src/studio-home/tabs-section/utils.js @@ -8,5 +8,11 @@ const sortAlphabeticallyArray = (arr) => [...arr] .sort((firstArrayData, secondArrayData) => firstArrayData .displayName.localeCompare(secondArrayData.displayName)); -// eslint-disable-next-line import/prefer-default-export -export { sortAlphabeticallyArray }; +const isMixedOrV1LibrariesMode = (libMode) => ['mixed', 'v1 only'].includes(libMode); +const isMixedOrV2LibrariesMode = (libMode) => ['mixed', 'v2 only'].includes(libMode); + +export { + sortAlphabeticallyArray, + isMixedOrV1LibrariesMode, + isMixedOrV2LibrariesMode, +}; From be8b2f4ce476771dcfa936f8984ed38d7ccb47a5 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Tue, 28 May 2024 17:31:08 +0300 Subject: [PATCH 015/106] feat: Add `LIBRARY_MODE` config variable This is to switch between different library modes. --- .env | 1 + .env.development | 1 + .env.test | 1 + README.rst | 16 ++++++++++++++++ .../feature-v2-and-legacy-libs.png | Bin 0 -> 246316 bytes src/index.jsx | 1 + src/studio-home/StudioHome.jsx | 5 ++--- src/studio-home/tabs-section/index.jsx | 11 ++++------- 8 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 docs/readme-images/feature-v2-and-legacy-libs.png diff --git a/.env b/.env index ce17454708..4235461134 100644 --- a/.env +++ b/.env @@ -43,3 +43,4 @@ AI_TRANSLATIONS_BASE_URL='' ENABLE_HOME_PAGE_COURSE_API_V2=false ENABLE_CHECKLIST_QUALITY='' ENABLE_GRADING_METHOD_IN_PROBLEMS=false +LIBRARY_MODE="v1 only" diff --git a/.env.development b/.env.development index 983ce9674f..5547e8ffec 100644 --- a/.env.development +++ b/.env.development @@ -46,3 +46,4 @@ AI_TRANSLATIONS_BASE_URL='http://localhost:18760' ENABLE_HOME_PAGE_COURSE_API_V2=false ENABLE_CHECKLIST_QUALITY=true ENABLE_GRADING_METHOD_IN_PROBLEMS=false +LIBRARY_MODE="mixed" diff --git a/.env.test b/.env.test index 28240ad2ff..0f73517968 100644 --- a/.env.test +++ b/.env.test @@ -37,3 +37,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" diff --git a/README.rst b/README.rst index 3847453ea3..6f1de194b4 100644 --- a/README.rst +++ b/README.rst @@ -264,6 +264,22 @@ In additional to the standard settings, the following local configuration items Tagging/Taxonomy functionality. +Feature: Libraries V2/Legacy Tabs +================================= + +.. image:: ./docs/readme-images/feature-v2-and-legacy-libs.png + +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``. + + * ``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. + Developing ********** diff --git a/docs/readme-images/feature-v2-and-legacy-libs.png b/docs/readme-images/feature-v2-and-legacy-libs.png new file mode 100644 index 0000000000000000000000000000000000000000..c8fd363655f7e4fc0b026bc9c7f9faf0df045e7a GIT binary patch literal 246316 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
-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

wa;w%Q#(cB+nr}wMmuC0|@ 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 { dispatch, } = useStudioHome(isPaginationCoursesEnabled); - // TODO: this should be a flag in the backend - const LIB_MODE = 'mixed'; + const libMode = getConfig().LIBRARY_MODE; const { userIsActive, @@ -82,7 +81,7 @@ const StudioHome = ({ intl }) => { } let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; - if (isMixedOrV2LibrariesMode(LIB_MODE)) { + if (isMixedOrV2LibrariesMode(libMode)) { libraryHref = `${libraryAuthoringMfeUrl}create`; } diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 789bb2bea1..997796913f 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -25,10 +25,7 @@ const TabsSection = ({ isPaginationCoursesEnabled, }) => { const navigate = useNavigate(); - - // TODO: this should be a flag in the backend - const LIB_MODE = 'mixed'; - + const libMode = getConfig().LIBRARY_MODE; const TABS_LIST = { courses: 'courses', libraries: 'libraries', @@ -94,7 +91,7 @@ const TabsSection = ({ } if (librariesEnabled) { - if (isMixedOrV2LibrariesMode(LIB_MODE)) { + if (isMixedOrV2LibrariesMode(libMode)) { tabs.push( Date: Tue, 28 May 2024 21:23:39 +0300 Subject: [PATCH 016/106] feat: Add url paths/navigation for each tab The path updates when selecting tabs, when accessing the url with the path directly it will open its respective tab. Navigating using the browser back/forward buttons is also supported. --- src/index.jsx | 2 ++ src/studio-home/data/api.js | 4 +++ src/studio-home/tabs-section/index.jsx | 48 +++++++++++++++++++++++--- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index e3d21096a3..f17c0563ab 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -52,6 +52,8 @@ const App = () => { createRoutesFromElements( } /> + } /> + } /> } /> } /> {getConfig().ENABLE_ACCESSIBILITY_PAGE === 'true' && ( diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index 0c09601d11..69e0487fff 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -40,6 +40,10 @@ export async function getStudioHomeLibraries() { return camelCaseObject(data); } +/** + * Get's studio home v2 Libraries. + * @returns {Promise} + */ export async function getStudioHomeLibrariesV2() { const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`); return camelCaseObject(data); diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 997796913f..089f9bc842 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -1,10 +1,10 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { Tab, Tabs } from '@openedx/paragon'; import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; @@ -25,6 +25,7 @@ const TabsSection = ({ isPaginationCoursesEnabled, }) => { const navigate = useNavigate(); + const { pathname } = useLocation(); const libMode = getConfig().LIBRARY_MODE; const TABS_LIST = { courses: 'courses', @@ -33,7 +34,37 @@ const TabsSection = ({ archived: 'archived', taxonomies: 'taxonomies', }; - const [tabKey, setTabKey] = useState(TABS_LIST.courses); + + const initTabKeyState = (pname) => { + if (pname.includes('/libraries')) { + return isMixedOrV2LibrariesMode(libMode) + ? TABS_LIST.libraries + : TABS_LIST.legacyLibraries; + } + + if (pname.includes('/legacy-libraries')) { + return TABS_LIST.legacyLibraries; + } + + // Default to courses tab + return TABS_LIST.courses; + }; + + const [tabKey, setTabKey] = useState(initTabKeyState(pathname)); + + // This is needed to handle navigating using the back/forward buttons in the browser + useEffect(() => { + // Handle special case when navigating directly to /legacy-libraries or /libraries in `v1 only` mode + // we need to call dispatch to fetch library data + if ( + (isMixedOrV1LibrariesMode(libMode) && pathname.includes('/libraries')) + || pathname.includes('/legacy-libraries') + ) { + dispatch(fetchLibraryData()); + } + setTabKey(initTabKeyState(pathname)); + }, [pathname]); + const { libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe, @@ -138,8 +169,17 @@ const TabsSection = ({ }, [archivedCourses, librariesEnabled, showNewCourseContainer, isLoadingCourses, isLoadingLibraries]); const handleSelectTab = (tab) => { - if (tab === TABS_LIST.legacyLibraries) { + if (tab === TABS_LIST.courses) { + navigate('/home'); + } else if (tab === TABS_LIST.legacyLibraries) { dispatch(fetchLibraryData()); + navigate( + libMode === 'v1 only' + ? '/libraries' + : '/legacy-libraries', + ); + } else if (tab === TABS_LIST.libraries) { + navigate('/libraries'); } else if (tab === TABS_LIST.taxonomies) { navigate('/taxonomies'); } From 4ffd65195d1605cf486d4df0e8c51bc2b731bc87 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Thu, 30 May 2024 14:52:53 +0300 Subject: [PATCH 017/106] feat: LibraryV2 redirect to lib mfe or placeholder --- src/index.jsx | 2 ++ .../tabs-section/LibraryV2Placeholder.tsx | 36 +++++++++++++++++++ src/studio-home/tabs-section/index.jsx | 5 ++- .../tabs-section/libraries-v2-tab/index.tsx | 23 +++++++++--- src/studio-home/tabs-section/messages.js | 8 +++++ 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/studio-home/tabs-section/LibraryV2Placeholder.tsx diff --git a/src/index.jsx b/src/index.jsx index f17c0563ab..8bd2d4ef06 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -23,6 +23,7 @@ import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; import { StudioHome } from './studio-home'; +import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder.tsx'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; @@ -54,6 +55,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> {getConfig().ENABLE_ACCESSIBILITY_PAGE === 'true' && ( diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.tsx b/src/studio-home/tabs-section/LibraryV2Placeholder.tsx new file mode 100644 index 0000000000..ba47ee8899 --- /dev/null +++ b/src/studio-home/tabs-section/LibraryV2Placeholder.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Container } from '@openedx/paragon'; +import { StudioFooter } from '@edx/frontend-component-footer'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import Header from '../../header'; +import SubHeader from '../../generic/sub-header/SubHeader'; +import messages from './messages'; + + +const LibraryV2Placeholder = () => { + const intl = useIntl(); + + return ( + <> +
+ +
+
+
+ +
+
+
+

{intl.formatMessage(messages.libraryV2PlaceholderBody)}

+
+
+
+ + + ); +}; + +export default LibraryV2Placeholder; diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 089f9bc842..aa9d1aa0e2 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -129,7 +129,10 @@ const TabsSection = ({ eventKey={TABS_LIST.libraries} title={intl.formatMessage(messages.librariesTabTitle)} > - + , ); } 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 1e14ffef6c..98888f79a8 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Icon, Row } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -8,7 +9,10 @@ import AlertMessage from '../../../generic/alert-message'; import CardItem from '../../card-item'; import messages from '../messages'; -const LibrariesV2Tab = () => { +const LibrariesV2Tab = ({ + libraryAuthoringMfeUrl, + redirectToLibraryAuthoringMfe, +}) => { const intl = useIntl(); const { data, @@ -24,6 +28,14 @@ const LibrariesV2Tab = () => { ); } + const libURL = (id: string): string => ( + libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe + ? `${libraryAuthoringMfeUrl}library/${id}` + // Redirection to the placeholder is done in the MFE rather than + // through the backend i.e. redirection from cms, because this this will probably change + : `${window.location.origin}/course-authoring/library/${id}` + ); + return ( isError ? ( { /> ) : (
- {data.map(({ org, slug, title }) => ( + {data.map(({ id, org, slug, title }) => ( ))}
@@ -54,5 +65,9 @@ const LibrariesV2Tab = () => { ); }; +LibrariesV2Tab.propTypes = { + libraryAuthoringMfeUrl: PropTypes.string.isRequired, + redirectToLibraryAuthoringMfe: PropTypes.bool.isRequired, +}; export default LibrariesV2Tab; diff --git a/src/studio-home/tabs-section/messages.js b/src/studio-home/tabs-section/messages.js index e1ad0fd44f..0ed614f55a 100644 --- a/src/studio-home/tabs-section/messages.js +++ b/src/studio-home/tabs-section/messages.js @@ -50,6 +50,14 @@ const messages = defineMessages({ defaultMessage: 'Taxonomies', description: 'Title of Taxonomies tab on the home page', }, + libraryV2PlaceholderTitle: { + id: 'course-authoring.studio-home.libraries.placeholder.title', + defaultMessage: 'Library V2 Placeholder', + }, + libraryV2PlaceholderBody: { + id: 'course-authoring.studio-home.libraries.placeholder.body', + defaultMessage: 'This is a placeholder page, as the Library Authoring MFE is not enabled.', + }, }); export default messages; From 7f97243f65750e2de0542023b1b144ed9fcee129 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Thu, 30 May 2024 18:56:20 +0300 Subject: [PATCH 018/106] feat: Add pagination support for lib v2s --- src/studio-home/data/api.js | 19 ++++++++-- src/studio-home/data/apiHooks.ts | 14 +++++-- .../tabs-section/libraries-v2-tab/index.tsx | 38 ++++++++++++++++--- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index 69e0487fff..2124f6fed7 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -42,10 +42,23 @@ export async function getStudioHomeLibraries() { /** * Get's studio home v2 Libraries. - * @returns {Promise} + * @param {object} customParams - Additional custom paramaters for the API request. + * @param {string} [customParams.type] - (optional) Library type, default `complex` + * @param {number} [customParams.page] - (optional) Page number of results + * @param {number} [customParams.pageSize] - (optional) The number of results on each page, default `50` + * @param {boolean} [customParams.pagination] - (optional) Whether pagination is supported, default `true` + * @returns {Promise} - A Promise that resolves to the response data container the studio home v2 libraries. */ -export async function getStudioHomeLibrariesV2() { - const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`); +export async function getStudioHomeLibrariesV2(customParams) { + // Set default params if not passed in + const customParamsDefaults = { + type: customParams.type || 'complex', + page: customParams.page || 1, + pageSize: customParams.pageSize || 50, + pagination: customParams.pagination !== undefined ? customParams.pagination : true, + }; + const customParamsFormat = snakeCaseObject(customParamsDefaults); + const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`, { params: customParamsFormat }); return camelCaseObject(data); } diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.ts index 7285874c64..79929f040f 100644 --- a/src/studio-home/data/apiHooks.ts +++ b/src/studio-home/data/apiHooks.ts @@ -2,12 +2,20 @@ import { useQuery } from '@tanstack/react-query'; import { getStudioHomeLibrariesV2 } from './api'; + +interface CustomParams { + type?: string, + page?: number, + pageSize?: number, + pagination?: boolean, +} + /** * Builds the query to fetch list of V2 Libraries */ -export const useListStudioHomeV2Libraries = () => ( +export const useListStudioHomeV2Libraries = (customParams: CustomParams) => ( useQuery({ - queryKey: ['listV2Libraries'], - queryFn: () => getStudioHomeLibrariesV2(), + queryKey: ['listV2Libraries', customParams], + queryFn: () => getStudioHomeLibrariesV2(customParams), }) ); 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 98888f79a8..c26527edd8 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Icon, Row } from '@openedx/paragon'; +import { Icon, Row, Pagination } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useListStudioHomeV2Libraries } from '../../data/apiHooks'; @@ -14,11 +14,18 @@ const LibrariesV2Tab = ({ redirectToLibraryAuthoringMfe, }) => { const intl = useIntl(); + + const [currentPage, setCurrentPage] = useState(1); + + const handlePageSelect = (page) => { + setCurrentPage(page); + }; + const { data, isLoading, isError, - } = useListStudioHomeV2Libraries(); + } = useListStudioHomeV2Libraries({page: currentPage}); if (isLoading) { return ( @@ -49,8 +56,19 @@ const LibrariesV2Tab = ({ )} /> ) : ( -
- {data.map(({ id, org, slug, title }) => ( +
+
+ {/* Temporary div to add spacing. This will be replaced with lib search/filters */} +
+

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

+
+ + {data.results.map(({ id, org, slug, title }) => ( ))} + + {data.numPages > 1 && + + }
) ); From c86b85a4eb77bb94e0f8e0a8ae7f4e16dfa9fcbc Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 16:50:53 +0300 Subject: [PATCH 019/106] fix: Redirect to placeholder create lib in v2/mixed disabled mfe --- src/studio-home/StudioHome.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 2d68af9c25..52be27a60f 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -51,6 +51,7 @@ const StudioHome = ({ intl }) => { studioShortName, studioRequestEmail, libraryAuthoringMfeUrl, + redirectToLibraryAuthoringMfe, } = studioHomeData; function getHeaderButtons() { @@ -82,7 +83,9 @@ const StudioHome = ({ intl }) => { let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; if (isMixedOrV2LibrariesMode(libMode)) { - libraryHref = `${libraryAuthoringMfeUrl}create`; + libraryHref = libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe + ? `${libraryAuthoringMfeUrl}create` + : `${window.location.origin}/course-authoring/library/create`; } headerButtons.push( From efbc625aaa03a51e0f010ae25a7821b0404b854a Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 17:03:01 +0300 Subject: [PATCH 020/106] temp: This removes TS code to get tests to run This commit is temporary as the current frontend build system in tests doesnt support TS syntax. That should be fixed soon, and this commit should be removed. --- src/studio-home/data/apiHooks.ts | 9 +-------- src/studio-home/tabs-section/libraries-v2-tab/index.tsx | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.ts index 79929f040f..ec163e5732 100644 --- a/src/studio-home/data/apiHooks.ts +++ b/src/studio-home/data/apiHooks.ts @@ -3,17 +3,10 @@ import { useQuery } from '@tanstack/react-query'; import { getStudioHomeLibrariesV2 } from './api'; -interface CustomParams { - type?: string, - page?: number, - pageSize?: number, - pagination?: boolean, -} - /** * Builds the query to fetch list of V2 Libraries */ -export const useListStudioHomeV2Libraries = (customParams: CustomParams) => ( +export const useListStudioHomeV2Libraries = (customParams) => ( useQuery({ queryKey: ['listV2Libraries', customParams], queryFn: () => getStudioHomeLibrariesV2(customParams), 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 c26527edd8..a659dcc1fa 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -35,7 +35,7 @@ const LibrariesV2Tab = ({ ); } - const libURL = (id: string): string => ( + const libURL = (id) => ( libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe ? `${libraryAuthoringMfeUrl}library/${id}` // Redirection to the placeholder is done in the MFE rather than From 21da6f84f922c0834d4a06e958ef89161a43b01a Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 18:35:29 +0300 Subject: [PATCH 021/106] test: Update existing tests to support changes --- src/setupTest.js | 1 + src/studio-home/StudioHome.test.jsx | 46 ++++++++++++++----- src/studio-home/__mocks__/studioHomeMock.js | 2 +- .../tabs-section/TabsSection.test.jsx | 39 +++++++++++++--- 4 files changed, 69 insertions(+), 19 deletions(-) diff --git a/src/setupTest.js b/src/setupTest.js index 35b1c9ebe2..f0f7f6a435 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -48,6 +48,7 @@ mergeConfig({ ENABLE_TEAM_TYPE_SETTING: process.env.ENABLE_TEAM_TYPE_SETTING === 'true', ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true', STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null, + LIBRARY_MODE: process.env.LIBRARY_MODE || 'v1 only', }, 'CourseAuthoringConfig'); class ResizeObserver { diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx index 7286acda0f..49ca600e5d 100644 --- a/src/studio-home/StudioHome.test.jsx +++ b/src/studio-home/StudioHome.test.jsx @@ -1,6 +1,8 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { initializeMockApp } from '@edx/frontend-platform'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { initializeMockApp, getConfig, setConfig } 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'; @@ -23,7 +25,6 @@ import { StudioHome } from '.'; let axiosMock; let store; -const mockPathname = '/foo-bar'; const { studioShortName, studioRequestEmail, @@ -34,17 +35,29 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useLocation: () => ({ - pathname: mockPathname, - }), -})); +const queryClient = new QueryClient(); const RootWrapper = () => ( - + - + + + + } + /> + } + /> + } + /> + + + ); @@ -145,7 +158,18 @@ describe('', async () => { }); describe('render new library button', () => { - it('href should include home_library', async () => { + beforeEach(() => { + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'mixed', + }); + }); + + it('href should include home_library when in "v1 only" lib mode', async () => { + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'v1 only', + }); useSelector.mockReturnValue({ ...studioHomeMock, courseCreatorStatus: COURSE_CREATOR_STATES.granted, diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index 5385201e52..4f66cc116f 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -62,7 +62,7 @@ module.exports = { }, ], librariesEnabled: true, - libraryAuthoringMfeUrl: 'http://localhost:3001', + libraryAuthoringMfeUrl: 'http://localhost:3001/', optimizationEnabled: false, redirectToLibraryAuthoringMfe: false, requestCourseCreatorUrl: '/request_course_creator', diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index ea5929aeec..945322dcd5 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -1,4 +1,6 @@ import React from 'react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { getConfig, initializeMockApp, setConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { @@ -34,15 +36,38 @@ const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`; const mockDispatch = jest.fn(); +const queryClient = new QueryClient(); + +const tabSectionComponent = (overrideProps) => ( + +); + const RootWrapper = (overrideProps) => ( - + - + + + + + + + + + ); From 79e6516b32cc6b03e5438f5c33138a20d72bea24 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 19:04:16 +0300 Subject: [PATCH 022/106] temp: Rename .tsx -> .jsx & .ts -> .js for tests This is a temporary commit since there are currently no webpack loaders that support tsx files in the test running. This commit should be removed once that is fixed upstream. --- src/index.jsx | 2 +- src/studio-home/data/{apiHooks.ts => apiHooks.js} | 0 .../{LibraryV2Placeholder.tsx => LibraryV2Placeholder.jsx} | 0 src/studio-home/tabs-section/index.jsx | 2 +- .../tabs-section/libraries-v2-tab/{index.tsx => index.jsx} | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename src/studio-home/data/{apiHooks.ts => apiHooks.js} (100%) rename src/studio-home/tabs-section/{LibraryV2Placeholder.tsx => LibraryV2Placeholder.jsx} (100%) rename src/studio-home/tabs-section/libraries-v2-tab/{index.tsx => index.jsx} (100%) diff --git a/src/index.jsx b/src/index.jsx index 8bd2d4ef06..93fe3c3f4c 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -23,7 +23,7 @@ import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; import { StudioHome } from './studio-home'; -import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder.tsx'; +import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.js similarity index 100% rename from src/studio-home/data/apiHooks.ts rename to src/studio-home/data/apiHooks.js diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.tsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx similarity index 100% rename from src/studio-home/tabs-section/LibraryV2Placeholder.tsx rename to src/studio-home/tabs-section/LibraryV2Placeholder.jsx diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index aa9d1aa0e2..703a4e6f04 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -9,7 +9,7 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; import LibrariesTab from './libraries-tab'; -import LibrariesV2Tab from './libraries-v2-tab/index.tsx'; +import LibrariesV2Tab from './libraries-v2-tab/index'; import ArchivedTab from './archived-tab'; import CoursesTab from './courses-tab'; import { RequestStatus } from '../../data/constants'; diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx similarity index 100% rename from src/studio-home/tabs-section/libraries-v2-tab/index.tsx rename to src/studio-home/tabs-section/libraries-v2-tab/index.jsx From 262cb3fdf8578b937bcb3233a7d53c8afcf53cd1 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 3 Jun 2024 19:24:05 +0300 Subject: [PATCH 023/106] fix: Fix lint issues --- src/studio-home/data/apiHooks.js | 5 +- .../tabs-section/LibraryV2Placeholder.jsx | 1 - .../tabs-section/libraries-v2-tab/index.jsx | 47 +++++++++++-------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/studio-home/data/apiHooks.js b/src/studio-home/data/apiHooks.js index ec163e5732..92575bf717 100644 --- a/src/studio-home/data/apiHooks.js +++ b/src/studio-home/data/apiHooks.js @@ -2,13 +2,14 @@ import { useQuery } from '@tanstack/react-query'; import { getStudioHomeLibrariesV2 } from './api'; - /** * Builds the query to fetch list of V2 Libraries */ -export const useListStudioHomeV2Libraries = (customParams) => ( +const useListStudioHomeV2Libraries = (customParams) => ( useQuery({ queryKey: ['listV2Libraries', customParams], queryFn: () => getStudioHomeLibrariesV2(customParams), }) ); + +export default useListStudioHomeV2Libraries; diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx index ba47ee8899..6844515bd9 100644 --- a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx +++ b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx @@ -7,7 +7,6 @@ import Header from '../../header'; import SubHeader from '../../generic/sub-header/SubHeader'; import messages from './messages'; - const LibraryV2Placeholder = () => { const intl = useIntl(); diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx index a659dcc1fa..9060493dd1 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Icon, Row, Pagination } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { useListStudioHomeV2Libraries } from '../../data/apiHooks'; +import useListStudioHomeV2Libraries from '../../data/apiHooks'; import { LoadingSpinner } from '../../../generic/Loading'; import AlertMessage from '../../../generic/alert-message'; import CardItem from '../../card-item'; @@ -25,7 +25,7 @@ const LibrariesV2Tab = ({ data, isLoading, isError, - } = useListStudioHomeV2Libraries({page: currentPage}); + } = useListStudioHomeV2Libraries({ page: currentPage }); if (isLoading) { return ( @@ -68,25 +68,32 @@ const LibrariesV2Tab = ({

- {data.results.map(({ id, org, slug, title }) => ( - - ))} + { + data.results.map(({ + id, org, slug, title, + }) => ( + + )) + } - {data.numPages > 1 && - + { + data.numPages > 1 + && ( + + ) }
) From 1ea229fc85d351a2610cd07085e33ac0728d3c12 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Tue, 4 Jun 2024 22:58:51 +0300 Subject: [PATCH 024/106] test: Add tests for new functionality --- src/studio-home/__mocks__/index.js | 2 +- .../listStudioHomeV2LibrariesMock.js | 44 ++++ src/studio-home/data/api.test.js | 24 ++- .../factories/mockApiResponses.jsx | 47 +++- .../tabs-section/LibraryV2Placeholder.jsx | 1 + .../tabs-section/TabsSection.test.jsx | 201 +++++++++++++++++- 6 files changed, 309 insertions(+), 10 deletions(-) create mode 100644 src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js diff --git a/src/studio-home/__mocks__/index.js b/src/studio-home/__mocks__/index.js index 92461eb0bb..af2a85b390 100644 --- a/src/studio-home/__mocks__/index.js +++ b/src/studio-home/__mocks__/index.js @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export { default as studioHomeMock } from './studioHomeMock'; +export { default as listStudioHomeV2LibrariesMock } from './listStudioHomeV2LibrariesMock'; diff --git a/src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js b/src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js new file mode 100644 index 0000000000..02257a9744 --- /dev/null +++ b/src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js @@ -0,0 +1,44 @@ +module.exports = { + next: null, + previous: null, + count: 2, + num_pages: 1, + current_page: 1, + start: 0, + results: [ + { + id: 'lib:SampleTaxonomyOrg1:AL1', + type: 'complex', + org: 'SampleTaxonomyOrg1', + slug: 'AL1', + title: 'Another Library 2', + description: '', + num_blocks: 0, + version: 0, + last_published: null, + allow_lti: false, + allow_public_learning: false, + allow_public_read: false, + has_unpublished_changes: false, + has_unpublished_deletes: false, + license: '', + }, + { + id: 'lib:SampleTaxonomyOrg1:TL1', + type: 'complex', + org: 'SampleTaxonomyOrg1', + slug: 'TL1', + title: 'Test Library 1', + description: '', + num_blocks: 0, + version: 0, + last_published: null, + allow_lti: false, + allow_public_learning: false, + allow_public_read: false, + has_unpublished_changes: false, + has_unpublished_deletes: false, + license: '', + }, + ], +}; diff --git a/src/studio-home/data/api.test.js b/src/studio-home/data/api.test.js index 593a2730de..66f6ee279f 100644 --- a/src/studio-home/data/api.test.js +++ b/src/studio-home/data/api.test.js @@ -13,8 +13,14 @@ import { getStudioHomeCourses, getStudioHomeCoursesV2, getStudioHomeLibraries, + getStudioHomeLibrariesV2, } from './api'; -import { generateGetStudioCoursesApiResponse, generateGetStudioHomeDataApiResponse, generateGetStuioHomeLibrariesApiResponse } from '../factories/mockApiResponses'; +import { + generateGetStudioCoursesApiResponse, + generateGetStudioHomeDataApiResponse, + generateGetStudioHomeLibrariesApiResponse, + generateGetStudioHomeLibrariesV2ApiResponse, +} from '../factories/mockApiResponses'; let axiosMock; @@ -64,11 +70,21 @@ describe('studio-home api calls', () => { expect(result).toEqual(expected); }); - it('should get studio libraries data', async () => { + it('should get studio v1 libraries data', async () => { const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`; - axiosMock.onGet(apiLink).reply(200, generateGetStuioHomeLibrariesApiResponse()); + axiosMock.onGet(apiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); const result = await getStudioHomeLibraries(); - const expected = generateGetStuioHomeLibrariesApiResponse(); + const expected = generateGetStudioHomeLibrariesApiResponse(); + + expect(axiosMock.history.get[0].url).toEqual(apiLink); + expect(result).toEqual(expected); + }); + + it('should get studio v2 libraries data', async () => { + const apiLink = `${getApiBaseUrl()}/api/libraries/v2/`; + axiosMock.onGet(apiLink).reply(200, generateGetStudioHomeLibrariesV2ApiResponse()); + const result = await getStudioHomeLibrariesV2({}); + const expected = generateGetStudioHomeLibrariesV2ApiResponse(); expect(axiosMock.history.get[0].url).toEqual(apiLink); expect(result).toEqual(expected); diff --git a/src/studio-home/factories/mockApiResponses.jsx b/src/studio-home/factories/mockApiResponses.jsx index 30615ba8d5..5d75f9f592 100644 --- a/src/studio-home/factories/mockApiResponses.jsx +++ b/src/studio-home/factories/mockApiResponses.jsx @@ -112,7 +112,7 @@ export const generateGetStudioCoursesApiResponseV2 = () => ({ }, }); -export const generateGetStuioHomeLibrariesApiResponse = () => ({ +export const generateGetStudioHomeLibrariesApiResponse = () => ({ libraries: [ { displayName: 'MBA', @@ -125,6 +125,51 @@ export const generateGetStuioHomeLibrariesApiResponse = () => ({ ], }); +export const generateGetStudioHomeLibrariesV2ApiResponse = () => ({ + next: null, + previous: null, + count: 2, + numPages: 1, + currentPage: 1, + start: 0, + results: [ + { + id: 'lib:SampleTaxonomyOrg1:AL1', + type: 'complex', + org: 'SampleTaxonomyOrg1', + slug: 'AL1', + title: 'Another Library 2', + description: '', + numBlocks: 0, + version: 0, + lastPublished: null, + allowLti: false, + allowPublicLearning: false, + allowpublicRead: false, + hasUnpublishedChanges: false, + hasUnpublishedDeletes: false, + license: '', + }, + { + id: 'lib:SampleTaxonomyOrg1:TL1', + type: 'complex', + org: 'SampleTaxonomyOrg1', + slug: 'TL1', + title: 'Test Library 1', + description: '', + numBlocks: 0, + version: 0, + lastPublished: null, + allowLti: false, + allowPublicLearning: false, + allowPublicRead: false, + hasUnpublishedChanges: false, + hasUnpublishedDeletes: false, + license: '', + }, + ], +}); + export const generateNewVideoApiResponse = () => ({ files: [{ edx_video_id: 'mOckID4', diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx index 6844515bd9..6b13853a2c 100644 --- a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx +++ b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx @@ -7,6 +7,7 @@ import Header from '../../header'; import SubHeader from '../../generic/sub-header/SubHeader'; import messages from './messages'; +/* istanbul ignore next */ const LibraryV2Placeholder = () => { const intl = useIntl(); diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index 945322dcd5..54741ebbb1 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -11,7 +11,7 @@ import { AppProvider } from '@edx/frontend-platform/react'; import MockAdapter from 'axios-mock-adapter'; import initializeStore from '../../store'; -import { studioHomeMock } from '../__mocks__'; +import { studioHomeMock, listStudioHomeV2LibrariesMock } from '../__mocks__'; import messages from '../messages'; import tabMessages from './messages'; import TabsSection from '.'; @@ -20,12 +20,32 @@ import { generateGetStudioHomeDataApiResponse, generateGetStudioCoursesApiResponse, generateGetStudioCoursesApiResponseV2, - generateGetStuioHomeLibrariesApiResponse, + generateGetStudioHomeLibrariesApiResponse, } from '../factories/mockApiResponses'; import { getApiBaseUrl, getStudioHomeApiUrl } from '../data/api'; import { executeThunk } from '../../utils'; import { fetchLibraryData, fetchStudioHomeData } from '../data/thunks'; +import useListStudioHomeV2Libraries from '../data/apiHooks'; + +jest.mock('../data/apiHooks', () => ({ + // Since only useListStudioHomeV2Libraries is exported as default + __esModule: true, + default: jest.fn(() => ({ + data: { + next: null, + previous: null, + count: 2, + num_pages: 1, + current_page: 1, + start: 0, + results: [], + }, + isLoading: false, + isError: false, + })), +})); + const { studioShortName } = studioHomeMock; let axiosMock; @@ -84,6 +104,10 @@ describe('', () => { }); store = initializeStore(initialState); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'mixed', + }); }); it('should render all tabs correctly', async () => { @@ -105,11 +129,47 @@ describe('', () => { expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument(); 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); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + + 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); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + + expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).not.toBeInTheDocument(); + }); + describe('course tab', () => { it('should render specific course details', async () => { render(); @@ -181,6 +241,46 @@ describe('', () => { const pagination = screen.queryByRole('navigation'); expect(pagination).not.toBeInTheDocument(); }); + + it('should set the url path to "/home" when switching away then back to courses tab', async () => { + const data = generateGetStudioCoursesApiResponseV2(); + data.results.courses = []; + render(); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + axiosMock.onGet(courseApiLinkV2).reply(200, data); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + // confirm the url path is initially /home + waitFor(() => { + expect(window.location.href).toContain('/home'); + }); + + // switch to libraries tab + axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); + await executeThunk(fetchLibraryData(), store.dispatch); + const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); + await act(async () => { + fireEvent.click(librariesTab); + }); + + // confirm that the url path has changed + expect(librariesTab).toHaveClass('active'); + waitFor(() => { + expect(window.location.href).toContain('/legacy-libraries'); + }); + + // switch back to courses tab + const coursesTab = screen.getByText(tabMessages.coursesTabTitle.defaultMessage); + await act(async () => { + fireEvent.click(coursesTab); + }); + + // confirm that the url path is /home + expect(coursesTab).toHaveClass('active'); + waitFor(() => { + expect(window.location.href).toContain('/home'); + }); + }); }); describe('taxonomies tab', () => { @@ -247,6 +347,8 @@ describe('', () => { expect(screen.getByText(tabMessages.coursesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).toBeInTheDocument(); expect(screen.queryByText(tabMessages.archivedTabTitle.defaultMessage)).toBeNull(); @@ -254,10 +356,10 @@ describe('', () => { }); describe('library tab', () => { - it('should switch to Libraries tab and render specific library details', async () => { + it('should switch to Legacy Libraries tab and render specific v1 library details', async () => { render(); axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); - axiosMock.onGet(libraryApiLink).reply(200, generateGetStuioHomeLibrariesApiResponse()); + axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); await executeThunk(fetchStudioHomeData(), store.dispatch); await executeThunk(fetchLibraryData(), store.dispatch); @@ -273,6 +375,97 @@ describe('', () => { expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible(); }); + it('should switch to Libraries tab and render specific v2 library details', async () => { + useListStudioHomeV2Libraries.mockReturnValue({ + data: listStudioHomeV2LibrariesMock, + isLoading: false, + isError: false, + }); + + render(); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + await act(async () => { + fireEvent.click(librariesTab); + }); + + expect(librariesTab).toHaveClass('active'); + + expect(screen.getByText('Showing 2 of 2')).toBeVisible(); + + expect(screen.getByText(listStudioHomeV2LibrariesMock.results[0].title)).toBeVisible(); + expect(screen.getByText( + `${listStudioHomeV2LibrariesMock.results[0].org} / ${listStudioHomeV2LibrariesMock.results[0].slug}`, + )).toBeVisible(); + + expect(screen.getByText(listStudioHomeV2LibrariesMock.results[1].title)).toBeVisible(); + expect(screen.getByText( + `${listStudioHomeV2LibrariesMock.results[1].org} / ${listStudioHomeV2LibrariesMock.results[1].slug}`, + )).toBeVisible(); + }); + + it('should switch to Libraries tab and render specific v1 library details ("v1 only" mode)', async () => { + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'v1 only', + }); + + render(); + 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); + await act(async () => { + fireEvent.click(librariesTab); + }); + + expect(librariesTab).toHaveClass('active'); + + expect(screen.getByText(studioHomeMock.libraries[0].displayName)).toBeVisible(); + + expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible(); + }); + + it('should switch to Libraries tab and render specific v2 library details ("v2 only" mode)', async () => { + setConfig({ + ...getConfig(), + LIBRARY_MODE: 'v2 only', + }); + + useListStudioHomeV2Libraries.mockReturnValue({ + data: listStudioHomeV2LibrariesMock, + isLoading: false, + isError: false, + }); + + render(); + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + await executeThunk(fetchStudioHomeData(), store.dispatch); + + const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage); + await act(async () => { + fireEvent.click(librariesTab); + }); + + expect(librariesTab).toHaveClass('active'); + + expect(screen.getByText('Showing 2 of 2')).toBeVisible(); + + expect(screen.getByText(listStudioHomeV2LibrariesMock.results[0].title)).toBeVisible(); + expect(screen.getByText( + `${listStudioHomeV2LibrariesMock.results[0].org} / ${listStudioHomeV2LibrariesMock.results[0].slug}`, + )).toBeVisible(); + + expect(screen.getByText(listStudioHomeV2LibrariesMock.results[1].title)).toBeVisible(); + expect(screen.getByText( + `${listStudioHomeV2LibrariesMock.results[1].org} / ${listStudioHomeV2LibrariesMock.results[1].slug}`, + )).toBeVisible(); + }); + it('should hide Libraries tab when libraries are disabled', async () => { const data = generateGetStudioHomeDataApiResponse(); data.librariesEnabled = false; From a0a30b7f31dd3ad9a0f518ac0932143ea81b0e14 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Thu, 6 Jun 2024 17:47:47 +0300 Subject: [PATCH 025/106] refactor: Change /legacy-libraries -> /libraries-v1 --- src/index.jsx | 2 +- src/studio-home/StudioHome.test.jsx | 2 +- .../tabs-section/TabsSection.test.jsx | 4 ++-- src/studio-home/tabs-section/index.jsx | 17 ++++++++--------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index 93fe3c3f4c..f881441df9 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -54,7 +54,7 @@ const App = () => { } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx index 49ca600e5d..4f11d3d4c1 100644 --- a/src/studio-home/StudioHome.test.jsx +++ b/src/studio-home/StudioHome.test.jsx @@ -52,7 +52,7 @@ const RootWrapper = () => ( element={} /> } /> diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index 54741ebbb1..fce235f7b7 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -82,7 +82,7 @@ const RootWrapper = (overrideProps) => ( element={tabSectionComponent(overrideProps)} /> @@ -266,7 +266,7 @@ describe('', () => { // confirm that the url path has changed expect(librariesTab).toHaveClass('active'); waitFor(() => { - expect(window.location.href).toContain('/legacy-libraries'); + expect(window.location.href).toContain('/libraries-v1'); }); // switch back to courses tab diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 703a4e6f04..5f3085bb9c 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -36,16 +36,16 @@ const TabsSection = ({ }; const initTabKeyState = (pname) => { + if (pname.includes('/libraries-v1')) { + return TABS_LIST.legacyLibraries; + } + if (pname.includes('/libraries')) { return isMixedOrV2LibrariesMode(libMode) ? TABS_LIST.libraries : TABS_LIST.legacyLibraries; } - if (pname.includes('/legacy-libraries')) { - return TABS_LIST.legacyLibraries; - } - // Default to courses tab return TABS_LIST.courses; }; @@ -54,11 +54,10 @@ const TabsSection = ({ // This is needed to handle navigating using the back/forward buttons in the browser useEffect(() => { - // Handle special case when navigating directly to /legacy-libraries or /libraries in `v1 only` mode + // Handle special case when navigating directly to /libraries-v1 or /libraries in `v1 only` mode // we need to call dispatch to fetch library data - if ( - (isMixedOrV1LibrariesMode(libMode) && pathname.includes('/libraries')) - || pathname.includes('/legacy-libraries') + if (pathname.includes('/libraries-v1') + || (isMixedOrV1LibrariesMode(libMode) && pathname.includes('/libraries')) ) { dispatch(fetchLibraryData()); } @@ -179,7 +178,7 @@ const TabsSection = ({ navigate( libMode === 'v1 only' ? '/libraries' - : '/legacy-libraries', + : '/libraries-v1', ); } else if (tab === TABS_LIST.libraries) { navigate('/libraries'); From 4deaea9a1f3d70aff3d175035f97947c5e996618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 6 Jun 2024 14:53:16 -0300 Subject: [PATCH 026/106] fix: add i18n messages --- src/library-authoring/EmptyStates.jsx | 6 ++++-- src/library-authoring/LibraryHome.jsx | 4 +++- src/library-authoring/messages.ts | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/library-authoring/EmptyStates.jsx b/src/library-authoring/EmptyStates.jsx index 6f54dc810b..d7b718c71d 100644 --- a/src/library-authoring/EmptyStates.jsx +++ b/src/library-authoring/EmptyStates.jsx @@ -9,8 +9,10 @@ import messages from './messages'; export const NoComponents = () => ( -
You have not added any content to this library yet.
- + +
); diff --git a/src/library-authoring/LibraryHome.jsx b/src/library-authoring/LibraryHome.jsx index e2c16862ca..4331a0b54d 100644 --- a/src/library-authoring/LibraryHome.jsx +++ b/src/library-authoring/LibraryHome.jsx @@ -1,6 +1,7 @@ // @ts-check /* eslint-disable react/prop-types */ import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Card, Stack, } from '@openedx/paragon'; @@ -9,6 +10,7 @@ import { NoComponents, NoSearchResults } from './EmptyStates'; import LibraryCollections from './LibraryCollections'; import LibraryComponents from './LibraryComponents'; import { useLibraryComponentCount } from './data/apiHook'; +import messages from './messages'; /** * @type {React.FC<{ @@ -46,7 +48,7 @@ const LibraryHome = ({ libraryId, filter }) => { return (
- Recently modified components and collections will be displayed here. +
diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts index 1a48fdeaf4..0d6c497d3a 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -16,6 +16,16 @@ const messages = defineMessages({ defaultMessage: 'No matching components found in this library.', description: 'Message displayed when no search results are found', }, + noComponents: { + id: 'course-authoring.library-authoring.no-components', + defaultMessage: 'You have not added any content to this library yet.', + description: 'Message displayed when the library is empty', + }, + addComponent: { + id: 'course-authoring.library-authoring.add-component', + defaultMessage: 'Add component', + description: 'Button text to add a new component', + }, componentsTempPlaceholder: { id: 'course-authoring.library-authoring.components-temp-placeholder', defaultMessage: 'There are {componentCount} components in this library', @@ -26,6 +36,11 @@ const messages = defineMessages({ defaultMessage: 'Coming soon!', description: 'Temp placeholder for the collections container. This will be replaced with the actual collection list.', }, + recentComponentsTempPlaceholder: { + id: 'course-authoring.library-authoring.recent-components-temp-placeholder', + defaultMessage: 'Recently modified components and collections will be displayed here.', + description: 'Temp placeholder for the recent components container. This will be replaced with the actual list.', + }, }); export default messages; From c2bdecfae35bea464a439f3043faddd305c76f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 6 Jun 2024 15:14:24 -0300 Subject: [PATCH 027/106] fix: libraryAuthoring enabled check --- src/search-modal/SearchResult.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search-modal/SearchResult.jsx b/src/search-modal/SearchResult.jsx index abf5746cbf..0d763b82ac 100644 --- a/src/search-modal/SearchResult.jsx +++ b/src/search-modal/SearchResult.jsx @@ -146,7 +146,7 @@ const SearchResult = ({ hit }) => { if (contextKey.startsWith('lib:')) { const urlSuffix = getLibraryComponentUrlSuffix(hit); - if (libraryAuthoringMfeUrl) { + if (redirectToLibraryAuthoringMfe && libraryAuthoringMfeUrl) { return `${libraryAuthoringMfeUrl}${urlSuffix}`; } From a24b3ba35bc129248a4983d724b680a7b7e83b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 6 Jun 2024 15:47:26 -0300 Subject: [PATCH 028/106] fix: add Create Library placeholder --- src/index.jsx | 3 +- src/library-authoring/CreateLibrary.jsx | 31 ++++++++++++++++ src/library-authoring/data/apiHook.ts | 1 + src/library-authoring/index.ts | 1 + src/library-authoring/messages.ts | 10 ++++++ .../tabs-section/LibraryV2Placeholder.jsx | 36 ------------------- src/studio-home/tabs-section/messages.js | 8 ----- 7 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 src/library-authoring/CreateLibrary.jsx delete mode 100644 src/studio-home/tabs-section/LibraryV2Placeholder.jsx diff --git a/src/index.jsx b/src/index.jsx index 588689aae7..2d3a7c271f 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -19,7 +19,7 @@ import { initializeHotjar } from '@edx/frontend-enterprise-hotjar'; import { logError } from '@edx/frontend-platform/logging'; import messages from './i18n'; -import { LibraryAuthoringPage } from './library-authoring'; +import { CreateLibrary, LibraryAuthoringPage } from './library-authoring'; import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; @@ -55,6 +55,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/library-authoring/CreateLibrary.jsx b/src/library-authoring/CreateLibrary.jsx new file mode 100644 index 0000000000..b75c23a4c0 --- /dev/null +++ b/src/library-authoring/CreateLibrary.jsx @@ -0,0 +1,31 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Container } from '@openedx/paragon'; + +import Header from '../header'; +import SubHeader from '../generic/sub-header/SubHeader'; + +import messages from './messages'; + +/** + * @type {React.FC} + */ +const CreateLibrary = () => ( + <> +
+ + } + /> +
+ +
+
+ +); + +export default CreateLibrary; diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.ts index 2b1516ee22..53509fbe3b 100644 --- a/src/library-authoring/data/apiHook.ts +++ b/src/library-authoring/data/apiHook.ts @@ -14,6 +14,7 @@ export const useContentLibrary = (libraryId?: string) => { return { data: undefined, error: 'No library ID provided', + isLoading: false, } } diff --git a/src/library-authoring/index.ts b/src/library-authoring/index.ts index 05cd9d1e61..69831c4ed9 100644 --- a/src/library-authoring/index.ts +++ b/src/library-authoring/index.ts @@ -1,3 +1,4 @@ // @ts-check // eslint-disable-next-line import/prefer-default-export export { default as LibraryAuthoringPage } from './LibraryAuthoringPage'; +export { default as CreateLibrary } from './CreateLibrary'; diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts index 0d6c497d3a..6a09703b64 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -41,6 +41,16 @@ const messages = defineMessages({ defaultMessage: 'Recently modified components and collections will be displayed here.', description: 'Temp placeholder for the recent components container. This will be replaced with the actual list.', }, + createLibrary: { + id: 'course-authoring.library-authoring.create-library', + defaultMessage: 'Create library', + description: 'Header for the create library form', + }, + createLibraryTempPlaceholder: { + id: 'course-authoring.library-authoring.create-library-temp-placeholder', + defaultMessage: 'This is a placeholder for the create library form. This will be replaced with the actual form.', + description: 'Temp placeholder for the create library container. This will be replaced with the new library form.', + }, }); export default messages; diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx deleted file mode 100644 index 6b13853a2c..0000000000 --- a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Container } from '@openedx/paragon'; -import { StudioFooter } from '@edx/frontend-component-footer'; -import { useIntl } from '@edx/frontend-platform/i18n'; - -import Header from '../../header'; -import SubHeader from '../../generic/sub-header/SubHeader'; -import messages from './messages'; - -/* istanbul ignore next */ -const LibraryV2Placeholder = () => { - const intl = useIntl(); - - return ( - <> -
- -
-
-
- -
-
-
-

{intl.formatMessage(messages.libraryV2PlaceholderBody)}

-
-
-
- - - ); -}; - -export default LibraryV2Placeholder; diff --git a/src/studio-home/tabs-section/messages.js b/src/studio-home/tabs-section/messages.js index 0ed614f55a..e1ad0fd44f 100644 --- a/src/studio-home/tabs-section/messages.js +++ b/src/studio-home/tabs-section/messages.js @@ -50,14 +50,6 @@ const messages = defineMessages({ defaultMessage: 'Taxonomies', description: 'Title of Taxonomies tab on the home page', }, - libraryV2PlaceholderTitle: { - id: 'course-authoring.studio-home.libraries.placeholder.title', - defaultMessage: 'Library V2 Placeholder', - }, - libraryV2PlaceholderBody: { - id: 'course-authoring.studio-home.libraries.placeholder.body', - defaultMessage: 'This is a placeholder page, as the Library Authoring MFE is not enabled.', - }, }); export default messages; From 28597411042e5c2fadf47cc8c7ef669725a4405f Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Thu, 6 Jun 2024 22:04:30 +0300 Subject: [PATCH 029/106] refactor: Remove hardcoded mfe path --- src/studio-home/StudioHome.jsx | 4 ++-- src/studio-home/card-item/index.jsx | 23 ++++++++++++++++--- .../tabs-section/libraries-v2-tab/index.jsx | 5 ++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 52be27a60f..fa1affd2e1 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -10,7 +10,7 @@ import { import { Add as AddIcon, Error } from '@openedx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { StudioFooter } from '@edx/frontend-component-footer'; -import { getConfig } from '@edx/frontend-platform'; +import { getConfig, getPath } from '@edx/frontend-platform'; import Loading from '../generic/Loading'; import InternetConnectionAlert from '../generic/internet-connection-alert'; @@ -85,7 +85,7 @@ const StudioHome = ({ intl }) => { if (isMixedOrV2LibrariesMode(libMode)) { libraryHref = libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe ? `${libraryAuthoringMfeUrl}create` - : `${window.location.origin}/course-authoring/library/create`; + : `${getPath(getConfig().PUBLIC_PATH)}library/create`; } headerButtons.push( diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.jsx index f495794d80..1ee4045390 100644 --- a/src/studio-home/card-item/index.jsx +++ b/src/studio-home/card-item/index.jsx @@ -10,7 +10,7 @@ import { } from '@openedx/paragon'; import { MoreHoriz } from '@openedx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { getConfig } from '@edx/frontend-platform'; +import { getConfig, getPath } from '@edx/frontend-platform'; import { COURSE_CREATOR_STATES } from '../../constants'; import { getStudioHomeData } from '../data/selectors'; @@ -35,7 +35,24 @@ const CardItem = ({ courseCreatorStatus, rerunCreatorStatus, } = useSelector(getStudioHomeData); - const courseUrl = () => new URL(url, getConfig().STUDIO_BASE_URL); + const destinationUrl = () => { + if (isLibraries) { + // This case is for the library authoring MFE + if (url.startsWith('http')) { + return new URL(url); + } + + if (url.includes(getPath(getConfig().PUBLIC_PATH))) { + // Redirection to the placeholder is done in the MFE rather than + // through the backend i.e. redirection from cms, because this this will probably change, + // hence why we use the MFE's origin + return new URL(url, window.location.origin); + } + } + + return new URL(url, getConfig().STUDIO_BASE_URL); + }; + const subtitle = isLibraries ? `${org} / ${number}` : `${org} / ${number} / ${run}`; const readOnlyItem = !(lmsLink || rerunLink || url); const showActions = !(readOnlyItem || isLibraries); @@ -51,7 +68,7 @@ const CardItem = ({ title={!readOnlyItem ? ( {hasDisplayName} diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx index 9060493dd1..ae0c1aecaf 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Icon, Row, Pagination } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { getConfig, getPath } from '@edx/frontend-platform'; import useListStudioHomeV2Libraries from '../../data/apiHooks'; import { LoadingSpinner } from '../../../generic/Loading'; @@ -38,9 +39,7 @@ const LibrariesV2Tab = ({ const libURL = (id) => ( libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe ? `${libraryAuthoringMfeUrl}library/${id}` - // Redirection to the placeholder is done in the MFE rather than - // through the backend i.e. redirection from cms, because this this will probably change - : `${window.location.origin}/course-authoring/library/${id}` + : `${getPath(getConfig().PUBLIC_PATH)}library/${id}` ); return ( From d853f29ffe45b37019283881d366dde2511333bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 6 Jun 2024 16:16:27 -0300 Subject: [PATCH 030/106] refactor: rename .ts files to .js --- src/library-authoring/LibraryComponents.jsx | 2 +- src/library-authoring/data/{api.ts => api.js} | 0 src/library-authoring/data/{apiHook.ts => apiHook.js} | 0 src/library-authoring/data/{types.ts => types.js} | 0 src/library-authoring/{index.ts => index.js} | 0 src/library-authoring/{messages.ts => messages.js} | 0 src/search-modal/{index.ts => index.js} | 0 7 files changed, 1 insertion(+), 1 deletion(-) rename src/library-authoring/data/{api.ts => api.js} (100%) rename src/library-authoring/data/{apiHook.ts => apiHook.js} (100%) rename src/library-authoring/data/{types.ts => types.js} (100%) rename src/library-authoring/{index.ts => index.js} (100%) rename src/library-authoring/{messages.ts => messages.js} (100%) rename src/search-modal/{index.ts => index.js} (100%) diff --git a/src/library-authoring/LibraryComponents.jsx b/src/library-authoring/LibraryComponents.jsx index a48fbfe645..3ce00c4fda 100644 --- a/src/library-authoring/LibraryComponents.jsx +++ b/src/library-authoring/LibraryComponents.jsx @@ -16,7 +16,7 @@ import messages from './messages'; * }>} */ const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { - const { componentCount, collectionCount } = useLibraryComponentCount(libraryId, searchKeywords); + const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); if (componentCount === 0) { return searchKeywords === '' ? : ; diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.js similarity index 100% rename from src/library-authoring/data/api.ts rename to src/library-authoring/data/api.js diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.js similarity index 100% rename from src/library-authoring/data/apiHook.ts rename to src/library-authoring/data/apiHook.js diff --git a/src/library-authoring/data/types.ts b/src/library-authoring/data/types.js similarity index 100% rename from src/library-authoring/data/types.ts rename to src/library-authoring/data/types.js diff --git a/src/library-authoring/index.ts b/src/library-authoring/index.js similarity index 100% rename from src/library-authoring/index.ts rename to src/library-authoring/index.js diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.js similarity index 100% rename from src/library-authoring/messages.ts rename to src/library-authoring/messages.js diff --git a/src/search-modal/index.ts b/src/search-modal/index.js similarity index 100% rename from src/search-modal/index.ts rename to src/search-modal/index.js From 567dcb48f08d7bb43ea389bda9ee3dd9c1c22966 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Thu, 6 Jun 2024 23:56:56 +0300 Subject: [PATCH 031/106] feat: Add function to construct Lib Auth MFE URL --- src/search-modal/SearchResult.jsx | 3 +- src/studio-home/StudioHome.jsx | 3 +- src/studio-home/StudioHome.test.jsx | 6 ++-- src/studio-home/__mocks__/studioHomeMock.js | 2 +- .../tabs-section/libraries-v2-tab/index.jsx | 3 +- src/utils.js | 24 +++++++++++++++ src/utils.test.js | 29 ++++++++++++++++++- 7 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/search-modal/SearchResult.jsx b/src/search-modal/SearchResult.jsx index 634c12d016..893452fe63 100644 --- a/src/search-modal/SearchResult.jsx +++ b/src/search-modal/SearchResult.jsx @@ -16,6 +16,7 @@ import { import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; +import { constructLibraryAuthoringURL } from '../utils'; import { COMPONENT_TYPE_ICON_MAP, TYPE_ICONS_MAP } from '../course-unit/constants'; import { getStudioHomeData } from '../studio-home/data/selectors'; import { useSearchContext } from './manager/SearchManager'; @@ -41,7 +42,7 @@ function getItemIcon(blockType) { */ function getLibraryHitUrl(hit, libraryAuthoringMfeUrl) { const { contextKey } = hit; - return `${libraryAuthoringMfeUrl}library/${contextKey}`; + return constructLibraryAuthoringURL(libraryAuthoringMfeUrl, contextKey); } /** diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index fa1affd2e1..2e59b78ed2 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -12,6 +12,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { StudioFooter } from '@edx/frontend-component-footer'; import { getConfig, getPath } from '@edx/frontend-platform'; +import { constructLibraryAuthoringURL } from '../utils'; import Loading from '../generic/Loading'; import InternetConnectionAlert from '../generic/internet-connection-alert'; import Header from '../header'; @@ -84,7 +85,7 @@ const StudioHome = ({ intl }) => { let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; if (isMixedOrV2LibrariesMode(libMode)) { libraryHref = libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe - ? `${libraryAuthoringMfeUrl}create` + ? constructLibraryAuthoringURL(libraryAuthoringMfeUrl, 'create') : `${getPath(getConfig().PUBLIC_PATH)}library/create`; } diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx index 4f11d3d4c1..a5799085dd 100644 --- a/src/studio-home/StudioHome.test.jsx +++ b/src/studio-home/StudioHome.test.jsx @@ -14,7 +14,7 @@ import MockAdapter from 'axios-mock-adapter'; import initializeStore from '../store'; import { RequestStatus } from '../data/constants'; import { COURSE_CREATOR_STATES } from '../constants'; -import { executeThunk } from '../utils'; +import { executeThunk, constructLibraryAuthoringURL } from '../utils'; import { studioHomeMock } from './__mocks__'; import { getStudioHomeApiUrl } from './data/api'; import { fetchStudioHomeData } from './data/thunks'; @@ -191,7 +191,9 @@ describe('', async () => { const { getByTestId } = render(); const createNewLibraryButton = getByTestId('new-library-button'); - expect(createNewLibraryButton.getAttribute('href')).toBe(`${libraryAuthoringMfeUrl}/create`); + expect(createNewLibraryButton.getAttribute('href')).toBe( + `${constructLibraryAuthoringURL(libraryAuthoringMfeUrl, 'create')}`, + ); }); }); diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index 4f66cc116f..5385201e52 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -62,7 +62,7 @@ module.exports = { }, ], librariesEnabled: true, - libraryAuthoringMfeUrl: 'http://localhost:3001/', + libraryAuthoringMfeUrl: 'http://localhost:3001', optimizationEnabled: false, redirectToLibraryAuthoringMfe: false, requestCourseCreatorUrl: '/request_course_creator', diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx index ae0c1aecaf..317da1a93e 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx @@ -4,6 +4,7 @@ import { Icon, Row, Pagination } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { getConfig, getPath } from '@edx/frontend-platform'; +import { constructLibraryAuthoringURL } from '../../../utils'; import useListStudioHomeV2Libraries from '../../data/apiHooks'; import { LoadingSpinner } from '../../../generic/Loading'; import AlertMessage from '../../../generic/alert-message'; @@ -38,7 +39,7 @@ const LibrariesV2Tab = ({ const libURL = (id) => ( libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe - ? `${libraryAuthoringMfeUrl}library/${id}` + ? constructLibraryAuthoringURL(libraryAuthoringMfeUrl, `library/${id}`) : `${getPath(getConfig().PUBLIC_PATH)}library/${id}` ); diff --git a/src/utils.js b/src/utils.js index d4bc8f6ff3..2abb63e5be 100644 --- a/src/utils.js +++ b/src/utils.js @@ -301,3 +301,27 @@ export const getFileSizeToClosestByte = (fileSize) => { const fileSizeFixedDecimal = Number.parseFloat(size).toFixed(2); return `${fileSizeFixedDecimal} ${units[divides]}`; }; + +/** + * Constructs library authoring MFE URL with correct slashes + * @param {string} libraryAuthoringMfeUrl - the base library authoring MFE url + * @param {string} path - the library authoring MFE url path + * @returns {string} - the correct internal route path + */ +export const constructLibraryAuthoringURL = (libraryAuthoringMfeUrl, path) => { + // Remove '/' at the beginning of path if any + const trimmedPath = path.startsWith('/') + ? path.slice(1, path.length) + : path; + + let constructedUrl = libraryAuthoringMfeUrl; + // Remove trailing `/` from base if found + if (libraryAuthoringMfeUrl.endsWith('/')) { + constructedUrl = constructedUrl.slice(0, -1); + } + + // Add the `/` and path to url + constructedUrl = `${constructedUrl}/${trimmedPath}`; + + return constructedUrl; +}; diff --git a/src/utils.test.js b/src/utils.test.js index e4aada849f..a5b12d6c37 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -1,6 +1,6 @@ import { getConfig, getPath } from '@edx/frontend-platform'; -import { getFileSizeToClosestByte, createCorrectInternalRoute } from './utils'; +import { getFileSizeToClosestByte, createCorrectInternalRoute, constructLibraryAuthoringURL } from './utils'; jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn(), @@ -78,3 +78,30 @@ describe('FilesAndUploads utils', () => { }); }); }); + +describe('constructLibraryAuthoringURL', () => { + it('should construct URL given no trailing `/` in base and no starting `/` in path', () => { + const libraryAuthoringMfeUrl = 'http://localhost:3001'; + const path = 'example'; + const constructedURL = constructLibraryAuthoringURL(libraryAuthoringMfeUrl, path); + expect(constructedURL).toEqual('http://localhost:3001/example'); + }); + it('should construct URL given a trailing `/` in base and no starting `/` in path', () => { + const libraryAuthoringMfeUrl = 'http://localhost:3001/'; + const path = 'example'; + const constructedURL = constructLibraryAuthoringURL(libraryAuthoringMfeUrl, path); + expect(constructedURL).toEqual('http://localhost:3001/example'); + }); + it('should construct URL with no trailing `/` in base and a starting `/` in path', () => { + const libraryAuthoringMfeUrl = 'http://localhost:3001'; + const path = '/example'; + const constructedURL = constructLibraryAuthoringURL(libraryAuthoringMfeUrl, path); + expect(constructedURL).toEqual('http://localhost:3001/example'); + }); + it('should construct URL with a trailing `/` in base and a starting `/` in path', () => { + const libraryAuthoringMfeUrl = 'http://localhost:3001/'; + const path = '/example'; + const constructedURL = constructLibraryAuthoringURL(libraryAuthoringMfeUrl, path); + expect(constructedURL).toEqual('http://localhost:3001/example'); + }); +}); From 094086e05f948cecb3437f89026a959ce0ff45a7 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Fri, 7 Jun 2024 00:19:47 +0300 Subject: [PATCH 032/106] feat: Make URL /library-v1 when referencing legacy --- src/studio-home/tabs-section/index.jsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx index 5f3085bb9c..75a3ae12ba 100644 --- a/src/studio-home/tabs-section/index.jsx +++ b/src/studio-home/tabs-section/index.jsx @@ -54,11 +54,9 @@ const TabsSection = ({ // This is needed to handle navigating using the back/forward buttons in the browser useEffect(() => { - // Handle special case when navigating directly to /libraries-v1 or /libraries in `v1 only` mode + // Handle special case when navigating directly to /libraries-v1 // we need to call dispatch to fetch library data - if (pathname.includes('/libraries-v1') - || (isMixedOrV1LibrariesMode(libMode) && pathname.includes('/libraries')) - ) { + if (pathname.includes('/libraries-v1')) { dispatch(fetchLibraryData()); } setTabKey(initTabKeyState(pathname)); @@ -175,11 +173,7 @@ const TabsSection = ({ navigate('/home'); } else if (tab === TABS_LIST.legacyLibraries) { dispatch(fetchLibraryData()); - navigate( - libMode === 'v1 only' - ? '/libraries' - : '/libraries-v1', - ); + navigate('/libraries-v1'); } else if (tab === TABS_LIST.libraries) { navigate('/libraries'); } else if (tab === TABS_LIST.taxonomies) { From a95c99000009c1c8b655de17d67495003372002b Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Fri, 7 Jun 2024 01:24:37 +0300 Subject: [PATCH 033/106] fix: Add missing part of path --- src/search-modal/SearchResult.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search-modal/SearchResult.jsx b/src/search-modal/SearchResult.jsx index 893452fe63..939043ef73 100644 --- a/src/search-modal/SearchResult.jsx +++ b/src/search-modal/SearchResult.jsx @@ -42,7 +42,7 @@ function getItemIcon(blockType) { */ function getLibraryHitUrl(hit, libraryAuthoringMfeUrl) { const { contextKey } = hit; - return constructLibraryAuthoringURL(libraryAuthoringMfeUrl, contextKey); + return constructLibraryAuthoringURL(libraryAuthoringMfeUrl, `library/${contextKey}`); } /** From e3ebc55532bae01884cd4c556653ad43d2adf90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 6 Jun 2024 19:56:20 -0300 Subject: [PATCH 034/106] fix: type and lint errors --- src/library-authoring/data/api.js | 21 ++++++++++++++---- src/library-authoring/data/apiHook.js | 31 +++++++++++---------------- src/library-authoring/data/types.js | 17 --------------- src/library-authoring/data/types.mjs | 18 ++++++++++++++++ src/search-modal/SearchResult.jsx | 2 +- 5 files changed, 49 insertions(+), 40 deletions(-) delete mode 100644 src/library-authoring/data/types.js create mode 100644 src/library-authoring/data/types.mjs diff --git a/src/library-authoring/data/api.js b/src/library-authoring/data/api.js index ff0dd3dec5..c97760b868 100644 --- a/src/library-authoring/data/api.js +++ b/src/library-authoring/data/api.js @@ -1,12 +1,25 @@ // @ts-check -import type { ContentLibrary } from './types'; import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -const getApiBaseUrl = (): string => getConfig().STUDIO_BASE_URL; -const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; +/** + * Get the URL for the content library API. + * @param {string} libraryId - The ID of the library to fetch. + */ +const getContentLibraryApiUrl = (libraryId) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; + +/** + * Fetch a content library by its ID. + * @param {string} [libraryId] - The ID of the library to fetch. + * @returns {Promise} + */ +/* eslint-disable import/prefer-default-export */ +export async function getContentLibrary(libraryId) { + if (!libraryId) { + throw new Error('libraryId is required'); + } -export async function getContentLibrary(libraryId: string): Promise { const { data } = await getAuthenticatedHttpClient().get(getContentLibraryApiUrl(libraryId)); return camelCaseObject(data); } diff --git a/src/library-authoring/data/apiHook.js b/src/library-authoring/data/apiHook.js index 53509fbe3b..8f11e49a4e 100644 --- a/src/library-authoring/data/apiHook.js +++ b/src/library-authoring/data/apiHook.js @@ -1,5 +1,5 @@ // @ts-check -import React, { useEffect } from 'react'; +import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { MeiliSearch } from 'meilisearch'; @@ -8,24 +8,21 @@ import { getContentLibrary } from './api'; /** * Hook to fetch a content library by its ID. + * @param {string} [libraryId] - The ID of the library to fetch. */ -export const useContentLibrary = (libraryId?: string) => { - if (!libraryId) { - return { - data: undefined, - error: 'No library ID provided', - isLoading: false, - } - } - - return useQuery({ +export const useContentLibrary = (libraryId) => ( + useQuery({ queryKey: ['contentLibrary', libraryId], queryFn: () => getContentLibrary(libraryId), - }); -}; - + }) +); -export const useLibraryComponentCount = (libraryId: string, searchKeywords: string) => { +/** + * Hook to fetch the count of components and collections in a library. + * @param {string} libraryId - The ID of the library to fetch. + * @param {string} searchKeywords - Keywords to search for. + */ +export const useLibraryComponentCount = (libraryId, searchKeywords) => { // Meilisearch code to get Collection and Component counts const { data: connectionDetails } = useContentSearchConnection(); @@ -52,6 +49,4 @@ export const useLibraryComponentCount = (libraryId: string, searchKeywords: stri componentCount, collectionCount, }; -} - - +}; diff --git a/src/library-authoring/data/types.js b/src/library-authoring/data/types.js deleted file mode 100644 index 1af41a8b1f..0000000000 --- a/src/library-authoring/data/types.js +++ /dev/null @@ -1,17 +0,0 @@ -export type ContentLibrary = { - id: string; - type: string; - org: string; - slug: string; - title: string; - description: string; - numBlocks: number; - version: number; - lastPublished: Date | null; - allowLti: boolean; - allowPublicLearning: boolean; - allowPublicRead: boolean; - hasUnpublishedChanges: boolean; - hasUnpublishedDeletes: boolean; - license: string; -} diff --git a/src/library-authoring/data/types.mjs b/src/library-authoring/data/types.mjs new file mode 100644 index 0000000000..34486525d7 --- /dev/null +++ b/src/library-authoring/data/types.mjs @@ -0,0 +1,18 @@ +/** + * @typedef {Object} ContentLibrary + * @property {string} id + * @property {string} type + * @property {string} org + * @property {string} slug + * @property {string} title + * @property {string} description + * @property {number} numBlocks + * @property {number} version + * @property {Date | null} lastPublished + * @property {boolean} allowLti + * @property {boolean} allowPublicLearning + * @property {boolean} allowPublicRead + * @property {boolean} hasUnpublishedChanges + * @property {boolean} hasUnpublishedDeletes + * @property {string} license + */ diff --git a/src/search-modal/SearchResult.jsx b/src/search-modal/SearchResult.jsx index 0d763b82ac..051aa3c852 100644 --- a/src/search-modal/SearchResult.jsx +++ b/src/search-modal/SearchResult.jsx @@ -156,7 +156,7 @@ const SearchResult = ({ hit }) => { return `/${urlSuffix}`; } - // No context URL for this hit (e.g. a library without library authoring mfe) + // istanbul ignore next - This case should never be reached return undefined; }, [libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe, hit]); From 8ed168d3902aa4af0401fb73d4e641da4bad508e Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Fri, 7 Jun 2024 02:35:12 +0300 Subject: [PATCH 035/106] fix: Issue with destinationUrl --- src/studio-home/card-item/index.jsx | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.jsx index 1ee4045390..3fd85ffb7e 100644 --- a/src/studio-home/card-item/index.jsx +++ b/src/studio-home/card-item/index.jsx @@ -35,23 +35,14 @@ const CardItem = ({ courseCreatorStatus, rerunCreatorStatus, } = useSelector(getStudioHomeData); - const destinationUrl = () => { - if (isLibraries) { - // This case is for the library authoring MFE - if (url.startsWith('http')) { - return new URL(url); - } - - if (url.includes(getPath(getConfig().PUBLIC_PATH))) { - // Redirection to the placeholder is done in the MFE rather than - // through the backend i.e. redirection from cms, because this this will probably change, - // hence why we use the MFE's origin - return new URL(url, window.location.origin); - } - } - - return new URL(url, getConfig().STUDIO_BASE_URL); - }; + const destinationUrl = () => ( + isLibraries && url.includes(getPath(getConfig().PUBLIC_PATH)) + // Redirection to the placeholder is done in the MFE rather than + // through the backend i.e. redirection from cms, because this this will probably change, + // hence why we use the MFE's origin + ? new URL(url, window.location.origin) + : new URL(url, getConfig().STUDIO_BASE_URL) + ); const subtitle = isLibraries ? `${org} / ${number}` : `${org} / ${number} / ${run}`; const readOnlyItem = !(lmsLink || rerunLink || url); From beda37f829cbdd43204144ecd2f0217d09d8f669 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Fri, 7 Jun 2024 02:35:43 +0300 Subject: [PATCH 036/106] test: Add checks for Tab.eventKey in tests --- src/studio-home/tabs-section/TabsSection.test.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx index fce235f7b7..90f47d5e85 100644 --- a/src/studio-home/tabs-section/TabsSection.test.jsx +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -149,6 +149,10 @@ describe('', () => { await executeThunk(fetchStudioHomeData(), store.dispatch); expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + const librariesTab = screen.getByRole('tab', { name: tabMessages.librariesTabTitle.defaultMessage }); + expect(librariesTab).toBeInTheDocument(); + // Check Tab.eventKey + expect(librariesTab).toHaveAttribute('data-rb-event-key', 'legacyLibraries'); expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).not.toBeInTheDocument(); }); @@ -166,6 +170,10 @@ describe('', () => { await executeThunk(fetchStudioHomeData(), store.dispatch); expect(screen.getByText(tabMessages.librariesTabTitle.defaultMessage)).toBeInTheDocument(); + const librariesTab = screen.getByRole('tab', { name: tabMessages.librariesTabTitle.defaultMessage }); + expect(librariesTab).toBeInTheDocument(); + // Check Tab.eventKey + expect(librariesTab).toHaveAttribute('data-rb-event-key', 'libraries'); expect(screen.queryByText(tabMessages.legacyLibrariesTabTitle.defaultMessage)).not.toBeInTheDocument(); }); From 2e3fa438afe5dec1753174f6e8db9c68974793e6 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Fri, 7 Jun 2024 03:28:32 +0300 Subject: [PATCH 037/106] fix: Revert card item url changes to keep simple --- src/studio-home/StudioHome.jsx | 5 ++++- src/studio-home/card-item/index.jsx | 12 ++---------- .../tabs-section/libraries-v2-tab/index.jsx | 5 ++++- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 2e59b78ed2..4953c6f3ae 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -86,7 +86,10 @@ const StudioHome = ({ intl }) => { if (isMixedOrV2LibrariesMode(libMode)) { libraryHref = libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe ? constructLibraryAuthoringURL(libraryAuthoringMfeUrl, 'create') - : `${getPath(getConfig().PUBLIC_PATH)}library/create`; + // Redirection to the placeholder is done in the MFE rather than + // through the backend i.e. redirection from cms, because this this will probably change, + // hence why we use the MFE's origin + : `${window.location.origin}${getPath(getConfig().PUBLIC_PATH)}library/create`; } headerButtons.push( diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.jsx index 3fd85ffb7e..0b06e66ac3 100644 --- a/src/studio-home/card-item/index.jsx +++ b/src/studio-home/card-item/index.jsx @@ -10,7 +10,7 @@ import { } from '@openedx/paragon'; import { MoreHoriz } from '@openedx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { getConfig, getPath } from '@edx/frontend-platform'; +import { getConfig } from '@edx/frontend-platform'; import { COURSE_CREATOR_STATES } from '../../constants'; import { getStudioHomeData } from '../data/selectors'; @@ -35,15 +35,7 @@ const CardItem = ({ courseCreatorStatus, rerunCreatorStatus, } = useSelector(getStudioHomeData); - const destinationUrl = () => ( - isLibraries && url.includes(getPath(getConfig().PUBLIC_PATH)) - // Redirection to the placeholder is done in the MFE rather than - // through the backend i.e. redirection from cms, because this this will probably change, - // hence why we use the MFE's origin - ? new URL(url, window.location.origin) - : new URL(url, getConfig().STUDIO_BASE_URL) - ); - + const destinationUrl = () => new URL(url, getConfig().STUDIO_BASE_URL); const subtitle = isLibraries ? `${org} / ${number}` : `${org} / ${number} / ${run}`; const readOnlyItem = !(lmsLink || rerunLink || url); const showActions = !(readOnlyItem || isLibraries); diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx index 317da1a93e..c3b58df554 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.jsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.jsx @@ -40,7 +40,10 @@ const LibrariesV2Tab = ({ const libURL = (id) => ( libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe ? constructLibraryAuthoringURL(libraryAuthoringMfeUrl, `library/${id}`) - : `${getPath(getConfig().PUBLIC_PATH)}library/${id}` + // Redirection to the placeholder is done in the MFE rather than + // through the backend i.e. redirection from cms, because this this will probably change, + // hence why we use the MFE's origin + : `${window.location.origin}${getPath(getConfig().PUBLIC_PATH)}library/${id}` ); return ( From 72edfacbc8439ca1ad81830e27177b996c32fc86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 7 Jun 2024 15:59:09 -0300 Subject: [PATCH 038/106] fix: add tests --- src/library-authoring/CreateLibrary.jsx | 4 +- src/library-authoring/EmptyStates.jsx | 1 + .../LibraryAuthoringPage.jsx | 10 +- .../LibraryAuthoringPage.test.jsx | 237 ++++++++++++++++++ src/library-authoring/data/api.js | 3 +- src/library-authoring/messages.js | 5 + 6 files changed, 252 insertions(+), 8 deletions(-) create mode 100644 src/library-authoring/LibraryAuthoringPage.test.jsx diff --git a/src/library-authoring/CreateLibrary.jsx b/src/library-authoring/CreateLibrary.jsx index b75c23a4c0..738e9fb769 100644 --- a/src/library-authoring/CreateLibrary.jsx +++ b/src/library-authoring/CreateLibrary.jsx @@ -9,9 +9,7 @@ import SubHeader from '../generic/sub-header/SubHeader'; import messages from './messages'; -/** - * @type {React.FC} - */ +/* istanbul ignore next This is only a placeholder component */ const CreateLibrary = () => ( <>
diff --git a/src/library-authoring/EmptyStates.jsx b/src/library-authoring/EmptyStates.jsx index d7b718c71d..54e3b8019d 100644 --- a/src/library-authoring/EmptyStates.jsx +++ b/src/library-authoring/EmptyStates.jsx @@ -1,3 +1,4 @@ +// @ts-check import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { diff --git a/src/library-authoring/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.jsx index 9c075ada88..0536e4faab 100644 --- a/src/library-authoring/LibraryAuthoringPage.jsx +++ b/src/library-authoring/LibraryAuthoringPage.jsx @@ -2,7 +2,7 @@ /* eslint-disable react/prop-types */ import React, { useEffect } from 'react'; import { StudioFooter } from '@edx/frontend-component-footer'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Container, Icon, IconButton, SearchField, Tab, Tabs, } from '@openedx/paragon'; @@ -30,7 +30,12 @@ const TAB_LIST = { const SubHeaderTitle = ({ title }) => ( <> {title} - {}} className="mr-2" /> + } + className="mr-2" + /> ); @@ -90,7 +95,6 @@ const LibraryAuthoringPage = () => { setSearchKeywords(value)} onChange={(value) => setSearchKeywords(value)} className="w-50" /> diff --git a/src/library-authoring/LibraryAuthoringPage.test.jsx b/src/library-authoring/LibraryAuthoringPage.test.jsx new file mode 100644 index 0000000000..59f510f2fd --- /dev/null +++ b/src/library-authoring/LibraryAuthoringPage.test.jsx @@ -0,0 +1,237 @@ +// @ts-check +import React from 'react'; +import MockAdapter from 'axios-mock-adapter'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import fetchMock from 'fetch-mock-jest'; + +import initializeStore from '../store'; +import { getContentSearchConfigUrl } from '../search-modal/data/api'; +import mockResult from '../search-modal/__mocks__/search-result.json'; +import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json'; +import LibraryAuthoringPage from './LibraryAuthoringPage'; +import { getContentLibraryApiUrl } from './data/api'; + +let store; +const mockUseParams = jest.fn(); +let axiosMock; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts + useParams: () => mockUseParams(), +})); + +const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const returnEmptyResult = (_url, req) => { + const requestData = JSON.parse(req.body?.toString() ?? ''); + const query = requestData?.queries[0]?.q ?? ''; + // We have to replace the query (search keywords) in the mock results with the actual query, + // because otherwise we may have an inconsistent state that causes more queries and unexpected results. + mockEmptyResult.results[0].query = query; + // And fake the required '_formatted' fields; it contains the highlighting ... around matched words + // eslint-disable-next-line no-underscore-dangle, no-param-reassign + mockEmptyResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); + return mockEmptyResult; +}; + +const libraryData = { + id: 'lib:org1:lib1', + type: 'complex', + org: 'org1', + slug: 'lib1', + title: 'lib1', + description: 'lib1', + numBlocks: 2, + version: 0, + lastPublished: null, + allowLti: false, + allowPublic_learning: false, + allowPublic_read: false, + hasUnpublished_changes: true, + hasUnpublished_deletes: false, + license: '', +}; + +const RootWrapper = () => ( + + + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + mockUseParams.mockReturnValue({ libraryId: '1' }); + + // The API method to get the Meilisearch connection details uses Axios: + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(getContentSearchConfigUrl()).reply(200, { + url: 'http://mock.meilisearch.local', + index_name: 'studio', + api_key: 'test-key', + }); + // + // The Meilisearch client-side API uses fetch, not Axios. + fetchMock.post(searchEndpoint, (_url, req) => { + const requestData = JSON.parse(req.body?.toString() ?? ''); + const query = requestData?.queries[0]?.q ?? ''; + // We have to replace the query (search keywords) in the mock results with the actual query, + // because otherwise Instantsearch will update the UI and change the query, + // leading to unexpected results in the test cases. + mockResult.results[0].query = query; + // And fake the required '_formatted' fields; it contains the highlighting ... around matched words + // eslint-disable-next-line no-underscore-dangle, no-param-reassign + mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); + return mockResult; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + axiosMock.restore(); + fetchMock.mockReset(); + queryClient.clear(); + }); + + it('shows the spinner before the query is complete', () => { + mockUseParams.mockReturnValue({ libraryId: '1' }); + // @ts-ignore Use unresolved promise to keep the Loading visible + axiosMock.onGet(getContentLibraryApiUrl('1')).reply(() => new Promise()); + const { getByRole } = render(); + const spinner = getByRole('status'); + expect(spinner.textContent).toEqual('Loading...'); + }); + + it('shows an error component if no library returned', async () => { + mockUseParams.mockReturnValue({ libraryId: 'invalid' }); + axiosMock.onGet(getContentLibraryApiUrl('invalid')).reply(400); + + const { findByTestId } = render(); + + expect(await findByTestId('notFoundAlert')).toBeInTheDocument(); + }); + + it('shows an error component if no library param', async () => { + mockUseParams.mockReturnValue({ libraryId: '' }); + + const { findByTestId } = render(); + + expect(await findByTestId('notFoundAlert')).toBeInTheDocument(); + }); + + it('show library data', async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + + const { + getByRole, getByText, queryByText, + } = render(); + + // Ensure the search endpoint is called + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + + expect(getByText('Content library')).toBeInTheDocument(); + expect(getByText(libraryData.title)).toBeInTheDocument(); + + expect(queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument(); + + expect(getByText('Recently Modified')).toBeInTheDocument(); + expect(getByText('Collections (0)')).toBeInTheDocument(); + expect(getByText('Components (6)')).toBeInTheDocument(); + expect(getByText('There are 6 components in this library')).toBeInTheDocument(); + + // Navigate to the components tab + fireEvent.click(getByRole('tab', { name: 'Components' })); + expect(queryByText('Recently Modified')).not.toBeInTheDocument(); + expect(queryByText('Collections (0)')).not.toBeInTheDocument(); + expect(queryByText('Components (6)')).not.toBeInTheDocument(); + expect(getByText('There are 6 components in this library')).toBeInTheDocument(); + + // Navigate to the collections tab + fireEvent.click(getByRole('tab', { name: 'Collections' })); + expect(queryByText('Recently Modified')).not.toBeInTheDocument(); + expect(queryByText('Collections (0)')).not.toBeInTheDocument(); + expect(queryByText('Components (6)')).not.toBeInTheDocument(); + expect(queryByText('There are 6 components in this library')).not.toBeInTheDocument(); + expect(getByText('Coming soon!')).toBeInTheDocument(); + + // Go back to Home tab + // This step is necessary to avoid the url change leak to other tests + fireEvent.click(getByRole('tab', { name: 'Home' })); + expect(getByText('Recently Modified')).toBeInTheDocument(); + expect(getByText('Collections (0)')).toBeInTheDocument(); + expect(getByText('Components (6)')).toBeInTheDocument(); + expect(getByText('There are 6 components in this library')).toBeInTheDocument(); + }); + + it('show library without components', async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); + + const { findByText, getByText } = render(); + + expect(await findByText('Content library')).toBeInTheDocument(); + expect(await findByText(libraryData.title)).toBeInTheDocument(); + + // Ensure the search endpoint is called + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + + expect(getByText('You have not added any content to this library yet.')).toBeInTheDocument(); + }); + + it('show library without search results', async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); + + const { findByText, getByRole, getByText } = render(); + + expect(await findByText('Content library')).toBeInTheDocument(); + expect(await findByText(libraryData.title)).toBeInTheDocument(); + + // Ensure the search endpoint is called + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + + fireEvent.change(getByRole('searchbox'), { target: { value: 'noresults' } }); + + // Ensure the search endpoint is called again + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + + expect(getByText('No matching components found in this library.')).toBeInTheDocument(); + + // Navigate to the components tab + fireEvent.click(getByRole('tab', { name: 'Components' })); + expect(getByText('No matching components found in this library.')).toBeInTheDocument(); + + // Go back to Home tab + // This step is necessary to avoid the url change leak to other tests + fireEvent.click(getByRole('tab', { name: 'Home' })); + }); +}); diff --git a/src/library-authoring/data/api.js b/src/library-authoring/data/api.js index c97760b868..9fae35d947 100644 --- a/src/library-authoring/data/api.js +++ b/src/library-authoring/data/api.js @@ -7,14 +7,13 @@ const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; * Get the URL for the content library API. * @param {string} libraryId - The ID of the library to fetch. */ -const getContentLibraryApiUrl = (libraryId) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; +export const getContentLibraryApiUrl = (libraryId) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; /** * Fetch a content library by its ID. * @param {string} [libraryId] - The ID of the library to fetch. * @returns {Promise} */ -/* eslint-disable import/prefer-default-export */ export async function getContentLibrary(libraryId) { if (!libraryId) { throw new Error('libraryId is required'); diff --git a/src/library-authoring/messages.js b/src/library-authoring/messages.js index 6a09703b64..5be2437a98 100644 --- a/src/library-authoring/messages.js +++ b/src/library-authoring/messages.js @@ -6,6 +6,11 @@ const messages = defineMessages({ defaultMessage: 'Content library', description: 'The page heading for the library page.', }, + headingInfoAlt: { + id: 'course-authoring.library-authoring.heading-info-alt', + defaultMessage: 'Info', + description: 'Alt text for the info icon next to the page heading.', + }, searchPlaceholder: { id: 'course-authoring.library-authoring.search', defaultMessage: 'Search...', From 91443e9d75baa925f18d6461647dfbf75f362a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 7 Jun 2024 17:13:06 -0300 Subject: [PATCH 039/106] fix: removing unused file --- src/studio-home/__mocks__/studioHomeMock.js | 2 +- .../tabs-section/LibraryV2Placeholder.jsx | 36 ------------------- webpack.dev-tutor.config.js | 0 3 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 src/studio-home/tabs-section/LibraryV2Placeholder.jsx create mode 100755 webpack.dev-tutor.config.js diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index 4f66cc116f..5385201e52 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -62,7 +62,7 @@ module.exports = { }, ], librariesEnabled: true, - libraryAuthoringMfeUrl: 'http://localhost:3001/', + libraryAuthoringMfeUrl: 'http://localhost:3001', optimizationEnabled: false, redirectToLibraryAuthoringMfe: false, requestCourseCreatorUrl: '/request_course_creator', diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx deleted file mode 100644 index 6b13853a2c..0000000000 --- a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Container } from '@openedx/paragon'; -import { StudioFooter } from '@edx/frontend-component-footer'; -import { useIntl } from '@edx/frontend-platform/i18n'; - -import Header from '../../header'; -import SubHeader from '../../generic/sub-header/SubHeader'; -import messages from './messages'; - -/* istanbul ignore next */ -const LibraryV2Placeholder = () => { - const intl = useIntl(); - - return ( - <> -
- -
-
-
- -
-
-
-

{intl.formatMessage(messages.libraryV2PlaceholderBody)}

-
-
-
- - - ); -}; - -export default LibraryV2Placeholder; diff --git a/webpack.dev-tutor.config.js b/webpack.dev-tutor.config.js new file mode 100755 index 0000000000..e69de29bb2 From a29cf7e45038e10a2c5cbe313f1202f0be8c0a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 7 Jun 2024 17:25:36 -0300 Subject: [PATCH 040/106] fix: add ts-check --- src/library-authoring/messages.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/library-authoring/messages.js b/src/library-authoring/messages.js index 5be2437a98..ff985aa62c 100644 --- a/src/library-authoring/messages.js +++ b/src/library-authoring/messages.js @@ -1,3 +1,4 @@ +// @ts-check import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ From 4deab763fc52676c3807ffe57bb128a50ecf5c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 7 Jun 2024 17:37:24 -0300 Subject: [PATCH 041/106] fix: removing deleted file references --- src/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.jsx b/src/index.jsx index 283d2544aa..bf7ee9c423 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -24,7 +24,6 @@ import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; import { StudioHome } from './studio-home'; -import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; From e8bca34a8656bcae26a6d5de45bc5b7b39c6b062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 7 Jun 2024 18:04:09 -0300 Subject: [PATCH 042/106] chore: trigger CI From 0078c0e7ea3bb156f1536a7ed5a9b95862cd248b Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 12 Jun 2024 20:43:02 -0500 Subject: [PATCH 043/106] feat: Initial structure of ComponentCard --- src/index.scss | 1 + .../LibraryAuthoringPage.jsx | 2 +- src/library-authoring/LibraryComponents.jsx | 35 ------- src/library-authoring/LibraryHome.jsx | 2 +- .../components/ComponentCard.jsx | 95 +++++++++++++++++++ .../components/ComponentCard.scss | 18 ++++ .../components/LibraryComponents.jsx | 53 +++++++++++ src/library-authoring/components/index.js | 3 + src/library-authoring/components/messages.js | 22 +++++ src/library-authoring/data/api.js | 1 + src/library-authoring/index.scss | 1 + src/library-authoring/messages.js | 5 - 12 files changed, 196 insertions(+), 42 deletions(-) delete mode 100644 src/library-authoring/LibraryComponents.jsx create mode 100644 src/library-authoring/components/ComponentCard.jsx create mode 100644 src/library-authoring/components/ComponentCard.scss create mode 100644 src/library-authoring/components/LibraryComponents.jsx create mode 100644 src/library-authoring/components/index.js create mode 100644 src/library-authoring/components/messages.js create mode 100644 src/library-authoring/index.scss diff --git a/src/index.scss b/src/index.scss index 912b40933f..381ca17082 100644 --- a/src/index.scss +++ b/src/index.scss @@ -29,6 +29,7 @@ @import "search-modal/SearchModal"; @import "certificates/scss/Certificates"; @import "group-configurations/GroupConfigurations"; +@import "library-authoring"; // To apply the glow effect to the selected Section/Subsection, in the Course Outline div.row:has(> div > div.highlight) { diff --git a/src/library-authoring/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.jsx index 0536e4faab..ebe23375b1 100644 --- a/src/library-authoring/LibraryAuthoringPage.jsx +++ b/src/library-authoring/LibraryAuthoringPage.jsx @@ -15,7 +15,7 @@ import Loading from '../generic/Loading'; import SubHeader from '../generic/sub-header/SubHeader'; import Header from '../header'; import NotFoundAlert from '../generic/NotFoundAlert'; -import LibraryComponents from './LibraryComponents'; +import LibraryComponents from './components/LibraryComponents'; import LibraryCollections from './LibraryCollections'; import LibraryHome from './LibraryHome'; import { useContentLibrary } from './data/apiHook'; diff --git a/src/library-authoring/LibraryComponents.jsx b/src/library-authoring/LibraryComponents.jsx deleted file mode 100644 index 3ce00c4fda..0000000000 --- a/src/library-authoring/LibraryComponents.jsx +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-check -/* eslint-disable react/prop-types */ -import React from 'react'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import { NoComponents, NoSearchResults } from './EmptyStates'; -import { useLibraryComponentCount } from './data/apiHook'; -import messages from './messages'; - -/** - * @type {React.FC<{ - * libraryId: string, - * filter: { - * searchKeywords: string, - * }, - * }>} - */ -const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { - const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); - - if (componentCount === 0) { - return searchKeywords === '' ? : ; - } - - return ( -
- -
- ); -}; - -export default LibraryComponents; diff --git a/src/library-authoring/LibraryHome.jsx b/src/library-authoring/LibraryHome.jsx index 4331a0b54d..919e98cd0a 100644 --- a/src/library-authoring/LibraryHome.jsx +++ b/src/library-authoring/LibraryHome.jsx @@ -8,7 +8,7 @@ import { import { NoComponents, NoSearchResults } from './EmptyStates'; import LibraryCollections from './LibraryCollections'; -import LibraryComponents from './LibraryComponents'; +import LibraryComponents from './components/LibraryComponents'; import { useLibraryComponentCount } from './data/apiHook'; import messages from './messages'; diff --git a/src/library-authoring/components/ComponentCard.jsx b/src/library-authoring/components/ComponentCard.jsx new file mode 100644 index 0000000000..512966e80e --- /dev/null +++ b/src/library-authoring/components/ComponentCard.jsx @@ -0,0 +1,95 @@ +import { + ActionRow, + Card, + Container, + Icon, + IconButton, + Dropdown, + Stack, +} from '@openedx/paragon'; +import PropTypes from 'prop-types'; +import { MoreVert } from '@openedx/paragon/icons'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import messages from './messages'; +import TagCount from '../../generic/tag-count'; + +const ComponentCardMenu = () => ( + + + + + + + + + + + + + + +); + +const ComponentCard = ({ icon, tagCount, blockType }) => ( + + + + } + actions={( + + + + )} + /> + + + + + + Type + + + +
+ Este es un titulo largo pero muuuuuyyyyyy largoooooo. +
+

+ This is a long. long. long descriprioooon + This is a long. long. long descriprioooon + This is a long. long. long descriprioooon + This is a long. long. long descriprioooon + This is a long. long. long descriprioooon + This is a long. long. long descriprioooon + This is a long. long. long descriprioooon + This is a long. long. long descriprioooon + This is a long. long. long descriprioooon +

+
+
+ +
+
+); + +ComponentCard.propTypes = { + icon: PropTypes.node.isRequired, + tagCount: PropTypes.number.isRequired, + blockType: PropTypes.string.isRequired, +}; + +export default ComponentCard; diff --git a/src/library-authoring/components/ComponentCard.scss b/src/library-authoring/components/ComponentCard.scss new file mode 100644 index 0000000000..e8725cf8d5 --- /dev/null +++ b/src/library-authoring/components/ComponentCard.scss @@ -0,0 +1,18 @@ +.library-component-card { + .library-component-header-html { + background-color: aqua; + } + + .library-component-card-description { + /* + Set overflow to description + I added '-webkit-box' to truncate multiple lines + */ + font-size: 18px; + display: -webkit-box; /* stylelint-disable-line value-no-vendor-prefix */ + -webkit-box-orient: vertical; + overflow: hidden; + max-height: 220px; + -webkit-line-clamp: 3; + } +} diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx new file mode 100644 index 0000000000..93096f6cc0 --- /dev/null +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -0,0 +1,53 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React from 'react'; + +import { TextFields } from '@openedx/paragon/icons'; +import { CardGrid } from '@openedx/paragon'; +import { NoComponents, NoSearchResults } from '../EmptyStates'; +import { useLibraryComponentCount } from '../data/apiHook'; +import ComponentsCard from './ComponentCard'; + +/** + * @type {React.FC<{ + * libraryId: string, + * filter: { + * searchKeywords: string, + * }, + * }>} + */ +const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { + const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); + + if (componentCount === 0) { + return searchKeywords === '' ? : ; + } + + return ( + + + + + + + + + + + + + + + + + ); +}; + +export default LibraryComponents; diff --git a/src/library-authoring/components/index.js b/src/library-authoring/components/index.js new file mode 100644 index 0000000000..1d3b1cd496 --- /dev/null +++ b/src/library-authoring/components/index.js @@ -0,0 +1,3 @@ +// @ts-check +// eslint-disable-next-line import/prefer-default-export +export { default as LibraryComponents } from './LibraryComponents'; diff --git a/src/library-authoring/components/messages.js b/src/library-authoring/components/messages.js new file mode 100644 index 0000000000..c1a318ef34 --- /dev/null +++ b/src/library-authoring/components/messages.js @@ -0,0 +1,22 @@ +// @ts-check +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + menuEdit: { + id: 'course-authoring.library-authoring.component.menu.edit', + defaultMessage: 'Edit', + description: 'Menu item for edit a component.', + }, + menuCopyToClipboard: { + id: 'course-authoring.library-authoring.component.menu.copy', + defaultMessage: 'Copy to Clipboard', + description: 'Menu item for copy a component.', + }, + menuAddToCollection: { + id: 'course-authoring.library-authoring.component.menu.add', + defaultMessage: 'Add to Collection', + description: 'Menu item for add a component to collection.', + }, +}); + +export default messages; diff --git a/src/library-authoring/data/api.js b/src/library-authoring/data/api.js index 9fae35d947..37a7a1fcfe 100644 --- a/src/library-authoring/data/api.js +++ b/src/library-authoring/data/api.js @@ -8,6 +8,7 @@ const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; * @param {string} libraryId - The ID of the library to fetch. */ export const getContentLibraryApiUrl = (libraryId) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; +// export const getLibraryBlocks = (libraryId) => `${getApiBaseUrl()}/` /** * Fetch a content library by its ID. diff --git a/src/library-authoring/index.scss b/src/library-authoring/index.scss new file mode 100644 index 0000000000..87c22f838e --- /dev/null +++ b/src/library-authoring/index.scss @@ -0,0 +1 @@ +@import "library-authoring/components/ComponentCard"; diff --git a/src/library-authoring/messages.js b/src/library-authoring/messages.js index ff985aa62c..c7a190e9ee 100644 --- a/src/library-authoring/messages.js +++ b/src/library-authoring/messages.js @@ -42,11 +42,6 @@ const messages = defineMessages({ defaultMessage: 'Coming soon!', description: 'Temp placeholder for the collections container. This will be replaced with the actual collection list.', }, - recentComponentsTempPlaceholder: { - id: 'course-authoring.library-authoring.recent-components-temp-placeholder', - defaultMessage: 'Recently modified components and collections will be displayed here.', - description: 'Temp placeholder for the recent components container. This will be replaced with the actual list.', - }, createLibrary: { id: 'course-authoring.library-authoring.create-library', defaultMessage: 'Create library', From 388c40e2ffd072d8c1fe770f777d8d7bea9bfe3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 13 Jun 2024 15:34:16 -0300 Subject: [PATCH 044/106] fix: fixes from review --- src/library-authoring/LibraryAuthoringPage.jsx | 1 + src/library-authoring/LibraryAuthoringPage.test.jsx | 2 +- webpack.dev-tutor.config.js | 0 3 files changed, 2 insertions(+), 1 deletion(-) delete mode 100755 webpack.dev-tutor.config.js diff --git a/src/library-authoring/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.jsx index 0536e4faab..1a70bfb491 100644 --- a/src/library-authoring/LibraryAuthoringPage.jsx +++ b/src/library-authoring/LibraryAuthoringPage.jsx @@ -96,6 +96,7 @@ const LibraryAuthoringPage = () => { value={searchKeywords} placeholder={intl.formatMessage(messages.searchPlaceholder)} onChange={(value) => setSearchKeywords(value)} + onSubmit={() => {}} className="w-50" /> ', () => { index_name: 'studio', api_key: 'test-key', }); - // + // The Meilisearch client-side API uses fetch, not Axios. fetchMock.post(searchEndpoint, (_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); diff --git a/webpack.dev-tutor.config.js b/webpack.dev-tutor.config.js deleted file mode 100755 index e69de29bb2..0000000000 From 7d6096e2cea78aa79497305058d019fda31e5a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 14 Jun 2024 10:41:18 -0300 Subject: [PATCH 045/106] fix: fix default parameter syntax --- src/search-modal/data/apiHooks.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/search-modal/data/apiHooks.js b/src/search-modal/data/apiHooks.js index 59a07c425a..eb64696fc8 100644 --- a/src/search-modal/data/apiHooks.js +++ b/src/search-modal/data/apiHooks.js @@ -42,12 +42,9 @@ export const useContentSearchResults = ({ indexName, extraFilter, searchKeywords, - blockTypesFilter, - tagsFilter, + blockTypesFilter = [], + tagsFilter = [], }) => { - blockTypesFilter ??= []; // eslint-disable-line no-param-reassign -- default value for optional parameter - tagsFilter ??= []; // eslint-disable-line no-param-reassign -- Default value for optional parameter - const query = useInfiniteQuery({ enabled: client !== undefined && indexName !== undefined, queryKey: [ From 19b293513f3cecc0676643c6091e6812d3178dfa Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 17 Jun 2024 12:35:35 -0500 Subject: [PATCH 046/106] feat: Connect TagCount, Colors and Icons with library components card --- .../components/ComponentCard.jsx | 93 ++++++++++--------- .../components/ComponentCard.scss | 43 ++++++++- .../components/LibraryComponents.jsx | 38 ++++---- src/library-authoring/constants.js | 18 ++++ src/library-authoring/data/apiHook.js | 26 ++++++ src/library-authoring/messages.js | 5 + src/library-authoring/utils.jsx | 7 ++ src/search-modal/SearchResult.jsx | 19 +--- src/search-modal/data/api.js | 12 ++- src/search-modal/utils.jsx | 17 ++++ 10 files changed, 199 insertions(+), 79 deletions(-) create mode 100644 src/library-authoring/constants.js create mode 100644 src/library-authoring/utils.jsx create mode 100644 src/search-modal/utils.jsx diff --git a/src/library-authoring/components/ComponentCard.jsx b/src/library-authoring/components/ComponentCard.jsx index 512966e80e..60bf90857e 100644 --- a/src/library-authoring/components/ComponentCard.jsx +++ b/src/library-authoring/components/ComponentCard.jsx @@ -12,6 +12,8 @@ import { MoreVert } from '@openedx/paragon/icons'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from './messages'; import TagCount from '../../generic/tag-count'; +import getItemIcon from '../../search-modal/utils'; +import getComponentColor from '../utils'; const ComponentCardMenu = () => ( @@ -42,52 +44,59 @@ const ComponentCardMenu = () => ( ); -const ComponentCard = ({ icon, tagCount, blockType }) => ( - - - - } - actions={( - - - - )} - /> - - - - - - Type +const ComponentCard = ({ + isLoading, + title, + description, + tagCount, + blockType, +}) => { + const componentIcon = getItemIcon(blockType); + + return ( + + + + } + actions={( + + + + )} + /> + + + + + + {blockType} + + - - -
- Este es un titulo largo pero muuuuuyyyyyy largoooooo. -
-

- This is a long. long. long descriprioooon - This is a long. long. long descriprioooon - This is a long. long. long descriprioooon - This is a long. long. long descriprioooon - This is a long. long. long descriprioooon - This is a long. long. long descriprioooon - This is a long. long. long descriprioooon - This is a long. long. long descriprioooon - This is a long. long. long descriprioooon -

-
-
+
+ {title} +
+

+ {description} +

+ + +
+
+ ); +}; - - -); +ComponentCard.defaultProps = { + isLoading: false, +}; ComponentCard.propTypes = { - icon: PropTypes.node.isRequired, + isLoading: PropTypes.bool, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, tagCount: PropTypes.number.isRequired, blockType: PropTypes.string.isRequired, }; diff --git a/src/library-authoring/components/ComponentCard.scss b/src/library-authoring/components/ComponentCard.scss index e8725cf8d5..74309cc972 100644 --- a/src/library-authoring/components/ComponentCard.scss +++ b/src/library-authoring/components/ComponentCard.scss @@ -1,6 +1,45 @@ .library-component-card { - .library-component-header-html { - background-color: aqua; + .library-component-header { + border-top-left-radius: .375rem; + border-top-right-radius: .375rem; + padding: 0 .5rem 0 1.25rem; + + .library-component-header-icon { + width: 2.3rem; + height: 2.3rem; + } + + .pgn__card-header-content { + margin-top: .55rem; + } + + .pgn__card-header-actions { + margin: .25rem 0 .25rem 1rem; + } + + &.bg-component { + background-color: #005C9E; + } + + &.bg-html { + background-color: #9747FF; + } + + &.bg-collection { + background-color: #FFCD29; + } + + &.bg-video { + background-color: #358F0A; + } + + &.bg-vertical { + background-color: #0B8E77; + } + + &.bg-other { + background-color: #666666; + } } .library-component-card-description { diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx index 93096f6cc0..f3603c4943 100644 --- a/src/library-authoring/components/LibraryComponents.jsx +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -2,11 +2,10 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import { TextFields } from '@openedx/paragon/icons'; import { CardGrid } from '@openedx/paragon'; import { NoComponents, NoSearchResults } from '../EmptyStates'; -import { useLibraryComponentCount } from '../data/apiHook'; -import ComponentsCard from './ComponentCard'; +import { useLibraryComponentCount, useLibraryComponents } from '../data/apiHook'; +import ComponentCard from './ComponentCard'; /** * @type {React.FC<{ @@ -18,6 +17,7 @@ import ComponentsCard from './ComponentCard'; */ const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); + const { hits, isFetching } = useLibraryComponents(libraryId, searchKeywords); if (componentCount === 0) { return searchKeywords === '' ? : ; @@ -31,21 +31,25 @@ const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { lg: 4, xl: 3, }} + hasEqualColumnHeights > - - - - - - - - - - - - - - + { isFetching ? + : hits.map((component) => { + let tagCount = 0; + if (component.tags) { + tagCount = component.tags.implicitCount || 0; + } + + return ( + + ); + })} + ); }; diff --git a/src/library-authoring/constants.js b/src/library-authoring/constants.js new file mode 100644 index 0000000000..83a9be8abd --- /dev/null +++ b/src/library-authoring/constants.js @@ -0,0 +1,18 @@ +import { COMPONENT_TYPES } from '../course-unit/constants'; + +const COMPONENT_TYPE_COLOR_MAP = { + [COMPONENT_TYPES.advanced]: 'bg-other', + [COMPONENT_TYPES.discussion]: 'bg-component', + [COMPONENT_TYPES.library]: 'bg-component', + [COMPONENT_TYPES.html]: 'bg-html', + [COMPONENT_TYPES.openassessment]: 'bg-component', + [COMPONENT_TYPES.problem]: 'bg-component', + [COMPONENT_TYPES.video]: 'bg-video', + [COMPONENT_TYPES.dragAndDrop]: 'bg-component', + vertical: 'bg-vertical', + sequential: 'bg-component', + chapter: 'bg-component', + collection: 'bg-collection', +}; + +export default COMPONENT_TYPE_COLOR_MAP; diff --git a/src/library-authoring/data/apiHook.js b/src/library-authoring/data/apiHook.js index 8f11e49a4e..0148e89e6e 100644 --- a/src/library-authoring/data/apiHook.js +++ b/src/library-authoring/data/apiHook.js @@ -17,6 +17,32 @@ export const useContentLibrary = (libraryId) => ( }) ); +/** + * Hook to fetch components in a library. + * @param {string} libraryId - The ID of the library to fetch. + * @param {string} searchKeywords - Keywords to search for. + */ +export const useLibraryComponents = (libraryId, searchKeywords) => { + const { data: connectionDetails } = useContentSearchConnection(); + + const indexName = connectionDetails?.indexName; + const client = React.useMemo(() => { + if (connectionDetails?.apiKey === undefined || connectionDetails?.url === undefined) { + return undefined; + } + return new MeiliSearch({ host: connectionDetails.url, apiKey: connectionDetails.apiKey }); + }, [connectionDetails?.apiKey, connectionDetails?.url]); + + const libFilter = `context_key = "${libraryId}"`; + + return useContentSearchResults({ + client, + indexName, + searchKeywords, + extraFilter: [libFilter], + }); +}; + /** * Hook to fetch the count of components and collections in a library. * @param {string} libraryId - The ID of the library to fetch. diff --git a/src/library-authoring/messages.js b/src/library-authoring/messages.js index c7a190e9ee..8dc7bb68a4 100644 --- a/src/library-authoring/messages.js +++ b/src/library-authoring/messages.js @@ -52,6 +52,11 @@ const messages = defineMessages({ defaultMessage: 'This is a placeholder for the create library form. This will be replaced with the actual form.', description: 'Temp placeholder for the create library container. This will be replaced with the new library form.', }, + recentComponentsTempPlaceholder: { + id: 'course-authoring.library-authoring.recent-components-temp-placeholder', + defaultMessage: 'Recently modified components and collections will be displayed here.', + description: 'Temp placeholder for the recent components container. This will be replaced with the actual list.', + }, }); export default messages; diff --git a/src/library-authoring/utils.jsx b/src/library-authoring/utils.jsx new file mode 100644 index 0000000000..7f3b83ccc3 --- /dev/null +++ b/src/library-authoring/utils.jsx @@ -0,0 +1,7 @@ +// @ts-check +import COMPONENT_TYPE_COLOR_MAP from './constants'; + +/** @param {string} blockType */ +export default function getComponentColor(blockType) { + return COMPONENT_TYPE_COLOR_MAP[blockType] ?? 'bg-component'; +} diff --git a/src/search-modal/SearchResult.jsx b/src/search-modal/SearchResult.jsx index 09fa2f4a19..f7c287cbfb 100644 --- a/src/search-modal/SearchResult.jsx +++ b/src/search-modal/SearchResult.jsx @@ -8,31 +8,16 @@ import { IconButton, Stack, } from '@openedx/paragon'; -import { - Article, - Folder, - OpenInNew, -} from '@openedx/paragon/icons'; +import { OpenInNew } from '@openedx/paragon/icons'; import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { constructLibraryAuthoringURL } from '../utils'; -import { COMPONENT_TYPE_ICON_MAP, TYPE_ICONS_MAP } from '../course-unit/constants'; import { getStudioHomeData } from '../studio-home/data/selectors'; import { useSearchContext } from './manager/SearchManager'; import Highlight from './Highlight'; import messages from './messages'; - -const STRUCTURAL_TYPE_ICONS = { - vertical: TYPE_ICONS_MAP.vertical, - sequential: Folder, - chapter: Folder, -}; - -/** @param {string} blockType */ -function getItemIcon(blockType) { - return STRUCTURAL_TYPE_ICONS[blockType] ?? COMPONENT_TYPE_ICON_MAP[blockType] ?? Article; -} +import getItemIcon from './utils'; /** * Returns the URL Suffix for library/library component hit diff --git a/src/search-modal/data/api.js b/src/search-modal/data/api.js index e526546e69..13c3afadb0 100644 --- a/src/search-modal/data/api.js +++ b/src/search-modal/data/api.js @@ -86,11 +86,21 @@ function formatTagsFilter(tagsFilter) { * @property {[{displayName: string}, ...Array<{displayName: string, usageKey: string}>]} breadcrumbs * First one is the name of the course/library itself. * After that is the name and usage key of any parent Section/Subsection/Unit/etc. - * @property {Record<'taxonomy'|'level0'|'level1'|'level2'|'level3', string[]>} tags + * @property {ContentHitTags} tags * @property {ContentDetails} [content] * @property {{displayName: string, content: ContentDetails}} formatted Same fields with ... highlights */ +/** + * @typedef {Object} ContentHitTags + * @property {string[]} taxonomy + * @property {string[]} level0 + * @property {string[]} level1 + * @property {string[]} level2 + * @property {string[]} level3 + * @property {number} implicitCount + */ + /** * Convert search hits to camelCase * @param {Record} hit A search result directly from Meilisearch diff --git a/src/search-modal/utils.jsx b/src/search-modal/utils.jsx new file mode 100644 index 0000000000..82d2e22aea --- /dev/null +++ b/src/search-modal/utils.jsx @@ -0,0 +1,17 @@ +// @ts-check +import { + Article, + Folder, +} from '@openedx/paragon/icons'; +import { COMPONENT_TYPE_ICON_MAP, TYPE_ICONS_MAP } from '../course-unit/constants'; + +const STRUCTURAL_TYPE_ICONS = { + vertical: TYPE_ICONS_MAP.vertical, + sequential: Folder, + chapter: Folder, +}; + +/** @param {string} blockType */ +export default function getItemIcon(blockType) { + return STRUCTURAL_TYPE_ICONS[blockType] ?? COMPONENT_TYPE_ICON_MAP[blockType] ?? Article; +} From c87c4813e2d966f312268cd410a654c827f0ec2b Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 18 Jun 2024 14:02:17 -0500 Subject: [PATCH 047/106] feat: Infinite scroll implemented on Components tab --- .../components/LibraryComponents.jsx | 82 +++++++++++++++---- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx index f3603c4943..3097c94703 100644 --- a/src/library-authoring/components/LibraryComponents.jsx +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -1,6 +1,6 @@ // @ts-check /* eslint-disable react/prop-types */ -import React from 'react'; +import React, { useEffect, useMemo } from 'react'; import { CardGrid } from '@openedx/paragon'; import { NoComponents, NoSearchResults } from '../EmptyStates'; @@ -17,7 +17,54 @@ import ComponentCard from './ComponentCard'; */ const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); - const { hits, isFetching } = useLibraryComponents(libraryId, searchKeywords); + const { + hits, + isFetching, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useLibraryComponents(libraryId, searchKeywords); + + const { showLoading, showContent } = useMemo(() => { + let resultShowLoading = false; + let resultShowContent = false; + + if (isFetching && !isFetchingNextPage) { + // First load; show loading but not content. + resultShowLoading = true; + resultShowContent = false; + } else if (isFetchingNextPage) { + // Load next page; show content and loading. + resultShowLoading = true; + resultShowContent = true; + } else if (!isFetching && !isFetchingNextPage) { + // State without loads; show content. + resultShowLoading = false; + resultShowContent = true; + } + return { + showLoading: resultShowLoading, + showContent: resultShowContent, + }; + }, [isFetching, isFetchingNextPage]); + + useEffect(() => { + const onscroll = () => { + // Verify the position of the scroll to implementa a infinite scroll. + // Used `loadLimit` to fetch next page before reach the end of the screen. + const loadLimit = 300; + const scrolledTo = window.scrollY + window.innerHeight; + const scrollDiff = document.body.scrollHeight - scrolledTo; + const isNearToBottom = scrollDiff <= loadLimit; + if (isNearToBottom && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }; + window.addEventListener('scroll', onscroll); + return () => { + window.removeEventListener('scroll', onscroll); + }; + }, [hasNextPage, isFetchingNextPage]); if (componentCount === 0) { return searchKeywords === '' ? : ; @@ -33,23 +80,22 @@ const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { }} hasEqualColumnHeights > - { isFetching ? - : hits.map((component) => { - let tagCount = 0; - if (component.tags) { - tagCount = component.tags.implicitCount || 0; - } - - return ( - - ); - })} + { showContent && hits.map((component) => { + let tagCount = 0; + if (component.tags) { + tagCount = component.tags.implicitCount || 0; + } + return ( + + ); + })} + { showLoading && } ); }; From c63bc2fbf586a791770b3b63e3262c5c6bd542ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 18 Jun 2024 18:56:16 -0300 Subject: [PATCH 048/106] fix: new library redirect --- src/studio-home/StudioHome.jsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 4953c6f3ae..92b26f37f9 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Button, Container, @@ -11,6 +11,7 @@ import { Add as AddIcon, Error } from '@openedx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { StudioFooter } from '@edx/frontend-component-footer'; import { getConfig, getPath } from '@edx/frontend-platform'; +import { useLocation } from 'react-router-dom'; import { constructLibraryAuthoringURL } from '../utils'; import Loading from '../generic/Loading'; @@ -19,7 +20,7 @@ import Header from '../header'; import SubHeader from '../generic/sub-header/SubHeader'; import HomeSidebar from './home-sidebar'; import TabsSection from './tabs-section'; -import { isMixedOrV2LibrariesMode } from './tabs-section/utils'; +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'; @@ -28,6 +29,8 @@ import { useStudioHome } from './hooks'; import AlertMessage from '../generic/alert-message'; const StudioHome = ({ intl }) => { + const location = useLocation(); + const isPaginationCoursesEnabled = getConfig().ENABLE_HOME_PAGE_COURSE_API_V2; const { isLoadingPage, @@ -47,6 +50,9 @@ const StudioHome = ({ intl }) => { const libMode = getConfig().LIBRARY_MODE; + const v1LibraryTab = isMixedOrV1LibrariesMode(libMode) && location?.pathname.split('/').pop() === 'libraries-v1'; + console.log('v1LibraryTab', v1LibraryTab); + const { userIsActive, studioShortName, @@ -55,7 +61,7 @@ const StudioHome = ({ intl }) => { redirectToLibraryAuthoringMfe, } = studioHomeData; - function getHeaderButtons() { + const getHeaderButtons = useCallback(() => { const headerButtons = []; if (isFailedLoadingPage || !userIsActive) { @@ -83,7 +89,7 @@ const StudioHome = ({ intl }) => { } let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; - if (isMixedOrV2LibrariesMode(libMode)) { + if (isMixedOrV2LibrariesMode(libMode) && !v1LibraryTab) { libraryHref = libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe ? constructLibraryAuthoringURL(libraryAuthoringMfeUrl, 'create') // Redirection to the placeholder is done in the MFE rather than @@ -91,6 +97,7 @@ const StudioHome = ({ intl }) => { // hence why we use the MFE's origin : `${window.location.origin}${getPath(getConfig().PUBLIC_PATH)}library/create`; } + console.log('libraryHref', libraryHref); headerButtons.push(
- +
); diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx index d23cfd5d50..65422b75fd 100644 --- a/src/library-authoring/components/LibraryComponents.jsx +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -8,14 +8,25 @@ import { useLibraryBlockTypes, useLibraryComponentCount, useLibraryComponents } import ComponentCard from './ComponentCard'; /** + * Library Components to show components grid + * + * Use style to: + * - 'full': Show all components with Infinite scroll pagination. + * - 'preview': Show first 4 components without pagination. + * * @type {React.FC<{ * libraryId: string, * filter: { * searchKeywords: string, * }, + * variant: 'full'|'preview', * }>} */ -const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { +const LibraryComponents = ({ + libraryId, + filter: { searchKeywords }, + variant, +}) => { const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); const { hits, @@ -61,27 +72,35 @@ const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { }, [isFetching, isFetchingNextPage]); useEffect(() => { - const onscroll = () => { - // Verify the position of the scroll to implementa a infinite scroll. - // Used `loadLimit` to fetch next page before reach the end of the screen. - const loadLimit = 300; - const scrolledTo = window.scrollY + window.innerHeight; - const scrollDiff = document.body.scrollHeight - scrolledTo; - const isNearToBottom = scrollDiff <= loadLimit; - if (isNearToBottom && hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }; - window.addEventListener('scroll', onscroll); - return () => { - window.removeEventListener('scroll', onscroll); - }; + if (variant === 'full') { + const onscroll = () => { + // Verify the position of the scroll to implementa a infinite scroll. + // Used `loadLimit` to fetch next page before reach the end of the screen. + const loadLimit = 300; + const scrolledTo = window.scrollY + window.innerHeight; + const scrollDiff = document.body.scrollHeight - scrolledTo; + const isNearToBottom = scrollDiff <= loadLimit; + if (isNearToBottom && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }; + window.addEventListener('scroll', onscroll); + return () => { + window.removeEventListener('scroll', onscroll); + }; + } + return () => {}; }, [hasNextPage, isFetchingNextPage, fetchNextPage]); if (componentCount === 0) { return searchKeywords === '' ? : ; } + let componentList = hits; + if (variant === 'preview') { + componentList = componentList.slice(0, 4); + } + return ( { }} hasEqualColumnHeights > - { showContent ? hits.map((component) => { + { showContent ? componentList.map((component) => { let tagCount = 0; if (component.tags) { tagCount = component.tags.implicitCount || 0; From 499acedcf0ddeed9c2b75aeb2c164a55d992e8e4 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 20 Jun 2024 17:29:30 -0500 Subject: [PATCH 060/106] style: Nits on lints and code --- .../components/ComponentCard.jsx | 16 ++++++++++++---- .../components/LibraryComponents.jsx | 12 +++++------- src/library-authoring/constants.js | 1 + src/library-authoring/data/api.js | 5 +++++ src/library-authoring/data/types.mjs | 6 ++++++ 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/library-authoring/components/ComponentCard.jsx b/src/library-authoring/components/ComponentCard.jsx index ba089063a6..7cc17bba4f 100644 --- a/src/library-authoring/components/ComponentCard.jsx +++ b/src/library-authoring/components/ComponentCard.jsx @@ -1,3 +1,5 @@ +// @ts-check +import React from 'react'; import { ActionRow, Card, @@ -44,7 +46,15 @@ const ComponentCardMenu = () => ( ); -const ComponentCard = ({ +export const ComponentCardLoading = () => ( + + + + + +); + +export const ComponentCard = ({ isLoading, title, description, @@ -71,7 +81,7 @@ const ComponentCard = ({ - + {blockTypeDisplayName} @@ -102,5 +112,3 @@ ComponentCard.propTypes = { blockType: PropTypes.string.isRequired, blockTypeDisplayName: PropTypes.string.isRequired, }; - -export default ComponentCard; diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx index 65422b75fd..e9a44fdc2e 100644 --- a/src/library-authoring/components/LibraryComponents.jsx +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -5,7 +5,7 @@ import React, { useEffect, useMemo } from 'react'; import { CardGrid } from '@openedx/paragon'; import { NoComponents, NoSearchResults } from '../EmptyStates'; import { useLibraryBlockTypes, useLibraryComponentCount, useLibraryComponents } from '../data/apiHook'; -import ComponentCard from './ComponentCard'; +import { ComponentCard, ComponentCardLoading } from './ComponentCard'; /** * Library Components to show components grid @@ -112,13 +112,11 @@ const LibraryComponents = ({ hasEqualColumnHeights > { showContent ? componentList.map((component) => { - let tagCount = 0; - if (component.tags) { - tagCount = component.tags.implicitCount || 0; - } + const tagCount = component.tags?.implicitCount || 0; return ( ); - }) : } - { showLoading && } + }) : } + { showLoading && } ); }; diff --git a/src/library-authoring/constants.js b/src/library-authoring/constants.js index 83a9be8abd..c79996ca8c 100644 --- a/src/library-authoring/constants.js +++ b/src/library-authoring/constants.js @@ -1,3 +1,4 @@ +// @ts-check import { COMPONENT_TYPES } from '../course-unit/constants'; const COMPONENT_TYPE_COLOR_MAP = { diff --git a/src/library-authoring/data/api.js b/src/library-authoring/data/api.js index afdacf2e7e..a7b2cb25b4 100644 --- a/src/library-authoring/data/api.js +++ b/src/library-authoring/data/api.js @@ -28,6 +28,11 @@ export async function getContentLibrary(libraryId) { return camelCaseObject(data); } +/** + * Fetch block types of a library + * @param {string} libraryId + * @returns {Promise} + */ export async function getLibraryBlockTypes(libraryId) { if (!libraryId) { throw new Error('libraryId is required'); diff --git a/src/library-authoring/data/types.mjs b/src/library-authoring/data/types.mjs index 34486525d7..7fc4761863 100644 --- a/src/library-authoring/data/types.mjs +++ b/src/library-authoring/data/types.mjs @@ -16,3 +16,9 @@ * @property {boolean} hasUnpublishedDeletes * @property {string} license */ + +/** + * @typedef {Object} LibraryBlockType + * @property {string} blockType + * @property {string} displayName + */ From da1c2e0bcf0268129b3f23b61d437014883020af Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 20 Jun 2024 17:51:34 -0500 Subject: [PATCH 061/106] refactor: Create block-type-utils with block type constants --- .../add-component/AddComponent.jsx | 2 +- .../add-component-btn/AddComponentIcon.jsx | 2 +- src/course-unit/constants.js | 47 ------------- .../sequence-navigation/UnitIcon.jsx | 2 +- .../course-xblock/CourseXBlock.jsx | 2 +- src/generic/block-type-utils/constants.js | 68 +++++++++++++++++++ src/generic/block-type-utils/index.jsx | 19 ++++++ .../components/ComponentCard.jsx | 3 +- src/library-authoring/constants.js | 19 ------ src/library-authoring/utils.jsx | 7 -- src/search-modal/SearchResult.jsx | 2 +- src/search-modal/utils.jsx | 17 ----- 12 files changed, 93 insertions(+), 97 deletions(-) create mode 100644 src/generic/block-type-utils/constants.js create mode 100644 src/generic/block-type-utils/index.jsx delete mode 100644 src/library-authoring/constants.js delete mode 100644 src/library-authoring/utils.jsx delete mode 100644 src/search-modal/utils.jsx diff --git a/src/course-unit/add-component/AddComponent.jsx b/src/course-unit/add-component/AddComponent.jsx index 78125366dc..9ef58df7bd 100644 --- a/src/course-unit/add-component/AddComponent.jsx +++ b/src/course-unit/add-component/AddComponent.jsx @@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useToggle } from '@openedx/paragon'; import { getCourseSectionVertical } from '../data/selectors'; -import { COMPONENT_TYPES } from '../constants'; +import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants'; import ComponentModalView from './add-component-modals/ComponentModalView'; import AddComponentButton from './add-component-btn'; import messages from './messages'; diff --git a/src/course-unit/add-component/add-component-btn/AddComponentIcon.jsx b/src/course-unit/add-component/add-component-btn/AddComponentIcon.jsx index 4ace3ea015..91cc5b09b1 100644 --- a/src/course-unit/add-component/add-component-btn/AddComponentIcon.jsx +++ b/src/course-unit/add-component/add-component-btn/AddComponentIcon.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import { Icon } from '@openedx/paragon'; import { EditNote as EditNoteIcon } from '@openedx/paragon/icons'; -import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../constants'; +import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants'; const AddComponentIcon = ({ type }) => { const icon = COMPONENT_TYPE_ICON_MAP[type] || EditNoteIcon; diff --git a/src/course-unit/constants.js b/src/course-unit/constants.js index b7e7bf5c6b..9ff040d63c 100644 --- a/src/course-unit/constants.js +++ b/src/course-unit/constants.js @@ -1,53 +1,6 @@ -import { - BackHand as BackHandIcon, - BookOpen as BookOpenIcon, - Edit as EditIcon, - EditNote as EditNoteIcon, - FormatListBulleted as FormatListBulletedIcon, - HelpOutline as HelpOutlineIcon, - LibraryAdd as LibraryIcon, - Lock as LockIcon, - QuestionAnswerOutline as QuestionAnswerOutlineIcon, - Science as ScienceIcon, - TextFields as TextFieldsIcon, - VideoCamera as VideoCameraIcon, -} from '@openedx/paragon/icons'; - import messages from './sidebar/messages'; import addComponentMessages from './add-component/messages'; -export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock']; - -export const COMPONENT_TYPES = { - advanced: 'advanced', - discussion: 'discussion', - library: 'library', - html: 'html', - openassessment: 'openassessment', - problem: 'problem', - video: 'video', - dragAndDrop: 'drag-and-drop-v2', -}; - -export const TYPE_ICONS_MAP = { - video: VideoCameraIcon, - other: BookOpenIcon, - vertical: FormatListBulletedIcon, - problem: EditIcon, - lock: LockIcon, -}; - -export const COMPONENT_TYPE_ICON_MAP = { - [COMPONENT_TYPES.advanced]: ScienceIcon, - [COMPONENT_TYPES.discussion]: QuestionAnswerOutlineIcon, - [COMPONENT_TYPES.library]: LibraryIcon, - [COMPONENT_TYPES.html]: TextFieldsIcon, - [COMPONENT_TYPES.openassessment]: EditNoteIcon, - [COMPONENT_TYPES.problem]: HelpOutlineIcon, - [COMPONENT_TYPES.video]: VideoCameraIcon, - [COMPONENT_TYPES.dragAndDrop]: BackHandIcon, -}; - export const getUnitReleaseStatus = (intl) => ({ release: intl.formatMessage(messages.releaseStatusTitle), released: intl.formatMessage(messages.releasedStatusTitle), diff --git a/src/course-unit/course-sequence/sequence-navigation/UnitIcon.jsx b/src/course-unit/course-sequence/sequence-navigation/UnitIcon.jsx index 69830e4bde..79cfc933b1 100644 --- a/src/course-unit/course-sequence/sequence-navigation/UnitIcon.jsx +++ b/src/course-unit/course-sequence/sequence-navigation/UnitIcon.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import { Icon } from '@openedx/paragon'; import { BookOpen as BookOpenIcon } from '@openedx/paragon/icons'; -import { TYPE_ICONS_MAP, UNIT_ICON_TYPES } from '../../constants'; +import { TYPE_ICONS_MAP, UNIT_ICON_TYPES } from '../../../generic/block-type-utils/constants'; const UnitIcon = ({ type }) => { const icon = TYPE_ICONS_MAP[type] || BookOpenIcon; diff --git a/src/course-unit/course-xblock/CourseXBlock.jsx b/src/course-unit/course-xblock/CourseXBlock.jsx index 88058ee5a5..8132812f54 100644 --- a/src/course-unit/course-xblock/CourseXBlock.jsx +++ b/src/course-unit/course-xblock/CourseXBlock.jsx @@ -16,7 +16,7 @@ import SortableItem from '../../generic/drag-helper/SortableItem'; import { scrollToElement } from '../../course-outline/utils'; import { COURSE_BLOCK_NAMES } from '../../constants'; import { copyToClipboard } from '../../generic/data/thunks'; -import { COMPONENT_TYPES } from '../constants'; +import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants'; import XBlockMessages from './xblock-messages/XBlockMessages'; import messages from './messages'; diff --git a/src/generic/block-type-utils/constants.js b/src/generic/block-type-utils/constants.js new file mode 100644 index 0000000000..6739470945 --- /dev/null +++ b/src/generic/block-type-utils/constants.js @@ -0,0 +1,68 @@ +import { + BackHand as BackHandIcon, + BookOpen as BookOpenIcon, + Edit as EditIcon, + EditNote as EditNoteIcon, + FormatListBulleted as FormatListBulletedIcon, + HelpOutline as HelpOutlineIcon, + LibraryAdd as LibraryIcon, + Lock as LockIcon, + QuestionAnswerOutline as QuestionAnswerOutlineIcon, + Science as ScienceIcon, + TextFields as TextFieldsIcon, + VideoCamera as VideoCameraIcon, + Folder, +} from '@openedx/paragon/icons'; + +export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock']; + +export const COMPONENT_TYPES = { + advanced: 'advanced', + discussion: 'discussion', + library: 'library', + html: 'html', + openassessment: 'openassessment', + problem: 'problem', + video: 'video', + dragAndDrop: 'drag-and-drop-v2', +}; + +export const TYPE_ICONS_MAP = { + video: VideoCameraIcon, + other: BookOpenIcon, + vertical: FormatListBulletedIcon, + problem: EditIcon, + lock: LockIcon, +}; + +export const COMPONENT_TYPE_ICON_MAP = { + [COMPONENT_TYPES.advanced]: ScienceIcon, + [COMPONENT_TYPES.discussion]: QuestionAnswerOutlineIcon, + [COMPONENT_TYPES.library]: LibraryIcon, + [COMPONENT_TYPES.html]: TextFieldsIcon, + [COMPONENT_TYPES.openassessment]: EditNoteIcon, + [COMPONENT_TYPES.problem]: HelpOutlineIcon, + [COMPONENT_TYPES.video]: VideoCameraIcon, + [COMPONENT_TYPES.dragAndDrop]: BackHandIcon, +}; + +export const STRUCTURAL_TYPE_ICONS = { + vertical: TYPE_ICONS_MAP.vertical, + sequential: Folder, + chapter: Folder, +}; + +export const COMPONENT_TYPE_COLOR_MAP = { + [COMPONENT_TYPES.advanced]: 'bg-other', + [COMPONENT_TYPES.discussion]: 'bg-component', + [COMPONENT_TYPES.library]: 'bg-component', + [COMPONENT_TYPES.html]: 'bg-html', + [COMPONENT_TYPES.openassessment]: 'bg-component', + [COMPONENT_TYPES.problem]: 'bg-component', + [COMPONENT_TYPES.video]: 'bg-video', + [COMPONENT_TYPES.dragAndDrop]: 'bg-component', + vertical: 'bg-vertical', + sequential: 'bg-component', + chapter: 'bg-component', + collection: 'bg-collection', +}; diff --git a/src/generic/block-type-utils/index.jsx b/src/generic/block-type-utils/index.jsx new file mode 100644 index 0000000000..c5e2e3ca0b --- /dev/null +++ b/src/generic/block-type-utils/index.jsx @@ -0,0 +1,19 @@ +// @ts-check +import { + Article, +} from '@openedx/paragon/icons'; +import { + COMPONENT_TYPE_ICON_MAP, + STRUCTURAL_TYPE_ICONS, + COMPONENT_TYPE_COLOR_MAP +} from './constants' + +/** @param {string} blockType */ +export function getItemIcon(blockType) { + return STRUCTURAL_TYPE_ICONS[blockType] ?? COMPONENT_TYPE_ICON_MAP[blockType] ?? Article; +} + +/** @param {string} blockType */ +export function getComponentColor(blockType) { + return COMPONENT_TYPE_COLOR_MAP[blockType] ?? 'bg-component'; +} diff --git a/src/library-authoring/components/ComponentCard.jsx b/src/library-authoring/components/ComponentCard.jsx index 7cc17bba4f..2529b1fd73 100644 --- a/src/library-authoring/components/ComponentCard.jsx +++ b/src/library-authoring/components/ComponentCard.jsx @@ -14,8 +14,7 @@ import { MoreVert } from '@openedx/paragon/icons'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from './messages'; import TagCount from '../../generic/tag-count'; -import getItemIcon from '../../search-modal/utils'; -import getComponentColor from '../utils'; +import { getItemIcon, getComponentColor } from '../../generic/block-type-utils'; const ComponentCardMenu = () => ( diff --git a/src/library-authoring/constants.js b/src/library-authoring/constants.js deleted file mode 100644 index c79996ca8c..0000000000 --- a/src/library-authoring/constants.js +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-check -import { COMPONENT_TYPES } from '../course-unit/constants'; - -const COMPONENT_TYPE_COLOR_MAP = { - [COMPONENT_TYPES.advanced]: 'bg-other', - [COMPONENT_TYPES.discussion]: 'bg-component', - [COMPONENT_TYPES.library]: 'bg-component', - [COMPONENT_TYPES.html]: 'bg-html', - [COMPONENT_TYPES.openassessment]: 'bg-component', - [COMPONENT_TYPES.problem]: 'bg-component', - [COMPONENT_TYPES.video]: 'bg-video', - [COMPONENT_TYPES.dragAndDrop]: 'bg-component', - vertical: 'bg-vertical', - sequential: 'bg-component', - chapter: 'bg-component', - collection: 'bg-collection', -}; - -export default COMPONENT_TYPE_COLOR_MAP; diff --git a/src/library-authoring/utils.jsx b/src/library-authoring/utils.jsx deleted file mode 100644 index 7f3b83ccc3..0000000000 --- a/src/library-authoring/utils.jsx +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-check -import COMPONENT_TYPE_COLOR_MAP from './constants'; - -/** @param {string} blockType */ -export default function getComponentColor(blockType) { - return COMPONENT_TYPE_COLOR_MAP[blockType] ?? 'bg-component'; -} diff --git a/src/search-modal/SearchResult.jsx b/src/search-modal/SearchResult.jsx index f7c287cbfb..1a3f4f9d8b 100644 --- a/src/search-modal/SearchResult.jsx +++ b/src/search-modal/SearchResult.jsx @@ -17,7 +17,7 @@ import { getStudioHomeData } from '../studio-home/data/selectors'; import { useSearchContext } from './manager/SearchManager'; import Highlight from './Highlight'; import messages from './messages'; -import getItemIcon from './utils'; +import { getItemIcon } from '../generic/block-type-utils'; /** * Returns the URL Suffix for library/library component hit diff --git a/src/search-modal/utils.jsx b/src/search-modal/utils.jsx deleted file mode 100644 index 82d2e22aea..0000000000 --- a/src/search-modal/utils.jsx +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-check -import { - Article, - Folder, -} from '@openedx/paragon/icons'; -import { COMPONENT_TYPE_ICON_MAP, TYPE_ICONS_MAP } from '../course-unit/constants'; - -const STRUCTURAL_TYPE_ICONS = { - vertical: TYPE_ICONS_MAP.vertical, - sequential: Folder, - chapter: Folder, -}; - -/** @param {string} blockType */ -export default function getItemIcon(blockType) { - return STRUCTURAL_TYPE_ICONS[blockType] ?? COMPONENT_TYPE_ICON_MAP[blockType] ?? Article; -} From c8bc5fc3d735ead1ef3f31c6137fc13ad150682a Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 20 Jun 2024 19:18:59 -0500 Subject: [PATCH 062/106] fix: Issues on course-unit tests --- src/course-unit/add-component/AddComponent.test.jsx | 2 +- src/course-unit/course-xblock/CourseXBlock.test.jsx | 3 ++- src/generic/block-type-utils/index.jsx | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/course-unit/add-component/AddComponent.test.jsx b/src/course-unit/add-component/AddComponent.test.jsx index 9bd5a5de04..f09378bf09 100644 --- a/src/course-unit/add-component/AddComponent.test.jsx +++ b/src/course-unit/add-component/AddComponent.test.jsx @@ -14,7 +14,7 @@ import { executeThunk } from '../../utils'; import { fetchCourseSectionVerticalData } from '../data/thunk'; import { getCourseSectionVerticalApiUrl } from '../data/api'; import { courseSectionVerticalMock } from '../__mocks__'; -import { COMPONENT_TYPES } from '../constants'; +import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants'; import AddComponent from './AddComponent'; import messages from './messages'; diff --git a/src/course-unit/course-xblock/CourseXBlock.test.jsx b/src/course-unit/course-xblock/CourseXBlock.test.jsx index ad8e09184b..0cdf05d4f6 100644 --- a/src/course-unit/course-xblock/CourseXBlock.test.jsx +++ b/src/course-unit/course-xblock/CourseXBlock.test.jsx @@ -16,7 +16,8 @@ import { getCourseSectionVerticalApiUrl, getXBlockBaseApiUrl } from '../data/api import { fetchCourseSectionVerticalData } from '../data/thunk'; import { executeThunk } from '../../utils'; import { getCourseId } from '../data/selectors'; -import { PUBLISH_TYPES, COMPONENT_TYPES } from '../constants'; +import { PUBLISH_TYPES } from '../constants'; +import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants'; import { courseSectionVerticalMock, courseVerticalChildrenMock } from '../__mocks__'; import CourseXBlock from './CourseXBlock'; import messages from './messages'; diff --git a/src/generic/block-type-utils/index.jsx b/src/generic/block-type-utils/index.jsx index c5e2e3ca0b..73e6555fb8 100644 --- a/src/generic/block-type-utils/index.jsx +++ b/src/generic/block-type-utils/index.jsx @@ -5,8 +5,8 @@ import { import { COMPONENT_TYPE_ICON_MAP, STRUCTURAL_TYPE_ICONS, - COMPONENT_TYPE_COLOR_MAP -} from './constants' + COMPONENT_TYPE_COLOR_MAP, +} from './constants'; /** @param {string} blockType */ export function getItemIcon(blockType) { From 0843a1e699815eb72302d0362f541effb128af0c Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 20 Jun 2024 19:20:01 -0500 Subject: [PATCH 063/106] refactor: Calculate tagCount in LibraryComponents instead on serach index --- .../components/LibraryComponents.jsx | 48 +++++++++++-------- src/search-modal/data/api.js | 12 +---- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx index e9a44fdc2e..8c6ca549e2 100644 --- a/src/library-authoring/components/LibraryComponents.jsx +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -36,6 +36,25 @@ const LibraryComponents = ({ fetchNextPage, } = useLibraryComponents(libraryId, searchKeywords); + const { componentList, tagCounts } = useMemo(() => { + const result = variant === 'preview' ? hits.slice(0, 4) : hits; + const tagsCountsResult = {}; + result.forEach((component) => { + if (!component.tags) { + tagsCountsResult[component.id] = 0; + } else { + tagsCountsResult[component.id] = (component.tags.level0?.length || 0) + + (component.tags.level1?.length || 0) + + (component.tags.level2?.length || 0) + + (component.tags.level3?.length || 0); + } + }); + return { + componentList: result, + tagCounts: tagsCountsResult, + }; + }, [hits]); + // TODO add this to LibraryContext const { data: blockTypesData } = useLibraryBlockTypes(libraryId); const blockTypes = useMemo(() => { @@ -96,11 +115,6 @@ const LibraryComponents = ({ return searchKeywords === '' ? : ; } - let componentList = hits; - if (variant === 'preview') { - componentList = componentList.slice(0, 4); - } - return ( - { showContent ? componentList.map((component) => { - const tagCount = component.tags?.implicitCount || 0; - - return ( - - ); - }) : } + { showContent ? componentList.map((component) => ( + + )) : } { showLoading && } ); diff --git a/src/search-modal/data/api.js b/src/search-modal/data/api.js index 13c3afadb0..e526546e69 100644 --- a/src/search-modal/data/api.js +++ b/src/search-modal/data/api.js @@ -86,21 +86,11 @@ function formatTagsFilter(tagsFilter) { * @property {[{displayName: string}, ...Array<{displayName: string, usageKey: string}>]} breadcrumbs * First one is the name of the course/library itself. * After that is the name and usage key of any parent Section/Subsection/Unit/etc. - * @property {ContentHitTags} tags + * @property {Record<'taxonomy'|'level0'|'level1'|'level2'|'level3', string[]>} tags * @property {ContentDetails} [content] * @property {{displayName: string, content: ContentDetails}} formatted Same fields with ... highlights */ -/** - * @typedef {Object} ContentHitTags - * @property {string[]} taxonomy - * @property {string[]} level0 - * @property {string[]} level1 - * @property {string[]} level2 - * @property {string[]} level3 - * @property {number} implicitCount - */ - /** * Convert search hits to camelCase * @param {Record} hit A search result directly from Meilisearch From 55acb063297f32adc47f19b49dea69f5aaf58d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 21 Jun 2024 16:07:16 -0300 Subject: [PATCH 064/106] refactor: test renaming to ts --- src/library-authoring/{index.js => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/library-authoring/{index.js => index.ts} (100%) diff --git a/src/library-authoring/index.js b/src/library-authoring/index.ts similarity index 100% rename from src/library-authoring/index.js rename to src/library-authoring/index.ts From a1a0ad8a80f2785b23279419f53c867e0003a0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Mon, 24 Jun 2024 09:41:38 -0300 Subject: [PATCH 065/106] refactor: renaming files js -> ts --- src/library-authoring/{CreateLibrary.jsx => CreateLibrary.tsx} | 0 src/library-authoring/{EmptyStates.jsx => EmptyStates.tsx} | 0 ...ibraryAuthoringPage.test.jsx => LibraryAuthoringPage.test.tsx} | 0 .../{LibraryAuthoringPage.jsx => LibraryAuthoringPage.tsx} | 0 .../{LibraryCollections.jsx => LibraryCollections.tsx} | 0 .../{LibraryComponents.jsx => LibraryComponents.tsx} | 0 src/library-authoring/{LibraryHome.jsx => LibraryHome.tsx} | 0 src/library-authoring/data/{api.js => api.ts} | 0 src/library-authoring/data/{apiHook.js => apiHook.ts} | 0 src/library-authoring/data/{types.mjs => types.d.ts} | 0 src/library-authoring/{messages.js => messages.ts} | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename src/library-authoring/{CreateLibrary.jsx => CreateLibrary.tsx} (100%) rename src/library-authoring/{EmptyStates.jsx => EmptyStates.tsx} (100%) rename src/library-authoring/{LibraryAuthoringPage.test.jsx => LibraryAuthoringPage.test.tsx} (100%) rename src/library-authoring/{LibraryAuthoringPage.jsx => LibraryAuthoringPage.tsx} (100%) rename src/library-authoring/{LibraryCollections.jsx => LibraryCollections.tsx} (100%) rename src/library-authoring/{LibraryComponents.jsx => LibraryComponents.tsx} (100%) rename src/library-authoring/{LibraryHome.jsx => LibraryHome.tsx} (100%) rename src/library-authoring/data/{api.js => api.ts} (100%) rename src/library-authoring/data/{apiHook.js => apiHook.ts} (100%) rename src/library-authoring/data/{types.mjs => types.d.ts} (100%) rename src/library-authoring/{messages.js => messages.ts} (100%) diff --git a/src/library-authoring/CreateLibrary.jsx b/src/library-authoring/CreateLibrary.tsx similarity index 100% rename from src/library-authoring/CreateLibrary.jsx rename to src/library-authoring/CreateLibrary.tsx diff --git a/src/library-authoring/EmptyStates.jsx b/src/library-authoring/EmptyStates.tsx similarity index 100% rename from src/library-authoring/EmptyStates.jsx rename to src/library-authoring/EmptyStates.tsx diff --git a/src/library-authoring/LibraryAuthoringPage.test.jsx b/src/library-authoring/LibraryAuthoringPage.test.tsx similarity index 100% rename from src/library-authoring/LibraryAuthoringPage.test.jsx rename to src/library-authoring/LibraryAuthoringPage.test.tsx diff --git a/src/library-authoring/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.tsx similarity index 100% rename from src/library-authoring/LibraryAuthoringPage.jsx rename to src/library-authoring/LibraryAuthoringPage.tsx diff --git a/src/library-authoring/LibraryCollections.jsx b/src/library-authoring/LibraryCollections.tsx similarity index 100% rename from src/library-authoring/LibraryCollections.jsx rename to src/library-authoring/LibraryCollections.tsx diff --git a/src/library-authoring/LibraryComponents.jsx b/src/library-authoring/LibraryComponents.tsx similarity index 100% rename from src/library-authoring/LibraryComponents.jsx rename to src/library-authoring/LibraryComponents.tsx diff --git a/src/library-authoring/LibraryHome.jsx b/src/library-authoring/LibraryHome.tsx similarity index 100% rename from src/library-authoring/LibraryHome.jsx rename to src/library-authoring/LibraryHome.tsx diff --git a/src/library-authoring/data/api.js b/src/library-authoring/data/api.ts similarity index 100% rename from src/library-authoring/data/api.js rename to src/library-authoring/data/api.ts diff --git a/src/library-authoring/data/apiHook.js b/src/library-authoring/data/apiHook.ts similarity index 100% rename from src/library-authoring/data/apiHook.js rename to src/library-authoring/data/apiHook.ts diff --git a/src/library-authoring/data/types.mjs b/src/library-authoring/data/types.d.ts similarity index 100% rename from src/library-authoring/data/types.mjs rename to src/library-authoring/data/types.d.ts diff --git a/src/library-authoring/messages.js b/src/library-authoring/messages.ts similarity index 100% rename from src/library-authoring/messages.js rename to src/library-authoring/messages.ts From 87358b7c80ceb625dd7a35b2bb849f08e426c145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Mon, 24 Jun 2024 10:06:58 -0300 Subject: [PATCH 066/106] fix: update typescript code --- src/library-authoring/CreateLibrary.tsx | 1 - .../LibraryAuthoringPage.tsx | 19 ++++------ src/library-authoring/LibraryCollections.tsx | 4 --- src/library-authoring/LibraryComponents.tsx | 18 +++++----- src/library-authoring/LibraryHome.tsx | 26 +++++--------- src/library-authoring/data/api.ts | 9 +++-- src/library-authoring/data/apiHook.ts | 7 ++-- src/library-authoring/data/types.d.ts | 35 +++++++++---------- src/library-authoring/index.ts | 1 - 9 files changed, 46 insertions(+), 74 deletions(-) diff --git a/src/library-authoring/CreateLibrary.tsx b/src/library-authoring/CreateLibrary.tsx index 738e9fb769..74d80c0328 100644 --- a/src/library-authoring/CreateLibrary.tsx +++ b/src/library-authoring/CreateLibrary.tsx @@ -1,5 +1,4 @@ // @ts-check -/* eslint-disable react/prop-types */ import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Container } from '@openedx/paragon'; diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 15e846b3b6..420ba8d4cf 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -1,6 +1,5 @@ // @ts-check -/* eslint-disable react/prop-types */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { StudioFooter } from '@edx/frontend-component-footer'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -27,7 +26,7 @@ const TAB_LIST = { collections: 'collections', }; -const SubHeaderTitle = ({ title }) => { +const SubHeaderTitle = ({ title }: { title: string }) => { const intl = useIntl(); return ( <> @@ -42,15 +41,12 @@ const SubHeaderTitle = ({ title }) => { ); }; -/** - * @type {React.FC} - */ const LibraryAuthoringPage = () => { const intl = useIntl(); const location = useLocation(); const navigate = useNavigate(); - const [tabKey, setTabKey] = React.useState(TAB_LIST.home); - const [searchKeywords, setSearchKeywords] = React.useState(''); + const [tabKey, setTabKey] = useState(TAB_LIST.home); + const [searchKeywords, setSearchKeywords] = useState(''); const { libraryId } = useParams(); @@ -73,10 +69,7 @@ const LibraryAuthoringPage = () => { return ; } - /** Handle tab change - * @param {string} key - */ - const handleTabChange = (key) => { + const handleTabChange = (key: string) => { setTabKey(key); navigate(key); }; @@ -98,7 +91,7 @@ const LibraryAuthoringPage = () => { setSearchKeywords(value)} + onChange={(value: string) => setSearchKeywords(value)} onSubmit={() => {}} className="w-50" /> diff --git a/src/library-authoring/LibraryCollections.tsx b/src/library-authoring/LibraryCollections.tsx index 5292b10f40..c364ce9a82 100644 --- a/src/library-authoring/LibraryCollections.tsx +++ b/src/library-authoring/LibraryCollections.tsx @@ -1,13 +1,9 @@ // @ts-check -/* eslint-disable react/prop-types */ import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from './messages'; -/** - * @type {React.FC} - */ const LibraryCollections = () => (
} - */ -const LibraryComponents = ({ libraryId, filter: { searchKeywords } }) => { +type LibraryComponentsProps = { + libraryId: string; + filter: { + searchKeywords: string; + }; +}; + +const LibraryComponents = ({ libraryId, filter: { searchKeywords } }: LibraryComponentsProps) => { const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); if (componentCount === 0) { diff --git a/src/library-authoring/LibraryHome.tsx b/src/library-authoring/LibraryHome.tsx index 4331a0b54d..56e8213353 100644 --- a/src/library-authoring/LibraryHome.tsx +++ b/src/library-authoring/LibraryHome.tsx @@ -1,5 +1,4 @@ // @ts-check -/* eslint-disable react/prop-types */ import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { @@ -12,13 +11,7 @@ import LibraryComponents from './LibraryComponents'; import { useLibraryComponentCount } from './data/apiHook'; import messages from './messages'; -/** - * @type {React.FC<{ - * title: string, - * children: React.ReactNode, - * }>} - */ -const Section = ({ title, children }) => ( +const Section = ({ title, children } : { title: string, children: React.ReactNode }) => ( ( ); -/** - * @type {React.FC<{ - * libraryId: string, - * filter: { - * searchKeywords: string, - * }, - * }>} - */ -const LibraryHome = ({ libraryId, filter }) => { +type LibraryHomeProps = { + libraryId: string, + filter: { + searchKeywords: string, + }, +}; + +const LibraryHome = ({ libraryId, filter } : LibraryHomeProps) => { const { searchKeywords } = filter; const { componentCount, collectionCount } = useLibraryComponentCount(libraryId, searchKeywords); diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index 9fae35d947..e78eb92ba5 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -2,19 +2,18 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import type { ContentLibrary } from './types'; + const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; /** * Get the URL for the content library API. - * @param {string} libraryId - The ID of the library to fetch. */ -export const getContentLibraryApiUrl = (libraryId) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; +export const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; /** * Fetch a content library by its ID. - * @param {string} [libraryId] - The ID of the library to fetch. - * @returns {Promise} */ -export async function getContentLibrary(libraryId) { +export async function getContentLibrary(libraryId?: string): Promise { if (!libraryId) { throw new Error('libraryId is required'); } diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.ts index 8f11e49a4e..d283019082 100644 --- a/src/library-authoring/data/apiHook.ts +++ b/src/library-authoring/data/apiHook.ts @@ -8,9 +8,8 @@ import { getContentLibrary } from './api'; /** * Hook to fetch a content library by its ID. - * @param {string} [libraryId] - The ID of the library to fetch. */ -export const useContentLibrary = (libraryId) => ( +export const useContentLibrary = (libraryId?: string) => ( useQuery({ queryKey: ['contentLibrary', libraryId], queryFn: () => getContentLibrary(libraryId), @@ -19,10 +18,8 @@ export const useContentLibrary = (libraryId) => ( /** * Hook to fetch the count of components and collections in a library. - * @param {string} libraryId - The ID of the library to fetch. - * @param {string} searchKeywords - Keywords to search for. */ -export const useLibraryComponentCount = (libraryId, searchKeywords) => { +export const useLibraryComponentCount = (libraryId: string, searchKeywords: string) => { // Meilisearch code to get Collection and Component counts const { data: connectionDetails } = useContentSearchConnection(); diff --git a/src/library-authoring/data/types.d.ts b/src/library-authoring/data/types.d.ts index 34486525d7..06dd3f2704 100644 --- a/src/library-authoring/data/types.d.ts +++ b/src/library-authoring/data/types.d.ts @@ -1,18 +1,17 @@ -/** - * @typedef {Object} ContentLibrary - * @property {string} id - * @property {string} type - * @property {string} org - * @property {string} slug - * @property {string} title - * @property {string} description - * @property {number} numBlocks - * @property {number} version - * @property {Date | null} lastPublished - * @property {boolean} allowLti - * @property {boolean} allowPublicLearning - * @property {boolean} allowPublicRead - * @property {boolean} hasUnpublishedChanges - * @property {boolean} hasUnpublishedDeletes - * @property {string} license - */ +export type ContentLibrary = { + id: string; + type: string; + org: string; + slug: string; + title: string; + description: string; + numBlocks: number; + version: number; + lastPublished: Date | null; + allowLti: boolean; + allowPublicLearning: boolean; + allowPublicRead: boolean; + hasUnpublishedChanges: boolean; + hasUnpublishedDeletes: boolean; + license: string; +}; diff --git a/src/library-authoring/index.ts b/src/library-authoring/index.ts index 69831c4ed9..2ef32e858d 100644 --- a/src/library-authoring/index.ts +++ b/src/library-authoring/index.ts @@ -1,4 +1,3 @@ // @ts-check -// eslint-disable-next-line import/prefer-default-export export { default as LibraryAuthoringPage } from './LibraryAuthoringPage'; export { default as CreateLibrary } from './CreateLibrary'; From 5687b7612fcea8805fa92c13a98d9372d787a564 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 24 Jun 2024 14:40:10 -0500 Subject: [PATCH 067/106] test: Added for LibraryComponents --- src/library-authoring/__mocks__/index.js | 2 + .../__mocks__/libraryComponentsMock.js | 74 +++++++ .../components/ComponentCard.jsx | 2 +- .../components/LibraryComponents.test.jsx | 188 ++++++++++++++++++ src/library-authoring/data/api.js | 2 +- 5 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 src/library-authoring/__mocks__/index.js create mode 100644 src/library-authoring/__mocks__/libraryComponentsMock.js create mode 100644 src/library-authoring/components/LibraryComponents.test.jsx diff --git a/src/library-authoring/__mocks__/index.js b/src/library-authoring/__mocks__/index.js new file mode 100644 index 0000000000..6d72558350 --- /dev/null +++ b/src/library-authoring/__mocks__/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as libraryComponentsMock } from './libraryComponentsMock'; diff --git a/src/library-authoring/__mocks__/libraryComponentsMock.js b/src/library-authoring/__mocks__/libraryComponentsMock.js new file mode 100644 index 0000000000..8f3dfa2a7f --- /dev/null +++ b/src/library-authoring/__mocks__/libraryComponentsMock.js @@ -0,0 +1,74 @@ +module.exports = [ + { + id: '1', + displayName: 'Text', + formatted: { + content: { + htmlContent: 'This is a text: ID=1', + }, + }, + tags: { + level0: ['1', '2', '3'], + }, + blockType: 'text', + }, + { + id: '2', + displayName: 'Text', + formatted: { + content: { + htmlContent: 'This is a text: ID=2', + }, + }, + tags: { + level0: ['1', '2', '3'], + }, + blockType: 'text', + }, + { + id: '3', + displayName: 'Video', + formatted: { + content: { + htmlContent: 'This is a video: ID=3', + }, + }, + tags: { + level0: ['1', '2'], + }, + blockType: 'video', + }, + { + id: '4', + displayName: 'Video', + formatted: { + content: { + htmlContent: 'This is a video: ID=4', + }, + }, + tags: { + level0: ['1', '2'], + }, + blockType: 'text', + }, + { + id: '5', + displayName: 'Problem', + formatted: { + content: { + htmlContent: 'This is a problem: ID=5', + }, + }, + blockType: 'problem', + }, + { + id: '6', + displayName: 'Problem', + formatted: { + content: { + htmlContent: 'This is a problem: ID=6', + }, + }, + blockType: 'problem', + }, +]; diff --git a/src/library-authoring/components/ComponentCard.jsx b/src/library-authoring/components/ComponentCard.jsx index 2529b1fd73..6680f8b007 100644 --- a/src/library-authoring/components/ComponentCard.jsx +++ b/src/library-authoring/components/ComponentCard.jsx @@ -46,7 +46,7 @@ const ComponentCardMenu = () => ( ); export const ComponentCardLoading = () => ( - + diff --git a/src/library-authoring/components/LibraryComponents.test.jsx b/src/library-authoring/components/LibraryComponents.test.jsx new file mode 100644 index 0000000000..92141b6fbe --- /dev/null +++ b/src/library-authoring/components/LibraryComponents.test.jsx @@ -0,0 +1,188 @@ +// @ts-check +import React from 'react'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, screen, fireEvent } from '@testing-library/react'; +import LibraryComponents from './LibraryComponents'; + +import initializeStore from '../../store'; +import { libraryComponentsMock } from '../__mocks__'; + +const mockUseLibraryComponents = jest.fn(); +const mockUseLibraryComponentCount = jest.fn(); +const mockUseLibraryBlockTypes = jest.fn(); +const mockFetchNextPage = jest.fn(); +let store; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const data = { + hits: [], + isFetching: true, + isFetchingNextPage: false, + hasNextPage: false, + fetchNextPage: mockFetchNextPage, +}; +const countData = { + componentCount: 1, + collectionCount: 0, +}; +const blockTypeData = { + data: [ + { + blockType: 'html', + displayName: 'Text', + }, + { + blockType: 'video', + displayName: 'Video', + }, + { + blockType: 'problem', + displayName: 'Problem', + }, + ], +}; + +jest.mock('../data/apiHook', () => ({ + useLibraryComponents: () => mockUseLibraryComponents(), + useLibraryComponentCount: () => mockUseLibraryComponentCount(), + useLibraryBlockTypes: () => mockUseLibraryBlockTypes(), +})); + +const RootWrapper = (props) => ( + + + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + mockUseLibraryComponents.mockReturnValue(data); + mockUseLibraryComponentCount.mockReturnValue(countData); + mockUseLibraryBlockTypes.mockReturnValue(blockTypeData); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should render empty state', async () => { + mockUseLibraryComponentCount.mockReturnValueOnce({ + ...countData, + componentCount: 0, + }); + render(); + expect(await screen.findByText(/you have not added any content to this library yet\./i)); + }); + + it('should render loading', async () => { + render(); + expect((await screen.findAllByTestId('card-loading'))[0]).toBeInTheDocument(); + }); + + it('should render components in full variant', async () => { + mockUseLibraryComponents.mockReturnValue({ + ...data, + hits: libraryComponentsMock, + isFetching: false, + }); + render(); + + expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument(); + expect(screen.getByText('This is a text: ID=2')).toBeInTheDocument(); + expect(screen.getByText('This is a video: ID=3')).toBeInTheDocument(); + expect(screen.getByText('This is a video: ID=4')).toBeInTheDocument(); + expect(screen.getByText('This is a problem: ID=5')).toBeInTheDocument(); + expect(screen.getByText('This is a problem: ID=6')).toBeInTheDocument(); + }); + + it('should render components in preview variant', async () => { + mockUseLibraryComponents.mockReturnValue({ + ...data, + hits: libraryComponentsMock, + isFetching: false, + }); + render(); + + expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument(); + expect(screen.getByText('This is a text: ID=2')).toBeInTheDocument(); + expect(screen.getByText('This is a video: ID=3')).toBeInTheDocument(); + expect(screen.getByText('This is a video: ID=4')).toBeInTheDocument(); + expect(screen.queryByText('This is a problem: ID=5')).not.toBeInTheDocument(); + expect(screen.queryByText('This is a problem: ID=6')).not.toBeInTheDocument(); + }); + + it('should call `fetchNextPage` on scroll to bottom in full variant', async () => { + mockUseLibraryComponents.mockReturnValue({ + ...data, + hits: libraryComponentsMock, + isFetching: false, + hasNextPage: true, + }); + + render(); + + Object.defineProperty(window, 'innerHeight', { value: 800 }); + Object.defineProperty(document.body, 'scrollHeight', { value: 1600 }); + + fireEvent.scroll(window, { target: { scrollY: 1000 } }); + + expect(mockFetchNextPage).toHaveBeenCalled(); + }); + + it('should not call `fetchNextPage` on croll to bottom in preview variant', async () => { + mockUseLibraryComponents.mockReturnValue({ + ...data, + hits: libraryComponentsMock, + isFetching: false, + hasNextPage: true, + }); + + render(); + + Object.defineProperty(window, 'innerHeight', { value: 800 }); + Object.defineProperty(document.body, 'scrollHeight', { value: 1600 }); + + fireEvent.scroll(window, { target: { scrollY: 1000 } }); + + expect(mockFetchNextPage).not.toHaveBeenCalled(); + }); + + it('should render content and loading when fetching next page', async () => { + mockUseLibraryComponents.mockReturnValue({ + ...data, + hits: libraryComponentsMock, + isFetching: true, + isFetchingNextPage: true, + hasNextPage: true, + }); + + render(); + + expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument(); + expect(screen.getByText('This is a problem: ID=6')).toBeInTheDocument(); + + expect((await screen.findAllByTestId('card-loading'))[0]).toBeInTheDocument(); + }); +}); diff --git a/src/library-authoring/data/api.js b/src/library-authoring/data/api.js index a7b2cb25b4..ef3a397fad 100644 --- a/src/library-authoring/data/api.js +++ b/src/library-authoring/data/api.js @@ -30,7 +30,7 @@ export async function getContentLibrary(libraryId) { /** * Fetch block types of a library - * @param {string} libraryId + * @param {string} [libraryId] * @returns {Promise} */ export async function getLibraryBlockTypes(libraryId) { From 51d36ceb6c6e3d6612d671e0efbd20c419805b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 25 Jun 2024 10:49:59 -0300 Subject: [PATCH 068/106] fix: remove @ts-check --- src/library-authoring/CreateLibrary.tsx | 1 - src/library-authoring/EmptyStates.tsx | 1 - .../LibraryAuthoringPage.test.tsx | 1 - .../LibraryAuthoringPage.tsx | 1 - src/library-authoring/LibraryCollections.tsx | 1 - src/library-authoring/LibraryComponents.tsx | 1 - src/library-authoring/LibraryHome.tsx | 1 - src/library-authoring/data/api.ts | 21 ++++++++++++++++--- src/library-authoring/data/apiHook.ts | 1 - src/library-authoring/index.ts | 1 - src/library-authoring/messages.ts | 1 - 11 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/library-authoring/CreateLibrary.tsx b/src/library-authoring/CreateLibrary.tsx index 74d80c0328..227f14dbe5 100644 --- a/src/library-authoring/CreateLibrary.tsx +++ b/src/library-authoring/CreateLibrary.tsx @@ -1,4 +1,3 @@ -// @ts-check import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Container } from '@openedx/paragon'; diff --git a/src/library-authoring/EmptyStates.tsx b/src/library-authoring/EmptyStates.tsx index 54e3b8019d..d7b718c71d 100644 --- a/src/library-authoring/EmptyStates.tsx +++ b/src/library-authoring/EmptyStates.tsx @@ -1,4 +1,3 @@ -// @ts-check import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 15b0d8200c..db7e815a10 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -1,4 +1,3 @@ -// @ts-check import React from 'react'; import MockAdapter from 'axios-mock-adapter'; import { initializeMockApp } from '@edx/frontend-platform'; diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 420ba8d4cf..305c9e753d 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -1,4 +1,3 @@ -// @ts-check import React, { useEffect, useState } from 'react'; import { StudioFooter } from '@edx/frontend-component-footer'; import { useIntl } from '@edx/frontend-platform/i18n'; diff --git a/src/library-authoring/LibraryCollections.tsx b/src/library-authoring/LibraryCollections.tsx index c364ce9a82..2f1eb8951f 100644 --- a/src/library-authoring/LibraryCollections.tsx +++ b/src/library-authoring/LibraryCollections.tsx @@ -1,4 +1,3 @@ -// @ts-check import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; diff --git a/src/library-authoring/LibraryComponents.tsx b/src/library-authoring/LibraryComponents.tsx index 786250a54a..fee8cb3502 100644 --- a/src/library-authoring/LibraryComponents.tsx +++ b/src/library-authoring/LibraryComponents.tsx @@ -1,4 +1,3 @@ -// @ts-check import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; diff --git a/src/library-authoring/LibraryHome.tsx b/src/library-authoring/LibraryHome.tsx index 56e8213353..1201e8a848 100644 --- a/src/library-authoring/LibraryHome.tsx +++ b/src/library-authoring/LibraryHome.tsx @@ -1,4 +1,3 @@ -// @ts-check import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index e78eb92ba5..95126d8269 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -1,15 +1,30 @@ -// @ts-check import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import type { ContentLibrary } from './types'; - const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; /** * Get the URL for the content library API. */ export const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; +export interface ContentLibrary { + id: string; + type: string; + org: string; + slug: string; + title: string; + description: string; + numBlocks: number; + version: number; + lastPublished: Date | null; + allowLti: boolean; + allowPublicLearning: boolean; + allowPublicRead: boolean; + hasUnpublishedChanges: boolean; + hasUnpublishedDeletes: boolean; + license: string; +} + /** * Fetch a content library by its ID. */ diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.ts index d283019082..56a6791d26 100644 --- a/src/library-authoring/data/apiHook.ts +++ b/src/library-authoring/data/apiHook.ts @@ -1,4 +1,3 @@ -// @ts-check import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { MeiliSearch } from 'meilisearch'; diff --git a/src/library-authoring/index.ts b/src/library-authoring/index.ts index 2ef32e858d..40da2db4af 100644 --- a/src/library-authoring/index.ts +++ b/src/library-authoring/index.ts @@ -1,3 +1,2 @@ -// @ts-check export { default as LibraryAuthoringPage } from './LibraryAuthoringPage'; export { default as CreateLibrary } from './CreateLibrary'; diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts index ff985aa62c..5be2437a98 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -1,4 +1,3 @@ -// @ts-check import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ From fbd418db9816bad0115e64fa5501d3ce545c5bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 25 Jun 2024 10:53:30 -0300 Subject: [PATCH 069/106] fix: remove unused file --- src/library-authoring/data/types.d.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 src/library-authoring/data/types.d.ts diff --git a/src/library-authoring/data/types.d.ts b/src/library-authoring/data/types.d.ts deleted file mode 100644 index 06dd3f2704..0000000000 --- a/src/library-authoring/data/types.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type ContentLibrary = { - id: string; - type: string; - org: string; - slug: string; - title: string; - description: string; - numBlocks: number; - version: number; - lastPublished: Date | null; - allowLti: boolean; - allowPublicLearning: boolean; - allowPublicRead: boolean; - hasUnpublishedChanges: boolean; - hasUnpublishedDeletes: boolean; - license: string; -}; From af011f4912e3425a0ecd339baebddbc88539e112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 25 Jun 2024 15:04:50 -0300 Subject: [PATCH 070/106] feat: use search manager --- .../LibraryAuthoringPage.jsx | 90 +++++++++++-------- src/library-authoring/LibraryHome.jsx | 8 +- .../components/LibraryComponents.jsx | 9 +- 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.jsx index 358b9f5494..751d31edf0 100644 --- a/src/library-authoring/LibraryAuthoringPage.jsx +++ b/src/library-authoring/LibraryAuthoringPage.jsx @@ -15,6 +15,12 @@ import Loading from '../generic/Loading'; import SubHeader from '../generic/sub-header/SubHeader'; import Header from '../header'; import NotFoundAlert from '../generic/NotFoundAlert'; +import { SearchContextProvider } from '../search-modal/manager/SearchManager'; +import SearchKeywordsField from '../search-modal/SearchKeywordsField'; +import ClearFiltersButton from '../search-modal/ClearFiltersButton'; +import FilterByBlockType from '../search-modal/FilterByBlockType'; +import FilterByTags from '../search-modal/FilterByTags'; +import Stats from '../search-modal/Stats'; import LibraryComponents from './components/LibraryComponents'; import LibraryCollections from './LibraryCollections'; import LibraryHome from './LibraryHome'; @@ -87,46 +93,52 @@ const LibraryAuthoringPage = () => { contentId={libraryId} isLibrary /> - - } - subtitle={intl.formatMessage(messages.headingSubtitle)} - /> - setSearchKeywords(value)} - className="w-50" - /> - - - - - - - } + + + } + subtitle={intl.formatMessage(messages.headingSubtitle)} /> - } - /> - } - /> - } - /> - - + +
+ + + +
+
+
+ + + + + + + } + /> + } + /> + } + /> + } + /> + + + ); diff --git a/src/library-authoring/LibraryHome.jsx b/src/library-authoring/LibraryHome.jsx index 4de98283b8..a7edeb388c 100644 --- a/src/library-authoring/LibraryHome.jsx +++ b/src/library-authoring/LibraryHome.jsx @@ -7,6 +7,7 @@ import { } from '@openedx/paragon'; import { NoComponents, NoSearchResults } from './EmptyStates'; +import { useSearchContext } from '../search-modal/manager/SearchManager'; import LibraryCollections from './LibraryCollections'; import LibraryComponents from './components/LibraryComponents'; import { useLibraryComponentCount } from './data/apiHook'; @@ -39,7 +40,12 @@ const Section = ({ title, children }) => ( */ const LibraryHome = ({ libraryId, filter }) => { const { searchKeywords } = filter; - const { componentCount, collectionCount } = useLibraryComponentCount(libraryId, searchKeywords); + + const { + totalHits: componentCount, + } = useSearchContext(); + + const collectionCount = 0; if (componentCount === 0) { return searchKeywords === '' ? : ; diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx index 8c6ca549e2..a1699920fc 100644 --- a/src/library-authoring/components/LibraryComponents.jsx +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -1,10 +1,11 @@ // @ts-check /* eslint-disable react/prop-types */ import React, { useEffect, useMemo } from 'react'; - import { CardGrid } from '@openedx/paragon'; + +import { useSearchContext } from '../../search-modal/manager/SearchManager'; import { NoComponents, NoSearchResults } from '../EmptyStates'; -import { useLibraryBlockTypes, useLibraryComponentCount, useLibraryComponents } from '../data/apiHook'; +import { useLibraryBlockTypes } from '../data/apiHook'; import { ComponentCard, ComponentCardLoading } from './ComponentCard'; /** @@ -27,14 +28,14 @@ const LibraryComponents = ({ filter: { searchKeywords }, variant, }) => { - const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); const { hits, + totalHists: componentCount, isFetching, isFetchingNextPage, hasNextPage, fetchNextPage, - } = useLibraryComponents(libraryId, searchKeywords); + } = useSearchContext(); const { componentList, tagCounts } = useMemo(() => { const result = variant === 'preview' ? hits.slice(0, 4) : hits; From 5c0fadb2088a1c69aac41948035391391e1086bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 25 Jun 2024 15:15:00 -0300 Subject: [PATCH 071/106] refactor: cleanup code --- .../LibraryAuthoringPage.jsx | 7 +-- src/library-authoring/LibraryHome.jsx | 9 +-- .../components/LibraryComponents.jsx | 6 +- src/library-authoring/data/apiHook.js | 63 ------------------- 4 files changed, 6 insertions(+), 79 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.jsx index 751d31edf0..9b3bc8a330 100644 --- a/src/library-authoring/LibraryAuthoringPage.jsx +++ b/src/library-authoring/LibraryAuthoringPage.jsx @@ -4,7 +4,7 @@ import React, { useEffect } from 'react'; import { StudioFooter } from '@edx/frontend-component-footer'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { - Container, Icon, IconButton, SearchField, Tab, Tabs, + Container, Icon, IconButton, Tab, Tabs, } from '@openedx/paragon'; import { InfoOutline } from '@openedx/paragon/icons'; import { @@ -53,7 +53,6 @@ const LibraryAuthoringPage = () => { const location = useLocation(); const navigate = useNavigate(); const [tabKey, setTabKey] = React.useState(TAB_LIST.home); - const [searchKeywords, setSearchKeywords] = React.useState(''); const { libraryId } = useParams(); @@ -122,11 +121,11 @@ const LibraryAuthoringPage = () => { } + element={} /> } + element={} /> ( /** * @type {React.FC<{ * libraryId: string, - * filter: { - * searchKeywords: string, - * }, * }>} */ -const LibraryHome = ({ libraryId, filter }) => { - const { searchKeywords } = filter; - +const LibraryHome = ({ libraryId }) => { const { totalHits: componentCount, + searchKeywords, } = useSearchContext(); const collectionCount = 0; diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx index a1699920fc..3ef8f27217 100644 --- a/src/library-authoring/components/LibraryComponents.jsx +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -17,20 +17,16 @@ import { ComponentCard, ComponentCardLoading } from './ComponentCard'; * * @type {React.FC<{ * libraryId: string, - * filter: { - * searchKeywords: string, - * }, * variant: 'full'|'preview', * }>} */ const LibraryComponents = ({ libraryId, - filter: { searchKeywords }, variant, }) => { const { hits, - totalHists: componentCount, + totalHits: componentCount, isFetching, isFetchingNextPage, hasNextPage, diff --git a/src/library-authoring/data/apiHook.js b/src/library-authoring/data/apiHook.js index bdc9200aaf..ca2a21c5c6 100644 --- a/src/library-authoring/data/apiHook.js +++ b/src/library-authoring/data/apiHook.js @@ -1,9 +1,6 @@ // @ts-check -import React from 'react'; import { useQuery } from '@tanstack/react-query'; -import { MeiliSearch } from 'meilisearch'; -import { useContentSearchConnection, useContentSearchResults } from '../../search-modal'; import { getContentLibrary, getLibraryBlockTypes } from './api'; /** @@ -27,63 +24,3 @@ export const useLibraryBlockTypes = (libraryId) => ( queryFn: () => getLibraryBlockTypes(libraryId), }) ); - -/** - * Hook to fetch components in a library. - * @param {string} libraryId - The ID of the library to fetch. - * @param {string} searchKeywords - Keywords to search for. - */ -export const useLibraryComponents = (libraryId, searchKeywords) => { - const { data: connectionDetails } = useContentSearchConnection(); - - const indexName = connectionDetails?.indexName; - const client = React.useMemo(() => { - if (connectionDetails?.apiKey === undefined || connectionDetails?.url === undefined) { - return undefined; - } - return new MeiliSearch({ host: connectionDetails.url, apiKey: connectionDetails.apiKey }); - }, [connectionDetails?.apiKey, connectionDetails?.url]); - - const libFilter = `context_key = "${libraryId}"`; - - return useContentSearchResults({ - client, - indexName, - searchKeywords, - extraFilter: [libFilter], - }); -}; - -/** - * Hook to fetch the count of components and collections in a library. - * @param {string} libraryId - The ID of the library to fetch. - * @param {string} searchKeywords - Keywords to search for. - */ -export const useLibraryComponentCount = (libraryId, searchKeywords) => { - // Meilisearch code to get Collection and Component counts - const { data: connectionDetails } = useContentSearchConnection(); - - const indexName = connectionDetails?.indexName; - const client = React.useMemo(() => { - if (connectionDetails?.apiKey === undefined || connectionDetails?.url === undefined) { - return undefined; - } - return new MeiliSearch({ host: connectionDetails.url, apiKey: connectionDetails.apiKey }); - }, [connectionDetails?.apiKey, connectionDetails?.url]); - - const libFilter = `context_key = "${libraryId}"`; - - const { totalHits: componentCount } = useContentSearchResults({ - client, - indexName, - searchKeywords, - extraFilter: [libFilter], // ToDo: Add filter for components when collection is implemented - }); - - const collectionCount = 0; // ToDo: Implement collections count - - return { - componentCount, - collectionCount, - }; -}; From 3a679274649b5ec8ea6d6f736ab0634ff7200ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 25 Jun 2024 15:16:40 -0300 Subject: [PATCH 072/106] fix: more cleanup --- src/library-authoring/LibraryHome.jsx | 2 +- src/library-authoring/components/LibraryComponents.jsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/library-authoring/LibraryHome.jsx b/src/library-authoring/LibraryHome.jsx index b4f380f0f4..8843910336 100644 --- a/src/library-authoring/LibraryHome.jsx +++ b/src/library-authoring/LibraryHome.jsx @@ -55,7 +55,7 @@ const LibraryHome = ({ libraryId }) => {
- +
); diff --git a/src/library-authoring/components/LibraryComponents.jsx b/src/library-authoring/components/LibraryComponents.jsx index 3ef8f27217..d216d9d226 100644 --- a/src/library-authoring/components/LibraryComponents.jsx +++ b/src/library-authoring/components/LibraryComponents.jsx @@ -31,6 +31,7 @@ const LibraryComponents = ({ isFetchingNextPage, hasNextPage, fetchNextPage, + searchKeywords, } = useSearchContext(); const { componentList, tagCounts } = useMemo(() => { From b4374e9228cd1747f4d7943ccfb595e2b0be9682 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Wed, 26 Jun 2024 22:05:07 +0300 Subject: [PATCH 073/106] feat: Adjust library home filter styles --- src/index.scss | 2 +- src/library-authoring/LibraryAuthoringPage.jsx | 2 +- src/search-modal/FilterBy.scss | 7 +++++++ src/search-modal/FilterByBlockType.jsx | 2 ++ src/search-modal/FilterByTags.jsx | 10 ++++++++-- src/search-modal/SearchFilterWidget.jsx | 8 +++++++- src/search-modal/SearchModal.scss | 16 ---------------- src/search-modal/index.scss | 2 ++ 8 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 src/search-modal/FilterBy.scss create mode 100644 src/search-modal/index.scss diff --git a/src/index.scss b/src/index.scss index 381ca17082..9cd7d4c3bc 100644 --- a/src/index.scss +++ b/src/index.scss @@ -26,7 +26,7 @@ @import "textbooks/Textbooks"; @import "content-tags-drawer/ContentTagsDropDownSelector"; @import "content-tags-drawer/ContentTagsCollapsible"; -@import "search-modal/SearchModal"; +@import "search-modal"; @import "certificates/scss/Certificates"; @import "group-configurations/GroupConfigurations"; @import "library-authoring"; diff --git a/src/library-authoring/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.jsx index 9b3bc8a330..543713dadd 100644 --- a/src/library-authoring/LibraryAuthoringPage.jsx +++ b/src/library-authoring/LibraryAuthoringPage.jsx @@ -102,8 +102,8 @@ const LibraryAuthoringPage = () => { />
- +
diff --git a/src/search-modal/FilterBy.scss b/src/search-modal/FilterBy.scss new file mode 100644 index 0000000000..189f31f11c --- /dev/null +++ b/src/search-modal/FilterBy.scss @@ -0,0 +1,7 @@ +// Options for the "filter by tag/block type" menu +.pgn__menu.filter-by-refinement-menu { + .pgn__menu-item { + // Make the "filter by tag/block type" menu expand to fit the tags hierarchy and longer block type names + width: 100%; + } +} diff --git a/src/search-modal/FilterByBlockType.jsx b/src/search-modal/FilterByBlockType.jsx index f39bd17233..f877f1f488 100644 --- a/src/search-modal/FilterByBlockType.jsx +++ b/src/search-modal/FilterByBlockType.jsx @@ -8,6 +8,7 @@ import { Menu, MenuItem, } from '@openedx/paragon'; +import { FilterList } from '@openedx/paragon/icons'; import SearchFilterWidget from './SearchFilterWidget'; import messages from './messages'; import BlockTypeLabel from './BlockTypeLabel'; @@ -72,6 +73,7 @@ const FilterByBlockType = () => { ({ label: }))} label={} + icon={FilterList} > { ({ label: tf.split(TAG_SEP).pop() }))} label={} + icon={Tag} > { placeholder={intl.formatMessage(messages.searchTagsByKeywordPlaceholder)} className="mx-3 mb-1" /> - + etc. * - * @type {React.FC<{appliedFilters: {label: React.ReactNode}[], label: React.ReactNode, children: React.ReactNode}>} + * @type {React.FC<{ + * appliedFilters: {label: React.ReactNode}[], + * label: React.ReactNode, + * children: React.ReactNode, + * icon?: React.ReactNode, + * }>} */ const SearchFilterWidget = ({ appliedFilters, ...props }) => { const [isOpen, open, close] = useToggle(false); @@ -34,6 +39,7 @@ const SearchFilterWidget = ({ appliedFilters, ...props }) => { variant={appliedFilters.length ? 'light' : 'outline-primary'} size="sm" onClick={open} + iconBefore={props.icon} iconAfter={ArrowDropDown} > {props.label} diff --git a/src/search-modal/SearchModal.scss b/src/search-modal/SearchModal.scss index eb1f7b17cc..d14fdac832 100644 --- a/src/search-modal/SearchModal.scss +++ b/src/search-modal/SearchModal.scss @@ -39,22 +39,6 @@ } } - // Options for the "filter by tag" menu - .pgn__menu.tags-refinement-menu { - .pgn__menu-item { - // Make the "filter by tag" menu much wider than normal, because we need the space to display the tags hierarchy - width: 100%; - } - } - - // Options for the "filter by block type" menu - .pgn__menu.block-type-refinement-menu { - .pgn__menu-item { - // Make the "filter by block type" menu expand to fit longer block types names - width: 100%; - } - } - .pgn__menu-item { // Fix a bug in Paragon menu dropdowns: the checkbox currently shrinks if the text is too long. // https://github.com/openedx/paragon/pull/3019 diff --git a/src/search-modal/index.scss b/src/search-modal/index.scss new file mode 100644 index 0000000000..150baad66e --- /dev/null +++ b/src/search-modal/index.scss @@ -0,0 +1,2 @@ +@import "search-modal/SearchModal"; +@import "search-modal/FilterBy"; From bd3d2c54b57f630fc4496aea556042d5e478f7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 28 Jun 2024 15:53:47 -0300 Subject: [PATCH 074/106] fix: remove LibraryV2Placeholder --- src/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.jsx b/src/index.jsx index 283d2544aa..bf7ee9c423 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -24,7 +24,6 @@ import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; import { StudioHome } from './studio-home'; -import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; From 2a5d42b523fd3a6f2f450b726a98edfc28afd5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Sun, 30 Jun 2024 16:20:51 -0300 Subject: [PATCH 075/106] fix: optional parameters --- src/search-modal/data/apiHooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/search-modal/data/apiHooks.ts b/src/search-modal/data/apiHooks.ts index 6ecdd02b5d..fe77482285 100644 --- a/src/search-modal/data/apiHooks.ts +++ b/src/search-modal/data/apiHooks.ts @@ -47,9 +47,9 @@ export const useContentSearchResults = ({ /** The keywords that the user is searching for, if any */ searchKeywords: string; /** Only search for these block types (e.g. `["html", "problem"]`) */ - blockTypesFilter: string[]; + blockTypesFilter?: string[]; /** Required tags (all must match), e.g. `["Difficulty > Hard", "Subject > Math"]` */ - tagsFilter: string[]; + tagsFilter?: string[]; }) => { const query = useInfiniteQuery({ enabled: client !== undefined && indexName !== undefined, From 62fe7f5c07e32806b05380ee71af62fe8f14cf24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Sun, 30 Jun 2024 16:33:22 -0300 Subject: [PATCH 076/106] chore: trigger CI From 31012df7989a611fd3658dc8f7ad1159e44e3507 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 1 Jul 2024 11:00:18 +0200 Subject: [PATCH 077/106] refactor: Migrate block-type-utils to TS --- .../block-type-utils/{constants.js => constants.ts} | 12 +++++++++--- .../block-type-utils/{index.jsx => index.tsx} | 12 ++++-------- .../components/LibraryComponents.tsx | 1 - src/library-authoring/data/apiHook.ts | 6 ++---- 4 files changed, 15 insertions(+), 16 deletions(-) rename src/generic/block-type-utils/{constants.js => constants.ts} (88%) rename src/generic/block-type-utils/{index.jsx => index.tsx} (55%) diff --git a/src/generic/block-type-utils/constants.js b/src/generic/block-type-utils/constants.ts similarity index 88% rename from src/generic/block-type-utils/constants.js rename to src/generic/block-type-utils/constants.ts index 6739470945..16cb3b02b8 100644 --- a/src/generic/block-type-utils/constants.js +++ b/src/generic/block-type-utils/constants.ts @@ -1,3 +1,4 @@ +import React from 'react'; import { BackHand as BackHandIcon, BookOpen as BookOpenIcon, @@ -14,6 +15,11 @@ import { Folder, } from '@openedx/paragon/icons'; + +interface TypeIconsMap { + [key: string]: React.ReactElement; +} + export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock']; export const COMPONENT_TYPES = { @@ -27,7 +33,7 @@ export const COMPONENT_TYPES = { dragAndDrop: 'drag-and-drop-v2', }; -export const TYPE_ICONS_MAP = { +export const TYPE_ICONS_MAP: TypeIconsMap = { video: VideoCameraIcon, other: BookOpenIcon, vertical: FormatListBulletedIcon, @@ -35,7 +41,7 @@ export const TYPE_ICONS_MAP = { lock: LockIcon, }; -export const COMPONENT_TYPE_ICON_MAP = { +export const COMPONENT_TYPE_ICON_MAP: TypeIconsMap = { [COMPONENT_TYPES.advanced]: ScienceIcon, [COMPONENT_TYPES.discussion]: QuestionAnswerOutlineIcon, [COMPONENT_TYPES.library]: LibraryIcon, @@ -46,7 +52,7 @@ export const COMPONENT_TYPE_ICON_MAP = { [COMPONENT_TYPES.dragAndDrop]: BackHandIcon, }; -export const STRUCTURAL_TYPE_ICONS = { +export const STRUCTURAL_TYPE_ICONS: TypeIconsMap = { vertical: TYPE_ICONS_MAP.vertical, sequential: Folder, chapter: Folder, diff --git a/src/generic/block-type-utils/index.jsx b/src/generic/block-type-utils/index.tsx similarity index 55% rename from src/generic/block-type-utils/index.jsx rename to src/generic/block-type-utils/index.tsx index 73e6555fb8..9354bcdd91 100644 --- a/src/generic/block-type-utils/index.jsx +++ b/src/generic/block-type-utils/index.tsx @@ -1,19 +1,15 @@ -// @ts-check -import { - Article, -} from '@openedx/paragon/icons'; +import React from 'react'; +import { Article } from '@openedx/paragon/icons'; import { COMPONENT_TYPE_ICON_MAP, STRUCTURAL_TYPE_ICONS, COMPONENT_TYPE_COLOR_MAP, } from './constants'; -/** @param {string} blockType */ -export function getItemIcon(blockType) { +export function getItemIcon(blockType: string): React.ReactElement { return STRUCTURAL_TYPE_ICONS[blockType] ?? COMPONENT_TYPE_ICON_MAP[blockType] ?? Article; } -/** @param {string} blockType */ -export function getComponentColor(blockType) { +export function getComponentColor(blockType: string): string { return COMPONENT_TYPE_COLOR_MAP[blockType] ?? 'bg-component'; } diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index f90d5277c6..2b632d3df8 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/prop-types */ import React, { useEffect, useMemo } from 'react'; import { CardGrid } from '@openedx/paragon'; diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.ts index 6b3ea393ec..f188371863 100644 --- a/src/library-authoring/data/apiHook.ts +++ b/src/library-authoring/data/apiHook.ts @@ -16,7 +16,7 @@ export const useContentLibrary = (libraryId?: string) => ( ); /** - * Hook to fetch a content library by its ID. + * Hook to fetch block types of a library. */ export const useLibraryBlockTypes = (libraryId?: string) => ( useQuery({ @@ -27,10 +27,8 @@ export const useLibraryBlockTypes = (libraryId?: string) => ( /** * Hook to fetch components in a library. - * @param {string} libraryId - The ID of the library to fetch. - * @param {string} searchKeywords - Keywords to search for. */ -export const useLibraryComponents = (libraryId, searchKeywords) => { +export const useLibraryComponents = (libraryId: string, searchKeywords: string) => { const { data: connectionDetails } = useContentSearchConnection(); const indexName = connectionDetails?.indexName; From 42f2666c5187aef4c26b5c9985be124d91264270 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 1 Jul 2024 11:23:38 +0200 Subject: [PATCH 078/106] test: Fixt LibraryAuthoringPage test --- src/library-authoring/LibraryAuthoringPage.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 464c293b10..b14dc19d78 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -149,7 +149,7 @@ describe('', () => { axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); const { - getByRole, getByText, queryByText, + getByRole, getByText, queryByText, getAllByText, } = render(); // Ensure the search endpoint is called @@ -163,7 +163,7 @@ describe('', () => { expect(getByText('Recently Modified')).toBeInTheDocument(); expect(getByText('Collections (0)')).toBeInTheDocument(); expect(getByText('Components (6)')).toBeInTheDocument(); - expect(getByText('Test HTML Block')).toBeInTheDocument(); + expect(getAllByText('Test HTML Block')[0]).toBeInTheDocument(); // Navigate to the components tab fireEvent.click(getByRole('tab', { name: 'Components' })); From 99b9b31e05bc5f6f4df8aa83820016818afa1eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 2 Jul 2024 09:02:25 -0300 Subject: [PATCH 079/106] feat: fixing merge issues and tests --- .../components/LibraryComponents.test.tsx | 91 +++++++++++++------ .../components/LibraryComponents.tsx | 5 +- src/library-authoring/data/apiHook.ts | 1 - src/search-modal/manager/SearchManager.ts | 4 +- 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/src/library-authoring/components/LibraryComponents.test.tsx b/src/library-authoring/components/LibraryComponents.test.tsx index 83919d94e7..5dc689cb14 100644 --- a/src/library-authoring/components/LibraryComponents.test.tsx +++ b/src/library-authoring/components/LibraryComponents.test.tsx @@ -1,38 +1,60 @@ import React from 'react'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, fireEvent } from '@testing-library/react'; -import LibraryComponents from './LibraryComponents'; +import MockAdapter from 'axios-mock-adapter'; +import fetchMock from 'fetch-mock-jest'; +import type { Store } from 'redux'; +import { getContentSearchConfigUrl } from '../../search-modal/data/api'; +import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json'; +import { SearchContextProvider } from '../../search-modal/manager/SearchManager'; import initializeStore from '../../store'; import { libraryComponentsMock } from '../__mocks__'; +import LibraryComponents from './LibraryComponents'; + +const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; -const mockUseLibraryComponents = jest.fn(); -const mockUseLibraryComponentCount = jest.fn(); const mockUseLibraryBlockTypes = jest.fn(); const mockFetchNextPage = jest.fn(); -let store; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); +const mockUseSearchContext = jest.fn(); const data = { + totalHits: 1, hits: [], isFetching: true, isFetchingNextPage: false, hasNextPage: false, fetchNextPage: mockFetchNextPage, + searchKeywords: '', }; -const countData = { - componentCount: 1, - collectionCount: 0, + +let store: Store; +let axiosMock: MockAdapter; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const returnEmptyResult = (_url: string, req) => { + const requestData = JSON.parse(req.body?.toString() ?? ''); + const query = requestData?.queries[0]?.q ?? ''; + // We have to replace the query (search keywords) in the mock results with the actual query, + // because otherwise we may have an inconsistent state that causes more queries and unexpected results. + mockEmptyResult.results[0].query = query; + // And fake the required '_formatted' fields; it contains the highlighting ... around matched words + // eslint-disable-next-line no-underscore-dangle, no-param-reassign + mockEmptyResult.results[0]?.hits.forEach((hit: any) => { hit._formatted = { ...hit }; }); + return mockEmptyResult; }; + const blockTypeData = { data: [ { @@ -51,16 +73,21 @@ const blockTypeData = { }; jest.mock('../data/apiHook', () => ({ - useLibraryComponents: () => mockUseLibraryComponents(), - useLibraryComponentCount: () => mockUseLibraryComponentCount(), useLibraryBlockTypes: () => mockUseLibraryBlockTypes(), })); +jest.mock('../../search-modal/manager/SearchManager', () => ({ + ...jest.requireActual('../../search-modal/manager/SearchManager'), + useSearchContext: () => mockUseSearchContext(), +})); + const RootWrapper = (props) => ( - + + + @@ -77,9 +104,18 @@ describe('', () => { }, }); store = initializeStore(); - mockUseLibraryComponents.mockReturnValue(data); - mockUseLibraryComponentCount.mockReturnValue(countData); mockUseLibraryBlockTypes.mockReturnValue(blockTypeData); + mockUseSearchContext.mockReturnValue(data); + + fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); + + // The API method to get the Meilisearch connection details uses Axios: + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(getContentSearchConfigUrl()).reply(200, { + url: 'http://mock.meilisearch.local', + index_name: 'studio', + api_key: 'test-key', + }); }); afterEach(() => { @@ -87,10 +123,11 @@ describe('', () => { }); it('should render empty state', async () => { - mockUseLibraryComponentCount.mockReturnValueOnce({ - ...countData, - componentCount: 0, + mockUseSearchContext.mockReturnValue({ + ...data, + totalHits: 0, }); + render(); expect(await screen.findByText(/you have not added any content to this library yet\./i)); }); @@ -101,7 +138,7 @@ describe('', () => { }); it('should render components in full variant', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: false, @@ -117,7 +154,7 @@ describe('', () => { }); it('should render components in preview variant', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: false, @@ -133,7 +170,7 @@ describe('', () => { }); it('should call `fetchNextPage` on scroll to bottom in full variant', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: false, @@ -151,7 +188,7 @@ describe('', () => { }); it('should not call `fetchNextPage` on croll to bottom in preview variant', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: false, @@ -169,7 +206,7 @@ describe('', () => { }); it('should render content and loading when fetching next page', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: true, diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 1d416a2cc3..372dbb7226 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -8,10 +8,7 @@ import { ComponentCard, ComponentCardLoading } from './ComponentCard'; type LibraryComponentsProps = { libraryId: string, - filter: { - searchKeywords: string, - }, - variant: string, + variant: 'full' | 'preview', }; /** diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.ts index a6d9b13f5c..b7e6b92cf8 100644 --- a/src/library-authoring/data/apiHook.ts +++ b/src/library-authoring/data/apiHook.ts @@ -1,4 +1,3 @@ -import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { getContentLibrary, getLibraryBlockTypes } from './api'; diff --git a/src/search-modal/manager/SearchManager.ts b/src/search-modal/manager/SearchManager.ts index 7b1204fa7d..eed267b46c 100644 --- a/src/search-modal/manager/SearchManager.ts +++ b/src/search-modal/manager/SearchManager.ts @@ -42,8 +42,8 @@ export const SearchContextProvider: React.FC<{ closeSearchModal?: () => void, }> = ({ extraFilter, ...props }) => { const [searchKeywords, setSearchKeywords] = React.useState(''); - const [blockTypesFilter, setBlockTypesFilter] = React.useState(/** type {string[]} */([])); - const [tagsFilter, setTagsFilter] = React.useState(/** type {string[]} */([])); + const [blockTypesFilter, setBlockTypesFilter] = React.useState([]); + const [tagsFilter, setTagsFilter] = React.useState([]); const canClearFilters = blockTypesFilter.length > 0 || tagsFilter.length > 0; const clearFilters = React.useCallback(() => { From f9023351470baba40258a74e94698b0a7bc20506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 2 Jul 2024 11:30:03 -0300 Subject: [PATCH 080/106] refactor: create search-manager feature --- .../LibraryAuthoringPage.test.tsx | 2 +- .../LibraryAuthoringPage.tsx | 12 ++-- src/library-authoring/LibraryHome.tsx | 2 +- .../components/LibraryComponents.test.tsx | 8 +-- .../components/LibraryComponents.tsx | 2 +- .../BlockTypeLabel.tsx | 0 .../ClearFiltersButton.tsx | 2 +- .../FilterByBlockType.tsx | 2 +- .../FilterByTags.tsx | 2 +- .../SearchFilterWidget.tsx | 0 .../SearchKeywordsField.tsx | 2 +- .../SearchManager.ts | 4 +- .../data/api.ts | 10 +-- .../data/apiHooks.ts | 0 src/search-manager/index.ts | 8 +++ src/search-manager/messages.ts | 70 +++++++++++++++++++ src/search-modal/EmptyStates.tsx | 2 +- src/search-modal/Highlight.tsx | 8 +-- src/search-modal/SearchModal.test.tsx | 2 +- src/search-modal/SearchResult.tsx | 9 +-- src/search-modal/SearchResults.tsx | 2 +- src/search-modal/SearchUI.test.tsx | 2 +- src/search-modal/SearchUI.tsx | 12 ++-- src/search-modal/Stats.tsx | 2 +- src/search-modal/index.js | 3 - src/search-modal/index.ts | 2 + src/search-modal/messages.ts | 60 ---------------- 27 files changed, 126 insertions(+), 104 deletions(-) rename src/{search-modal => search-manager}/BlockTypeLabel.tsx (100%) rename src/{search-modal => search-manager}/ClearFiltersButton.tsx (91%) rename src/{search-modal => search-manager}/FilterByBlockType.tsx (98%) rename src/{search-modal => search-manager}/FilterByTags.tsx (99%) rename src/{search-modal => search-manager}/SearchFilterWidget.tsx (100%) rename src/{search-modal => search-manager}/SearchKeywordsField.tsx (94%) rename src/{search-modal/manager => search-manager}/SearchManager.ts (97%) rename src/{search-modal => search-manager}/data/api.ts (98%) rename src/{search-modal => search-manager}/data/apiHooks.ts (100%) create mode 100644 src/search-manager/index.ts create mode 100644 src/search-manager/messages.ts delete mode 100644 src/search-modal/index.js create mode 100644 src/search-modal/index.ts diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index b14dc19d78..ccb1e87c42 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -9,7 +9,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import fetchMock from 'fetch-mock-jest'; import initializeStore from '../store'; -import { getContentSearchConfigUrl } from '../search-modal/data/api'; +import { getContentSearchConfigUrl } from '../search-manager/data/api'; import mockResult from '../search-modal/__mocks__/search-result.json'; import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json'; import LibraryAuthoringPage from './LibraryAuthoringPage'; diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 44fe073027..ab4af5048b 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -13,11 +13,13 @@ import Loading from '../generic/Loading'; import SubHeader from '../generic/sub-header/SubHeader'; import Header from '../header'; import NotFoundAlert from '../generic/NotFoundAlert'; -import { SearchContextProvider } from '../search-modal/manager/SearchManager'; -import SearchKeywordsField from '../search-modal/SearchKeywordsField'; -import ClearFiltersButton from '../search-modal/ClearFiltersButton'; -import FilterByBlockType from '../search-modal/FilterByBlockType'; -import FilterByTags from '../search-modal/FilterByTags'; +import { + ClearFiltersButton, + FilterByBlockType, + FilterByTags, + SearchContextProvider, + SearchKeywordsField, +} from '../search-manager'; import Stats from '../search-modal/Stats'; import LibraryComponents from './components/LibraryComponents'; import LibraryCollections from './LibraryCollections'; diff --git a/src/library-authoring/LibraryHome.tsx b/src/library-authoring/LibraryHome.tsx index c21cfb300b..3b50df12c3 100644 --- a/src/library-authoring/LibraryHome.tsx +++ b/src/library-authoring/LibraryHome.tsx @@ -5,7 +5,7 @@ import { } from '@openedx/paragon'; import { NoComponents, NoSearchResults } from './EmptyStates'; -import { useSearchContext } from '../search-modal/manager/SearchManager'; +import { useSearchContext } from '../search-manager'; import LibraryCollections from './LibraryCollections'; import LibraryComponents from './components/LibraryComponents'; import messages from './messages'; diff --git a/src/library-authoring/components/LibraryComponents.test.tsx b/src/library-authoring/components/LibraryComponents.test.tsx index 5dc689cb14..54f10adbe5 100644 --- a/src/library-authoring/components/LibraryComponents.test.tsx +++ b/src/library-authoring/components/LibraryComponents.test.tsx @@ -9,9 +9,9 @@ import MockAdapter from 'axios-mock-adapter'; import fetchMock from 'fetch-mock-jest'; import type { Store } from 'redux'; -import { getContentSearchConfigUrl } from '../../search-modal/data/api'; +import { getContentSearchConfigUrl } from '../../search-manager/data/api'; +import { SearchContextProvider } from '../../search-manager/SearchManager'; import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json'; -import { SearchContextProvider } from '../../search-modal/manager/SearchManager'; import initializeStore from '../../store'; import { libraryComponentsMock } from '../__mocks__'; import LibraryComponents from './LibraryComponents'; @@ -76,8 +76,8 @@ jest.mock('../data/apiHook', () => ({ useLibraryBlockTypes: () => mockUseLibraryBlockTypes(), })); -jest.mock('../../search-modal/manager/SearchManager', () => ({ - ...jest.requireActual('../../search-modal/manager/SearchManager'), +jest.mock('../../search-manager', () => ({ + ...jest.requireActual('../../search-manager'), useSearchContext: () => mockUseSearchContext(), })); diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 372dbb7226..48288946b3 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo } from 'react'; import { CardGrid } from '@openedx/paragon'; -import { useSearchContext } from '../../search-modal/manager/SearchManager'; +import { useSearchContext } from '../../search-manager'; import { NoComponents, NoSearchResults } from '../EmptyStates'; import { useLibraryBlockTypes } from '../data/apiHook'; import { ComponentCard, ComponentCardLoading } from './ComponentCard'; diff --git a/src/search-modal/BlockTypeLabel.tsx b/src/search-manager/BlockTypeLabel.tsx similarity index 100% rename from src/search-modal/BlockTypeLabel.tsx rename to src/search-manager/BlockTypeLabel.tsx diff --git a/src/search-modal/ClearFiltersButton.tsx b/src/search-manager/ClearFiltersButton.tsx similarity index 91% rename from src/search-modal/ClearFiltersButton.tsx rename to src/search-manager/ClearFiltersButton.tsx index 7a29e51722..eeae127381 100644 --- a/src/search-modal/ClearFiltersButton.tsx +++ b/src/search-manager/ClearFiltersButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button } from '@openedx/paragon'; import messages from './messages'; -import { useSearchContext } from './manager/SearchManager'; +import { useSearchContext } from './SearchManager'; /** * A button that appears when at least one filter is active, and will clear the filters when clicked. diff --git a/src/search-modal/FilterByBlockType.tsx b/src/search-manager/FilterByBlockType.tsx similarity index 98% rename from src/search-modal/FilterByBlockType.tsx rename to src/search-manager/FilterByBlockType.tsx index 5aba1bc7df..ed16e2b059 100644 --- a/src/search-modal/FilterByBlockType.tsx +++ b/src/search-manager/FilterByBlockType.tsx @@ -9,7 +9,7 @@ import { import SearchFilterWidget from './SearchFilterWidget'; import messages from './messages'; import BlockTypeLabel from './BlockTypeLabel'; -import { useSearchContext } from './manager/SearchManager'; +import { useSearchContext } from './SearchManager'; /** * A button with a dropdown that allows filtering the current search by component type (XBlock type) diff --git a/src/search-modal/FilterByTags.tsx b/src/search-manager/FilterByTags.tsx similarity index 99% rename from src/search-modal/FilterByTags.tsx rename to src/search-manager/FilterByTags.tsx index d827713fcf..afdb440394 100644 --- a/src/search-modal/FilterByTags.tsx +++ b/src/search-manager/FilterByTags.tsx @@ -13,7 +13,7 @@ import { import { ArrowDropDown, ArrowDropUp, Warning } from '@openedx/paragon/icons'; import SearchFilterWidget from './SearchFilterWidget'; import messages from './messages'; -import { useSearchContext } from './manager/SearchManager'; +import { useSearchContext } from './SearchManager'; import { useTagFilterOptions } from './data/apiHooks'; import { LoadingSpinner } from '../generic/Loading'; import { TAG_SEP } from './data/api'; diff --git a/src/search-modal/SearchFilterWidget.tsx b/src/search-manager/SearchFilterWidget.tsx similarity index 100% rename from src/search-modal/SearchFilterWidget.tsx rename to src/search-manager/SearchFilterWidget.tsx diff --git a/src/search-modal/SearchKeywordsField.tsx b/src/search-manager/SearchKeywordsField.tsx similarity index 94% rename from src/search-modal/SearchKeywordsField.tsx rename to src/search-manager/SearchKeywordsField.tsx index e63d0eb59e..78bb3d9cd6 100644 --- a/src/search-modal/SearchKeywordsField.tsx +++ b/src/search-manager/SearchKeywordsField.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { SearchField } from '@openedx/paragon'; import messages from './messages'; -import { useSearchContext } from './manager/SearchManager'; +import { useSearchContext } from './SearchManager'; /** * The "main" input field where users type in search keywords. The search happens as they type (no need to press enter). diff --git a/src/search-modal/manager/SearchManager.ts b/src/search-manager/SearchManager.ts similarity index 97% rename from src/search-modal/manager/SearchManager.ts rename to src/search-manager/SearchManager.ts index eed267b46c..6db1d6031f 100644 --- a/src/search-modal/manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -8,8 +8,8 @@ import React from 'react'; import { MeiliSearch, type Filter } from 'meilisearch'; -import { ContentHit } from '../data/api'; -import { useContentSearchConnection, useContentSearchResults } from '../data/apiHooks'; +import { ContentHit } from './data/api'; +import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks'; export interface SearchContextData { client?: MeiliSearch; diff --git a/src/search-modal/data/api.ts b/src/search-manager/data/api.ts similarity index 98% rename from src/search-modal/data/api.ts rename to src/search-manager/data/api.ts index 1511c78ec1..d13ef2641b 100644 --- a/src/search-modal/data/api.ts +++ b/src/search-manager/data/api.ts @@ -7,12 +7,12 @@ export const getContentSearchConfigUrl = () => new URL( getConfig().STUDIO_BASE_URL, ).href; +export const HIGHLIGHT_PRE_TAG = '__meili-highlight__'; // Indicate the start of a highlighted (matching) term +export const HIGHLIGHT_POST_TAG = '__/meili-highlight__'; // Indicate the end of a highlighted (matching) term + /** The separator used for hierarchical tags in the search index, e.g. tags.level1 = "Subject > Math > Calculus" */ export const TAG_SEP = ' > '; -export const highlightPreTag = '__meili-highlight__'; // Indicate the start of a highlighted (matching) term -export const highlightPostTag = '__/meili-highlight__'; // Indicate the end of a highlighted (matching) term - /** * Get the content search configuration from the CMS. */ @@ -160,8 +160,8 @@ export async function fetchSearchResults({ ...tagsFilterFormatted, ], attributesToHighlight: ['display_name', 'content'], - highlightPreTag, - highlightPostTag, + highlightPreTag: HIGHLIGHT_PRE_TAG, + highlightPostTag: HIGHLIGHT_POST_TAG, attributesToCrop: ['content'], cropLength: 20, offset, diff --git a/src/search-modal/data/apiHooks.ts b/src/search-manager/data/apiHooks.ts similarity index 100% rename from src/search-modal/data/apiHooks.ts rename to src/search-manager/data/apiHooks.ts diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts new file mode 100644 index 0000000000..96396a7e70 --- /dev/null +++ b/src/search-manager/index.ts @@ -0,0 +1,8 @@ +export { SearchContextProvider, useSearchContext } from './SearchManager'; +export { default as ClearFiltersButton } from './ClearFiltersButton'; +export { default as FilterByBlockType } from './FilterByBlockType'; +export { default as FilterByTags } from './FilterByTags'; +export { default as SearchKeywordsField } from './SearchKeywordsField'; +export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG } from './data/api'; + +export type { ContentHit } from './data/api'; diff --git a/src/search-manager/messages.ts b/src/search-manager/messages.ts new file mode 100644 index 0000000000..73addfdfb1 --- /dev/null +++ b/src/search-manager/messages.ts @@ -0,0 +1,70 @@ +import { defineMessages as _defineMessages } from '@edx/frontend-platform/i18n'; +import type { defineMessages as defineMessagesType } from 'react-intl'; + +// frontend-platform currently doesn't provide types... do it ourselves. +const defineMessages = _defineMessages as typeof defineMessagesType; + +const messages = defineMessages({ + clearFilters: { + id: 'course-authoring.search-manager.clearFilters', + defaultMessage: 'Clear Filters', + description: 'Label for the button that removes all applied search filters', + }, + inputPlaceholder: { + id: 'course-authoring.search-manager.inputPlaceholder', + defaultMessage: 'Search', + description: 'Placeholder text shown in the keyword input field when the user has not yet entered a keyword', + }, + blockTypeFilter: { + id: 'course-authoring.search-manager.blockTypeFilter', + defaultMessage: 'Type', + description: 'Label for the filter that allows limiting results to a specific component type', + }, + 'blockTypeFilter.empty': { + id: 'course-authoring.search-manager.blockTypeFilter.empty', + defaultMessage: 'No matching components', + description: 'Label shown when there are no options available to filter by component type', + }, + childTagsExpand: { + id: 'course-authoring.search-manager.child-tags-expand', + defaultMessage: 'Expand to show child tags of "{tagName}"', + description: 'This text describes the â–¼ expand toggle button to non-visual users.', + }, + childTagsCollapse: { + id: 'course-authoring.search-manager.child-tags-collapse', + defaultMessage: 'Collapse to hide child tags of "{tagName}"', + description: 'This text describes the â–² collapse toggle button to non-visual users.', + }, + 'blockTagsFilter.empty': { + id: 'course-authoring.search-manager.blockTagsFilter.empty', + defaultMessage: 'No tags in current results', + description: 'Label shown when there are no options available to filter by tags', + }, + 'blockTagsFilter.error': { + id: 'course-authoring.search-manager.blockTagsFilter.error', + defaultMessage: 'Error loading tags', + description: 'Label shown when the tags could not be loaded', + }, + 'blockTagsFilter.incomplete': { + id: 'course-authoring.search-manager.blockTagsFilter.incomplete', + defaultMessage: 'Sorry, not all tags could be loaded', + description: 'Label shown when the system is not able to display all of the available tag options.', + }, + blockTagsFilter: { + id: 'course-authoring.search-manager.blockTagsFilter', + defaultMessage: 'Tags', + description: 'Label for the filter that allows finding components with specific tags', + }, + searchTagsByKeywordPlaceholder: { + id: 'course-authoring.search-manager.searchTagsByKeywordPlaceholder', + defaultMessage: 'Search tags', + description: 'Placeholder text shown in the input field that allows searching through the available tags', + }, + submitSearchTagsByKeyword: { + id: 'course-authoring.search-manager.submitSearchTagsByKeyword', + defaultMessage: 'Submit tag keyword search', + description: 'Text shown to screen reader users for the search button on the tags keyword search', + }, +}); + +export default messages; diff --git a/src/search-modal/EmptyStates.tsx b/src/search-modal/EmptyStates.tsx index a63abdb29c..901aae5b98 100644 --- a/src/search-modal/EmptyStates.tsx +++ b/src/search-modal/EmptyStates.tsx @@ -3,7 +3,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n'; import type { MessageDescriptor } from 'react-intl'; import { Alert, Stack } from '@openedx/paragon'; -import { useSearchContext } from './manager/SearchManager'; +import { useSearchContext } from '../search-manager'; import EmptySearchImage from './images/empty-search.svg'; import NoResultImage from './images/no-results.svg'; import messages from './messages'; diff --git a/src/search-modal/Highlight.tsx b/src/search-modal/Highlight.tsx index 078344bbf9..00c867ba62 100644 --- a/src/search-modal/Highlight.tsx +++ b/src/search-modal/Highlight.tsx @@ -1,21 +1,21 @@ /* eslint-disable react/no-array-index-key */ import React from 'react'; -import { highlightPostTag, highlightPreTag } from './data/api'; +import { HIGHLIGHT_POST_TAG, HIGHLIGHT_PRE_TAG } from '../search-manager'; /** * Render some text that contains matching words which should be highlighted */ const Highlight: React.FC<{ text: string }> = ({ text }) => { - const parts = text.split(highlightPreTag); + const parts = text.split(HIGHLIGHT_PRE_TAG); return ( {parts.map((part, idx) => { if (idx === 0) { return {part}; } - const endIdx = part.indexOf(highlightPostTag); + const endIdx = part.indexOf(HIGHLIGHT_POST_TAG); if (endIdx === -1) { return {part}; } const highLightPart = part.substring(0, endIdx); - const otherPart = part.substring(endIdx + highlightPostTag.length); + const otherPart = part.substring(endIdx + HIGHLIGHT_POST_TAG.length); return {highLightPart}{otherPart}; })} diff --git a/src/search-modal/SearchModal.test.tsx b/src/search-modal/SearchModal.test.tsx index 18c970f9e0..ef35726395 100644 --- a/src/search-modal/SearchModal.test.tsx +++ b/src/search-modal/SearchModal.test.tsx @@ -11,7 +11,7 @@ import MockAdapter from 'axios-mock-adapter'; import initializeStore from '../store'; import SearchModal from './SearchModal'; -import { getContentSearchConfigUrl } from './data/api'; +import { getContentSearchConfigUrl } from '../search-manager/data/api'; let store: Store; let axiosMock: MockAdapter; diff --git a/src/search-modal/SearchResult.tsx b/src/search-modal/SearchResult.tsx index 33a9fc7e33..cfd817cd50 100644 --- a/src/search-modal/SearchResult.tsx +++ b/src/search-modal/SearchResult.tsx @@ -10,13 +10,14 @@ import { OpenInNew } from '@openedx/paragon/icons'; import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; -import { constructLibraryAuthoringURL } from '../utils'; +import { getItemIcon } from '../generic/block-type-utils'; +import { useSearchContext } from '../search-manager'; import { getStudioHomeData } from '../studio-home/data/selectors'; -import { useSearchContext } from './manager/SearchManager'; -import type { ContentHit } from './data/api'; +import { constructLibraryAuthoringURL } from '../utils'; import Highlight from './Highlight'; import messages from './messages'; -import { getItemIcon } from '../generic/block-type-utils'; + +import type { ContentHit } from '../search-manager'; /** * Returns the URL Suffix for library/library component hit diff --git a/src/search-modal/SearchResults.tsx b/src/search-modal/SearchResults.tsx index bbe271ffd9..7c741e9ce2 100644 --- a/src/search-modal/SearchResults.tsx +++ b/src/search-modal/SearchResults.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { StatefulButton } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { useSearchContext } from './manager/SearchManager'; +import { useSearchContext } from '../search-manager'; import SearchResult from './SearchResult'; import messages from './messages'; diff --git a/src/search-modal/SearchUI.test.tsx b/src/search-modal/SearchUI.test.tsx index 7a62193e6d..b566730baa 100644 --- a/src/search-modal/SearchUI.test.tsx +++ b/src/search-modal/SearchUI.test.tsx @@ -28,7 +28,7 @@ import mockTagsFacetResultLevel0 from './__mocks__/facet-search-level0.json'; import mockTagsFacetResultLevel1 from './__mocks__/facet-search-level1.json'; import mockTagsKeywordSearchResult from './__mocks__/tags-keyword-search.json'; import SearchUI from './SearchUI'; -import { getContentSearchConfigUrl } from './data/api'; +import { getContentSearchConfigUrl } from '../search-manager/data/api'; // mockResult contains only a single result - this one: const mockResultDisplayName = 'Test HTML Block'; diff --git a/src/search-modal/SearchUI.tsx b/src/search-modal/SearchUI.tsx index 1ce23a8dbd..f0a4b1362e 100644 --- a/src/search-modal/SearchUI.tsx +++ b/src/search-modal/SearchUI.tsx @@ -8,14 +8,16 @@ import { import { Check } from '@openedx/paragon/icons'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import ClearFiltersButton from './ClearFiltersButton'; +import { + ClearFiltersButton, + FilterByBlockType, + FilterByTags, + SearchContextProvider, + SearchKeywordsField, +} from '../search-manager'; import EmptyStates from './EmptyStates'; import SearchResults from './SearchResults'; -import SearchKeywordsField from './SearchKeywordsField'; -import FilterByBlockType from './FilterByBlockType'; -import FilterByTags from './FilterByTags'; import Stats from './Stats'; -import { SearchContextProvider } from './manager/SearchManager'; import messages from './messages'; const SearchUI: React.FC<{ courseId: string, closeSearchModal?: () => void }> = (props) => { diff --git a/src/search-modal/Stats.tsx b/src/search-modal/Stats.tsx index b172a864bd..239a9ea0b0 100644 --- a/src/search-modal/Stats.tsx +++ b/src/search-modal/Stats.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from './messages'; -import { useSearchContext } from './manager/SearchManager'; +import { useSearchContext } from '../search-manager'; /** * Simple component that displays the # of matching results diff --git a/src/search-modal/index.js b/src/search-modal/index.js deleted file mode 100644 index 190635618d..0000000000 --- a/src/search-modal/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-check -export { default as SearchModal } from './SearchModal'; -export { useContentSearchConnection, useContentSearchResults } from './data/apiHooks'; diff --git a/src/search-modal/index.ts b/src/search-modal/index.ts new file mode 100644 index 0000000000..d1f988e149 --- /dev/null +++ b/src/search-modal/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as SearchModal } from './SearchModal'; diff --git a/src/search-modal/messages.ts b/src/search-modal/messages.ts index 66e6f041c5..400f3cca00 100644 --- a/src/search-modal/messages.ts +++ b/src/search-modal/messages.ts @@ -5,36 +5,6 @@ import type { defineMessages as defineMessagesType } from 'react-intl'; const defineMessages = _defineMessages as typeof defineMessagesType; const messages = defineMessages({ - blockTypeFilter: { - id: 'course-authoring.course-search.blockTypeFilter', - defaultMessage: 'Type', - description: 'Label for the filter that allows limiting results to a specific component type', - }, - 'blockTypeFilter.empty': { - id: 'course-authoring.course-search.blockTypeFilter.empty', - defaultMessage: 'No matching components', - description: 'Label shown when there are no options available to filter by component type', - }, - blockTagsFilter: { - id: 'course-authoring.course-search.blockTagsFilter', - defaultMessage: 'Tags', - description: 'Label for the filter that allows finding components with specific tags', - }, - 'blockTagsFilter.empty': { - id: 'course-authoring.course-search.blockTagsFilter.empty', - defaultMessage: 'No tags in current results', - description: 'Label shown when there are no options available to filter by tags', - }, - 'blockTagsFilter.error': { - id: 'course-authoring.course-search.blockTagsFilter.error', - defaultMessage: 'Error loading tags', - description: 'Label shown when the tags could not be loaded', - }, - 'blockTagsFilter.incomplete': { - id: 'course-authoring.course-search.blockTagsFilter.incomplete', - defaultMessage: 'Sorry, not all tags could be loaded', - description: 'Label shown when the system is not able to display all of the available tag options.', - }, 'blockType.annotatable': { id: 'course-authoring.course-search.blockType.annotatable', defaultMessage: 'Annotation', @@ -90,21 +60,6 @@ const messages = defineMessages({ defaultMessage: 'Video', description: 'Name of the "Video" component type in Studio', }, - childTagsExpand: { - id: 'course-authoring.course-search.child-tags-expand', - defaultMessage: 'Expand to show child tags of "{tagName}"', - description: 'This text describes the â–¼ expand toggle button to non-visual users.', - }, - childTagsCollapse: { - id: 'course-authoring.course-search.child-tags-collapse', - defaultMessage: 'Collapse to hide child tags of "{tagName}"', - description: 'This text describes the â–² collapse toggle button to non-visual users.', - }, - clearFilters: { - id: 'course-authoring.course-search.clearFilters', - defaultMessage: 'Clear Filters', - description: 'Label for the button that removes all applied search filters', - }, numResults: { id: 'course-authoring.course-search.num-results', defaultMessage: '{numResults, plural, one {# result} other {# results}} found', @@ -125,21 +80,6 @@ const messages = defineMessages({ defaultMessage: 'Search', description: 'Title for the course search dialog', }, - inputPlaceholder: { - id: 'course-authoring.course-search.inputPlaceholder', - defaultMessage: 'Search', - description: 'Placeholder text shown in the keyword input field when the user has not yet entered a keyword', - }, - searchTagsByKeywordPlaceholder: { - id: 'course-authoring.course-search.searchTagsByKeywordPlaceholder', - defaultMessage: 'Search tags', - description: 'Placeholder text shown in the input field that allows searching through the available tags', - }, - submitSearchTagsByKeyword: { - id: 'course-authoring.course-search.submitSearchTagsByKeyword', - defaultMessage: 'Submit tag keyword search', - description: 'Text shown to screen reader users for the search button on the tags keyword search', - }, showMore: { id: 'course-authoring.course-search.showMore', defaultMessage: 'Show more', From e485ab30f8db8962dc877dc32533d105524f8e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 2 Jul 2024 12:20:54 -0300 Subject: [PATCH 081/106] test: fix search-modal locator --- src/search-modal/SearchUI.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/search-modal/SearchUI.test.tsx b/src/search-modal/SearchUI.test.tsx index b566730baa..14593b6037 100644 --- a/src/search-modal/SearchUI.test.tsx +++ b/src/search-modal/SearchUI.test.tsx @@ -414,7 +414,9 @@ describe('', () => { const popupMenu = getByRole('group'); const problemFilterCheckbox = getByLabelTextIn(popupMenu, /Problem/i); fireEvent.click(problemFilterCheckbox, {}); - await waitFor(() => { expect(rendered.getByText('Type: Problem')).toBeInTheDocument(); }); + await waitFor(() => { + expect(rendered.getByRole('button', { name: /type: problem/i, hidden: true })).toBeInTheDocument(); + }); // Now wait for the filter to be applied and the new results to be fetched. await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(3, searchEndpoint, 'post'); }); // Because we're mocking the results, there's no actual changes to the mock results, From 1bf270878cb581c3e119087926082f1f0bccc9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 3 Jul 2024 12:26:30 +0200 Subject: [PATCH 082/106] fix: fix scss files location --- src/index.scss | 1 + src/{search-modal => search-manager}/FilterBy.scss | 0 src/search-manager/index.scss | 1 + src/search-modal/index.scss | 1 - 4 files changed, 2 insertions(+), 1 deletion(-) rename src/{search-modal => search-manager}/FilterBy.scss (100%) create mode 100644 src/search-manager/index.scss diff --git a/src/index.scss b/src/index.scss index 9cd7d4c3bc..db1b1d8ac6 100644 --- a/src/index.scss +++ b/src/index.scss @@ -27,6 +27,7 @@ @import "content-tags-drawer/ContentTagsDropDownSelector"; @import "content-tags-drawer/ContentTagsCollapsible"; @import "search-modal"; +@import "search-manager"; @import "certificates/scss/Certificates"; @import "group-configurations/GroupConfigurations"; @import "library-authoring"; diff --git a/src/search-modal/FilterBy.scss b/src/search-manager/FilterBy.scss similarity index 100% rename from src/search-modal/FilterBy.scss rename to src/search-manager/FilterBy.scss diff --git a/src/search-manager/index.scss b/src/search-manager/index.scss new file mode 100644 index 0000000000..2f43789e70 --- /dev/null +++ b/src/search-manager/index.scss @@ -0,0 +1 @@ +@import "search-manager/FilterBy"; diff --git a/src/search-modal/index.scss b/src/search-modal/index.scss index 150baad66e..1daa8a4871 100644 --- a/src/search-modal/index.scss +++ b/src/search-modal/index.scss @@ -1,2 +1 @@ @import "search-modal/SearchModal"; -@import "search-modal/FilterBy"; From 6aa1b0ced16de210186ea66b4ef4ca2ed0d5a834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 3 Jul 2024 13:14:47 +0200 Subject: [PATCH 083/106] refactor: moving Stats --- src/library-authoring/LibraryAuthoringPage.tsx | 2 +- src/{search-modal => search-manager}/Stats.tsx | 4 ++-- src/search-manager/index.ts | 1 + src/search-manager/messages.ts | 5 +++++ src/search-modal/SearchUI.tsx | 2 +- src/search-modal/messages.ts | 5 ----- 6 files changed, 10 insertions(+), 9 deletions(-) rename src/{search-modal => search-manager}/Stats.tsx (82%) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 14dea97ce7..d4d133088e 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -19,8 +19,8 @@ import { FilterByTags, SearchContextProvider, SearchKeywordsField, + Stats, } from '../search-manager'; -import Stats from '../search-modal/Stats'; import LibraryComponents from './components/LibraryComponents'; import LibraryCollections from './LibraryCollections'; import LibraryHome from './LibraryHome'; diff --git a/src/search-modal/Stats.tsx b/src/search-manager/Stats.tsx similarity index 82% rename from src/search-modal/Stats.tsx rename to src/search-manager/Stats.tsx index 239a9ea0b0..407e14e47b 100644 --- a/src/search-modal/Stats.tsx +++ b/src/search-manager/Stats.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from './messages'; -import { useSearchContext } from '../search-manager'; +import { useSearchContext } from './SearchManager'; /** * Simple component that displays the # of matching results */ -const Stats: React.FC> = () => { +const Stats: React.FC = () => { const { totalHits, searchKeywords, canClearFilters } = useSearchContext(); if (!searchKeywords && !canClearFilters) { diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index 96396a7e70..76c73aab3f 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -3,6 +3,7 @@ export { default as ClearFiltersButton } from './ClearFiltersButton'; export { default as FilterByBlockType } from './FilterByBlockType'; export { default as FilterByTags } from './FilterByTags'; export { default as SearchKeywordsField } from './SearchKeywordsField'; +export { default as Stats } from './Stats'; export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG } from './data/api'; export type { ContentHit } from './data/api'; diff --git a/src/search-manager/messages.ts b/src/search-manager/messages.ts index 73addfdfb1..507013497a 100644 --- a/src/search-manager/messages.ts +++ b/src/search-manager/messages.ts @@ -65,6 +65,11 @@ const messages = defineMessages({ defaultMessage: 'Submit tag keyword search', description: 'Text shown to screen reader users for the search button on the tags keyword search', }, + numResults: { + id: 'course-authoring.course-search.num-results', + defaultMessage: '{numResults, plural, one {# result} other {# results}} found', + description: 'This count displays how many matching results were found from the user\'s search', + }, }); export default messages; diff --git a/src/search-modal/SearchUI.tsx b/src/search-modal/SearchUI.tsx index f0a4b1362e..d8a4c350f1 100644 --- a/src/search-modal/SearchUI.tsx +++ b/src/search-modal/SearchUI.tsx @@ -14,10 +14,10 @@ import { FilterByTags, SearchContextProvider, SearchKeywordsField, + Stats, } from '../search-manager'; import EmptyStates from './EmptyStates'; import SearchResults from './SearchResults'; -import Stats from './Stats'; import messages from './messages'; const SearchUI: React.FC<{ courseId: string, closeSearchModal?: () => void }> = (props) => { diff --git a/src/search-modal/messages.ts b/src/search-modal/messages.ts index 400f3cca00..9dd7d6ccb7 100644 --- a/src/search-modal/messages.ts +++ b/src/search-modal/messages.ts @@ -60,11 +60,6 @@ const messages = defineMessages({ defaultMessage: 'Video', description: 'Name of the "Video" component type in Studio', }, - numResults: { - id: 'course-authoring.course-search.num-results', - defaultMessage: '{numResults, plural, one {# result} other {# results}} found', - description: 'This count displays how many matching results were found from the user\'s search', - }, searchAllCourses: { id: 'course-authoring.course-search.searchAllCourses', defaultMessage: 'All courses', From 782386f6d0c36633ece083d6017d896326c69f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 3 Jul 2024 13:19:21 +0200 Subject: [PATCH 084/106] fix: remove Stats from library home --- src/library-authoring/LibraryAuthoringPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index d4d133088e..573aa90936 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -19,7 +19,6 @@ import { FilterByTags, SearchContextProvider, SearchKeywordsField, - Stats, } from '../search-manager'; import LibraryComponents from './components/LibraryComponents'; import LibraryCollections from './LibraryCollections'; @@ -103,7 +102,6 @@ const LibraryAuthoringPage = () => {
-
Date: Wed, 3 Jul 2024 13:34:03 +0200 Subject: [PATCH 085/106] fix: remove loading state if already data is already loaded to avoid flickering --- .../components/LibraryComponents.tsx | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 48288946b3..fcfcc326b1 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -63,28 +63,7 @@ const LibraryComponents = ({ return result; }, [blockTypesData]); - const { showLoading, showContent } = useMemo(() => { - let resultShowLoading = false; - let resultShowContent = false; - - if (isFetching && !isFetchingNextPage) { - // First load; show loading but not content. - resultShowLoading = true; - resultShowContent = false; - } else if (isFetchingNextPage) { - // Load next page; show content and loading. - resultShowLoading = true; - resultShowContent = true; - } else if (!isFetching && !isFetchingNextPage) { - // State without loads; show content. - resultShowLoading = false; - resultShowContent = true; - } - return { - showLoading: resultShowLoading, - showContent: resultShowContent, - }; - }, [isFetching, isFetchingNextPage]); + const showLoading = isFetching || isFetchingNextPage; useEffect(() => { if (variant === 'full') { @@ -121,7 +100,7 @@ const LibraryComponents = ({ }} hasEqualColumnHeights > - { showContent ? componentList.map((component) => ( + {componentList.map((component) => ( - )) : } + ))} { showLoading && } ); From 6acee0ac9706bdaafd4a734723b74bf35e9e9110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 3 Jul 2024 16:27:47 +0200 Subject: [PATCH 086/106] feat: library home page bare bones --- src/CourseAuthoringPage.jsx | 33 +-- src/header/Header.jsx | 51 ++-- src/index.jsx | 5 +- src/library-authoring/CreateLibrary.tsx | 27 ++ src/library-authoring/EmptyStates.tsx | 23 ++ .../LibraryAuthoringPage.test.tsx | 236 ++++++++++++++++++ .../LibraryAuthoringPage.tsx | 131 ++++++++++ src/library-authoring/LibraryCollections.tsx | 14 ++ src/library-authoring/LibraryComponents.tsx | 32 +++ src/library-authoring/LibraryHome.tsx | 54 ++++ src/library-authoring/data/api.ts | 38 +++ src/library-authoring/data/apiHook.ts | 48 ++++ src/library-authoring/index.ts | 2 + src/library-authoring/messages.ts | 65 +++++ src/search-modal/SearchResult.tsx | 27 +- src/search-modal/SearchUI.test.tsx | 18 +- src/search-modal/data/apiHooks.ts | 8 +- src/search-modal/index.js | 3 + src/studio-home/StudioHome.jsx | 15 +- .../tabs-section/LibraryV2Placeholder.jsx | 36 --- 20 files changed, 747 insertions(+), 119 deletions(-) create mode 100644 src/library-authoring/CreateLibrary.tsx create mode 100644 src/library-authoring/EmptyStates.tsx create mode 100644 src/library-authoring/LibraryAuthoringPage.test.tsx create mode 100644 src/library-authoring/LibraryAuthoringPage.tsx create mode 100644 src/library-authoring/LibraryCollections.tsx create mode 100644 src/library-authoring/LibraryComponents.tsx create mode 100644 src/library-authoring/LibraryHome.tsx create mode 100644 src/library-authoring/data/api.ts create mode 100644 src/library-authoring/data/apiHook.ts create mode 100644 src/library-authoring/index.ts create mode 100644 src/library-authoring/messages.ts create mode 100644 src/search-modal/index.js delete mode 100644 src/studio-home/tabs-section/LibraryV2Placeholder.jsx diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx index c4281a8c13..eaa16c49c2 100644 --- a/src/CourseAuthoringPage.jsx +++ b/src/CourseAuthoringPage.jsx @@ -15,29 +15,6 @@ import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors'; import { RequestStatus } from './data/constants'; import Loading from './generic/Loading'; -const AppHeader = ({ - courseNumber, courseOrg, courseTitle, courseId, -}) => ( -
-); - -AppHeader.propTypes = { - courseId: PropTypes.string.isRequired, - courseNumber: PropTypes.string, - courseOrg: PropTypes.string, - courseTitle: PropTypes.string.isRequired, -}; - -AppHeader.defaultProps = { - courseNumber: null, - courseOrg: null, -}; - const CourseAuthoringPage = ({ courseId, children }) => { const dispatch = useDispatch(); @@ -74,11 +51,11 @@ const CourseAuthoringPage = ({ courseId, children }) => { This functionality will be removed in TNL-9591 */} {inProgress ? !isEditor && : (!isEditor && ( - ) )} diff --git a/src/header/Header.jsx b/src/header/Header.jsx index 7cc1adcb08..e5ba1a4b3c 100644 --- a/src/header/Header.jsx +++ b/src/header/Header.jsx @@ -6,16 +6,17 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { StudioHeader } from '@edx/frontend-component-header'; import { useToggle } from '@openedx/paragon'; -import SearchModal from '../search-modal/SearchModal'; +import { SearchModal } from '../search-modal'; import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils'; import messages from './messages'; const Header = ({ - courseId, - courseOrg, - courseNumber, - courseTitle, + contentId, + org, + number, + title, isHiddenMainMenu, + isLibrary, }) => { const intl = useIntl(); @@ -23,40 +24,40 @@ const Header = ({ const studioBaseUrl = getConfig().STUDIO_BASE_URL; const meiliSearchEnabled = [true, 'true'].includes(getConfig().MEILISEARCH_ENABLED); - const mainMenuDropdowns = [ + const mainMenuDropdowns = !isLibrary ? [ { id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`, buttonTitle: intl.formatMessage(messages['header.links.content']), - items: getContentMenuItems({ studioBaseUrl, courseId, intl }), + items: getContentMenuItems({ studioBaseUrl, courseId: contentId, intl }), }, { id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`, buttonTitle: intl.formatMessage(messages['header.links.settings']), - items: getSettingMenuItems({ studioBaseUrl, courseId, intl }), + items: getSettingMenuItems({ studioBaseUrl, courseId: contentId, intl }), }, { id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, buttonTitle: intl.formatMessage(messages['header.links.tools']), - items: getToolsMenuItems({ studioBaseUrl, courseId, intl }), + items: getToolsMenuItems({ studioBaseUrl, courseId: contentId, intl }), }, - ]; - const outlineLink = `${studioBaseUrl}/course/${courseId}`; + ] : []; + const outlineLink = !isLibrary ? `${studioBaseUrl}/course/${contentId}` : `/course-authoring/library/${contentId}`; return ( <> { meiliSearchEnabled && ( )} @@ -65,19 +66,21 @@ const Header = ({ }; Header.propTypes = { - courseId: PropTypes.string, - courseNumber: PropTypes.string, - courseOrg: PropTypes.string, - courseTitle: PropTypes.string, + contentId: PropTypes.string, + number: PropTypes.string, + org: PropTypes.string, + title: PropTypes.string, isHiddenMainMenu: PropTypes.bool, + isLibrary: PropTypes.bool, }; Header.defaultProps = { - courseId: '', - courseNumber: '', - courseOrg: '', - courseTitle: '', + contentId: '', + number: '', + org: '', + title: '', isHiddenMainMenu: false, + isLibrary: false, }; export default Header; diff --git a/src/index.jsx b/src/index.jsx index f881441df9..bf7ee9c423 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -19,11 +19,11 @@ import { initializeHotjar } from '@edx/frontend-enterprise-hotjar'; import { logError } from '@edx/frontend-platform/logging'; import messages from './i18n'; +import { CreateLibrary, LibraryAuthoringPage } from './library-authoring'; import initializeStore from './store'; import CourseAuthoringRoutes from './CourseAuthoringRoutes'; import Head from './head/Head'; import { StudioHome } from './studio-home'; -import LibraryV2Placeholder from './studio-home/tabs-section/LibraryV2Placeholder'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; @@ -55,7 +55,8 @@ const App = () => { } /> } /> } /> - } /> + } /> + } /> } /> } /> {getConfig().ENABLE_ACCESSIBILITY_PAGE === 'true' && ( diff --git a/src/library-authoring/CreateLibrary.tsx b/src/library-authoring/CreateLibrary.tsx new file mode 100644 index 0000000000..227f14dbe5 --- /dev/null +++ b/src/library-authoring/CreateLibrary.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Container } from '@openedx/paragon'; + +import Header from '../header'; +import SubHeader from '../generic/sub-header/SubHeader'; + +import messages from './messages'; + +/* istanbul ignore next This is only a placeholder component */ +const CreateLibrary = () => ( + <> +
+ + } + /> +
+ +
+
+ +); + +export default CreateLibrary; diff --git a/src/library-authoring/EmptyStates.tsx b/src/library-authoring/EmptyStates.tsx new file mode 100644 index 0000000000..d7b718c71d --- /dev/null +++ b/src/library-authoring/EmptyStates.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + Button, Stack, +} from '@openedx/paragon'; +import { Add } from '@openedx/paragon/icons'; + +import messages from './messages'; + +export const NoComponents = () => ( + + + + +); + +export const NoSearchResults = () => ( +
+ +
+); diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx new file mode 100644 index 0000000000..db7e815a10 --- /dev/null +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -0,0 +1,236 @@ +import React from 'react'; +import MockAdapter from 'axios-mock-adapter'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import fetchMock from 'fetch-mock-jest'; + +import initializeStore from '../store'; +import { getContentSearchConfigUrl } from '../search-modal/data/api'; +import mockResult from '../search-modal/__mocks__/search-result.json'; +import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json'; +import LibraryAuthoringPage from './LibraryAuthoringPage'; +import { getContentLibraryApiUrl } from './data/api'; + +let store; +const mockUseParams = jest.fn(); +let axiosMock; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts + useParams: () => mockUseParams(), +})); + +const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const returnEmptyResult = (_url, req) => { + const requestData = JSON.parse(req.body?.toString() ?? ''); + const query = requestData?.queries[0]?.q ?? ''; + // We have to replace the query (search keywords) in the mock results with the actual query, + // because otherwise we may have an inconsistent state that causes more queries and unexpected results. + mockEmptyResult.results[0].query = query; + // And fake the required '_formatted' fields; it contains the highlighting ... around matched words + // eslint-disable-next-line no-underscore-dangle, no-param-reassign + mockEmptyResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); + return mockEmptyResult; +}; + +const libraryData = { + id: 'lib:org1:lib1', + type: 'complex', + org: 'org1', + slug: 'lib1', + title: 'lib1', + description: 'lib1', + numBlocks: 2, + version: 0, + lastPublished: null, + allowLti: false, + allowPublic_learning: false, + allowPublic_read: false, + hasUnpublished_changes: true, + hasUnpublished_deletes: false, + license: '', +}; + +const RootWrapper = () => ( + + + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + mockUseParams.mockReturnValue({ libraryId: '1' }); + + // The API method to get the Meilisearch connection details uses Axios: + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(getContentSearchConfigUrl()).reply(200, { + url: 'http://mock.meilisearch.local', + index_name: 'studio', + api_key: 'test-key', + }); + + // The Meilisearch client-side API uses fetch, not Axios. + fetchMock.post(searchEndpoint, (_url, req) => { + const requestData = JSON.parse(req.body?.toString() ?? ''); + const query = requestData?.queries[0]?.q ?? ''; + // We have to replace the query (search keywords) in the mock results with the actual query, + // because otherwise Instantsearch will update the UI and change the query, + // leading to unexpected results in the test cases. + mockResult.results[0].query = query; + // And fake the required '_formatted' fields; it contains the highlighting ... around matched words + // eslint-disable-next-line no-underscore-dangle, no-param-reassign + mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); + return mockResult; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + axiosMock.restore(); + fetchMock.mockReset(); + queryClient.clear(); + }); + + it('shows the spinner before the query is complete', () => { + mockUseParams.mockReturnValue({ libraryId: '1' }); + // @ts-ignore Use unresolved promise to keep the Loading visible + axiosMock.onGet(getContentLibraryApiUrl('1')).reply(() => new Promise()); + const { getByRole } = render(); + const spinner = getByRole('status'); + expect(spinner.textContent).toEqual('Loading...'); + }); + + it('shows an error component if no library returned', async () => { + mockUseParams.mockReturnValue({ libraryId: 'invalid' }); + axiosMock.onGet(getContentLibraryApiUrl('invalid')).reply(400); + + const { findByTestId } = render(); + + expect(await findByTestId('notFoundAlert')).toBeInTheDocument(); + }); + + it('shows an error component if no library param', async () => { + mockUseParams.mockReturnValue({ libraryId: '' }); + + const { findByTestId } = render(); + + expect(await findByTestId('notFoundAlert')).toBeInTheDocument(); + }); + + it('show library data', async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + + const { + getByRole, getByText, queryByText, + } = render(); + + // Ensure the search endpoint is called + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + + expect(getByText('Content library')).toBeInTheDocument(); + expect(getByText(libraryData.title)).toBeInTheDocument(); + + expect(queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument(); + + expect(getByText('Recently Modified')).toBeInTheDocument(); + expect(getByText('Collections (0)')).toBeInTheDocument(); + expect(getByText('Components (6)')).toBeInTheDocument(); + expect(getByText('There are 6 components in this library')).toBeInTheDocument(); + + // Navigate to the components tab + fireEvent.click(getByRole('tab', { name: 'Components' })); + expect(queryByText('Recently Modified')).not.toBeInTheDocument(); + expect(queryByText('Collections (0)')).not.toBeInTheDocument(); + expect(queryByText('Components (6)')).not.toBeInTheDocument(); + expect(getByText('There are 6 components in this library')).toBeInTheDocument(); + + // Navigate to the collections tab + fireEvent.click(getByRole('tab', { name: 'Collections' })); + expect(queryByText('Recently Modified')).not.toBeInTheDocument(); + expect(queryByText('Collections (0)')).not.toBeInTheDocument(); + expect(queryByText('Components (6)')).not.toBeInTheDocument(); + expect(queryByText('There are 6 components in this library')).not.toBeInTheDocument(); + expect(getByText('Coming soon!')).toBeInTheDocument(); + + // Go back to Home tab + // This step is necessary to avoid the url change leak to other tests + fireEvent.click(getByRole('tab', { name: 'Home' })); + expect(getByText('Recently Modified')).toBeInTheDocument(); + expect(getByText('Collections (0)')).toBeInTheDocument(); + expect(getByText('Components (6)')).toBeInTheDocument(); + expect(getByText('There are 6 components in this library')).toBeInTheDocument(); + }); + + it('show library without components', async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); + + const { findByText, getByText } = render(); + + expect(await findByText('Content library')).toBeInTheDocument(); + expect(await findByText(libraryData.title)).toBeInTheDocument(); + + // Ensure the search endpoint is called + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + + expect(getByText('You have not added any content to this library yet.')).toBeInTheDocument(); + }); + + it('show library without search results', async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); + + const { findByText, getByRole, getByText } = render(); + + expect(await findByText('Content library')).toBeInTheDocument(); + expect(await findByText(libraryData.title)).toBeInTheDocument(); + + // Ensure the search endpoint is called + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + + fireEvent.change(getByRole('searchbox'), { target: { value: 'noresults' } }); + + // Ensure the search endpoint is called again + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + + expect(getByText('No matching components found in this library.')).toBeInTheDocument(); + + // Navigate to the components tab + fireEvent.click(getByRole('tab', { name: 'Components' })); + expect(getByText('No matching components found in this library.')).toBeInTheDocument(); + + // Go back to Home tab + // This step is necessary to avoid the url change leak to other tests + fireEvent.click(getByRole('tab', { name: 'Home' })); + }); +}); diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx new file mode 100644 index 0000000000..305c9e753d --- /dev/null +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState } from 'react'; +import { StudioFooter } from '@edx/frontend-component-footer'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Container, Icon, IconButton, SearchField, Tab, Tabs, +} from '@openedx/paragon'; +import { InfoOutline } from '@openedx/paragon/icons'; +import { + Routes, Route, useLocation, useNavigate, useParams, +} from 'react-router-dom'; + +import Loading from '../generic/Loading'; +import SubHeader from '../generic/sub-header/SubHeader'; +import Header from '../header'; +import NotFoundAlert from '../generic/NotFoundAlert'; +import LibraryComponents from './LibraryComponents'; +import LibraryCollections from './LibraryCollections'; +import LibraryHome from './LibraryHome'; +import { useContentLibrary } from './data/apiHook'; +import messages from './messages'; + +const TAB_LIST = { + home: '', + components: 'components', + collections: 'collections', +}; + +const SubHeaderTitle = ({ title }: { title: string }) => { + const intl = useIntl(); + return ( + <> + {title} + + + ); +}; + +const LibraryAuthoringPage = () => { + const intl = useIntl(); + const location = useLocation(); + const navigate = useNavigate(); + const [tabKey, setTabKey] = useState(TAB_LIST.home); + const [searchKeywords, setSearchKeywords] = useState(''); + + const { libraryId } = useParams(); + + const { data: libraryData, isLoading } = useContentLibrary(libraryId); + + useEffect(() => { + const currentPath = location.pathname.split('/').pop(); + if (currentPath && Object.values(TAB_LIST).includes(currentPath)) { + setTabKey(currentPath); + } else { + setTabKey(TAB_LIST.home); + } + }, [location]); + + if (isLoading) { + return ; + } + + if (!libraryId || !libraryData) { + return ; + } + + const handleTabChange = (key: string) => { + setTabKey(key); + navigate(key); + }; + + return ( + <> +
+ + } + subtitle={intl.formatMessage(messages.headingSubtitle)} + /> + setSearchKeywords(value)} + onSubmit={() => {}} + className="w-50" + /> + + + + + + + } + /> + } + /> + } + /> + } + /> + + + + + ); +}; + +export default LibraryAuthoringPage; diff --git a/src/library-authoring/LibraryCollections.tsx b/src/library-authoring/LibraryCollections.tsx new file mode 100644 index 0000000000..2f1eb8951f --- /dev/null +++ b/src/library-authoring/LibraryCollections.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; + +const LibraryCollections = () => ( +
+ +
+); + +export default LibraryCollections; diff --git a/src/library-authoring/LibraryComponents.tsx b/src/library-authoring/LibraryComponents.tsx new file mode 100644 index 0000000000..fee8cb3502 --- /dev/null +++ b/src/library-authoring/LibraryComponents.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import { NoComponents, NoSearchResults } from './EmptyStates'; +import { useLibraryComponentCount } from './data/apiHook'; +import messages from './messages'; + +type LibraryComponentsProps = { + libraryId: string; + filter: { + searchKeywords: string; + }; +}; + +const LibraryComponents = ({ libraryId, filter: { searchKeywords } }: LibraryComponentsProps) => { + const { componentCount } = useLibraryComponentCount(libraryId, searchKeywords); + + if (componentCount === 0) { + return searchKeywords === '' ? : ; + } + + return ( +
+ +
+ ); +}; + +export default LibraryComponents; diff --git a/src/library-authoring/LibraryHome.tsx b/src/library-authoring/LibraryHome.tsx new file mode 100644 index 0000000000..1201e8a848 --- /dev/null +++ b/src/library-authoring/LibraryHome.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + Card, Stack, +} from '@openedx/paragon'; + +import { NoComponents, NoSearchResults } from './EmptyStates'; +import LibraryCollections from './LibraryCollections'; +import LibraryComponents from './LibraryComponents'; +import { useLibraryComponentCount } from './data/apiHook'; +import messages from './messages'; + +const Section = ({ title, children } : { title: string, children: React.ReactNode }) => ( + + + + {children} + + +); + +type LibraryHomeProps = { + libraryId: string, + filter: { + searchKeywords: string, + }, +}; + +const LibraryHome = ({ libraryId, filter } : LibraryHomeProps) => { + const { searchKeywords } = filter; + const { componentCount, collectionCount } = useLibraryComponentCount(libraryId, searchKeywords); + + if (componentCount === 0) { + return searchKeywords === '' ? : ; + } + + return ( + +
+ +
+
+ +
+
+ +
+
+ ); +}; + +export default LibraryHome; diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts new file mode 100644 index 0000000000..95126d8269 --- /dev/null +++ b/src/library-authoring/data/api.ts @@ -0,0 +1,38 @@ +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; +/** + * Get the URL for the content library API. + */ +export const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; + +export interface ContentLibrary { + id: string; + type: string; + org: string; + slug: string; + title: string; + description: string; + numBlocks: number; + version: number; + lastPublished: Date | null; + allowLti: boolean; + allowPublicLearning: boolean; + allowPublicRead: boolean; + hasUnpublishedChanges: boolean; + hasUnpublishedDeletes: boolean; + license: string; +} + +/** + * Fetch a content library by its ID. + */ +export async function getContentLibrary(libraryId?: string): Promise { + if (!libraryId) { + throw new Error('libraryId is required'); + } + + const { data } = await getAuthenticatedHttpClient().get(getContentLibraryApiUrl(libraryId)); + return camelCaseObject(data); +} diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.ts new file mode 100644 index 0000000000..56a6791d26 --- /dev/null +++ b/src/library-authoring/data/apiHook.ts @@ -0,0 +1,48 @@ +import React from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { MeiliSearch } from 'meilisearch'; + +import { useContentSearchConnection, useContentSearchResults } from '../../search-modal'; +import { getContentLibrary } from './api'; + +/** + * Hook to fetch a content library by its ID. + */ +export const useContentLibrary = (libraryId?: string) => ( + useQuery({ + queryKey: ['contentLibrary', libraryId], + queryFn: () => getContentLibrary(libraryId), + }) +); + +/** + * Hook to fetch the count of components and collections in a library. + */ +export const useLibraryComponentCount = (libraryId: string, searchKeywords: string) => { + // Meilisearch code to get Collection and Component counts + const { data: connectionDetails } = useContentSearchConnection(); + + const indexName = connectionDetails?.indexName; + const client = React.useMemo(() => { + if (connectionDetails?.apiKey === undefined || connectionDetails?.url === undefined) { + return undefined; + } + return new MeiliSearch({ host: connectionDetails.url, apiKey: connectionDetails.apiKey }); + }, [connectionDetails?.apiKey, connectionDetails?.url]); + + const libFilter = `context_key = "${libraryId}"`; + + const { totalHits: componentCount } = useContentSearchResults({ + client, + indexName, + searchKeywords, + extraFilter: [libFilter], // ToDo: Add filter for components when collection is implemented + }); + + const collectionCount = 0; // ToDo: Implement collections count + + return { + componentCount, + collectionCount, + }; +}; diff --git a/src/library-authoring/index.ts b/src/library-authoring/index.ts new file mode 100644 index 0000000000..40da2db4af --- /dev/null +++ b/src/library-authoring/index.ts @@ -0,0 +1,2 @@ +export { default as LibraryAuthoringPage } from './LibraryAuthoringPage'; +export { default as CreateLibrary } from './CreateLibrary'; diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts new file mode 100644 index 0000000000..0aabbccd3a --- /dev/null +++ b/src/library-authoring/messages.ts @@ -0,0 +1,65 @@ +import { defineMessages as _defineMessages } from '@edx/frontend-platform/i18n'; +import type { defineMessages as defineMessagesType } from 'react-intl'; + +// frontend-platform currently doesn't provide types... do it ourselves. +const defineMessages = _defineMessages as typeof defineMessagesType; + +const messages = defineMessages({ + headingSubtitle: { + id: 'course-authoring.library-authoring.heading-subtitle', + defaultMessage: 'Content library', + description: 'The page heading for the library page.', + }, + headingInfoAlt: { + id: 'course-authoring.library-authoring.heading-info-alt', + defaultMessage: 'Info', + description: 'Alt text for the info icon next to the page heading.', + }, + searchPlaceholder: { + id: 'course-authoring.library-authoring.search', + defaultMessage: 'Search...', + description: 'Placeholder for search field', + }, + noSearchResults: { + id: 'course-authoring.library-authoring.no-search-results', + defaultMessage: 'No matching components found in this library.', + description: 'Message displayed when no search results are found', + }, + noComponents: { + id: 'course-authoring.library-authoring.no-components', + defaultMessage: 'You have not added any content to this library yet.', + description: 'Message displayed when the library is empty', + }, + addComponent: { + id: 'course-authoring.library-authoring.add-component', + defaultMessage: 'Add component', + description: 'Button text to add a new component', + }, + componentsTempPlaceholder: { + id: 'course-authoring.library-authoring.components-temp-placeholder', + defaultMessage: 'There are {componentCount} components in this library', + description: 'Temp placeholder for the component container. This will be replaced with the actual component list.', + }, + collectionsTempPlaceholder: { + id: 'course-authoring.library-authoring.collections-temp-placeholder', + defaultMessage: 'Coming soon!', + description: 'Temp placeholder for the collections container. This will be replaced with the actual collection list.', + }, + recentComponentsTempPlaceholder: { + id: 'course-authoring.library-authoring.recent-components-temp-placeholder', + defaultMessage: 'Recently modified components and collections will be displayed here.', + description: 'Temp placeholder for the recent components container. This will be replaced with the actual list.', + }, + createLibrary: { + id: 'course-authoring.library-authoring.create-library', + defaultMessage: 'Create library', + description: 'Header for the create library form', + }, + createLibraryTempPlaceholder: { + id: 'course-authoring.library-authoring.create-library-temp-placeholder', + defaultMessage: 'This is a placeholder for the create library form. This will be replaced with the actual form.', + description: 'Temp placeholder for the create library container. This will be replaced with the new library form.', + }, +}); + +export default messages; diff --git a/src/search-modal/SearchResult.tsx b/src/search-modal/SearchResult.tsx index 77f806be4a..ada7cdd862 100644 --- a/src/search-modal/SearchResult.tsx +++ b/src/search-modal/SearchResult.tsx @@ -35,9 +35,9 @@ function getItemIcon(blockType: string): React.ReactElement { /** * Returns the URL Suffix for library/library component hit */ -function getLibraryHitUrl(hit: ContentHit, libraryAuthoringMfeUrl: string): string { +function getLibraryComponentUrlSuffix(hit: ContentHit) { const { contextKey } = hit; - return constructLibraryAuthoringURL(libraryAuthoringMfeUrl, `library/${contextKey}`); + return `library/${contextKey}`; } /** @@ -117,10 +117,6 @@ const SearchResult: React.FC<{ hit: ContentHit }> = ({ hit }) => { const { closeSearchModal } = useSearchContext(); const { libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe } = useSelector(getStudioHomeData); - const { usageKey } = hit; - - const noRedirectUrl = usageKey.startsWith('lb:') && !redirectToLibraryAuthoringMfe; - /** * Returns the URL for the context of the hit */ @@ -136,13 +132,19 @@ const SearchResult: React.FC<{ hit: ContentHit }> = ({ hit }) => { return `/${urlSuffix}`; } - if (usageKey.startsWith('lb:')) { - if (redirectToLibraryAuthoringMfe) { - return getLibraryHitUrl(hit, libraryAuthoringMfeUrl); + if (contextKey.startsWith('lib:')) { + const urlSuffix = getLibraryComponentUrlSuffix(hit); + if (redirectToLibraryAuthoringMfe && libraryAuthoringMfeUrl) { + return constructLibraryAuthoringURL(libraryAuthoringMfeUrl, urlSuffix); } + + if (newWindow) { + return `${getPath(getConfig().PUBLIC_PATH)}${urlSuffix}`; + } + return `/${urlSuffix}`; } - // No context URL for this hit (e.g. a library without library authoring mfe) + // istanbul ignore next - This case should never be reached return undefined; }, [libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe, hit]); @@ -189,12 +191,12 @@ const SearchResult: React.FC<{ hit: ContentHit }> = ({ hit }) => { return ( @@ -213,7 +215,6 @@ const SearchResult: React.FC<{ hit: ContentHit }> = ({ hit }) => { diff --git a/src/search-modal/SearchUI.test.tsx b/src/search-modal/SearchUI.test.tsx index 92f0f244d3..7a62193e6d 100644 --- a/src/search-modal/SearchUI.test.tsx +++ b/src/search-modal/SearchUI.test.tsx @@ -342,9 +342,10 @@ describe('', () => { window.location = location; }); - test('click lib component result doesnt navigates to the context withou libraryAuthoringMfe', async () => { + test('click lib component result navigates to course-authoring/library without libraryAuthoringMfe', async () => { const data = generateGetStudioHomeDataApiResponse(); data.redirectToLibraryAuthoringMfe = false; + data.libraryAuthoringMfeUrl = ''; axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data); await executeThunk(fetchStudioHomeData(), store.dispatch); @@ -354,18 +355,21 @@ describe('', () => { const resultItem = await findByRole('button', { name: /Library Content/ }); // Clicking the "Open in new window" button should open the result in a new window: - const { open, location } = window; + const { open } = window; window.open = jest.fn(); fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' })); - expect(window.open).not.toHaveBeenCalled(); + + expect(window.open).toHaveBeenCalledWith( + '/library/lib:org1:libafter1', + '_blank', + ); window.open = open; - // @ts-ignore - window.location = { href: '' }; // Clicking in the result should navigate to the result's URL: fireEvent.click(resultItem); - expect(window.location.href === location.href); - window.location = location; + expect(mockNavigate).toHaveBeenCalledWith( + '/library/lib:org1:libafter1', + ); }); }); diff --git a/src/search-modal/data/apiHooks.ts b/src/search-modal/data/apiHooks.ts index cfc4454d5f..fe77482285 100644 --- a/src/search-modal/data/apiHooks.ts +++ b/src/search-modal/data/apiHooks.ts @@ -35,8 +35,8 @@ export const useContentSearchResults = ({ indexName, extraFilter, searchKeywords, - blockTypesFilter, - tagsFilter, + blockTypesFilter = [], + tagsFilter = [], }: { /** The Meilisearch API client */ client?: MeiliSearch; @@ -47,9 +47,9 @@ export const useContentSearchResults = ({ /** The keywords that the user is searching for, if any */ searchKeywords: string; /** Only search for these block types (e.g. `["html", "problem"]`) */ - blockTypesFilter: string[]; + blockTypesFilter?: string[]; /** Required tags (all must match), e.g. `["Difficulty > Hard", "Subject > Math"]` */ - tagsFilter: string[]; + tagsFilter?: string[]; }) => { const query = useInfiniteQuery({ enabled: client !== undefined && indexName !== undefined, diff --git a/src/search-modal/index.js b/src/search-modal/index.js new file mode 100644 index 0000000000..190635618d --- /dev/null +++ b/src/search-modal/index.js @@ -0,0 +1,3 @@ +// @ts-check +export { default as SearchModal } from './SearchModal'; +export { useContentSearchConnection, useContentSearchResults } from './data/apiHooks'; diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx index 4953c6f3ae..4400e54517 100644 --- a/src/studio-home/StudioHome.jsx +++ b/src/studio-home/StudioHome.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Button, Container, @@ -11,6 +11,7 @@ import { Add as AddIcon, Error } from '@openedx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { StudioFooter } from '@edx/frontend-component-footer'; import { getConfig, getPath } from '@edx/frontend-platform'; +import { useLocation } from 'react-router-dom'; import { constructLibraryAuthoringURL } from '../utils'; import Loading from '../generic/Loading'; @@ -19,7 +20,7 @@ import Header from '../header'; import SubHeader from '../generic/sub-header/SubHeader'; import HomeSidebar from './home-sidebar'; import TabsSection from './tabs-section'; -import { isMixedOrV2LibrariesMode } from './tabs-section/utils'; +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'; @@ -28,6 +29,8 @@ import { useStudioHome } from './hooks'; import AlertMessage from '../generic/alert-message'; const StudioHome = ({ intl }) => { + const location = useLocation(); + const isPaginationCoursesEnabled = getConfig().ENABLE_HOME_PAGE_COURSE_API_V2; const { isLoadingPage, @@ -47,6 +50,8 @@ const StudioHome = ({ intl }) => { const libMode = getConfig().LIBRARY_MODE; + const v1LibraryTab = isMixedOrV1LibrariesMode(libMode) && location?.pathname.split('/').pop() === 'libraries-v1'; + const { userIsActive, studioShortName, @@ -55,7 +60,7 @@ const StudioHome = ({ intl }) => { redirectToLibraryAuthoringMfe, } = studioHomeData; - function getHeaderButtons() { + const getHeaderButtons = useCallback(() => { const headerButtons = []; if (isFailedLoadingPage || !userIsActive) { @@ -83,7 +88,7 @@ const StudioHome = ({ intl }) => { } let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`; - if (isMixedOrV2LibrariesMode(libMode)) { + if (isMixedOrV2LibrariesMode(libMode) && !v1LibraryTab) { libraryHref = libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe ? constructLibraryAuthoringURL(libraryAuthoringMfeUrl, 'create') // Redirection to the placeholder is done in the MFE rather than @@ -106,7 +111,7 @@ const StudioHome = ({ intl }) => { ); return headerButtons; - } + }, [location]); const headerButtons = userIsActive ? getHeaderButtons() : []; if (isLoadingPage && !isFiltered) { diff --git a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx b/src/studio-home/tabs-section/LibraryV2Placeholder.jsx deleted file mode 100644 index 6b13853a2c..0000000000 --- a/src/studio-home/tabs-section/LibraryV2Placeholder.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Container } from '@openedx/paragon'; -import { StudioFooter } from '@edx/frontend-component-footer'; -import { useIntl } from '@edx/frontend-platform/i18n'; - -import Header from '../../header'; -import SubHeader from '../../generic/sub-header/SubHeader'; -import messages from './messages'; - -/* istanbul ignore next */ -const LibraryV2Placeholder = () => { - const intl = useIntl(); - - return ( - <> -
- -
-
-
- -
-
-
-

{intl.formatMessage(messages.libraryV2PlaceholderBody)}

-
-
-
- - - ); -}; - -export default LibraryV2Placeholder; From 3400f71363c9b687b564392a87997bbe75341e8b Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Wed, 3 Jul 2024 17:17:01 +0200 Subject: [PATCH 087/106] feat: Add clear filter btn to filter widget --- src/search-manager/FilterBy.scss | 4 ++++ src/search-manager/FilterByBlockType.tsx | 3 ++- src/search-manager/FilterByTags.tsx | 5 +++-- src/search-manager/SearchFilterWidget.tsx | 25 +++++++++++++++++++++++ src/search-manager/messages.ts | 5 +++++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/search-manager/FilterBy.scss b/src/search-manager/FilterBy.scss index 189f31f11c..3caccac691 100644 --- a/src/search-manager/FilterBy.scss +++ b/src/search-manager/FilterBy.scss @@ -5,3 +5,7 @@ width: 100%; } } + +.clear-filter-button:hover { + color: $info-900 !important; +} diff --git a/src/search-manager/FilterByBlockType.tsx b/src/search-manager/FilterByBlockType.tsx index 2c0c506422..dc65c7ca86 100644 --- a/src/search-manager/FilterByBlockType.tsx +++ b/src/search-manager/FilterByBlockType.tsx @@ -70,9 +70,10 @@ const FilterByBlockType: React.FC> = () => { ({ label: }))} label={} + clearFilter={() => setBlockTypesFilter([])} icon={FilterList} > - + > = () => { const intl = useIntl(); - const { tagsFilter } = useSearchContext(); + const { tagsFilter, setTagsFilter } = useSearchContext(); const [tagSearchKeywords, setTagSearchKeywords] = React.useState(''); // e.g. {"Location", "Location > North America"} if those two paths of the tag tree are expanded @@ -186,9 +186,10 @@ const FilterByTags: React.FC> = () => { ({ label: tf.split(TAG_SEP).pop() }))} label={} + clearFilter={() => setTagsFilter([])} icon={Tag} > - + void, icon?: React.ReactNode; // eslint-disable-line react/require-default-props }> = ({ appliedFilters, ...props }) => { + const intl = useIntl(); const [isOpen, open, close] = useToggle(false); const [target, setTarget] = React.useState(null); + const clearAndClose = React.useCallback(() => { + props.clearFilter(); + close(); + }, [props.clearFilter]); + return ( <>
@@ -53,6 +63,21 @@ const SearchFilterWidget: React.FC<{ style={{ textAlign: 'start' }} > {props.children} + + { + !!appliedFilters.length + && ( +
+ +
+ ) + }
diff --git a/src/search-manager/messages.ts b/src/search-manager/messages.ts index 507013497a..7336878470 100644 --- a/src/search-manager/messages.ts +++ b/src/search-manager/messages.ts @@ -70,6 +70,11 @@ const messages = defineMessages({ defaultMessage: '{numResults, plural, one {# result} other {# results}} found', description: 'This count displays how many matching results were found from the user\'s search', }, + clearFilter: { + id: 'course-authoring.search-manager.searchFilterWidget.clearFilter', + defaultMessage: 'Clear Filter', + description: 'Label for the button that removes applied search filters in a specific widget', + }, }); export default messages; From 751597544557f4ef9e2997fb7f74751c37d8fb11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 3 Jul 2024 17:45:16 +0200 Subject: [PATCH 088/106] refactor: migrate Header to ts and fix some types --- src/header/{Header.jsx => Header.tsx} | 44 +++++++++++---------------- src/search-modal/SearchModal.tsx | 3 +- src/search-modal/SearchUI.tsx | 2 +- 3 files changed, 20 insertions(+), 29 deletions(-) rename src/header/{Header.jsx => Header.tsx} (82%) diff --git a/src/header/Header.jsx b/src/header/Header.tsx similarity index 82% rename from src/header/Header.jsx rename to src/header/Header.tsx index e5ba1a4b3c..d32c4de66a 100644 --- a/src/header/Header.jsx +++ b/src/header/Header.tsx @@ -1,6 +1,5 @@ -// @ts-check +/* eslint-disable react/require-default-props */ import React from 'react'; -import PropTypes from 'prop-types'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; import { StudioHeader } from '@edx/frontend-component-header'; @@ -10,14 +9,23 @@ import { SearchModal } from '../search-modal'; import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils'; import messages from './messages'; +interface HeaderProps { + contentId?: string, + number?: string, + org?: string, + title?: string, + isHiddenMainMenu?: boolean, + isLibrary?: boolean, +} + const Header = ({ - contentId, - org, - number, - title, - isHiddenMainMenu, - isLibrary, -}) => { + contentId = '', + org = '', + number = '', + title = '', + isHiddenMainMenu = false, + isLibrary = false, +}: HeaderProps) => { const intl = useIntl(); const [isShowSearchModalOpen, openSearchModal, closeSearchModal] = useToggle(false); @@ -65,22 +73,4 @@ const Header = ({ ); }; -Header.propTypes = { - contentId: PropTypes.string, - number: PropTypes.string, - org: PropTypes.string, - title: PropTypes.string, - isHiddenMainMenu: PropTypes.bool, - isLibrary: PropTypes.bool, -}; - -Header.defaultProps = { - contentId: '', - number: '', - org: '', - title: '', - isHiddenMainMenu: false, - isLibrary: false, -}; - export default Header; diff --git a/src/search-modal/SearchModal.tsx b/src/search-modal/SearchModal.tsx index 9cb24dabf1..ca143df51f 100644 --- a/src/search-modal/SearchModal.tsx +++ b/src/search-modal/SearchModal.tsx @@ -5,7 +5,8 @@ import { ModalDialog } from '@openedx/paragon'; import messages from './messages'; import SearchUI from './SearchUI'; -const SearchModal: React.FC<{ courseId: string, isOpen: boolean, onClose: () => void }> = ({ courseId, ...props }) => { +// eslint-disable-next-line react/require-default-props +const SearchModal: React.FC<{ courseId?: string, isOpen: boolean, onClose: () => void }> = ({ courseId, ...props }) => { const intl = useIntl(); const title = intl.formatMessage(messages.title); diff --git a/src/search-modal/SearchUI.tsx b/src/search-modal/SearchUI.tsx index 1ce23a8dbd..ce60d762b8 100644 --- a/src/search-modal/SearchUI.tsx +++ b/src/search-modal/SearchUI.tsx @@ -18,7 +18,7 @@ import Stats from './Stats'; import { SearchContextProvider } from './manager/SearchManager'; import messages from './messages'; -const SearchUI: React.FC<{ courseId: string, closeSearchModal?: () => void }> = (props) => { +const SearchUI: React.FC<{ courseId?: string, closeSearchModal?: () => void }> = (props) => { const hasCourseId = Boolean(props.courseId); const [searchThisCourseEnabled, setSearchThisCourse] = React.useState(hasCourseId); const switchToThisCourse = React.useCallback(() => setSearchThisCourse(true), []); From c453ef035c6448337e6e04109243173e8a7b8b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 5 Jul 2024 18:15:31 +0200 Subject: [PATCH 089/106] fix: add intl to tab names --- src/library-authoring/LibraryAuthoringPage.tsx | 6 +++--- src/library-authoring/messages.ts | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 573aa90936..40bdf050cc 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -109,9 +109,9 @@ const LibraryAuthoringPage = () => { onSelect={handleTabChange} className="my-3" > - - - + + + Date: Fri, 5 Jul 2024 18:15:50 +0200 Subject: [PATCH 090/106] fix: remove bottom border to align label with checkbox --- src/search-manager/FilterByTags.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search-manager/FilterByTags.tsx b/src/search-manager/FilterByTags.tsx index b368e8a503..476a07ed35 100644 --- a/src/search-manager/FilterByTags.tsx +++ b/src/search-manager/FilterByTags.tsx @@ -58,7 +58,7 @@ const TagMenuItem: React.FC<{ onChange={onClickCheckbox} className="pgn__form-checkbox-input flex-shrink-0" /> -