Skip to content

Commit

Permalink
feat: use state params to keep query and filter while searching (open…
Browse files Browse the repository at this point in the history
…edx#1249)

* feat: use state params to keep query and filter while searching

* feat: Minor factors renaming methods

* feat: fix tests

* feat: Only change url when hit search button

* feat: fix tests
  • Loading branch information
germanolleunlp authored Dec 11, 2023
1 parent d065c23 commit 4d72333
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Tabs, Tab } from '@edx/paragon';
import { useParams } from 'react-router';
import CoursewareSearchResults from './CoursewareSearchResults';
import messages from './messages';
import { useCoursewareSearchParams } from './hooks';
import { useModel } from '../../generic/model-store';

const noFilterKey = 'none';
Expand All @@ -17,6 +18,7 @@ export const filteredResultsBySelection = ({ key = noFilterKey, results = [] })
export const CoursewareSearchResultsFilter = ({ intl }) => {
const { courseId } = useParams();
const lastSearch = useModel('contentSearchResults', courseId);
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();

if (!lastSearch || !lastSearch?.results?.length) { return null; }

Expand All @@ -31,6 +33,8 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
...lastSearch.filters,
];

const activeKey = filters.find(({ key }) => key === filterKeyword)?.key || noFilterKey;

const getFilterTitle = (key, fallback) => {
const msg = messages[`filter:${key}`];
if (!msg) { return fallback; }
Expand All @@ -42,7 +46,8 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
id="courseware-search-results-tabs"
data-testid="courseware-search-results-tabs"
variant="tabs"
defaultActiveKey={noFilterKey}
activeKey={activeKey}
onSelect={setFilter}
>
{filters.map(({ key, label }) => (
<Tab key={key} eventKey={key} title={getFilterTitle(key, label)}>
Expand Down
11 changes: 11 additions & 0 deletions src/course-home/courseware-search/CoursewareResultsFilter.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
waitFor,
} from '../../setupTest';
import { CoursewareSearchResultsFilter, filteredResultsBySelection } from './CoursewareResultsFilter';
import { useCoursewareSearchParams } from './hooks';
import initializeStore from '../../store';
import { useModel } from '../../generic/model-store';
import searchResultsFactory from './test-data/search-results-factory';

jest.mock('./hooks');
jest.mock('../../generic/model-store', () => ({
useModel: jest.fn(),
}));
Expand Down Expand Up @@ -47,6 +49,14 @@ const intl = {
formatMessage: (message) => message?.defaultMessage || '',
};

const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};

function renderComponent(props = {}) {
const store = initializeStore();
history.push(pathname);
Expand Down Expand Up @@ -101,6 +111,7 @@ describe('CoursewareSearchResultsFilter', () => {
});

it('should render', async () => {
useCoursewareSearchParams.mockReturnValue(coursewareSearch);
useModel.mockReturnValue(searchResultsFactory());

await renderComponent();
Expand Down
32 changes: 19 additions & 13 deletions src/course-home/courseware-search/CoursewareSearch.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect } from 'react';
import { useParams } from 'react-router';
import { useDispatch } from 'react-redux';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
Expand All @@ -10,7 +10,7 @@ import {
Close,
} from '@edx/paragon/icons';
import { setShowSearch } from '../data/slice';
import { useElementBoundingBox, useLockScroll } from './hooks';
import { useCoursewareSearchParams, useElementBoundingBox, useLockScroll } from './hooks';
import messages from './messages';

import CoursewareSearchForm from './CoursewareSearchForm';
Expand All @@ -20,6 +20,7 @@ import { searchCourseContent } from '../data/thunks';

const CoursewareSearch = ({ intl, ...sectionProps }) => {
const { courseId } = useParams();
const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams();
const dispatch = useDispatch();
const { org } = useModel('courseHomeMeta', courseId);
const {
Expand All @@ -28,14 +29,14 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
errors,
total,
} = useModel('contentSearchResults', courseId);
const [searchKeyword, setSearchKeyword] = useState(lastSearchKeyword);

useLockScroll();

const info = useElementBoundingBox('courseTabsNavigation');
const top = info ? `${Math.floor(info.top)}px` : 0;

const clearSearch = () => {
clearSearchParams();
dispatch(updateModel({
modelType: 'contentSearchResults',
model: {
Expand All @@ -48,8 +49,8 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
}));
};

const handleSubmit = () => {
if (!searchKeyword) {
const handleSubmit = (value) => {
if (!value) {
clearSearch();
return;
}
Expand All @@ -58,20 +59,25 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
org_key: org,
courserun_key: courseId,
event_type: 'searchKeyword',
keyword: searchKeyword,
keyword: value,
});

dispatch(searchCourseContent(courseId, searchKeyword));
dispatch(searchCourseContent(courseId, value));
setQuery(value);
};

useEffect(() => {
handleSubmit(searchKeyword);
}, []);

const handleOnChange = (value) => {
if (value === searchKeyword) { return; }
if (!value) { clearSearch(); }
};

setSearchKeyword(value);

if (!value) {
clearSearch();
}
const handleSearchCloseClick = () => {
clearSearch();
dispatch(setShowSearch(false));
};

let status = 'idle';
Expand All @@ -90,7 +96,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
variant="tertiary"
className="p-1"
aria-label={intl.formatMessage(messages.searchCloseAction)}
onClick={() => dispatch(setShowSearch(false))}
onClick={handleSearchCloseClick}
data-testid="courseware-search-close-button"
><Icon src={Close} />
</Button>
Expand Down
30 changes: 27 additions & 3 deletions src/course-home/courseware-search/CoursewareSearch.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import {
fireEvent,
} from '../../setupTest';
import { CoursewareSearch } from './index';
import { useElementBoundingBox, useLockScroll } from './hooks';
import { useElementBoundingBox, useLockScroll, useCoursewareSearchParams } from './hooks';
import initializeStore from '../../store';
import { useModel, updateModel } from '../../generic/model-store';
import { searchCourseContent } from '../data/thunks';
import { setShowSearch } from '../data/slice';
import { updateModel, useModel } from '../../generic/model-store';

jest.mock('./hooks');
jest.mock('../../generic/model-store', () => ({
Expand Down Expand Up @@ -56,6 +56,14 @@ const defaultProps = {
total: 0,
};

const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};

const intl = {
formatMessage: (message) => message?.defaultMessage || '',
};
Expand All @@ -73,11 +81,23 @@ function renderComponent(props = {}) {
return container;
}

const mockModels = ((props) => {
const mockModels = ((props = defaultProps) => {
useModel.mockReturnValue({
...defaultProps,
...props,
});

updateModel.mockReturnValue({
type: 'MOCK_ACTION',
payload: {
modelType: 'contentSearchResults',
model: defaultProps,
},
});
});

const mockSearchParams = ((props = coursewareSearch) => {
useCoursewareSearchParams.mockReturnValue(props);
});

describe('CoursewareSearch', () => {
Expand All @@ -94,6 +114,7 @@ describe('CoursewareSearch', () => {

it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
mockModels();
mockSearchParams();
renderComponent();

expect(useElementBoundingBox).toBeCalledTimes(1);
Expand All @@ -102,6 +123,7 @@ describe('CoursewareSearch', () => {

it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
mockModels();
mockSearchParams();
renderComponent();

const section = screen.getByTestId('courseware-search-section');
Expand All @@ -128,6 +150,7 @@ describe('CoursewareSearch', () => {
useElementBoundingBox.mockImplementation(() => undefined);

mockModels();
mockSearchParams();
renderComponent();

const section = screen.getByTestId('courseware-search-section');
Expand All @@ -138,6 +161,7 @@ describe('CoursewareSearch', () => {
describe('when passing extra props', () => {
it('should pass on extra props to section element', () => {
mockModels();
mockSearchParams();
renderComponent({ foo: 'bar' });

const section = screen.getByTestId('courseware-search-section');
Expand Down
17 changes: 13 additions & 4 deletions src/course-home/courseware-search/CoursewareSearchToggle.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import React from 'react';
import React, { useEffect } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Icon } from '@edx/paragon';
import { Search } from '@edx/paragon/icons';
import { useDispatch } from 'react-redux';
import { setShowSearch } from '../data/slice';
import messages from './messages';
import { useCoursewareSearchFeatureFlag } from './hooks';
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
import { setShowSearch } from '../data/slice';

const CoursewareSearchToggle = ({
intl,
}) => {
const dispatch = useDispatch();
const enabled = useCoursewareSearchFeatureFlag();
const { query } = useCoursewareSearchParams();

const handleSearchOpenClick = () => {
dispatch(setShowSearch(true));
};

useEffect(() => {
if (enabled && !!query) { handleSearchOpenClick(); }
}, [enabled]);

if (!enabled) { return null; }

Expand All @@ -22,7 +31,7 @@ const CoursewareSearchToggle = ({
size="sm"
className="p-1 mt-2 mr-2 rounded-lg"
aria-label={intl.formatMessage(messages.searchOpenAction)}
onClick={() => dispatch(setShowSearch(true))}
onClick={handleSearchOpenClick}
data-testid="courseware-search-open-button"
>
<Icon src={Search} />
Expand Down
24 changes: 24 additions & 0 deletions src/course-home/courseware-search/CoursewareSearchToggle.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,33 @@ import { setShowSearch } from '../data/slice';
import { CoursewareSearchToggle } from './index';

const mockDispatch = jest.fn();
const mockCoursewareSearchParams = jest.fn();

jest.mock('../data/thunks');
jest.mock('../data/slice');

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));

jest.mock('./hooks', () => ({
...jest.requireActual('./hooks'),
useCoursewareSearchParams: () => mockCoursewareSearchParams,
}));

const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};

const mockSearchParams = ((props = coursewareSearch) => {
mockCoursewareSearchParams.mockReturnValue(props);
});

function renderComponent() {
const { container } = render(<CoursewareSearchToggle />);
return container;
Expand All @@ -36,6 +55,7 @@ describe('CoursewareSearchToggle', () => {

it('Should not render when the waffle flag is disabled', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: false }));
mockSearchParams();

await act(async () => renderComponent());
await waitFor(() => {
Expand All @@ -46,6 +66,8 @@ describe('CoursewareSearchToggle', () => {

it('Should render when the waffle flag is enabled', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true }));
mockSearchParams();

await act(async () => renderComponent());

await waitFor(() => {
Expand All @@ -56,6 +78,8 @@ describe('CoursewareSearchToggle', () => {

it('Should dispatch setShowSearch(true) when clicking the search button', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true }));
mockSearchParams();

await act(async () => renderComponent());
const button = await screen.findByTestId('courseware-search-open-button');
fireEvent.click(button);
Expand Down
17 changes: 16 additions & 1 deletion src/course-home/courseware-search/hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useLayoutEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useParams, useSearchParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { debounce } from 'lodash';
import { fetchCoursewareSearchSettings } from '../data/thunks';
Expand Down Expand Up @@ -69,3 +69,18 @@ export function useLockScroll() {
};
}, []);
}

export function useCoursewareSearchParams() {
const [searchParams, setSearchParams] = useSearchParams();
const clearSearchParams = () => setSearchParams({ q: '', f: '' });

const query = searchParams.get('q');
const filter = searchParams.get('f');

const setQuery = (q) => setSearchParams((params) => ({ q, f: params.get('f') }));
const setFilter = (f) => setSearchParams((params) => ({ q: params.get('q'), f }));

return {
query, filter, setQuery, setFilter, clearSearchParams,
};
}
Loading

0 comments on commit 4d72333

Please sign in to comment.