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

Content Search Modal: Filters [FC-0040] #918

Merged
merged 50 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
80ef950
feat: Prototype search UI using Instantsearch + Meilisearch
bradenmacdonald Mar 5, 2024
ed28225
feat: Refine by XBlock type too
bradenmacdonald Mar 5, 2024
50ed3dd
chore: update to match backend changes
bradenmacdonald Mar 15, 2024
130ab39
feat: display breadcrumbs
bradenmacdonald Mar 15, 2024
8a963a9
chore: convert back to JSX
bradenmacdonald Mar 22, 2024
f454ec8
chore: change modal dialog title to match design
bradenmacdonald Mar 22, 2024
a32a48b
feat: move search controls to the modal header, add This/All dropdown
bradenmacdonald Mar 22, 2024
0fdc39a
feat: start to implement filter widget, improve result formatting
bradenmacdonald Mar 23, 2024
c3ad930
chore: upgrade to newer instantsearch package
bradenmacdonald Mar 23, 2024
62c3582
fix: use fixed height for the search modal
bradenmacdonald Mar 24, 2024
fa73255
feat: paragon search keywords field
bradenmacdonald Mar 24, 2024
dadaaac
feat: limit search to the current course
bradenmacdonald Mar 24, 2024
8a48d16
feat: show a bottom border on the filter header, per the design
bradenmacdonald Mar 24, 2024
46eaa2b
feat: implement filter by block (component) type
bradenmacdonald Mar 24, 2024
ce211be
docs: better docstring for SearchFilterWidget
bradenmacdonald Mar 24, 2024
b25b0c8
fix: i18n, close button zIndex
bradenmacdonald Mar 25, 2024
56bdce0
feat: Clear Filters button
bradenmacdonald Mar 25, 2024
1714ab1
feat: Adjust size of filter buttons
bradenmacdonald Mar 25, 2024
b8598fe
fix: styling of this course / all courses toggle button
bradenmacdonald Mar 25, 2024
a546b5b
feat: Display friendly names for component types
bradenmacdonald Mar 25, 2024
3966a38
fix: styling of scroll area for results
bradenmacdonald Mar 25, 2024
268712a
fix: the list of types shouldn't change order when selecting a filter
bradenmacdonald Mar 25, 2024
3f25217
fix: display friendly component type name in active filter indicator
bradenmacdonald Mar 25, 2024
cd0ae53
feat: show message if there's no "Type" filter options
bradenmacdonald Mar 26, 2024
ac76db2
feat: improve styling of the filter buttons per UX feedback
bradenmacdonald Mar 26, 2024
9ed967b
feat: filter by tags widget
bradenmacdonald Mar 26, 2024
4687caa
chore: remove fix that's now in upstream paragon :)
bradenmacdonald Mar 26, 2024
4c6b11c
feat: Simplify the "# of results" indicator.
bradenmacdonald Mar 26, 2024
8745b1c
chore: make i18n messages more consistent
bradenmacdonald Mar 26, 2024
d1b44d7
feat: add empty states
bradenmacdonald Mar 26, 2024
7b37866
fix: sorting of Component Type filter
bradenmacdonald Mar 26, 2024
968fda0
fix: Missing 'key' prop for React in list of tags
bradenmacdonald Mar 26, 2024
a81da5d
docs: expand JSDoc comments for the components
bradenmacdonald Mar 26, 2024
e39deb9
fix: styling of the "This Course" / "All courses" toggle
bradenmacdonald Mar 27, 2024
d162d91
feat: preserve the order of the user's block type filter selection
bradenmacdonald Mar 27, 2024
e2b285d
chore: fix lint issues
bradenmacdonald Mar 27, 2024
e60da45
chore: update with latest master branch
bradenmacdonald Mar 27, 2024
9c2b484
fix: type errors found in latest version of Paragon
bradenmacdonald Mar 27, 2024
2f2a3db
test: Add tests for the content search UI
bradenmacdonald Mar 27, 2024
7a62769
chore: revert Paragon version bump - introduces other issues
bradenmacdonald Apr 3, 2024
c114076
fix: <SearchModal> was causing any tests with the header to fail
bradenmacdonald Apr 3, 2024
a44bfc1
feat: add search icon and tests
rpenido Mar 28, 2024
0e96c7d
refactor: moving api and adding more tests
rpenido Mar 29, 2024
9096a12
fix: search API endpoint was re-fetched too often
bradenmacdonald Apr 3, 2024
1eb5f9b
test: fix broken test, remove broken test that was removed in other PR
bradenmacdonald Apr 3, 2024
a414f72
test: add a test for the "Tags" filter too
bradenmacdonald Apr 3, 2024
30689b6
fix: various fixes from Romulo
bradenmacdonald Apr 3, 2024
7f4d236
fix: more minor cleanups from Romulo
bradenmacdonald Apr 6, 2024
584e95a
chore: update this branch with latest master
bradenmacdonald Apr 8, 2024
650b733
chore: fix some missing internationalization
bradenmacdonald Apr 8, 2024
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
514 changes: 510 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-ai-translations": "^2.0.0",
"@edx/frontend-component-footer": "^13.0.2",
"@edx/frontend-component-header": "^5.0.2",
"@edx/frontend-component-header": "^5.1.0",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-lib-content-components": "^2.1.4",
"@edx/frontend-platform": "7.0.1",
Expand All @@ -53,6 +53,7 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@meilisearch/instant-meilisearch": "^0.17.0",
"@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator",
"@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes",
"@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant",
Expand Down Expand Up @@ -80,6 +81,7 @@
"react-datepicker": "^4.13.0",
"react-dom": "17.0.2",
"react-helmet": "^6.1.0",
"react-instantsearch": "7.7.0",
"react-redux": "7.2.9",
"react-responsive": "9.0.2",
"react-router": "6.16.0",
Expand Down Expand Up @@ -107,6 +109,7 @@
"axios": "^0.27.2",
"axios-mock-adapter": "1.22.0",
"eslint-import-resolver-webpack": "^0.13.8",
"fetch-mock-jest": "^1.5.1",
"glob": "7.2.3",
"husky": "7.0.4",
"jest-canvas-mock": "^2.5.2",
Expand Down
2 changes: 1 addition & 1 deletion src/content-tags-drawer/ContentTagsDropDownSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ const ContentTagsDropDownSelector = ({
<Icon
src={isOpen(tagData.value) ? ArrowDropUp : ArrowDropDown}
onClick={() => clickAndEnterHandler(tagData.value)}
tabIndex="-1"
tabIndex={-1}
/>
</div>
)}
Expand Down
47 changes: 30 additions & 17 deletions src/header/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// @ts-check
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';

