From 07440731fa6d28635e8273f36fc8ced442224868 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Fri, 18 May 2018 14:33:28 +0100 Subject: [PATCH] [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 --- demo-shell/src/app.config.json | 10 +++ .../app/components/files/files.component.html | 9 +- .../app/components/files/files.component.ts | 18 ++++ .../search/search-result.component.html | 29 ++++--- .../search/search-result.component.scss | 4 + .../search/search-result.component.ts | 19 ++++ .../document-list.component.md | 1 + .../search-filter.component.md | 50 ++++++++++- .../search-sorting-picker.component.md | 13 +++ docs/core/sorting-picker.component.md | 43 ++++++++++ docs/docassets/images/sorting-picker.png | Bin 0 -> 4880 bytes .../components/document-list.component.ts | 20 ++++- .../data/share-datatable-adapter.spec.ts | 25 ++++++ .../data/share-datatable-adapter.ts | 43 +++++++--- .../search-sorting-picker.component.html | 6 ++ .../search-sorting-picker.component.spec.ts | 81 ++++++++++++++++++ .../search-sorting-picker.component.ts | 70 +++++++++++++++ lib/content-services/search/public-api.ts | 1 + .../search/search-configuration.interface.ts | 5 ++ .../search-query-builder.service.spec.ts | 18 ++++ .../search/search-query-builder.service.ts | 45 +++++++++- .../search-sorting-definition.interface.ts | 24 ++++++ lib/content-services/search/search.module.ts | 10 ++- lib/core/app-config/schema.json | 40 +++++++++ .../sorting-picker.component.html | 12 +++ .../sorting-picker.component.spec.ts | 52 +++++++++++ .../sorting-picker.component.ts | 61 +++++++++++++ lib/core/core.module.ts | 7 +- lib/core/index.ts | 1 + 29 files changed, 682 insertions(+), 35 deletions(-) create mode 100644 docs/content-services/search-sorting-picker.component.md create mode 100644 docs/core/sorting-picker.component.md create mode 100644 docs/docassets/images/sorting-picker.png create mode 100644 lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.html create mode 100644 lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.spec.ts create mode 100644 lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.ts create mode 100644 lib/content-services/search/search-sorting-definition.interface.ts create mode 100644 lib/core/components/sorting-picker/sorting-picker.component.html create mode 100644 lib/core/components/sorting-picker/sorting-picker.component.spec.ts create mode 100644 lib/core/components/sorting-picker/sorting-picker.component.ts diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index 7390174bf5..429518c272 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -55,6 +55,16 @@ ], "search": { "include": ["path", "allowableOperations"], + "sorting": { + "options": [ + { "key": "name", "label": "Name", "type": "FIELD", "field": "cm:name", "ascending": true }, + { "key": "content.sizeInBytes", "label": "Size", "type": "FIELD", "field": "content.size", "ascending": true }, + { "key": "description", "label": "Description", "type": "FIELD", "field": "cm:description", "ascending": true } + ], + "defaults": [ + { "key": "name", "type": "FIELD", "field": "cm:name", "ascending": true } + ] + }, "filterQueries": [ { "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" }, { "query": "NOT cm:creator:System" } diff --git a/demo-shell/src/app/components/files/files.component.html b/demo-shell/src/app/components/files/files.component.html index 7428590744..10ed77a5ef 100644 --- a/demo-shell/src/app/components/files/files.component.html +++ b/demo-shell/src/app/components/files/files.component.html @@ -1,6 +1,6 @@
- + @@ -28,7 +28,7 @@ -
+
@@ -194,6 +194,9 @@ [display]="displayMode" [node]="nodeResult" [includeFields]="includeFields" + [sorting]="sorting" + [sortingMode]="sortingMode" + [showHeader]="showHeader" (error)="onNavigationError($event)" (success)="resetError()" (ready)="emitReadyEvent($event)" @@ -416,7 +419,7 @@
-
+

Current folder ID: {{ documentList.currentFolderId }}

diff --git a/demo-shell/src/app/components/files/files.component.ts b/demo-shell/src/app/components/files/files.component.ts index 86504a4b9f..70bb53982d 100644 --- a/demo-shell/src/app/components/files/files.component.ts +++ b/demo-shell/src/app/components/files/files.component.ts @@ -74,6 +74,24 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { processId; + @Input() + sorting = ['name', 'asc']; + + @Input() + sortingMode = 'client'; + + @Input() + showRecentFiles = true; + + @Input() + showSitePicker = true; + + @Input() + showSettingsPanel = true; + + @Input() + showHeader = true; + @Input() selectionMode = 'multiple'; diff --git a/demo-shell/src/app/components/search/search-result.component.html b/demo-shell/src/app/components/search/search-result.component.html index 41b8cc9351..118c3785de 100644 --- a/demo-shell/src/app/components/search/search-result.component.html +++ b/demo-shell/src/app/components/search/search-result.component.html @@ -14,17 +14,26 @@
+
+ +
+ [showHeader]="false" + [sorting]="sorting" + [sortingMode]="'server'" + [showRecentFiles]="false" + [showSitePicker]="false" + [showSettingsPanel]="false" + [currentFolderId]="null" + [nodeResult]="resultNodePageList" + [disableDragArea]="true" + [pagination]="pagination" + (changedPageSize)="onRefreshPagination($event)" + (changedPageNumber)="onRefreshPagination($event)" + (turnedNextPage)="onRefreshPagination($event)" + (loadNext)="onRefreshPagination($event)" + (turnedPreviousPage)="onRefreshPagination($event)" + (deleteElementSuccess)="onDeleteElementSuccess($event)">
diff --git a/demo-shell/src/app/components/search/search-result.component.scss b/demo-shell/src/app/components/search/search-result.component.scss index d9a23640aa..ea0d0a049a 100644 --- a/demo-shell/src/app/components/search/search-result.component.scss +++ b/demo-shell/src/app/components/search/search-result.component.scss @@ -14,6 +14,10 @@ &__content { flex: 1; } + + &__sorting { + text-align: right; + } } div.search-results-container { diff --git a/demo-shell/src/app/components/search/search-result.component.ts b/demo-shell/src/app/components/search/search-result.component.ts index 907c204a5f..51c58fd5c8 100644 --- a/demo-shell/src/app/components/search/search-result.component.ts +++ b/demo-shell/src/app/components/search/search-result.component.ts @@ -39,6 +39,8 @@ export class SearchResultComponent implements OnInit { maxItems: number; skipCount = 0; + sorting = ['name', 'asc']; + constructor(public router: Router, private preferences: UserPreferencesService, private queryBuilder: SearchQueryBuilderService, @@ -51,6 +53,13 @@ export class SearchResultComponent implements OnInit { } ngOnInit() { + + this.sorting = this.getSorting(); + + this.queryBuilder.updated.subscribe(() => { + this.sorting = this.getSorting(); + }); + if (this.route) { this.route.params.forEach((params: Params) => { this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null; @@ -79,4 +88,14 @@ export class SearchResultComponent implements OnInit { onDeleteElementSuccess(element: any) { this.searchResult.reload(); } + + private getSorting(): string[] { + const primary = this.queryBuilder.getPrimarySorting(); + + if (primary) { + return [primary.key, primary.ascending ? 'asc' : 'desc']; + } + + return ['name', 'asc']; + } } diff --git a/docs/content-services/document-list.component.md b/docs/content-services/document-list.component.md index 54da35f7b6..5bb0152363 100644 --- a/docs/content-services/document-list.component.md +++ b/docs/content-services/document-list.component.md @@ -88,6 +88,7 @@ Displays the documents from a repository. | skipCount | `number` | 0 | Number of elements to skip over for pagination purposes | | sorting | `string[]` | | Defines default sorting. The format is an array of 2 strings `[key, direction]` i.e. `['name', 'desc']` or `['name', 'asc']`. Set this value only if you want to override the default sorting detected by the component based on columns. | | thumbnails | `boolean` | false | Show document thumbnails rather than icons | +| sortingMode | `string` | `client` | Defines sorting mode. Can be either `client` or `server`. | ### Events diff --git a/docs/content-services/search-filter.component.md b/docs/content-services/search-filter.component.md index c33754105d..f367ea3812 100644 --- a/docs/content-services/search-filter.component.md +++ b/docs/content-services/search-filter.component.md @@ -34,6 +34,16 @@ Below is an example configuration: ```json { "search": { + "sorting": { + "options": [ + { "key": "name", "label": "Name", "type": "FIELD", "field": "cm:name", "ascending": true }, + { "key": "content.sizeInBytes", "label": "Size", "type": "FIELD", "field": "content.size", "ascending": true }, + { "key": "description", "label": "Description", "type": "FIELD", "field": "cm:description", "ascending": true } + ], + "defaults": [ + { "key": "name", "type": "FIELD", "field": "cm:name", "ascending": true } + ] + }, "filterQueries": [ { "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" }, { "query": "NOT cm:creator:System" } @@ -107,6 +117,42 @@ In addition, it is also possible to provide a set of queries that are always exe Note that the entries of the `filterQueries` array are joined using the `AND` operator. +### Sorting + +The Sorting configuration section consists of two blocks: +`options` that holds a list of items that users can select from, +and `defaults` that contains predefined sorting to use by default. + +```json +{ + "search": { + "sorting": { + "options": [ + { "key": "name", "label": "Name", "type": "FIELD", "field": "cm:name", "ascending": true }, + { "key": "content.sizeInBytes", "label": "Size", "type": "FIELD", "field": "content.size", "ascending": true }, + { "key": "description", "label": "Description", "type": "FIELD", "field": "cm:description", "ascending": true } + ], + "defaults": [ + { "key": "name", "type": "FIELD", "field": "cm:name", "ascending": true } + ] + } + } +} +``` + +#### Sorting Definition Attributes + +| Name | Type | Description | +| --- | --- | --- | +| key | string | Unique key to identify the entry, can also be used to map DataColumn instances. | +| label | string | Display text, can also be an i18n resource key. | +| type | string | This specifies how to order - either by using a field or based on the position of the document in the index, or by score/relevance. | +| field | string | The name of the field. | +| ascending | boolean | The sorting order. | + +See also: +* Alfresco Content Services API Reference / Search Api / [Sort](https://docs.alfresco.com/5.2/concepts/search-api-sort.html) + ### Categories The Search Settings component and Query Builder require a `categories` section provided within the configuration. @@ -647,4 +693,6 @@ and pass custom attributes in case your component supports them: ## See also -- [Search Query Builder service](search-query-builder.service.md) +- [Search Query Builder service](search-query-builder.service.md) +- [Search Chip List Component](search-chip-list.component.md) +- [Search Sorting Picker Component](search-sorting-picker.component.md) diff --git a/docs/content-services/search-sorting-picker.component.md b/docs/content-services/search-sorting-picker.component.md new file mode 100644 index 0000000000..5206fef90d --- /dev/null +++ b/docs/content-services/search-sorting-picker.component.md @@ -0,0 +1,13 @@ +--- +Added: v2.3.0 +Status: Active +--- + +# Search Sorting Picker Component + +Provides an ability to select one of the predefined sorting definitions for search results: + +```html + + +``` diff --git a/docs/core/sorting-picker.component.md b/docs/core/sorting-picker.component.md new file mode 100644 index 0000000000..878f7fac3e --- /dev/null +++ b/docs/core/sorting-picker.component.md @@ -0,0 +1,43 @@ +--- +Added: v2.4.0 +Status: Active +--- + +# Sorting Picker Component + +Provides an ability to pick one of the predefined sorting definitions and define sorting direction: + +```html + + +``` + +![Sorting Picker](../docassets/images/sorting-picker.png) + +## Options format + +You can bind a collection of any objects that expose the following properties: + +```ts +{ + key: string; + label: string; +} +``` + +## Properties + +| Name | Type | Default Value | Description | +| options | `Array<{key: string, label: string}>` | `[]` | Available sorting options. | +| selected | `string` | | Currently selected option key. | +| ascending | `boolean` | true | Current sorting direction. | + +## Events + +| Name | Type | Description | +| --- | --- | --- | +| change | `EventEmitter<{ key: string, ascending: boolean }>` | Raised each time sorting key or direction gets changed. | diff --git a/docs/docassets/images/sorting-picker.png b/docs/docassets/images/sorting-picker.png new file mode 100644 index 0000000000000000000000000000000000000000..9d5f9d2331358995f0bb5bca0617fee700462f60 GIT binary patch literal 4880 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 YWtereUpHTy`~3`1mRFZU$(V=y3r5P{H2?qr literal 0 HcmV?d00001 diff --git a/lib/content-services/document-list/components/document-list.component.ts b/lib/content-services/document-list/components/document-list.component.ts index 6fffb6a7d6..2d0cdb795c 100644 --- a/lib/content-services/document-list/components/document-list.component.ts +++ b/lib/content-services/document-list/components/document-list.component.ts @@ -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(); diff --git a/lib/content-services/document-list/data/share-datatable-adapter.spec.ts b/lib/content-services/document-list/data/share-datatable-adapter.spec.ts index c0e61cedd4..355a1d5a01 100644 --- a/lib/content-services/document-list/data/share-datatable-adapter.spec.ts +++ b/lib/content-services/document-list/data/share-datatable-adapter.spec.ts @@ -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 = [ {}]; let adapter = new ShareDataTableAdapter(documentListService, schema); diff --git a/lib/content-services/document-list/data/share-datatable-adapter.ts b/lib/content-services/document-list/data/share-datatable-adapter.ts index 694ffc0c26..de57e931df 100644 --- a/lib/content-services/document-list/data/share-datatable-adapter.ts +++ b/lib/content-services/document-list/data/share-datatable-adapter.ts @@ -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 { @@ -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'); + } } } } diff --git a/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.html b/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.html new file mode 100644 index 0000000000..95aecf807a --- /dev/null +++ b/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.html @@ -0,0 +1,6 @@ + + diff --git a/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.spec.ts b/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.spec.ts new file mode 100644 index 0000000000..923244706a --- /dev/null +++ b/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.spec.ts @@ -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: [ + { 'key': 'name', 'label': 'Name', 'type': 'FIELD', 'field': 'cm:name', 'ascending': true }, + { 'key': 'content.sizeInBytes', 'label': 'Size', 'type': 'FIELD', 'field': 'content.size', 'ascending': true }, + { 'key': 'description', 'label': 'Description', 'type': 'FIELD', 'field': 'cm:description', 'ascending': true } + ], + defaults: [ + { 'key': 'name', 'type': 'FIELD', 'field': 'cm:name', 'ascending': true } + ] + }, + categories: [ + { 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(); + }); +}); diff --git a/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.ts b/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.ts new file mode 100644 index 0000000000..053797cf42 --- /dev/null +++ b/lib/content-services/search/components/search-sorting-picker/search-sorting-picker.component.ts @@ -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(); + } + } + +} diff --git a/lib/content-services/search/public-api.ts b/lib/content-services/search/public-api.ts index 0e66590069..df82cba669 100644 --- a/lib/content-services/search/public-api.ts +++ b/lib/content-services/search/public-api.ts @@ -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'; diff --git a/lib/content-services/search/search-configuration.interface.ts b/lib/content-services/search/search-configuration.interface.ts index 56a0a136c6..8a20ceb7e7 100644 --- a/lib/content-services/search/search-configuration.interface.ts +++ b/lib/content-services/search/search-configuration.interface.ts @@ -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; @@ -32,4 +33,8 @@ export interface SearchConfiguration { queries: Array; }; facetFields?: Array; + sorting?: { + options: Array; + defaults: Array; + }; } diff --git a/lib/content-services/search/search-query-builder.service.spec.ts b/lib/content-services/search/search-query-builder.service.spec.ts index e18da7b4af..375cdb01d8 100644 --- a/lib/content-services/search/search-query-builder.service.spec.ts +++ b/lib/content-services/search/search-query-builder.service.spec.ts @@ -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: [ + { id: 'cat1', enabled: true }, + { 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: [ diff --git a/lib/content-services/search/search-query-builder.service.ts b/lib/content-services/search/search-query-builder.service.ts index 8446cdca40..b3b87fb5c9 100644 --- a/lib/content-services/search/search-query-builder.service.ts +++ b/lib/content-services/search/search-query-builder.service.ts @@ -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 = []; queryFragments: { [id: string]: string } = {}; filterQueries: FilterQuery[] = []; - ranges: { [id: string]: SearchRange } = {}; paging: { maxItems?: number; skipCount?: number } = null; + sorting: Array = []; config: SearchConfiguration; + // TODO: to be supported in future iterations + ranges: { [id: string]: SearchRange } = {}; + constructor(appConfig: AppConfigService, private alfrescoApiService: AlfrescoApiService) { this.config = appConfig.get('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; diff --git a/lib/content-services/search/search-sorting-definition.interface.ts b/lib/content-services/search/search-sorting-definition.interface.ts new file mode 100644 index 0000000000..6dcea1276e --- /dev/null +++ b/lib/content-services/search/search-sorting-definition.interface.ts @@ -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; +} diff --git a/lib/content-services/search/search.module.ts b/lib/content-services/search/search.module.ts index 53fb0af7a6..6e3f210f37 100644 --- a/lib/content-services/search/search.module.ts +++ b/lib/content-services/search/search.module.ts @@ -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, diff --git a/lib/core/app-config/schema.json b/lib/core/app-config/schema.json index 494cdef0cf..10b5126dba 100644 --- a/lib/core/app-config/schema.json +++ b/lib/core/app-config/schema.json @@ -599,6 +599,46 @@ } } } + }, + "sorting": { + "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", + "options": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["key", "label", "type", "field", "ascending"], + "properties": { + "key": { "type": "string" }, + "label": { "type": "string" }, + "type": { "type": "string" }, + "field": { "type": "string" }, + "ascending": { "type": "boolean" } + } + } + } + } + } } } } diff --git a/lib/core/components/sorting-picker/sorting-picker.component.html b/lib/core/components/sorting-picker/sorting-picker.component.html new file mode 100644 index 0000000000..a7750f96b8 --- /dev/null +++ b/lib/core/components/sorting-picker/sorting-picker.component.html @@ -0,0 +1,12 @@ + + + + {{ option.label | translate }} + + + + + diff --git a/lib/core/components/sorting-picker/sorting-picker.component.spec.ts b/lib/core/components/sorting-picker/sorting-picker.component.spec.ts new file mode 100644 index 0000000000..7d8a8cf3e5 --- /dev/null +++ b/lib/core/components/sorting-picker/sorting-picker.component.spec.ts @@ -0,0 +1,52 @@ +/*! + * @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 { SortingPickerComponent } from './sorting-picker.component'; + +describe('SortingPickerComponent', () => { + + let component: SortingPickerComponent; + + beforeEach(() => { + component = new SortingPickerComponent(); + }); + + it('should raise changed event on changing value', (done) => { + component.selected = 'key1'; + component.ascending = false; + + component.change.subscribe((event: { key: string, ascending: boolean }) => { + expect(event.key).toBe('key2'); + expect(event.ascending).toBeFalsy(); + done(); + }); + component.onChanged( { value: 'key2' }); + }); + + it('should raise changed event on changing direction', (done) => { + component.selected = 'key1'; + component.ascending = false; + + component.change.subscribe((event: { key: string, ascending: boolean }) => { + expect(event.key).toBe('key1'); + expect(event.ascending).toBeTruthy(); + done(); + }); + component.toggleSortDirection(); + }); + +}); diff --git a/lib/core/components/sorting-picker/sorting-picker.component.ts b/lib/core/components/sorting-picker/sorting-picker.component.ts new file mode 100644 index 0000000000..9dd7536322 --- /dev/null +++ b/lib/core/components/sorting-picker/sorting-picker.component.ts @@ -0,0 +1,61 @@ +/*! + * @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, ViewEncapsulation, Input, EventEmitter, Output } from '@angular/core'; +import { MatSelectChange } from '@angular/material'; + +@Component({ + selector: 'adf-sorting-picker', + templateUrl: './sorting-picker.component.html', + encapsulation: ViewEncapsulation.None, + host: { class: 'adf-sorting-picker' } +}) +export class SortingPickerComponent { + + /** Available sorting options */ + @Input() + options: Array<{key: string, label: string}> = []; + + /** Currently selected option key */ + @Input() + selected: string; + + /** Current sorting direction */ + @Input() + ascending = true; + + /** Raised each time sorting key or direction gets changed. */ + @Output() + change = new EventEmitter<{ key: string, ascending: boolean }>(); + + onChanged(event: MatSelectChange) { + this.selected = event.value; + this.raiseChangedEvent(); + } + + toggleSortDirection() { + this.ascending = !this.ascending; + this.raiseChangedEvent(); + } + + private raiseChangedEvent() { + this.change.emit({ + key: this.selected, + ascending: this.ascending + }); + } +} diff --git a/lib/core/core.module.ts b/lib/core/core.module.ts index 610d9f773c..cc28912336 100644 --- a/lib/core/core.module.ts +++ b/lib/core/core.module.ts @@ -81,6 +81,7 @@ import { UserPreferencesService } from './services/user-preferences.service'; import { SearchConfigurationService } from './services/search-configuration.service'; import { startupServiceFactory } from './services/startup-service-factory'; import { EmptyContentComponent } from './components/empty-content/empty-content.component'; +import { SortingPickerComponent } from './components/sorting-picker/sorting-picker.component'; export function createTranslateLoader(http: HttpClient, logService: LogService) { return new TranslateLoaderService(http, logService); @@ -232,7 +233,8 @@ export class CoreModuleLazy { }) ], declarations: [ - EmptyContentComponent + EmptyContentComponent, + SortingPickerComponent ], exports: [ AboutModule, @@ -262,7 +264,8 @@ export class CoreModuleLazy { DataTableModule, TranslateModule, ButtonsMenuModule, - EmptyContentComponent + EmptyContentComponent, + SortingPickerComponent ], providers: [ ...providers(), diff --git a/lib/core/index.ts b/lib/core/index.ts index 332048bc4e..b262848dcd 100644 --- a/lib/core/index.ts +++ b/lib/core/index.ts @@ -36,6 +36,7 @@ export * from './comments/index'; export * from './buttons-menu/index'; export * from './components/empty-content/empty-content.component'; +export * from './components/sorting-picker/sorting-picker.component'; export * from './pipes/index'; export * from './services/index';