mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-10-08 14:51:14 +00:00
[AAE-11496] Move 'content-plugin' to projects folder as 'aca-content' (#2817)
* [AAE-11496] Move content-plugin to projects * Fix unit test
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<button mat-icon-button [matMenuTriggerFor]="dataSorting" id="aca-button-action-menu" aria-label="Search action menu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #dataSorting="matMenu">
|
||||
<button mat-menu-item [matMenuTriggerFor]="sorting" id="aca-button-sorting-menu">{{ 'SEARCH.SORT.SORTING_OPTION' | translate }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #sorting="matMenu">
|
||||
<ng-template matMenuContent>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let option of options"
|
||||
[id]="option.key + '-sorting-option'"
|
||||
[matMenuTriggerFor]="direction"
|
||||
[matMenuTriggerData]="{ option: option }"
|
||||
>
|
||||
{{ option.label | translate }}
|
||||
</button>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #direction="matMenu">
|
||||
<ng-template matMenuContent let-option="option">
|
||||
<button mat-menu-item [id]="option.key + '-sorting-option-asc'" (click)="onAscSortingClicked(option)">
|
||||
{{ 'SEARCH.SORT.ASCENDING' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item [id]="option.key + '-sorting-option-desc'" (click)="onDescSortingClicked(option)">
|
||||
{{ 'SEARCH.SORT.DESCENDING' | translate }}
|
||||
</button>
|
||||
</ng-template>
|
||||
</mat-menu>
|
@@ -0,0 +1,137 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { SearchSortingDefinition } from '@alfresco/adf-content-services/lib/search/models/search-sorting-definition.interface';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { SearchActionMenuComponent } from './search-action-menu.component';
|
||||
|
||||
const mockSortingData: SearchSortingDefinition[] = [
|
||||
{
|
||||
ascending: false,
|
||||
field: 'fieldA',
|
||||
key: 'keyA',
|
||||
label: 'LabelA',
|
||||
type: 'A'
|
||||
},
|
||||
{
|
||||
ascending: true,
|
||||
field: 'fieldB',
|
||||
key: 'keyB',
|
||||
label: 'Zorro',
|
||||
type: 'Z'
|
||||
}
|
||||
];
|
||||
|
||||
describe('SearchActionMenuComponent', () => {
|
||||
let fixture: ComponentFixture<SearchActionMenuComponent>;
|
||||
let component: SearchActionMenuComponent;
|
||||
let queryService: SearchQueryBuilderService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule],
|
||||
declarations: [SearchActionMenuComponent],
|
||||
providers: [SearchQueryBuilderService]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(SearchActionMenuComponent);
|
||||
queryService = TestBed.inject(SearchQueryBuilderService);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should emit sortingSelected event when asc sorting option is selected', async () => {
|
||||
spyOn(queryService, 'getSortingOptions').and.returnValue(mockSortingData);
|
||||
const expectedOption: SearchSortingDefinition = {
|
||||
ascending: true,
|
||||
field: 'fieldA',
|
||||
key: 'keyA',
|
||||
label: 'LabelA',
|
||||
type: 'A'
|
||||
};
|
||||
spyOn(component.sortingSelected, 'emit').and.callThrough();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const actionMenuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#aca-button-action-menu');
|
||||
actionMenuButton.dispatchEvent(new Event('click'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const sortingMenuButton: HTMLButtonElement | null = document.querySelector('#aca-button-sorting-menu');
|
||||
sortingMenuButton?.dispatchEvent(new Event('click'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const fieldAMenuButton: HTMLButtonElement | null = document.querySelector('#keyA-sorting-option');
|
||||
fieldAMenuButton?.dispatchEvent(new Event('click'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const directionButton: HTMLButtonElement | null = document.querySelector('#keyA-sorting-option-asc');
|
||||
directionButton?.dispatchEvent(new Event('click'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.sortingSelected.emit).toHaveBeenCalledWith(expectedOption);
|
||||
});
|
||||
|
||||
it('should emit sortingSelected event when desc sorting option is selected', async () => {
|
||||
spyOn(queryService, 'getSortingOptions').and.returnValue(mockSortingData);
|
||||
const expectedOption: SearchSortingDefinition = {
|
||||
ascending: false,
|
||||
field: 'fieldB',
|
||||
key: 'keyB',
|
||||
label: 'Zorro',
|
||||
type: 'Z'
|
||||
};
|
||||
spyOn(component.sortingSelected, 'emit').and.callThrough();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const actionMenuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#aca-button-action-menu');
|
||||
actionMenuButton.dispatchEvent(new Event('click'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const sortingMenuButton: HTMLButtonElement | null = document.querySelector('#aca-button-sorting-menu');
|
||||
sortingMenuButton?.dispatchEvent(new Event('click'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const fieldAMenuButton: HTMLButtonElement | null = document.querySelector('#keyB-sorting-option');
|
||||
fieldAMenuButton?.dispatchEvent(new Event('click'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const directionButton: HTMLButtonElement | null = document.querySelector('#keyB-sorting-option-desc');
|
||||
directionButton?.dispatchEvent(new Event('click'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.sortingSelected.emit).toHaveBeenCalledWith(expectedOption);
|
||||
});
|
||||
});
|
@@ -0,0 +1,56 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { SearchSortingDefinition } from '@alfresco/adf-content-services/lib/search/models/search-sorting-definition.interface';
|
||||
import { Component, OnInit, Output, ViewEncapsulation, EventEmitter } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-search-action-menu',
|
||||
templateUrl: './search-action-menu.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchActionMenuComponent implements OnInit {
|
||||
@Output()
|
||||
sortingSelected: EventEmitter<SearchSortingDefinition> = new EventEmitter();
|
||||
|
||||
options: SearchSortingDefinition[] = [];
|
||||
|
||||
constructor(private queryBuilder: SearchQueryBuilderService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.options = this.queryBuilder.getSortingOptions();
|
||||
}
|
||||
|
||||
onAscSortingClicked(option: SearchSortingDefinition) {
|
||||
option.ascending = true;
|
||||
this.sortingSelected.emit(option);
|
||||
}
|
||||
|
||||
onDescSortingClicked(option: SearchSortingDefinition) {
|
||||
option.ascending = false;
|
||||
this.sortingSelected.emit(option);
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<div class="app-search-container">
|
||||
<button
|
||||
mat-icon-button
|
||||
id="app-search-button"
|
||||
class="app-search-button"
|
||||
(click)="searchSubmit(searchTerm)"
|
||||
[title]="'SEARCH.BUTTON.TOOLTIP' | translate"
|
||||
>
|
||||
<mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon>
|
||||
</button>
|
||||
<mat-form-field class="app-input-form-field" [floatLabel]="'never'">
|
||||
<input
|
||||
matInput
|
||||
#searchInput
|
||||
[attr.aria-label]="'SEARCH.INPUT.ARIA-LABEL' | translate"
|
||||
[type]="inputType"
|
||||
id="app-control-input"
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="inputChange($event)"
|
||||
(keyup.enter)="searchSubmit($event)"
|
||||
[placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate"
|
||||
/>
|
||||
<div matSuffix class="app-suffix-search-icon-wrapper">
|
||||
<mat-icon *ngIf="searchTerm.length" (click)="clear()" class="app-clear-icon">clear</mat-icon>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
@@ -0,0 +1,61 @@
|
||||
$top-margin: 12px;
|
||||
|
||||
.app-search-container {
|
||||
margin-top: -$top-margin;
|
||||
padding-top: 2px;
|
||||
font-size: 16px;
|
||||
padding-left: 15px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.mat-form-field {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mat-form-field-underline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mat-form-field-label-wrapper {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
// fixes pointer event on FF
|
||||
&.searchMenuTrigger .mat-form-field-label-wrapper {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.app-input-form-field {
|
||||
letter-spacing: -0.7px;
|
||||
width: calc(100% - 56px);
|
||||
|
||||
.mat-input-element {
|
||||
letter-spacing: -0.7px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-search-button.mat-icon-button {
|
||||
top: -2px;
|
||||
left: -8px;
|
||||
|
||||
.mat-icon {
|
||||
font-size: 24px;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-suffix-search-icon-wrapper {
|
||||
height: 6px;
|
||||
margin: 14px 1px;
|
||||
float: left;
|
||||
|
||||
.mat-icon {
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-clear-icon {
|
||||
font-size: 18px;
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { SearchInputControlComponent } from './search-input-control.component';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('SearchInputControlComponent', () => {
|
||||
let fixture: ComponentFixture<SearchInputControlComponent>;
|
||||
let component: SearchInputControlComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule],
|
||||
declarations: [SearchInputControlComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(SearchInputControlComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit submit event on searchSubmit', async () => {
|
||||
const keyboardEvent = { target: { value: 'a' } };
|
||||
|
||||
let eventArgs = null;
|
||||
component.submit.subscribe((args) => (eventArgs = args));
|
||||
|
||||
await component.searchSubmit(keyboardEvent);
|
||||
expect(eventArgs).toBe(keyboardEvent);
|
||||
});
|
||||
|
||||
it('should emit searchChange event on inputChange', async () => {
|
||||
const searchTerm = 'b';
|
||||
|
||||
let eventArgs = null;
|
||||
component.searchChange.subscribe((args) => (eventArgs = args));
|
||||
|
||||
await component.inputChange(searchTerm);
|
||||
expect(eventArgs).toBe(searchTerm);
|
||||
});
|
||||
|
||||
it('should emit searchChange event on clear', async () => {
|
||||
let eventArgs = null;
|
||||
component.searchChange.subscribe((args) => (eventArgs = args));
|
||||
|
||||
await component.clear();
|
||||
expect(eventArgs).toBe('');
|
||||
});
|
||||
|
||||
it('should clear searchTerm', async () => {
|
||||
component.searchTerm = 'c';
|
||||
fixture.detectChanges();
|
||||
|
||||
await component.clear();
|
||||
expect(component.searchTerm).toBe('');
|
||||
});
|
||||
|
||||
it('should check if searchTerm has a length less than 2', () => {
|
||||
expect(component.isTermTooShort()).toBe(false);
|
||||
|
||||
component.searchTerm = 'd';
|
||||
fixture.detectChanges();
|
||||
expect(component.isTermTooShort()).toBe(true);
|
||||
|
||||
component.searchTerm = 'dd';
|
||||
fixture.detectChanges();
|
||||
expect(component.isTermTooShort()).toBe(false);
|
||||
});
|
||||
});
|
@@ -0,0 +1,84 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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, EventEmitter, Input, OnDestroy, Output, ViewEncapsulation, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-input-control',
|
||||
templateUrl: './search-input-control.component.html',
|
||||
styleUrls: ['./search-input-control.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: { class: 'app-search-control' }
|
||||
})
|
||||
export class SearchInputControlComponent implements OnDestroy {
|
||||
onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
/** Type of the input field to render, e.g. "search" or "text" (default). */
|
||||
@Input()
|
||||
inputType = 'text';
|
||||
|
||||
/** Emitted when the search is submitted pressing ENTER button.
|
||||
* The search term is provided as value of the event.
|
||||
*/
|
||||
@Output()
|
||||
// eslint-disable-next-line @angular-eslint/no-output-native
|
||||
submit: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/** Emitted when the search term is changed. The search term is provided
|
||||
* in the 'value' property of the returned object. If the term is less
|
||||
* than three characters in length then the term is truncated to an empty
|
||||
* string.
|
||||
*/
|
||||
@Output()
|
||||
searchChange: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
@ViewChild('searchInput', { static: true })
|
||||
searchInput: ElementRef;
|
||||
|
||||
searchTerm = '';
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
searchSubmit(event: any) {
|
||||
this.submit.emit(event);
|
||||
}
|
||||
|
||||
inputChange(event: any) {
|
||||
this.searchChange.emit(event);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.searchTerm = '';
|
||||
this.searchChange.emit('');
|
||||
}
|
||||
|
||||
isTermTooShort() {
|
||||
return !!(this.searchTerm && this.searchTerm.length < 2);
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CoreModule } from '@alfresco/adf-core';
|
||||
import { SearchInputComponent } from './search-input/search-input.component';
|
||||
import { SearchInputControlComponent } from './search-input-control/search-input-control.component';
|
||||
import { ContentModule } from '@alfresco/adf-content-services';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, CoreModule.forChild(), ContentModule.forChild(), A11yModule],
|
||||
declarations: [SearchInputComponent, SearchInputControlComponent],
|
||||
exports: [SearchInputComponent, SearchInputControlComponent]
|
||||
})
|
||||
export class AppSearchInputModule {}
|
@@ -0,0 +1,52 @@
|
||||
<div
|
||||
class="app-search-container searchMenuTrigger"
|
||||
[matMenuTriggerFor]="searchOptionsMenu"
|
||||
(menuOpened)="onMenuOpened()"
|
||||
(menuClosed)="syncInputValues()"
|
||||
>
|
||||
<button mat-icon-button class="app-search-button" (click)="searchByOption()" [title]="'SEARCH.BUTTON.TOOLTIP' | translate">
|
||||
<mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon>
|
||||
</button>
|
||||
<mat-form-field class="app-input-form-field" [floatLabel]="'never'">
|
||||
<input
|
||||
matInput
|
||||
[attr.aria-label]="'SEARCH.INPUT.ARIA-LABEL' | translate"
|
||||
[type]="'text'"
|
||||
[disabled]="true"
|
||||
[value]="searchedWord"
|
||||
[placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate"
|
||||
/>
|
||||
|
||||
<div matSuffix class="app-suffix-search-icon-wrapper">
|
||||
<mat-icon>arrow_drop_down</mat-icon>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-menu #searchOptionsMenu="matMenu" [overlapTrigger]="true" class="app-search-options-menu">
|
||||
<div (keydown.tab)="$event.stopPropagation()" (keydown.shift.tab)="$event.stopPropagation()">
|
||||
<div cdkTrapFocus>
|
||||
<app-search-input-control
|
||||
#searchInputControl
|
||||
(click)="$event.stopPropagation()"
|
||||
(submit)="onSearchSubmit($event)"
|
||||
(searchChange)="onSearchChange($event)"
|
||||
>
|
||||
</app-search-input-control>
|
||||
<mat-hint *ngIf="hasLibraryConstraint()" class="app-search-hint">{{ 'SEARCH.INPUT.HINT' | translate }}</mat-hint>
|
||||
|
||||
<div id="search-options">
|
||||
<mat-checkbox
|
||||
*ngFor="let option of searchOptions"
|
||||
id="{{ option.id }}"
|
||||
[(ngModel)]="option.value"
|
||||
[disabled]="option.shouldDisable()"
|
||||
(change)="searchByOption()"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
{{ option.key | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-menu>
|
@@ -0,0 +1,155 @@
|
||||
$search-width: 594px;
|
||||
$search-height: 40px;
|
||||
$search-background: #f5f6f5;
|
||||
$search-border-radius: 4px;
|
||||
$top-margin: 12px;
|
||||
|
||||
.app-search-container {
|
||||
color: var(--theme-foreground-text-color);
|
||||
|
||||
.app-input-form-field {
|
||||
.mat-input-element {
|
||||
caret-color: var(--theme-text-color);
|
||||
|
||||
&:disabled {
|
||||
color: var(--theme-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-focused label.mat-form-field-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.app-search-options-menu {
|
||||
&.mat-menu-panel {
|
||||
background-color: var(--theme-dialog-background-color);
|
||||
width: $search-width;
|
||||
max-width: unset;
|
||||
border-radius: $search-border-radius;
|
||||
margin-top: $top-margin;
|
||||
}
|
||||
|
||||
.mat-menu-content:not(:empty) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#search-options {
|
||||
color: var(--theme-text-color);
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
}
|
||||
|
||||
mat-checkbox {
|
||||
.mat-checkbox-frame {
|
||||
border-color: var(--theme-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.aca-search-input {
|
||||
width: 100%;
|
||||
max-width: $search-width;
|
||||
background-color: $search-background;
|
||||
border-radius: $search-border-radius;
|
||||
height: $search-height;
|
||||
}
|
||||
|
||||
.app-search-container {
|
||||
width: 100%;
|
||||
max-width: $search-width;
|
||||
height: $search-height + $top-margin;
|
||||
}
|
||||
|
||||
.app-search-control {
|
||||
margin-top: -$top-margin;
|
||||
}
|
||||
|
||||
#search-options {
|
||||
padding: 20px 0;
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.7px;
|
||||
|
||||
mat-checkbox {
|
||||
padding: 3px 24px 3px 19px;
|
||||
|
||||
.mat-checkbox-inner-container {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.mat-checkbox-label {
|
||||
padding: 0 0 0 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-checkbox-layout {
|
||||
max-width: 155px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-search-hint {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 959px) {
|
||||
$search-width-small: 400px;
|
||||
|
||||
.aca-search-input {
|
||||
max-width: $search-width-small;
|
||||
}
|
||||
|
||||
.app-search-container {
|
||||
max-width: $search-width-small;
|
||||
}
|
||||
|
||||
.app-search-options-menu {
|
||||
&.mat-menu-panel {
|
||||
width: $search-width-small;
|
||||
}
|
||||
}
|
||||
|
||||
#search-options {
|
||||
padding-left: 20px;
|
||||
|
||||
mat-checkbox {
|
||||
padding: 3px 20px 3px 0;
|
||||
|
||||
.mat-checkbox-label {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-checkbox-layout {
|
||||
max-width: 105px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
$search-width-xsmall: 220px;
|
||||
|
||||
.aca-search-input {
|
||||
max-width: $search-width-xsmall;
|
||||
}
|
||||
|
||||
.app-search-container {
|
||||
max-width: $search-width-xsmall;
|
||||
}
|
||||
|
||||
.app-search-options-menu {
|
||||
&.mat-menu-panel {
|
||||
width: $search-width-xsmall;
|
||||
margin-top: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
#search-options .mat-checkbox-layout {
|
||||
max-width: 180px;
|
||||
}
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
||||
import { SearchInputComponent } from './search-input.component';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { Actions, ofType } from '@ngrx/effects';
|
||||
import { SearchByTermAction, SearchActionTypes, SnackbarErrorAction, SnackbarActionTypes } from '@alfresco/aca-shared/store';
|
||||
import { AppHookService } from '@alfresco/aca-shared';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
|
||||
describe('SearchInputComponent', () => {
|
||||
let fixture: ComponentFixture<SearchInputComponent>;
|
||||
let component: SearchInputComponent;
|
||||
let actions$: Actions;
|
||||
let appHookService: AppHookService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule],
|
||||
declarations: [SearchInputComponent],
|
||||
providers: [SearchQueryBuilderService],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
});
|
||||
|
||||
actions$ = TestBed.inject(Actions);
|
||||
fixture = TestBed.createComponent(SearchInputComponent);
|
||||
appHookService = TestBed.inject(AppHookService);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should change flag on library400Error event', () => {
|
||||
expect(component.has400LibraryError).toBe(false);
|
||||
appHookService.library400Error.next();
|
||||
|
||||
expect(component.has400LibraryError).toBe(true);
|
||||
});
|
||||
|
||||
it('should have no library constraint by default', () => {
|
||||
expect(component.hasLibraryConstraint()).toBe(false);
|
||||
});
|
||||
|
||||
it('should have library constraint on 400 error received', () => {
|
||||
const libItem = component.searchOptions.find((item) => item.key.toLowerCase().indexOf('libraries') > 0);
|
||||
libItem.value = true;
|
||||
appHookService.library400Error.next();
|
||||
|
||||
expect(component.hasLibraryConstraint()).toBe(true);
|
||||
});
|
||||
|
||||
describe('onSearchSubmit()', () => {
|
||||
it('should call search action with correct search options', (done) => {
|
||||
const searchedTerm = 's';
|
||||
component.searchedWord = 'te';
|
||||
actions$
|
||||
.pipe(
|
||||
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
|
||||
map((action) => {
|
||||
expect(action.searchOptions[0].key).toBe('SEARCH.INPUT.FILES');
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
component.onSearchSubmit({ target: { value: searchedTerm } });
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call search action with correct searched term', (done) => {
|
||||
const searchedTerm = 's';
|
||||
actions$
|
||||
.pipe(
|
||||
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
|
||||
map((action) => {
|
||||
expect(action.payload).toBe(searchedTerm);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
component.onSearchSubmit({ target: { value: searchedTerm } });
|
||||
fixture.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSearchChange()', () => {
|
||||
it('should call search action with correct search options', (done) => {
|
||||
const searchedTerm = 's';
|
||||
const currentSearchOptions = [{ key: 'SEARCH.INPUT.FILES' }];
|
||||
actions$
|
||||
.pipe(
|
||||
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
|
||||
map((action) => {
|
||||
expect(action.searchOptions[0].key).toBe(currentSearchOptions[0].key);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
component.onSearchChange(searchedTerm);
|
||||
});
|
||||
|
||||
it('should call search action with correct searched term', (done) => {
|
||||
const searchedTerm = 's';
|
||||
actions$
|
||||
.pipe(
|
||||
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
|
||||
map((action) => {
|
||||
expect(action.payload).toBe(searchedTerm);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
component.onSearchChange(searchedTerm);
|
||||
});
|
||||
|
||||
it('should show snack for empty search', (done) => {
|
||||
const searchedTerm = '';
|
||||
actions$
|
||||
.pipe(
|
||||
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
|
||||
map((action) => {
|
||||
expect(action.payload).toBe('APP.BROWSE.SEARCH.EMPTY_SEARCH');
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
component.onSearchSubmit(searchedTerm);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLibrariesChecked()', () => {
|
||||
it('should return false by default', () => {
|
||||
expect(component.isLibrariesChecked()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when libraries checked', () => {
|
||||
const libItem = component.searchOptions.find((item) => item.key.toLowerCase().indexOf('libraries') > 0);
|
||||
libItem.value = true;
|
||||
expect(component.isLibrariesChecked()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isContentChecked()', () => {
|
||||
it('should return false by default', () => {
|
||||
expect(component.isContentChecked()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when files checked', () => {
|
||||
const filesItem = component.searchOptions.find((item) => item.key.toLowerCase().indexOf('files') > 0);
|
||||
filesItem.value = true;
|
||||
expect(component.isContentChecked()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when folders checked', () => {
|
||||
const foldersItem = component.searchOptions.find((item) => item.key.toLowerCase().indexOf('folders') > 0);
|
||||
foldersItem.value = true;
|
||||
expect(component.isContentChecked()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when both files and folders checked', () => {
|
||||
const filesItem = component.searchOptions.find((item) => item.key.toLowerCase().indexOf('files') > 0);
|
||||
filesItem.value = true;
|
||||
const foldersItem = component.searchOptions.find((item) => item.key.toLowerCase().indexOf('folders') > 0);
|
||||
foldersItem.value = true;
|
||||
expect(component.isContentChecked()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,276 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { AppHookService } from '@alfresco/aca-shared';
|
||||
import { AppStore, SearchByTermAction, SearchOptionIds, SearchOptionModel, SnackbarErrorAction } from '@alfresco/aca-shared/store';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
import { Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { NavigationEnd, PRIMARY_OUTLET, Router, RouterEvent, UrlSegment, UrlSegmentGroup, UrlTree } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
import { SearchInputControlComponent } from '../search-input-control/search-input-control.component';
|
||||
import { SearchLibrariesQueryBuilderService } from '../search-libraries-results/search-libraries-query-builder.service';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-search-input',
|
||||
templateUrl: './search-input.component.html',
|
||||
styleUrls: ['./search-input.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: { class: 'aca-search-input' }
|
||||
})
|
||||
export class SearchInputComponent implements OnInit, OnDestroy {
|
||||
onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
hasOneChange = false;
|
||||
hasNewChange = false;
|
||||
navigationTimer: any;
|
||||
has400LibraryError = false;
|
||||
searchOnChange: boolean;
|
||||
|
||||
searchedWord: string = null;
|
||||
searchOptions: Array<SearchOptionModel> = [
|
||||
{
|
||||
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)
|
||||
}
|
||||
];
|
||||
|
||||
@ViewChild('searchInputControl', { static: true })
|
||||
searchInputControl: SearchInputControlComponent;
|
||||
|
||||
@ViewChild(MatMenuTrigger, { static: true })
|
||||
trigger: MatMenuTrigger;
|
||||
|
||||
constructor(
|
||||
private queryBuilder: SearchQueryBuilderService,
|
||||
private queryLibrariesBuilder: SearchLibrariesQueryBuilderService,
|
||||
private config: AppConfigService,
|
||||
private router: Router,
|
||||
private store: Store<AppStore>,
|
||||
private appHookService: AppHookService
|
||||
) {
|
||||
this.searchOnChange = this.config.get<boolean>('search.aca:triggeredOnChange', true);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.showInputValue();
|
||||
|
||||
this.router.events
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.pipe(filter((e) => e instanceof RouterEvent))
|
||||
.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.showInputValue();
|
||||
}
|
||||
});
|
||||
|
||||
this.appHookService.library400Error.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
|
||||
this.has400LibraryError = true;
|
||||
});
|
||||
}
|
||||
|
||||
showInputValue() {
|
||||
this.has400LibraryError = false;
|
||||
this.searchedWord = this.getUrlSearchTerm();
|
||||
|
||||
if (this.searchInputControl) {
|
||||
this.searchInputControl.searchTerm = this.searchedWord;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
this.removeContentFilters();
|
||||
}
|
||||
|
||||
onMenuOpened() {
|
||||
this.searchInputControl.searchInput.nativeElement.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user submits the search, e.g. hits enter or clicks submit
|
||||
*
|
||||
* @param event Parameters relating to the search
|
||||
*/
|
||||
onSearchSubmit(event: any) {
|
||||
const searchTerm = event.target ? (event.target as HTMLInputElement).value : event;
|
||||
if (searchTerm) {
|
||||
this.searchedWord = searchTerm;
|
||||
|
||||
this.searchByOption();
|
||||
} else {
|
||||
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.EMPTY_SEARCH'));
|
||||
}
|
||||
this.trigger.closeMenu();
|
||||
}
|
||||
|
||||
onSearchChange(searchTerm: string) {
|
||||
if (!this.searchOnChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.has400LibraryError = false;
|
||||
this.searchedWord = searchTerm;
|
||||
|
||||
if (this.hasOneChange) {
|
||||
this.hasNewChange = true;
|
||||
} else {
|
||||
this.hasOneChange = true;
|
||||
}
|
||||
|
||||
if (this.hasNewChange) {
|
||||
clearTimeout(this.navigationTimer);
|
||||
this.hasNewChange = false;
|
||||
}
|
||||
|
||||
this.navigationTimer = setTimeout(() => {
|
||||
if (searchTerm) {
|
||||
this.store.dispatch(new SearchByTermAction(searchTerm, this.searchOptions));
|
||||
}
|
||||
this.hasOneChange = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
searchByOption() {
|
||||
this.syncInputValues();
|
||||
this.has400LibraryError = false;
|
||||
if (this.isLibrariesChecked()) {
|
||||
if (this.onLibrariesSearchResults && this.isSameSearchTerm()) {
|
||||
this.queryLibrariesBuilder.update();
|
||||
} else if (this.searchedWord) {
|
||||
this.store.dispatch(new SearchByTermAction(this.searchedWord, this.searchOptions));
|
||||
}
|
||||
} else {
|
||||
if (this.isFoldersChecked() && !this.isFilesChecked()) {
|
||||
this.filterContent(SearchOptionIds.Folders);
|
||||
} else if (this.isFilesChecked() && !this.isFoldersChecked()) {
|
||||
this.filterContent(SearchOptionIds.Files);
|
||||
} else {
|
||||
this.removeContentFilters();
|
||||
}
|
||||
|
||||
if (this.onSearchResults && this.isSameSearchTerm()) {
|
||||
this.queryBuilder.update();
|
||||
} else if (this.searchedWord) {
|
||||
this.store.dispatch(new SearchByTermAction(this.searchedWord, this.searchOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get onLibrariesSearchResults() {
|
||||
return this.router.url.indexOf('/search-libraries') === 0;
|
||||
}
|
||||
|
||||
get onSearchResults() {
|
||||
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.isOptionChecked(SearchOptionIds.Libraries);
|
||||
}
|
||||
|
||||
isOptionChecked(optionId: string): boolean {
|
||||
const libItem = this.searchOptions.find((item) => item.id === optionId);
|
||||
return !!libItem && libItem.value;
|
||||
}
|
||||
|
||||
isContentChecked(): boolean {
|
||||
return this.isFilesChecked() || this.isFoldersChecked();
|
||||
}
|
||||
|
||||
hasLibraryConstraint(): boolean {
|
||||
if (this.isLibrariesChecked()) {
|
||||
return this.has400LibraryError || this.searchInputControl.isTermTooShort();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
filterContent(option: SearchOptionIds.Folders | SearchOptionIds.Files) {
|
||||
const oppositeOption = option === SearchOptionIds.Folders ? SearchOptionIds.Files : SearchOptionIds.Folders;
|
||||
|
||||
this.queryBuilder.addFilterQuery(`+TYPE:'cm:${option}'`);
|
||||
this.queryBuilder.removeFilterQuery(`+TYPE:'cm:${oppositeOption}'`);
|
||||
}
|
||||
|
||||
removeContentFilters() {
|
||||
this.queryBuilder.removeFilterQuery(`+TYPE:'cm:${SearchOptionIds.Files}'`);
|
||||
this.queryBuilder.removeFilterQuery(`+TYPE:'cm:${SearchOptionIds.Folders}'`);
|
||||
}
|
||||
|
||||
syncInputValues() {
|
||||
if (this.searchInputControl.searchTerm !== this.searchedWord) {
|
||||
if (this.searchInputControl.searchTerm) {
|
||||
this.searchedWord = this.searchInputControl.searchTerm;
|
||||
} else {
|
||||
this.searchInputControl.searchTerm = this.searchedWord;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getUrlSearchTerm(): string {
|
||||
let searchTerm = '';
|
||||
if (this.onSearchResults || this.onLibrariesSearchResults) {
|
||||
const urlTree: UrlTree = this.router.parseUrl(this.router.url);
|
||||
const urlSegmentGroup: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
||||
|
||||
if (urlSegmentGroup) {
|
||||
const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
|
||||
searchTerm = urlSegments[0].parameters['q'] ? decodeURIComponent(urlSegments[0].parameters['q']) : '';
|
||||
}
|
||||
}
|
||||
|
||||
return searchTerm;
|
||||
}
|
||||
|
||||
isSameSearchTerm(): boolean {
|
||||
const urlSearchTerm = this.getUrlSearchTerm();
|
||||
return this.searchedWord === urlSearchTerm;
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { TestBed } from '@angular/core/testing';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { AlfrescoApiService } from '@alfresco/adf-core';
|
||||
import { SearchLibrariesQueryBuilderService, LibrarySearchQuery } from './search-libraries-query-builder.service';
|
||||
|
||||
describe('SearchLibrariesQueryBuilderService', () => {
|
||||
let apiService: AlfrescoApiService;
|
||||
let builder: SearchLibrariesQueryBuilderService;
|
||||
let queriesApi;
|
||||
const query = {} as LibrarySearchQuery;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule]
|
||||
});
|
||||
|
||||
apiService = TestBed.inject(AlfrescoApiService);
|
||||
apiService.reset();
|
||||
builder = new SearchLibrariesQueryBuilderService(apiService);
|
||||
queriesApi = builder['queriesApi'];
|
||||
});
|
||||
|
||||
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 () => {
|
||||
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));
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
it('should raise an event on error', async () => {
|
||||
const err = '{"error": {"statusCode": 400}}';
|
||||
spyOn(queriesApi, 'findSites').and.returnValue(Promise.reject(err));
|
||||
|
||||
spyOn(builder, 'buildQuery').and.returnValue(query);
|
||||
|
||||
let eventArgs = null;
|
||||
builder.hadError.subscribe((args) => (eventArgs = args));
|
||||
|
||||
await builder.execute();
|
||||
expect(eventArgs).toBe(err);
|
||||
});
|
||||
});
|
@@ -0,0 +1,103 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { QueriesApi, SitePaging } from '@alfresco/js-api';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface LibrarySearchQuery {
|
||||
term: string;
|
||||
opts: {
|
||||
skipCount: number;
|
||||
maxItems: number;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SearchLibrariesQueryBuilderService {
|
||||
_queriesApi: QueriesApi;
|
||||
get queriesApi(): QueriesApi {
|
||||
this._queriesApi = this._queriesApi ?? new QueriesApi(this.alfrescoApiService.getInstance());
|
||||
return this._queriesApi;
|
||||
}
|
||||
|
||||
private _userQuery = '';
|
||||
|
||||
updated: Subject<any> = new Subject();
|
||||
executed: Subject<any> = new Subject();
|
||||
hadError: 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();
|
||||
if (query) {
|
||||
this.updated.next(query);
|
||||
}
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const query = this.buildQuery();
|
||||
if (query) {
|
||||
const data = await this.findLibraries(query);
|
||||
this.executed.next(data);
|
||||
}
|
||||
}
|
||||
|
||||
buildQuery(): LibrarySearchQuery {
|
||||
const query = this.userQuery;
|
||||
if (query && query.length > 1) {
|
||||
const resultQuery = {
|
||||
term: query,
|
||||
opts: {
|
||||
skipCount: this.paging && this.paging.skipCount,
|
||||
maxItems: this.paging && this.paging.maxItems
|
||||
}
|
||||
};
|
||||
return resultQuery;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private findLibraries(libraryQuery: LibrarySearchQuery): Promise<SitePaging> {
|
||||
return this.queriesApi.findSites(libraryQuery.term, libraryQuery.opts).catch((err) => {
|
||||
this.hadError.next(err);
|
||||
return { list: { pagination: { totalItems: 0 }, entries: [] } };
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
<aca-page-layout>
|
||||
<aca-page-layout-header>
|
||||
<adf-breadcrumb root="APP.BROWSE.SEARCH_LIBRARIES.TITLE"> </adf-breadcrumb>
|
||||
<adf-toolbar class="adf-toolbar--inline">
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [actionRef]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
</aca-page-layout-header>
|
||||
|
||||
<aca-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" *ngIf="totalResults !== 1">
|
||||
{{ 'APP.BROWSE.SEARCH_LIBRARIES.FOUND_RESULTS' | translate: { number: totalResults } }}
|
||||
</div>
|
||||
<div class="adf-search-results--info-text" *ngIf="totalResults === 1">
|
||||
{{ 'APP.BROWSE.SEARCH_LIBRARIES.FOUND_ONE_RESULT' | translate: { number: totalResults } }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<adf-document-list
|
||||
#documentList
|
||||
acaContextActions
|
||||
acaDocumentList
|
||||
[showHeader]="showHeader"
|
||||
[selectionMode]="'single'"
|
||||
[sorting]="['name', 'asc']"
|
||||
[node]="data"
|
||||
[imageResolver]="imageResolver"
|
||||
(node-dblclick)="handleNodeClick($event)"
|
||||
(name-click)="handleNodeClick($event)"
|
||||
>
|
||||
<data-columns>
|
||||
<ng-container *ngFor="let column of columns; trackBy: trackByColumnId">
|
||||
<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>
|
||||
<adf-dynamic-column [id]="column.template" [context]="context"> </adf-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>
|
||||
|
||||
<adf-custom-empty-content-template>
|
||||
<ng-container *ngIf="data">
|
||||
<div class="empty-search__block" aria-live="polite">
|
||||
<p class="empty-search__text">
|
||||
{{ 'APP.BROWSE.SEARCH.NO_RESULTS' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</adf-custom-empty-content-template>
|
||||
</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>
|
||||
</aca-page-layout-content>
|
||||
</aca-page-layout>
|
@@ -0,0 +1,42 @@
|
||||
@import '../../../ui/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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { AppConfigModule, 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, AppConfigModule],
|
||||
declarations: [DataTableComponent, DocumentListComponent, SearchLibrariesResultsComponent],
|
||||
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);
|
||||
});
|
||||
});
|
@@ -0,0 +1,161 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { NavigateLibraryAction, AppStore } from '@alfresco/aca-shared/store';
|
||||
import { NodePaging, Pagination, SiteEntry } from '@alfresco/js-api';
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ContentManagementService } from '../../../services/content-management.service';
|
||||
import { PageComponent } from '../../page.component';
|
||||
import { SearchLibrariesQueryBuilderService } from './search-libraries-query-builder.service';
|
||||
import { AppExtensionService, AppHookService } from '@alfresco/aca-shared';
|
||||
import { DocumentListPresetRef } from '@alfresco/adf-extensions';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-search-results',
|
||||
templateUrl: './search-libraries-results.component.html',
|
||||
styleUrls: ['./search-libraries-results.component.scss']
|
||||
})
|
||||
export class SearchLibrariesResultsComponent extends PageComponent implements OnInit {
|
||||
isSmallScreen = false;
|
||||
searchedWord: string;
|
||||
queryParamName = 'q';
|
||||
data: NodePaging;
|
||||
totalResults = 0;
|
||||
isLoading = false;
|
||||
columns: DocumentListPresetRef[] = [];
|
||||
|
||||
constructor(
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
private librariesQueryBuilder: SearchLibrariesQueryBuilderService,
|
||||
private route: ActivatedRoute,
|
||||
private appHookService: AppHookService,
|
||||
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.appHookService.libraryJoined.subscribe(() => this.librariesQueryBuilder.update()),
|
||||
this.appHookService.libraryDeleted.subscribe(() => this.librariesQueryBuilder.update()),
|
||||
this.appHookService.libraryLeft.subscribe(() => this.librariesQueryBuilder.update()),
|
||||
|
||||
this.librariesQueryBuilder.updated.subscribe(() => {
|
||||
this.isLoading = true;
|
||||
|
||||
this.librariesQueryBuilder.execute();
|
||||
}),
|
||||
|
||||
this.librariesQueryBuilder.executed.subscribe((data) => {
|
||||
this.onSearchResultLoaded(data);
|
||||
this.isLoading = false;
|
||||
}),
|
||||
|
||||
this.librariesQueryBuilder.hadError.subscribe((err) => {
|
||||
try {
|
||||
const {
|
||||
error: { statusCode }
|
||||
} = JSON.parse(err.message);
|
||||
if (statusCode === 400) {
|
||||
this.appHookService.library400Error.next();
|
||||
}
|
||||
} catch (e) {}
|
||||
}),
|
||||
|
||||
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.paging.skipCount = 0;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
handleNodeClick(event: Event) {
|
||||
this.navigateTo((event as CustomEvent).detail?.node);
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
<div class="search-file-name">
|
||||
<span tabindex="0" role="link" *ngIf="isFile" (click)="showPreview($event)" (keyup.enter)="showPreview($event)" class="link">
|
||||
{{ name$ | async }}
|
||||
</span>
|
||||
<span tabindex="0" role="link" *ngIf="!isFile" (click)="navigate($event)" (keyup.enter)="navigate($event)" class="bold link">
|
||||
{{ name$ | async }}
|
||||
</span>
|
||||
<span>{{ title$ | async }}</span>
|
||||
</div>
|
||||
<div class="result-location">
|
||||
<aca-location-link [context]="context" [showLocation]="true"></aca-location-link>
|
||||
</div>
|
@@ -0,0 +1,28 @@
|
||||
.aca-search-results-row {
|
||||
|
||||
.result-location {
|
||||
height: 15px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: var(--theme-text-bold-color);
|
||||
}
|
||||
|
||||
.aca-location-link .adf-datatable-cell-value {
|
||||
padding: 0;
|
||||
color: var(--theme-foreground-text-color);
|
||||
}
|
||||
|
||||
.link:hover,
|
||||
.aca-location-link .adf-datatable-cell-value:hover {
|
||||
color: var(--theme-primary-color) !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.adf-is-selected .link:not(:hover),
|
||||
.adf-is-selected .adf-datatable-cell-value:not(:hover) {
|
||||
color: var(--theme-primary-color) !important;
|
||||
}
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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, Input, OnInit, ViewEncapsulation, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
|
||||
import { MinimalNodeEntity } from '@alfresco/js-api';
|
||||
import { ViewNodeAction, NavigateToFolder } from '@alfresco/aca-shared/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { AlfrescoApiService } from '@alfresco/adf-core';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-search-results-row',
|
||||
templateUrl: './search-results-row.component.html',
|
||||
styleUrls: ['./search-results-row.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'aca-search-results-row' }
|
||||
})
|
||||
export class SearchResultsRowComponent implements OnInit, OnDestroy {
|
||||
private node: MinimalNodeEntity;
|
||||
private onDestroy$ = new Subject<boolean>();
|
||||
|
||||
@Input()
|
||||
context: any;
|
||||
|
||||
name$ = new BehaviorSubject<string>('');
|
||||
title$ = new BehaviorSubject<string>('');
|
||||
|
||||
constructor(private store: Store<any>, private alfrescoApiService: AlfrescoApiService, private router: Router) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.updateValues();
|
||||
|
||||
this.alfrescoApiService.nodeUpdated.pipe(takeUntil(this.onDestroy$)).subscribe((node) => {
|
||||
const row = this.context.row;
|
||||
if (row) {
|
||||
const { entry } = row.node;
|
||||
|
||||
if (entry.id === node.id) {
|
||||
entry.name = node.name;
|
||||
entry.properties = Object.assign({}, node.properties);
|
||||
|
||||
this.updateValues();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateValues() {
|
||||
this.node = this.context.row.node;
|
||||
|
||||
const { name, properties } = this.node.entry;
|
||||
const title = properties ? properties['cm:title'] : '';
|
||||
|
||||
this.name$.next(name);
|
||||
|
||||
if (title !== name) {
|
||||
this.title$.next(title ? `( ${title} )` : '');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
get isFile(): boolean {
|
||||
return this.node.entry.isFile;
|
||||
}
|
||||
|
||||
showPreview(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.store.dispatch(new ViewNodeAction(this.node.entry.id, { location: this.router.url }));
|
||||
}
|
||||
|
||||
navigate(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.store.dispatch(new NavigateToFolder(this.node));
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { NodeEntry } from '@alfresco/js-api';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CoreModule } from '@angular/flex-layout';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { AppSearchResultsModule } from '../search-results.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;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), CoreModule, AppTestingModule, AppSearchResultsModule]
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@@ -0,0 +1,60 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CoreModule } from '@alfresco/adf-core';
|
||||
import { ContentModule } from '@alfresco/adf-content-services';
|
||||
import { LockedByModule } from '@alfresco/aca-shared';
|
||||
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';
|
||||
import { DirectivesModule } from '../../directives/directives.module';
|
||||
import { AppLayoutModule } from '../layout/layout.module';
|
||||
import { ContextMenuModule } from '../context-menu/context-menu.module';
|
||||
import { SearchActionMenuComponent } from './search-action-menu/search-action-menu.component';
|
||||
import { DocumentListCustomComponentsModule } from '../dl-custom-components/document-list-custom-components.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
CoreModule.forChild(),
|
||||
ContentModule.forChild(),
|
||||
AppCommonModule,
|
||||
AppInfoDrawerModule,
|
||||
AppToolbarModule,
|
||||
DirectivesModule,
|
||||
AppLayoutModule,
|
||||
ContextMenuModule,
|
||||
LockedByModule,
|
||||
DocumentListCustomComponentsModule
|
||||
],
|
||||
declarations: [SearchResultsComponent, SearchLibrariesResultsComponent, SearchResultsRowComponent, SearchActionMenuComponent],
|
||||
exports: [SearchResultsComponent, SearchLibrariesResultsComponent, SearchResultsRowComponent, SearchActionMenuComponent]
|
||||
})
|
||||
export class AppSearchResultsModule {}
|
@@ -0,0 +1,133 @@
|
||||
<aca-page-layout>
|
||||
<aca-page-layout-header>
|
||||
<adf-breadcrumb root="APP.BROWSE.SEARCH.TITLE"> </adf-breadcrumb>
|
||||
<adf-toolbar class="adf-toolbar--inline">
|
||||
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
|
||||
<aca-toolbar-action [actionRef]="entry"></aca-toolbar-action>
|
||||
</ng-container>
|
||||
</adf-toolbar>
|
||||
</aca-page-layout-header>
|
||||
|
||||
<aca-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" aria-live="polite"> </mat-progress-bar>
|
||||
<div class="adf-search-results__content-header content">
|
||||
<adf-search-form class="content__side--left"></adf-search-form>
|
||||
|
||||
<mat-divider [vertical]="true" class="content__divider"></mat-divider>
|
||||
|
||||
<adf-search-filter-chips class="content__filter"></adf-search-filter-chips>
|
||||
|
||||
<button
|
||||
mat-button
|
||||
adf-reset-search
|
||||
class="content__reset-action"
|
||||
title="{{'APP.BROWSE.SEARCH.RESET_ACTION' | translate }}"
|
||||
[attr.aria-label]="'APP.BROWSE.SEARCH.RESET_ACTION' | translate ">
|
||||
<mat-icon> refresh </mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<adf-document-list
|
||||
#documentList
|
||||
acaDocumentList
|
||||
acaContextActions
|
||||
[selectionMode]="'multiple'"
|
||||
[sortingMode]="'server'"
|
||||
[sorting]="sorting"
|
||||
[imageResolver]="imageResolver"
|
||||
[node]="$any(data)"
|
||||
(node-dblclick)="handleNodeClick($event)"
|
||||
>
|
||||
<data-columns>
|
||||
<data-column key="$thumbnail" type="image" [sr-title]="'ADF-DOCUMENT-LIST.LAYOUT.THUMBNAIL'" [sortable]="false">
|
||||
<ng-template let-context>
|
||||
<aca-custom-thumbnail-column [context]="context"></aca-custom-thumbnail-column>
|
||||
</ng-template>
|
||||
|
||||
<adf-data-column-header>
|
||||
<ng-template>
|
||||
<aca-search-action-menu (sortingSelected)="onSearchSortingUpdate($event)"></aca-search-action-menu>
|
||||
</ng-template>
|
||||
</adf-data-column-header>
|
||||
</data-column>
|
||||
|
||||
<data-column key type="text" class="adf-ellipsis-cell adf-expand-cell-5" title="APP.DOCUMENT_LIST.COLUMNS.NAME" [sortable]="false">
|
||||
<ng-template let-context>
|
||||
<aca-search-results-row [context]="context"></aca-search-results-row>
|
||||
</ng-template>
|
||||
</data-column>
|
||||
|
||||
<data-column key="properties" title="Description" class="adf-expand-cell-3" [sortable]="false" *ngIf="!isSmallScreen">
|
||||
<ng-template let-context>
|
||||
<span class="adf-datatable-cell-value adf-ellipsis-cell">
|
||||
{{context.row?.node?.entry?.properties && context.row?.node?.entry?.properties['cm:description']}}
|
||||
</span>
|
||||
</ng-template>
|
||||
</data-column>
|
||||
|
||||
<data-column key="content.sizeInBytes" type="fileSize" title="APP.DOCUMENT_LIST.COLUMNS.SIZE" class="adf-no-grow-cell adf-ellipsis-cell" [sortable]="false" *ngIf="!isSmallScreen"></data-column>
|
||||
<data-column key="modifiedAt" type="date" title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON" class="adf-no-grow-cell adf-ellipsis-cell" format="timeAgo" [sortable]="false" *ngIf="!isSmallScreen"></data-column>
|
||||
<data-column key="modifiedByUser.displayName" title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY" class="adf-no-grow-cell adf-ellipsis-cell" [sortable]="false" *ngIf="!isSmallScreen"></data-column>
|
||||
</data-columns>
|
||||
|
||||
<adf-custom-empty-content-template>
|
||||
<ng-container *ngIf="data">
|
||||
<div class="empty-search__block" aria-live="polite">
|
||||
<p class="empty-search__text">
|
||||
{{ 'APP.BROWSE.SEARCH.NO_RESULTS' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</adf-custom-empty-content-template>
|
||||
</adf-document-list>
|
||||
|
||||
<adf-pagination *ngIf="!documentList.isEmpty()" acaPagination [target]="documentList" (change)="onPaginationChanged($event)">
|
||||
</adf-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
[ngClass]="
|
||||
(infoDrawerPreview$ | async) === true ? 'adf-search-results--right_panel_section-extended' : 'adf-search-results--right_panel_section'
|
||||
"
|
||||
*ngIf="infoDrawerOpened$ | async"
|
||||
>
|
||||
<adf-viewer
|
||||
class="adf-search-results--embedded_viewer"
|
||||
[nodeId]="selection.last.entry.id"
|
||||
*ngIf="infoDrawerPreview$ | async; else infoDrawerPanel"
|
||||
>
|
||||
<adf-viewer-toolbar>
|
||||
<div class="adf-search-results--preview-toolbar">
|
||||
<div>
|
||||
<button mat-icon-button title="{{ 'ADF_VIEWER.ACTIONS.CLOSE' | translate }}" (click)="onDrawerClosed()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
mat-icon-button
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.PREVIEW' | translate }}"
|
||||
color="accent"
|
||||
class="adf-search-results--visibility_button"
|
||||
>
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button title="{{ 'ADF_VIEWER.ACTIONS.CLOSE' | translate }}" (click)="onPreviewClosed()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</adf-viewer-toolbar>
|
||||
</adf-viewer>
|
||||
<ng-template #infoDrawerPanel>
|
||||
<div class="sidebar">
|
||||
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</aca-page-layout-content>
|
||||
</aca-page-layout>
|
@@ -0,0 +1,128 @@
|
||||
@import '../../../ui/mixins';
|
||||
|
||||
$adf-chip-background: #efefef;
|
||||
$contrast-gray: #646569;
|
||||
|
||||
.adf-search-results {
|
||||
@include flex-row;
|
||||
|
||||
&__facets {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__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;
|
||||
|
||||
.adf-search-filter-chip {
|
||||
&.mat-chip {
|
||||
background-color: $adf-chip-background;
|
||||
color: $contrast-gray;
|
||||
}
|
||||
.adf-search-filter-placeholder {
|
||||
color: $contrast-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--info-text {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
&--embedded_viewer {
|
||||
position: unset;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--right_panel_section {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&--right_panel_section-extended {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-basis: 55%;
|
||||
}
|
||||
|
||||
&--preview-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin: 5px 5px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&--visibility_button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.adf-search-filter {
|
||||
min-width: 260px;
|
||||
max-width: 320px;
|
||||
padding: 5px;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
place-content: flex-start space-between;
|
||||
align-items: flex-start;
|
||||
padding: 16px 12px;
|
||||
|
||||
&__button {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__filter {
|
||||
padding: 0 12px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__reset-action {
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
&__sort-picker {
|
||||
min-width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-datatable {
|
||||
|
||||
aca-search-action-menu button {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.aca-location-link a {
|
||||
font-size: 12px;
|
||||
max-width: 350px;
|
||||
text-align: left;
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,283 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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 { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { SearchResultsComponent } from './search-results.component';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { AppSearchResultsModule } from '../search-results.module';
|
||||
import { AppConfigService, CoreModule, TranslationService } from '@alfresco/adf-core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NavigateToFolder, SnackbarErrorAction } from '@alfresco/aca-shared/store';
|
||||
import { Pagination, SearchRequest } from '@alfresco/js-api';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('SearchComponent', () => {
|
||||
let component: SearchResultsComponent;
|
||||
let fixture: ComponentFixture<SearchResultsComponent>;
|
||||
let config: AppConfigService;
|
||||
let store: Store<any>;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
let translate: TranslationService;
|
||||
let router: Router;
|
||||
const searchRequest = {} as SearchRequest;
|
||||
let params: BehaviorSubject<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
params = new BehaviorSubject({ q: 'TYPE: "cm:folder" AND %28=cm: name: email OR cm: name: budget%29' });
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), CoreModule.forRoot(), AppTestingModule, AppSearchResultsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
snapshot: {
|
||||
data: {
|
||||
sortingPreferenceKey: ''
|
||||
}
|
||||
},
|
||||
params: params.asObservable()
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
config = TestBed.inject(AppConfigService);
|
||||
store = TestBed.inject(Store);
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
translate = TestBed.inject(TranslationService);
|
||||
router = TestBed.inject(Router);
|
||||
|
||||
config.config = {
|
||||
search: {}
|
||||
};
|
||||
|
||||
fixture = TestBed.createComponent(SearchResultsComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
params.complete();
|
||||
});
|
||||
|
||||
it('should raise an error if search fails', fakeAsync(() => {
|
||||
spyOn(queryBuilder['searchApi'], 'search').and.returnValue(
|
||||
Promise.reject({
|
||||
message: `{ "error": { "statusCode": 500 } } `
|
||||
})
|
||||
);
|
||||
|
||||
spyOn(queryBuilder, 'buildQuery').and.returnValue(searchRequest);
|
||||
spyOn(store, 'dispatch').and.stub();
|
||||
|
||||
queryBuilder.execute();
|
||||
tick();
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.ERRORS.GENERIC'));
|
||||
}));
|
||||
|
||||
it('should raise a known error if search fails', fakeAsync(() => {
|
||||
spyOn(translate, 'instant').and.callFake((key: string) => {
|
||||
if (key === 'APP.BROWSE.SEARCH.ERRORS.401') {
|
||||
return 'Known Error';
|
||||
}
|
||||
return key;
|
||||
});
|
||||
|
||||
spyOn(queryBuilder['searchApi'], 'search').and.returnValue(
|
||||
Promise.reject({
|
||||
message: `{ "error": { "statusCode": 401 } } `
|
||||
})
|
||||
);
|
||||
|
||||
spyOn(queryBuilder, 'buildQuery').and.returnValue(searchRequest);
|
||||
spyOn(store, 'dispatch').and.stub();
|
||||
|
||||
queryBuilder.execute();
|
||||
tick();
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('Known Error'));
|
||||
}));
|
||||
|
||||
it('should raise a generic error if search fails', fakeAsync(() => {
|
||||
spyOn(translate, 'instant').and.callFake((key: string) => {
|
||||
if (key === 'APP.BROWSE.SEARCH.ERRORS.GENERIC') {
|
||||
return 'Generic Error';
|
||||
}
|
||||
return key;
|
||||
});
|
||||
|
||||
spyOn(queryBuilder['searchApi'], 'search').and.returnValue(
|
||||
Promise.reject({
|
||||
message: `{ "error": { "statusCode": 401 } } `
|
||||
})
|
||||
);
|
||||
|
||||
spyOn(queryBuilder, 'buildQuery').and.returnValue(searchRequest);
|
||||
spyOn(store, 'dispatch').and.stub();
|
||||
|
||||
queryBuilder.execute();
|
||||
tick();
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('Generic Error'));
|
||||
}));
|
||||
|
||||
it('should decode encoded URI', () => {
|
||||
expect(queryBuilder.userQuery).toEqual('(TYPE: "cm:folder" AND (=cm: name: email OR cm: name: budget))');
|
||||
});
|
||||
|
||||
it('should return null if formatting invalid query', () => {
|
||||
expect(component.formatSearchQuery(null)).toBeNull();
|
||||
expect(component.formatSearchQuery('')).toBeNull();
|
||||
});
|
||||
|
||||
it('should use original user input if text contains colons', () => {
|
||||
const query = 'TEXT:test OR TYPE:folder';
|
||||
expect(component.formatSearchQuery(query)).toBe(query);
|
||||
});
|
||||
|
||||
it('should be able to search if search input contains https url', () => {
|
||||
const query = component.formatSearchQuery('https://alfresco.com');
|
||||
expect(query).toBe(`(cm:name:"https://alfresco.com*")`);
|
||||
});
|
||||
|
||||
it('should be able to search if search input contains http url', () => {
|
||||
const query = component.formatSearchQuery('http://alfresco.com');
|
||||
expect(query).toBe(`(cm:name:"http://alfresco.com*")`);
|
||||
});
|
||||
|
||||
it('should use original user input if text contains quotes', () => {
|
||||
const query = `"Hello World"`;
|
||||
expect(component.formatSearchQuery(query)).toBe(query);
|
||||
});
|
||||
|
||||
it('should format user input according to the configuration fields', () => {
|
||||
const query = component.formatSearchQuery('hello', ['cm:name', 'cm:title']);
|
||||
expect(query).toBe(`(cm:name:"hello*" OR cm:title:"hello*")`);
|
||||
});
|
||||
|
||||
it('should format user input as cm:name if configuration not provided', () => {
|
||||
const query = component.formatSearchQuery('hello', undefined);
|
||||
expect(query).toBe(`(cm:name:"hello*")`);
|
||||
});
|
||||
|
||||
it('should use AND operator when conjunction has no operators', () => {
|
||||
const query = component.formatSearchQuery('big yellow banana', ['cm:name']);
|
||||
|
||||
expect(query).toBe(`(cm:name:"big*") AND (cm:name:"yellow*") AND (cm:name:"banana*")`);
|
||||
});
|
||||
|
||||
it('should support conjunctions with AND operator', () => {
|
||||
const query = component.formatSearchQuery('big AND yellow AND banana', ['cm:name', 'cm:title']);
|
||||
|
||||
expect(query).toBe(
|
||||
`(cm:name:"big*" OR cm:title:"big*") AND (cm:name:"yellow*" OR cm:title:"yellow*") AND (cm:name:"banana*" OR cm:title:"banana*")`
|
||||
);
|
||||
});
|
||||
|
||||
it('should support conjunctions with OR operator', () => {
|
||||
const query = component.formatSearchQuery('big OR yellow OR banana', ['cm:name', 'cm:title']);
|
||||
|
||||
expect(query).toBe(
|
||||
`(cm:name:"big*" OR cm:title:"big*") OR (cm:name:"yellow*" OR cm:title:"yellow*") OR (cm:name:"banana*" OR cm:title:"banana*")`
|
||||
);
|
||||
});
|
||||
|
||||
it('should support exact term matching with default fields', () => {
|
||||
const query = component.formatSearchQuery('=orange', ['cm:name', 'cm:title']);
|
||||
|
||||
expect(query).toBe(`(=cm:name:"orange" OR =cm:title:"orange")`);
|
||||
});
|
||||
|
||||
it('should support exact term matching with operators', () => {
|
||||
const query = component.formatSearchQuery('=test1.pdf or =test2.pdf', ['cm:name', 'cm:title']);
|
||||
|
||||
expect(query).toBe(`(=cm:name:"test1.pdf" OR =cm:title:"test1.pdf") or (=cm:name:"test2.pdf" OR =cm:title:"test2.pdf")`);
|
||||
});
|
||||
|
||||
it('should navigate to folder on double click', () => {
|
||||
const node: any = {
|
||||
entry: {
|
||||
isFolder: true
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(store, 'dispatch').and.stub();
|
||||
|
||||
component.onNodeDoubleClick(node);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new NavigateToFolder(node));
|
||||
});
|
||||
|
||||
it('should preview file node on double click', () => {
|
||||
const node: any = {
|
||||
entry: {
|
||||
isFolder: false
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(component, 'showPreview').and.stub();
|
||||
|
||||
component.onNodeDoubleClick(node);
|
||||
|
||||
expect(component.showPreview).toHaveBeenCalledWith(node, {
|
||||
location: router.url
|
||||
});
|
||||
});
|
||||
|
||||
it('should re-run search on pagination change', () => {
|
||||
const page = new Pagination({
|
||||
maxItems: 10,
|
||||
skipCount: 0
|
||||
});
|
||||
|
||||
component.onPaginationChanged(page);
|
||||
|
||||
expect(queryBuilder.paging).toEqual({
|
||||
maxItems: 10,
|
||||
skipCount: 0
|
||||
});
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update the user query whenever param changed', () => {
|
||||
params.next({ q: '=orange' });
|
||||
expect(queryBuilder.userQuery).toBe(`((=cm:name:"orange"))`);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update the user query whenever configuration changed', () => {
|
||||
params.next({ q: '=orange' });
|
||||
queryBuilder.configUpdated.next({ 'aca:fields': ['cm:tag'] } as any);
|
||||
expect(queryBuilder.userQuery).toBe(`((=cm:tag:"orange"))`);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@@ -0,0 +1,265 @@
|
||||
/*!
|
||||
* @license
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* Copyright (C) 2005 - 2020 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, ViewEncapsulation } from '@angular/core';
|
||||
import { MinimalNodeEntity, Pagination, ResultSetPaging } from '@alfresco/js-api';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { PageComponent } from '../../page.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
AppStore,
|
||||
infoDrawerPreview,
|
||||
NavigateToFolder,
|
||||
SetInfoDrawerPreviewStateAction,
|
||||
SetInfoDrawerStateAction,
|
||||
showFacetFilter,
|
||||
ShowInfoDrawerPreviewAction,
|
||||
SnackbarErrorAction
|
||||
} from '@alfresco/aca-shared/store';
|
||||
import { ContentManagementService } from '../../../services/content-management.service';
|
||||
import { TranslationService } from '@alfresco/adf-core';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { AppExtensionService } from '@alfresco/aca-shared';
|
||||
import { SearchSortingDefinition } from '@alfresco/adf-content-services/lib/search/models/search-sorting-definition.interface';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-search-results',
|
||||
templateUrl: './search-results.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
styleUrls: ['./search-results.component.scss']
|
||||
})
|
||||
export class SearchResultsComponent extends PageComponent implements OnInit {
|
||||
showFacetFilter$: Observable<boolean>;
|
||||
infoDrawerPreview$: Observable<boolean>;
|
||||
|
||||
searchedWord: string;
|
||||
queryParamName = 'q';
|
||||
data: ResultSetPaging;
|
||||
sorting = ['name', 'asc'];
|
||||
isLoading = false;
|
||||
isSmallScreen = false;
|
||||
|
||||
constructor(
|
||||
private queryBuilder: SearchQueryBuilderService,
|
||||
private route: ActivatedRoute,
|
||||
store: Store<AppStore>,
|
||||
extensions: AppExtensionService,
|
||||
content: ContentManagementService,
|
||||
private translationService: TranslationService,
|
||||
private router: Router,
|
||||
private breakpointObserver: BreakpointObserver
|
||||
) {
|
||||
super(store, extensions, content);
|
||||
|
||||
queryBuilder.paging = {
|
||||
skipCount: 0,
|
||||
maxItems: 25
|
||||
};
|
||||
|
||||
this.showFacetFilter$ = store.select(showFacetFilter);
|
||||
this.infoDrawerPreview$ = store.select(infoDrawerPreview);
|
||||
combineLatest([this.route.params, this.queryBuilder.configUpdated])
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(([params, searchConfig]) => {
|
||||
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
|
||||
const query = this.formatSearchQuery(this.searchedWord, searchConfig['aca:fields']);
|
||||
if (query) {
|
||||
this.queryBuilder.userQuery = decodeURIComponent(query);
|
||||
}
|
||||
});
|
||||
|
||||
this.breakpointObserver
|
||||
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((result) => {
|
||||
this.isSmallScreen = result.matches;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.queryBuilder.resetToDefaults();
|
||||
this.sorting = this.getSorting();
|
||||
|
||||
this.subscriptions.push(
|
||||
this.queryBuilder.updated.subscribe((query) => {
|
||||
if (query) {
|
||||
this.sorting = this.getSorting();
|
||||
this.isLoading = true;
|
||||
}
|
||||
}),
|
||||
|
||||
this.queryBuilder.executed.subscribe((data) => {
|
||||
this.queryBuilder.paging.skipCount = 0;
|
||||
|
||||
this.onSearchResultLoaded(data);
|
||||
this.isLoading = false;
|
||||
}),
|
||||
|
||||
this.queryBuilder.error.subscribe((err: any) => {
|
||||
this.onSearchError(err);
|
||||
})
|
||||
);
|
||||
|
||||
if (this.route) {
|
||||
this.route.params.forEach((params: Params) => {
|
||||
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
|
||||
if (this.searchedWord) {
|
||||
this.queryBuilder.update();
|
||||
} else {
|
||||
this.queryBuilder.userQuery = null;
|
||||
this.queryBuilder.executed.next({
|
||||
list: { pagination: { totalItems: 0 }, entries: [] }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSearchError(error: { message: any }) {
|
||||
const { statusCode } = JSON.parse(error.message).error;
|
||||
|
||||
const messageKey = `APP.BROWSE.SEARCH.ERRORS.${statusCode}`;
|
||||
let translated = this.translationService.instant(messageKey);
|
||||
|
||||
if (translated === messageKey) {
|
||||
translated = this.translationService.instant(`APP.BROWSE.SEARCH.ERRORS.GENERIC`);
|
||||
}
|
||||
|
||||
this.store.dispatch(new SnackbarErrorAction(translated));
|
||||
}
|
||||
|
||||
private isOperator(input: string): boolean {
|
||||
if (input) {
|
||||
input = input.trim().toUpperCase();
|
||||
|
||||
const operators = ['AND', 'OR'];
|
||||
return operators.includes(input);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private formatFields(fields: string[], term: string): string {
|
||||
let prefix = '';
|
||||
let suffix = '*';
|
||||
|
||||
if (term.startsWith('=')) {
|
||||
prefix = '=';
|
||||
suffix = '';
|
||||
term = term.substring(1);
|
||||
}
|
||||
|
||||
return '(' + fields.map((field) => `${prefix}${field}:"${term}${suffix}"`).join(' OR ') + ')';
|
||||
}
|
||||
|
||||
formatSearchQuery(userInput: string, fields = ['cm:name']) {
|
||||
if (!userInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^http[s]?:\/\//.test(userInput)) {
|
||||
return this.formatFields(fields, userInput);
|
||||
}
|
||||
|
||||
userInput = userInput.trim();
|
||||
|
||||
if (userInput.includes(':') || userInput.includes('"')) {
|
||||
return userInput;
|
||||
}
|
||||
|
||||
const words = userInput.split(' ');
|
||||
|
||||
if (words.length > 1) {
|
||||
const separator = words.some(this.isOperator) ? ' ' : ' AND ';
|
||||
|
||||
return words
|
||||
.map((term) => {
|
||||
if (this.isOperator(term)) {
|
||||
return term;
|
||||
}
|
||||
|
||||
return this.formatFields(fields, term);
|
||||
})
|
||||
.join(separator);
|
||||
}
|
||||
|
||||
return this.formatFields(fields, userInput);
|
||||
}
|
||||
|
||||
onSearchResultLoaded(nodePaging: ResultSetPaging) {
|
||||
this.data = nodePaging;
|
||||
}
|
||||
|
||||
onPaginationChanged(pagination: Pagination) {
|
||||
this.queryBuilder.paging = {
|
||||
maxItems: pagination.maxItems,
|
||||
skipCount: pagination.skipCount
|
||||
};
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
|
||||
private getSorting(): string[] {
|
||||
const primary = this.queryBuilder.getPrimarySorting();
|
||||
|
||||
if (primary) {
|
||||
return [primary.key, primary.ascending ? 'asc' : 'desc'];
|
||||
}
|
||||
|
||||
return ['name', 'asc'];
|
||||
}
|
||||
|
||||
onNodeDoubleClick(node: MinimalNodeEntity) {
|
||||
if (node && node.entry) {
|
||||
if (node.entry.isFolder) {
|
||||
this.store.dispatch(new NavigateToFolder(node));
|
||||
return;
|
||||
}
|
||||
|
||||
this.showPreview(node, { location: this.router.url });
|
||||
}
|
||||
}
|
||||
|
||||
handleNodeClick(event: Event) {
|
||||
this.onNodeDoubleClick((event as CustomEvent).detail?.node);
|
||||
}
|
||||
|
||||
onPreviewClosed() {
|
||||
this.store.dispatch(new ShowInfoDrawerPreviewAction());
|
||||
}
|
||||
|
||||
onDrawerClosed() {
|
||||
this.store.dispatch(new SetInfoDrawerPreviewStateAction(false));
|
||||
this.store.dispatch(new SetInfoDrawerStateAction(false));
|
||||
}
|
||||
|
||||
onSearchSortingUpdate(option: SearchSortingDefinition) {
|
||||
this.queryBuilder.sorting = [{ ...option, ascending: option.ascending }];
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user