import { useIntl } from '@edx/frontend-platform/i18n';
import { StudioHeader } from '@edx/frontend-component-header';
import { useToggle } from '@openedx/paragon';

import SearchModal from '../search-modal/SearchModal';
import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils';
import messages from './messages';

Expand All @@ -13,10 +16,13 @@
courseNumber,
courseTitle,
isHiddenMainMenu,
// injected
intl,
}) => {
const intl = useIntl();

const [isShowSearchModalOpen, openSearchModal, closeSearchModal] = useToggle(false);

const studioBaseUrl = getConfig().STUDIO_BASE_URL;
const meiliSearchEnabled = [true, 'true'].includes(getConfig().MEILISEARCH_ENABLED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Liked it!

const mainMenuDropdowns = [
{
id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`,
Expand All @@ -35,17 +41,26 @@
},
];
const outlineLink = `${studioBaseUrl}/course/${courseId}`;

return (
<StudioHeader
{...{
org: courseOrg,
number: courseNumber,
title: courseTitle,
isHiddenMainMenu,
mainMenuDropdowns,
outlineLink,
}}
/>
<>
<StudioHeader
org={courseOrg}
number={courseNumber}
title={courseTitle}
isHiddenMainMenu={isHiddenMainMenu}
mainMenuDropdowns={mainMenuDropdowns}
outlineLink={outlineLink}
searchButtonAction={meiliSearchEnabled && openSearchModal}
/>
{ meiliSearchEnabled && (
<SearchModal

Check warning on line 57 in src/header/Header.jsx

View check run for this annotation

Codecov / codecov/patch

src/header/Header.jsx#L57

Added line #L57 was not covered by tests
isOpen={isShowSearchModalOpen}
courseId={courseId}
onClose={closeSearchModal}
/>
)}
</>
);
};

Expand All @@ -55,8 +70,6 @@
courseOrg: PropTypes.string,
courseTitle: PropTypes.string,
isHiddenMainMenu: PropTypes.bool,
// injected
intl: intlShape.isRequired,
};

Header.defaultProps = {
Expand All @@ -67,4 +80,4 @@
isHiddenMainMenu: false,
};

export default injectIntl(Header);
export default Header;
5 changes: 5 additions & 0 deletions src/header/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ const messages = defineMessages({
defaultMessage: 'Back to course outline in Studio',
description: 'The aria label for the link back to the Studio Course Outline',
},
courseSearchTitle: {
id: 'courseSearch.title',
defaultMessage: 'Search Course(s)',
description: 'Title for the course search dialog',
},
});

export default messages;
16 changes: 16 additions & 0 deletions src/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,19 @@
};
}, [dependency]);
};

export const useKeyHandler = ({ keyName, handler, dependencies = [] }) => {
useEffect(() => {
const checkKey = (event) => {

Check warning on line 38 in src/hooks.js

View check run for this annotation

Codecov / codecov/patch

src/hooks.js#L37-L38

Added lines #L37 - L38 were not covered by tests
if (event.key === keyName) {
handler();

Check warning on line 40 in src/hooks.js

View check run for this annotation

Codecov / codecov/patch

src/hooks.js#L40

Added line #L40 was not covered by tests
}
};

window.addEventListener('keydown', checkKey);

Check warning on line 44 in src/hooks.js

View check run for this annotation

Codecov / codecov/patch

src/hooks.js#L44

Added line #L44 was not covered by tests

return () => {
window.removeEventListener('keydown', checkKey);

Check warning on line 47 in src/hooks.js

View check run for this annotation

Codecov / codecov/patch

src/hooks.js#L46-L47

Added lines #L46 - L47 were not covered by tests
};
}, dependencies);
};
1 change: 1 addition & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
@import "course-checklist/CourseChecklist";
@import "content-tags-drawer/ContentTagsDropDownSelector";
@import "content-tags-drawer/ContentTagsCollapsible";
@import "search-modal/SearchModal";
25 changes: 25 additions & 0 deletions src/search-modal/BlockTypeLabel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';

/**
* Displays a friendly, localized text name for the given XBlock/component type
* e.g. `vertical` becomes `"Unit"`
* @type {React.FC<{type: string}>}
*/
const BlockTypeLabel = ({ type }) => {
// TODO: Load the localized list of Component names from Studio REST API?
const msg = messages[`blockType.${type}`];

if (msg) {
return <FormattedMessage {...msg} />;
}
// Replace underscores and hypens with spaces, then let the browser capitalize this
// in a locale-aware way to get a reasonable display value.
// e.g. 'drag-and-drop-v2' -> "Drag And Drop V2"
return <span style={{ textTransform: 'capitalize' }}>XXX {type.replace(/[_-]/g, ' ')}</span>;

Check warning on line 22 in src/search-modal/BlockTypeLabel.jsx

View check run for this annotation

Codecov / codecov/patch

src/search-modal/BlockTypeLabel.jsx#L22

Added line #L22 was not covered by tests
};

export default BlockTypeLabel;
25 changes: 25 additions & 0 deletions src/search-modal/ClearFiltersButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* 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';

/**
* 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) {
return (
<Button variant="link" size="sm" onClick={refine}>
<FormattedMessage {...messages.clearFilters} />
</Button>
);
}
return null;
};

export default ClearFiltersButton;
30 changes: 30 additions & 0 deletions src/search-modal/EmptyStates.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';
import { useStats, useClearRefinements } from 'react-instantsearch';

/**
* If the user hasn't put any keywords/filters yet, display an "empty state".
* Likewise, if the results are empty (0 results), display a special message.
* Otherwise, display the results, which are assumed to be the children prop.
* @type {React.FC<{children: React.ReactElement}>}
*/
const EmptyStates = ({ children }) => {
const { nbHits, query } = useStats();
const { canRefine: hasFiltersApplied } = useClearRefinements();
const hasQuery = !!query;

if (!hasQuery && !hasFiltersApplied) {
// We haven't started the search yet. Display the "start your search" empty state
// Note this isn't localized because it's going to be replaced in a fast-follow PR.
return <p className="text-muted text-center mt-6">Enter a keyword or select a filter to begin searching.</p>;
}
if (nbHits === 0) {
// Note this isn't localized because it's going to be replaced in a fast-follow PR.
return <p className="text-muted text-center mt-6">No results found. Change your search and try again.</p>;

Check warning on line 24 in src/search-modal/EmptyStates.jsx

View check run for this annotation

Codecov / codecov/patch

src/search-modal/EmptyStates.jsx#L24

Added line #L24 was not covered by tests
}

return children;
};

export default EmptyStates;
90 changes: 90 additions & 0 deletions src/search-modal/FilterByBlockType.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable react/prop-types */
// @ts-check
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';

/**
* A button with a dropdown that allows filtering the current search by component type (XBlock type)
* e.g. Limit results to "Text" (html) and "Problem" (problem) components.
* The button displays the first type selected, and a count of how many other types are selected, if more than one.
* @type {React.FC<Record<never, never>>}
*/
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);

const handleCheckboxChange = React.useCallback((e) => {
refine(e.target.value);
}, [refine]);

return (
<SearchFilterWidget
appliedFilters={appliedItems.map(item => ({ label: <BlockTypeLabel type={String(item.value)} /> }))}
label={<FormattedMessage {...messages.blockTypeFilter} />}
>
<Form.Group>
<Form.CheckboxSet
name="block-type-filter"
defaultValue={appliedItems.map(item => item.value)}
>
<Menu style={{ boxShadow: 'none' }}>
{
items.map((item) => (
<MenuItem
key={item.value}
as={Form.Checkbox}
value={item.value}
checked={item.isRefined}
onChange={handleCheckboxChange}
>
<BlockTypeLabel type={item.value} />{' '}
<Badge variant="light" pill>{item.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 ? (
<MenuItem disabled><FormattedMessage {...messages['blockTypeFilter.empty']} /></MenuItem>
) : null
}
</Menu>
</Form.CheckboxSet>
</Form.Group>
{
canToggleShowMore && !isShowingMore
? <Button onClick={toggleShowMore}><FormattedMessage {...messages.showMore} /></Button>

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

View check run for this annotation

Codecov / codecov/patch

src/search-modal/FilterByBlockType.jsx#L83

Added line #L83 was not covered by tests
: null
}
</SearchFilterWidget>
);
};

export default FilterByBlockType;
Loading
Loading