Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow filtering by multiple tags [FC-0040] #945

Merged
merged 16 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 1 addition & 340 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,15 @@
"email-validator": "2.0.4",
"file-saver": "^2.0.5",
"formik": "2.2.6",
"instantsearch.css": "^8.1.0",
"jszip": "^3.10.1",
"lodash": "4.17.21",
"meilisearch": "^0.38.0",
"moment": "2.29.4",
"prop-types": "15.7.2",
"react": "17.0.2",
"react-datepicker": "^4.13.0",
"react-dom": "17.0.2",
"react-helmet": "^6.1.0",
"react-instantsearch": "^7.7.1",
"react-redux": "7.2.9",
"react-responsive": "9.0.2",
"react-router": "6.16.0",
Expand Down
8 changes: 4 additions & 4 deletions src/search-modal/ClearFiltersButton.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';
import { useClearRefinements } from 'react-instantsearch';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import messages from './messages';
import { useSearchContext } from './manager/SearchManager';

/**
* A button that appears when at least one filter is active, and will clear the filters when clicked.
* @type {React.FC<Record<never, never>>}
*/
const ClearFiltersButton = () => {
const { refine, canRefine } = useClearRefinements();
if (canRefine) {
const { canClearFilters, clearFilters } = useSearchContext();
if (canClearFilters) {
return (
<Button variant="link" size="sm" onClick={refine}>
<Button variant="link" size="sm" onClick={clearFilters}>
<FormattedMessage {...messages.clearFilters} />
</Button>
);
Expand Down
23 changes: 17 additions & 6 deletions src/search-modal/EmptyStates.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// @ts-check
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Stack } from '@openedx/paragon';
import { useStats, useClearRefinements } from 'react-instantsearch';
import { Alert, Stack } from '@openedx/paragon';

import { useSearchContext } from './manager/SearchManager';
import EmptySearchImage from './images/empty-search.svg';
import NoResultImage from './images/no-results.svg';
import messages from './messages';
Expand All @@ -24,10 +24,21 @@ const InfoMessage = ({ title, subtitle, image }) => (
* @type {React.FC<{children: React.ReactElement}>}
*/
const EmptyStates = ({ children }) => {
const { nbHits, query } = useStats();
const { canRefine: hasFiltersApplied } = useClearRefinements();
const hasQuery = !!query;
const {
canClearFilters: hasFiltersApplied,
totalHits,
searchKeywords,
hasError,
} = useSearchContext();
const hasQuery = !!searchKeywords;

if (hasError) {
return (
<Alert variant="danger">
<FormattedMessage {...messages.searchError} />
</Alert>
);
}
if (!hasQuery && !hasFiltersApplied) {
// We haven't started the search yet. Display the "start your search" empty state
return (
Expand All @@ -38,7 +49,7 @@ const EmptyStates = ({ children }) => {
/>
);
}
if (nbHits === 0) {
if (totalHits === 0) {
return (
<InfoMessage
title={messages.noResultsTitle}
Expand Down
57 changes: 22 additions & 35 deletions src/search-modal/FilterByBlockType.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Button,
Badge,
Form,
Menu,
MenuItem,
} from '@openedx/paragon';
import {
useCurrentRefinements,
useRefinementList,
} from 'react-instantsearch';
import SearchFilterWidget from './SearchFilterWidget';
import messages from './messages';
import BlockTypeLabel from './BlockTypeLabel';
import { useSearchContext } from './manager/SearchManager';

/**
* A button with a dropdown that allows filtering the current search by component type (XBlock type)
Expand All @@ -25,64 +21,55 @@
*/
const FilterByBlockType = () => {
const {
items,
refine,
canToggleShowMore,
isShowingMore,
toggleShowMore,
} = useRefinementList({ attribute: 'block_type', sortBy: ['count:desc', 'name'] });

// Get the list of applied 'items' (selected block types to filter) in the original order that the user clicked them.
// The first choice will be shown on the button, and we don't want it to change as the user selects more options.
// (But for the dropdown menu, we always want them sorted by 'count:desc' and 'name'; not in order of selection.)
const refinementsData = useCurrentRefinements({ includedAttributes: ['block_type'] });
const appliedItems = refinementsData.items[0]?.refinements ?? [];
// If we didn't need to preserve the order the user clicked on, the above two lines could be simplified to:
// const appliedItems = items.filter(item => item.isRefined);
blockTypes,
blockTypesFilter,
setBlockTypesFilter,
} = useSearchContext();
// TODO: sort blockTypes first by count, then by name

const handleCheckboxChange = React.useCallback((e) => {
refine(e.target.value);
}, [refine]);
setBlockTypesFilter(currentFilters => {
if (currentFilters.includes(e.target.value)) {
return currentFilters.filter(x => x !== e.target.value);

Check warning on line 33 in src/search-modal/FilterByBlockType.jsx

View check run for this annotation

Codecov / codecov/patch

src/search-modal/FilterByBlockType.jsx#L33

Added line #L33 was not covered by tests
}
return [...currentFilters, e.target.value];
});
}, [setBlockTypesFilter]);

return (
<SearchFilterWidget
appliedFilters={appliedItems.map(item => ({ label: <BlockTypeLabel type={String(item.value)} /> }))}
appliedFilters={blockTypesFilter.map(blockType => ({ label: <BlockTypeLabel type={blockType} /> }))}
label={<FormattedMessage {...messages.blockTypeFilter} />}
>
<Form.Group>
<Form.CheckboxSet
name="block-type-filter"
defaultValue={appliedItems.map(item => item.value)}
defaultValue={blockTypesFilter}
>
<Menu style={{ boxShadow: 'none' }}>
{
items.map((item) => (
Object.entries(blockTypes).map(([blockType, count]) => (
<MenuItem
key={item.value}
key={blockType}
as={Form.Checkbox}
value={item.value}
checked={item.isRefined}
value={blockType}
checked={blockTypesFilter.includes(blockType)}
onChange={handleCheckboxChange}
>
<BlockTypeLabel type={item.value} />{' '}
<Badge variant="light" pill>{item.count}</Badge>
<BlockTypeLabel type={blockType} />{' '}
<Badge variant="light" pill>{count}</Badge>
</MenuItem>
))
}
{
// Show a message if there are no options at all to avoid the impression that the dropdown isn't working
items.length === 0 ? (
blockTypes.length === 0 ? (
<MenuItem disabled><FormattedMessage {...messages['blockTypeFilter.empty']} /></MenuItem>
) : null
}
</Menu>
</Form.CheckboxSet>
</Form.Group>
{
canToggleShowMore && !isShowingMore
? <Button onClick={toggleShowMore}><FormattedMessage {...messages.showMore} /></Button>
: null
}
</SearchFilterWidget>
);
};
Expand Down
Loading
Loading