coveo-search-ui-extensions
Version:
Small generic components to extend the functionality of Coveo's Search UI framework.
411 lines • 19.2 kB
JavaScript
import { Component, Initialization, ComponentOptions, l, get } from 'coveo-search-ui';
import { formatTime, formatDate } from '../../utils/time';
import { UserAction, UserProfileModel, UserActionSession } from '../../models/UserProfileModel';
import { duplicate, search, view, dot, flag } from '../../utils/icons';
import { UserActionType } from '../../rest/UserProfilingEndpoint';
import './Strings';
const MSEC_IN_SECOND = 1000;
const SECONDS_IN_MINUTE = 60;
const MAX_MINUTES_IN_SESSION = 30;
const MAX_MSEC_IN_SESSION = MAX_MINUTES_IN_SESSION * SECONDS_IN_MINUTE * MSEC_IN_SECOND;
const SESSION_BEFORE_TO_DISPLAY = 2;
const SESSION_AFTER_TO_DISPLAY = 2;
const MAIN_CLASS = 'coveo-user-activity';
const ORIGIN_CLASS = 'coveo-footer';
const ACTIVITY_TITLE_SECTION = 'coveo-activity-title-section';
const ACTIVITY_TITLE_CLASS = 'coveo-activity-title';
const ACTIVITY_CLASS = 'coveo-activity';
const EVENT_CLASS = 'coveo-action';
const CLICK_EVENT_CLASS = 'coveo-click';
const SEARCH_EVENT_CLASS = 'coveo-search';
const CUSTOM_EVENT_CLASS = 'coveo-custom';
const VIEW_EVENT_CLASS = 'coveo-view';
const FOLDED_CLASS = 'coveo-folded';
const FOLDED_ACTIONS_CLASS = 'coveo-folded-actions';
const TEXT_CLASS = 'coveo-text';
const ICON_CLASS = 'coveo-icon';
const CASE_CREATION_ACTION_CLASS = 'coveo-case-creation-action';
export class UserActivity extends Component {
/**
* Create an instance of the **UserActivity** class. Initialize is needed the **UserProfileModel** and fetch user actions related to the **UserId**.
*
* @param element Element on which to bind the component.
* @param options Initialization options of the component.
* @param bindings Bindings of the Search-UI environment.
*/
constructor(element, options, bindings) {
super(element, UserActivity.ID, bindings);
this.element = element;
this.options = options;
this.bindings = bindings;
this.hasExpandedActions = false;
this.isPartOfTheSameSession = (action, previousDateTime) => {
return Math.abs(action.timestamp.valueOf() - previousDateTime.valueOf()) < MAX_MSEC_IN_SESSION;
};
this.options = ComponentOptions.initComponentOptions(element, UserActivity, options);
if (typeof this.options.ticketCreationDateTime === 'string' || typeof this.options.ticketCreationDateTime === 'number') {
this.options.ticketCreationDateTime = UserActivity.parseDate(this.options.ticketCreationDateTime);
}
if (!this.options.userId) {
this.disable();
return;
}
this.userProfileModel = get(this.root, UserProfileModel);
this.userProfileModel.getActions(this.options.userId).then((actions) => {
const sortMostRecentFirst = (a, b) => b.timestamp.getTime() - a.timestamp.getTime();
const sortedActions = [...actions].sort(sortMostRecentFirst);
let filteredActions = sortedActions;
if (this.options.customActionsExclude && this.options.customActionsExclude.length > 0) {
filteredActions = sortedActions.filter((action) => this.filterActions(action));
}
this.sessions = this.splitActionsBySessions(filteredActions);
this.buildSessionsToDisplay();
this.render();
});
}
static parseDate(value) {
try {
return new Date(value);
}
catch (e) {
console.warn(l(`${UserActivity.ID}_invalidDate`) + ` '${value}'`);
return null;
}
}
filterActions(action) {
return action.type !== UserActionType.Custom || !this.shouldExcludeCustomAction(action);
}
shouldExcludeCustomAction(action) {
const eventValue = action.raw.event_value || '';
const eventType = action.raw.event_type || '';
return this.options.customActionsExclude.includes(eventValue) || this.options.customActionsExclude.includes(eventType);
}
splitActionsBySessions(actions) {
var _a;
if (actions.length === 0) {
return [];
}
const splitSessions = [new UserActionSession(actions[0].timestamp, [])];
let previousDateTime = (_a = actions[0]) === null || _a === void 0 ? void 0 : _a.timestamp;
let currentSession = splitSessions[0];
actions.forEach((action) => {
if (this.isPartOfTheSameSession(action, previousDateTime)) {
currentSession.actions.push(action);
}
else {
splitSessions.push(new UserActionSession(action.timestamp, [action]));
currentSession = splitSessions[splitSessions.length - 1];
}
previousDateTime = action.timestamp;
});
return splitSessions;
}
buildSessionsToDisplay() {
if (this.options.ticketCreationDateTime instanceof Date) {
({ caseSubmitSessionIndex: this.caseSubmitSessionIndex, caseSubmitSession: this.caseSubmitSession } = this.findCaseSubmitSession());
if (this.caseSubmitSessionIndex !== -1) {
const sessionIndexBefore = this.caseSubmitSessionIndex - SESSION_BEFORE_TO_DISPLAY;
const sessionIndexAfter = this.caseSubmitSessionIndex + SESSION_AFTER_TO_DISPLAY;
this.sessionsToDisplay = this.findSurroundingSessions(sessionIndexBefore, sessionIndexAfter);
this.caseSubmitSession.expanded = true;
const insertTicketCreatedIndex = this.caseSubmitSession.actions.findIndex((action) => action.timestamp <= this.options.ticketCreationDateTime);
this.caseSubmitSession.actions.splice(insertTicketCreatedIndex, 0, this.buildTicketCreatedAction());
return;
}
else {
console.warn(`Could not find a user action session corresponding to this date: ${this.options.ticketCreationDateTime}.`);
}
}
if (this.sessions.length > 0) {
this.sessionsToDisplay = this.sessions.slice(0, 5);
this.sessionsToDisplay[0].expanded = true;
}
else {
this.sessionsToDisplay = [];
}
}
buildTicketCreatedAction() {
return new UserAction(UserActionType.TicketCreated, this.options.ticketCreationDateTime, {});
}
findCaseSubmitSession() {
let caseSubmitSessionIndex = this.findSessionIncludingCaseSubmit();
let caseSubmitSession = null;
if (caseSubmitSessionIndex !== -1) {
// If we found a session that correctly includes the timestamp when the ticket was created
caseSubmitSession = this.sessions[caseSubmitSessionIndex];
// return { caseSubmitSessionIndex: foundCaseSubmitSessionIndex, caseSubmitSession: this.sessions[foundCaseSubmitSessionIndex] };
}
else {
// We can try to find a session that occurred just before the ticket create.
caseSubmitSessionIndex = this.findPotentialSessionJustBeforeCaseSubmit();
if (caseSubmitSessionIndex !== -1) {
caseSubmitSession = this.sessions[caseSubmitSessionIndex];
}
}
return {
caseSubmitSessionIndex,
caseSubmitSession,
};
}
findSessionIncludingCaseSubmit() {
return this.sessions.findIndex((session) => session.actions[0].timestamp >= this.options.ticketCreationDateTime &&
session.actions[session.actions.length - 1].timestamp <= this.options.ticketCreationDateTime);
}
findPotentialSessionJustBeforeCaseSubmit() {
const potentialSessionIndex = this.sessions.findIndex((session) => session.actions[0].timestamp <= this.options.ticketCreationDateTime);
if (potentialSessionIndex !== -1) {
const lastActionInSession = this.sessions[potentialSessionIndex].actions[0];
if (!this.isPartOfTheSameSession(lastActionInSession, this.options.ticketCreationDateTime)) {
// If the session before the ticket create is not part of the same session, create a standalone session.
this.sessions.splice(potentialSessionIndex, 0, new UserActionSession(this.options.ticketCreationDateTime, []));
}
return potentialSessionIndex;
}
return -1;
}
findSurroundingSessions(from, to) {
// +1 because with slice `end` is not included.
return this.sessions.slice(Math.max(0, from), Math.min(this.sessions.length, to + 1));
}
render() {
this.element.innerHTML = '';
const panel = document.createElement('div');
panel.classList.add(MAIN_CLASS);
const activitySection = this.buildActivitySection();
activitySection.classList.add(ACTIVITY_CLASS);
panel.appendChild(activitySection);
this.element.appendChild(panel);
}
buildActivitySection() {
if (this.sessionsToDisplay.length > 0) {
const list = document.createElement('ol');
const sessionsBuilt = this.buildSessionsItems(this.sessionsToDisplay);
sessionsBuilt.forEach((sessionItem) => {
if (sessionItem) {
list.appendChild(sessionItem);
}
});
return list;
}
else {
return this.buildNoActionsMessage();
}
}
buildNoActionsMessage() {
const noActionsDiv = document.createElement('div');
noActionsDiv.innerHTML = `
<p>${l(UserActivity.ID + '_no_actions_timeline')}.</p>
<div>
<span>${l('UserActions_no_actions_causes_title')}</span>
<ul class="coveo-no-actions-causes">
<li>${l('UserActions_no_actions_cause_not_associated')}.</li>
<li>${l(UserActivity.ID + '_no_actions_cause_filtered')}.</li>
</ul>
</div>
<p>${l('UserActions_no_actions_contact_admin')}.</p>`;
return noActionsDiv;
}
buildSessionsItems(sessions) {
let hitExpanded = false;
const htmlElements = [];
sessions.forEach((session, index) => {
var _a, _b;
if (session.expanded) {
htmlElements.push(this.buildSessionItem(session));
hitExpanded = true;
}
else {
if (!hitExpanded && ((_a = sessions[index + 1]) === null || _a === void 0 ? void 0 : _a.expanded)) {
htmlElements.push(this.buildFoldedSession(session, l(`${UserActivity.ID}_showNewSession`)));
}
if (hitExpanded && ((_b = sessions[index - 1]) === null || _b === void 0 ? void 0 : _b.expanded)) {
htmlElements.push(this.buildFoldedSession(session, l(`${UserActivity.ID}_showPastSession`)));
}
}
});
return htmlElements;
}
buildFoldedSession(sessionToExpand, showMoreButtonText) {
const li = document.createElement('li');
li.classList.add(FOLDED_CLASS);
const hr = document.createElement('hr');
const span = document.createElement('span');
span.classList.add(TEXT_CLASS);
span.innerText = showMoreButtonText || l(`${UserActivity.ID}_showMore`);
hr.appendChild(span);
li.addEventListener('click', () => {
sessionToExpand.expanded = true;
this.render();
});
li.appendChild(hr);
return li;
}
buildSessionItem(session) {
if (session.actions.length === 0) {
return null;
}
const sessionContainer = document.createElement('div');
sessionContainer.classList.add('coveo-session-container');
sessionContainer.appendChild(this.buildSessionHeader(session));
this.buildSessionContent(session.actions, session === this.caseSubmitSession).forEach((actionHTML) => sessionContainer.appendChild(actionHTML));
return sessionContainer;
}
buildSessionHeader(session) {
const sessionHeader = document.createElement('div');
sessionHeader.classList.add('coveo-session-header');
sessionHeader.innerText = l(`${UserActivity.ID}_session`) + ` ${formatDate(session.timestamp)}`;
return sessionHeader;
}
buildSessionContent(actions, withFolded) {
let actionsHTML = [];
let actionsToDisplay = actions;
if (withFolded && this.options.ticketCreationDateTime && !this.hasExpandedActions) {
// Special behavior because, in the session with the Ticket Creation event,
// until the user expands them, the actions that occurred AFTER a ticket creation are collapsed.
actionsToDisplay = actionsToDisplay.filter((action) => action.timestamp <= this.options.ticketCreationDateTime);
if (actionsToDisplay.length < actions.length) {
actionsHTML.push(this.buildFoldedActions());
}
}
actionsHTML = actionsHTML.concat(actionsToDisplay.map((action) => this.buildActionListItem(action)));
return actionsHTML;
}
buildFoldedActions() {
const li = document.createElement('li');
li.classList.add(FOLDED_ACTIONS_CLASS);
const span = document.createElement('span');
span.classList.add(TEXT_CLASS);
span.innerText = l(`${UserActivity.ID}_showMoreActions`);
li.addEventListener('click', () => {
this.hasExpandedActions = true;
this.render();
});
for (let i = 0; i < 3; i++) {
const el = this.buildIcon(dot);
li.appendChild(el);
}
li.appendChild(span);
return li;
}
buildActionListItem(action) {
try {
const defaultBuilder = (action) => this.buildCustomEvent(action);
const buildersMap = {
[UserActionType.Click]: (action) => this.buildClickEvent(action),
[UserActionType.Search]: (action) => this.buildSearchEvent(action),
[UserActionType.PageView]: (action) => this.buildViewEvent(action),
[UserActionType.TicketCreated]: (action) => this.buildTicketCreated(action),
[UserActionType.Custom]: (action) => this.buildCustomEvent(action),
};
const builder = buildersMap[action.type] || defaultBuilder;
return builder(action);
}
catch (err) {
console.error(err);
return null;
}
}
buildSearchEvent(action) {
const li = document.createElement('li');
li.classList.add(EVENT_CLASS, SEARCH_EVENT_CLASS);
li.appendChild(this.buildTitleSection(action, action.query || l(`${UserActivity.ID}_emptySearch`)));
li.appendChild(this.buildFooterElement(action));
li.appendChild(this.buildIcon(search));
return li;
}
buildClickEvent(action) {
const li = document.createElement('li');
li.classList.add(EVENT_CLASS, CLICK_EVENT_CLASS);
const titleSection = document.createElement('div');
titleSection.classList.add(ACTIVITY_TITLE_SECTION);
const clickedURLElement = document.createElement('a');
clickedURLElement.classList.add(ACTIVITY_TITLE_CLASS);
clickedURLElement.innerText = (action.document && action.document.title) || '';
clickedURLElement.title = (action.document && action.document.title) || '';
clickedURLElement.href = (action.document && action.document.clickUri) || '';
titleSection.appendChild(clickedURLElement);
document.createAttributeNS('svg', 'svg');
li.appendChild(titleSection);
li.appendChild(this.buildFooterElement(action));
li.appendChild(this.buildIcon(duplicate));
return li;
}
buildViewEvent(action) {
const li = document.createElement('li');
li.classList.add(EVENT_CLASS, VIEW_EVENT_CLASS);
if (UserActivity.clickable_uri_ids.indexOf(action.raw.content_id_key) !== -1) {
//If the content id key is included in the clickable_uri list, make the component a link
const titleSection = document.createElement('div');
titleSection.classList.add(ACTIVITY_TITLE_SECTION);
const a = document.createElement('a');
a.href = action.raw.content_id_value;
a.innerText = action.raw.title || action.raw.content_id_value;
titleSection.appendChild(a);
li.appendChild(titleSection);
}
else {
li.appendChild(this.buildTitleSection(action, action.raw.title || `${action.raw.content_id_key}: ${action.raw.content_id_value}`));
}
li.appendChild(this.buildFooterElement(action));
li.appendChild(this.buildIcon(view));
return li;
}
buildCustomEvent(action) {
const li = document.createElement('li');
li.classList.add(EVENT_CLASS, CUSTOM_EVENT_CLASS);
li.appendChild(this.buildTitleSection(action, `${action.raw.event_value || action.raw.event_type || l(`${UserActivity.ID}_custom`)}`));
li.appendChild(this.buildFooterElement(action));
li.appendChild(this.buildIcon(dot));
return li;
}
buildTicketCreated(action) {
const li = document.createElement('li');
li.classList.add(EVENT_CLASS, CUSTOM_EVENT_CLASS, CASE_CREATION_ACTION_CLASS);
li.appendChild(this.buildTitleSection(action, l(`${UserActivity.ID}_ticketCreated`)));
li.appendChild(this.buildFooterElement(action));
li.appendChild(this.buildIcon(flag));
return li;
}
buildFooterElement(action) {
const el = document.createElement('div');
el.classList.add(ORIGIN_CLASS);
el.innerText = `${formatTime(action.timestamp)}`;
if (action.raw.origin_level_1) {
el.innerText += ` - ${action.raw.origin_level_1}`;
}
return el;
}
buildTitleElement(_, content) {
const el = document.createElement('div');
el.classList.add(ACTIVITY_TITLE_CLASS);
el.innerText = content;
el.title = content;
return el;
}
buildTitleSection(action, content) {
const titleSection = document.createElement('div');
titleSection.classList.add(ACTIVITY_TITLE_SECTION);
titleSection.appendChild(this.buildTitleElement(action, content));
return titleSection;
}
buildIcon(icon) {
const el = document.createElement('div');
el.classList.add(ICON_CLASS);
el.innerHTML = icon;
return el;
}
}
UserActivity.ID = 'UserActivity';
UserActivity.options = {
userId: ComponentOptions.buildStringOption({ required: true }),
customActionsExclude: ComponentOptions.buildListOption({
defaultValue: ['ticket_create_start', 'ticket_field_update', 'ticket_next_stage', 'ticket_classification_click'],
required: true,
}),
ticketCreationDateTime: ComponentOptions.buildCustomOption((value) => UserActivity.parseDate(value), {
required: false,
}),
};
UserActivity.clickable_uri_ids = ['@clickableuri'];
Initialization.registerAutoCreateComponent(UserActivity);
//# sourceMappingURL=UserActivity.js.map