@atlaskit/global-search
Version:
A cross-product search component (batteries included)
537 lines (450 loc) • 19.7 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.FilterType = exports.EMPTY_CROSS_PRODUCT_SEARCH_RESPONSE = exports.DEFAULT_AB_TEST = void 0;
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _Result = require("../model/Result");
var _JiraItemMapper = require("./JiraItemMapper");
var _ConfluenceItemMapper = require("./ConfluenceItemMapper");
var _utilServiceSupport = require("@atlaskit/util-service-support");
var _types = require("./types");
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) { (0, _defineProperty2.default)(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; }
var DEFAULT_AB_TEST = Object.freeze({
experimentId: 'default',
abTestId: 'default',
controlId: 'default'
});
exports.DEFAULT_AB_TEST = DEFAULT_AB_TEST;
var QUICKSEARCH_API_URL = 'quicksearch/v1';
var EMPTY_CROSS_PRODUCT_SEARCH_RESPONSE = {
results: {}
};
exports.EMPTY_CROSS_PRODUCT_SEARCH_RESPONSE = EMPTY_CROSS_PRODUCT_SEARCH_RESPONSE;
var FilterType;
exports.FilterType = FilterType;
(function (FilterType) {
FilterType["Spaces"] = "spaces";
FilterType["Contributors"] = "contributors";
})(FilterType || (exports.FilterType = FilterType = {}));
var CachingCrossProductSearchClientImpl = /*#__PURE__*/function () {
// result limit per scope
function CachingCrossProductSearchClientImpl(url, cloudId, isUserAnonymous, prefetchResults) {
(0, _classCallCheck2.default)(this, CachingCrossProductSearchClientImpl);
(0, _defineProperty2.default)(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;
}
(0, _createClass2.default)(CachingCrossProductSearchClientImpl, [{
key: "getNavAutocompleteSuggestions",
value: function () {
var _getNavAutocompleteSuggestions = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(query) {
var path, results, matchingScope, matchingDocuments;
return _regenerator.default.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: [_types.Scope.NavSearchCompleteConfluence],
query: query
});
case 3:
results = _context.sent;
matchingScope = results.scopes.find(function (scope) {
return scope.id === _types.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 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(_ref) {
var query, sessionId, referrerId, currentQuickSearchContext, _ref$resultLimit, resultLimit, isBootstrapQuery, scope, searchPromise;
return _regenerator.default.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' ? _types.Scope.UserConfluence : currentQuickSearchContext === 'jira' ? _types.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 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(_ref2) {
var query, sessionId, referrerId, scopes, modelParams, _ref2$resultLimit, resultLimit, _ref2$filters, filters, _ref2$mapItemToResult, mapItemToResult, body, response;
return _regenerator.default.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 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(_ref3) {
var context, modelParams, _ref3$resultLimit, resultLimit, _ref3$filters, filters, mapItemToResult, scopes, recents, body, response;
return _regenerator.default.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 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5(product) {
var scope;
return _regenerator.default.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 = _types.Scope.ConfluencePageBlogAttachment;
return _context5.abrupt("break", 8);
case 5:
scope = _types.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 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee6(scope) {
var path, body, response, scopeWithAbTest, abTestPromise;
return _regenerator.default.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 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee7(path, body) {
var options;
return _regenerator.default.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", _utilServiceSupport.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;
}();
exports.default = CachingCrossProductSearchClientImpl;
function mapPersonItemToResult(item) {
var mention = item.nickname || item.name;
return {
resultType: _Result.ResultType.PersonResult,
resultId: 'people-' + item.account_id,
name: item.name,
href: '/people/' + item.account_id,
avatarUrl: item.picture,
contentType: _Result.ContentType.Person,
analyticsType: _Result.AnalyticsType.ResultPerson,
mentionName: mention,
presenceMessage: item.job_title || ''
};
}
function mapUrsResultItemToResult(item) {
return {
resultType: _Result.ResultType.PersonResult,
resultId: 'people-' + item.id,
name: item.name,
href: '/people/' + item.id,
avatarUrl: item.avatarUrl,
contentType: _Result.ContentType.Person,
analyticsType: _Result.AnalyticsType.ResultPerson,
mentionName: item.nickname || '',
presenceMessage: ''
};
}
function postQueryMapItemToResult(scope, item) {
if (scope.startsWith('confluence')) {
return (0, _ConfluenceItemMapper.mapConfluenceItemToResult)(scope, item);
}
if (scope.startsWith('jira')) {
return (0, _JiraItemMapper.mapJiraItemToResult)(_Result.AnalyticsType.ResultJira)(item);
}
if (scope === _types.Scope.People) {
return mapPersonItemToResult(item);
}
if (scope === _types.Scope.UserConfluence || scope === _types.Scope.UserJira) {
return mapUrsResultItemToResult(item);
}
if (scope === _types.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 [_types.Scope.JiraIssue, _types.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;
}