Skip to content

Commit

Permalink
refactor: create search-manager feature
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Jul 2, 2024
1 parent 99b9b31 commit b948788
Show file tree
Hide file tree
Showing 27 changed files with 126 additions and 104 deletions.
2 changes: 1 addition & 1 deletion src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock-jest';

import initializeStore from '../store';
import { getContentSearchConfigUrl } from '../search-modal/data/api';
import { getContentSearchConfigUrl } from '../search-manager/data/api';
import mockResult from '../search-modal/__mocks__/search-result.json';
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
import LibraryAuthoringPage from './LibraryAuthoringPage';
Expand Down
12 changes: 7 additions & 5 deletions src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import Loading from '../generic/Loading';
import SubHeader from '../generic/sub-header/SubHeader';
import Header from '../header';
import NotFoundAlert from '../generic/NotFoundAlert';
import { SearchContextProvider } from '../search-modal/manager/SearchManager';
import SearchKeywordsField from '../search-modal/SearchKeywordsField';
import ClearFiltersButton from '../search-modal/ClearFiltersButton';
import FilterByBlockType from '../search-modal/FilterByBlockType';
import FilterByTags from '../search-modal/FilterByTags';
import {
ClearFiltersButton,
FilterByBlockType,
FilterByTags,
SearchContextProvider,
SearchKeywordsField,
} from '../search-manager';
import Stats from '../search-modal/Stats';
import LibraryComponents from './components/LibraryComponents';
import LibraryCollections from './LibraryCollections';
Expand Down
2 changes: 1 addition & 1 deletion src/library-authoring/LibraryHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@openedx/paragon';

import { NoComponents, NoSearchResults } from './EmptyStates';
import { useSearchContext } from '../search-modal/manager/SearchManager';
import { useSearchContext } from '../search-manager';
import LibraryCollections from './LibraryCollections';
import LibraryComponents from './components/LibraryComponents';
import messages from './messages';
Expand Down
8 changes: 4 additions & 4 deletions src/library-authoring/components/LibraryComponents.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import MockAdapter from 'axios-mock-adapter';
import fetchMock from 'fetch-mock-jest';
import type { Store } from 'redux';

import { getContentSearchConfigUrl } from '../../search-modal/data/api';
import { getContentSearchConfigUrl } from '../../search-manager/data/api';
import { SearchContextProvider } from '../../search-manager/SearchManager';
import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json';
import { SearchContextProvider } from '../../search-modal/manager/SearchManager';
import initializeStore from '../../store';
import { libraryComponentsMock } from '../__mocks__';
import LibraryComponents from './LibraryComponents';
Expand Down Expand Up @@ -76,8 +76,8 @@ jest.mock('../data/apiHook', () => ({
useLibraryBlockTypes: () => mockUseLibraryBlockTypes(),
}));

