Skip to content

Commit

Permalink
feat(SFINT-4378) added a tooltip to show origin1 on clickedDocList an…
Browse files Browse the repository at this point in the history
…d queriesList (#103)

- Added a tooltip to documents and queries
- Added tests for the tooltips
  • Loading branch information
erocheleau committed Feb 2, 2022
1 parent d6d72d0 commit 5aee218
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 37 deletions.
50 changes: 27 additions & 23 deletions src/components/UserActions/ClickedDocumentList.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
import {
Component,
IComponentBindings,
Initialization,
ComponentOptions,
IQueryResult,
Template,
HtmlTemplate,
QueryUtils,
l,
get,
} from 'coveo-search-ui';
import { Component, IComponentBindings, Initialization, ComponentOptions, Template, HtmlTemplate, QueryUtils, l, get } from 'coveo-search-ui';
import { UserProfileModel, UserAction } from '../../models/UserProfileModel';
import { ExpandableList } from './ExpandableList';
import { UserActionType } from '../../rest/UserProfilingEndpoint';
Expand All @@ -19,7 +8,7 @@ import './Strings';
/**
* Initialization options of the **ClickedDocumentList** class.
*/
export interface IClickedDocumentList {
export interface IClickedDocumentListOptions {
/**
* Number of Clicked Documents shown.
*
Expand Down Expand Up @@ -61,7 +50,7 @@ export class ClickedDocumentList extends Component {
/**
* Default initialization options of the **ClickedDocumentList** class.
*/
static readonly options: IClickedDocumentList = {
static readonly options: IClickedDocumentListOptions = {
numberOfItems: ComponentOptions.buildNumberOption({
defaultValue: 3,
min: 1,
Expand All @@ -84,7 +73,7 @@ export class ClickedDocumentList extends Component {
};

private userProfileModel: UserProfileModel;
private sortedDocumentsList: IQueryResult[];
private sortedDocumentsList: UserAction[];

/**
* Create an instance of **ClickedDocumentList**. Initialize is needed the **UserProfileModel** and fetch user actions related to the **UserId**.
Expand All @@ -93,7 +82,7 @@ export class ClickedDocumentList extends Component {
* @param options Initialization options of the component.
* @param bindings Bindings of the Search-UI environment.
*/
constructor(public element: HTMLElement, public options: IClickedDocumentList, public bindings: IComponentBindings) {
constructor(public element: HTMLElement, public options: IClickedDocumentListOptions, public bindings: IComponentBindings) {
super(element, ClickedDocumentList.ID, bindings);

this.options = ComponentOptions.initComponentOptions(element, ClickedDocumentList, options);
Expand All @@ -113,7 +102,7 @@ export class ClickedDocumentList extends Component {
.reduce(this.filterDuplicatesClickAction, [])
.map((action) => {
action.document.searchInterface = this.searchInterface;
return action.document;
return action;
});
this.render();
}, this.logger.error.bind(this.logger));
Expand All @@ -124,19 +113,22 @@ export class ClickedDocumentList extends Component {
}

private render() {
new ExpandableList<IQueryResult>(this.element, this.sortedDocumentsList, {
new ExpandableList<UserAction>(this.element, this.sortedDocumentsList, {
maximumItemsShown: this.sortedDocumentsList.length,
minimumItemsShown: this.options.numberOfItems,
transform: (result) => {
QueryUtils.setStateObjectOnQueryResult(this.queryStateModel.get(), result);
QueryUtils.setSearchInterfaceObjectOnQueryResult(this.searchInterface, result);
return (<Promise<HTMLElement>>this.options.template.instantiateToElement(result, {
transform: (action) => {
QueryUtils.setStateObjectOnQueryResult(this.queryStateModel.get(), action.document);
QueryUtils.setSearchInterfaceObjectOnQueryResult(this.searchInterface, action.document);
return (<Promise<HTMLElement>>this.options.template.instantiateToElement(action.document, {
wrapInDiv: true,
checkCondition: true,
currentLayout: 'list',
responsiveComponents: this.searchInterface.responsiveComponents,
})).then((element) => {
Initialization.automaticallyCreateComponentsInsideResult(element, result);
Initialization.automaticallyCreateComponentsInsideResult(element, action.document);
if (action.raw.origin_level_1) {
this.addTooltipElement(element, action);
}
return element;
});
},
Expand All @@ -146,6 +138,18 @@ export class ClickedDocumentList extends Component {
showLessMessage: l(`${ClickedDocumentList.ID}_less`),
});
}

private addTooltipElement(element: HTMLElement, action: UserAction) {
const insertBeforeElement = element.querySelector('.CoveoResultLink');
if (insertBeforeElement) {
const tooltip = document.createElement('div');
tooltip.classList.add('coveo-tooltip-origin1');
tooltip.innerText = action.raw.origin_level_1;

const parentNode = insertBeforeElement.parentNode;
parentNode.insertBefore(tooltip, insertBeforeElement);
}
}
}

Initialization.registerAutoCreateComponent(ClickedDocumentList);
33 changes: 25 additions & 8 deletions src/components/UserActions/QueryList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, IComponentBindings, Initialization, ComponentOptions, get, Omnibox, l } from 'coveo-search-ui';
import { UserProfileModel } from '../../models/UserProfileModel';
import { UserAction, UserProfileModel } from '../../models/UserProfileModel';
import { ExpandableList } from './ExpandableList';
import { search } from '../../utils/icons';
import './Strings';
Expand Down Expand Up @@ -90,7 +90,7 @@ export class QueryList extends Component {
};

private userProfileModel: UserProfileModel;
private sortedQueryList: string[];
private sortedQueryList: UserAction[];

/**
* Create an instance of **QueryList**. Initialize is needed the **UserProfileModel** and fetch user actions related to the **UserId**.
Expand All @@ -115,21 +115,26 @@ export class QueryList extends Component {
.filter((action) => action.query)
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
.reverse()
.map((action) => action.query)
.reduce(this.removeDuplicateQueries, []);
.reduce(this.filterDuplicateQueries, []);
this.render();
}, this.logger.error.bind(this.logger));
}

private removeDuplicateQueries(acc: string[], query: string): string[] {
return acc.indexOf(query) === -1 ? [...acc, query] : acc;
private filterDuplicateQueries(accumulator: UserAction[], action: UserAction): UserAction[] {
return !accumulator.find((existing) => existing.query === action.query) ? [...accumulator, action] : accumulator;
}

private render() {
new ExpandableList<string>(this.element, this.sortedQueryList, {
new ExpandableList<UserAction>(this.element, this.sortedQueryList, {
maximumItemsShown: this.sortedQueryList.length,
minimumItemsShown: this.options.numberOfItems,
transform: (query: string) => this.options.transform(query).then(this.makeClickable.bind(this, query)),
transform: (action: UserAction) =>
this.options.transform(action.query).then((element) => {
if (action.raw.origin_level_1) {
this.addTooltipElement(element, action);
}
return this.makeClickable(action.query, element);
}),
listLabel: this.options.listLabel,
messageWhenEmpty: l(`${QueryList.ID}_no_queries`),
showMoreMessage: l(`${QueryList.ID}_more`),
Expand All @@ -156,6 +161,18 @@ export class QueryList extends Component {
}
return listItem;
}

private addTooltipElement(element: HTMLElement, action: UserAction) {
const insertBeforeElement = element.querySelector('.coveo-link');
if (insertBeforeElement) {
const tooltip = document.createElement('div');
tooltip.classList.add('coveo-tooltip-origin1');
tooltip.innerText = action.raw.origin_level_1;

const parentNode = insertBeforeElement.parentNode;
parentNode.insertBefore(tooltip, insertBeforeElement);
}
}
}

Initialization.registerAutoCreateComponent(QueryList);
25 changes: 25 additions & 0 deletions src/components/UserActions/UserActions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ $clickable-blue: #004990;
li {
.coveo-list-row {
display: flex;
position: relative;

a {
width: calc(100% - #{$icon-container-size});
Expand All @@ -116,6 +117,30 @@ $clickable-blue: #004990;
}
}
}

.coveo-tooltip-origin1 {
display: none;
position: absolute;
top: -2.75em;
padding: 8px 12px;
background-color: $clickable-blue;
border-radius: 4px;
color: white;
margin-left: 1.5em;

&:after {
position: absolute;
top: 100%;
content: '';
left: 1em;
border: 10px solid $clickable-blue;
border-color: $clickable-blue transparent transparent transparent;
}
}

&:hover .coveo-tooltip-origin1 {
display: flex;
}
}
}
}
Expand Down
72 changes: 66 additions & 6 deletions tests/components/UserActions/ClickedDocumentList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,66 @@ describe('ClickedDocumentList', () => {
}
});

it('should display a tooltip on hover with the origin_level_1', async () => {
sandbox.stub(Initialization, 'automaticallyCreateComponentsInsideResult');

const expectedOriginLevel1 = 'tooltip-content';
const CLICK_EVENTS = [BUILD_ACTION('foo', 1)];
CLICK_EVENTS[0].raw.origin_level_1 = expectedOriginLevel1;

const mock = Mock.advancedComponentSetup<ClickedDocumentList>(
ClickedDocumentList,
new Mock.AdvancedComponentSetupOptions(null, { userId: 'testuserId' }, (env) => {
fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => Promise.resolve(CLICK_EVENTS));
return env;
})
);
await waitForPromiseCompletion();

const tooltipElement = mock.env.element.querySelector<HTMLElement>('.coveo-tooltip-origin1');
expect(tooltipElement).not.toBeNull();
expect(tooltipElement.innerText).toBe(expectedOriginLevel1);

// trigger the hover
const listElement = mock.env.element.querySelector<HTMLElement>('.coveo-list-row');
const hoverEvent = new MouseEvent('mouseenter', {
view: window,
bubbles: true,
cancelable: true,
});
listElement.dispatchEvent(hoverEvent);

const pseudo = getComputedStyle(tooltipElement, ':after');
expect(pseudo).not.toBeNull();
});

it('should not display a tooltip if the origin_level_1 is missing', async () => {
sandbox.stub(Initialization, 'automaticallyCreateComponentsInsideResult');

const CLICK_EVENTS = [BUILD_ACTION('foo', 1)];
CLICK_EVENTS[0].raw.origin_level_1 = undefined;

const mock = Mock.advancedComponentSetup<ClickedDocumentList>(
ClickedDocumentList,
new Mock.AdvancedComponentSetupOptions(null, { userId: 'testuserId' }, (env) => {
fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => Promise.resolve(CLICK_EVENTS));
return env;
})
);
await waitForPromiseCompletion();

const listElement = mock.env.element.querySelector<HTMLElement>('.coveo-list-row');
const hoverEvent = new MouseEvent('mouseenter', {
view: window,
bubbles: true,
cancelable: true,
});
listElement.dispatchEvent(hoverEvent);

const tooltipElement = mock.env.element.querySelector<HTMLElement>('.coveo-tooltip-origin1');
expect(tooltipElement).toBeNull();
});

it('should show all documents when expanded', async () => {
sandbox.stub(Initialization, 'automaticallyCreateComponentsInsideResult');

Expand All @@ -152,16 +212,16 @@ describe('ClickedDocumentList', () => {
expect(list.childElementCount).toBe(TEST_CLICKS.length);
});

it('should not show the same query twice', async () => {
it('should not show the same document twice', async () => {
// Setup.
const createComponentInsideStub = sandbox.stub(Initialization, 'automaticallyCreateComponentsInsideResult');

const CLICK_EVENTS = [
BUILD_ACTION('someQuery', 4),
BUILD_ACTION('someQuery2', 3),
BUILD_ACTION('someQuery2', 2),
BUILD_ACTION('someQuery2', 1),
BUILD_ACTION('someQuery', 0),
BUILD_ACTION('someDocument', 4),
BUILD_ACTION('someDocument2', 3),
BUILD_ACTION('someDocument2', 2),
BUILD_ACTION('someDocument2', 1),
BUILD_ACTION('someDocument', 0),
];

const SORTED_AND_TRIMMED_CLICK_EVENTS = [CLICK_EVENTS[0], CLICK_EVENTS[1]];
Expand Down
54 changes: 54 additions & 0 deletions tests/components/UserActions/QueryList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,60 @@ describe('QueryList', () => {
}
});

it('should display a tooltip on hover with the origin_level_1', async () => {
const expectedOriginLevel1 = 'tooltip-content';
const SEARCH_EVENTS = [
new UserAction(
UserActionType.Search,
new Date(0),
{ query_expression: 'someQuery', origin_level_1: expectedOriginLevel1 },
null,
'someQuery'
),
];

const mock = Mock.advancedComponentSetup<QueryList>(
QueryList,
new Mock.AdvancedComponentSetupOptions(null, { userId: 'testuserId' }, (env) => {
fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => Promise.resolve(SEARCH_EVENTS));
return env;
})
);
await waitForPromiseCompletion();

const tooltipElement = mock.env.element.querySelector<HTMLElement>('.coveo-tooltip-origin1');
expect(tooltipElement).not.toBeNull();
expect(tooltipElement.innerText).toBe(expectedOriginLevel1);

// trigger the hover
const listElement = mock.env.element.querySelector<HTMLElement>('.coveo-list-row');
const hoverEvent = new MouseEvent('mouseenter', {
view: window,
bubbles: true,
cancelable: true,
});
listElement.dispatchEvent(hoverEvent);

const pseudo = getComputedStyle(tooltipElement, ':after');
expect(pseudo).not.toBeNull();
});

it('should not display a tooltip if the origin_level_1 is missing', async () => {
const SEARCH_EVENTS = [new UserAction(UserActionType.Search, new Date(0), { query_expression: 'someQuery' }, null, 'someQuery')];

const mock = Mock.advancedComponentSetup<QueryList>(
QueryList,
new Mock.AdvancedComponentSetupOptions(null, { userId: 'testuserId' }, (env) => {
fakeUserProfileModel(env.root, sandbox).getActions.callsFake(() => Promise.resolve(SEARCH_EVENTS));
return env;
})
);
await waitForPromiseCompletion();

const tooltipElement = mock.env.element.querySelector<HTMLElement>('.coveo-tooltip-origin1');
expect(tooltipElement).toBeNull();
});

it('should show all queries when expanded', async () => {
const mock = Mock.advancedComponentSetup<QueryList>(
QueryList,
Expand Down

0 comments on commit 5aee218

Please sign in to comment.