UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

593 lines (530 loc) • 19.5 kB
import * as _ from 'underscore'; import { QueryEvents } from '../../events/QueryEvents'; import { ISearchAlertsEventArgs, ISearchAlertsFailEventArgs, SearchAlertsEvents } from '../../events/SearchAlertEvents'; import { SettingsEvents } from '../../events/SettingsEvents'; import { ModalBox as ModalBoxModule } from '../../ExternalModulesShim'; import { exportGlobally } from '../../GlobalExports'; import { Assert } from '../../misc/Assert'; import { AjaxError } from '../../rest/AjaxError'; import { IQuery } from '../../rest/Query'; import { ISubscription, ISubscriptionItemRequest, ISubscriptionQueryRequest, ISubscriptionRequest, SUBSCRIPTION_TYPE } from '../../rest/Subscription'; import { l } from '../../strings/Strings'; import { $$, Dom } from '../../utils/Dom'; import { SVGIcons } from '../../utils/SVGIcons'; import { analyticsActionCauseList, IAnalyticsActionCause, IAnalyticsSearchAlertsMeta, IAnalyticsSearchAlertsUpdateMeta } from '../Analytics/AnalyticsActionListMeta'; import { Component } from '../Base/Component'; import { IComponentBindings } from '../Base/ComponentBindings'; import { ComponentOptions } from '../Base/ComponentOptions'; import { IFieldOption } from '../Base/IComponentOptions'; import { Initialization } from '../Base/Initialization'; import { get } from '../Base/RegisteredNamedMethods'; import { Dropdown } from '../FormWidgets/Dropdown'; import { SearchInterface } from '../SearchInterface/SearchInterface'; import { ISettingsPopulateMenuArgs } from '../Settings/Settings'; import { SearchAlertsMessage } from './SearchAlertsMessage'; import ModalBox = Coveo.ModalBox.ModalBox; export interface ISearchAlertsOptions { enableManagePanel?: boolean; enableFollowQuery?: boolean; modifiedDateField?: IFieldOption; enableMessage?: boolean; messageCloseDelay?: number; } /** * The Search Alerts component renders items in the {@link Settings} menu that allow the end user to follow queries * and to manage search alerts. A user following a query receives email notifications when the query results change. * * **Note:** * > It is necessary to meet certain requirements to be able to use this component (see * > [Deploying Search Alerts on a Coveo JS Search Page](https://docs.coveo.com/en/1932/)). * * See also the {@link FollowItem} component. */ export class SearchAlerts extends Component { static ID = 'SearchAlerts'; static doExport = () => { exportGlobally({ SearchAlerts: SearchAlerts, SearchAlertsMessage: SearchAlertsMessage }); }; /** * The options for the search alerts * @componentOptions */ static options: ISearchAlertsOptions = { /** * Specifies whether to add the **Manage Alerts** item in the {@link Settings} menu to allow the end user to manage * search alerts. * * Clicking the **Manage Alerts** item calls the {@link SearchAlerts.openPanel} method. * * Default value is `true`. */ enableManagePanel: ComponentOptions.buildBooleanOption({ defaultValue: true }), /** * Specifies whether to add the **Follow Query** item in the {@link Settings} menu to allow the end user to follow * the last query. * * Clicking the **Follow Query** item calls the {@link SearchAlerts.followQuery} method. * * Default value is `true`. */ enableFollowQuery: ComponentOptions.buildBooleanOption({ defaultValue: true }), /** * Specifies which field to use to represent the modification date when sending the * {@link ISubscriptionQueryRequest}. * * Default value is `undefined`. */ modifiedDateField: ComponentOptions.buildFieldOption(), /** * Specifies whether to display info and error messages when performing search alerts actions. * * If this options is `true`, the SearchAlerts constructor will automatically instantiate a * {@link SearchAlertsMessage} component and set it to the {@link SearchAlerts.message} attribute. * * See also {@link SearchAlerts.options.messageCloseDelay}. * * Default value is `true`. */ enableMessage: ComponentOptions.buildBooleanOption({ defaultValue: true }), /** * If {@link SearchAlerts.options.enableMessage} is `true`, specifies how long to display the search alert messages * (in milliseconds). * * Default value is `2000`. Minimum value is `0`. */ messageCloseDelay: ComponentOptions.buildNumberOption({ defaultValue: 2000, min: 0, depend: 'enableMessage' }) }; private modal: ModalBox; /** * A reference to a {@link SearchAlertsMessage} component that the SearchAlerts component uses to display messages. */ public message: SearchAlertsMessage; /** * Creates a new SearchAlerts component. * @param element The HTMLElement on which to instantiate the component. * @param options The options for the SearchAlerts component. * @param bindings The bindings that the component requires to function normally. If not set, these will be * automatically resolved (with a slower execution time). */ constructor( public element: HTMLElement, public options: ISearchAlertsOptions, bindings?: IComponentBindings, private ModalBox = ModalBoxModule ) { super(element, SearchAlerts.ID, bindings); this.options = ComponentOptions.initComponentOptions(element, SearchAlerts, options); if (this.options.enableMessage) { this.message = new SearchAlertsMessage(element, { closeDelay: this.options.messageCloseDelay }, this.getBindings()); } if (!this.queryController.getEndpoint().options.isGuestUser) { this.bind.onRootElement(SettingsEvents.settingsPopulateMenu, (args: ISettingsPopulateMenuArgs) => { if (this.options.enableManagePanel) { args.menuData.push({ text: l('SearchAlerts_Panel'), className: 'coveo-subscriptions-panel', onOpen: () => this.openPanel(), onClose: () => this.close(), svgIcon: SVGIcons.icons.dropdownFollowQuery, svgIconClassName: 'coveo-subscriptions-panel-svg' }); } }); } else { this.logger.warn('Logged in as guest user, search alerts are therefore not available.'); } let once = false; this.bind.onRootElement(QueryEvents.querySuccess, () => { if (!once) { once = true; this.queryController .getEndpoint() .listSubscriptions() .then(() => { this.bind.onRootElement(SettingsEvents.settingsPopulateMenu, (args: ISettingsPopulateMenuArgs) => { if (this.options.enableFollowQuery) { args.menuData.push({ text: l('SearchAlerts_followQuery'), className: 'coveo-follow-query', tooltip: l('FollowQueryDescription'), onOpen: () => this.followQuery(), onClose: () => {}, svgIcon: SVGIcons.icons.dropdownFollowQuery, svgIconClassName: 'coveo-follow-query-svg' }); } }); }) .catch((e: AjaxError) => { // Trap 403 error, as the listSubscription call is called on every page initialization // to check for current subscriptions. By default, the search alert service is not enabled for most organization // Don't want to pollute the console with un-needed noise and confusion if (e.status != 403) { throw e; } }); } }); } /** * Follows the last query. The user will start receiving email notifications when the query results change. * * Also logs the `searchAlertsFollowQuery` event in the usage analytics with the name of the request as meta data. */ public followQuery() { const queryBuilder = this.queryController.createQueryBuilder({}); const request = this.buildFollowQueryRequest(queryBuilder.build(), this.options); this.usageAnalytics.logCustomEvent<IAnalyticsSearchAlertsMeta>( analyticsActionCauseList.searchAlertsFollowQuery, { subscription: request.name }, this.element ); this.queryController .getEndpoint() .follow(request) .then((subscription: ISubscription) => { if (subscription) { const eventArgs: ISearchAlertsEventArgs = { subscription: subscription, dom: this.findQueryBoxDom() }; $$(this.root).trigger(SearchAlertsEvents.searchAlertsCreated, eventArgs); } else { this.triggerSearchAlertsFail(); } }) .catch(() => { this.triggerSearchAlertsFail(); }); } /** * Opens the **Manage Alerts** panel. This panel allows the end user to stop following queries or items. It also * allows the end user to specify email notification frequency for each followed query or item. */ public async openPanel(): Promise<void> { const title = $$('div'); const titleInfo = $$( 'div', { className: 'coveo-subscriptions-panel-title' }, l('SearchAlerts_Panel') ); title.append(titleInfo.el); const container = $$('div'); const table = $$('table', { className: 'coveo-subscriptions-panel-content', cellspacing: 0 }); container.append(table.el); const tableHead = $$('thead'); table.append(tableHead.el); const rowHead = $$('tr'); tableHead.append(rowHead.el); const headerType = $$( 'th', { className: 'coveo-subscriptions-panel-content-type' }, l('SearchAlerts_Type') ); const headerContent = $$('th', null, l('SearchAlerts_Content')); const headerFrequency = $$('th', null, l('SearchAlerts_Frequency')); const headerActions = $$( 'th', { className: 'coveo-subscriptions-panel-content-actions' }, l('SearchAlerts_Actions') ); rowHead.append(headerType.el); rowHead.append(headerContent.el); rowHead.append(headerFrequency.el); rowHead.append(headerActions.el); const tableBodySpacer = $$( 'tbody', { className: 'coveo-subscriptions-panel-spacer' }, $$( 'tr', null, $$('td', { colsspan: 3 }) ) ); table.append(tableBodySpacer.el); const tableBodySubscriptions = $$( 'tbody', { className: 'coveo-subscriptions-panel-subscriptions' }, $$( 'tr', { className: 'coveo-subscriptions-panel-no-subscriptions' }, $$( 'td', { colspan: 3 }, l('SearchAlerts_PanelNoSearchAlerts') ) ) ); table.append(tableBodySubscriptions.el); let sizeModForModalBox = 'big'; try { const subscriptions = await this.queryController.getEndpoint().listSubscriptions(); _.each(subscriptions, subscription => this.addSearchAlert(subscription, container)); } catch (e) { this.logger.error('Error retrieving subscriptions', e); sizeModForModalBox = 'small'; container.empty(); container.append(this.getFailureMessage().el); } this.modal = this.ModalBox.open(container.el, { title: title.el.outerHTML, className: 'coveo-subscriptions-panel', sizeMod: sizeModForModalBox, body: this.searchInterface.options.modalContainer }); } private getFailureMessage(): Dom { return $$( 'div', { className: 'coveo-subscriptions-panel-fail' }, l('SearchAlerts_Fail') ); } private handleSearchAlertsFail() { if (this.modal != null) { const modalBody = $$(this.modal.wrapper).find('.coveo-modal-body'); $$(modalBody).empty(); $$(modalBody).append(this.getFailureMessage().el); } } private close() { if (this.modal) { this.modal.close(); this.modal = null; } } private addSearchAlert(subscription: ISubscription, container: Dom) { const frequencies = [ { value: 'monthly', label: l('Monthly') }, { value: 'daily', label: l('Daily') }, { value: 'monday', label: l('Monday') }, { value: 'tuesday', label: l('Tuesday') }, { value: 'wednesday', label: l('Wednesday') }, { value: 'thursday', label: l('Thursday') }, { value: 'friday', label: l('Friday') }, { value: 'saturday', label: l('Saturday') }, { value: 'sunday', label: l('Sunday') } ]; let context: string; if (subscription.name) { if (subscription.name == '<empty>') { context = '&lt;empty&gt;'; } else { const textExtracted = $$('div').el; textExtracted.innerHTML = subscription.name; context = $$(textExtracted).text(); } } else if (subscription.type == SUBSCRIPTION_TYPE.followQuery) { const typeConfig = <ISubscriptionQueryRequest>subscription.typeConfig; context = _.escape(typeConfig.query.q) || l('EmptyQuery'); } else { const typeConfig = <ISubscriptionItemRequest>subscription.typeConfig; context = _.escape(typeConfig.title || typeConfig.id); } const row = $$('tr', { className: 'coveo-subscriptions-panel-subscription' }); const pluckFrequenciesValues = _.pluck(frequencies, 'value'); const valueToLabel = (valueMappedToLabel: string) => _.findWhere(frequencies, { value: valueMappedToLabel }).label; const buildDropdown = () => { return new Dropdown( (dropdownInstance: Dropdown) => { this.usageAnalytics.logCustomEvent<IAnalyticsSearchAlertsUpdateMeta>( analyticsActionCauseList.searchAlertsUpdateSubscription, { subscription: context, frequency: dropdownInstance.getValue() }, this.element ); subscription.frequency = dropdownInstance.getValue(); this.updateAndSyncSearchAlert(subscription); }, pluckFrequenciesValues, valueToLabel ).build(); }; const contentTypeElement = $$( 'td', { className: 'coveo-subscriptions-panel-content-type' }, l('SearchAlerts_Type_' + subscription.type) ); const contextElement = $$('td', { className: 'coveo-subscriptions-panel-context', title: context }); contextElement.setHtml(context); const frequencyElement = $$( 'td', null, $$( 'div', { className: 'coveo-subscriptions-panel-frequency' }, buildDropdown() ) ); const contentActionsElement = $$( 'td', { className: 'coveo-subscriptions-panel-content-actions' }, null, $$( 'div', { className: 'coveo-subscriptions-panel-action coveo-subscriptions-panel-action-unfollow' }, l('SearchAlerts_unFollowing') ), $$( 'div', { className: 'coveo-subscriptions-panel-action coveo-subscriptions-panel-action-follow' }, l('SearchAlerts_follow') ) ); row.append(contentTypeElement.el); row.append(contextElement.el); row.append(frequencyElement.el); row.append(contentActionsElement.el); const noSearchAlerts = container.find('.coveo-subscriptions-panel-no-subscriptions'); row.insertBefore(noSearchAlerts); const frequencyInput = <HTMLSelectElement>frequencyElement.find('select'); frequencyInput.value = subscription.frequency; $$(row.find('.coveo-subscriptions-panel-action-unfollow')).on('click', () => { row.addClass('coveo-subscription-unfollowed'); this.queryController .getEndpoint() .deleteSubscription(subscription) .then(() => { if (subscription.type == SUBSCRIPTION_TYPE.followDocument) { this.logAnalyticsEvent(analyticsActionCauseList.searchAlertsUnfollowDocument, subscription); } else if (subscription.type == SUBSCRIPTION_TYPE.followQuery) { this.logAnalyticsEvent(analyticsActionCauseList.searchAlertsUnfollowQuery, subscription); } delete subscription.id; const eventArgs: ISearchAlertsEventArgs = { subscription: subscription }; $$(this.root).trigger(SearchAlertsEvents.searchAlertsDeleted, eventArgs); }) .catch(() => { this.handleSearchAlertsFail(); }); }); $$(row.find('.coveo-subscriptions-panel-action-follow')).on('click', () => { row.removeClass('coveo-subscription-unfollowed'); this.queryController .getEndpoint() .follow(subscription) .then(updatedSearchAlert => { if (subscription.type == SUBSCRIPTION_TYPE.followDocument) { this.logAnalyticsEvent(analyticsActionCauseList.searchAlertsFollowDocument, subscription); } else if (subscription.type == SUBSCRIPTION_TYPE.followQuery) { this.logAnalyticsEvent(analyticsActionCauseList.searchAlertsFollowQuery, subscription); } subscription.id = updatedSearchAlert.id; const eventArgs: ISearchAlertsEventArgs = { subscription: subscription }; $$(this.root).trigger(SearchAlertsEvents.searchAlertsCreated, eventArgs); }) .catch(() => { this.handleSearchAlertsFail(); }); }); } private updateAndSyncSearchAlert(subscription: ISubscription) { this.queryController .getEndpoint() .updateSubscription(subscription) .then((updated: ISubscription) => _.extend(subscription, updated)) .catch(() => { this.handleSearchAlertsFail(); }); } private triggerSearchAlertsFail() { const eventArgs: ISearchAlertsFailEventArgs = { dom: this.findQueryBoxDom() }; $$(this.root).trigger(SearchAlertsEvents.searchAlertsFail, eventArgs); } protected findQueryBoxDom(): HTMLElement { let dom: HTMLElement; const components = this.searchInterface.getComponents<Component>('Querybox'); if (components && components.length > 0) { dom = _.first(components).element; } else { const components = this.searchInterface.getComponents<Component>('Omnibox'); if (components && components.length > 0) { dom = _.first(components).element; } } return dom; } private buildFollowQueryRequest(query: IQuery, options: ISearchAlertsOptions): ISubscriptionRequest { const typeConfig: ISubscriptionQueryRequest = { query: query }; if (options.modifiedDateField) { typeConfig.modifiedDateField = <string>options.modifiedDateField; } return { type: SUBSCRIPTION_TYPE.followQuery, typeConfig: typeConfig, name: this.message.getFollowQueryMessage(query.q) }; } private logAnalyticsEvent(cause: IAnalyticsActionCause, subscription: ISubscription) { this.usageAnalytics.logCustomEvent<IAnalyticsSearchAlertsMeta>( cause, { subscription: subscription.name }, this.element ); } static create(element: HTMLElement, options?: ISearchAlertsOptions, root?: HTMLElement): SearchAlerts { Assert.exists(element); return new SearchAlerts(element, options, (<SearchInterface>get(root, SearchInterface)).getBindings()); } } Initialization.registerAutoCreateComponent(SearchAlerts);