UNPKG

@atlaskit/global-search

Version:

A cross-product search component (batteries included)

519 lines (441 loc) 18.8 kB
import _regeneratorRuntime from "@babel/runtime/regenerator"; import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator"; import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; 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 { ResultType, AnalyticsType, ContentType } from '../model/Result'; import { mapJiraItemToResult } from './JiraItemMapper'; import { mapConfluenceItemToResult } from './ConfluenceItemMapper'; import { utils } from '@atlaskit/util-service-support'; import { Scope } from './types'; export var DEFAULT_AB_TEST = Object.freeze({ experimentId: 'default', abTestId: 'default', controlId: 'default' }); var QUICKSEARCH_API_URL = 'quicksearch/v1'; export var EMPTY_CROSS_PRODUCT_SEARCH_RESPONSE = { results: {} }; export var FilterType; (function (FilterType) { FilterType["Spaces"] = "spaces"; FilterType["Contributors"] = "contributors"; })(FilterType || (FilterType = {})); var CachingCrossProductSearchClientImpl = /*#__PURE__*/function () { // result limit per scope function CachingCrossProductSearchClientImpl(url, cloudId, isUserAnonymous, prefetchResults) { _classCallCheck(this, CachingCrossProductSearchClientImpl); _defineProperty(this, "RESULT_LIMIT", 10); this.serviceConfig = { url: url }; this.cloudId = cloudId; this.isUserAnonymous = isUserAnonymous; this.abTestDataCache = prefetchResults ? prefetchResults.abTestPromise : {}; this.crossProductRecentsCache = prefetchResults ? prefetchResults.crossProductRecentItemsPromise : undefined; } _createClass(CachingCrossProductSearchClientImpl, [{ key: "getNavAutocompleteSuggestions", value: function () { var _getNavAutocompleteSuggestions = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(query) { var path, results, matchingScope, matchingDocuments; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: path = 'quicksearch/v1'; _context.next = 3; return this.makeRequest(path, { cloudId: this.cloudId, scopes: [Scope.NavSearchCompleteConfluence], query: query }); case 3: results = _context.sent; matchingScope = results.scopes.find(function (scope) { return scope.id === Scope.NavSearchCompleteConfluence; }); matchingDocuments = matchingScope ? matchingScope.results : []; return _context.abrupt("return", matchingDocuments.map(mapItemToNavCompletionString)); case 7: case "end": return _context.stop(); } } }, _callee, this); })); function getNavAutocompleteSuggestions(_x) { return _getNavAutocompleteSuggestions.apply(this, arguments); } return getNavAutocompleteSuggestions; }() }, { key: "getPeople", value: function () { var _getPeople = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(_ref) { var query, sessionId, referrerId, currentQuickSearchContext, _ref$resultLimit, resultLimit, isBootstrapQuery, scope, searchPromise; return _regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: query = _ref.query, sessionId = _ref.sessionId, referrerId = _ref.referrerId, currentQuickSearchContext = _ref.currentQuickSearchContext, _ref$resultLimit = _ref.resultLimit, resultLimit = _ref$resultLimit === void 0 ? 3 : _ref$resultLimit; isBootstrapQuery = !query; // We will use the bootstrap people cache if the query is a bootstrap query and there is a result cached if (!(isBootstrapQuery && this.bootstrapPeopleCache)) { _context2.next = 4; break; } return _context2.abrupt("return", this.bootstrapPeopleCache); case 4: scope = currentQuickSearchContext === 'confluence' ? Scope.UserConfluence : currentQuickSearchContext === 'jira' ? Scope.UserJira : null; if (!scope) { _context2.next = 9; break; } searchPromise = this.search({ query: query, sessionId: sessionId, referrerId: referrerId, scopes: [scope], modelParams: [], resultLimit: resultLimit }); if (isBootstrapQuery) { this.bootstrapPeopleCache = searchPromise; } return _context2.abrupt("return", searchPromise); case 9: return _context2.abrupt("return", { results: {} }); case 10: case "end": return _context2.stop(); } } }, _callee2, this); })); function getPeople(_x2) { return _getPeople.apply(this, arguments); } return getPeople; }() }, { key: "search", value: function () { var _search = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(_ref2) { var query, sessionId, referrerId, scopes, modelParams, _ref2$resultLimit, resultLimit, _ref2$filters, filters, _ref2$mapItemToResult, mapItemToResult, body, response; return _regeneratorRuntime.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: query = _ref2.query, sessionId = _ref2.sessionId, referrerId = _ref2.referrerId, scopes = _ref2.scopes, modelParams = _ref2.modelParams, _ref2$resultLimit = _ref2.resultLimit, resultLimit = _ref2$resultLimit === void 0 ? this.RESULT_LIMIT : _ref2$resultLimit, _ref2$filters = _ref2.filters, filters = _ref2$filters === void 0 ? [] : _ref2$filters, _ref2$mapItemToResult = _ref2.mapItemToResult, mapItemToResult = _ref2$mapItemToResult === void 0 ? postQueryMapItemToResult : _ref2$mapItemToResult; body = _objectSpread({ query: query, cloudId: this.cloudId, limit: resultLimit, scopes: scopes, filters: filters, searchSession: { sessionId: sessionId, referrerId: referrerId } }, modelParams.length > 0 ? { modelParams: modelParams } : {}); _context3.next = 4; return this.makeRequest(QUICKSEARCH_API_URL, body); case 4: response = _context3.sent; return _context3.abrupt("return", this.parseResponse(response, mapItemToResult)); case 6: case "end": return _context3.stop(); } } }, _callee3, this); })); function search(_x3) { return _search.apply(this, arguments); } return search; }() }, { key: "getRecentItems", value: function () { var _getRecentItems = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4(_ref3) { var context, modelParams, _ref3$resultLimit, resultLimit, _ref3$filters, filters, mapItemToResult, scopes, recents, body, response; return _regeneratorRuntime.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: context = _ref3.context, modelParams = _ref3.modelParams, _ref3$resultLimit = _ref3.resultLimit, resultLimit = _ref3$resultLimit === void 0 ? this.RESULT_LIMIT : _ref3$resultLimit, _ref3$filters = _ref3.filters, filters = _ref3$filters === void 0 ? [] : _ref3$filters, mapItemToResult = _ref3.mapItemToResult; if (!this.isUserAnonymous) { _context4.next = 3; break; } return _context4.abrupt("return", EMPTY_CROSS_PRODUCT_SEARCH_RESPONSE); case 3: scopes = mapContextToScopes(context); if (!this.crossProductRecentsCache) { _context4.next = 10; break; } _context4.next = 7; return this.crossProductRecentsCache; case 7: recents = _context4.sent; if (!areAllScopesInCache(scopes, recents)) { _context4.next = 10; break; } return _context4.abrupt("return", { results: recents }); case 10: body = _objectSpread({ query: '', cloudId: this.cloudId, limit: resultLimit, scopes: scopes, filters: filters }, modelParams.length > 0 ? { modelParams: modelParams } : {}); _context4.next = 13; return this.makeRequest(QUICKSEARCH_API_URL, body); case 13: response = _context4.sent; return _context4.abrupt("return", this.parseResponse(response, mapItemToResult)); case 15: case "end": return _context4.stop(); } } }, _callee4, this); })); function getRecentItems(_x4) { return _getRecentItems.apply(this, arguments); } return getRecentItems; }() }, { key: "getAbTestDataForProduct", value: function () { var _getAbTestDataForProduct = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee5(product) { var scope; return _regeneratorRuntime.wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: _context5.t0 = product; _context5.next = _context5.t0 === 'confluence' ? 3 : _context5.t0 === 'jira' ? 5 : 7; break; case 3: scope = Scope.ConfluencePageBlogAttachment; return _context5.abrupt("break", 8); case 5: scope = Scope.JiraIssue; return _context5.abrupt("break", 8); case 7: throw new Error('Invalid product for abtest'); case 8: _context5.next = 10; return this.getAbTestData(scope); case 10: return _context5.abrupt("return", _context5.sent); case 11: case "end": return _context5.stop(); } } }, _callee5, this); })); function getAbTestDataForProduct(_x5) { return _getAbTestDataForProduct.apply(this, arguments); } return getAbTestDataForProduct; }() /** * @deprecated use {getAbTestDataForProduct} instead. Using manually defined scopes here can * break caching behaviour. * * This will be moved into private scope in the near future. */ }, { key: "getAbTestData", value: function () { var _getAbTestData = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee6(scope) { var path, body, response, scopeWithAbTest, abTestPromise; return _regeneratorRuntime.wrap(function _callee6$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: if (!this.abTestDataCache[scope]) { _context6.next = 2; break; } return _context6.abrupt("return", this.abTestDataCache[scope]); case 2: path = 'experiment/v1'; body = { cloudId: this.cloudId, scopes: [scope] }; _context6.next = 6; return this.makeRequest(path, body); case 6: response = _context6.sent; scopeWithAbTest = response.scopes.find(function (s) { return s.id === scope; }); abTestPromise = scopeWithAbTest ? Promise.resolve(scopeWithAbTest.abTest) : Promise.resolve(DEFAULT_AB_TEST); this.abTestDataCache[scope] = abTestPromise; return _context6.abrupt("return", abTestPromise); case 11: case "end": return _context6.stop(); } } }, _callee6, this); })); function getAbTestData(_x6) { return _getAbTestData.apply(this, arguments); } return getAbTestData; }() }, { key: "makeRequest", value: function () { var _makeRequest = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee7(path, body) { var options; return _regeneratorRuntime.wrap(function _callee7$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: options = { path: path, requestInit: { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } }; return _context7.abrupt("return", utils.requestService(this.serviceConfig, options)); case 2: case "end": return _context7.stop(); } } }, _callee7, this); })); function makeRequest(_x7, _x8) { return _makeRequest.apply(this, arguments); } return makeRequest; }() /** * Converts the raw xpsearch-aggregator response into a CrossProductSearchResults object containing * the results set and the experimentId that generated them. * * @param response * @param searchSessionId * @returns a CrossProductSearchResults object */ }, { key: "parseResponse", value: function parseResponse(response, mapItemToResult) { var abTest; var results = response.scopes.filter(function (scope) { return scope.results; }).reduce(function (resultsMap, scopeResult) { var items = scopeResult.results.map(function (result) { return mapItemToResult(scopeResult.id, result); }); // mapItemToResult returns a generic result type, technically we can't guarantee that the // type returned by `mapItemToResult` can be coerced into the expected type, e.g. there's // no guarantee the `Result` can be casted to `ConfluenceObjectResult`. We just make the assumption // here for now and suppress the typescript error resultsMap[scopeResult.id] = { // @ts-ignore items: items, totalSize: scopeResult.size !== undefined ? scopeResult.size : items.length }; if (!abTest) { abTest = scopeResult.abTest; } return resultsMap; }, {}); return { results: results, abTest: abTest }; } }]); return CachingCrossProductSearchClientImpl; }(); export { CachingCrossProductSearchClientImpl as default }; function mapPersonItemToResult(item) { var mention = item.nickname || item.name; return { resultType: ResultType.PersonResult, resultId: 'people-' + item.account_id, name: item.name, href: '/people/' + item.account_id, avatarUrl: item.picture, contentType: ContentType.Person, analyticsType: AnalyticsType.ResultPerson, mentionName: mention, presenceMessage: item.job_title || '' }; } function mapUrsResultItemToResult(item) { return { resultType: ResultType.PersonResult, resultId: 'people-' + item.id, name: item.name, href: '/people/' + item.id, avatarUrl: item.avatarUrl, contentType: ContentType.Person, analyticsType: AnalyticsType.ResultPerson, mentionName: item.nickname || '', presenceMessage: '' }; } function postQueryMapItemToResult(scope, item) { if (scope.startsWith('confluence')) { return mapConfluenceItemToResult(scope, item); } if (scope.startsWith('jira')) { return mapJiraItemToResult(AnalyticsType.ResultJira)(item); } if (scope === Scope.People) { return mapPersonItemToResult(item); } if (scope === Scope.UserConfluence || scope === Scope.UserJira) { return mapUrsResultItemToResult(item); } if (scope === Scope.NavSearchCompleteConfluence) { throw new Error('nav.completion-confluence cannot be transformed into a result because it is not a search result'); } throw new Error("Non-exhaustive match for scope: ".concat(scope)); } function mapItemToNavCompletionString(item) { var completionItem = item; return completionItem.query; } function mapContextToScopes(context) { if (context === 'jira') { return [Scope.JiraIssue, Scope.JiraBoardProjectFilter]; } else { throw new Error("Supplied contet ".concat(context, " is not supported for pre-fetching")); } } function areAllScopesInCache(scopes, cache) { return scopes.filter(function (scope) { return cache[scope] === undefined; }).length === 0; }