UNPKG

@atlaskit/global-search

Version:

A cross-product search component (batteries included)

388 lines (357 loc) 16.2 kB
import _extends from "@babel/runtime/helpers/extends"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; import React from 'react'; import { injectIntl, FormattedHTMLMessage } from 'react-intl'; import styled from 'styled-components'; // AFP-2532 TODO: Fix automatic suppressions below // eslint-disable-next-line @atlassian/tangerine/import/entry-points import { gridSize } from '@atlaskit/theme'; import { withAnalytics } from '@atlaskit/analytics'; import { withAnalyticsEvents } from '@atlaskit/analytics-next'; import StickyFooter from '../common/StickyFooter'; import { SearchScreenCounter } from '../../util/ScreenCounter'; import { Scope } from '../../api/types'; import { BaseJiraQuickSearchContainerJira } from '../common/QuickSearchContainer'; import { messages } from '../../messages'; import SearchResultsComponent from '../common/SearchResults'; import NoResultsState from './NoResultsState'; import JiraAdvancedSearch from './JiraAdvancedSearch'; import { mapRecentResultsToUIGroups, mapSearchResultsToUIGroups, MAX_RECENT_RESULTS_TO_SHOW } from './JiraSearchResultsMapper'; import { handlePromiseError, JiraEntityTypes, redirectToJiraAdvancedSearch, ADVANCED_JIRA_SEARCH_RESULT_ID } from '../SearchResultsUtil'; import { AnalyticsType } from '../../model/Result'; import { getUniqueResultId } from '../ResultList'; import performanceNow from '../../util/performance-now'; import { fireSelectedAdvancedSearch } from '../../util/analytics-event-helper'; import AdvancedIssueSearchLink from './AdvancedIssueSearchLink'; import { getJiraMaxObjects } from '../../util/experiment-utils'; import { buildJiraModelParams } from '../../util/model-parameters'; import { injectFeatures } from '../FeaturesProvider'; import { mapJiraItemToResult } from '../../api/JiraItemMapper'; import { appendListWithoutDuplication } from '../../util/search-results-utils'; const JIRA_RESULT_LIMIT = 6; const JIRA_PREQUERY_RESULT_LIMIT = 10; const NoResultsAdvancedSearchContainer = styled.div` margin-top: ${4 * gridSize()}px; `; const BeforePreQueryStateContainer = styled.div` margin-top: ${gridSize()}px; `; const containsQuery = (string, query) => { return string.toLowerCase().indexOf(query.toLowerCase()) > -1; }; const getRecentItemMatches = (query, recentItems) => { if (!recentItems) { return []; } const issueKeyMatches = recentItems.objects.filter(result => result.objectKey && containsQuery(result.objectKey, query)); const titleMatches = recentItems.objects.filter(result => containsQuery(result.name, query)); return issueKeyMatches.concat(titleMatches).slice(0, MAX_RECENT_RESULTS_TO_SHOW); }; const mergeSearchResultsWithRecentItems = (searchResults, recentItems) => { const defaultSearchResults = { objects: [], containers: [], people: [] }; const results = { ...defaultSearchResults, ...searchResults }; return { objects: appendListWithoutDuplication(recentItems, results.objects), containers: results.containers, people: results.people }; }; /** * NOTE: This component is only consumed internally as such avoid using optional props * i.e. instead of "propX?: something" use "propX: something | undefined" * * This improves type safety and prevent us from accidentally forgetting a parameter. */ const SCOPES = [Scope.JiraIssue, Scope.JiraBoardProjectFilter]; const LOGGER_NAME = 'AK.GlobalSearch.JiraQuickSearchContainer'; /** * Container/Stateful Component that handles the data fetching and state handling when the user interacts with Search. */ export class JiraQuickSearchContainer extends React.Component { constructor(...args) { super(...args); _defineProperty(this, "state", { selectedAdvancedSearchType: JiraEntityTypes.Issues }); _defineProperty(this, "screenCounters", { preQueryScreenCounter: new SearchScreenCounter(), postQueryScreenCounter: new SearchScreenCounter() }); _defineProperty(this, "handleSearchSubmit", (event, searchSessionId) => { const { onAdvancedSearch = () => {} } = this.props; const target = event.target; const query = target.value; let defaultPrevented = false; onAdvancedSearch(Object.assign({}, event, { preventDefault() { defaultPrevented = true; event.preventDefault(); event.stopPropagation(); }, stopPropagation() {} }), this.state.selectedAdvancedSearchType, query, searchSessionId); if (!defaultPrevented) { redirectToJiraAdvancedSearch(this.state.selectedAdvancedSearchType, query); } }); _defineProperty(this, "handleAdvancedSearch", (event, entity, query, searchSessionId, analyticsData, isLoading) => { const { referralContextIdentifiers, onAdvancedSearch = () => {} } = this.props; const eventData = { resultId: ADVANCED_JIRA_SEARCH_RESULT_ID, ...analyticsData, query, // queryversion is missing contentType: entity, type: AnalyticsType.AdvancedSearchJira, isLoading }; fireSelectedAdvancedSearch(eventData, searchSessionId, referralContextIdentifiers, this.props.createAnalyticsEvent); onAdvancedSearch(event, entity, query, searchSessionId); }); _defineProperty(this, "getPreQueryDisplayedResults", (recentItems, searchSessionId) => { const { features } = this.props; return mapRecentResultsToUIGroups(recentItems, searchSessionId, features, this.props.appPermission); }); _defineProperty(this, "getPostQueryDisplayedResults", (searchResults, latestSearchQuery, recentItems, isLoading, searchSessionId) => { const { features } = this.props; if (features.isInFasterSearchExperiment) { const currentSearchResults = isLoading || !searchResults ? {} : searchResults; const recentResults = getRecentItemMatches(latestSearchQuery, recentItems); const mergedRecentSearchResults = mergeSearchResultsWithRecentItems(currentSearchResults, recentResults); return mapSearchResultsToUIGroups(mergedRecentSearchResults, searchSessionId, features, this.props.appPermission, latestSearchQuery); } return mapSearchResultsToUIGroups(searchResults, searchSessionId, features, this.props.appPermission, latestSearchQuery); }); _defineProperty(this, "getSearchResultsComponent", ({ retrySearch, latestSearchQuery, isError, searchResults, isLoading, recentItems, keepPreQueryState, searchSessionId, searchMore, currentFilters, onFilterChanged }) => { const query = latestSearchQuery; const { referralContextIdentifiers, onAdvancedSearch = () => {}, appPermission, features, isJiraPeopleProfilesEnabled } = this.props; return /*#__PURE__*/React.createElement(SearchResultsComponent, _extends({ query: query, isPreQuery: !query, isError: isError, isLoading: isLoading, retrySearch: retrySearch, keepPreQueryState: features.isInFasterSearchExperiment ? false : keepPreQueryState, searchSessionId: searchSessionId }, this.screenCounters, { referralContextIdentifiers: referralContextIdentifiers, searchMore: searchMore, currentFilters: currentFilters, onFilterChanged: onFilterChanged, renderNoRecentActivity: () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(FormattedHTMLMessage, messages.jira_no_recent_activity_body), /*#__PURE__*/React.createElement(NoResultsAdvancedSearchContainer, null, /*#__PURE__*/React.createElement(JiraAdvancedSearch, { appPermission: appPermission, query: query, analyticsData: { resultsCount: 0, wasOnNoResultsScreen: true }, onClick: (mouseEvent, entity) => this.handleAdvancedSearch(mouseEvent, entity, query, searchSessionId, { resultsCount: 0, wasOnNoResultsScreen: true }, isLoading), isJiraPeopleProfilesEnabled: isJiraPeopleProfilesEnabled }))), renderAdvancedSearchGroup: analyticsData => /*#__PURE__*/React.createElement(StickyFooter, { style: { marginTop: `${2 * gridSize()}px` } }, /*#__PURE__*/React.createElement(JiraAdvancedSearch, { appPermission: appPermission, analyticsData: analyticsData, query: query, onClick: (mouseEvent, entity) => this.handleAdvancedSearch(mouseEvent, entity, query, searchSessionId, analyticsData, isLoading), isJiraPeopleProfilesEnabled: isJiraPeopleProfilesEnabled })), renderBeforePreQueryState: () => /*#__PURE__*/React.createElement(BeforePreQueryStateContainer, null, /*#__PURE__*/React.createElement(AdvancedIssueSearchLink, { onClick: ({ event }) => onAdvancedSearch(event, JiraEntityTypes.Issues, query, searchSessionId) })), getPreQueryGroups: () => this.getPreQueryDisplayedResults(recentItems, searchSessionId), getPostQueryGroups: () => this.getPostQueryDisplayedResults(searchResults, latestSearchQuery, recentItems, isLoading, searchSessionId), renderNoResult: () => /*#__PURE__*/React.createElement(NoResultsState, { query: query, onAdvancedSearch: (mouseEvent, entity) => this.handleAdvancedSearch(mouseEvent, entity, query, searchSessionId, { resultsCount: 0, wasOnNoResultsScreen: true }, isLoading), isJiraPeopleProfilesEnabled: isJiraPeopleProfilesEnabled }) })); }); _defineProperty(this, "getRecentlyInteractedPeople", () => { /* the following code is temporarily feature flagged for performance reasons and will be shortly reinstated. https://product-fabric.atlassian.net/browse/QS-459 */ if (this.props.features.disableJiraPreQueryPeopleSearch) { return Promise.resolve([]); } else { const peoplePromise = this.props.peopleSearchClient.getRecentPeople(); return handlePromiseError(peoplePromise, [], error => this.props.logger.safeError(LOGGER_NAME, 'error in recently interacted people promise', error)); } }); _defineProperty(this, "getJiraRecentItems", () => { const { features } = this.props; return this.props.crossProductSearchClient.getRecentItems({ context: 'jira', modelParams: [], resultLimit: getJiraMaxObjects(features.abTest, JIRA_PREQUERY_RESULT_LIMIT), mapItemToResult: (_, item) => mapJiraItemToResult(AnalyticsType.RecentJira)(item) }).then(xpRecentResults => { const objects = xpRecentResults.results[Scope.JiraIssue]; const containers = xpRecentResults.results[Scope.JiraBoardProjectFilter]; return { objects: objects ? objects.items : [], containers: containers ? containers.items : [], people: [] }; }).catch(error => { this.props.logger.safeError(LOGGER_NAME, 'error in recent Jira items promise', error); return { objects: [], containers: [], people: [] }; }); }); _defineProperty(this, "canSearchUsers", () => { /* the following code is temporarily feature flagged for performance reasons and will be shortly reinstated. https://product-fabric.atlassian.net/browse/QS-459 */ if (this.props.features.disableJiraPreQueryPeopleSearch) { return Promise.resolve(false); } else { return handlePromiseError(this.props.jiraClient.canSearchUsers(), false, error => this.props.logger.safeError(LOGGER_NAME, 'error fetching browse user permission', error)); } }); _defineProperty(this, "getRecentItems", () => { return { eagerRecentItemsPromise: this.getJiraRecentItems().then(results => ({ results })), lazyLoadedRecentItemsPromise: Promise.all([this.getRecentlyInteractedPeople(), this.canSearchUsers()]).then(([people, canSearchUsers]) => { return { people: canSearchUsers ? people : [] }; }) }; }); _defineProperty(this, "getSearchResults", (query, sessionId, startTime, queryVersion) => { const { features } = this.props; const referrerId = this.props.referralContextIdentifiers && this.props.referralContextIdentifiers.searchReferrerId; const crossProductSearchPromise = this.props.crossProductSearchClient.search({ query, sessionId, referrerId, scopes: SCOPES, modelParams: buildJiraModelParams(queryVersion, this.props.referralContextIdentifiers && this.props.referralContextIdentifiers.currentContainerId), resultLimit: getJiraMaxObjects(features.abTest, JIRA_RESULT_LIMIT) }); const searchPeoplePromise = Promise.resolve([]); const mapPromiseToPerformanceTime = p => p.then(() => performanceNow() - startTime); return Promise.all([crossProductSearchPromise, searchPeoplePromise, mapPromiseToPerformanceTime(crossProductSearchPromise), mapPromiseToPerformanceTime(searchPeoplePromise), this.canSearchUsers()]).then(([xpsearchResults, peopleResults, crossProductSearchElapsedMs, peopleElapsedMs, canSearchPeople]) => { const objects = xpsearchResults.results[Scope.JiraIssue]; const containers = xpsearchResults.results[Scope.JiraBoardProjectFilter]; const objectItems = objects ? objects.items : []; this.highlightMatchingFirstResult(query, objectItems); return { results: { objects: objectItems, containers: containers ? containers.items : [], people: canSearchPeople ? peopleResults : [] }, timings: { crossProductSearchElapsedMs, peopleElapsedMs }, abTest: xpsearchResults.abTest }; }); }); } highlightMatchingFirstResult(query, issueResults) { if (issueResults && issueResults.length > 0 && typeof issueResults[0].objectKey === 'string' && (issueResults[0].objectKey.toLowerCase() === query.toLowerCase() || !!+query && issueResults[0].objectKey.toLowerCase().endsWith(`${-query}`))) { this.setState({ selectedResultId: getUniqueResultId(issueResults[0]) }); } } handleSelectedResultIdChanged(newSelectedId) { this.setState({ selectedResultId: newSelectedId }); } render() { const { linkComponent, createAnalyticsEvent, logger, features, referralContextIdentifiers, isJiraPeopleProfilesEnabled } = this.props; const { selectedResultId } = this.state; return /*#__PURE__*/React.createElement(BaseJiraQuickSearchContainerJira, { placeholder: this.props.intl.formatMessage(messages.jira_search_placeholder), linkComponent: linkComponent, getPreQueryDisplayedResults: (recentItems, searchSessionId) => this.getPreQueryDisplayedResults(recentItems, searchSessionId), getPostQueryDisplayedResults: this.getPostQueryDisplayedResults, getSearchResultsComponent: this.getSearchResultsComponent, getRecentItems: this.getRecentItems, getSearchResults: this.getSearchResults, handleSearchSubmit: this.handleSearchSubmit // @ts-ignore , createAnalyticsEvent: createAnalyticsEvent, logger: logger, selectedResultId: selectedResultId, onSelectedResultIdChanged: newId => this.handleSelectedResultIdChanged(newId), referralContextIdentifiers: referralContextIdentifiers, product: "jira", features: features, advancedSearchId: ADVANCED_JIRA_SEARCH_RESULT_ID, isJiraPeopleProfilesEnabled: isJiraPeopleProfilesEnabled }); } } const JiraQuickSearchContainerWithIntl = injectIntl(withAnalytics(JiraQuickSearchContainer, {}, {})); export default injectFeatures(withAnalyticsEvents()(JiraQuickSearchContainerWithIntl));