+
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
+
+
+```
+
+
+
+## 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 0000000000..9d5f9d2331
Binary files /dev/null and b/docs/docassets/images/sorting-picker.png differ
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';