@atlaskit/global-search
Version:
A cross-product search component (batteries included)
388 lines (357 loc) • 16.2 kB
JavaScript
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));