mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ADF-2131] Search sorting (#3334)
* sorting configuration * detect primary sorting and use with document list * search results sorting * docs update * unit tests and code updates * update code * update code * generic sorting picker, test updates * ability to switch off client side sorting * update docs for document list
This commit is contained in:
committed by
Eugenio Romano
parent
73bc62ae8f
commit
07440731fa
@@ -138,6 +138,10 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
@Input()
|
||||
sorting = ['name', 'asc'];
|
||||
|
||||
/** Defines sorting mode. Can be either `client` or `server`. */
|
||||
@Input()
|
||||
sortingMode = 'client';
|
||||
|
||||
/** The inline style to apply to every row. See
|
||||
* the Angular NgStyle
|
||||
* docs for more details and usage examples.
|
||||
@@ -329,7 +333,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
|
||||
ngOnInit() {
|
||||
this.loadLayoutPresets();
|
||||
this.data = new ShareDataTableAdapter(this.documentListService, null, this.getDefaultSorting());
|
||||
this.data = new ShareDataTableAdapter(this.documentListService, null, this.getDefaultSorting(), this.sortingMode);
|
||||
this.data.thumbnails = this.thumbnails;
|
||||
this.data.permissionsStyle = this.permissionsStyle;
|
||||
|
||||
@@ -367,7 +371,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
}
|
||||
|
||||
if (!this.data) {
|
||||
this.data = new ShareDataTableAdapter(this.documentListService, schema, this.getDefaultSorting());
|
||||
this.data = new ShareDataTableAdapter(this.documentListService, schema, this.getDefaultSorting(), this.sortingMode);
|
||||
} else if (schema && schema.length > 0) {
|
||||
this.data.setColumns(schema);
|
||||
}
|
||||
@@ -381,6 +385,18 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
this.resetSelection();
|
||||
|
||||
if (changes.sortingMode && !changes.sortingMode.firstChange && this.data) {
|
||||
this.data.sortingMode = changes.sortingMode.currentValue;
|
||||
}
|
||||
|
||||
if (changes.sorting && !changes.sorting.firstChange && this.data) {
|
||||
const newValue = changes.sorting.currentValue;
|
||||
if (newValue && newValue.length > 0) {
|
||||
const [key, direction] = newValue;
|
||||
this.data.setSorting(new DataSorting(key, direction));
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.folderNode && changes.folderNode.currentValue) {
|
||||
this.currentFolderId = changes.folderNode.currentValue.id;
|
||||
this.resetNewFolderPagination();
|
||||
|
@@ -32,6 +32,31 @@ describe('ShareDataTableAdapter', () => {
|
||||
spyOn(documentListService, 'getDocumentThumbnailUrl').and.returnValue(imageUrl);
|
||||
});
|
||||
|
||||
it('should use client sorting by default', () => {
|
||||
const adapter = new ShareDataTableAdapter(documentListService, []);
|
||||
expect(adapter.sortingMode).toBe('client');
|
||||
});
|
||||
|
||||
it('should not be case sensitive for sorting mode value', () => {
|
||||
const adapter = new ShareDataTableAdapter(documentListService, []);
|
||||
|
||||
adapter.sortingMode = 'CLIENT';
|
||||
expect(adapter.sortingMode).toBe('client');
|
||||
|
||||
adapter.sortingMode = 'SeRvEr';
|
||||
expect(adapter.sortingMode).toBe('server');
|
||||
});
|
||||
|
||||
it('should fallback to client sorting for unknown values', () => {
|
||||
const adapter = new ShareDataTableAdapter(documentListService, []);
|
||||
|
||||
adapter.sortingMode = 'SeRvEr';
|
||||
expect(adapter.sortingMode).toBe('server');
|
||||
|
||||
adapter.sortingMode = 'quantum';
|
||||
expect(adapter.sortingMode).toBe('client');
|
||||
});
|
||||
|
||||
it('should setup rows and columns with constructor', () => {
|
||||
let schema = [<DataColumn> {}];
|
||||
let adapter = new ShareDataTableAdapter(documentListService, schema);
|
||||
|
@@ -26,6 +26,7 @@ export class ShareDataTableAdapter implements DataTableAdapter {
|
||||
ERR_ROW_NOT_FOUND: string = 'Row not found';
|
||||
ERR_COL_NOT_FOUND: string = 'Column not found';
|
||||
|
||||
private _sortingMode: string;
|
||||
private sorting: DataSorting;
|
||||
private rows: DataRow[];
|
||||
private columns: DataColumn[];
|
||||
@@ -37,12 +38,26 @@ export class ShareDataTableAdapter implements DataTableAdapter {
|
||||
permissionsStyle: PermissionStyleModel[];
|
||||
selectedRow: DataRow;
|
||||
|
||||
set sortingMode(value: string) {
|
||||
let newValue = (value || 'client').toLowerCase();
|
||||
if (newValue !== 'client' && newValue !== 'server') {
|
||||
newValue = 'client';
|
||||
}
|
||||
this._sortingMode = newValue;
|
||||
}
|
||||
|
||||
get sortingMode(): string {
|
||||
return this._sortingMode;
|
||||
}
|
||||
|
||||
constructor(private documentListService: DocumentListService,
|
||||
schema: DataColumn[] = [],
|
||||
sorting?: DataSorting) {
|
||||
sorting?: DataSorting,
|
||||
sortingMode: string = 'client') {
|
||||
this.rows = [];
|
||||
this.columns = schema || [];
|
||||
this.sorting = sorting;
|
||||
this.sortingMode = sortingMode;
|
||||
}
|
||||
|
||||
getRows(): Array<DataRow> {
|
||||
@@ -148,6 +163,10 @@ export class ShareDataTableAdapter implements DataTableAdapter {
|
||||
}
|
||||
|
||||
private sortRows(rows: DataRow[], sorting: DataSorting) {
|
||||
if (this.sortingMode === 'server') {
|
||||
return;
|
||||
}
|
||||
|
||||
const options: Intl.CollatorOptions = {};
|
||||
|
||||
if (sorting && sorting.key && rows && rows.length > 0) {
|
||||
@@ -194,17 +213,19 @@ export class ShareDataTableAdapter implements DataTableAdapter {
|
||||
rows = rows.filter(this.filter);
|
||||
}
|
||||
|
||||
// Sort by first sortable or just first column
|
||||
if (this.columns && this.columns.length > 0) {
|
||||
let sorting = this.getSorting();
|
||||
if (sorting) {
|
||||
this.sortRows(rows, sorting);
|
||||
} else {
|
||||
let sortable = this.columns.filter(c => c.sortable);
|
||||
if (sortable.length > 0) {
|
||||
this.sort(sortable[0].key, 'asc');
|
||||
if (this.sortingMode !== 'server') {
|
||||
// Sort by first sortable or just first column
|
||||
if (this.columns && this.columns.length > 0) {
|
||||
let sorting = this.getSorting();
|
||||
if (sorting) {
|
||||
this.sortRows(rows, sorting);
|
||||
} else {
|
||||
this.sort(this.columns[0].key, 'asc');
|
||||
let sortable = this.columns.filter(c => c.sortable);
|
||||
if (sortable.length > 0) {
|
||||
this.sort(sortable[0].key, 'asc');
|
||||
} else {
|
||||
this.sort(this.columns[0].key, 'asc');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<adf-sorting-picker
|
||||
[options]="options"
|
||||
[selected]="value"
|
||||
[ascending]="ascending"
|
||||
(change)="onChanged($event)">
|
||||
</adf-sorting-picker>
|
@@ -0,0 +1,81 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 Alfresco Software, Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { 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';
|
||||
|
||||
describe('SearchSortingPickerComponent', () => {
|
||||
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
let component: SearchSortingPickerComponent;
|
||||
|
||||
const buildConfig = (searchSettings): AppConfigService => {
|
||||
const config = new AppConfigService(null);
|
||||
config.config.search = searchSettings;
|
||||
return config;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const config: SearchConfiguration = {
|
||||
sorting: {
|
||||
options: [
|
||||
<any> { 'key': 'name', 'label': 'Name', 'type': 'FIELD', 'field': 'cm:name', 'ascending': true },
|
||||
<any> { 'key': 'content.sizeInBytes', 'label': 'Size', 'type': 'FIELD', 'field': 'content.size', 'ascending': true },
|
||||
<any> { 'key': 'description', 'label': 'Description', 'type': 'FIELD', 'field': 'cm:description', 'ascending': true }
|
||||
],
|
||||
defaults: [
|
||||
<any> { 'key': 'name', 'type': 'FIELD', 'field': 'cm:name', 'ascending': true }
|
||||
]
|
||||
},
|
||||
categories: [
|
||||
<any> { id: 'cat1', enabled: true }
|
||||
]
|
||||
};
|
||||
queryBuilder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||
component = new SearchSortingPickerComponent(queryBuilder);
|
||||
});
|
||||
|
||||
it('should load options from query builder', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.options.length).toBe(3);
|
||||
expect(component.options[0].key).toEqual('name');
|
||||
expect(component.options[1].key).toEqual('content.sizeInBytes');
|
||||
expect(component.options[2].key).toEqual('description');
|
||||
});
|
||||
|
||||
it('should pre-select the primary sorting definition', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.value).toEqual('name');
|
||||
expect(component.ascending).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update query builder each time selection is changed', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
component.ngOnInit();
|
||||
component.onChanged({ key: 'description', ascending: false });
|
||||
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
expect(queryBuilder.sorting.length).toBe(1);
|
||||
expect(queryBuilder.sorting[0].key).toEqual('description');
|
||||
expect(queryBuilder.sorting[0].ascending).toBeFalsy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 Alfresco Software, Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchSortingDefinition } from '../../search-sorting-definition.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-sorting-picker',
|
||||
templateUrl: './search-sorting-picker.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: { class: 'adf-search-sorting-picker' }
|
||||
})
|
||||
export class SearchSortingPickerComponent implements OnInit {
|
||||
|
||||
options: SearchSortingDefinition[] = [];
|
||||
value: string;
|
||||
ascending: boolean;
|
||||
|
||||
constructor(private queryBuilder: SearchQueryBuilderService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.options = this.queryBuilder.getSortingOptions();
|
||||
|
||||
const primary = this.queryBuilder.getPrimarySorting();
|
||||
if (primary) {
|
||||
this.value = primary.key;
|
||||
this.ascending = primary.ascending;
|
||||
}
|
||||
}
|
||||
|
||||
onChanged(sorting: { key: string, ascending: boolean }) {
|
||||
this.value = sorting.key;
|
||||
this.ascending = sorting.ascending;
|
||||
this.applySorting();
|
||||
}
|
||||
|
||||
private findOptionByKey(key: string): SearchSortingDefinition {
|
||||
if (key) {
|
||||
return this.options.find(opt => opt.key === key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private applySorting() {
|
||||
const option = this.findOptionByKey(this.value);
|
||||
if (option) {
|
||||
this.queryBuilder.sorting = [{
|
||||
...option,
|
||||
ascending: this.ascending
|
||||
}];
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -34,5 +34,6 @@ export * from './components/search-trigger.directive';
|
||||
export * from './components/empty-search-result.component';
|
||||
export * from './components/search-filter/search-filter.component';
|
||||
export * from './components/search-chip-list/search-chip-list.component';
|
||||
export * from './components/search-sorting-picker/search-sorting-picker.component';
|
||||
|
||||
export * from './search.module';
|
||||
|
@@ -19,6 +19,7 @@ import { FilterQuery } from './filter-query.interface';
|
||||
import { FacetQuery } from './facet-query.interface';
|
||||
import { FacetField } from './facet-field.interface';
|
||||
import { SearchCategory } from './search-category.interface';
|
||||
import { SearchSortingDefinition } from './search-sorting-definition.interface';
|
||||
|
||||
export interface SearchConfiguration {
|
||||
include?: Array<string>;
|
||||
@@ -32,4 +33,8 @@ export interface SearchConfiguration {
|
||||
queries: Array<FacetQuery>;
|
||||
};
|
||||
facetFields?: Array<FacetField>;
|
||||
sorting?: {
|
||||
options: Array<SearchSortingDefinition>;
|
||||
defaults: Array<SearchSortingDefinition>;
|
||||
};
|
||||
}
|
||||
|
@@ -299,6 +299,24 @@ describe('SearchQueryBuilder', () => {
|
||||
expect(compiled.facetFields.facets).toEqual(jasmine.objectContaining(config.facetFields));
|
||||
});
|
||||
|
||||
it('should build query with sorting', () => {
|
||||
const config: SearchConfiguration = {
|
||||
fields: [],
|
||||
categories: [
|
||||
<any> { id: 'cat1', enabled: true },
|
||||
<any> { id: 'cat2', enabled: true }
|
||||
]
|
||||
};
|
||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||
const sorting: any = { type: 'FIELD', field: 'cm:name', ascending: true };
|
||||
builder.sorting = [sorting];
|
||||
|
||||
builder.queryFragments['cat1'] = 'cm:name:test';
|
||||
|
||||
const compiled = builder.buildQuery();
|
||||
expect(compiled.sort[0]).toEqual(jasmine.objectContaining(sorting));
|
||||
});
|
||||
|
||||
it('should use pagination settings', () => {
|
||||
const config: SearchConfiguration = {
|
||||
categories: [
|
||||
|
@@ -18,12 +18,13 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
|
||||
import { QueryBody, RequestFacetFields, RequestFacetField } from 'alfresco-js-api';
|
||||
import { QueryBody, RequestFacetFields, RequestFacetField, RequestSortDefinitionInner } 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';
|
||||
|
||||
@Injectable()
|
||||
export class SearchQueryBuilderService {
|
||||
@@ -34,17 +35,24 @@ export class SearchQueryBuilderService {
|
||||
categories: Array<SearchCategory> = [];
|
||||
queryFragments: { [id: string]: string } = {};
|
||||
filterQueries: FilterQuery[] = [];
|
||||
ranges: { [id: string]: SearchRange } = {};
|
||||
paging: { maxItems?: number; skipCount?: number } = null;
|
||||
sorting: Array<SearchSortingDefinition> = [];
|
||||
|
||||
config: SearchConfiguration;
|
||||
|
||||
// TODO: to be supported in future iterations
|
||||
ranges: { [id: string]: SearchRange } = {};
|
||||
|
||||
constructor(appConfig: AppConfigService, private alfrescoApiService: AlfrescoApiService) {
|
||||
this.config = appConfig.get<SearchConfiguration>('search');
|
||||
|
||||
if (this.config) {
|
||||
this.categories = (this.config.categories || []).filter(f => f.enabled);
|
||||
this.filterQueries = this.config.filterQueries || [];
|
||||
|
||||
if (this.config.sorting) {
|
||||
this.sorting = this.config.sorting.defaults || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +120,8 @@ export class SearchQueryBuilderService {
|
||||
fields: this.config.fields,
|
||||
filterQueries: this.filterQueries,
|
||||
facetQueries: this.facetQueries,
|
||||
facetFields: this.facetFields
|
||||
facetFields: this.facetFields,
|
||||
sort: this.sort
|
||||
};
|
||||
|
||||
return result;
|
||||
@@ -121,6 +130,36 @@ export class SearchQueryBuilderService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns primary sorting definition.
|
||||
*/
|
||||
getPrimarySorting(): SearchSortingDefinition {
|
||||
if (this.sorting && this.sorting.length > 0) {
|
||||
return this.sorting[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all pre-configured sorting options that users can choose from.
|
||||
*/
|
||||
getSortingOptions(): SearchSortingDefinition[] {
|
||||
if (this.config && this.config.sorting) {
|
||||
return this.config.sorting.options || [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private get sort(): RequestSortDefinitionInner[] {
|
||||
return this.sorting.map(def => {
|
||||
return {
|
||||
type: def.type,
|
||||
field: def.field,
|
||||
ascending: def.ascending
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private get facetQueries(): FacetQuery[] {
|
||||
const config = this.config.facetQueries;
|
||||
|
||||
|
@@ -0,0 +1,24 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 Alfresco Software, Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface SearchSortingDefinition {
|
||||
key: string;
|
||||
label: string;
|
||||
type: string;
|
||||
field: string;
|
||||
ascending: boolean;
|
||||
}
|
@@ -21,7 +21,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MaterialModule } from '../material.module';
|
||||
|
||||
import { PipeModule } from '@alfresco/adf-core';
|
||||
import { PipeModule, CoreModule } from '@alfresco/adf-core';
|
||||
|
||||
import { SearchTriggerDirective } from './components/search-trigger.directive';
|
||||
|
||||
@@ -37,6 +37,7 @@ import { SearchSliderComponent } from './components/search-slider/search-slider.
|
||||
import { SearchNumberRangeComponent } from './components/search-number-range/search-number-range.component';
|
||||
import { SearchCheckListComponent } from './components/search-check-list/search-check-list.component';
|
||||
import { SearchDateRangeComponent } from './components/search-date-range/search-date-range.component';
|
||||
import { SearchSortingPickerComponent } from './components/search-sorting-picker/search-sorting-picker.component';
|
||||
|
||||
export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
|
||||
SearchComponent,
|
||||
@@ -49,6 +50,7 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
@@ -64,7 +66,8 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
|
||||
SearchSliderComponent,
|
||||
SearchNumberRangeComponent,
|
||||
SearchCheckListComponent,
|
||||
SearchDateRangeComponent
|
||||
SearchDateRangeComponent,
|
||||
SearchSortingPickerComponent
|
||||
],
|
||||
exports: [
|
||||
...ALFRESCO_SEARCH_DIRECTIVES,
|
||||
@@ -74,7 +77,8 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
|
||||
SearchSliderComponent,
|
||||
SearchNumberRangeComponent,
|
||||
SearchCheckListComponent,
|
||||
SearchDateRangeComponent
|
||||
SearchDateRangeComponent,
|
||||
SearchSortingPickerComponent
|
||||
],
|
||||
entryComponents: [
|
||||
SearchWidgetContainerComponent,
|
||||
|
Reference in New Issue
Block a user