Skip to content

Commit

Permalink
[ACS-6427] Add search highlighting (#3637)
Browse files Browse the repository at this point in the history
* [ACS-6427] Initial commit for search highlights

* [ACS-6427] Add correct highlighting config, handle highlights in search results

* [ACS-6427] CR fixes

* [ACS-6427] CR fix

* [ACS-6427] Locator fix

* [ACS-6427] E2E fix

---------

Co-authored-by: swapnil.verma <swapnil.verma@globallogic.com>
  • Loading branch information
MichalKinas and swapnil-verma-gl committed Feb 15, 2024
1 parent 60a4aba commit 4766bfe
Show file tree
Hide file tree
Showing 12 changed files with 304 additions and 86 deletions.
2 changes: 1 addition & 1 deletion e2e/protractor/suites/search/search-filters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ describe('Search filters', () => {
await peopleFilter.closeDialog();

await searchInput.clickSearchButton();
await searchInput.searchFor(fileJpgUser1.name);
await searchInput.searchFor(`${fileJpgUser1.name}*`);
await dataTable.waitForBody();

const expectedUsers2 = [`${user1} ${user1}`];
Expand Down
66 changes: 63 additions & 3 deletions projects/aca-content/assets/app.extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1532,7 +1532,27 @@
"visible": "app.areCategoriesEnabled"
}
}
]
],
"highlight": {
"prefix": "<span class='aca-highlight'>",
"postfix": "</span>",
"fields": [
{
"field": "cm:title"
},
{
"field": "cm:name"
},
{
"field": "cm:description",
"snippetCount": 1
},
{
"field": "cm:content",
"snippetCount": 1
}
]
}
},
{
"id": "app.search.dublin-core",
Expand Down Expand Up @@ -1688,7 +1708,27 @@
}
}
}
]
],
"highlight": {
"prefix": "<span class='aca-highlight'>",
"postfix": "</span>",
"fields": [
{
"field": "cm:title"
},
{
"field": "cm:name"
},
{
"field": "cm:description",
"snippetCount": 1
},
{
"field": "cm:content",
"snippetCount": 1
}
]
}
},
{
"id": "app.search.effectivity",
Expand Down Expand Up @@ -1846,7 +1886,27 @@
}
}
}
]
],
"highlight": {
"prefix": "<span class='aca-highlight'>",
"postfix": "</span>",
"fields": [
{
"field": "cm:title"
},
{
"field": "cm:name"
},
{
"field": "cm:description",
"snippetCount": 1
},
{
"field": "cm:content",
"snippetCount": 1
}
]
}
}

],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
<div class="search-file-name">
<span tabindex="0" role="link" *ngIf="isFile" (click)="showPreview($event)" (keyup.enter)="showPreview($event)" class="aca-link">
{{ name$ | async }}
</span>
<span tabindex="0" role="link" *ngIf="!isFile" (click)="navigate($event)" (keyup.enter)="navigate($event)" class="bold aca-link">
{{ name$ | async }}
</span>
<span>{{ title$ | async }}</span>
<span
tabindex="0"
role="link"
*ngIf="isFile"
(click)="showPreview($event)"
(keyup.enter)="showPreview($event)"
class="aca-link aca-crop-text"
[title]="nameStripped"
[innerHTML]="name$ | async"
></span>
<span
tabindex="0"
role="link"
*ngIf="!isFile"
(click)="navigate($event)"
(keyup.enter)="navigate($event)"
class="bold aca-link aca-crop-text"
[title]="nameStripped"
[innerHTML]="name$ | async"
></span>
<span
data-automation-id="search-results-entry-title"
class="aca-crop-text"
[title]="titleStripped"
[innerHTML]="title$ | async"
></span>
</div>
<div
data-automation-id="search-results-entry-description"
class="aca-crop-text"
[title]="descriptionStripped"
[innerHTML]="description$ | async"
></div>
<div class="aca-result-location">
<aca-location-link [context]="context" [showLocation]="true"></aca-location-link>
</div>
<div class="aca-result-content aca-crop-text" [title]="contentStripped" [innerHTML]="content$ | async"></div>
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
.aca-search-results-row {
padding: 10px 0;
width: inherit;

.aca-highlight {
background: var(--theme-search-highlight-background-color);
}

.aca-crop-text {
overflow: hidden;
text-overflow: ellipsis;
}

.aca-result-location {
height: 15px;
padding-top: 3px;
}

.aca-result-content {
padding: 0 5px;
font-style: italic;
}

.aca-link {
text-decoration: none;
color: var(--theme-text-bold-color);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/

import { NodeEntry, ResultSetRowEntry } from '@alfresco/js-api';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { first } from 'rxjs/operators';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { SearchResultsRowComponent } from './search-results-row.component';

describe('SearchResultsRowComponent', () => {
let component: SearchResultsRowComponent;
let fixture: ComponentFixture<SearchResultsRowComponent>;

const nodeEntry: NodeEntry = {
entry: {
id: 'fake-node-entry',
modifiedByUser: { displayName: 'IChangeThings' },
modifiedAt: new Date(),
isFile: true,
properties: { 'cm:title': 'BananaRama' }
}
} as NodeEntry;

const resultEntry: ResultSetRowEntry = {
entry: {
id: 'fake-node-entry',
modifiedAt: new Date(),
isFile: true,
name: 'Random name',
properties: { 'cm:title': 'Random title', 'cm:description': 'some random description' },
search: {
score: 10,
highlight: [
{
field: 'cm:content',
snippets: [`Interesting <span class='aca-highlight'>random</span> content`]
},
{
field: 'cm:name',
snippets: [`<span class='aca-highlight'>Random</span>`]
},
{
field: 'cm:title',
snippets: [`<span class='aca-highlight'>Random</span> title`]
},
{
field: 'cm:description',
snippets: [`some <span class='aca-highlight'>random</span> description`]
}
]
}
}
} as ResultSetRowEntry;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, SearchResultsRowComponent]
});

fixture = TestBed.createComponent(SearchResultsRowComponent);
component = fixture.componentInstance;
});

