UNPKG

coveo-search-ui-extensions

Version:

Small generic components to extend the functionality of Coveo's Search UI framework.

411 lines 19.2 kB
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