jest.mock('../../search-modal/manager/SearchManager', () => ({
...jest.requireActual('../../search-modal/manager/SearchManager'),
jest.mock('../../search-manager', () => ({
...jest.requireActual('../../search-manager'),
useSearchContext: () => mockUseSearchContext(),
}));

Expand Down
2 changes: 1 addition & 1 deletion src/library-authoring/components/LibraryComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo } from 'react';
import { CardGrid } from '@openedx/paragon';

import { useSearchContext } from '../../search-modal/manager/SearchManager';
import { useSearchContext } from '../../search-manager';
import { NoComponents, NoSearchResults } from '../EmptyStates';
import { useLibraryBlockTypes } from '../data/apiHook';
import { ComponentCard, ComponentCardLoading } from './ComponentCard';
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import messages from './messages';
import { useSearchContext } from './manager/SearchManager';
import { useSearchContext } from './SearchManager';

/**
* A button that appears when at least one filter is active, and will clear the filters when clicked.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import SearchFilterWidget from './SearchFilterWidget';
import messages from './messages';
import BlockTypeLabel from './BlockTypeLabel';
import { useSearchContext } from './manager/SearchManager';
import { useSearchContext } from './SearchManager';

/**
* A button with a dropdown that allows filtering the current search by component type (XBlock type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { ArrowDropDown, ArrowDropUp, Warning } from '@openedx/paragon/icons';
import SearchFilterWidget from './SearchFilterWidget';
import messages from './messages';
import { useSearchContext } from './manager/SearchManager';
import { useSearchContext } from './SearchManager';
import { useTagFilterOptions } from './data/apiHooks';
import { LoadingSpinner } from '../generic/Loading';
import { TAG_SEP } from './data/api';
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { SearchField } from '@openedx/paragon';
import messages from './messages';
import { useSearchContext } from './manager/SearchManager';
import { useSearchContext } from './SearchManager';

/**
* The "main" input field where users type in search keywords. The search happens as they type (no need to press enter).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import React from 'react';
import { MeiliSearch, type Filter } from 'meilisearch';

import { ContentHit } from '../data/api';
import { useContentSearchConnection, useContentSearchResults } from '../data/apiHooks';
import { ContentHit } from './data/api';
import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks';

export interface SearchContextData {
client?: MeiliSearch;
Expand Down
10 changes: 5 additions & 5 deletions src/search-modal/data/api.ts → src/search-manager/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ export const getContentSearchConfigUrl = () => new URL(
getConfig().STUDIO_BASE_URL,
).href;

export const HIGHLIGHT_PRE_TAG = '__meili-highlight__'; // Indicate the start of a highlighted (matching) term
export const HIGHLIGHT_POST_TAG = '__/meili-highlight__'; // Indicate the end of a highlighted (matching) term

/** The separator used for hierarchical tags in the search index, e.g. tags.level1 = "Subject > Math > Calculus" */
export const TAG_SEP = ' > ';

export const highlightPreTag = '__meili-highlight__'; // Indicate the start of a highlighted (matching) term
export const highlightPostTag = '__/meili-highlight__'; // Indicate the end of a highlighted (matching) term

/**
* Get the content search configuration from the CMS.
*/
Expand Down Expand Up @@ -160,8 +160,8 @@ export async function fetchSearchResults({
...tagsFilterFormatted,
],
attributesToHighlight: ['display_name', 'content'],
highlightPreTag,
highlightPostTag,
highlightPreTag: HIGHLIGHT_PRE_TAG,
highlightPostTag: HIGHLIGHT_POST_TAG,
attributesToCrop: ['content'],
cropLength: 20,
offset,
Expand Down
File renamed without changes.
8 changes: 8 additions & 0 deletions src/search-manager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { SearchContextProvider, useSearchContext } from './SearchManager';
export { default as ClearFiltersButton } from './ClearFiltersButton';
export { default as FilterByBlockType } from './FilterByBlockType';
export { default as FilterByTags } from './FilterByTags';
export { default as SearchKeywordsField } from './SearchKeywordsField';
export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG } from './data/api';

export type { ContentHit } from './data/api';
70 changes: 70 additions & 0 deletions src/search-manager/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { defineMessages as _defineMessages } from '@edx/frontend-platform/i18n';
import type { defineMessages as defineMessagesType } from 'react-intl';

// frontend-platform currently doesn't provide types... do it ourselves.
const defineMessages = _defineMessages as typeof defineMessagesType;

const messages = defineMessages({
clearFilters: {
id: 'course-authoring.search-manager.clearFilters',
defaultMessage: 'Clear Filters',
description: 'Label for the button that removes all applied search filters',
},
inputPlaceholder: {
id: 'course-authoring.search-manager.inputPlaceholder',
defaultMessage: 'Search',
description: 'Placeholder text shown in the keyword input field when the user has not yet entered a keyword',
},
blockTypeFilter: {
id: 'course-authoring.search-manager.blockTypeFilter',
defaultMessage: 'Type',
description: 'Label for the filter that allows limiting results to a specific component type',
},
'blockTypeFilter.empty': {
id: 'course-authoring.search-manager.blockTypeFilter.empty',
defaultMessage: 'No matching components',
description: 'Label shown when there are no options available to filter by component type',
},
childTagsExpand: {
id: 'course-authoring.search-manager.child-tags-expand',
defaultMessage: 'Expand to show child tags of "{tagName}"',
description: 'This text describes the ▼ expand toggle button to non-visual users.',
},
childTagsCollapse: {
id: 'course-authoring.search-manager.child-tags-collapse',
defaultMessage: 'Collapse to hide child tags of "{tagName}"',
description: 'This text describes the ▲ collapse toggle button to non-visual users.',
},
'blockTagsFilter.empty': {
id: 'course-authoring.search-manager.blockTagsFilter.empty',
defaultMessage: 'No tags in current results',
description: 'Label shown when there are no options available to filter by tags',
},
'blockTagsFilter.error': {
id: 'course-authoring.search-manager.blockTagsFilter.error',
defaultMessage: 'Error loading tags',
description: 'Label shown when the tags could not be loaded',
},
'blockTagsFilter.incomplete': {
id: 'course-authoring.search-manager.blockTagsFilter.incomplete',
defaultMessage: 'Sorry, not all tags could be loaded',
description: 'Label shown when the system is not able to display all of the available tag options.',
},
blockTagsFilter: {
id: 'course-authoring.search-manager.blockTagsFilter',
defaultMessage: 'Tags',
description: 'Label for the filter that allows finding components with specific tags',
},
searchTagsByKeywordPlaceholder: {
id: 'course-authoring.search-manager.searchTagsByKeywordPlaceholder',
defaultMessage: 'Search tags',
description: 'Placeholder text shown in the input field that allows searching through the available tags',
},
submitSearchTagsByKeyword: {
id: 'course-authoring.search-manager.submitSearchTagsByKeyword',
defaultMessage: 'Submit tag keyword search',
description: 'Text shown to screen reader users for the search button on the tags keyword search',
},
});

export default messages;
2 changes: 1 addition & 1 deletion src/search-modal/EmptyStates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
import type { MessageDescriptor } from 'react-intl';
import { Alert, Stack } from '@openedx/paragon';

import { useSearchContext } from './manager/SearchManager';
import { useSearchContext } from '../search-manager';
import EmptySearchImage from './images/empty-search.svg';
import NoResultImage from './images/no-results.svg';
import messages from './messages';
Expand Down
8 changes: 4 additions & 4 deletions src/search-modal/Highlight.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
/* eslint-disable react/no-array-index-key */
import React from 'react';

import { highlightPostTag, highlightPreTag } from './data/api';
import { HIGHLIGHT_POST_TAG, HIGHLIGHT_PRE_TAG } from '../search-manager';

/**
* Render some text that contains matching words which should be highlighted
*/
const Highlight: React.FC<{ text: string }> = ({ text }) => {
const parts = text.split(highlightPreTag);
const parts = text.split(HIGHLIGHT_PRE_TAG);
return (
<span>
{parts.map((part, idx) => {
if (idx === 0) { return <React.Fragment key={idx}>{part}</React.Fragment>; }
const endIdx = part.indexOf(highlightPostTag);
const endIdx = part.indexOf(HIGHLIGHT_POST_TAG);
if (endIdx === -1) { return <React.Fragment key={idx}>{part}</React.Fragment>; }
const highLightPart = part.substring(0, endIdx);
const otherPart = part.substring(endIdx + highlightPostTag.length);
const otherPart = part.substring(endIdx + HIGHLIGHT_POST_TAG.length);
return <React.Fragment key={idx}><mark>{highLightPart}</mark>{otherPart}</React.Fragment>;
})}
</span>
Expand Down
2 changes: 1 addition & 1 deletion src/search-modal/SearchModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import MockAdapter from 'axios-mock-adapter';

import initializeStore from '../store';
import SearchModal from './SearchModal';
import { getContentSearchConfigUrl } from './data/api';
import { getContentSearchConfigUrl } from '../search-manager/data/api';

let store: Store;
let axiosMock: MockAdapter;
Expand Down
9 changes: 5 additions & 4 deletions src/search-modal/SearchResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import { OpenInNew } from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';

import { constructLibraryAuthoringURL } from '../utils';
import { getItemIcon } from '../generic/block-type-utils';
import { useSearchContext } from '../search-manager';
import { getStudioHomeData } from '../studio-home/data/selectors';
import { useSearchContext } from './manager/SearchManager';
import type { ContentHit } from './data/api';
import { constructLibraryAuthoringURL } from '../utils';
import Highlight from './Highlight';
import messages from './messages';
import { getItemIcon } from '../generic/block-type-utils';

import type { ContentHit } from '../search-manager';

/**
* Returns the URL Suffix for library/library component hit
Expand Down
2 changes: 1 addition & 1 deletion src/search-modal/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { StatefulButton } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';

import { useSearchContext } from './manager/SearchManager';
import { useSearchContext } from '../search-manager';
import SearchResult from './SearchResult';
import messages from './messages';

Expand Down
2 changes: 1 addition & 1 deletion src/search-modal/SearchUI.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import mockTagsFacetResultLevel0 from './__mocks__/facet-search-level0.json';
import mockTagsFacetResultLevel1 from './__mocks__/facet-search-level1.json';
import mockTagsKeywordSearchResult from './__mocks__/tags-keyword-search.json';
import SearchUI from './SearchUI';
import { getContentSearchConfigUrl } from './data/api';
import { getContentSearchConfigUrl } from '../search-manager/data/api';

// mockResult contains only a single result - this one:
const mockResultDisplayName = 'Test HTML Block';
Expand Down
12 changes: 7 additions & 5 deletions src/search-modal/SearchUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import {
import { Check } from '@openedx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import ClearFiltersButton from './ClearFiltersButton';
import {
ClearFiltersButton,
FilterByBlockType,
FilterByTags,
SearchContextProvider,
SearchKeywordsField

Check failure on line 16 in src/search-modal/SearchUI.tsx

View workflow job for this annotation

GitHub Actions / tests

Missing trailing comma
} from '../search-manager';
import EmptyStates from './EmptyStates';
import SearchResults from './SearchResults';
import SearchKeywordsField from './SearchKeywordsField';
import FilterByBlockType from './FilterByBlockType';
import FilterByTags from './FilterByTags';
import Stats from './Stats';
import { SearchContextProvider } from './manager/SearchManager';
import messages from './messages';

const SearchUI: React.FC<{ courseId: string, closeSearchModal?: () => void }> = (props) => {
Expand Down
2 changes: 1 addition & 1 deletion src/search-modal/Stats.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { useSearchContext } from './manager/SearchManager';
import { useSearchContext } from '../search-manager';

/**
* Simple component that displays the # of matching results
Expand Down
3 changes: 0 additions & 3 deletions src/search-modal/index.js

This file was deleted.

2 changes: 2 additions & 0 deletions src/search-modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as SearchModal } from './SearchModal';
Loading

0 comments on commit b948788

Please sign in to comment.