UNPKG

@atlaskit/global-search

Version:

A cross-product search component (batteries included)

365 lines 22.2 kB
import { __assign, __awaiter, __extends, __generator, __read } from "tslib"; import * as React from 'react'; import { injectIntl, FormattedHTMLMessage, } from 'react-intl'; // @ts-ignore import { withAnalytics } from '@atlaskit/analytics'; import { FilterType, } from '../../api/CrossProductSearchClient'; import { Scope } from '../../api/types'; import { SearchScreenCounter } from '../../util/ScreenCounter'; import { ConfluenceAdvancedSearchTypes, redirectToConfluenceAdvancedSearch, handlePromiseError, ADVANCED_CONFLUENCE_SEARCH_RESULT_ID, } from '../SearchResultsUtil'; import performanceNow from '../../util/performance-now'; import { BaseConfluenceQuickSearchContainer, } from '../common/QuickSearchContainer'; import { messages } from '../../messages'; import NoResultsState from './NoResultsState'; import SearchResultsComponent from '../common/SearchResults'; import { getConfluenceAdvancedSearchLink } from '../SearchResultsUtil'; import AdvancedSearchGroup from './AdvancedSearchGroup'; import { mapRecentResultsToUIGroups, mapSearchResultsToUIGroups, MAX_RECENT_RESULTS_TO_SHOW, } from './ConfluenceSearchResultsMapper'; import { CONF_MAX_DISPLAYED_RESULTS } from '../../util/experiment-utils'; import { appendListWithoutDuplication } from '../../util/search-results-utils'; import { buildConfluenceModelParams } from '../../util/model-parameters'; import ConfluenceFilterGroup from './ConfluenceFilterGroup'; import NoResultsInFilterState from './NoResultsInFilterState'; import some from 'lodash.some'; import { injectFeatures } from '../FeaturesProvider'; var getRecentItemMatches = function (query, recentItems) { if (!recentItems) { return []; } return recentItems.objects.items .filter(function (result) { return result.name.toLowerCase().indexOf(query.toLowerCase()) > -1; }) .slice(0, MAX_RECENT_RESULTS_TO_SHOW); }; var mergeSearchResultsWithRecentItems = function (searchResults, recentItems) { var defaultSearchResults = { objects: { items: [], totalSize: 0, }, spaces: { items: [], totalSize: 0, }, people: { items: [], totalSize: 0, }, }; var results = __assign(__assign({}, defaultSearchResults), searchResults); return { objects: { items: appendListWithoutDuplication(recentItems, results.objects.items), // We don't add the 3 extra results to the total. // The rationale here is that the server results will eventually contain the 3 recent items // so the total here already includes the recent items. // In the case where we don't know the number from the server, we also can't show more so // this numeber should just be the size of the current list. totalSize: results.objects.totalSize, numberOfCurrentItems: results.objects.numberOfCurrentItems, }, spaces: results.spaces, people: results.people, }; }; var LOGGER_NAME = 'AK.GlobalSearch.ConfluenceQuickSearchContainer'; var CCS_AUTOCOMPLETE = 'ccsearch-autocomplete'; /** * Container Component that handles the data fetching when the user interacts with Search. */ var ConfluenceQuickSearchContainer = /** @class */ (function (_super) { __extends(ConfluenceQuickSearchContainer, _super); function ConfluenceQuickSearchContainer() { var _this = _super !== null && _super.apply(this, arguments) || this; _this.screenCounters = { preQueryScreenCounter: new SearchScreenCounter(), postQueryScreenCounter: new SearchScreenCounter(), }; _this.handleSearchSubmit = function (event, searchSessionId) { var _a = _this.props.onAdvancedSearch, onAdvancedSearch = _a === void 0 ? function () { } : _a; var target = event.target; var query = target.value; var defaultPrevented = false; onAdvancedSearch(Object.assign({}, event, { preventDefault: function () { defaultPrevented = true; event.preventDefault(); event.stopPropagation(); }, stopPropagation: function () { }, }), ConfluenceAdvancedSearchTypes.Content, query, searchSessionId); if (!defaultPrevented) { redirectToConfluenceAdvancedSearch(query); } }; _this.handleSearchErrorAnalyticsThunk = function (source) { return function (error) { _this.handleSearchErrorAnalytics(error, source); _this.props.logger.safeError(LOGGER_NAME, "error in promise " + source, error); }; }; _this.getSearchResults = function (query, sessionId, startTime, queryVersion, filters) { var confXpSearchPromise = _this.searchCrossProductConfluence(query, sessionId, queryVersion, filters); confXpSearchPromise.catch(_this.handleSearchErrorAnalyticsThunk('xpsearch-confluence')); var mapPromiseToPerformanceTime = function (p) { return p.then(function () { return performanceNow() - startTime; }); }; return Promise.all([ confXpSearchPromise, mapPromiseToPerformanceTime(confXpSearchPromise), ]).then(function (_a) { var _b = __read(_a, 2), xpsearchResults = _b[0], confSearchElapsedMs = _b[1]; var spaces = xpsearchResults.results[Scope.ConfluenceSpace]; var objects = xpsearchResults.results[Scope.ConfluencePageBlogAttachment]; var people = xpsearchResults.results[Scope.People]; return { results: { objects: { items: objects ? objects.items : [], totalSize: objects ? objects.totalSize : 0, }, spaces: { items: spaces ? spaces.items : [], totalSize: spaces ? spaces.totalSize : 0, }, people: { items: people ? people.items : [], totalSize: people ? people.totalSize : 0, }, }, timings: { confSearchElapsedMs: confSearchElapsedMs, }, }; }); }; _this.getRecentPeople = function (sessionId) { var _a = _this.props, peopleSearchClient = _a.peopleSearchClient, crossProductSearchClient = _a.crossProductSearchClient, features = _a.features, referralContextIdentifiers = _a.referralContextIdentifiers; var referrerId = referralContextIdentifiers && referralContextIdentifiers.searchReferrerId; // We want to be consistent with the search results when prefetching is enabled so we will use URS (via aggregator) to get the // bootstrapped people results, see prefetchResults.ts. return !features.useUrsForBootstrapping ? peopleSearchClient.getRecentPeople() : crossProductSearchClient .getPeople({ query: '', sessionId: sessionId, referrerId: referrerId, currentQuickSearchContext: 'confluence', resultLimit: 3, }) .then(function (xProductResult) { var recentPeople = xProductResult.results[Scope.UserConfluence]; return recentPeople ? recentPeople.items : []; }); }; _this.getRecentItems = function (sessionId) { var confluenceClient = _this.props.confluenceClient; var recentActivityPromisesMap = { 'recent-confluence-items': confluenceClient.getRecentItems(), 'recent-confluence-spaces': confluenceClient.getRecentSpaces(), }; var recentActivityPromises = Object.keys(recentActivityPromisesMap).map(function (key) { return handlePromiseError(recentActivityPromisesMap[key], [], _this.handleSearchErrorAnalyticsThunk(key)); }); // NOTE: // We lose type safety here as typescript assumes there's no guarantee the order in which a map // gets converted into promises. Also there is currently no way (and no way in the forseeable future) // to get typescript to convert union types into tuple types (https://github.com/Microsoft/TypeScript/issues/13298) var required = Promise.all(recentActivityPromises).then(function (_a) { var _b = __read(_a, 2), recentlyViewedPages = _b[0], recentlyViewedSpaces = _b[1]; return { results: { objects: { items: recentlyViewedPages, totalSize: recentlyViewedPages.length, }, spaces: { items: recentlyViewedSpaces, totalSize: recentlyViewedSpaces.length, }, people: { items: [], totalSize: 0, }, }, }; }); return { eagerRecentItemsPromise: required, lazyLoadedRecentItemsPromise: _this.getRecentPeople(sessionId).then(function (recentPeople) { return ({ people: { items: recentPeople, totalSize: recentPeople.length, }, }); }), }; }; _this.getAutocompleteSuggestions = function (query) { var _a = _this.props, autocompleteClient = _a.autocompleteClient, features = _a.features; if (features.isNavAutocompleteEnabled) { var crossProductSearchClient = _this.props.crossProductSearchClient; var navAutocompletePromise = handlePromiseError(crossProductSearchClient.getNavAutocompleteSuggestions(query), [query], _this.handleSearchErrorAnalyticsThunk(CCS_AUTOCOMPLETE)); return navAutocompletePromise; } else if (features.isAutocompleteEnabled) { var autocompletePromise = handlePromiseError(autocompleteClient.getAutocompleteSuggestions(query), [query], _this.handleSearchErrorAnalyticsThunk(CCS_AUTOCOMPLETE)); return autocompletePromise; } return Promise.resolve([]); }; _this.getPreQueryDisplayedResults = function (recentItems, searchSessionId) { var features = _this.props.features; return mapRecentResultsToUIGroups(recentItems, features, searchSessionId); }; _this.getPostQueryDisplayedResults = function (searchResults, latestSearchQuery, recentItems, isLoading, searchSessionId) { var features = _this.props.features; var currentSearchResults = isLoading || !searchResults ? {} : searchResults; var recentResults = getRecentItemMatches(latestSearchQuery, recentItems); var mergedRecentSearchResults = mergeSearchResultsWithRecentItems(currentSearchResults, recentResults); return mapSearchResultsToUIGroups(mergedRecentSearchResults, features, searchSessionId, isLoading); }; _this.getNoResultsStateComponent = function (latestSearchQuery, searchSessionId, currentFilters, onFilterChanged) { var _a = _this.props, _b = _a.onAdvancedSearch, onAdvancedSearch = _b === void 0 ? function () { } : _b, referralContextIdentifiers = _a.referralContextIdentifiers; var filtersAreApplied = currentFilters.length > 0; if (filtersAreApplied && referralContextIdentifiers && referralContextIdentifiers.currentContainerName) { return (React.createElement(NoResultsInFilterState, { spaceTitle: referralContextIdentifiers.currentContainerName, query: latestSearchQuery, onClickAdvancedSearch: function (event, entity) { return onAdvancedSearch(event, entity, latestSearchQuery, searchSessionId); }, onClearFilters: function () { onFilterChanged([]); } })); } return (React.createElement(NoResultsState, { query: latestSearchQuery, onClick: function (event, entity) { return onAdvancedSearch(event, entity, latestSearchQuery, searchSessionId); } })); }; _this.getFilterComponent = function (searchResults) { return function (_a) { var latestSearchQuery = _a.latestSearchQuery, searchResultsTotalSize = _a.searchResultsTotalSize, isLoading = _a.isLoading, searchSessionId = _a.searchSessionId, currentFilters = _a.currentFilters, onFilterChanged = _a.onFilterChanged; var _b = _this.props, _c = _b.onAdvancedSearch, onAdvancedSearch = _c === void 0 ? function () { } : _c, features = _b.features; if (!features.spaceballsExperimentEnabled || searchResults === null) { return; } // only show filter post-query if (!latestSearchQuery) { return; } // don't show space filter if there are no results in all spaces if (currentFilters.length === 0 && searchResultsTotalSize === 0) { return; } var filterObject = _this.createQueryBasedSpaceFilter(currentFilters, searchResults); if (filterObject && filterObject.filter.spaceKeys[0] && filterObject.metadata) { var filter = filterObject.filter; var spaces_1 = filter ? filter.spaceKeys : []; return (React.createElement(ConfluenceFilterGroup, { onFilterChanged: onFilterChanged, isDisabled: isLoading, spaceTitle: filterObject.metadata.spaceTitle, spaceAvatar: filterObject.metadata.spaceAvatar, spaceKey: filter.spaceKeys[0], isFilterOn: currentFilters.length !== 0, searchSessionId: searchSessionId, onAdvancedSearch: function (event) { return onAdvancedSearch(event, ConfluenceAdvancedSearchTypes.Content, latestSearchQuery, searchSessionId, spaces_1); } })); } }; }; _this.createQueryBasedSpaceFilter = function (currentFilters, searchResults) { function instanceOfSpaceFilter(filter) { return filter.filter['@type'] === FilterType.Spaces; } function createSpaceFilter(_a) { var spaces = _a.spaces; var space = spaces.items[0]; return space && space.key && space.avatarUrl ? { filter: { '@type': FilterType.Spaces, spaceKeys: [space.key], }, metadata: { spaceAvatar: space.avatarUrl, spaceTitle: space.name, }, } : undefined; } var existingSpaceFilter = currentFilters.find(instanceOfSpaceFilter); if (existingSpaceFilter) { return existingSpaceFilter; } return createSpaceFilter(searchResults); }; _this.getSearchResultsComponent = function (_a) { var retrySearch = _a.retrySearch, latestSearchQuery = _a.latestSearchQuery, isError = _a.isError, searchResults = _a.searchResults, isLoading = _a.isLoading, recentItems = _a.recentItems, searchSessionId = _a.searchSessionId, searchMore = _a.searchMore, currentFilters = _a.currentFilters, onFilterChanged = _a.onFilterChanged; var _b = _this.props.onAdvancedSearch, onAdvancedSearch = _b === void 0 ? function () { } : _b; var onSearchMoreAdvancedSearchClicked = function (event) { onAdvancedSearch(event, ConfluenceAdvancedSearchTypes.Content, latestSearchQuery, searchSessionId); }; return (React.createElement(SearchResultsComponent, __assign({ query: latestSearchQuery, isPreQuery: !latestSearchQuery, isError: isError, onFilterChanged: onFilterChanged, getFilterComponent: _this.getFilterComponent(searchResults), isLoading: isLoading, retrySearch: retrySearch, searchMore: searchMore, currentFilters: currentFilters, onSearchMoreAdvancedSearchClicked: onSearchMoreAdvancedSearchClicked, keepPreQueryState: false, searchSessionId: searchSessionId }, _this.screenCounters, { referralContextIdentifiers: _this.props.referralContextIdentifiers, renderNoRecentActivity: function () { return (React.createElement(FormattedHTMLMessage, __assign({}, messages.no_recent_activity_body, { values: { url: getConfluenceAdvancedSearchLink() } }))); }, renderAdvancedSearchGroup: function (analyticsData) { return (React.createElement(AdvancedSearchGroup, { key: "advanced", query: latestSearchQuery, analyticsData: analyticsData, onClick: function (event, entity) { return onAdvancedSearch(event, entity, latestSearchQuery, searchSessionId); } })); }, getPreQueryGroups: function () { return _this.getPreQueryDisplayedResults(recentItems, searchSessionId); }, getPostQueryGroups: function () { return _this.getPostQueryDisplayedResults(searchResults, latestSearchQuery, recentItems, isLoading, searchSessionId); }, renderNoResult: function () { return _this.getNoResultsStateComponent(latestSearchQuery, searchSessionId, currentFilters, onFilterChanged); } }))); }; return _this; } ConfluenceQuickSearchContainer.prototype.searchCrossProductConfluence = function (query, sessionId, queryVersion, filters) { return __awaiter(this, void 0, void 0, function () { var _a, crossProductSearchClient, modelContext, referralContextIdentifiers, scopes, modelParams, referrerId, results; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = this.props, crossProductSearchClient = _a.crossProductSearchClient, modelContext = _a.modelContext, referralContextIdentifiers = _a.referralContextIdentifiers; scopes = [Scope.ConfluencePageBlogAttachment]; if (!some(filters, function (filter) { return filter['@type'] === 'spaces'; })) { scopes = scopes.concat([Scope.ConfluenceSpace, Scope.People]); } modelParams = buildConfluenceModelParams(queryVersion, modelContext || {}); referrerId = referralContextIdentifiers && referralContextIdentifiers.searchReferrerId; return [4 /*yield*/, crossProductSearchClient.search({ query: query, sessionId: sessionId, referrerId: referrerId, scopes: scopes, modelParams: modelParams, resultLimit: CONF_MAX_DISPLAYED_RESULTS, filters: filters, })]; case 1: results = _b.sent(); return [2 /*return*/, results]; } }); }); }; // TODO extract ConfluenceQuickSearchContainer.prototype.handleSearchErrorAnalytics = function (error, source) { var firePrivateAnalyticsEvent = this.props.firePrivateAnalyticsEvent; if (firePrivateAnalyticsEvent) { try { firePrivateAnalyticsEvent('atlassian.fabric.global-search.search-error', { name: error.name, message: error.message, source: source, }); } catch (e) { this.props.logger.safeError(LOGGER_NAME, 'Can not fire event atlassian.fabric.global-search.search-error', e); } } }; ConfluenceQuickSearchContainer.prototype.render = function () { var _a = this.props, linkComponent = _a.linkComponent, logger = _a.logger, inputControls = _a.inputControls, features = _a.features; var isAutocompleteEnabled = features.isAutocompleteEnabled; var isNavAutocompleteEnabled = features.isNavAutocompleteEnabled; return (React.createElement(BaseConfluenceQuickSearchContainer, { placeholder: this.props.intl.formatMessage(messages.confluence_search_placeholder), linkComponent: linkComponent, getSearchResultsComponent: this.getSearchResultsComponent, referralContextIdentifiers: this.props.referralContextIdentifiers, getRecentItems: this.getRecentItems, getSearchResults: this.getSearchResults, getAutocompleteSuggestions: isAutocompleteEnabled || isNavAutocompleteEnabled ? this.getAutocompleteSuggestions : undefined, product: "confluence", handleSearchSubmit: this.handleSearchSubmit, getPreQueryDisplayedResults: this.getPreQueryDisplayedResults, getPostQueryDisplayedResults: this.getPostQueryDisplayedResults, logger: logger, inputControls: inputControls, features: features, advancedSearchId: ADVANCED_CONFLUENCE_SEARCH_RESULT_ID })); }; return ConfluenceQuickSearchContainer; }(React.Component)); export { ConfluenceQuickSearchContainer }; export default injectFeatures(injectIntl(withAnalytics(ConfluenceQuickSearchContainer, {}, {}))); //# sourceMappingURL=ConfluenceQuickSearchContainer.js.map