coveo-search-ui
Version:
Coveo JavaScript Search Framework
593 lines (530 loc) • 19.5 kB
text/typescript
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 = '<empty>';
} 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);