[ACA-19] Library Search Results (#783)

* [ACA-19] libraries results page

* [ACA-19] libraries search query builder service + trigger action on search option select

* [ACA-19] remove sorting

* [ACA-19] extension - set custom columns for search libraries results

* [ACA-19] add role column

* [ACA-19] adapt text

* [ACA-19] reformat with Prettier

* [ACA-19] fix unit tests

* [ACA-19] reformat with Prettier

* [ACA-19] some unit tests & code cleanup

* [ACA-19] fix navigation

* [ACA-19] remove duplicates

* [ACA-19] unit test
This commit is contained in:
Suzana Dirla
2018-11-08 14:22:09 +02:00
committed by Denys Vuika
parent cb3754e29d
commit 1e3136332e
14 changed files with 649 additions and 12 deletions

View File

@@ -26,8 +26,10 @@
</app-search-input-control>
<div id="search-options">
<mat-checkbox *ngFor="let option of searchOptions"
id="{{ option.id }}"
[(ngModel)]="option.value"
[disabled]="option.shouldDisable()"
(change)="onOptionChange()"
(click)="$event.stopPropagation()">
{{ option.key | translate }}
</mat-checkbox>

View File

@@ -37,6 +37,8 @@ import { AppTestingModule } from '../../../testing/app-testing.module';
import { Actions, ofType } from '@ngrx/effects';
import { SEARCH_BY_TERM, SearchByTermAction } from '../../../store/actions';
import { map } from 'rxjs/operators';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { SearchLibrariesQueryBuilderService } from '../search-libraries-results/search-libraries-query-builder.service';
describe('SearchInputComponent', () => {
let fixture: ComponentFixture<SearchInputComponent>;
@@ -47,7 +49,8 @@ describe('SearchInputComponent', () => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [SearchInputComponent],
schemas: [NO_ERRORS_SCHEMA]
schemas: [NO_ERRORS_SCHEMA],
providers: [SearchQueryBuilderService, SearchLibrariesQueryBuilderService]
})
.compileComponents()
.then(() => {

View File

@@ -38,6 +38,14 @@ import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states/app.state';
import { SearchByTermAction } from '../../../store/actions';
import { filter } from 'rxjs/operators';
import { SearchLibrariesQueryBuilderService } from '../search-libraries-results/search-libraries-query-builder.service';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
export enum SearchOptionIds {
Files = 'files',
Folders = 'folders',
Libraries = 'libraries'
}
@Component({
selector: 'aca-search-input',
@@ -51,18 +59,21 @@ export class SearchInputComponent implements OnInit {
navigationTimer: any;
searchedWord = null;
searchOptions: any = [
searchOptions: Array<any> = [
{
id: SearchOptionIds.Files,
key: 'SEARCH.INPUT.FILES',
value: false,
shouldDisable: this.isLibrariesChecked.bind(this)
},
{
id: SearchOptionIds.Folders,
key: 'SEARCH.INPUT.FOLDERS',
value: false,
shouldDisable: this.isLibrariesChecked.bind(this)
},
{
id: SearchOptionIds.Libraries,
key: 'SEARCH.INPUT.LIBRARIES',
value: false,
shouldDisable: this.isContentChecked.bind(this)
@@ -72,7 +83,12 @@ export class SearchInputComponent implements OnInit {
@ViewChild('searchInputControl')
searchInputControl: SearchInputControlComponent;
constructor(private router: Router, private store: Store<AppStore>) {}
constructor(
private librariesQueryBuilder: SearchLibrariesQueryBuilderService,
private queryBuilder: SearchQueryBuilderService,
private router: Router,
private store: Store<AppStore>
) {}
ngOnInit() {
this.showInputValue();
@@ -89,7 +105,7 @@ export class SearchInputComponent implements OnInit {
showInputValue() {
this.searchedWord = '';
if (this.onSearchResults) {
if (this.onSearchResults || this.onLibrariesSearchResults) {
const urlTree: UrlTree = this.router.parseUrl(this.router.url);
const urlSegmentGroup: UrlSegmentGroup =
urlTree.root.children[PRIMARY_OUTLET];
@@ -141,14 +157,57 @@ export class SearchInputComponent implements OnInit {
}, 1000);
}
onOptionChange() {
if (this.searchedWord) {
if (this.isLibrariesChecked()) {
if (this.onLibrariesSearchResults) {
this.librariesQueryBuilder.update();
} else {
this.store.dispatch(
new SearchByTermAction(this.searchedWord, this.searchOptions)
);
}
} else if (this.isContentChecked()) {
if (this.onSearchResults) {
// TODO: send here data to this.queryBuilder to be able to search for files/folders
this.queryBuilder.update();
} else {
this.store.dispatch(
new SearchByTermAction(this.searchedWord, this.searchOptions)
);
}
}
}
}
get onLibrariesSearchResults() {
return this.router.url.indexOf('/search-libraries') === 0;
}
get onSearchResults() {
return this.router.url.indexOf('/search') === 0;
return (
!this.onLibrariesSearchResults && this.router.url.indexOf('/search') === 0
);
}
isFilesChecked(): boolean {
return this.isOptionChecked(SearchOptionIds.Files);
}
isFoldersChecked(): boolean {
return this.isOptionChecked(SearchOptionIds.Folders);
}
isLibrariesChecked(): boolean {
return this.searchOptions[2].value;
return this.isOptionChecked(SearchOptionIds.Libraries);
}
isOptionChecked(optionId: string): boolean {
const libItem = this.searchOptions.find(item => item.id === optionId);
return !!libItem && libItem.value;
}
isContentChecked(): boolean {
return this.searchOptions[0].value || this.searchOptions[1].value;
return this.isFilesChecked() || this.isFoldersChecked();
}
}

View File

@@ -0,0 +1,96 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { SearchLibrariesQueryBuilderService } from './search-libraries-query-builder.service';
describe('SearchLibrariesQueryBuilderService', () => {
let apiService: AlfrescoApiService;
let builder: SearchLibrariesQueryBuilderService;
let queriesApi;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule]
});
apiService = TestBed.get(AlfrescoApiService);
apiService.reset();
queriesApi = apiService.getInstance().core.queriesApi;
builder = new SearchLibrariesQueryBuilderService(apiService);
});
it('should have empty user query by default', () => {
expect(builder.userQuery).toBe('');
});
it('should trim user query value', () => {
builder.userQuery = ' something ';
expect(builder.userQuery).toEqual('something');
});
it('should build query and raise an event on update', async () => {
const query = {};
spyOn(builder, 'buildQuery').and.returnValue(query);
let eventArgs = null;
builder.updated.subscribe(args => (eventArgs = args));
await builder.update();
expect(eventArgs).toBe(query);
});
it('should build query and raise an event on execute', async () => {
const data = {};
spyOn(queriesApi, 'findSites').and.returnValue(Promise.resolve(data));
const query = {};
spyOn(builder, 'buildQuery').and.returnValue(query);
let eventArgs = null;
builder.executed.subscribe(args => (eventArgs = args));
await builder.execute();
expect(eventArgs).toBe(data);
});
it('should require a query fragment to build query', () => {
const compiled = builder.buildQuery();
expect(compiled).toBeNull();
});
it('should build query when there is a useQuery value', () => {
const searchedTerm = 'test';
builder.userQuery = searchedTerm;
const compiled = builder.buildQuery();
expect(compiled.term).toBe(searchedTerm);
});
it('should use pagination settings', () => {
const searchedTerm = 'test';
builder.paging = { maxItems: 5, skipCount: 5 };
builder.userQuery = searchedTerm;
const compiled = builder.buildQuery();
expect(compiled.opts).toEqual({ maxItems: 5, skipCount: 5 });
});
});

View File

@@ -0,0 +1,86 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* 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
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { AlfrescoApiService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { SitePaging } from 'alfresco-js-api';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class SearchLibrariesQueryBuilderService {
private _userQuery = '';
updated: Subject<any> = new Subject();
executed: Subject<any> = new Subject();
paging: { maxItems?: number; skipCount?: number } = null;
get userQuery(): string {
return this._userQuery;
}
set userQuery(value: string) {
this._userQuery = value ? value.trim() : '';
}
constructor(private alfrescoApiService: AlfrescoApiService) {}
update(): void {
const query = this.buildQuery();
this.updated.next(query);
}
async execute() {
const query = this.buildQuery();
if (query) {
const data = await this.findLibraries(query);
this.executed.next(data);
}
}
buildQuery(): any {
const query = this.userQuery;
if (query) {
const resultQuery = {
term: query,
opts: {
skipCount: this.paging && this.paging.skipCount,
maxItems: this.paging && this.paging.maxItems
}
};
return resultQuery;
}
return null;
}
private findLibraries(libraryQuery: { term; opts }): Promise<SitePaging> {
return this.alfrescoApiService
.getInstance()
.core.queriesApi.findSites(libraryQuery.term, libraryQuery.opts)
.catch(() => ({ list: { pagination: { totalItems: 0 }, entries: [] } }));
}
}

View File

@@ -0,0 +1,99 @@
<app-page-layout>
<app-page-layout-header>
<adf-breadcrumb root="APP.BROWSE.SEARCH_LIBRARIES.TITLE">
</adf-breadcrumb>
<adf-toolbar class="inline">
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [actionRef]="entry"></aca-toolbar-action>
</ng-container>
</adf-toolbar>
</app-page-layout-header>
<app-page-layout-content>
<div class="main-content">
<div class="adf-search-results">
<div class="adf-search-results__content">
<mat-progress-bar
*ngIf="isLoading"
color="primary"
mode="indeterminate">
</mat-progress-bar>
<div class="adf-search-results__content-header content" *ngIf="data?.list.entries.length">
<div class="content__side--left">
<div class="adf-search-results--info-text">{{ 'APP.BROWSE.SEARCH_LIBRARIES.FOUND_RESULTS' | translate: { number: totalResults } }}</div>
</div>
</div>
<adf-document-list
#documentList
acaDocumentList
[showHeader]="true"
[selectionMode]="'single'"
[sorting]="[ 'name', 'asc' ]"
[node]="data"
(node-dblclick)="navigateTo($event.detail?.node)"
(name-click)="navigateTo($event.detail?.node)">
<data-columns>
<ng-container *ngFor="let column of columns; trackBy: trackById">
<ng-container *ngIf="column.template && !(column.desktopOnly && isSmallScreen)">
<data-column
[key]="column.key"
[title]="column.title"
[type]="column.type"
[format]="column.format"
[class]="column.class"
[sortable]="column.sortable">
<ng-template let-context>
<app-dynamic-column
[id]="column.template"
[context]="context">
</app-dynamic-column>
</ng-template>
</data-column>
</ng-container>
<ng-container *ngIf="!column.template && !(column.desktopOnly && isSmallScreen)">
<data-column
[key]="column.key"
[title]="column.title"
[type]="column.type"
[format]="column.format"
[class]="column.class"
[sortable]="column.sortable">
</data-column>
</ng-container>
</ng-container>
</data-columns>
<empty-folder-content>
<ng-template>
<ng-container *ngIf="data">
<div class="empty-search__block">
<p class="empty-search__text">
{{ 'APP.BROWSE.SEARCH.NO_RESULTS' | translate }}
</p>
</div>
</ng-container>
</ng-template>
</empty-folder-content>
</adf-document-list>
<adf-pagination *ngIf="!documentList.isEmpty()"
acaPagination
[target]="documentList"
(change)="onPaginationChanged($event)">
</adf-pagination>
</div>
</div>
</div>
<div class="sidebar" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
</div>
</app-page-layout-content>
</app-page-layout>

View File

@@ -0,0 +1,42 @@
@import 'mixins';
.adf-search-results {
@include flex-row;
&__content {
@include flex-column;
border-left: 1px solid #eee;
}
&__content-header {
display: flex;
padding: 0 25px 0 25px;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #eee;
}
&--info-text {
flex: 1;
font-size: 16px;
color: rgba(0, 0, 0, 0.54);
}
.text--bold {
font-weight: 600;
}
.content {
@include flex-row;
flex: unset;
height: unset;
padding-top: 8px;
padding-bottom: 8px;
flex-wrap: wrap;
&__side--left {
@include flex-column;
height: unset;
}
}
}

View File

@@ -0,0 +1,38 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { AppConfigPipe, DataTableComponent } from '@alfresco/adf-core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { SearchLibrariesResultsComponent } from './search-libraries-results.component';
import { SearchLibrariesQueryBuilderService } from './search-libraries-query-builder.service';
import { DocumentListComponent } from '@alfresco/adf-content-services';
describe('SearchLibrariesResultsComponent', () => {
let component: SearchLibrariesResultsComponent;
let fixture: ComponentFixture<SearchLibrariesResultsComponent>;
const emptyPage = { list: { pagination: { totalItems: 0 }, entries: [] } };
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [
DataTableComponent,
DocumentListComponent,
SearchLibrariesResultsComponent,
AppConfigPipe
],
schemas: [NO_ERRORS_SCHEMA],
providers: [SearchLibrariesQueryBuilderService]
});
fixture = TestBed.createComponent(SearchLibrariesResultsComponent);
component = fixture.componentInstance;
});
it('should show empty page by default', async () => {
spyOn(component, 'onSearchResultLoaded').and.callThrough();
fixture.detectChanges();
expect(component.onSearchResultLoaded).toHaveBeenCalledWith(emptyPage);
});
});

View File

@@ -0,0 +1,148 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* 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
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, OnInit } from '@angular/core';
import { NodePaging, Pagination, SiteEntry } from 'alfresco-js-api';
import { ActivatedRoute, Params } from '@angular/router';
import { PageComponent } from '../../page.component';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states/app.state';
import { NavigateLibraryAction } from '../../../store/actions';
import { AppExtensionService } from '../../../extensions/extension.service';
import { ContentManagementService } from '../../../services/content-management.service';
import { SearchLibrariesQueryBuilderService } from './search-libraries-query-builder.service';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({
selector: 'aca-search-results',
templateUrl: './search-libraries-results.component.html',
styleUrls: ['./search-libraries-results.component.scss'],
providers: [SearchLibrariesQueryBuilderService]
})
export class SearchLibrariesResultsComponent extends PageComponent
implements OnInit {
isSmallScreen = false;
searchedWord: string;
queryParamName = 'q';
data: NodePaging;
totalResults = 0;
isLoading = false;
columns: any[] = [];
constructor(
private breakpointObserver: BreakpointObserver,
private librariesQueryBuilder: SearchLibrariesQueryBuilderService,
private route: ActivatedRoute,
store: Store<AppStore>,
extensions: AppExtensionService,
content: ContentManagementService
) {
super(store, extensions, content);
librariesQueryBuilder.paging = {
skipCount: 0,
maxItems: 25
};
}
ngOnInit() {
super.ngOnInit();
this.columns = this.extensions.documentListPresets.searchLibraries || [];
this.subscriptions.push(
this.librariesQueryBuilder.updated.subscribe(() => {
this.isLoading = true;
this.librariesQueryBuilder.execute();
}),
this.librariesQueryBuilder.executed.subscribe(data => {
this.librariesQueryBuilder.paging.skipCount = 0;
this.onSearchResultLoaded(data);
this.isLoading = false;
}),
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.subscribe(result => {
this.isSmallScreen = result.matches;
})
);
if (this.route) {
this.route.params.forEach((params: Params) => {
this.searchedWord = params.hasOwnProperty(this.queryParamName)
? params[this.queryParamName]
: null;
const query = this.formatSearchQuery(this.searchedWord);
if (query && query.length > 1) {
this.librariesQueryBuilder.userQuery = query;
this.librariesQueryBuilder.update();
} else {
this.librariesQueryBuilder.userQuery = null;
this.librariesQueryBuilder.executed.next({
list: { pagination: { totalItems: 0 }, entries: [] }
});
}
});
}
}
private formatSearchQuery(userInput: string) {
if (!userInput) {
return null;
}
return userInput.trim();
}
onSearchResultLoaded(nodePaging: NodePaging) {
this.data = nodePaging;
this.totalResults = this.getNumberOfResults();
}
getNumberOfResults() {
if (this.data && this.data.list && this.data.list.pagination) {
return this.data.list.pagination.totalItems;
}
return 0;
}
onPaginationChanged(pagination: Pagination) {
this.librariesQueryBuilder.paging = {
maxItems: pagination.maxItems,
skipCount: pagination.skipCount
};
this.librariesQueryBuilder.update();
}
navigateTo(node: SiteEntry) {
if (node && node.entry && node.entry.guid) {
this.store.dispatch(new NavigateLibraryAction(node.entry.guid));
}
}
}

View File

@@ -29,6 +29,7 @@ import { CoreModule } from '@alfresco/adf-core';
import { ContentModule } from '@alfresco/adf-content-services';
import { SearchResultsComponent } from './search-results/search-results.component';
import { SearchResultsRowComponent } from './search-results-row/search-results-row.component';
import { SearchLibrariesResultsComponent } from './search-libraries-results/search-libraries-results.component';
import { AppInfoDrawerModule } from '../info-drawer/info.drawer.module';
import { AppToolbarModule } from '../toolbar/toolbar.module';
import { AppCommonModule } from '../common/common.module';
@@ -46,7 +47,15 @@ import { AppLayoutModule } from '../layout/layout.module';
DirectivesModule,
AppLayoutModule
],
declarations: [SearchResultsComponent, SearchResultsRowComponent],
exports: [SearchResultsComponent, SearchResultsRowComponent]
declarations: [
SearchResultsComponent,
SearchLibrariesResultsComponent,
SearchResultsRowComponent
],
exports: [
SearchResultsComponent,
SearchLibrariesResultsComponent,
SearchResultsRowComponent
]
})
export class AppSearchResultsModule {}