diff --git a/e2e/protractor/suites/search/search-filters.test.ts b/e2e/protractor/suites/search/search-filters.test.ts index ba1028ba6c..74cb928219 100644 --- a/e2e/protractor/suites/search/search-filters.test.ts +++ b/e2e/protractor/suites/search/search-filters.test.ts @@ -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}`]; diff --git a/projects/aca-content/assets/app.extensions.json b/projects/aca-content/assets/app.extensions.json index 410cdb2e07..57f3a3e797 100644 --- a/projects/aca-content/assets/app.extensions.json +++ b/projects/aca-content/assets/app.extensions.json @@ -1532,7 +1532,27 @@ "visible": "app.areCategoriesEnabled" } } - ] + ], + "highlight": { + "prefix": "", + "postfix": "", + "fields": [ + { + "field": "cm:title" + }, + { + "field": "cm:name" + }, + { + "field": "cm:description", + "snippetCount": 1 + }, + { + "field": "cm:content", + "snippetCount": 1 + } + ] + } }, { "id": "app.search.dublin-core", @@ -1688,7 +1708,27 @@ } } } - ] + ], + "highlight": { + "prefix": "", + "postfix": "", + "fields": [ + { + "field": "cm:title" + }, + { + "field": "cm:name" + }, + { + "field": "cm:description", + "snippetCount": 1 + }, + { + "field": "cm:content", + "snippetCount": 1 + } + ] + } }, { "id": "app.search.effectivity", @@ -1846,7 +1886,27 @@ } } } - ] + ], + "highlight": { + "prefix": "", + "postfix": "", + "fields": [ + { + "field": "cm:title" + }, + { + "field": "cm:name" + }, + { + "field": "cm:description", + "snippetCount": 1 + }, + { + "field": "cm:content", + "snippetCount": 1 + } + ] + } } ], diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.html b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.html index 13d97c9b04..7358cd2ad5 100644 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.html +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.html @@ -1,12 +1,38 @@
- - {{ name$ | async }} - - - {{ name$ | async }} - - {{ title$ | async }} + + +
+
+
diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss index 4e4a351053..603480cb7c 100644 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss @@ -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); diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.spec.ts b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.spec.ts new file mode 100644 index 0000000000..fa0bbf7c13 --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.spec.ts @@ -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 . + */ + +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; + + 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 random content`] + }, + { + field: 'cm:name', + snippets: [`Random`] + }, + { + field: 'cm:title', + snippets: [`Random title`] + }, + { + field: 'cm:description', + snippets: [`some random 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('Random'); + 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(' ( Random 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 random 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 random content...'); + expect(contentElement.title).toBe('...Interesting random content...'); + done(); + }); + fixture.detectChanges(); + }); +}); diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.ts b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.ts index 2bb23bd140..941aaa1698 100644 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.ts +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.ts @@ -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'; @@ -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 = ""; + private readonly highlightPostfix = ''; + private node: NodeEntry; private onDestroy$ = new Subject(); @@ -54,6 +57,12 @@ export class SearchResultsRowComponent implements OnInit, OnDestroy { name$ = new BehaviorSubject(''); title$ = new BehaviorSubject(''); + description$ = new BehaviorSubject(''); + content$ = new BehaviorSubject(''); + nameStripped = ''; + titleStripped = ''; + descriptionStripped = ''; + contentStripped = ''; isFile = false; @@ -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); } } @@ -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'), '') + : ''; + } } diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.components.spec.ts b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.components.spec.ts deleted file mode 100644 index fc08ee3c51..0000000000 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.components.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * 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 . - */ - -import { NodeEntry } from '@alfresco/js-api'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AppTestingModule } from '../../../testing/app-testing.module'; -import { SearchResultsRowComponent } from './search-results-row.component'; - -describe('SearchResultsRowComponent', () => { - let component: SearchResultsRowComponent; - let fixture: ComponentFixture; - const nodeEntry: NodeEntry = { - entry: { - id: 'fake-node-entry', - modifiedByUser: { displayName: 'IChangeThings' }, - modifiedAt: new Date(), - isFile: true, - properties: { 'cm:title': 'BananaRama' } - } - } as NodeEntry; - - 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(); - }); -}); diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.html b/projects/aca-content/src/lib/components/search/search-results/search-results.component.html index 78d6505d5e..8990e8d73f 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.html +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.html @@ -64,14 +64,6 @@ - - - - {{context.row?.node?.entry?.properties && context.row?.node?.entry?.properties['cm:description']}} - - - - diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts b/projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts index ce3c18fbbb..eb2786ed40 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts @@ -178,6 +178,11 @@ describe('SearchComponent', () => { expect(query).toBe(`(cm:name:"hello*" OR cm:title:"hello*")`); }); + it('should not apply suffix to the TEXT field for correct highlighting', () => { + const query = component.formatSearchQuery('hello', ['cm:name', 'TEXT']); + expect(query).toBe(`(cm:name:"hello*" OR TEXT:"hello")`); + }); + it('should format user input as cm:name if configuration not provided', () => { const query = component.formatSearchQuery('hello'); expect(query).toBe(`(cm:name:"hello*")`); diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts b/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts index e8f516904d..31f43bfa11 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts @@ -206,7 +206,15 @@ export class SearchResultsComponent extends PageComponent implements OnInit { term = term.substring(1); } - return '(' + fields.map((field) => `${prefix}${field}:"${term}${suffix}"`).join(' OR ') + ')'; + return ( + '(' + + fields + .map((field) => { + return field !== 'TEXT' ? `${prefix}${field}:"${term}${suffix}"` : `${prefix}${field}:"${term}"`; + }) + .join(' OR ') + + ')' + ); } formatSearchQuery(userInput: string, fields = ['cm:name']) { diff --git a/projects/aca-content/src/lib/ui/variables/variables.scss b/projects/aca-content/src/lib/ui/variables/variables.scss index 5aca473ee9..aa2e77f543 100644 --- a/projects/aca-content/src/lib/ui/variables/variables.scss +++ b/projects/aca-content/src/lib/ui/variables/variables.scss @@ -41,6 +41,7 @@ $page-layout-header-background-color: $background-card-color; $search-chip-icon-color: #757575; $disabled-chip-background-color: #f5f5f5; $contrast-gray: mat.get-color-from-palette($foreground, 'secondary-tex'); +$search-highlight-background-color: #ffd180; // CSS Variables $defaults: ( @@ -86,7 +87,8 @@ $defaults: ( --theme-page-layout-header-background-color: $page-layout-header-background-color, --theme-search-chip-icon-color: $search-chip-icon-color, --theme-disabled-chip-background-color: $disabled-chip-background-color, - --theme-secondary-text: $secondary-text + --theme-secondary-text: $secondary-text, + --theme-search-highlight-background-color: $search-highlight-background-color ); // propagates SCSS variables into the CSS variables scope diff --git a/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts b/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts index 48ca515aa8..1110404b0b 100644 --- a/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts +++ b/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts @@ -80,7 +80,7 @@ export class DataTableComponent extends BaseComponent { * * @returns reference to cell element which contains link. */ - getCellLinkByName = (name: string): Locator => this.getChild('.adf-cell-value span', { hasText: name }); + getCellLinkByName = (name: string): Locator => this.getChild('.adf-cell-value span[role="link"]', { hasText: name }); /** * Method used in cases where we want to localize the element by [aria-label]