diff --git a/src/course-home/courseware-search/CoursewareSearch.jsx b/src/course-home/courseware-search/CoursewareSearch.jsx
new file mode 100644
index 00000000..a6bff1f2
--- /dev/null
+++ b/src/course-home/courseware-search/CoursewareSearch.jsx
@@ -0,0 +1,30 @@
+import React, { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Button, Icon } from '@edx/paragon';
+import { Search } from '@edx/paragon/icons';
+import messages from './messages';
+import { fetchCoursewareSearchSettings } from '../data/thunks';
+
+const CoursewareSearch = ({ intl, ...rest }) => {
+ const { courseId } = useParams();
+ const [enabled, setEnabled] = useState(false);
+
+ useEffect(() => {
+ fetchCoursewareSearchSettings(courseId).then(response => setEnabled(response.enabled));
+ }, [courseId]);
+
+ if (!enabled) { return null; }
+
+ return (
+
+ );
+};
+
+CoursewareSearch.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(CoursewareSearch);
diff --git a/src/course-home/courseware-search/CoursewareSearch.test.jsx b/src/course-home/courseware-search/CoursewareSearch.test.jsx
new file mode 100644
index 00000000..a84f7ef6
--- /dev/null
+++ b/src/course-home/courseware-search/CoursewareSearch.test.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import {
+ act,
+ initializeMockApp,
+ render,
+ screen,
+ waitFor,
+} from '../../setupTest';
+import { fetchCoursewareSearchSettings } from '../data/thunks';
+import { CoursewareSearch } from './index';
+
+jest.mock('../data/thunks');
+
+function renderComponent() {
+ const { container } = render();
+ return container;
+}
+
+describe('CoursewareSearch', () => {
+ beforeAll(async () => {
+ initializeMockApp();
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('Should not render when the waffle flag is disabled', async () => {
+ fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: false }));
+
+ await act(async () => renderComponent());
+ await waitFor(() => {
+ expect(fetchCoursewareSearchSettings).toHaveBeenCalledTimes(1);
+ expect(screen.queryByTestId('courseware-search-button')).not.toBeInTheDocument();
+ });
+ });
+
+ it('Should render when the waffle flag is enabled', async () => {
+ fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true }));
+ await act(async () => renderComponent());
+ await waitFor(() => {
+ expect(fetchCoursewareSearchSettings).toHaveBeenCalledTimes(1);
+ expect(screen.queryByTestId('courseware-search-button')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/course-home/courseware-search/index.js b/src/course-home/courseware-search/index.js
new file mode 100644
index 00000000..c773bbcb
--- /dev/null
+++ b/src/course-home/courseware-search/index.js
@@ -0,0 +1,2 @@
+/* eslint-disable import/prefer-default-export */
+export { default as CoursewareSearch } from './CoursewareSearch';
diff --git a/src/course-home/courseware-search/messages.js b/src/course-home/courseware-search/messages.js
new file mode 100644
index 00000000..a8572494
--- /dev/null
+++ b/src/course-home/courseware-search/messages.js
@@ -0,0 +1,11 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ searchOpenAction: {
+ id: 'learn.coursewareSerch.openAction',
+ defaultMessage: 'Search within this course',
+ description: 'Aria-label for a button that will pop up Courseware Search.',
+ },
+});
+
+export default messages;
diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js
index b32973ef..ce159dcb 100644
--- a/src/course-home/data/api.js
+++ b/src/course-home/data/api.js
@@ -445,3 +445,9 @@ export async function unsubscribeFromCourseGoal(token) {
return getAuthenticatedHttpClient().post(url.href)
.then(res => camelCaseObject(res));
}
+
+export async function getCoursewareSearchEnabledFlag(courseId) {
+ const url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`);
+ const { data } = await getAuthenticatedHttpClient().get(url.href);
+ return { enabled: data.enabled || false };
+}
diff --git a/src/course-home/data/redux.test.js b/src/course-home/data/redux.test.js
index f96c830b..cf03fffd 100644
--- a/src/course-home/data/redux.test.js
+++ b/src/course-home/data/redux.test.js
@@ -250,4 +250,36 @@ describe('Data layer integration tests', () => {
expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}"}`);
});
});
+
+ describe('Test fetchCoursewareSearchSettings', () => {
+ it('Should return enabled as true when enabled', async () => {
+ const apiUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`;
+ axiosMock.onGet(apiUrl).reply(200, { enabled: true });
+
+ const { enabled } = await thunks.fetchCoursewareSearchSettings(courseId);
+
+ expect(axiosMock.history.get[0].url).toEqual(apiUrl);
+ expect(enabled).toBe(true);
+ });
+
+ it('Should return enabled as false when disabled', async () => {
+ const apiUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`;
+ axiosMock.onGet(apiUrl).reply(200, { enabled: false });
+
+ const { enabled } = await thunks.fetchCoursewareSearchSettings(courseId);
+
+ expect(axiosMock.history.get[0].url).toEqual(apiUrl);
+ expect(enabled).toBe(false);
+ });
+
+ it('Should return enabled as false on error', async () => {
+ const apiUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`;
+ axiosMock.onGet(apiUrl).networkError();
+
+ const { enabled } = await thunks.fetchCoursewareSearchSettings(courseId);
+
+ expect(axiosMock.history.get[0].url).toEqual(apiUrl);
+ expect(enabled).toBe(false);
+ });
+ });
});
diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js
index b7c1c3f9..47339137 100644
--- a/src/course-home/data/thunks.js
+++ b/src/course-home/data/thunks.js
@@ -12,6 +12,7 @@ import {
postDismissWelcomeMessage,
postRequestCert,
getLiveTabIframe,
+ getCoursewareSearchEnabledFlag,
} from './api';
import {
@@ -139,3 +140,12 @@ export function processEvent(eventData, getTabData) {
}
};
}
+
+export async function fetchCoursewareSearchSettings(courseId) {
+ try {
+ const { enabled } = await getCoursewareSearchEnabledFlag(courseId);
+ return { enabled };
+ } catch (e) {
+ return { enabled: false };
+ }
+}
diff --git a/src/course-tabs/CourseTabsNavigation.jsx b/src/course-tabs/CourseTabsNavigation.jsx
index 838a558a..2157c9f9 100644
--- a/src/course-tabs/CourseTabsNavigation.jsx
+++ b/src/course-tabs/CourseTabsNavigation.jsx
@@ -5,11 +5,15 @@ import classNames from 'classnames';
import messages from './messages';
import Tabs from '../generic/tabs/Tabs';
+import { CoursewareSearch } from '../course-home/courseware-search';
const CourseTabsNavigation = ({
activeTabSlug, className, tabs, intl,
}) => (