Refactoring: extracted search from top bar code

This commit is contained in:
Przemek Wiech 2020-05-04 23:56:10 +02:00
parent eb31f13030
commit aaba839a46
4 changed files with 143 additions and 136 deletions

View File

@ -19,12 +19,15 @@ export class MenuItem extends React.Component<
Props & MenuItemProps & DropdownItemProps
> {
render() {
const newProps = {...this.props};
// Remove menuType from props to avoid error message in the console.
delete newProps.menuType;
return (
<>
{this.props.menuType === MenuType.Menu ? (
<Menu.Item {...this.props}>{this.props.children}</Menu.Item>
<Menu.Item {...newProps}>{this.props.children}</Menu.Item>
) : (
<Dropdown.Item {...this.props}>{this.props.children}</Dropdown.Item>
<Dropdown.Item {...newProps}>{this.props.children}</Dropdown.Item>
)}
</>
);

129
src/menu/search.tsx Normal file
View File

@ -0,0 +1,129 @@
import * as React from 'react';
import debounce from 'debounce';
import {analyticsEvent} from '../util/analytics';
import {buildSearchIndex, SearchIndex, SearchResult} from './search_index';
import {formatDateOrRange} from '../util/date_util';
import {IndiInfo, JsonGedcomData} from 'topola';
import {intlShape} from 'react-intl';
import {JsonIndi} from 'topola';
import {RouteComponentProps} from 'react-router-dom';
import {Search, SearchProps, SearchResultProps} from 'semantic-ui-react';
function getNameLine(result: SearchResult) {
const name = [result.indi.firstName, result.indi.lastName].join(' ').trim();
if (result.id.length > 8) {
return name;
}
return (
<>
{name} <i>({result.id})</i>
</>
);
}
interface Props {
/** Data used for the search index. */
data: JsonGedcomData;
onSelection: (indiInfo: IndiInfo) => void;
}
interface State {
searchResults: SearchResultProps[];
}
/** Displays and handles the search box in the top bar. */
export class SearchBar extends React.Component<
RouteComponentProps & Props,
State
> {
state: State = {
searchResults: [],
};
/** Make intl appear in this.context. */
static contextTypes = {
intl: intlShape,
};
searchRef?: {setValue(value: string): void};
searchIndex?: SearchIndex;
private getDescriptionLine(indi: JsonIndi) {
const birthDate = formatDateOrRange(indi.birth, this.context.intl);
const deathDate = formatDateOrRange(indi.death, this.context.intl);
if (!deathDate) {
return birthDate;
}
return `${birthDate} ${deathDate}`;
}
/** Produces an object that is displayed in the Semantic UI Search results. */
private displaySearchResult(result: SearchResult) {
return {
id: result.id,
key: result.id,
title: getNameLine(result),
description: this.getDescriptionLine(result.indi),
};
}
/** On search input change. */
private handleSearch(input: string | undefined) {
if (!input) {
return;
}
const results = this.searchIndex!.search(input).map((result) =>
this.displaySearchResult(result),
);
this.setState(Object.assign({}, this.state, {searchResults: results}));
}
/** On search result selected. */
private handleResultSelect(id: string) {
analyticsEvent('search_result_selected');
this.props.onSelection({id, generation: 0});
this.searchRef!.setValue('');
}
private initializeSearchIndex() {
this.searchIndex = buildSearchIndex(this.props.data);
}
componentDidMount() {
this.initializeSearchIndex();
}
componentDidUpdate(prevProps: Props) {
if (prevProps.data !== this.props.data) {
this.initializeSearchIndex();
}
}
render() {
return (
<Search
onSearchChange={debounce(
(_: React.MouseEvent<HTMLElement>, data: SearchProps) =>
this.handleSearch(data.value),
200,
)}
onResultSelect={(_, data) => this.handleResultSelect(data.result.id)}
results={this.state.searchResults}
noResultsMessage={this.context.intl.formatMessage({
id: 'menu.search.no_results',
defaultMessage: 'No results found',
})}
placeholder={this.context.intl.formatMessage({
id: 'menu.search.placeholder',
defaultMessage: 'Search for people',
})}
selectFirstResult={true}
ref={(ref) =>
(this.searchRef = (ref as unknown) as {
setValue(value: string): void;
})
}
id="search"
/>
);
}
}

View File

@ -1,36 +0,0 @@
import * as React from 'react';
import {formatDateOrRange} from '../util/date_util';
import {InjectedIntl} from 'react-intl';
import {JsonIndi} from 'topola';
import {SearchResult} from './search_index';
function getNameLine(result: SearchResult) {
const name = [result.indi.firstName, result.indi.lastName].join(' ').trim();
if (result.id.length > 8) {
return name;
}
return (
<>
{name} <i>({result.id})</i>
</>
);
}
function getDescriptionLine(indi: JsonIndi, intl: InjectedIntl) {
const birthDate = formatDateOrRange(indi.birth, intl);
const deathDate = formatDateOrRange(indi.death, intl);
if (!deathDate) {
return birthDate;
}
return `${birthDate} ${deathDate}`;
}
/** Produces an object that is displayed in the Semantic UI Search results. */
export function displaySearchResult(result: SearchResult, intl: InjectedIntl) {
return {
id: result.id,
key: result.id,
title: getNameLine(result),
description: getDescriptionLine(result.indi, intl),
};
}

View File

@ -1,10 +1,6 @@
import * as queryString from 'query-string';
import * as React from 'react';
import debounce from 'debounce';
import {analyticsEvent} from '../util/analytics';
import {buildSearchIndex, SearchIndex} from './search_index';
import {displaySearchResult} from './search_util';
import {FormattedMessage, intlShape} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import {IndiInfo, JsonGedcomData} from 'topola';
import {Link} from 'react-router-dom';
import {MenuType} from './menu_item';
@ -12,26 +8,14 @@ import {RouteComponentProps} from 'react-router-dom';
import {UploadMenu} from './upload_menu';
import {UrlMenu} from './url_menu';
import {WikiTreeLoginMenu, WikiTreeMenu} from './wikitree_menu';
import {
Icon,
Menu,
Dropdown,
Search,
SearchProps,
SearchResultProps,
Responsive,
} from 'semantic-ui-react';
import {Icon, Menu, Dropdown, Responsive} from 'semantic-ui-react';
import {SearchBar} from './search';
enum ScreenSize {
LARGE,
SMALL,
}
/** Menus and dialogs state. */
interface State {
searchResults: SearchResultProps[];
}
interface EventHandlers {
onSelection: (indiInfo: IndiInfo) => void;
onPrint: () => void;
@ -53,45 +37,7 @@ interface Props {
showWikiTreeMenus: boolean;
}
export class TopBar extends React.Component<
RouteComponentProps & Props,
State
> {
state: State = {
searchResults: [],
};
/** Make intl appear in this.context. */
static contextTypes = {
intl: intlShape,
};
searchRef?: {setValue(value: string): void};
searchIndex?: SearchIndex;
/** On search input change. */
private handleSearch(input: string | undefined) {
if (!input) {
return;
}
const results = this.searchIndex!.search(input).map((result) =>
displaySearchResult(result, this.context.intl),
);
this.setState(Object.assign({}, this.state, {searchResults: results}));
}
/** On search result selected. */
private handleResultSelect(id: string) {
analyticsEvent('search_result_selected');
this.props.eventHandlers.onSelection({id, generation: 0});
this.searchRef!.setValue('');
}
private initializeSearchIndex() {
if (this.props.data) {
this.searchIndex = buildSearchIndex(this.props.data);
}
}
export class TopBar extends React.Component<RouteComponentProps & Props> {
private changeView(view: string) {
const location = this.props.location;
const search = queryString.parse(location.search);
@ -102,45 +48,6 @@ export class TopBar extends React.Component<
}
}
componentDidMount() {
this.initializeSearchIndex();
}
componentDidUpdate(prevProps: Props) {
if (prevProps.data !== this.props.data) {
this.initializeSearchIndex();
}
}
private search() {
return (
<Search
onSearchChange={debounce(
(_: React.MouseEvent<HTMLElement>, data: SearchProps) =>
this.handleSearch(data.value),
200,
)}
onResultSelect={(_, data) => this.handleResultSelect(data.result.id)}
results={this.state.searchResults}
noResultsMessage={this.context.intl.formatMessage({
id: 'menu.search.no_results',
defaultMessage: 'No results found',
})}
placeholder={this.context.intl.formatMessage({
id: 'menu.search.placeholder',
defaultMessage: 'Search for people',
})}
selectFirstResult={true}
ref={(ref) =>
(this.searchRef = (ref as unknown) as {
setValue(value: string): void;
})
}
id="search"
/>
);
}
private chartMenus(screenSize: ScreenSize) {
if (!this.props.showingChart) {
return null;
@ -232,7 +139,11 @@ export class TopBar extends React.Component<
>
<Dropdown.Menu>{chartTypeItems}</Dropdown.Menu>
</Dropdown>
{this.search()}
<SearchBar
data={this.props.data!}
onSelection={this.props.eventHandlers.onSelection}
{...this.props}
/>
</>
);