UNPKG

@atlaskit/global-search

Version:

A cross-product search component (batteries included)

558 lines (486 loc) 25.4 kB
import _regeneratorRuntime from "@babel/runtime/regenerator"; import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator"; import _extends from "@babel/runtime/helpers/extends"; import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _assertThisInitialized from "@babel/runtime/helpers/assertThisInitialized"; import _inherits from "@babel/runtime/helpers/inherits"; import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn"; import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } import 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 getRecentItemMatches(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 mergeSearchResultsWithRecentItems(searchResults, recentItems) { var defaultSearchResults = { objects: { items: [], totalSize: 0 }, spaces: { items: [], totalSize: 0 }, people: { items: [], totalSize: 0 } }; var results = _objectSpread(_objectSpread({}, 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. */ export var ConfluenceQuickSearchContainer = /*#__PURE__*/function (_React$Component) { _inherits(ConfluenceQuickSearchContainer, _React$Component); var _super = _createSuper(ConfluenceQuickSearchContainer); function ConfluenceQuickSearchContainer() { var _this; _classCallCheck(this, ConfluenceQuickSearchContainer); for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this = _super.call.apply(_super, [this].concat(args)); _defineProperty(_assertThisInitialized(_this), "screenCounters", { preQueryScreenCounter: new SearchScreenCounter(), postQueryScreenCounter: new SearchScreenCounter() }); _defineProperty(_assertThisInitialized(_this), "handleSearchSubmit", function (event, searchSessionId) { var _this$props$onAdvance = _this.props.onAdvancedSearch, onAdvancedSearch = _this$props$onAdvance === void 0 ? function () {} : _this$props$onAdvance; var target = event.target; var query = target.value; var defaultPrevented = false; onAdvancedSearch(Object.assign({}, event, { preventDefault: function preventDefault() { defaultPrevented = true; event.preventDefault(); event.stopPropagation(); }, stopPropagation: function stopPropagation() {} }), ConfluenceAdvancedSearchTypes.Content, query, searchSessionId); if (!defaultPrevented) { redirectToConfluenceAdvancedSearch(query); } }); _defineProperty(_assertThisInitialized(_this), "handleSearchErrorAnalyticsThunk", function (source) { return function (error) { _this.handleSearchErrorAnalytics(error, source); _this.props.logger.safeError(LOGGER_NAME, "error in promise ".concat(source), error); }; }); _defineProperty(_assertThisInitialized(_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 mapPromiseToPerformanceTime(p) { return p.then(function () { return performanceNow() - startTime; }); }; return Promise.all([confXpSearchPromise, mapPromiseToPerformanceTime(confXpSearchPromise)]).then(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), xpsearchResults = _ref2[0], confSearchElapsedMs = _ref2[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 } }; }); }); _defineProperty(_assertThisInitialized(_this), "getRecentPeople", function (sessionId) { var _this$props = _this.props, peopleSearchClient = _this$props.peopleSearchClient, crossProductSearchClient = _this$props.crossProductSearchClient, features = _this$props.features, referralContextIdentifiers = _this$props.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 : []; }); }); _defineProperty(_assertThisInitialized(_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 (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), recentlyViewedPages = _ref4[0], recentlyViewedSpaces = _ref4[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 } }; }) }; }); _defineProperty(_assertThisInitialized(_this), "getAutocompleteSuggestions", function (query) { var _this$props2 = _this.props, autocompleteClient = _this$props2.autocompleteClient, features = _this$props2.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([]); }); _defineProperty(_assertThisInitialized(_this), "getPreQueryDisplayedResults", function (recentItems, searchSessionId) { var features = _this.props.features; return mapRecentResultsToUIGroups(recentItems, features, searchSessionId); }); _defineProperty(_assertThisInitialized(_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 // Hide the lozenge for the faster search screen ); }); _defineProperty(_assertThisInitialized(_this), "getNoResultsStateComponent", function (latestSearchQuery, searchSessionId, currentFilters, onFilterChanged) { var _this$props3 = _this.props, _this$props3$onAdvanc = _this$props3.onAdvancedSearch, onAdvancedSearch = _this$props3$onAdvanc === void 0 ? function () {} : _this$props3$onAdvanc, referralContextIdentifiers = _this$props3.referralContextIdentifiers; var filtersAreApplied = currentFilters.length > 0; if (filtersAreApplied && referralContextIdentifiers && referralContextIdentifiers.currentContainerName) { return /*#__PURE__*/React.createElement(NoResultsInFilterState, { spaceTitle: referralContextIdentifiers.currentContainerName, query: latestSearchQuery, onClickAdvancedSearch: function onClickAdvancedSearch(event, entity) { return onAdvancedSearch(event, entity, latestSearchQuery, searchSessionId); }, onClearFilters: function onClearFilters() { onFilterChanged([]); } }); } return /*#__PURE__*/React.createElement(NoResultsState, { query: latestSearchQuery, onClick: function onClick(event, entity) { return onAdvancedSearch(event, entity, latestSearchQuery, searchSessionId); } }); }); _defineProperty(_assertThisInitialized(_this), "getFilterComponent", function (searchResults) { return function (_ref5) { var latestSearchQuery = _ref5.latestSearchQuery, searchResultsTotalSize = _ref5.searchResultsTotalSize, isLoading = _ref5.isLoading, searchSessionId = _ref5.searchSessionId, currentFilters = _ref5.currentFilters, onFilterChanged = _ref5.onFilterChanged; var _this$props4 = _this.props, _this$props4$onAdvanc = _this$props4.onAdvancedSearch, _onAdvancedSearch = _this$props4$onAdvanc === void 0 ? function () {} : _this$props4$onAdvanc, features = _this$props4.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 = _filter ? _filter.spaceKeys : []; return /*#__PURE__*/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 onAdvancedSearch(event) { return _onAdvancedSearch(event, ConfluenceAdvancedSearchTypes.Content, latestSearchQuery, searchSessionId, _spaces); } }); } }; }); _defineProperty(_assertThisInitialized(_this), "createQueryBasedSpaceFilter", function (currentFilters, searchResults) { function instanceOfSpaceFilter(filter) { return filter.filter['@type'] === FilterType.Spaces; } function createSpaceFilter(_ref6) { var spaces = _ref6.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); }); _defineProperty(_assertThisInitialized(_this), "getSearchResultsComponent", function (_ref7) { var retrySearch = _ref7.retrySearch, latestSearchQuery = _ref7.latestSearchQuery, isError = _ref7.isError, searchResults = _ref7.searchResults, isLoading = _ref7.isLoading, recentItems = _ref7.recentItems, searchSessionId = _ref7.searchSessionId, searchMore = _ref7.searchMore, currentFilters = _ref7.currentFilters, onFilterChanged = _ref7.onFilterChanged; var _this$props$onAdvance2 = _this.props.onAdvancedSearch, onAdvancedSearch = _this$props$onAdvance2 === void 0 ? function () {} : _this$props$onAdvance2; var onSearchMoreAdvancedSearchClicked = function onSearchMoreAdvancedSearchClicked(event) { onAdvancedSearch(event, ConfluenceAdvancedSearchTypes.Content, latestSearchQuery, searchSessionId); }; return /*#__PURE__*/React.createElement(SearchResultsComponent, _extends({ 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 renderNoRecentActivity() { return /*#__PURE__*/React.createElement(FormattedHTMLMessage, _extends({}, messages.no_recent_activity_body, { values: { url: getConfluenceAdvancedSearchLink() } })); }, renderAdvancedSearchGroup: function renderAdvancedSearchGroup(analyticsData) { return /*#__PURE__*/React.createElement(AdvancedSearchGroup, { key: "advanced", query: latestSearchQuery, analyticsData: analyticsData, onClick: function onClick(event, entity) { return onAdvancedSearch(event, entity, latestSearchQuery, searchSessionId); } }); }, getPreQueryGroups: function getPreQueryGroups() { return _this.getPreQueryDisplayedResults(recentItems, searchSessionId); }, getPostQueryGroups: function getPostQueryGroups() { return _this.getPostQueryDisplayedResults(searchResults, latestSearchQuery, recentItems, isLoading, searchSessionId); }, renderNoResult: function renderNoResult() { return _this.getNoResultsStateComponent(latestSearchQuery, searchSessionId, currentFilters, onFilterChanged); } })); }); return _this; } _createClass(ConfluenceQuickSearchContainer, [{ key: "searchCrossProductConfluence", value: function () { var _searchCrossProductConfluence = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(query, sessionId, queryVersion, filters) { var _this$props5, crossProductSearchClient, modelContext, referralContextIdentifiers, scopes, modelParams, referrerId, results; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _this$props5 = this.props, crossProductSearchClient = _this$props5.crossProductSearchClient, modelContext = _this$props5.modelContext, referralContextIdentifiers = _this$props5.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; _context.next = 7; return crossProductSearchClient.search({ query: query, sessionId: sessionId, referrerId: referrerId, scopes: scopes, modelParams: modelParams, resultLimit: CONF_MAX_DISPLAYED_RESULTS, filters: filters }); case 7: results = _context.sent; return _context.abrupt("return", results); case 9: case "end": return _context.stop(); } } }, _callee, this); })); function searchCrossProductConfluence(_x, _x2, _x3, _x4) { return _searchCrossProductConfluence.apply(this, arguments); } return searchCrossProductConfluence; }() // TODO extract }, { key: "handleSearchErrorAnalytics", value: function handleSearchErrorAnalytics(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); } } } }, { key: "render", value: function render() { var _this$props6 = this.props, linkComponent = _this$props6.linkComponent, logger = _this$props6.logger, inputControls = _this$props6.inputControls, features = _this$props6.features; var isAutocompleteEnabled = features.isAutocompleteEnabled; var isNavAutocompleteEnabled = features.isNavAutocompleteEnabled; return /*#__PURE__*/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 default injectFeatures(injectIntl(withAnalytics(ConfluenceQuickSearchContainer, {}, {})));