diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 3265d8a1c4..3be7c838f7 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -420,3 +420,10 @@ export async function unsubscribeFromCourseGoal(token) { return getAuthenticatedHttpClient().post(url.href) .then(res => camelCaseObject(res)); } + +export async function searchCourseContentFromAPI(courseId, searchKeyword) { + const url = new URL(`${getConfig().LMS_BASE_URL}/search/${courseId}`); + const formData = `search_string=${searchKeyword}&page_size=20&page_index=0`; + return getAuthenticatedHttpClient().post(url.href, formData) + .then(res => camelCaseObject(res)); +} diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index b7c1c3f9a2..cc5d10346d 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -12,6 +12,7 @@ import { postDismissWelcomeMessage, postRequestCert, getLiveTabIframe, + searchCourseContentFromAPI, } from './api'; import { @@ -139,3 +140,18 @@ export function processEvent(eventData, getTabData) { } }; } + +export function searchCourseContent(courseId, searchKeyword) { + return async (dispatch) => { + searchCourseContentFromAPI(courseId, searchKeyword).then(response => { + const { data } = response; + dispatch(addModel({ + modelType: 'contentSearchResults', + model: { + id: courseId, + ...data, + }, + })); + }); + }; +} diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 072f0d04c6..e180d8bb54 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -28,6 +28,7 @@ import { useModel } from '../../generic/model-store'; import WelcomeMessage from './widgets/WelcomeMessage'; import ProctoringInfoPanel from './widgets/ProctoringInfoPanel'; import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert'; +import CoursewareSearch from './widgets/CoursewareSearch'; const OutlineTab = ({ intl }) => { const { @@ -157,6 +158,7 @@ const OutlineTab = ({ intl }) => { )} + {rootCourseId && ( <>
diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js index 8d3204594f..5382b70392 100644 --- a/src/course-home/outline-tab/messages.js +++ b/src/course-home/outline-tab/messages.js @@ -331,6 +331,16 @@ const messages = defineMessages({ defaultMessage: 'Onboarding Past Due', description: 'Text that show when the deadline of proctortrack onboarding exam has passed, it appears on button that start the onboarding exam however for this case the button is disabled for obvious reason', }, + coursewareSearchInputLabel: { + id: 'learning.coursewareSearch.inputLabel', + defaultMessage: 'Course Content Search', + description: 'Search input label', + }, + coursewareSearchButtonLabel: { + id: 'learning.coursewareSearch.buttonLabel', + defaultMessage: 'Search', + description: 'Search button label', + }, }); export default messages; diff --git a/src/course-home/outline-tab/widgets/CoursewareSearch.jsx b/src/course-home/outline-tab/widgets/CoursewareSearch.jsx new file mode 100644 index 0000000000..0426caf57d --- /dev/null +++ b/src/course-home/outline-tab/widgets/CoursewareSearch.jsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Form, + Container, + Hyperlink, + Layout, +} from '@edx/paragon'; +import { useDispatch } from 'react-redux'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import PropTypes from 'prop-types'; + +import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; +import { getConfig } from '@edx/frontend-platform'; +import messages from '../messages'; +import { useModel, updateModel } from '../../../generic/model-store'; +import { searchCourseContent } from '../../data/thunks'; + +const CoursewareSearch = ({ courseId, intl }) => { + const { + org, + } = useModel('courseHomeMeta', courseId); + + const eventProperties = { + org_key: org, + courserun_key: courseId, + }; + + const dispatch = useDispatch(); + + const { + results, + took, + } = useModel('contentSearchResults', courseId); + + const [searchKeyword, setSearchKeyword] = useState(''); + + useEffect(() => { + if (!searchKeyword) { + dispatch(updateModel({ + modelType: 'contentSearchResults', + model: { + id: courseId, + results: [], + took: false, + }, + })); + } + }, [searchKeyword]); + + const searchClick = () => { + sendTrackingLogEvent('edx.course.home.courseware_search.clicked', { + ...eventProperties, + event_type: 'search', + keyword: searchKeyword, + }); + dispatch(searchCourseContent(courseId, searchKeyword)); + }; + + return ( +
+ + setSearchKeyword(e.target.value)} + /> + +
+ + {(took && results.length === 0) && ( + + { + `Could not find any component matching "${searchKeyword}"` + } + + )} + {(took && results.length > 0) && results.map(resultItem => ( + + + + + { resultItem.data.location.join('/') } + + + + + ))} +
+ ); +}; + +CoursewareSearch.propTypes = { + courseId: PropTypes.string.isRequired, + intl: intlShape.isRequired, +}; + +export default injectIntl(CoursewareSearch);