UNPKG

@atlaskit/mention

Version:

A React component used to display user profiles in a list for 'Mention' functionality

327 lines (318 loc) 9.99 kB
import { utils as serviceUtils } from '@atlaskit/util-service-support'; import { isAppMention, isTeamMention, MentionNameStatus, SliNames, Actions } from '../types'; import debug from '../util/logger'; const MAX_QUERY_ITEMS = 100; const MAX_NOTIFIED_ITEMS = 20; // Re-exporting types to prevent breaking change // Re-exporting types to prevent breaking change import { SLI_EVENT_TYPE } from '../util/analytics'; import debounce from 'lodash/debounce'; /** * Support */ const emptySecurityProvider = () => { return { params: {}, headers: {} }; }; class AbstractResource { constructor() { this.changeListeners = new Map(); this.allResultsListeners = new Map(); this.errListeners = new Map(); this.infoListeners = new Map(); this.analyticsListeners = new Map(); } subscribe(key, callback, errCallback, infoCallback, allResultsCallback, analyticsListeners) { if (callback) { this.changeListeners.set(key, callback); } if (errCallback) { this.errListeners.set(key, errCallback); } if (infoCallback) { this.infoListeners.set(key, infoCallback); } if (allResultsCallback) { this.allResultsListeners.set(key, allResultsCallback); } if (analyticsListeners) { this.analyticsListeners.set(key, analyticsListeners); } } unsubscribe(key) { this.changeListeners.delete(key); this.errListeners.delete(key); this.infoListeners.delete(key); this.allResultsListeners.delete(key); this.analyticsListeners.delete(key); } } class AbstractMentionResource extends AbstractResource { shouldHighlightMention(_mention) { return false; } // eslint-disable-next-line class-methods-use-this filter(query) { throw new Error(`not yet implemented.\nParams: query=${query}`); } // eslint-disable-next-line class-methods-use-this, no-unused-vars recordMentionSelection(_mention) { // Do nothing } isFiltering(_query) { return false; } _notifyListeners(mentionsResult, stats) { debug('ak-mention-resource._notifyListeners', mentionsResult && mentionsResult.mentions && mentionsResult.mentions.length, this.changeListeners); this.changeListeners.forEach((listener, key) => { try { listener(mentionsResult.mentions.slice(0, MAX_NOTIFIED_ITEMS), mentionsResult.query, stats); } catch (e) { // ignore error from listener debug(`error from listener '${key}', ignoring`, e); } }); } _notifyAllResultsListeners(mentionsResult) { debug('ak-mention-resource._notifyAllResultsListeners', mentionsResult && mentionsResult.mentions && mentionsResult.mentions.length, this.changeListeners); this.allResultsListeners.forEach((listener, key) => { try { listener(mentionsResult.mentions.slice(0, MAX_NOTIFIED_ITEMS), mentionsResult.query); } catch (e) { // ignore error from listener debug(`error from listener '${key}', ignoring`, e); } }); } _notifyErrorListeners(error, query) { this.errListeners.forEach((listener, key) => { try { listener(error, query); } catch (e) { // ignore error from listener debug(`error from listener '${key}', ignoring`, e); } }); } _notifyInfoListeners(info) { this.infoListeners.forEach((listener, key) => { try { listener(info); } catch (e) { // ignore error fromr listener debug(`error from listener '${key}', ignoring`, e); } }); } _notifyAnalyticsListeners(event, actionSubject, action, attributes) { this.analyticsListeners.forEach((listener, key) => { try { listener(event, actionSubject, action, attributes); } catch (e) { // ignore error from listener debug(`error from listener '${key}', ignoring`, e); } }); } } /** * Provides a Javascript API */ export class MentionResource extends AbstractMentionResource { constructor(config) { super(); this.verifyMentionConfig(config); this.config = config; this.lastReturnedSearch = 0; this.activeSearches = new Set(); this.productName = config.productName; this.shouldEnableInvite = !!config.shouldEnableInvite; this.onInviteItemClick = config.onInviteItemClick; this.inviteXProductUser = config.inviteXProductUser; this.userRole = config.userRole || 'basic'; if (this.config.debounceTime) { this.filter = debounce(this.filter, this.config.debounceTime); } } shouldHighlightMention(mention) { if (this.config.shouldHighlightMention) { return this.config.shouldHighlightMention(mention); } return false; } notify(searchTime, mentionResult, query) { if (searchTime > this.lastReturnedSearch) { this.lastReturnedSearch = searchTime; this._notifyListeners(mentionResult, { duration: Date.now() - searchTime }); } else { const date = new Date(searchTime).toISOString().substr(17, 6); debug('Stale search result, skipping', date, query); // eslint-disable-line no-console, max-len } this._notifyAllResultsListeners(mentionResult); } notifyError(error, query) { this._notifyErrorListeners(error, query); if (query) { this.activeSearches.delete(query); } } async filter(query, contextIdentifier) { try { const searchTime = Date.now(); let results; if (!query) { results = await this.initialState(contextIdentifier); } else { this.activeSearches.add(query); const searchResponse = this.search(query, contextIdentifier); results = await searchResponse.mentions; } this.notify(searchTime, results, query); } catch (error) { this.notifyError(error, query); } } isFiltering(query) { return this.activeSearches.has(query); } resolveMentionName(id) { if (!this.config.mentionNameResolver) { return { id, name: '', status: MentionNameStatus.UNKNOWN }; } return this.config.mentionNameResolver.lookupName(id); } cacheMentionName(id, mentionName) { if (!this.config.mentionNameResolver) { return; } this.config.mentionNameResolver.cacheName(id, mentionName); } supportsMentionNameResolving() { return !!this.config.mentionNameResolver; } updateActiveSearches(query) { this.activeSearches.add(query); } verifyMentionConfig(config) { if (!config.url) { throw new Error('config.url is a required parameter'); } if (!config.securityProvider) { config.securityProvider = emptySecurityProvider; } } initialState(contextIdentifier) { return this.remoteInitialState(contextIdentifier); } /** * Clear a context object to generate query params by removing empty * strings, `undefined` and empty values. * * @param contextIdentifier the current context identifier * @returns a safe context for query encoding */ clearContext(contextIdentifier = {}) { return Object.keys(contextIdentifier).filter(key => contextIdentifier[key]).reduce((context, key) => ({ [key]: contextIdentifier[key], ...context }), {}); } getQueryParams(contextIdentifier) { const configParams = {}; if (this.config.containerId) { configParams['containerId'] = this.config.containerId; } if (this.config.productId) { configParams['productIdentifier'] = this.config.productId; } // if contextParams exist then it will override configParams for containerId return { ...configParams, ...this.clearContext(contextIdentifier) }; } /** * Returns the initial mention display list before a search is performed for the specified * container. * * @param contextIdentifier * @returns Promise */ async remoteInitialState(contextIdentifier) { const queryParams = this.getQueryParams(contextIdentifier); const options = { path: 'bootstrap', queryParams }; try { const result = await serviceUtils.requestService(this.config, options); this._notifyAnalyticsListeners(SLI_EVENT_TYPE, SliNames.INITIAL_STATE, Actions.SUCCEEDED); return this.transformServiceResponse(result, ''); } catch (error) { this._notifyAnalyticsListeners(SLI_EVENT_TYPE, SliNames.INITIAL_STATE, Actions.FAILED); throw error; } } search(query, contextIdentifier) { return { mentions: this.remoteSearch(query, contextIdentifier) }; } async remoteSearch(query, contextIdentifier) { const options = { path: 'search', queryParams: { query, limit: MAX_QUERY_ITEMS, ...this.getQueryParams(contextIdentifier) } }; try { const result = await serviceUtils.requestService(this.config, options); this._notifyAnalyticsListeners(SLI_EVENT_TYPE, SliNames.SEARCH, Actions.SUCCEEDED); return this.transformServiceResponse(result, query); } catch (error) { this._notifyAnalyticsListeners(SLI_EVENT_TYPE, SliNames.SEARCH, Actions.FAILED); throw error; } } transformServiceResponse(result, query) { const mentions = result.mentions.map(mention => { let lozenge; if (isAppMention(mention)) { lozenge = mention.userType; } else if (isTeamMention(mention)) { lozenge = mention.userType; } return { ...mention, lozenge, query }; }); return { ...result, mentions, query: result.query || query }; } } export class HttpError { constructor(statusCode, statusMessage) { this.statusCode = statusCode; this.message = statusMessage; this.name = 'HttpError'; this.stack = new Error().stack; } } export const isResolvingMentionProvider = p => !!(p && p.supportsMentionNameResolving && p.supportsMentionNameResolving()); export { AbstractResource, AbstractMentionResource }; export default MentionResource;