Skip to content

Commit

Permalink
feat(sfint-4354) custom events filtering (#108)
Browse files Browse the repository at this point in the history
* SFINT-4354
Added a filter on custom actions
Added empty message for filtering all actions
Added tests
Update typescript compiler lib (for array.includes)
  • Loading branch information
erocheleau authored Feb 9, 2022
1 parent 30edce8 commit c159252
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 20 deletions.
2 changes: 1 addition & 1 deletion config/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion src/components/UserActions/Strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
72 changes: 60 additions & 12 deletions src/components/UserActions/UserActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Date>((value: string) => UserActivity.parseDate(value), {
required: false,
}),
Expand Down Expand Up @@ -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();

Expand All @@ -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;
};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = `
<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;
}

private buildSessionsItems(sessions: UserActionSession[]): HTMLElement[] {
Expand Down
62 changes: 56 additions & 6 deletions tests/components/UserActions/UserActivity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
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;
Expand Down Expand Up @@ -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<HTMLElement>(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<HTMLElement>(ACTION_CUSTOM_SELECTOR);
expect(customActionsElements.length).toBe(1);
const actionTitleEl = customActionsElements[0].querySelector(ACTION_TITLE_SELECTOR);
expect(actionTitleEl.innerHTML).toBe('ticket_field_update');
});
});
});
});

Expand Down

0 comments on commit c159252

Please sign in to comment.