From c1592520f8312a4e377e5eae0440a1c50992ef3d Mon Sep 17 00:00:00 2001 From: Etienne Rocheleau Date: Wed, 9 Feb 2022 10:09:36 -0500 Subject: [PATCH] feat(sfint-4354) custom events filtering (#108) * SFINT-4354 Added a filter on custom actions Added empty message for filtering all actions Added tests Update typescript compiler lib (for array.includes) --- config/tsconfig.json | 2 +- src/components/UserActions/Strings.ts | 4 +- src/components/UserActions/UserActivity.ts | 72 +++++++++++++++---- .../UserActions/UserActivity.spec.ts | 62 ++++++++++++++-- 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/config/tsconfig.json b/config/tsconfig.json index 5e6ff557..66948ed7 100644 --- a/config/tsconfig.json +++ b/config/tsconfig.json @@ -9,7 +9,7 @@ "moduleResolution": "node", "sourceMap": true, "skipLibCheck": true, - "lib": ["es2015", "dom", "es2018.promise"], + "lib": ["es2015", "es2016", "dom", "es2018.promise"], "outDir": "../bin/es6", "declaration": true, "declarationDir": "../bin/typings", diff --git a/src/components/UserActions/Strings.ts b/src/components/UserActions/Strings.ts index a34990da..4433e665 100644 --- a/src/components/UserActions/Strings.ts +++ b/src/components/UserActions/Strings.ts @@ -3,7 +3,7 @@ import { Translation, Language } from '../../utils/translation'; Translation.register(Language.English, { UserActions: 'User Actions', UserActions_no_actions_title: 'No actions available for this user', - UserActions_no_actions_causes_title: 'Potential causes', + UserActions_no_actions_causes_title: 'Possible causes', UserActions_no_actions_cause_not_enabled: 'User actions are not enabled for your organization', UserActions_no_actions_cause_not_associated: 'There are no user actions associated with the user', UserActions_no_actions_cause_case_too_old: 'The case is too old to detect related actions', @@ -23,6 +23,8 @@ Translation.register(Language.English, { UserActivity_duration: 'Duration', UserActivity_other_event: 'Other Event', UserActivity_other_events: 'Other Events', + UserActivity_no_actions_timeline: 'No actions to display in the timeline', + UserActivity_no_actions_cause_filtered: 'All the actions were filtered', UserActivity_search: 'Query', UserActivity_query: 'User Query', diff --git a/src/components/UserActions/UserActivity.ts b/src/components/UserActions/UserActivity.ts index 24368b19..4816b295 100644 --- a/src/components/UserActions/UserActivity.ts +++ b/src/components/UserActions/UserActivity.ts @@ -37,6 +37,13 @@ export interface IUserActivityOptions { * - a JavaScript Date */ ticketCreationDateTime: Date; + + /** + * List of causes or event types to exclude from custom events being displayed. + * + * Default: `[ticket_create_start, ticket_field_update, ticket_next_stage, ticket_classification_click]` + */ + customActionsExclude: string[]; } const MAIN_CLASS = 'coveo-user-activity'; @@ -59,6 +66,10 @@ export class UserActivity extends Component { static readonly ID = 'UserActivity'; static readonly options: IUserActivityOptions = { 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: string) => UserActivity.parseDate(value), { required: false, }), @@ -97,9 +108,13 @@ export class UserActivity extends Component { this.userProfileModel.getActions(this.options.userId).then((actions) => { const sortMostRecentFirst = (a: UserAction, b: UserAction) => b.timestamp.getTime() - a.timestamp.getTime(); + const sortedActions = [...actions].sort(sortMostRecentFirst); - const sortedActions = actions.sort(sortMostRecentFirst); - this.sessions = this.splitActionsBySessions(sortedActions); + 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(); @@ -116,6 +131,16 @@ export class UserActivity extends Component { } } + private filterActions(action: UserAction): boolean { + return action.type !== UserActionType.Custom || !this.shouldExcludeCustomAction(action); + } + + private shouldExcludeCustomAction(action: UserAction): boolean { + const eventValue = action.raw.event_value || ''; + const eventType = action.raw.event_type || ''; + return this.options.customActionsExclude.includes(eventValue) || this.options.customActionsExclude.includes(eventType); + } + private isPartOfTheSameSession = (action: UserAction, previousDateTime: Date): boolean => { return Math.abs(action.timestamp.valueOf() - previousDateTime.valueOf()) < MAX_MSEC_IN_SESSION; }; @@ -157,8 +182,12 @@ export class UserActivity extends Component { console.warn(`Could not find a user action session corresponding to this date: ${this.options.ticketCreationDateTime}.`); } } - this.sessionsToDisplay = this.sessions.slice(0, 5); - this.sessionsToDisplay[0].expanded = true; + if (this.sessions.length > 0) { + this.sessionsToDisplay = this.sessions.slice(0, 5); + this.sessionsToDisplay[0].expanded = true; + } else { + this.sessionsToDisplay = []; + } } private buildTicketCreatedAction(): UserAction { @@ -229,17 +258,36 @@ export class UserActivity extends Component { } private buildActivitySection(): HTMLElement { - const list = document.createElement('ol'); + if (this.sessionsToDisplay.length > 0) { + const list = document.createElement('ol'); - const sessionsBuilt = this.buildSessionsItems(this.sessionsToDisplay); + const sessionsBuilt = this.buildSessionsItems(this.sessionsToDisplay); - sessionsBuilt.forEach((sessionItem) => { - if (sessionItem) { - list.appendChild(sessionItem); - } - }); + sessionsBuilt.forEach((sessionItem) => { + if (sessionItem) { + list.appendChild(sessionItem); + } + }); + + return list; + } else { + return this.buildNoActionsMessage(); + } + } - return list; + private buildNoActionsMessage(): HTMLElement { + const noActionsDiv = document.createElement('div'); + noActionsDiv.innerHTML = ` +

${l(UserActivity.ID + '_no_actions_timeline')}.

+
+ ${l('UserActions_no_actions_causes_title')} +
    +
  • ${l('UserActions_no_actions_cause_not_associated')}.
  • +
  • ${l(UserActivity.ID + '_no_actions_cause_filtered')}.
  • +
+
+

${l('UserActions_no_actions_contact_admin')}.

`; + return noActionsDiv; } private buildSessionsItems(sessions: UserActionSession[]): HTMLElement[] { diff --git a/tests/components/UserActions/UserActivity.spec.ts b/tests/components/UserActions/UserActivity.spec.ts index 07625521..0921db4b 100644 --- a/tests/components/UserActions/UserActivity.spec.ts +++ b/tests/components/UserActions/UserActivity.spec.ts @@ -58,16 +58,21 @@ describe('UserActivity', () => { const getMockComponent = async ( returnedActions: UserAction | UserAction[], ticketCreationDateTime: Date | string | number, + customActionsExclude: string[] | null = null, element = document.createElement('div') ) => { const mock = Mock.advancedComponentSetup( UserActivity, - new Mock.AdvancedComponentSetupOptions(element, { userId: 'testuserId', ticketCreationDateTime: ticketCreationDateTime }, (env) => { - env.element = element; - getActionsPromise = Promise.resolve(returnedActions); - fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => getActionsPromise); - return env; - }) + new Mock.AdvancedComponentSetupOptions( + element, + { userId: 'testuserId', ticketCreationDateTime: ticketCreationDateTime, customActionsExclude: customActionsExclude }, + (env) => { + env.element = element; + getActionsPromise = Promise.resolve(returnedActions); + fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => getActionsPromise); + return env; + } + ) ); await getActionsPromise; return mock; @@ -435,6 +440,51 @@ describe('UserActivity', () => { const actionFooterEl = actionEl.querySelector(ACTION_FOOTER_SELECTOR); expect(actionFooterEl.innerHTML).toMatch(FAKE_EVENT_CUSTOM.raw.origin_level_1); }); + + describe('customActionsExclude option', () => { + it('should exclude events respecting the default value', async () => { + const customEventShouldBeExcluded = { + ...FAKE_EVENT_CUSTOM, + raw: { + ...FAKE_EVENT_CUSTOM.raw, + event_value: 'ticket_field_update', + }, + }; + const customEventShouldNotBeExcluded = FAKE_EVENT_CUSTOM; + + const mock = await getMockComponent([customEventShouldBeExcluded, customEventShouldNotBeExcluded], null); + + const customActionsElements = mock.cmp.element.querySelectorAll(ACTION_CUSTOM_SELECTOR); + expect(customActionsElements.length).toBe(1); + const actionTitleEl = customActionsElements[0].querySelector(ACTION_TITLE_SELECTOR); + expect(actionTitleEl.innerHTML).toBe(FAKE_EVENT_CUSTOM.raw.event_value); + }); + + it('should respect changing the default values', async () => { + const customEventShouldBeExcluded = { + ...FAKE_EVENT_CUSTOM, + raw: { + ...FAKE_EVENT_CUSTOM.raw, + event_value: 'foo', + }, + }; + + const customEventShouldNotBeExcluded = { + ...FAKE_EVENT_CUSTOM, + raw: { + ...FAKE_EVENT_CUSTOM.raw, + event_value: 'ticket_field_update', + }, + }; + + const mock = await getMockComponent([customEventShouldBeExcluded, customEventShouldNotBeExcluded], null, ['foo']); + + const customActionsElements = mock.cmp.element.querySelectorAll(ACTION_CUSTOM_SELECTOR); + expect(customActionsElements.length).toBe(1); + const actionTitleEl = customActionsElements[0].querySelector(ACTION_TITLE_SELECTOR); + expect(actionTitleEl.innerHTML).toBe('ticket_field_update'); + }); + }); }); });