Compare commits

...

1 Commits

Author SHA1 Message Date
Simon Chen
0834baee05 feat: add courseware search feature to course home 2023-04-11 11:07:43 -04:00
5 changed files with 144 additions and 0 deletions

View File

@@ -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));
}

View File

@@ -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,
},
}));
});
};
}

View File

@@ -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 }) => {
)}
<StartOrResumeCourseCard />
<WelcomeMessage courseId={courseId} />
<CoursewareSearch courseId={courseId} />
{rootCourseId && (
<>
<div className="row w-100 m-0 mb-3 justify-content-end">

View File

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

View File

@@ -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 (
<div>
<Form.Group>
<Form.Control
className="float-left w-75"
floatingLabel={intl.formatMessage(messages.coursewareSearchInputLabel)}
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
<Button
variant="primary"
className="float-left"
onClick={() => searchClick()}
>
{intl.formatMessage(messages.coursewareSearchButtonLabel)}
</Button>
<div className="clearfix" />
</Form.Group>
{(took && results.length === 0) && (
<Container size="xl">
{
`Could not find any component matching "${searchKeyword}"`
}
</Container>
)}
{(took && results.length > 0) && results.map(resultItem => (
<Container
size="xl"
>
<Layout>
<Layout.Element>
<Hyperlink
destination={`${getConfig().LMS_BASE_URL}${resultItem.data.url}`}
>
{ resultItem.data.location.join('/') }
</Hyperlink>
</Layout.Element>
</Layout>
</Container>
))}
</div>
);
};
CoursewareSearch.propTypes = {
courseId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(CoursewareSearch);