@atlaskit/global-search
Version:
A cross-product search component (batteries included)
362 lines (319 loc) • 11.8 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import React from 'react';
import GlobalQuickSearch from '../GlobalQuickSearch';
import performanceNow from '../../util/performance-now';
import { buildShownEventDetails } from '../../util/analytics-util';
import { firePreQueryShownEvent, firePostQueryShownEvent, fireExperimentExposureEvent } from '../../util/analytics-event-helper';
import { withAnalyticsEvents } from '@atlaskit/analytics-next';
import deepEqual from 'deep-equal';
import { CONF_OBJECTS_ITEMS_PER_PAGE } from '../../util/experiment-utils';
import { injectSearchSession } from '../SearchSessionProvider';
const resultMapToArray = results => results.map(result => result.items);
const LOGGER_NAME = 'AK.GlobalSearch.QuickSearchContainer';
/**
* Container/Stateful Component that handles the data fetching and state handling when the user interacts with Search.
*/
export class QuickSearchContainer extends React.Component {
// used to terminate if component is unmounted while waiting for a promise
constructor(props) {
super(props);
_defineProperty(this, "unmounted", false);
_defineProperty(this, "latestQueryVersion", 0);
_defineProperty(this, "doSearch", async (query, queryVersion, filters) => {
const startTime = performanceNow();
this.latestQueryVersion = queryVersion;
try {
const {
results,
timings
} = await this.props.getSearchResults(query, this.props.searchSessionId, startTime, queryVersion, filters.map(({
filter
}) => filter));
if (this.unmounted) {
return;
}
const elapsedMs = performanceNow() - startTime;
if (this.state.latestSearchQuery === query) {
this.setState({
searchResults: results,
isError: false,
isLoading: false,
keepPreQueryState: false
}, () => {
this.fireShownPostQueryEvent(startTime, elapsedMs, this.state.searchResults || {}, // Remove 'any' as part of QS-740
this.state.recentItems || {}, // Remove 'any' as part of QS-740
timings || {}, this.props.searchSessionId, this.state.latestSearchQuery, this.state.currentFilters.map(({
filter
}) => filter) || [], this.state.isLoading);
});
}
} catch (e) {
this.props.logger.safeError(LOGGER_NAME, 'error while getting search results', e);
this.setState({
isError: true,
isLoading: false,
keepPreQueryState: false
});
}
});
_defineProperty(this, "fireExperimentExposureEvent", () => {
const {
createAnalyticsEvent,
features,
searchSessionId
} = this.props;
if (createAnalyticsEvent) {
fireExperimentExposureEvent(features.abTest, searchSessionId, createAnalyticsEvent);
}
});
_defineProperty(this, "fireShownPreQueryEvent", (requestStartTime, renderStartTime) => {
const {
searchSessionId
} = this.props;
const {
recentItems
} = this.state;
const {
createAnalyticsEvent,
getPreQueryDisplayedResults,
enablePreQueryFromAggregator,
referralContextIdentifiers,
features
} = this.props;
if (createAnalyticsEvent && getPreQueryDisplayedResults) {
const elapsedMs = requestStartTime ? performanceNow() - requestStartTime : 0;
const renderTime = renderStartTime ? performanceNow() - renderStartTime : 0;
const resultsArray = resultMapToArray(getPreQueryDisplayedResults(recentItems, searchSessionId));
const eventAttributes = { ...buildShownEventDetails(...resultsArray)
};
firePreQueryShownEvent(eventAttributes, elapsedMs, renderTime, searchSessionId, createAnalyticsEvent, features.abTest, referralContextIdentifiers, !!enablePreQueryFromAggregator);
}
});
_defineProperty(this, "fireShownPostQueryEvent", (startTime, elapsedMs, searchResults, recentItems, timings, searchSessionId, latestSearchQuery, latestFilters, isLoading) => {
const {
features
} = this.props;
const performanceTiming = {
startTime,
elapsedMs,
...timings
};
const {
createAnalyticsEvent,
getPostQueryDisplayedResults,
referralContextIdentifiers
} = this.props;
const filtersApplied = {};
for (const filter of latestFilters) {
filtersApplied[filter['@type']] = true;
}
if (createAnalyticsEvent && getPostQueryDisplayedResults) {
const resultsArray = resultMapToArray(getPostQueryDisplayedResults(searchResults, latestSearchQuery, recentItems, isLoading, searchSessionId));
const resultsDetails = buildShownEventDetails(...resultsArray);
firePostQueryShownEvent(resultsDetails, performanceTiming, searchSessionId, latestSearchQuery, filtersApplied, createAnalyticsEvent, features.abTest, referralContextIdentifiers);
}
});
_defineProperty(this, "handleSearch", (newLatestSearchQuery, queryVersion, filters) => {
if (this.state.latestSearchQuery !== newLatestSearchQuery || filters !== this.state.currentFilters) {
this.setState({
latestSearchQuery: newLatestSearchQuery,
currentFilters: filters,
isLoading: true
});
}
if (newLatestSearchQuery.length === 0) {
// reset search results so that internal state between query and results stays consistent
this.setState({
isError: false,
isLoading: false,
keepPreQueryState: true,
currentFilters: []
}, () => {
this.fireShownPreQueryEvent();
});
} else {
this.doSearch(newLatestSearchQuery, queryVersion, filters);
}
});
_defineProperty(this, "retrySearch", () => {
this.handleSearch(this.state.latestSearchQuery, this.latestQueryVersion, this.state.currentFilters);
});
_defineProperty(this, "handleAutocomplete", async query => {
const {
getAutocompleteSuggestions
} = this.props;
if (!getAutocompleteSuggestions) {
return;
}
try {
const results = await getAutocompleteSuggestions(query);
if (this.unmounted) {
return;
}
this.setState({
autocompleteSuggestions: results
});
} catch (e) {
this.props.logger.safeError(LOGGER_NAME, 'error while getting autocompletion', e);
}
});
_defineProperty(this, "getMoreSearchResults", async scope => {
const {
product
} = this.props;
if (product === 'confluence') {
try {
// This is a hack, we assume product = confluence means that this cast is safe. When GenericResultsMap is gone
// we probably won't need this cast anymore.
const currentResultsByScope = this.state.searchResults; // @ts-ignore More hacks as there's no guarantee that the scope is one that is available here
const result = currentResultsByScope[scope];
if (result) {
const numberOfCurrentItems = result.numberOfCurrentItems || CONF_OBJECTS_ITEMS_PER_PAGE;
this.setState({
searchResults: { ...this.state.searchResults,
[scope]: { ...result,
numberOfCurrentItems: numberOfCurrentItems + CONF_OBJECTS_ITEMS_PER_PAGE
}
}
});
}
} catch (e) {
this.props.logger.safeError(LOGGER_NAME, `error while getting more results for ${scope}`, e);
this.setState({
isLoading: false
});
}
}
});
_defineProperty(this, "handleSearchSubmit", event => {
const {
handleSearchSubmit
} = this.props;
if (handleSearchSubmit) {
handleSearchSubmit(event, this.props.searchSessionId);
}
});
_defineProperty(this, "handleFilter", filter => {
this.handleSearch(this.state.latestSearchQuery, this.latestQueryVersion, filter);
});
this.state = {
isLoading: true,
isError: false,
latestSearchQuery: '',
recentItems: null,
searchResults: null,
keepPreQueryState: true,
currentFilters: []
};
}
shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(nextProps, this.props) || !deepEqual(nextState, this.state);
}
componentDidCatch(error, info) {
this.props.logger.safeError(LOGGER_NAME, 'component did catch an error', {
error,
info,
safeState: {
searchSessionId: this.props.searchSessionId,
latestSearchQuery: !!this.state.latestSearchQuery,
isLoading: this.state.isLoading,
isError: this.state.isError,
keepPreQueryState: this.state.keepPreQueryState,
recentItems: !!this.state.recentItems,
searchResults: !!this.state.searchResults
}
});
this.setState({
isError: true
});
}
componentWillUnmount() {
this.unmounted = true;
}
async componentDidMount() {
const startTime = performanceNow();
this.fireExperimentExposureEvent();
try {
const {
eagerRecentItemsPromise,
lazyLoadedRecentItemsPromise
} = this.props.getRecentItems(this.props.searchSessionId);
const {
results
} = await eagerRecentItemsPromise;
const renderStartTime = performanceNow();
if (this.unmounted) {
return;
}
this.setState({
recentItems: results,
isLoading: false
});
lazyLoadedRecentItemsPromise.then(lazyLoadedRecentItems => {
this.setState({
recentItems: Object.assign({}, results, lazyLoadedRecentItems)
}, async () => {
this.fireShownPreQueryEvent(startTime, renderStartTime);
});
});
} catch (e) {
this.props.logger.safeError(LOGGER_NAME, 'error while getting recent items', e);
if (this.state.isLoading) {
this.setState({
isLoading: false
});
}
}
}
render() {
const {
linkComponent,
getSearchResultsComponent,
placeholder,
selectedResultId,
onSelectedResultIdChanged,
inputControls,
searchSessionId,
advancedSearchId
} = this.props;
const {
isLoading,
latestSearchQuery,
isError,
searchResults,
recentItems,
keepPreQueryState,
autocompleteSuggestions,
currentFilters
} = this.state;
return /*#__PURE__*/React.createElement(GlobalQuickSearch, {
onSearch: this.handleSearch,
onSearchSubmit: this.handleSearchSubmit,
onAutocomplete: this.handleAutocomplete,
isLoading: isLoading,
placeholder: placeholder,
linkComponent: linkComponent,
searchSessionId: searchSessionId,
selectedResultId: selectedResultId,
onSelectedResultIdChanged: onSelectedResultIdChanged,
inputControls: inputControls,
autocompleteSuggestions: autocompleteSuggestions,
filters: this.state.currentFilters,
advancedSearchId: advancedSearchId
}, getSearchResultsComponent({
retrySearch: this.retrySearch,
latestSearchQuery,
isError,
searchResults,
isLoading,
recentItems,
keepPreQueryState,
searchSessionId,
searchMore: this.getMoreSearchResults,
currentFilters,
onFilterChanged: this.handleFilter
}));
}
}
export const BaseConfluenceQuickSearchContainer = injectSearchSession(withAnalyticsEvents()(QuickSearchContainer));
export const BaseJiraQuickSearchContainerJira = injectSearchSession(withAnalyticsEvents()(QuickSearchContainer));