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';