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 8bd2d4ef06..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.tsx'; 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/LibraryAuthoringPage.jsx b/src/library-authoring/LibraryAuthoringPage.jsx new file mode 100644 index 0000000000..a7743c2a8e --- /dev/null +++ b/src/library-authoring/LibraryAuthoringPage.jsx @@ -0,0 +1,92 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React, { useEffect } from 'react'; +import { StudioFooter } from '@edx/frontend-component-footer'; +import { Tab, Tabs } from '@openedx/paragon'; +import { + Routes, Route, useLocation, useNavigate, +} from 'react-router-dom'; + +import Header from '../header'; +import NotFoundAlert from '../generic/NotFoundAlert'; +import LibraryComponents from './LibraryComponents'; +import LibraryCollections from './LibraryCollections'; +import LibraryHome from './LibraryHome'; + +const TAB_LIST = { + home: '', + components: 'components', + collections: 'collections', +}; + +/** + * @type {React.FC} + */ +const LibraryAuthoringPage = () => { + const location = useLocation(); + const navigate = useNavigate(); + const [tabKey, setTabKey] = React.useState(TAB_LIST.home); + + // FIXME: These values should be fetched from the backend + const libraryTitle = 'Library Title'; + const libraryNumber = '001'; + const libraryOrg = 'Library Org'; + const libraryId = 'lib:org1:libafter1'; + + useEffect(() => { + const currentPath = location.pathname.split('/').pop(); + if (currentPath && Object.values(TAB_LIST).includes(currentPath)) { + setTabKey(currentPath); + } + }, [location]); + + /** Handle tab change + * @param {string} key + */ + const handleTabChange = (key) => { + setTabKey(key); + navigate(key); + }; + + return ( +
+
+ + + + + + + } + /> + } + /> + } + /> + } + /> + + +
+ ); +}; + +export default LibraryAuthoringPage; diff --git a/src/library-authoring/LibraryCollections.jsx b/src/library-authoring/LibraryCollections.jsx new file mode 100644 index 0000000000..f739f5ed65 --- /dev/null +++ b/src/library-authoring/LibraryCollections.jsx @@ -0,0 +1,14 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React from 'react'; + +/** + * @type {React.FC} + */ +const LibraryCollections = () => ( +
+ Coming soon +
+); + +export default LibraryCollections; diff --git a/src/library-authoring/LibraryComponents.jsx b/src/library-authoring/LibraryComponents.jsx new file mode 100644 index 0000000000..3548f297f5 --- /dev/null +++ b/src/library-authoring/LibraryComponents.jsx @@ -0,0 +1,14 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React from 'react'; + +/** + * @type {React.FC} + */ +const LibraryComponents = () => ( +
+ Library components will be displayed here. +
+); + +export default LibraryComponents; diff --git a/src/library-authoring/LibraryHome.jsx b/src/library-authoring/LibraryHome.jsx new file mode 100644 index 0000000000..3963ccf5a8 --- /dev/null +++ b/src/library-authoring/LibraryHome.jsx @@ -0,0 +1,66 @@ +// @ts-check +/* eslint-disable react/prop-types */ +import React from 'react'; +import { MeiliSearch } from 'meilisearch'; + +import { useContentSearchConnection, useContentSearchResults } from '../search-modal'; +import LibraryCollections from './LibraryCollections'; +import LibraryComponents from './LibraryComponents'; + +/** + * @type {React.FC<{ + * title: string, + * children: React.ReactNode, + * }>} + */ +const Section = ({ title, children }) => ( +
+

{title}

+ {children} +
+); + +/** + * @type {React.FC<{ + * libraryId: string, + * }>} + */ +const LibraryHome = ({ libraryId }) => { + // 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 searchKeywords = ''; + const libFilter = `context_key = "${libraryId}"`; + + const { totalHits: componentCount } = useContentSearchResults({ + client, + indexName, + searchKeywords, + extraFilter: [libFilter], // ToDo: Add filter for components when collection is implemented + }); + + const collectionsCount = 0; // ToDo: Implement collections count + + return ( + <> +
+ Recently modified components and collections will be displayed here. +
+
+ +
+
+ +
+ + ); +}; + +export default LibraryHome; 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/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';