[ADF-5417] Support multiple search configuration in app config json (#7096)

* [ACS-1642] Support multiple search configuration in app config json

* [ci:force] unit test added

* adf component added

* [ci:force] check compatibility

* [ci:force] * run all e2e

* * revert initvalue

* [ci:force] revert app config

* [ci:force] docs update
This commit is contained in:
Dharan
2021-06-15 18:22:46 +05:30
committed by GitHub
parent 1cffef9421
commit ba03c60adb
55 changed files with 909 additions and 475 deletions

View File

@@ -349,5 +349,8 @@
"PROCESS_ID": "Process Instance ID",
"ACTION_TYPE": "Action Type"
}
},
"SEARCH_FORMS": {
"ALL": "All"
}
}

View File

@@ -34,7 +34,7 @@
"copyright": "© 2016 - 2021 Alfresco Software, Inc. All Rights Reserved."
},
"notifications": true,
"search": {
"search": [{
"filterWithContains": true,
"app:fields": [
"cm:name",
@@ -365,8 +365,10 @@
"postfix": ")"
}
]
}
},
"name": "SEARCH_FORMS.ALL",
"default": true
}],
"search-headers": {
"filterWithContains": true,
"app:fields": [

View File

@@ -9,6 +9,8 @@
<mat-progress-bar *ngIf="isLoading" color="primary" mode="indeterminate"></mat-progress-bar>
<div class="app-search-results__sorting">
<adf-search-sorting-picker></adf-search-sorting-picker>
<adf-search-form (formChange)="onFormChange($event)"></adf-search-form>
</div>
<app-files-component
[showHeader]="false"

View File

@@ -16,7 +16,9 @@
}
&__sorting {
text-align: right;
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
}

View File

@@ -15,12 +15,12 @@
* limitations under the License.
*/
import { Component, OnInit, Optional, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Pagination, ResultSetPaging } from '@alfresco/js-api';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { UserPreferencesService, SearchService, AppConfigService } from '@alfresco/adf-core';
import { Subject } from 'rxjs';
import { SearchForm, SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { SearchService, UserPreferencesService } from '@alfresco/adf-core';
import { combineLatest, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
@@ -38,14 +38,24 @@ export class SearchResultComponent implements OnInit, OnDestroy {
isLoading = true;
sorting = ['name', 'asc'];
searchForms: SearchForm[];
private onDestroy$ = new Subject<boolean>();
constructor(public router: Router,
private config: AppConfigService,
private preferences: UserPreferencesService,
private queryBuilder: SearchQueryBuilderService,
@Optional() private route: ActivatedRoute) {
private route: ActivatedRoute) {
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['app:fields']);
if (query) {
this.queryBuilder.userQuery = query;
}
});
queryBuilder.paging = {
maxItems: this.preferences.paginationSize,
skipCount: 0
@@ -76,10 +86,7 @@ export class SearchResultComponent implements OnInit, OnDestroy {
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) {
this.queryBuilder.userQuery = query;
if (this.searchedWord) {
this.queryBuilder.update();
} else {
this.queryBuilder.userQuery = null;
@@ -94,15 +101,11 @@ export class SearchResultComponent implements OnInit, OnDestroy {
}
}
private formatSearchQuery(userInput: string) {
private formatSearchQuery(userInput: string, fields = ['cm:name']) {
if (!userInput) {
return null;
}
const fields = this.config.get<string[]>('search.app:fields', ['cm:name']);
const query = fields.map((field) => `${field}:"${userInput}*"`).join(' OR ');
return query;
return fields.map((field) => `${field}:"${userInput}*"`).join(' OR ');
}
ngOnDestroy() {
@@ -136,4 +139,8 @@ export class SearchResultComponent implements OnInit, OnDestroy {
return ['name', 'asc'];
}
onFormChange(form: SearchForm) {
this.queryBuilder.updateSelectedConfiguration(form.index);
}
}

View File

@@ -0,0 +1,75 @@
---
Title: Search Form component
Added: v4.5.0
Status: Active
Last reviewed: 2021-06-11
---
# [Search Form component](../../../lib/content-services/src/lib/search/components/search-form/search-form.component.ts "Defined in search-form.component.ts")
Selecting a configuration from a set of configured options.
![Search Form screenshot](../../docassets/images/search-form-component.png)
## Basic usage
```json
{
"search": [
{
"categories": [
{
"id": "queryName",
"name": "Name",
"enabled": true,
"expanded": true,
"component": {
"selector": "text",
"settings": {
"searchPrefix": "",
"searchSuffix": "",
"pattern": "cm:name:'(.*?)'",
"field": "cm:name",
"placeholder": "Enter the name",
"allowUpdateOnChange": true
}
}
}
],
"name": "ALL",
"default": true
},
{
"categories": [
{
"id": "queryName",
"name": "Name",
"enabled": true,
"expanded": true,
"component": {
"selector": "text",
"settings": {
"searchPrefix": "",
"searchSuffix": "",
"pattern": "cm:name:'(.*?)'",
"field": "cm:name",
"placeholder": "Enter the name",
"allowUpdateOnChange": true
}
}
}
],
"name": "Other"
}
]
}
```
## Details
This component lets the user pick a configuration for a search.
## See also
- [Search Query Builder](../services/search-query-builder.service.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -16,7 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { SearchCategory } from '../search/search-category.interface';
import { SearchCategory } from '../search/models/search-category.interface';
@Injectable({
providedIn: 'root'

View File

@@ -57,7 +57,7 @@ import { ContentActionModel } from './../models/content-action.model';
import { PermissionStyleModel } from './../models/permissions-style.model';
import { NodeEntityEvent, NodeEntryEvent } from './node.event';
import { NavigableComponentInterface } from '../../breadcrumb/navigable-component.interface';
import { FilterSearch } from './../../search/filter-search.interface';
import { FilterSearch } from './../../search/models/filter-search.interface';
import { RowFilter } from '../data/row-filter.model';
import { DocumentListService } from '../services/document-list.service';
import { DocumentLoaderNode } from '../models/document-folder.model';

View File

@@ -20,7 +20,7 @@ import { PaginationModel, DataSorting } from '@alfresco/adf-core';
import { DocumentListComponent } from '../document-list.component';
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../../search/search-query-service.token';
import { SearchHeaderQueryBuilderService } from '../../../search/search-header-query-builder.service';
import { FilterSearch } from './../../../search/filter-search.interface';
import { FilterSearch } from './../../../search/models/filter-search.interface';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { NodePaging, MinimalNode } from '@alfresco/js-api';

View File

@@ -290,6 +290,8 @@
}
}
},
"FORMS": "Search Forms",
"UNKNOWN_FORM": "Unknown Configuration",
"SEARCH_HEADER" : {
"TITLE":"Filter",
"TYPE": "Type",

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { SearchCategory } from '../search/search-category.interface';
import { SearchCategory } from '../search/models/search-category.interface';
export const mockContentModelTextProperty = {
name: 'name',

View File

@@ -27,24 +27,31 @@ import {
RequestHighlight,
RequestScope
} from '@alfresco/js-api';
import { SearchCategory } from './search-category.interface';
import { FilterQuery } from './filter-query.interface';
import { SearchRange } from './search-range.interface';
import { SearchConfiguration } from './search-configuration.interface';
import { FacetQuery } from './facet-query.interface';
import { SearchSortingDefinition } from './search-sorting-definition.interface';
import { FacetField } from './facet-field.interface';
import { FacetFieldBucket } from './facet-field-bucket.interface';
import { SearchCategory } from './models/search-category.interface';
import { FilterQuery } from './models/filter-query.interface';
import { SearchRange } from './models/search-range.interface';
import { SearchConfiguration } from './models/search-configuration.interface';
import { FacetQuery } from './models/facet-query.interface';
import { SearchSortingDefinition } from './models/search-sorting-definition.interface';
import { FacetField } from './models/facet-field.interface';
import { FacetFieldBucket } from './models/facet-field-bucket.interface';
import { SearchForm } from './models/search-form.interface';
@Injectable({
providedIn: 'root'
})
export abstract class BaseQueryBuilderService {
private _userQuery = '';
/* Stream that emits the search configuration whenever the user change the search forms */
configUpdated = new Subject<SearchConfiguration>();
/* Stream that emits the query before search whenever user search */
updated = new Subject<QueryBody>();
/* Stream that emits the results whenever user search */
executed = new Subject<ResultSetPaging>();
/* Stream that emits the error whenever user search */
error = new Subject();
categories: SearchCategory[] = [];
@@ -54,6 +61,8 @@ export abstract class BaseQueryBuilderService {
sorting: SearchSortingDefinition[] = [];
sortingOptions: SearchSortingDefinition[] = [];
private scope: RequestScope;
private selectedConfiguration: number;
private _userQuery = '';
protected userFacetBuckets: { [key: string]: FacetFieldBucket[] } = {};
@@ -77,15 +86,62 @@ export abstract class BaseQueryBuilderService {
this.resetToDefaults();
}
public abstract loadConfiguration(): SearchConfiguration;
public abstract loadConfiguration(): SearchConfiguration | SearchConfiguration[];
public abstract isFilterServiceActive(): boolean;
public resetToDefaults() {
const currentConfig = this.loadConfiguration();
const currentConfig = this.getDefaultConfiguration();
this.configUpdated.next(currentConfig);
this.setUpSearchConfiguration(currentConfig);
}
public getDefaultConfiguration(): SearchConfiguration | undefined {
const configurations = this.loadConfiguration();
if (this.selectedConfiguration >= 0) {
return configurations[this.selectedConfiguration];
}
if (Array.isArray(configurations)) {
return configurations.find((configuration) => configuration.default);
}
return configurations;
}
public updateSelectedConfiguration(index: number): void {
const currentConfig = this.loadConfiguration();
if (Array.isArray(currentConfig) && currentConfig[index] !== undefined) {
this.configUpdated.next(currentConfig[index]);
this.selectedConfiguration = index;
this.resetSearchOptions();
this.setUpSearchConfiguration(currentConfig[index]);
this.update();
}
}
private resetSearchOptions(): void {
this.categories = [];
this.queryFragments = {};
this.filterQueries = [];
this.sorting = [];
this.sortingOptions = [];
this.scope = null;
}
public getSearchConfigurationDetails(): SearchForm[] {
const configurations = this.loadConfiguration();
if (Array.isArray(configurations)) {
return configurations.map((configuration, index) => ({
index,
name: configuration.name || 'SEARCH.UNKNOWN_FORM',
default: configuration.default || false,
selected: this.selectedConfiguration !== undefined ? index === this.selectedConfiguration : configuration.default
}));
}
return [];
}
private setUpSearchConfiguration(currentConfiguration: SearchConfiguration) {
if (currentConfiguration) {
this.config = JSON.parse(JSON.stringify(currentConfiguration));

View File

@@ -16,7 +16,7 @@
*/
import { SearchCheckListComponent, SearchListOption } from './search-check-list.component';
import { SearchFilterList } from '../search-filter/models/search-filter-list.model';
import { SearchFilterList } from '../../models/search-filter-list.model';
import { setupTestBed } from '@alfresco/adf-core';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { ComponentFixture, TestBed } from '@angular/core/testing';

View File

@@ -17,10 +17,10 @@
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { SearchFilterList } from '../search-filter/models/search-filter-list.model';
import { SearchFilterList } from '../../models/search-filter-list.model';
export interface SearchListOption {
name: string;

View File

@@ -20,8 +20,8 @@ import { FormControl, Validators, FormGroup } from '@angular/forms';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MomentDateAdapter, MOMENT_DATE_FORMATS, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
import { Moment } from 'moment';

View File

@@ -19,8 +19,8 @@ import { OnInit, Component, ViewEncapsulation, OnDestroy } from '@angular/core';
import { FormControl, Validators, FormGroup } from '@angular/forms';
import { UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
import { Moment } from 'moment';

View File

@@ -25,7 +25,7 @@ import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
import { By } from '@angular/platform-browser';
import { SearchFilterContainerComponent } from './search-filter-container.component';
import { MatMenuTrigger } from '@angular/material/menu';
import { SearchCategory } from '../../search-category.interface';
import { SearchCategory } from '../../models/search-category.interface';
const mockCategory: SearchCategory = {
'id': 'queryName',

View File

@@ -31,7 +31,7 @@ import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cd
import { DataColumn, TranslationService } from '@alfresco/adf-core';
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
import { SearchCategory } from '../../search-category.interface';
import { SearchCategory } from '../../models/search-category.interface';
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
import { Subject } from 'rxjs';
import { MatMenuTrigger } from '@angular/material/menu';

View File

@@ -19,9 +19,9 @@ import { SearchFilterComponent } from './search-filter.component';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { AppConfigService, SearchService, setupTestBed, TranslationService } from '@alfresco/adf-core';
import { Subject } from 'rxjs';
import { FacetFieldBucket } from '../../facet-field-bucket.interface';
import { FacetField } from '../../facet-field.interface';
import { SearchFilterList } from './models/search-filter-list.model';
import { FacetFieldBucket } from '../../models/facet-field-bucket.interface';
import { FacetField } from '../../models/facet-field.interface';
import { SearchFilterList } from '../../models/search-filter-list.model';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ContentTestingModule } from '../../../testing/content.testing.module';

View File

@@ -19,9 +19,9 @@ import { Component, ViewEncapsulation, OnInit, OnDestroy, Inject, Input } from '
import { MatCheckboxChange } from '@angular/material/checkbox';
import { TranslationService, SearchService } from '@alfresco/adf-core';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { FacetFieldBucket } from '../../facet-field-bucket.interface';
import { FacetField } from '../../facet-field.interface';
import { SearchFilterList } from './models/search-filter-list.model';
import { FacetFieldBucket } from '../../models/facet-field-bucket.interface';
import { FacetField } from '../../models/facet-field.interface';
import { SearchFilterList } from '../../models/search-filter-list.model';
import { takeUntil } from 'rxjs/operators';
import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api';
import { Subject } from 'rxjs';

View File

@@ -0,0 +1,8 @@
<mat-form-field floatLabel="always" *ngIf="searchForms.length">
<mat-label>{{ 'SEARCH.FORMS' | translate }}</mat-label>
<mat-select [(value)]="selected" (selectionChange)="onSelectionChange($event.value)">
<mat-option *ngFor="let form of searchForms" [value]="form.index">
{{form.name | translate}}
</mat-option>
</mat-select>
</mat-form-field>

View File

@@ -0,0 +1,91 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchFormComponent } from './search-form.component';
import { setupTestBed } from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
import { SearchForm } from '../../models/search-form.interface';
import { By } from '@angular/platform-browser';
describe('SearchFormComponent', () => {
let fixture: ComponentFixture<SearchFormComponent>;
let component: SearchFormComponent;
let queryBuilder: SearchHeaderQueryBuilderService;
const mockSearchForms: SearchForm[] = [
{ default: false, index: 0, name: 'All', selected: false },
{ default: true, index: 1, name: 'First', selected: true },
{ default: false, index: 2, name: 'Second', selected: false }
];
setupTestBed({
imports: [
TranslateModule.forRoot(),
ContentTestingModule
],
providers: [
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchHeaderQueryBuilderService }
]
});
beforeEach(() => {
fixture = TestBed.createComponent(SearchFormComponent);
component = fixture.componentInstance;
queryBuilder = TestBed.inject<SearchHeaderQueryBuilderService>(SEARCH_QUERY_SERVICE_TOKEN);
spyOn(queryBuilder, 'getSearchConfigurationDetails').and.returnValue(mockSearchForms);
fixture.detectChanges();
});
it('should show search forms', async () => {
await fixture.whenStable();
fixture.detectChanges();
expect(component.selected).toBe(1);
const label = fixture.debugElement.query(By.css('.mat-form-field mat-label'));
expect(label.nativeElement.innerText).toContain('SEARCH.FORMS');
const selectValue = fixture.debugElement.query(By.css('.mat-select-value'));
expect(selectValue.nativeElement.innerText).toContain('First');
});
it('should emit on form change', async (done) => {
component.formChange.subscribe((form) => {
expect(form).toEqual(mockSearchForms[2]);
done();
});
await fixture.whenStable();
fixture.detectChanges();
const matSelect = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
matSelect.click();
fixture.detectChanges();
const matOption = fixture.debugElement.queryAll(By.css('.mat-option'))[2].nativeElement;
matOption.click();
});
it('should not display search form if no form configured', async () => {
component.searchForms = [];
await fixture.whenStable();
fixture.detectChanges();
const field = fixture.debugElement.query(By.css('.mat-form-field'));
expect(field).toEqual(null, 'search form displayed for empty configuration');
});
});

View File

@@ -0,0 +1,44 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { SearchForm } from '../../models/search-form.interface';
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
@Component({
selector: 'adf-search-form',
templateUrl: './search-form.component.html'
})
export class SearchFormComponent implements OnInit {
@Output()
formChange: EventEmitter<SearchForm> = new EventEmitter<SearchForm>();
selected: number;
searchForms: SearchForm[] = [];
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private queryBuilder: SearchQueryBuilderService) {}
ngOnInit(): void {
this.searchForms = this.queryBuilder.getSearchConfigurationDetails();
this.selected = this.searchForms.find(form => form.selected)?.index;
}
onSelectionChange(index: number) {
this.formChange.emit(this.searchForms[index]);
}
}

View File

@@ -17,8 +17,8 @@
import { OnInit, Component, ViewEncapsulation } from '@angular/core';
import { FormControl, Validators, FormGroup } from '@angular/forms';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';

View File

@@ -16,7 +16,7 @@
*/
import { SearchCheckListComponent, SearchListOption } from '../search-check-list/search-check-list.component';
import { SearchFilterList } from '../search-filter/models/search-filter-list.model';
import { SearchFilterList } from '../../models/search-filter-list.model';
import { setupTestBed } from '@alfresco/adf-core';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { ComponentFixture, TestBed } from '@angular/core/testing';

View File

@@ -91,8 +91,8 @@ describe('SearchRadioComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const optionElements = fixture.debugElement.query(By.css('mat-radio-button'));
optionElements.triggerEventHandler('change', { checked: true });
const optionElements = fixture.debugElement.query(By.css('mat-radio-group'));
optionElements.triggerEventHandler('change', { value: sizeOptions[0].value });
fixture.detectChanges();
expect(component.context.update).toHaveBeenCalled();

View File

@@ -18,10 +18,10 @@
import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
import { MatRadioChange } from '@angular/material/radio';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { SearchFilterList } from '../search-filter/models/search-filter-list.model';
import { SearchFilterList } from '../../models/search-filter-list.model';
export interface SearchRadioOption {
name: string;
@@ -67,9 +67,11 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
const initialValue = this.getSelectedValue();
if (initialValue !== null) {
this.setValue(initialValue);
this.value = initialValue;
this.context.queryFragments[this.id] = initialValue;
} else if (this.startValue !== null) {
this.setValue(initialValue);
this.value = initialValue;
this.context.queryFragments[this.id] = initialValue;
}
}

View File

@@ -16,8 +16,8 @@
*/
import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { MatSliderChange } from '@angular/material/slider';

View File

@@ -18,7 +18,7 @@
import { SearchSortingPickerComponent } from './search-sorting-picker.component';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { AppConfigService } from '@alfresco/adf-core';
import { SearchConfiguration } from '../../search-configuration.interface';
import { SearchConfiguration } from '../../models/search-configuration.interface';
import { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../../testing/content.testing.module';

View File

@@ -17,7 +17,7 @@
import { Component, OnInit, ViewEncapsulation, Inject } from '@angular/core';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { SearchSortingDefinition } from '../../search-sorting-definition.interface';
import { SearchSortingDefinition } from '../../models/search-sorting-definition.interface';
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
@Component({

View File

@@ -16,8 +16,8 @@
*/
import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
@Component({

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { SearchFilterList } from './components/search-filter/models/search-filter-list.model';
import { SearchFilterList } from './search-filter-list.model';
import { FacetFieldBucket } from './facet-field-bucket.interface';
export interface FacetField {

View File

@@ -49,4 +49,6 @@ export interface SearchConfiguration {
defaults: SearchSortingDefinition[];
};
highlight?: RequestHighlight;
name?: string;
default?: boolean;
}

View File

@@ -0,0 +1,23 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface SearchForm {
index: number;
name: string;
default: boolean;
selected: boolean;
}

View File

@@ -16,7 +16,7 @@
*/
import { SearchWidgetSettings } from './search-widget-settings.interface';
import { SearchQueryBuilderService } from './search-query-builder.service';
import { SearchQueryBuilderService } from '../search-query-builder.service';
export interface SearchWidget {
id: string;

View File

@@ -15,17 +15,19 @@
* limitations under the License.
*/
export * from './facet-field-bucket.interface';
export * from './facet-field.interface';
export * from './facet-query.interface';
export * from './filter-query.interface';
export * from './filter-search.interface';
export * from './search-category.interface';
export * from './search-widget-settings.interface';
export * from './search-widget.interface';
export * from './search-configuration.interface';
export * from './models/facet-field-bucket.interface';
export * from './models/facet-field.interface';
export * from './models/facet-query.interface';
export * from './models/filter-query.interface';
export * from './models/filter-search.interface';
export * from './models/search-category.interface';
export * from './models/search-widget-settings.interface';
export * from './models/search-widget.interface';
export * from './models/search-configuration.interface';
export * from './search-query-builder.service';
export * from './search-range.interface';
export * from './models/search-range.interface';
export * from './models/search-form.interface';
export * from './search-query-service.token';
export * from './search-header-query-builder.service';
@@ -49,5 +51,6 @@ export * from './components/search-sorting-picker/search-sorting-picker.componen
export * from './components/search-text/search-text.component';
export * from './components/search-widget-container/search-widget-container.component';
export * from './components/search-datetime-range/search-datetime-range.component';
export * from './components/search-form/search-form.component';
export * from './search.module';

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { SearchConfiguration } from './search-configuration.interface';
import { SearchConfiguration } from './models/search-configuration.interface';
import { AppConfigService } from '@alfresco/adf-core';
import { SearchHeaderQueryBuilderService } from './search-header-query-builder.service';
import { TestBed } from '@angular/core/testing';

View File

@@ -17,14 +17,14 @@
import { Injectable } from '@angular/core';
import { AlfrescoApiService, AppConfigService, NodesApiService, DataSorting } from '@alfresco/adf-core';
import { SearchConfiguration } from './search-configuration.interface';
import { SearchConfiguration } from './models/search-configuration.interface';
import { BaseQueryBuilderService } from './base-query-builder.service';
import { SearchCategory } from './search-category.interface';
import { SearchCategory } from './models/search-category.interface';
import { MinimalNode, QueryBody } from '@alfresco/js-api';
import { filter } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { SearchSortingDefinition } from './search-sorting-definition.interface';
import { FilterSearch } from './filter-search.interface';
import { SearchSortingDefinition } from './models/search-sorting-definition.interface';
import { FilterSearch } from './models/filter-search.interface';
@Injectable({
providedIn: 'root'

View File

@@ -16,9 +16,9 @@
*/
import { SearchQueryBuilderService } from './search-query-builder.service';
import { SearchConfiguration } from './search-configuration.interface';
import { SearchConfiguration } from './models/search-configuration.interface';
import { AppConfigService } from '@alfresco/adf-core';
import { FacetField } from './facet-field.interface';
import { FacetField } from './models/facet-field.interface';
import { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../testing/content.testing.module';
@@ -669,4 +669,91 @@ describe('SearchQueryBuilder', () => {
expect(queryBody.scope).toEqual(mockScope);
});
it('should return empty if array of search config not found', () => {
const builder = new SearchQueryBuilderService(buildConfig({}), null);
const forms = builder.getSearchConfigurationDetails();
expect(forms).toEqual([]);
});
describe('Multiple search configuration', () => {
let configs: SearchConfiguration[];
let builder: SearchQueryBuilderService;
beforeEach(() => {
configs = [
{
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: true }
],
filterQueries: [
{ query: 'query1' },
{ query: 'query2' }
],
name: 'config1',
default: true
},
{
categories: [
<any> { id: 'mouse', enabled: true }
],
filterQueries: [
{ query: 'query1' },
{ query: 'query2' }
],
name: 'config2',
default: false
},
{
categories: [
<any> { id: 'cat_and_mouse', enabled: true }
],
default: false
}
];
builder = new SearchQueryBuilderService(buildConfig(configs), null);
});
it('should pick the default configuration from list', () => {
builder.categories = [];
builder.filterQueries = [];
expect(builder.categories.length).toBe(0);
expect(builder.filterQueries.length).toBe(0);
builder.resetToDefaults();
expect(builder.categories.length).toBe(2);
expect(builder.categories.length).toBe(2);
expect(builder.filterQueries.length).toBe(2);
});
it('should list available search form names', () => {
const forms = builder.getSearchConfigurationDetails();
expect(forms).toEqual([
{ index: 0, name: 'config1', default: true, selected: true },
{ index: 1, name: 'config2', default: false, selected: false },
{ index: 2, name: 'SEARCH.UNKNOWN_FORM', default: false, selected: false }
]);
});
it('should allow the user switch the form', () => {
builder.updateSelectedConfiguration(1);
expect(builder.categories.length).toBe(1);
expect(builder.filterQueries.length).toBe(2);
});
it('should keep the selected configuration value', () => {
builder.updateSelectedConfiguration(1);
const forms = builder.getSearchConfigurationDetails();
expect(forms).toEqual([
{ index: 0, name: 'config1', default: true, selected: false },
{ index: 1, name: 'config2', default: false, selected: true },
{ index: 2, name: 'SEARCH.UNKNOWN_FORM', default: false, selected: false }
]);
});
});
});

View File

@@ -17,7 +17,7 @@
import { Injectable } from '@angular/core';
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
import { SearchConfiguration } from './search-configuration.interface';
import { SearchConfiguration } from './models/search-configuration.interface';
import { BaseQueryBuilderService } from './base-query-builder.service';
@Injectable()

View File

@@ -40,6 +40,7 @@ import { SEARCH_QUERY_SERVICE_TOKEN } from './search-query-service.token';
import { SearchQueryBuilderService } from './search-query-builder.service';
import { SearchFilterContainerComponent } from './components/search-filter-container/search-filter-container.component';
import { SearchDatetimeRangeComponent } from './components/search-datetime-range/search-datetime-range.component';
import { SearchFormComponent } from './components/search-form/search-form.component';
@NgModule({
imports: [
@@ -65,7 +66,8 @@ import { SearchDatetimeRangeComponent } from './components/search-datetime-range
SearchDateRangeComponent,
SearchDatetimeRangeComponent,
SearchSortingPickerComponent,
SearchFilterContainerComponent
SearchFilterContainerComponent,
SearchFormComponent
],
exports: [
SearchComponent,
@@ -83,7 +85,8 @@ import { SearchDatetimeRangeComponent } from './components/search-datetime-range
SearchDateRangeComponent,
SearchDatetimeRangeComponent,
SearchSortingPickerComponent,
SearchFilterContainerComponent
SearchFilterContainerComponent,
SearchFormComponent
],
providers: [
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService },

View File

@@ -498,6 +498,404 @@
}
}
]
},
"search-configuration": {
"description": "Search configuration parameters",
"type": "object",
"required": [
"categories"
],
"properties": {
"include": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"fields": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"filterQueries": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"query"
],
"properties": {
"query": {
"type": "string"
}
}
}
},
"filterWithContains": {
"type": "boolean"
},
"resetButton": {
"type": "boolean"
},
"facetFields": {
"type": "object",
"required": [
"fields"
],
"properties": {
"expanded": {
"description": "Toggles expanded state of the facet field",
"type": "boolean"
},
"fields": {
"description": "List of custom facet fields",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"field",
"mincount",
"label"
],
"properties": {
"field": {
"type": "string",
"description": "This specifies the facet field."
},
"mincount": {
"type": "number",
"description": "This specifies the minimum count required for a facet field to be included in the response. The default value is 1."
},
"label": {
"type": "string",
"description": "This specifies the label to include in place of the facet field."
},
"prefix": {
"type": "string",
"description": "This restricts the possible constraints to only indexed values with a specified prefix."
},
"limit": {
"type": "number",
"description": "Maximum number of results"
},
"pageSize": {
"type": "number",
"description": "Display page size"
},
"offset": {
"type": "integer"
}
}
}
}
}
},
"facetIntervals": {
"type": "object",
"required": [
"intervals"
],
"properties": {
"intervals": {
"description": "List of facet intervals",
"type": "array",
"items": {
"type": "object",
"required": [
"label",
"field",
"sets"
],
"properties": {
"label": {
"description": "This specifies the label to use to identify the field facet.",
"type": "string"
},
"field": {
"description": "This specifies the field to facet on.",
"type": "string"
},
"sets": {
"type": "array",
"items": {
"type": "object",
"required": [
"label",
"start",
"end"
],
"properties": {
"label": {
"description": "This specifies the label to use to identify the set.",
"type": "string"
},
"start": {
"description": "This specifies the start of the range.",
"type": "string"
},
"end": {
"description": "This specifies the end of the range.",
"type": "string"
},
"startInclusive": {
"description": "When true, the set will include values greater or equal to 'start'. The default value is true.",
"type": "boolean"
},
"endInclusive": {
"description": "When true, the set will include values less than or equal to 'end'. The default value is true.",
"type": "boolean"
}
}
}
},
"pageSize": {
"type": "number",
"description": "Display page size"
},
"mincount": {
"type": "number",
"description": "This specifies the minimum count required for a facet interval to be displayed. The default value is 1."
}
}
}
},
"expanded": {
"description": "Toggles expanded state of the facet intervals",
"type": "boolean"
}
}
},
"facetQueries": {
"type": "object",
"required": [
"label",
"queries"
],
"properties": {
"label": {
"description": "Label text for the default facet queries group",
"type": "string"
},
"pageSize": {
"description": "Default page size for the facet queries groups",
"type": "number"
},
"expanded": {
"description": "Toggles expanded state of the facet queries groups",
"type": "boolean"
},
"mincount": {
"description": "This specifies the minimum count required for a facet query to be displayed. The default value is 1.",
"type": "number"
},
"queries": {
"description": "List of custom facet queries",
"type": "array",
"items": {
"type": "object",
"required": [
"query",
"label"
],
"properties": {
"query": {
"type": "string"
},
"label": {
"description": "Unique identifier for the query",
"type": "string"
},
"group": {
"description": "The group that the facet query belongs to. If no group is defined, the facet query will appear under the default facet queries group label",
"type": "string"
}
}
}
}
}
},
"categories": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {},
"name": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"expanded": {
"type": "boolean"
},
"component": {
"type": "object",
"required": [
"selector",
"settings"
],
"properties": {
"selector": {
"description": "Unique component runtime identifier",
"type": "string"
},
"settings": {
"description": "Component-specific settings",
"type": "object"
}
}
}
}
}
},
"highlight": {
"type": "object",
"description": " Request that highlight fragments to be added to result set rows The properties reflect SOLR highlighting parameters.",
"properties": {
"prefix": {
"description": "The string used to mark the start of a highlight in a fragment",
"type": "string"
},
"postfix": {
"description": "The string used to mark the end of a highlight in a fragment",
"type": "string"
},
"snippetCount": {
"description": "The maximum number of distinct highlight snippets to return for each highlight field",
"type": "number"
},
"fragmentSize": {
"description": "The character length of each snippet",
"type": "number"
},
"maxAnalyzedChars": {
"description": "The number of characters to be considered for highlighting. Matches after this count will not be shown",
"type": "number"
},
"mergeContiguous": {
"description": "If fragments over lap they can be merged into one larger fragment",
"type": "boolean"
},
"usePhraseHighlighter": {
"description": "Should phrases be identified",
"type": "boolean"
},
"fields": {
"type": "array",
"minItems": 1,
"items": {
"description": "The fields to highlight and field specific configuration properties for each field",
"type": "object",
"properties": {
"field": {
"description": "The name of the field to highlight",
"type": "string"
},
"snippetCount": {
"type": "number"
},
"fragmentSize": {
"type": "number"
},
"mergeContiguous": {
"type": "boolean"
},
"prefix": {
"type": "string"
},
"postfix": {
"type": "string"
}
}
}
}
}
},
"sorting": {
"type" : "object",
"description": "Sorting options and defaults",
"required": [
"options"
],
"properties": {
"options": {
"type": "array",
"minItems": 1,
"items": {
"description": "Sorting options available for users to choose from",
"type": "object",
"required": [
"key",
"label",
"type",
"field",
"ascending"
],
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
},
"type": {
"type": "string"
},
"field": {
"type": "string"
},
"ascending": {
"type": "boolean"
}
}
}
},
"defaults": {
"description": "Predefined sorting to execute by default",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
},
"type": {
"type": "string"
},
"field": {
"type": "string"
},
"ascending": {
"type": "boolean"
}
}
}
}
}
},
"name": {
"description": "The name of search configuration",
"type": "string"
},
"default": {
"description": "Apply current search configuration by default",
"type": "boolean"
}
}
}
},
"type": "object",
@@ -1123,394 +1521,19 @@
}
},
"search": {
"description": "Search configuration parameters",
"type": "object",
"required": [
"categories"
],
"properties": {
"include": {
"anyOf": [
{
"description": "Search configuration",
"$ref": "#/definitions/search-configuration"
},
{
"type": "array",
"minItems": 1,
"description": "Multiple search configuration",
"items": {
"type": "string"
}
},
"fields": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"filterQueries": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"query"
],
"properties": {
"query": {
"type": "string"
}
}
}
},
"filterWithContains": {
"type": "boolean"
},
"resetButton": {
"type": "boolean"
},
"facetFields": {
"type": "object",
"required": [
"fields"
],
"properties": {
"expanded": {
"description": "Toggles expanded state of the facet field",
"type": "boolean"
},
"fields": {
"description": "List of custom facet fields",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"field",
"mincount",
"label"
],
"properties": {
"field": {
"type": "string",
"description": "This specifies the facet field."
},
"mincount": {
"type": "number",
"description": "This specifies the minimum count required for a facet field to be included in the response. The default value is 1."
},
"label": {
"type": "string",
"description": "This specifies the label to include in place of the facet field."
},
"prefix": {
"type": "string",
"description": "This restricts the possible constraints to only indexed values with a specified prefix."
},
"limit": {
"type": "number",
"description": "Maximum number of results"
},
"pageSize": {
"type": "number",
"description": "Display page size"
},
"offset": {
"type": "integer"
}
}
}
}
}
},
"facetIntervals": {
"type": "object",
"required": [
"intervals"
],
"properties": {
"intervals": {
"description": "List of facet intervals",
"type": "array",
"items": {
"type": "object",
"required": [
"label",
"field",
"sets"
],
"properties": {
"label": {
"description": "This specifies the label to use to identify the field facet.",
"type": "string"
},
"field": {
"description": "This specifies the field to facet on.",
"type": "string"
},
"sets": {
"type": "array",
"items": {
"type": "object",
"required": [
"label",
"start",
"end"
],
"properties": {
"label": {
"description": "This specifies the label to use to identify the set.",
"type": "string"
},
"start": {
"description": "This specifies the start of the range.",
"type": "string"
},
"end": {
"description": "This specifies the end of the range.",
"type": "string"
},
"startInclusive": {
"description": "When true, the set will include values greater or equal to 'start'. The default value is true.",
"type": "boolean"
},
"endInclusive": {
"description": "When true, the set will include values less than or equal to 'end'. The default value is true.",
"type": "boolean"
}
}
}
},
"pageSize": {
"type": "number",
"description": "Display page size"
},
"mincount": {
"type": "number",
"description": "This specifies the minimum count required for a facet interval to be displayed. The default value is 1."
}
}
}
},
"expanded": {
"description": "Toggles expanded state of the facet intervals",
"type": "boolean"
}
}
},
"facetQueries": {
"type": "object",
"required": [
"label",
"queries"
],
"properties": {
"label": {
"description": "Label text for the default facet queries group",
"type": "string"
},
"pageSize": {
"description": "Default page size for the facet queries groups",
"type": "number"
},
"expanded": {
"description": "Toggles expanded state of the facet queries groups",
"type": "boolean"
},
"mincount": {
"description": "This specifies the minimum count required for a facet query to be displayed. The default value is 1.",
"type": "number"
},
"queries": {
"description": "List of custom facet queries",
"type": "array",
"items": {
"type": "object",
"required": [
"query",
"label"
],
"properties": {
"query": {
"type": "string"
},
"label": {
"description": "Unique identifier for the query",
"type": "string"
},
"group": {
"description": "The group that the facet query belongs to. If no group is defined, the facet query will appear under the default facet queries group label",
"type": "string"
}
}
}
}
}
},
"categories": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {},
"name": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"expanded": {
"type": "boolean"
},
"component": {
"type": "object",
"required": [
"selector",
"settings"
],
"properties": {
"selector": {
"description": "Unique component runtime identifier",
"type": "string"
},
"settings": {
"description": "Component-specific settings",
"type": "object"
}
}
}
}
}
},
"highlight": {
"type": "object",
"description": " Request that highlight fragments to be added to result set rows The properties reflect SOLR highlighting parameters.",
"properties": {
"prefix": {
"description": "The string used to mark the start of a highlight in a fragment",
"type": "string"
},
"postfix": {
"description": "The string used to mark the end of a highlight in a fragment",
"type": "string"
},
"snippetCount": {
"description": "The maximum number of distinct highlight snippets to return for each highlight field",
"type": "number"
},
"fragmentSize": {
"description": "The character length of each snippet",
"type": "number"
},
"maxAnalyzedChars": {
"description": "The number of characters to be considered for highlighting. Matches after this count will not be shown",
"type": "number"
},
"mergeContiguous": {
"description": "If fragments over lap they can be merged into one larger fragment",
"type": "boolean"
},
"usePhraseHighlighter": {
"description": "Should phrases be identified",
"type": "boolean"
},
"fields": {
"type": "array",
"minItems": 1,
"items": {
"description": "The fields to highlight and field specific configuration properties for each field",
"type": "object",
"properties": {
"field": {
"description": "The name of the field to highlight",
"type": "string"
},
"snippetCount": {
"type": "number"
},
"fragmentSize": {
"type": "number"
},
"mergeContiguous": {
"type": "boolean"
},
"prefix": {
"type": "string"
},
"postfix": {
"type": "string"
}
}
}
}
}
},
"sorting": {
"type" : "object",
"description": "Sorting options and defaults",
"required": [
"options"
],
"properties": {
"options": {
"type": "array",
"minItems": 1,
"items": {
"description": "Sorting options available for users to choose from",
"type": "object",
"required": [
"key",
"label",
"type",
"field",
"ascending"
],
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
},
"type": {
"type": "string"
},
"field": {
"type": "string"
},
"ascending": {
"type": "boolean"
}
}
}
},
"defaults": {
"description": "Predefined sorting to execute by default",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
},
"type": {
"type": "string"
},
"field": {
"type": "string"
},
"ascending": {
"type": "boolean"
}
}
}
}
}
"$ref": "#/definitions/search-configuration"
}
}
]
},
"adf-viewer": {
"description": "Viewer default properties",

View File

@@ -14,5 +14,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './src/public-api';