it('should show the current node', () => {
component.context = { row: { node: nodeEntry } };
fixture.detectChanges();

const element = fixture.nativeElement.querySelector('div');
expect(element).not.toBeNull();
});

it('should correctly parse highlights', (done) => {
component.context = { row: { node: resultEntry } };
component.content$
.asObservable()
.pipe(first())
.subscribe(() => {
fixture.detectChanges();

const nameElement: HTMLSpanElement = fixture.debugElement.query(By.css('.aca-link.aca-crop-text')).nativeElement;
expect(nameElement.innerHTML).toBe('<span class="aca-highlight">Random</span>');
expect(nameElement.title).toBe('Random');

const titleElement: HTMLSpanElement = fixture.debugElement.query(By.css('[data-automation-id="search-results-entry-title"]')).nativeElement;
expect(titleElement.innerHTML).toBe(' ( <span class="aca-highlight">Random</span> title )');
expect(titleElement.title).toBe('Random title');

const descriptionElement: HTMLDivElement = fixture.debugElement.query(
By.css('[data-automation-id="search-results-entry-description"]')
).nativeElement;
expect(descriptionElement.innerHTML).toBe('some <span class="aca-highlight">random</span> description');
expect(descriptionElement.title).toBe('some random description');

const contentElement: HTMLDivElement = fixture.debugElement.query(By.css('.aca-result-content.aca-crop-text')).nativeElement;
expect(contentElement.innerHTML).toBe('...Interesting <span class="aca-highlight">random</span> content...');
expect(contentElement.title).toBe('...Interesting random content...');
done();
});
fixture.detectChanges();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/

import { Component, Input, OnInit, ViewEncapsulation, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
import { NodeEntry } from '@alfresco/js-api';
import { NodeEntry, SearchEntryHighlight } from '@alfresco/js-api';
import { ViewNodeAction, NavigateToFolder } from '@alfresco/aca-shared/store';
import { Store } from '@ngrx/store';
import { BehaviorSubject, Subject } from 'rxjs';
Expand All @@ -46,6 +46,9 @@ import { MatDialogModule } from '@angular/material/dialog';
host: { class: 'aca-search-results-row' }
})
export class SearchResultsRowComponent implements OnInit, OnDestroy {
private readonly highlightPrefix = "<span class='aca-highlight'>";
private readonly highlightPostfix = '</span>';

private node: NodeEntry;
private onDestroy$ = new Subject<boolean>();

Expand All @@ -54,6 +57,12 @@ export class SearchResultsRowComponent implements OnInit, OnDestroy {

name$ = new BehaviorSubject<string>('');
title$ = new BehaviorSubject<string>('');
description$ = new BehaviorSubject<string>('');
content$ = new BehaviorSubject<string>('');
nameStripped = '';
titleStripped = '';
descriptionStripped = '';
contentStripped = '';

isFile = false;

Expand Down Expand Up @@ -86,13 +95,42 @@ export class SearchResultsRowComponent implements OnInit, OnDestroy {
this.node = this.context.row.node;
this.isFile = this.node.entry.isFile;

const { name, properties } = this.node.entry;
const title = properties ? properties['cm:title'] : '';

const highlights: SearchEntryHighlight[] = this.node.entry['search']?.['highlight'];
let name = this.node.entry.name;
const properties = this.node.entry.properties;
let title = properties?.['cm:title'] || '';
let description = properties?.['cm:description'] || '';
let content = '';

highlights?.forEach((highlight) => {
switch (highlight.field) {
case 'cm:name':
name = highlight.snippets[0];
break;
case 'cm:title':
title = highlight.snippets[0];
break;
case 'cm:description':
description = highlight.snippets[0];
break;
case 'cm:content':
content = `...${highlight.snippets[0]}...`;
break;
default:
break;
}
});
this.name$.next(name);
this.description$.next(description);
this.content$.next(content);

this.nameStripped = this.stripHighlighting(name);
this.descriptionStripped = this.stripHighlighting(description);
this.contentStripped = this.stripHighlighting(content);

if (title !== name) {
this.title$.next(title ? `( ${title} )` : '');
this.title$.next(title ? ` ( ${title} )` : '');
this.titleStripped = this.stripHighlighting(title);
}
}

Expand All @@ -114,4 +152,10 @@ export class SearchResultsRowComponent implements OnInit, OnDestroy {
event.stopPropagation();
this.store.dispatch(new NavigateToFolder(this.node));
}

private stripHighlighting(highlightedContent: string): string {
return highlightedContent
? highlightedContent.replace(new RegExp(this.highlightPrefix, 'g'), '').replace(new RegExp(this.highlightPostfix, 'g'), '')
: '';
}
}
Loading

0 comments on commit 4766bfe

Please sign in to comment.