diff --git a/.vscode/settings.json b/.vscode/settings.json index d1d4ace8ba..aed227378d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "MD031" : false }, "cSpell.words": [ + "mincount", "webscript" ] } diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index 21d4f81841..2d3ea63b49 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -54,19 +54,17 @@ ], "search": { "include": ["path", "allowableOperations"], - "fields": [], "filterQueries": [ { "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" }, { "query": "NOT cm:creator:System" } ], - "facetFields": { - "facets": [ - { "field": "content.mimetype", "mincount": 1, "label": "Type" }, - { "field": "content.size", "mincount": 1, "label": "Size" }, - { "field": "creator", "mincount": 1, "label": "Creator" }, - { "field": "modifier", "mincount": 1, "label": "Modifier" } - ] - }, + "facetFields": [ + { "field": "content.mimetype", "mincount": 1, "label": "Type" }, + { "field": "content.size", "mincount": 1, "label": "Size" }, + { "field": "creator", "mincount": 1, "label": "Creator" }, + { "field": "modifier", "mincount": 1, "label": "Modifier" }, + { "field": "created", "mincount": 1, "label": "Created" } + ], "facetQueries": [ { "query": "created:2018", "label": "Created This Year" }, { "query": "content.mimetype", "label": "Type" }, @@ -77,67 +75,91 @@ { "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" }, { "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" } ], - "query": { - "categories": [ - { - "id": "queryName", - "name": "Name", - "enabled": true, - "expanded": true, - "component": { - "selector": "text", - "settings": { - "pattern": "cm:name:'(.*?)'", - "field": "cm:name", - "placeholder": "Enter the name" - } - } - }, - { - "id": "contentSize", - "name": "Content Size", - "enabled": true, - "component": { - "selector": "slider", - "settings": { - "field": "cm:content.size", - "min": 0, - "max": 18, - "step": 1, - "thumbLabel": true - } - } - }, - { - "id": "contentSizeRange", - "name": "Content Size (range)", - "enabled": true, - "component": { - "selector": "number-range", - "settings": { - "field": "cm:content.size" - } - } - }, - { - "id": "queryType", - "name": "Type", - "enabled": true, - "component": { - "selector": "radio", - "settings": { - "field": null, - "options": [ - { "name": "None", "value": "", "default": true }, - { "name": "All", "value": "TYPE:'cm:folder' OR TYPE:'cm:content'" }, - { "name": "Folder", "value": "TYPE:'cm:folder'" }, - { "name": "Document", "value": "TYPE:'cm:content'" } - ] - } + "categories": [ + { + "id": "queryName", + "name": "Name", + "enabled": true, + "expanded": true, + "component": { + "selector": "text", + "settings": { + "pattern": "cm:name:'(.*?)'", + "field": "cm:name", + "placeholder": "Enter the name" } } - ] - } + }, + { + "id": "checkList", + "name": "Check List", + "enabled": true, + "component": { + "selector": "check-list", + "settings": { + "operator": "OR", + "options": [ + { "name": "Folder", "value": "TYPE:'cm:folder'" }, + { "name": "Document", "value": "TYPE:'cm:content'" } + ] + } + } + }, + { + "id": "contentSize", + "name": "Content Size", + "enabled": true, + "component": { + "selector": "slider", + "settings": { + "field": "cm:content.size", + "min": 0, + "max": 18, + "step": 1, + "thumbLabel": true + } + } + }, + { + "id": "contentSizeRange", + "name": "Content Size (range)", + "enabled": true, + "component": { + "selector": "number-range", + "settings": { + "field": "cm:content.size" + } + } + }, + { + "id": "createdDateRange", + "name": "Created Date (range)", + "enabled": true, + "component": { + "selector": "date-range", + "settings": { + "field": "cm:created" + } + } + }, + { + "id": "queryType", + "name": "Type", + "enabled": true, + "component": { + "selector": "radio", + "settings": { + "field": null, + "options": [ + { "name": "None", "value": "", "default": true }, + { "name": "All", "value": "TYPE:'cm:folder' OR TYPE:'cm:content'" }, + { "name": "Folder", "value": "TYPE:'cm:folder'" }, + { "name": "Document", "value": "TYPE:'cm:content'" } + ] + } + } + } + ] }, "pagination": { "size": 25, 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 b44da7d1b6..b66a49baec 100644 --- a/demo-shell/src/app/components/search/search-result.component.ts +++ b/demo-shell/src/app/components/search/search-result.component.ts @@ -34,17 +34,14 @@ export class SearchResultComponent implements OnInit { queryParamName = 'q'; searchedWord = ''; resultNodePageList: NodePaging; - maxItems: number; - skipCount = 0; pagination: Pagination; constructor(public router: Router, private preferences: UserPreferencesService, private queryBuilder: SearchQueryBuilderService, @Optional() private route: ActivatedRoute) { - this.maxItems = this.preferences.paginationSize; queryBuilder.paging = { - maxItems: this.maxItems, + maxItems: this.preferences.paginationSize, skipCount: 0 }; } @@ -57,18 +54,14 @@ export class SearchResultComponent implements OnInit { this.queryBuilder.update(); }); } - this.maxItems = this.preferences.paginationSize; } onSearchResultLoaded(nodePaging: NodePaging) { this.resultNodePageList = nodePaging; - this.pagination = nodePaging.list.pagination; + this.pagination = {...nodePaging.list.pagination }; } onRefreshPagination(pagination: Pagination) { - this.maxItems = pagination.maxItems; - this.skipCount = pagination.skipCount; - this.queryBuilder.paging = { maxItems: pagination.maxItems, skipCount: pagination.skipCount diff --git a/docs/content-services/search-filter.component.md b/docs/content-services/search-filter.component.md index 7e8dcda7ab..427f7112be 100644 --- a/docs/content-services/search-filter.component.md +++ b/docs/content-services/search-filter.component.md @@ -28,22 +28,16 @@ Below is an example configuration: ```json { "search": { - "limits": { - "permissionEvaluationTime": null, - "permissionEvaluationCount": null - }, "filterQueries": [ { "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" }, { "query": "NOT cm:creator:System" } ], - "facetFields": { - "facets": [ + "facetFields": [ { "field": "content.mimetype", "mincount": 1, "label": "Type" }, { "field": "content.size", "mincount": 1, "label": "Size" }, { "field": "creator", "mincount": 1, "label": "Creator" }, { "field": "modifier", "mincount": 1, "label": "Modifier" } - ] - }, + ], "facetQueries": [ { "query": "created:2018", "label": "Created This Year" }, { "query": "content.mimetype", "label": "Type" }, @@ -54,34 +48,61 @@ Below is an example configuration: { "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" }, { "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" } ], - "query": { - "categories": [ - { - "id": "queryName", - "name": "Name", - "enabled": true, - "expanded": true, - "component": { - "selector": "adf-search-text", - "settings": { - "pattern": "cm:name:'(.*?)'", - "field": "cm:name", - "placeholder": "Enter the name" - } + "categories": [ + { + "id": "queryName", + "name": "Name", + "enabled": true, + "expanded": true, + "component": { + "selector": "adf-search-text", + "settings": { + "pattern": "cm:name:'(.*?)'", + "field": "cm:name", + "placeholder": "Enter the name" } } - ] - } + } + ] } } ``` +Please refer to the [schema.json](https://github.com/Alfresco/alfresco-ng2-components/blob/master/lib/core/app-config/schema.json) to get more details on available settings, values and formats. + +### Extra fields and filter queries + +You can explicitly define the `include` section for the query from within the application configuration file. + +```json +{ + "search": { + "include": ["path", "allowableOperations"] + } +} +``` + +In addition, it is also possible to provide a set of queries that are always executed alongside user-defined settings: + +```json +{ + "search": { + "filterQueries": [ + { "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" }, + { "query": "NOT cm:creator:System" } + ] + } +} +``` + +Note that the entries of the `filterQueries` array are joined using the `AND` operator. + ### Categories The Search Settings component and Query Builder require a `categories` section provided within the configuration. -Categories are needed to build widgets so that users can modify the search query at runtime. Every Category can be represented by a single Angular component, which can be either a simple one or a -composite one. +Categories are needed to build widgets so that users can modify the search query at runtime. +Every Category can be represented by a single Angular component, which can be either a simple one or a composite one. ```ts export interface SearchCategory { @@ -115,6 +136,380 @@ export interface SearchWidgetSettings { } ``` +### Facet Fields + +```json +{ + "search": { + "facetFields": [ + { "field": "content.mimetype", "mincount": 1, "label": "Type" }, + { "field": "content.size", "mincount": 1, "label": "Size" }, + { "field": "creator", "mincount": 1, "label": "Creator" }, + { "field": "modifier", "mincount": 1, "label": "Modifier" }, + { "field": "created", "mincount": 1, "label": "Created" } + ] + } +} +``` + +Every field declared within the `facetFields` group is presented by a separate collapsible category at runtime. + +By default, users see only top 5 entries. +If there are more than 5 entries, the "Show more" button is displayed to allow displaying next block of results. + +![Facet Fields](../docassets/images/search-facet-fields.png) + +### Facet Queries + +```json +{ + "search": { + "facetQueries": [ + { "query": "created:2018", "label": "Created This Year" }, + { "query": "content.mimetype", "label": "Type" }, + { "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"}, + { "query": "content.size:[10240 TO 102400]", "label": "Size: small"}, + { "query": "content.size:[102400 TO 1048576]", "label": "Size: medium" }, + { "query": "content.size:[1048576 TO 16777216]", "label": "Size: large" }, + { "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" }, + { "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" } + ] + } +} +``` + +The queries declared in the `facetQueries` are collected into a single collapsible category. + +![Facet Queries](../docassets/images/search-facet-queries.png) + +## Widgets + +You can use external application configuration to define a set of Angular components (aka Search Filter Widgets) +that provide extra features and/or behaviour for the Search Filter component. + +The Search Filter supports the following widgets out of the box: + +- Check List (`check-list`) +- Date Range (`date-range`) +- Number Range (`number-range`) +- Radio List (`radio`) +- Slider (`slider`) +- Text (`text`) + +At runtime, ADF uses `selector` attribute values to map and create corresponding Angular element. + +### Check List Widget + +Provides you with a list of check-boxes, each backed by a particular query fragment. +You can choose a `label` (or i18n resources key) and a `value`, alongside the conditional `operator` (either `AND` or `OR`). + +```json +{ + "search": { + "categories": [ + { + "id": "checkList", + "name": "Check List", + "enabled": true, + "component": { + "selector": "check-list", + "settings": { + "operator": "OR", + "options": [ + { "name": "Folder", "value": "TYPE:'cm:folder'" }, + { "name": "Document", "value": "TYPE:'cm:content'" } + ] + } + } + } + ] + } +} +``` + +![Check List Widget](../docassets/images/search-check-list.png) + +If user checks both boxes, the underlying query will get the following fragment: + +```text +... (TYPE:'cm:folder' OR TYPE:'cm:content') ... +``` + +### Date Range Widget + +Provides ability to select a range between two Dates based on the particular `field`. + +```json +{ + "search": { + "categories": [ + { + "id": "createdDateRange", + "name": "Created Date (range)", + "enabled": true, + "component": { + "selector": "date-range", + "settings": { + "field": "cm:created" + } + } + } + ] + } +} +``` + +![Date Range Widget](../docassets/images/search-date-range.png) + +### Number Range Widget + +Provides ability to select a range between two Numbers based on the particular `field`. + +```json +{ + "search": { + "categories": [ + { + "id": "contentSizeRange", + "name": "Content Size (range)", + "enabled": true, + "component": { + "selector": "number-range", + "settings": { + "field": "cm:content.size" + } + } + } + ] + } +} +``` + +![Number Range Widget](../docassets/images/search-number-range.png) + +### Radio List Widget + +Provides you with a list of radio-boxes, each backed by a particular query fragment. +The behaviour is very similar to those of the `check-list` except `radio` allows selecting only one item. + +```json +{ + "search": { + "categories": [ + { + "id": "queryType", + "name": "Type", + "enabled": true, + "component": { + "selector": "radio", + "settings": { + "field": null, + "options": [ + { "name": "None", "value": "", "default": true }, + { "name": "All", "value": "TYPE:'cm:folder' OR TYPE:'cm:content'" }, + { "name": "Folder", "value": "TYPE:'cm:folder'" }, + { "name": "Document", "value": "TYPE:'cm:content'" } + ] + } + } + } + ] + } +} +``` + +![Radio Widget](../docassets/images/search-radio.png) + +### Slider Widget + +Provides ability to select a numeric range based on `min` and `max` values in the form of horizontal slider. + +```json +{ + "search": { + "categories": [ + { + "id": "contentSize", + "name": "Content Size", + "enabled": true, + "component": { + "selector": "slider", + "settings": { + "field": "cm:content.size", + "min": 0, + "max": 18, + "step": 1, + "thumbLabel": true + } + } + } + ] + } +} +``` + +![Slider Widget](../docassets/images/search-slider.png) + +### Text Widget + +```json +{ + "search": { + "categories": [ + { + "id": "queryName", + "name": "Name", + "enabled": true, + "expanded": true, + "component": { + "selector": "text", + "settings": { + "pattern": "cm:name:'(.*?)'", + "field": "cm:name", + "placeholder": "Enter the name" + } + } + } + ] + } +} +``` + +![Text Widget](../docassets/images/search-text.png) + +## Custom Widgets + +### Implementing custom widget + +It is possible to create custom Angular components that display and/or modify resulting search query. + +You start creating a Search Filter widget by generating a blank Angular component that implements `SearchWidget` interface: + +```ts +export interface SearchWidget { + id: string; + settings?: SearchWidgetSettings; + context?: SearchQueryBuilderService; +} +``` + +Every widget implementation must have an `id`, and may also support external `settings`. +At runtime, every time a new instance of the widget is created, it also receives a reference to the `SearchQueryBuilderService` +so that you component can access query related information, events and methods. + +```ts +@Component({...}) +export class MyComponent implements SearchWidget, OnInit { + + id: string; + settings: SearchWidgetSettings; + context: SearchQueryBuilderService; + + key1: string; + key2: string; +} +``` + +### Reading external settings + +At runtime, ADF provides every Search Filter widget with the `settings` instance, based on the JSON data +that administrator has provided for your widget in the external configuration file. + +It is your responsibility to parse the `settings` property values, convert types or setup defaults. +ADF does not provide any validation of the objects, it only reads from the configuration and passes data to your component instance. + +```ts +@Component({...}) +export class MyComponent implements SearchWidget, OnInit { + + id: string; + settings: SearchWidgetSettings; + context: SearchQueryBuilderService; + + key1: string; + key2: string; + + ngOnInit() { + if (this.settings) { + this.key1 = this.settings['key1']; + this.key2 = this.settings['key2']; + } + } +} +``` + +### Updating final query + +The `SearchQueryBuilderService` keeps track on all query fragments populated by widgets +and composes them together alongside other settings when performing a final query. + +Every query fragment is stored/retrieved using widget `id`. +It is your responsibility to format the query correctly. + +Once your value is ready, update the context and run `update` method to let other components know the query has been changed: + +```ts +@Component({...}) +export class MyComponent implements SearchWidget, OnInit { + + ... + + onUIChanged() { + this.context.queryFragments[this.id] = `some query`; + this.context.update(); + } + +} +``` + +When executed, your fragment will be injected into the resulting query based on the category order in the application configuration file. + +```text +... AND (widget1) AND (some query) AND (widget2) AND ... +``` + +### Registering custom widget + +You can register your own Widgets by utilizing the `SearchFilterService` service: + +```ts +import { MyComponent } from './my-component.ts' + +@Component({...}) +export class MyAppOrComponent { + + constructor(searchFilterService: SearchFilterService) { + searchFilterService.widgets['my-widget'] = MyComponent; + } + +} +``` + +That allows you to declare your widget in the external configuration +and pass custom attributes in case your component supports them: + +```json +{ + "search": { + "categories": [ + { + "id": "someUniqueId", + "name": "String or i18n key", + "enabled": true, + "component": { + "selector": "my-widget", + "settings": { + "key1": "value1", + "key2": "value2", + "keyN": "valueN" + } + } + } + ] + } +} +``` + ## See also - [Search Query Builder service](search-query-builder.service.md) diff --git a/docs/docassets/images/search-check-list.png b/docs/docassets/images/search-check-list.png new file mode 100644 index 0000000000..64bbcdfc2a Binary files /dev/null and b/docs/docassets/images/search-check-list.png differ diff --git a/docs/docassets/images/search-date-range.png b/docs/docassets/images/search-date-range.png new file mode 100644 index 0000000000..691ffd6cdf Binary files /dev/null and b/docs/docassets/images/search-date-range.png differ diff --git a/docs/docassets/images/search-facet-fields.png b/docs/docassets/images/search-facet-fields.png new file mode 100644 index 0000000000..c4216e31ab Binary files /dev/null and b/docs/docassets/images/search-facet-fields.png differ diff --git a/docs/docassets/images/search-facet-queries.png b/docs/docassets/images/search-facet-queries.png new file mode 100644 index 0000000000..a3fdc7702c Binary files /dev/null and b/docs/docassets/images/search-facet-queries.png differ diff --git a/docs/docassets/images/search-number-range.png b/docs/docassets/images/search-number-range.png new file mode 100644 index 0000000000..8b55d13384 Binary files /dev/null and b/docs/docassets/images/search-number-range.png differ diff --git a/docs/docassets/images/search-radio.png b/docs/docassets/images/search-radio.png new file mode 100644 index 0000000000..3c37571477 Binary files /dev/null and b/docs/docassets/images/search-radio.png differ diff --git a/docs/docassets/images/search-slider.png b/docs/docassets/images/search-slider.png new file mode 100644 index 0000000000..3d6d374dd9 Binary files /dev/null and b/docs/docassets/images/search-slider.png differ diff --git a/docs/docassets/images/search-text.png b/docs/docassets/images/search-text.png new file mode 100644 index 0000000000..9f97725533 Binary files /dev/null and b/docs/docassets/images/search-text.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 edbdcf8039..f91aa014ea 100644 --- a/lib/content-services/document-list/components/document-list.component.ts +++ b/lib/content-services/document-list/components/document-list.component.ts @@ -392,9 +392,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte this.loadFolder(); } else if (this.data) { if (changes.node && changes.node.currentValue) { - if (changes.node.currentValue.list.pagination) { - changes.node.currentValue.list.pagination.skipCount = 0; - } this.data.loadPage(changes.node.currentValue); this.onDataReady(changes.node.currentValue); } else if (changes.rowFilter) { diff --git a/lib/content-services/i18n/en.json b/lib/content-services/i18n/en.json index 1363bda3d0..b9e4c0eb53 100644 --- a/lib/content-services/i18n/en.json +++ b/lib/content-services/i18n/en.json @@ -173,11 +173,19 @@ "FILTER": { "ACTIONS": { "CLEAR": "Clear", - "APPLY": "Apply" + "APPLY": "Apply", + "CLEAR-ALL": "Clear all" }, "RANGE": { "FROM": "From", - "TO": "To" + "TO": "To", + "FROM-DATE": "Choose a date", + "TO-DATE": "Choose a date" + }, + "VALIDATION": { + "REQUIRED-VALUE": "Required value", + "NO-DAYS": "No days selected.", + "INVALID-FORMAT": "Invalid Format" } }, "ICONS": { diff --git a/lib/content-services/search/components/search-check-list/search-check-list.component.html b/lib/content-services/search/components/search-check-list/search-check-list.component.html new file mode 100644 index 0000000000..536916a43b --- /dev/null +++ b/lib/content-services/search/components/search-check-list/search-check-list.component.html @@ -0,0 +1,12 @@ + + {{ option.name | translate }} + + +
+ +
diff --git a/lib/content-services/search/components/search-check-list/search-check-list.component.scss b/lib/content-services/search/components/search-check-list/search-check-list.component.scss new file mode 100644 index 0000000000..920a5be2ea --- /dev/null +++ b/lib/content-services/search/components/search-check-list/search-check-list.component.scss @@ -0,0 +1,8 @@ +.adf-search-check-list { + display: flex; + flex-direction: column; + + .mat-checkbox { + margin: 5px; + } +} diff --git a/lib/content-services/search/components/search-check-list/search-check-list.component.spec.ts b/lib/content-services/search/components/search-check-list/search-check-list.component.spec.ts new file mode 100644 index 0000000000..74be8996ce --- /dev/null +++ b/lib/content-services/search/components/search-check-list/search-check-list.component.spec.ts @@ -0,0 +1,118 @@ +/*! + * @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 { SearchCheckListComponent } from './search-check-list.component'; + +describe('SearchCheckListComponent', () => { + + let component: SearchCheckListComponent; + + beforeEach(() => { + component = new SearchCheckListComponent(); + }); + + it('should setup options from settings', () => { + const options: any = [ + { 'name': 'Folder', 'value': "TYPE:'cm:folder'" }, + { 'name': 'Document', 'value': "TYPE:'cm:content'" } + ]; + component.settings = { options: options }; + component.ngOnInit(); + + expect(component.options).toEqual(options); + }); + + it('should setup operator from the settings', () => { + component.settings = { operator: 'AND' }; + component.ngOnInit(); + expect(component.operator).toBe('AND'); + }); + + it('should use OR operator by default', () => { + component.settings = { operator: null }; + component.ngOnInit(); + expect(component.operator).toBe('OR'); + }); + + it('should update query builder on checkbox change', () => { + component.options = [ + { name: 'Folder', value: "TYPE:'cm:folder'", checked: false }, + { name: 'Document', value: "TYPE:'cm:content'", checked: false } + ]; + + component.id = 'checklist'; + component.context = { + queryFragments: {}, + update() {} + }; + + component.ngOnInit(); + + spyOn(component.context, 'update').and.stub(); + + component.changeHandler( + { checked: true }, + component.options[0] + ); + + expect(component.context.queryFragments[component.id]).toEqual(`TYPE:'cm:folder'`); + + component.changeHandler( + { checked: true }, + component.options[1] + ); + + expect(component.context.queryFragments[component.id]).toEqual( + `TYPE:'cm:folder' OR TYPE:'cm:content'` + ); + }); + + it('should reset selected boxes', () => { + component.options = [ + { name: 'Folder', value: "TYPE:'cm:folder'", checked: true }, + { name: 'Document', value: "TYPE:'cm:content'", checked: true } + ]; + + component.reset(); + + expect(component.options[0].checked).toBeFalsy(); + expect(component.options[1].checked).toBeFalsy(); + }); + + it('should update query builder on reset', () => { + component.id = 'checklist'; + component.context = { + queryFragments: { + 'checklist': 'query' + }, + update() {} + }; + spyOn(component.context, 'update').and.stub(); + + component.ngOnInit(); + component.options = [ + { name: 'Folder', value: "TYPE:'cm:folder'", checked: true }, + { name: 'Document', value: "TYPE:'cm:content'", checked: true } + ]; + + component.reset(); + + expect(component.context.update).toHaveBeenCalled(); + expect(component.context.queryFragments[component.id]).toBe(''); + }); + +}); diff --git a/lib/content-services/search/components/search-check-list/search-check-list.component.ts b/lib/content-services/search/components/search-check-list/search-check-list.component.ts new file mode 100644 index 0000000000..317b10c120 --- /dev/null +++ b/lib/content-services/search/components/search-check-list/search-check-list.component.ts @@ -0,0 +1,77 @@ +/*! + * @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, OnInit } from '@angular/core'; +import { MatCheckboxChange } from '@angular/material'; +import { SearchWidget } from '../../search-widget.interface'; +import { SearchWidgetSettings } from '../../search-widget-settings.interface'; +import { SearchQueryBuilderService } from '../../search-query-builder.service'; + +@Component({ + selector: 'adf-search-check-list', + templateUrl: './search-check-list.component.html', + styleUrls: ['./search-check-list.component.scss'], + encapsulation: ViewEncapsulation.None, + host: { class: 'adf-search-check-list' } +}) +export class SearchCheckListComponent implements SearchWidget, OnInit { + + id: string; + settings?: SearchWidgetSettings; + context?: SearchQueryBuilderService; + options: { name: string, value: string, checked: boolean }[] = []; + operator: string = 'OR'; + + ngOnInit(): void { + if (this.settings) { + this.operator = this.settings.operator || 'OR'; + + if (this.settings.options && this.settings.options.length > 0) { + this.options = [...this.settings.options]; + } + } + } + + reset() { + this.options.forEach(opt => { + opt.checked = false; + }); + + if (this.id && this.context) { + this.context.queryFragments[this.id] = ''; + this.context.update(); + } + } + + changeHandler(event: MatCheckboxChange, option: any) { + option.checked = event.checked; + this.flush(); + } + + flush() { + const checkedValues = this.options + .filter(option => option.checked) + .map(option => option.value); + + const query = checkedValues.join(` ${this.operator} `); + + if (this.id && this.context) { + this.context.queryFragments[this.id] = query; + this.context.update(); + } + } +} diff --git a/lib/content-services/search/components/search-date-range/search-date-range.component.html b/lib/content-services/search/components/search-date-range/search-date-range.component.html new file mode 100644 index 0000000000..16c9a68c45 --- /dev/null +++ b/lib/content-services/search/components/search-date-range/search-date-range.component.html @@ -0,0 +1,36 @@ +
+ + + + + + + {{ 'SEARCH.FILTER.VALIDATION.REQUIRED-VALUE' | translate }} + + + + + + + + + {{ 'SEARCH.FILTER.VALIDATION.NO-DAYS' | translate }} + + + +
+ + +
+
+ diff --git a/lib/content-services/search/components/search-date-range/search-date-range.component.scss b/lib/content-services/search/components/search-date-range/search-date-range.component.scss new file mode 100644 index 0000000000..3bb0f88e60 --- /dev/null +++ b/lib/content-services/search/components/search-date-range/search-date-range.component.scss @@ -0,0 +1,8 @@ +.adf-search-date-range > form { + display: inline-flex; + flex-direction: column; + + .mat-button { + text-transform: uppercase; + } +} diff --git a/lib/content-services/search/components/search-date-range/search-date-range.component.spec.ts b/lib/content-services/search/components/search-date-range/search-date-range.component.spec.ts new file mode 100644 index 0000000000..19f6fd0e97 --- /dev/null +++ b/lib/content-services/search/components/search-date-range/search-date-range.component.spec.ts @@ -0,0 +1,98 @@ +/*! + * @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 { SearchDateRangeComponent } from './search-date-range.component'; +import moment from 'moment-es6'; + +describe('SearchDateRangeComponent', () => { + + let component: SearchDateRangeComponent; + let fromDate = '2016-10-16'; + let toDate = '2017-10-16'; + + beforeEach(() => { + component = new SearchDateRangeComponent(); + }); + + it('should setup form elements on init', () => { + component.ngOnInit(); + expect(component.form).toBeDefined(); + expect(component.to).toBeDefined(); + expect(component.form).toBeDefined(); + }); + + it('should reset form', () => { + component.ngOnInit(); + component.form.setValue({ from: fromDate, to: toDate }); + + expect(component.from.value).toEqual(fromDate); + expect(component.to.value).toEqual(toDate); + + component.reset(); + + expect(component.from.value).toEqual(''); + expect(component.to.value).toEqual(''); + expect(component.form.value).toEqual({ from: '', to: '' }); + }); + + it('should update query builder on reset', () => { + const context: any = { + queryFragments: { + createdDateRange: 'query' + }, + update() {} + }; + + component.id = 'createdDateRange'; + component.context = context; + + spyOn(context, 'update').and.stub(); + + component.ngOnInit(); + component.reset(); + + expect(context.queryFragments.createdDateRange).toEqual(''); + expect(context.update).toHaveBeenCalled(); + }); + + it('should update query builder on value changes', () => { + const context: any = { + queryFragments: {}, + update() {} + }; + + component.id = 'createdDateRange'; + component.context = context; + component.settings = { field: 'cm:created' }; + + spyOn(context, 'update').and.stub(); + + component.ngOnInit(); + component.apply({ + from: fromDate, + to: toDate + }, true); + + const startDate = moment(fromDate).startOf('day').format(); + const endDate = moment(toDate).endOf('day').format(); + + const expectedQuery = `cm:created:['${startDate}' TO '${endDate}']`; + expect(context.queryFragments[component.id]).toEqual(expectedQuery); + expect(context.update).toHaveBeenCalled(); + }); + +}); diff --git a/lib/content-services/search/components/search-date-range/search-date-range.component.ts b/lib/content-services/search/components/search-date-range/search-date-range.component.ts new file mode 100644 index 0000000000..52924b1f8b --- /dev/null +++ b/lib/content-services/search/components/search-date-range/search-date-range.component.ts @@ -0,0 +1,92 @@ +/*! + * @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 { OnInit, Component, ViewEncapsulation } from '@angular/core'; +import { FormControl, Validators, FormGroup } from '@angular/forms'; +import { SearchWidget } from '../../search-widget.interface'; +import { SearchWidgetSettings } from '../../search-widget-settings.interface'; +import { SearchQueryBuilderService } from '../../search-query-builder.service'; +import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher'; +import moment from 'moment-es6'; + +@Component({ + selector: 'adf-search-date-range', + templateUrl: './search-date-range.component.html', + styleUrls: ['./search-date-range.component.scss'], + encapsulation: ViewEncapsulation.None, + host: { class: 'adf-search-date-range' } +}) +export class SearchDateRangeComponent implements SearchWidget, OnInit { + + from: FormControl; + to: FormControl; + + form: FormGroup; + matcher = new LiveErrorStateMatcher(); + + id: string; + settings?: SearchWidgetSettings; + context?: SearchQueryBuilderService; + maxFrom: any; + + ngOnInit() { + const validators = Validators.compose([ + Validators.required + ]); + + this.from = new FormControl('', validators); + this.to = new FormControl('', validators); + + this.form = new FormGroup({ + from: this.from, + to: this.to + }); + + this.maxFrom = moment().startOf('day'); + } + + apply(model: { from: string, to: string }, isValid: boolean) { + if (isValid && this.id && this.context && this.settings && this.settings.field) { + const start = moment(model.from).startOf('day').format(); + const end = moment(model.to).endOf('day').format(); + + this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`; + this.context.update(); + } + } + + reset() { + this.form.reset({ + from: '', + to: '' + }); + if (this.id && this.context) { + this.context.queryFragments[this.id] = ''; + this.context.update(); + } + } + + hasSelectedDays(from: string, to: string): boolean { + if (from && to) { + const start = moment(from).startOf('day'); + const end = moment(to).endOf('day'); + + return start.isBefore(end); + } + return true; + } +} diff --git a/lib/content-services/search/components/search-filter/search-filter.component.html b/lib/content-services/search/components/search-filter/search-filter.component.html index 159329e01c..957ef03bb9 100644 --- a/lib/content-services/search/components/search-filter/search-filter.component.html +++ b/lib/content-services/search/components/search-filter/search-filter.component.html @@ -35,7 +35,7 @@ @@ -43,12 +43,19 @@
{{ bucket.display || bucket.label }} ({{ bucket.count }})
+ +
diff --git a/lib/content-services/search/components/search-filter/search-filter.component.spec.ts b/lib/content-services/search/components/search-filter/search-filter.component.spec.ts index f1a914dfb4..f4085e4626 100644 --- a/lib/content-services/search/components/search-filter/search-filter.component.spec.ts +++ b/lib/content-services/search/components/search-filter/search-filter.component.spec.ts @@ -64,19 +64,19 @@ describe('SearchSettingsComponent', () => { }); it('should update facet field model on expand', () => { - const field: any = { $expanded: false }; + const field: any = { expanded: false }; component.onFacetFieldExpanded(field); - expect(field.$expanded).toBeTruthy(); + expect(field.expanded).toBeTruthy(); }); it('should update facet field model on collapse', () => { - const field: any = { $expanded: true }; + const field: any = { expanded: true }; component.onFacetFieldCollapsed(field); - expect(field.$expanded).toBeFalsy(); + expect(field.expanded).toBeFalsy(); }); it('should update bucket model and query builder on facet toggle', () => { @@ -118,6 +118,7 @@ describe('SearchSettingsComponent', () => { it('should unselect facet query and update builder', () => { const config: SearchConfiguration = { + categories: [], facetQueries: [ { label: 'q1', query: 'query1' } ] @@ -238,7 +239,7 @@ describe('SearchSettingsComponent', () => { it('should fetch facet fields from response payload', () => { component.responseFacetFields = []; - const fields = [ + const fields: any = [ { label: 'f1', buckets: [] }, { label: 'f2', buckets: [] } ]; @@ -256,9 +257,9 @@ describe('SearchSettingsComponent', () => { }); it('should restore expanded state for new response facet fields', () => { - component.responseFacetFields = [ + component.responseFacetFields = [ { label: 'f1', buckets: [] }, - { label: 'f2', buckets: [], $expanded: true } + { label: 'f2', buckets: [], expanded: true } ]; const fields = [ @@ -276,8 +277,8 @@ describe('SearchSettingsComponent', () => { component.onDataLoaded(data); expect(component.responseFacetFields.length).toBe(2); - expect(component.responseFacetFields[0].$expanded).toBeFalsy(); - expect(component.responseFacetFields[1].$expanded).toBeTruthy(); + expect(component.responseFacetFields[0].expanded).toBeFalsy(); + expect(component.responseFacetFields[1].expanded).toBeTruthy(); }); it('should restore checked buckets for new response facet fields', () => { @@ -285,7 +286,7 @@ describe('SearchSettingsComponent', () => { const bucket2 = { label: 'b2', $field: 'f2', count: 1, filterQuery: 'q2' }; component.selectedBuckets = [ bucket2 ]; - component.responseFacetFields = [ + component.responseFacetFields = [ { label: 'f2', buckets: [] } ]; diff --git a/lib/content-services/search/components/search-filter/search-filter.component.ts b/lib/content-services/search/components/search-filter/search-filter.component.ts index 9c261cc7b5..54d5030eb5 100644 --- a/lib/content-services/search/components/search-filter/search-filter.component.ts +++ b/lib/content-services/search/components/search-filter/search-filter.component.ts @@ -63,11 +63,11 @@ export class SearchFilterComponent implements OnInit { } onFacetFieldExpanded(field: ResponseFacetField) { - field.$expanded = true; + field.expanded = true; } onFacetFieldCollapsed(field: ResponseFacetField) { - field.$expanded = false; + field.expanded = false; } onFacetQueryToggle(event: MatCheckboxChange, query: ResponseFacetQuery) { @@ -143,11 +143,13 @@ export class SearchFilterComponent implements OnInit { return q; }); - const expandedFields = this.responseFacetFields.filter(f => f.$expanded).map(f => f.label); + const expandedFields = this.responseFacetFields.filter(f => f.expanded).map(f => f.label); this.responseFacetFields = (context.facetsFields || []).map( (field: ResponseFacetField) => { - field.$expanded = expandedFields.includes(field.label); + field.pageSize = field.pageSize || 5; + field.currentPageSize = field.pageSize; + field.expanded = expandedFields.includes(field.label); (field.buckets || []).forEach(bucket => { bucket.$field = field.label; @@ -160,6 +162,20 @@ export class SearchFilterComponent implements OnInit { bucket.$checked = true; } }); + + field.hasMoreItems = (): boolean => { + return field.buckets && field.buckets.length > 0 && field.buckets.length > field.currentPageSize; + }; + + field.showMoreItems = () => { + field.currentPageSize += field.pageSize; + }; + + field.getVisibleBuckets = (): FacetFieldBucket[] => { + const buckets = field.buckets || []; + return buckets.slice(0, field.currentPageSize); + }; + return field; } ); @@ -168,5 +184,4 @@ export class SearchFilterComponent implements OnInit { this.responseFacetFields = []; } } - } diff --git a/lib/content-services/search/components/search-filter/search-filter.service.ts b/lib/content-services/search/components/search-filter/search-filter.service.ts index c1010939aa..57fe9fda88 100644 --- a/lib/content-services/search/components/search-filter/search-filter.service.ts +++ b/lib/content-services/search/components/search-filter/search-filter.service.ts @@ -20,6 +20,8 @@ import { SearchTextComponent } from '../search-text/search-text.component'; import { SearchRadioComponent } from '../search-radio/search-radio.component'; import { SearchSliderComponent } from '../search-slider/search-slider.component'; import { SearchNumberRangeComponent } from '../search-number-range/search-number-range.component'; +import { SearchCheckListComponent } from '../search-check-list/search-check-list.component'; +import { SearchDateRangeComponent } from '../search-date-range/search-date-range.component'; @Injectable() export class SearchFilterService { @@ -31,7 +33,9 @@ export class SearchFilterService { 'text': SearchTextComponent, 'radio': SearchRadioComponent, 'slider': SearchSliderComponent, - 'number-range': SearchNumberRangeComponent + 'number-range': SearchNumberRangeComponent, + 'check-list': SearchCheckListComponent, + 'date-range': SearchDateRangeComponent }; } diff --git a/lib/content-services/search/components/search-number-range/search-number-range.component.html b/lib/content-services/search/components/search-number-range/search-number-range.component.html index 6a25a88bed..b070a0074b 100644 --- a/lib/content-services/search/components/search-number-range/search-number-range.component.html +++ b/lib/content-services/search/components/search-number-range/search-number-range.component.html @@ -4,15 +4,21 @@ - Invalid format - Required value + + {{ 'SEARCH.FILTER.VALIDATION.INVALID-FORMAT' | translate }} + + + {{ 'SEARCH.FILTER.VALIDATION.REQUIRED-VALUE' | translate }} + - Invalid format + + {{ 'SEARCH.FILTER.VALIDATION.INVALID-FORMAT' | translate }} + diff --git a/lib/content-services/search/facet-field.interface.ts b/lib/content-services/search/facet-field.interface.ts index 5edb82408a..bba89a325e 100644 --- a/lib/content-services/search/facet-field.interface.ts +++ b/lib/content-services/search/facet-field.interface.ts @@ -19,6 +19,9 @@ export interface FacetField { field: string; label: string; mincount?: number; + limit?: number; + offset?: number; + prefix?: string; $checked?: boolean; } diff --git a/lib/content-services/search/response-facet-field.interface.ts b/lib/content-services/search/response-facet-field.interface.ts index f14bf6d230..61a6a754ed 100644 --- a/lib/content-services/search/response-facet-field.interface.ts +++ b/lib/content-services/search/response-facet-field.interface.ts @@ -20,6 +20,11 @@ import { FacetFieldBucket } from './facet-field-bucket.interface'; export interface ResponseFacetField { label: string; buckets: Array; + pageSize?: number; + currentPageSize?: number; + expanded?: boolean; - $expanded?: boolean; + hasMoreItems(): boolean; + showMoreItems(): void; + getVisibleBuckets(): Array; } diff --git a/lib/content-services/search/search-configuration.interface.ts b/lib/content-services/search/search-configuration.interface.ts index 30948fcf8f..f948453921 100644 --- a/lib/content-services/search/search-configuration.interface.ts +++ b/lib/content-services/search/search-configuration.interface.ts @@ -23,12 +23,8 @@ import { SearchCategory } from './search-category.interface'; export interface SearchConfiguration { include?: Array; fields?: Array; - query?: { - categories: Array - }; + categories: Array; filterQueries?: Array; facetQueries?: Array; - facetFields?: { - facets: Array - }; + facetFields?: 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 52f75332a1..22884d64b7 100644 --- a/lib/content-services/search/search-query-builder.service.spec.ts +++ b/lib/content-services/search/search-query-builder.service.spec.ts @@ -37,13 +37,11 @@ describe('SearchQueryBuilder', () => { it('should use only enabled categories', () => { const config: SearchConfiguration = { - query: { - categories: [ - { id: 'cat1', enabled: true }, - { id: 'cat2', enabled: false }, - { id: 'cat3', enabled: true } - ] - } + categories: [ + { id: 'cat1', enabled: true }, + { id: 'cat2', enabled: false }, + { id: 'cat3', enabled: true } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); @@ -54,9 +52,7 @@ describe('SearchQueryBuilder', () => { it('should fetch filter queries from config', () => { const config: SearchConfiguration = { - query: { - categories: [] - }, + categories: [], filterQueries: [ { query: 'query1' }, { query: 'query2' } @@ -123,6 +119,7 @@ describe('SearchQueryBuilder', () => { it('should fetch facet query from config', () => { const config: SearchConfiguration = { + categories: [], facetQueries: [ { query: 'q1', label: 'query1' }, { query: 'q2', label: 'query2' } @@ -137,6 +134,7 @@ describe('SearchQueryBuilder', () => { it('should not fetch empty facet query from the config', () => { const config: SearchConfiguration = { + categories: [], facetQueries: [ { query: 'q1', label: 'query1' } ] @@ -179,11 +177,9 @@ describe('SearchQueryBuilder', () => { it('should require a query fragment to build query', () => { const config: SearchConfiguration = { - query: { - categories: [ - { id: 'cat1', enabled: true } - ] - } + categories: [ + { id: 'cat1', enabled: true } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); builder.queryFragments['cat1'] = null; @@ -194,11 +190,9 @@ describe('SearchQueryBuilder', () => { it('should build query with single fragment', () => { const config: SearchConfiguration = { - query: { - categories: [ - { id: 'cat1', enabled: true } - ] - } + categories: [ + { id: 'cat1', enabled: true } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); @@ -210,12 +204,10 @@ describe('SearchQueryBuilder', () => { it('should build query with multiple fragments', () => { const config: SearchConfiguration = { - query: { - categories: [ - { id: 'cat1', enabled: true }, - { id: 'cat2', enabled: true } - ] - } + categories: [ + { id: 'cat1', enabled: true }, + { id: 'cat2', enabled: true } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); @@ -231,12 +223,10 @@ describe('SearchQueryBuilder', () => { it('should build query with custom fields', () => { const config: SearchConfiguration = { fields: ['field1', 'field2'], - query: { - categories: [ - { id: 'cat1', enabled: true }, - { id: 'cat2', enabled: true } - ] - } + categories: [ + { id: 'cat1', enabled: true }, + { id: 'cat2', enabled: true } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); @@ -249,12 +239,10 @@ describe('SearchQueryBuilder', () => { it('should build query with empty custom fields', () => { const config: SearchConfiguration = { fields: [], - query: { - categories: [ - { id: 'cat1', enabled: true }, - { id: 'cat2', enabled: true } - ] - } + categories: [ + { id: 'cat1', enabled: true }, + { id: 'cat2', enabled: true } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); @@ -266,11 +254,9 @@ describe('SearchQueryBuilder', () => { it('should build query with custom filter queries', () => { const config: SearchConfiguration = { - query: { - categories: [ - { id: 'cat1', enabled: true } - ] - } + categories: [ + { id: 'cat1', enabled: true } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); builder.queryFragments['cat1'] = 'cm:name:test'; @@ -284,11 +270,9 @@ describe('SearchQueryBuilder', () => { it('should build query with custom facet queries', () => { const config: SearchConfiguration = { - query: { - categories: [ - { id: 'cat1', enabled: true } - ] - }, + categories: [ + { id: 'cat1', enabled: true } + ], facetQueries: [ { query: 'q1', label: 'q2' } ] @@ -302,32 +286,26 @@ describe('SearchQueryBuilder', () => { it('should build query with custom facet fields', () => { const config: SearchConfiguration = { - query: { - categories: [ - { id: 'cat1', enabled: true } - ] - }, - facetFields: { - facets: [ - { field: 'field1', label: 'field1' }, - { field: 'field2', label: 'field2' } - ] - } + categories: [ + { id: 'cat1', enabled: true } + ], + facetFields: [ + { field: 'field1', label: 'field1', mincount: 1, limit: null, offset: 0, prefix: null }, + { field: 'field2', label: 'field2', mincount: 1, limit: null, offset: 0, prefix: null } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); builder.queryFragments['cat1'] = 'cm:name:test'; const compiled = builder.buildQuery(); - expect(compiled.facetFields).toEqual(config.facetFields); + expect(compiled.facetFields.facets).toEqual(jasmine.objectContaining(config.facetFields)); }); it('should use pagination settings', () => { const config: SearchConfiguration = { - query: { - categories: [ - { id: 'cat1', enabled: true } - ] - } + categories: [ + { id: 'cat1', enabled: true } + ] }; const builder = new SearchQueryBuilderService(buildConfig(config), null); builder.queryFragments['cat1'] = 'cm:name:test'; diff --git a/lib/content-services/search/search-query-builder.service.ts b/lib/content-services/search/search-query-builder.service.ts index 8ad60f1c26..5cc7dd2369 100644 --- a/lib/content-services/search/search-query-builder.service.ts +++ b/lib/content-services/search/search-query-builder.service.ts @@ -18,7 +18,7 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs/Subject'; import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core'; -import { QueryBody } from 'alfresco-js-api'; +import { QueryBody, RequestFacetFields, RequestFacetField } from 'alfresco-js-api'; import { SearchCategory } from './search-category.interface'; import { FilterQuery } from './filter-query.interface'; import { SearchRange } from './search-range.interface'; @@ -45,8 +45,8 @@ export class SearchQueryBuilderService { throw new Error('Search configuration not found.'); } - if (this.config.query && this.config.query.categories) { - this.categories = this.config.query.categories.filter(f => f.enabled); + if (this.config.categories) { + this.categories = this.config.categories.filter(f => f.enabled); } this.filterQueries = this.config.filterQueries || []; @@ -116,7 +116,7 @@ export class SearchQueryBuilderService { fields: this.config.fields, filterQueries: this.filterQueries, facetQueries: this.config.facetQueries, - facetFields: this.config.facetFields + facetFields: this.facetFields }; return result; @@ -124,4 +124,23 @@ export class SearchQueryBuilderService { return null; } + + private get facetFields(): RequestFacetFields { + const facetFields = this.config.facetFields; + + if (facetFields && facetFields.length > 0) { + return { + facets: facetFields.map(facet => { + field: facet.field, + mincount: facet.mincount, + label: facet.label, + limit: facet.limit, + offset: facet.offset, + prefix: facet.prefix + }) + }; + } + + return null; + } } diff --git a/lib/content-services/search/search.module.ts b/lib/content-services/search/search.module.ts index 59663158fb..53fb0af7a6 100644 --- a/lib/content-services/search/search.module.ts +++ b/lib/content-services/search/search.module.ts @@ -35,6 +35,8 @@ import { SearchTextComponent } from './components/search-text/search-text.compon import { SearchRadioComponent } from './components/search-radio/search-radio.component'; import { SearchSliderComponent } from './components/search-slider/search-slider.component'; 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'; export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [ SearchComponent, @@ -60,7 +62,9 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [ SearchTextComponent, SearchRadioComponent, SearchSliderComponent, - SearchNumberRangeComponent + SearchNumberRangeComponent, + SearchCheckListComponent, + SearchDateRangeComponent ], exports: [ ...ALFRESCO_SEARCH_DIRECTIVES, @@ -68,14 +72,18 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [ SearchTextComponent, SearchRadioComponent, SearchSliderComponent, - SearchNumberRangeComponent + SearchNumberRangeComponent, + SearchCheckListComponent, + SearchDateRangeComponent ], entryComponents: [ SearchWidgetContainerComponent, SearchTextComponent, SearchRadioComponent, SearchSliderComponent, - SearchNumberRangeComponent + SearchNumberRangeComponent, + SearchCheckListComponent, + SearchDateRangeComponent ] }) export class SearchModule {} diff --git a/lib/core/app-config/schema.json b/lib/core/app-config/schema.json index 711c7650d6..6e77397b17 100644 --- a/lib/core/app-config/schema.json +++ b/lib/core/app-config/schema.json @@ -480,21 +480,25 @@ "search": { "description": "Search configuration parameters", "type": "object", + "required": [ "categories" ], "properties": { "include": { "type": "array", + "minItems": 1, "items": { "type": "string" } }, "fields": { "type": "array", + "minItems": 1, "items": { "type": "string" } }, "filterQueries": { "type": "array", + "minItems": 1, "items": { "type": "object", "required": [ "query" ], @@ -504,20 +508,33 @@ } }, "facetFields": { - "type": "object", - "required": [ "facets" ], - "properties": { - "facets": { - "type": "array", - "items": { - "type": "object", - "required": [ "field", "mincount", "label" ], - "properties": { - "field": { "type": "string" }, - "mincount": { "type": "integer" }, - "label": { "type": "string" } - } - } + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ "field", "mincount", "label" ], + "properties": { + "field": { + "type": "string", + "description": "This specifies the facet field." + }, + "mincount": { + "type": "integer", + "description": "This specifies the minimum count required for a facet field to be included in the response. The default value is 1." + }, + "label": { + "type": "string", + "description": "This specifies the label to include in place of the facet field." + }, + "prefix": { + "type": "string", + "description": "This restricts the possible constraints to only indexed values with a specified prefix." + }, + "limit": { + "type": "integer", + "description": "Maximum number of results" + }, + "offset": { "type": "integer" } } } }, @@ -532,36 +549,29 @@ } } }, - "query": { - "type": "object", - "required": [ "categories" ], - "properties": { - "categories": { - "type": "array", - "minItems": 1, - "items": { + "categories": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ "id", "name" ], + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "enabled": { "type": "boolean" }, + "expanded": { "type": "boolean" }, + "component": { "type": "object", - "required": [ "id", "name" ], + "required": [ "selector", "settings" ], "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "enabled": { "type": "boolean" }, - "expanded": { "type": "boolean" }, - "component": { - "type": "object", - "required": [ "selector", "settings" ], - "properties": { - "selector": { "type": "string" }, - "settings": { "type": "object" } - } - } + "selector": { "type": "string" }, + "settings": { "type": "object" } } } } } } } - } } }