UNPKG

@atlaskit/global-search

Version:

A cross-product search component (batteries included)

362 lines (319 loc) 11.8 kB
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));