@atlaskit/global-search
Version:
A cross-product search component (batteries included)
558 lines (486 loc) • 25.4 kB
JavaScript
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, {}, {})));