Skip to content

Commit

Permalink
Merge branch 'feature/SNOW-352_Add_TopQueries_Component' of https://g…
Browse files Browse the repository at this point in the history
…ithub.com/wosullivan/search-ui-extensions into feature/SNOW-352_Add_TopQueries_Component
  • Loading branch information
wosullivan committed Oct 2, 2020
2 parents 29d8845 + 5a72f90 commit e98dbad
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 135 deletions.
4 changes: 1 addition & 3 deletions src/components/TopQueries/TopQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ export class TopQueries extends Component {
const li = document.createElement('li');
const a = document.createElement('a');
a.classList.add('coveo-link');
a.addEventListener('click', () => {
this.options.onClick(completion.expression, this);
});
a.addEventListener('click', () => this.options.onClick(completion.expression, this));
a.innerHTML = completion.expression;

li.appendChild(a);
Expand Down
30 changes: 1 addition & 29 deletions src/components/UserActions/ClickedDocumentList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ import {
QueryUtils,
l,
get,
ResultLink,
} from 'coveo-search-ui';
import { UserProfileModel, UserAction } from '../../models/UserProfileModel';
import { ExpandableList } from './ExpandableList';
import { UserActionType } from '../../rest/UserProfilingEndpoint';
import { duplicate } from '../../utils/icons';
import './Strings';
import { UserActionEvents } from './Events';

/**
* Initialization options of the **ClickedDocumentList** class.
Expand Down Expand Up @@ -138,11 +136,7 @@ export class ClickedDocumentList extends Component {
currentLayout: 'list',
responsiveComponents: this.searchInterface.responsiveComponents,
})).then((element) => {
Initialization.automaticallyCreateComponentsInsideResult(element, result, {
ResultLink: {
onClick: this.openDocument(result),
},
});
Initialization.automaticallyCreateComponentsInsideResult(element, result);
return element;
});
},
Expand All @@ -152,28 +146,6 @@ export class ClickedDocumentList extends Component {
showLessMessage: l(`${ClickedDocumentList.ID}_less`),
});
}

private openDocument(result: IQueryResult) {
return function (this: ResultLink) {
this.usageAnalytics.logCustomEvent(
UserActionEvents.documentClick,
{
documentUrl: result.clickUri,
documentTitle: result.title,
sourceName: QueryUtils.getSource(result),
author: QueryUtils.getAuthor(result),
},
this.element,
result
);

if (this.options.alwaysOpenInNewWindow) {
this.openLinkInNewWindow(false);
} else {
this.openLink(false);
}
};
}
}

Initialization.registerAutoCreateComponent(ClickedDocumentList);
4 changes: 2 additions & 2 deletions src/components/UserActions/Events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { IAnalyticsActionCause } from 'coveo-search-ui';
export const USER_ACTION_EVENT_TYPE = 'User Actions';

export class UserActionEvents {
public static readonly documentClick: IAnalyticsActionCause = Object.freeze({
name: 'userActionDocumentClick',
public static readonly load: IAnalyticsActionCause = Object.freeze({
name: 'userActionLoad',
type: USER_ACTION_EVENT_TYPE,
});
public static readonly submit: IAnalyticsActionCause = Object.freeze({
Expand Down
3 changes: 2 additions & 1 deletion src/components/UserActions/UserActions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ $timestamp-grey: #67768b;
margin-left: 0.5em;
}

.coveo-click {
.coveo-click,
.coveo-view {
a {
cursor: pointer;
color: $coveo-blue-6;
Expand Down
12 changes: 11 additions & 1 deletion src/components/UserActions/UserActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export class UserActivity extends Component {
}),
};

private static clickable_uri_ids = ['@clickableuri'];
private actions: UserAction[];
private foldedActions: UserAction[];
private userProfileModel: UserProfileModel;
Expand Down Expand Up @@ -276,8 +277,17 @@ export class UserActivity extends Component {
li.classList.add(VIEW_EVENT_CLASS);

const dataElement = document.createElement('div');
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 a = document.createElement('a');
a.href = action.raw.content_id_value;
a.innerText = action.raw.content_id_value;
dataElement.appendChild(a);
} else {
dataElement.innerText = `${action.raw.content_id_key}: ${action.raw.content_id_value}`;
}

dataElement.classList.add(DATA_CLASS);
dataElement.innerText = `${action.raw.content_id_key}: ${action.raw.content_id_value}`;

li.appendChild(this.buildTitleSection(action));
li.appendChild(dataElement);
Expand Down
52 changes: 47 additions & 5 deletions src/models/UserProfileModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { Model, QueryBuilder, IQueryResult, AccessToken, Assert, ISearchEndpoint, SearchEndpoint } from 'coveo-search-ui';
import {
Model,
QueryBuilder,
IQueryResult,
AccessToken,
Assert,
ISearchEndpoint,
SearchEndpoint,
Component,
SearchInterface,
IQuery,
IQueryResults,
} from 'coveo-search-ui';
import { UserActionEvents } from '../components/UserActions/Events';
import { UserProfilingEndpoint, IActionHistory, UserActionType } from '../rest/UserProfilingEndpoint';

/**
Expand Down Expand Up @@ -135,18 +148,22 @@ export class UserProfileModel extends Model {
return Promise.resolve({});
}

const query = new QueryBuilder();
query.advancedExpression.addFieldExpression(
const builder = new QueryBuilder();
builder.advancedExpression.addFieldExpression(
'@urihash',
'==',
urihashes.filter((x) => x)
);

// Ensure we fetch the good amount of document.
query.numberOfResults = urihashes.length;
builder.numberOfResults = urihashes.length;

// Here we directly use the Search Endpoint to query without side effects.
const searchRequest = await this.options.searchEndpoint.search(query.build());
const query = builder.build();
const searchRequest = await this.options.searchEndpoint.search(query);

// Here we directly send the event using the Analytics Endpoint to prevent any unwanted side effects.
this.sendUserActionLoad(query, searchRequest);

const documentsDict = searchRequest.results.reduce((acc, result) => ({ ...acc, [result.raw.urihash]: result }), {});

Expand All @@ -165,6 +182,7 @@ export class UserProfileModel extends Model {
try {
documents = await this.fetchDocuments(urihashes);
} catch (error) {
console.log(error);
this.logger.error(UserProfileModel.ERROR_MESSAGE.FETCH_CLICKED_DOCUMENT_FAIL, error);
}

Expand All @@ -190,6 +208,30 @@ export class UserProfileModel extends Model {
private isSearch(action: IActionHistory) {
return action.name === UserActionType.Search;
}

private sendUserActionLoad(query: IQuery, result: IQueryResults) {
const uaClient = (Component.get(this.element, SearchInterface, true) as SearchInterface)?.usageAnalytics;
uaClient?.logSearchEvent(UserActionEvents.load, {});
uaClient?.endpoint.sendSearchEvents([
{
...uaClient.getPendingSearchEvent().templateSearchEvent,
...{
queryPipeline: result.pipeline,
splitTestRunName: result.splitTestRun,
splitTestRunVersion: result.splitTestRun ? result.pipeline : undefined,
queryText: query.q ?? '',
advancedQuery: query.aq ?? '',
didYouMean: query.enableDidYouMean,
numberOfResults: result.totalCount,
responseTime: result.duration,
pageNumber: query.firstResult / query.numberOfResults,
resultsPerPage: query.numberOfResults,
searchQueryUid: result.searchUid,
contextual: false,
},
},
]);
}
}

/**
Expand Down
92 changes: 1 addition & 91 deletions tests/components/UserActions/ClickedDocumentList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { createSandbox, SinonSandbox, SinonStub } from 'sinon';
import { Mock, Fake } from 'coveo-search-ui-tests';
import { ClickedDocumentList } from '../../../src/components/UserActions/ClickedDocumentList';
import { UserProfileModel, UserAction } from '../../../src/models/UserProfileModel';
import { Logger, Initialization, ResultLink, NoopAnalyticsClient, QueryUtils } from 'coveo-search-ui';
import { Logger, Initialization } from 'coveo-search-ui';
import { generate, fakeUserProfileModel, waitForPromiseCompletion } from '../../utils';
import { UserActionType } from '../../../src/rest/UserProfilingEndpoint';
import { UserActionEvents } from '../../../src/components/UserActions/Events';

describe('ClickedDocumentList', () => {
const BUILD_ACTION = (hash: string, i: number) => {
Expand Down Expand Up @@ -271,95 +270,6 @@ describe('ClickedDocumentList', () => {
expect(mock.cmp.disabled).toBe(true);
});

it('Should open the link in a new tab when alwaysOpenInNewWindow is true', async () => {
let openLinkStub = sandbox.stub(ResultLink.prototype, 'openLink');
let openInANewTabStub = sandbox.stub(ResultLink.prototype, 'openLinkInNewWindow');
const mock = Mock.advancedComponentSetup<ClickedDocumentList>(
ClickedDocumentList,
new Mock.AdvancedComponentSetupOptions(null, { userId: 'testuserId' }, (env) => {
fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => Promise.resolve(TEST_CLICKS));
env.componentOptionsModel;
env.usageAnalytics = new NoopAnalyticsClient();
env.withResult();
env.searchInterface.options.originalOptionsObject['ResultLink'] = {
...env.searchInterface.options.originalOptionsObject['ResultLink'],
};
env.searchInterface.options.originalOptionsObject['ResultLink']['alwaysOpenInNewWindow'] = true;
return env;
})
);

await waitForPromiseCompletion();

mock.env.element.querySelector<HTMLOListElement>('.coveo-list .CoveoResultLink').click();

expect(openLinkStub.calledWith(false)).toBe(false);
expect(openInANewTabStub.calledWith(false)).toBe(true);
});

it('Should not open the link in a new tab when alwaysOpenInNewWindow is false', async () => {
let openLinkStub = sandbox.stub(ResultLink.prototype, 'openLink');
let openInANewTabStub = sandbox.stub(ResultLink.prototype, 'openLinkInNewWindow');
const mock = Mock.advancedComponentSetup<ClickedDocumentList>(
ClickedDocumentList,
new Mock.AdvancedComponentSetupOptions(null, { userId: 'testuserId' }, (env) => {
fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => Promise.resolve(TEST_CLICKS));
env.componentOptionsModel;
env.usageAnalytics = new NoopAnalyticsClient();
env.withResult();
env.searchInterface.options.originalOptionsObject['ResultLink'] = {
...env.searchInterface.options.originalOptionsObject['ResultLink'],
};
env.searchInterface.options.originalOptionsObject['ResultLink']['alwaysOpenInNewWindow'] = false;
return env;
})
);

await waitForPromiseCompletion();

mock.env.element.querySelector<HTMLOListElement>('.coveo-list .CoveoResultLink').click();

expect(openLinkStub.calledWith(false)).toBe(true);
expect(openInANewTabStub.calledWith(false)).toBe(false);
});

it('should log a userActionDocumentClick event when a result link in the template is clicked', async () => {
let openLinkStub = sandbox.stub(ResultLink.prototype, 'openLink');
let logSearchStub: SinonStub;
let logCustomStub: SinonStub;
const mock = Mock.advancedComponentSetup<ClickedDocumentList>(
ClickedDocumentList,
new Mock.AdvancedComponentSetupOptions(null, { userId: 'testuserId' }, (env) => {
fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => Promise.resolve(TEST_CLICKS));
env.usageAnalytics = new NoopAnalyticsClient();
env.withResult();
logSearchStub = sandbox.stub(env.usageAnalytics, 'logSearchEvent');
logCustomStub = sandbox.stub(env.usageAnalytics, 'logCustomEvent');
return env;
})
);

await waitForPromiseCompletion();

mock.env.element.querySelector<HTMLOListElement>('.coveo-list .CoveoResultLink').click();

expect(openLinkStub.calledWith(false)).toBe(true);
expect(logSearchStub.called).toBeFalse;
expect(
logCustomStub.calledWith(
UserActionEvents.documentClick,
{
documentUrl: mock.env.result.clickUri,
documentTitle: mock.env.result.title,
sourceName: QueryUtils.getSource(mock.env.result),
author: QueryUtils.getAuthor(mock.env.result),
},
null,
mock.env.result
)
).toBeTrue;
});

describe('template', () => {
it('should use the given template in options', async () => {
sandbox.stub(Initialization, 'automaticallyCreateComponentsInsideResult');
Expand Down
2 changes: 1 addition & 1 deletion tests/components/UserActions/UserActions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ describe('UserActions', () => {
it('should fetch all user actions', async function () {
await mock.cmp.show();

expect(modelMock.getActions.calledWithExactly(someUserId)).toBe(true);
expect(modelMock.getActions.calledWith(someUserId)).toBe(true);
});

it('should trigger a userActionsShow event', async () => {
Expand Down
Loading

0 comments on commit e98dbad

Please sign in to comment.