[ADF-2846] new facets (#3251)
* schema and configuration improvements * check list search widget * "Clear all" button to reset the list * page size and "show more" for response facet fields * test fixes * fix tests * fix pagination bug (skipCount reseting) * integrate date range picker from #3248 * i18n support for date and number range * some docs for search filter * docs update * docs update * cleanup code as per review
1
.vscode/settings.json
vendored
@ -20,6 +20,7 @@
|
|||||||
"MD031" : false
|
"MD031" : false
|
||||||
},
|
},
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"mincount",
|
||||||
"webscript"
|
"webscript"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -54,19 +54,17 @@
|
|||||||
],
|
],
|
||||||
"search": {
|
"search": {
|
||||||
"include": ["path", "allowableOperations"],
|
"include": ["path", "allowableOperations"],
|
||||||
"fields": [],
|
|
||||||
"filterQueries": [
|
"filterQueries": [
|
||||||
{ "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
|
{ "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
|
||||||
{ "query": "NOT cm:creator:System" }
|
{ "query": "NOT cm:creator:System" }
|
||||||
],
|
],
|
||||||
"facetFields": {
|
"facetFields": [
|
||||||
"facets": [
|
{ "field": "content.mimetype", "mincount": 1, "label": "Type" },
|
||||||
{ "field": "content.mimetype", "mincount": 1, "label": "Type" },
|
{ "field": "content.size", "mincount": 1, "label": "Size" },
|
||||||
{ "field": "content.size", "mincount": 1, "label": "Size" },
|
{ "field": "creator", "mincount": 1, "label": "Creator" },
|
||||||
{ "field": "creator", "mincount": 1, "label": "Creator" },
|
{ "field": "modifier", "mincount": 1, "label": "Modifier" },
|
||||||
{ "field": "modifier", "mincount": 1, "label": "Modifier" }
|
{ "field": "created", "mincount": 1, "label": "Created" }
|
||||||
]
|
],
|
||||||
},
|
|
||||||
"facetQueries": [
|
"facetQueries": [
|
||||||
{ "query": "created:2018", "label": "Created This Year" },
|
{ "query": "created:2018", "label": "Created This Year" },
|
||||||
{ "query": "content.mimetype", "label": "Type" },
|
{ "query": "content.mimetype", "label": "Type" },
|
||||||
@ -77,67 +75,91 @@
|
|||||||
{ "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" },
|
{ "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" },
|
||||||
{ "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" }
|
{ "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" }
|
||||||
],
|
],
|
||||||
"query": {
|
"categories": [
|
||||||
"categories": [
|
{
|
||||||
{
|
"id": "queryName",
|
||||||
"id": "queryName",
|
"name": "Name",
|
||||||
"name": "Name",
|
"enabled": true,
|
||||||
"enabled": true,
|
"expanded": true,
|
||||||
"expanded": true,
|
"component": {
|
||||||
"component": {
|
"selector": "text",
|
||||||
"selector": "text",
|
"settings": {
|
||||||
"settings": {
|
"pattern": "cm:name:'(.*?)'",
|
||||||
"pattern": "cm:name:'(.*?)'",
|
"field": "cm:name",
|
||||||
"field": "cm:name",
|
"placeholder": "Enter the 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'" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
}
|
{
|
||||||
|
"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": {
|
"pagination": {
|
||||||
"size": 25,
|
"size": 25,
|
||||||
|
@ -34,17 +34,14 @@ export class SearchResultComponent implements OnInit {
|
|||||||
queryParamName = 'q';
|
queryParamName = 'q';
|
||||||
searchedWord = '';
|
searchedWord = '';
|
||||||
resultNodePageList: NodePaging;
|
resultNodePageList: NodePaging;
|
||||||
maxItems: number;
|
|
||||||
skipCount = 0;
|
|
||||||
pagination: Pagination;
|
pagination: Pagination;
|
||||||
|
|
||||||
constructor(public router: Router,
|
constructor(public router: Router,
|
||||||
private preferences: UserPreferencesService,
|
private preferences: UserPreferencesService,
|
||||||
private queryBuilder: SearchQueryBuilderService,
|
private queryBuilder: SearchQueryBuilderService,
|
||||||
@Optional() private route: ActivatedRoute) {
|
@Optional() private route: ActivatedRoute) {
|
||||||
this.maxItems = this.preferences.paginationSize;
|
|
||||||
queryBuilder.paging = {
|
queryBuilder.paging = {
|
||||||
maxItems: this.maxItems,
|
maxItems: this.preferences.paginationSize,
|
||||||
skipCount: 0
|
skipCount: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -57,18 +54,14 @@ export class SearchResultComponent implements OnInit {
|
|||||||
this.queryBuilder.update();
|
this.queryBuilder.update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.maxItems = this.preferences.paginationSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchResultLoaded(nodePaging: NodePaging) {
|
onSearchResultLoaded(nodePaging: NodePaging) {
|
||||||
this.resultNodePageList = nodePaging;
|
this.resultNodePageList = nodePaging;
|
||||||
this.pagination = nodePaging.list.pagination;
|
this.pagination = {...nodePaging.list.pagination };
|
||||||
}
|
}
|
||||||
|
|
||||||
onRefreshPagination(pagination: Pagination) {
|
onRefreshPagination(pagination: Pagination) {
|
||||||
this.maxItems = pagination.maxItems;
|
|
||||||
this.skipCount = pagination.skipCount;
|
|
||||||
|
|
||||||
this.queryBuilder.paging = {
|
this.queryBuilder.paging = {
|
||||||
maxItems: pagination.maxItems,
|
maxItems: pagination.maxItems,
|
||||||
skipCount: pagination.skipCount
|
skipCount: pagination.skipCount
|
||||||
|
@ -28,22 +28,16 @@ Below is an example configuration:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"search": {
|
"search": {
|
||||||
"limits": {
|
|
||||||
"permissionEvaluationTime": null,
|
|
||||||
"permissionEvaluationCount": null
|
|
||||||
},
|
|
||||||
"filterQueries": [
|
"filterQueries": [
|
||||||
{ "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
|
{ "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
|
||||||
{ "query": "NOT cm:creator:System" }
|
{ "query": "NOT cm:creator:System" }
|
||||||
],
|
],
|
||||||
"facetFields": {
|
"facetFields": [
|
||||||
"facets": [
|
|
||||||
{ "field": "content.mimetype", "mincount": 1, "label": "Type" },
|
{ "field": "content.mimetype", "mincount": 1, "label": "Type" },
|
||||||
{ "field": "content.size", "mincount": 1, "label": "Size" },
|
{ "field": "content.size", "mincount": 1, "label": "Size" },
|
||||||
{ "field": "creator", "mincount": 1, "label": "Creator" },
|
{ "field": "creator", "mincount": 1, "label": "Creator" },
|
||||||
{ "field": "modifier", "mincount": 1, "label": "Modifier" }
|
{ "field": "modifier", "mincount": 1, "label": "Modifier" }
|
||||||
]
|
],
|
||||||
},
|
|
||||||
"facetQueries": [
|
"facetQueries": [
|
||||||
{ "query": "created:2018", "label": "Created This Year" },
|
{ "query": "created:2018", "label": "Created This Year" },
|
||||||
{ "query": "content.mimetype", "label": "Type" },
|
{ "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:[16777216 TO 134217728]", "label": "Size: xtra large" },
|
||||||
{ "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" }
|
{ "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" }
|
||||||
],
|
],
|
||||||
"query": {
|
"categories": [
|
||||||
"categories": [
|
{
|
||||||
{
|
"id": "queryName",
|
||||||
"id": "queryName",
|
"name": "Name",
|
||||||
"name": "Name",
|
"enabled": true,
|
||||||
"enabled": true,
|
"expanded": true,
|
||||||
"expanded": true,
|
"component": {
|
||||||
"component": {
|
"selector": "adf-search-text",
|
||||||
"selector": "adf-search-text",
|
"settings": {
|
||||||
"settings": {
|
"pattern": "cm:name:'(.*?)'",
|
||||||
"pattern": "cm:name:'(.*?)'",
|
"field": "cm:name",
|
||||||
"field": "cm:name",
|
"placeholder": "Enter the 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
|
### Categories
|
||||||
|
|
||||||
The Search Settings component and Query Builder require a `categories` section provided within the configuration.
|
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
|
Categories are needed to build widgets so that users can modify the search query at runtime.
|
||||||
composite one.
|
Every Category can be represented by a single Angular component, which can be either a simple one or a composite one.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export interface SearchCategory {
|
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 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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'" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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'" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
|
## See also
|
||||||
|
|
||||||
- [Search Query Builder service](search-query-builder.service.md)
|
- [Search Query Builder service](search-query-builder.service.md)
|
||||||
|
BIN
docs/docassets/images/search-check-list.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/docassets/images/search-date-range.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/docassets/images/search-facet-fields.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
docs/docassets/images/search-facet-queries.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
docs/docassets/images/search-number-range.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
docs/docassets/images/search-radio.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
docs/docassets/images/search-slider.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
docs/docassets/images/search-text.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
@ -392,9 +392,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
|||||||
this.loadFolder();
|
this.loadFolder();
|
||||||
} else if (this.data) {
|
} else if (this.data) {
|
||||||
if (changes.node && changes.node.currentValue) {
|
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.data.loadPage(changes.node.currentValue);
|
||||||
this.onDataReady(changes.node.currentValue);
|
this.onDataReady(changes.node.currentValue);
|
||||||
} else if (changes.rowFilter) {
|
} else if (changes.rowFilter) {
|
||||||
|
@ -173,11 +173,19 @@
|
|||||||
"FILTER": {
|
"FILTER": {
|
||||||
"ACTIONS": {
|
"ACTIONS": {
|
||||||
"CLEAR": "Clear",
|
"CLEAR": "Clear",
|
||||||
"APPLY": "Apply"
|
"APPLY": "Apply",
|
||||||
|
"CLEAR-ALL": "Clear all"
|
||||||
},
|
},
|
||||||
"RANGE": {
|
"RANGE": {
|
||||||
"FROM": "From",
|
"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": {
|
"ICONS": {
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
<mat-checkbox
|
||||||
|
*ngFor="let option of options"
|
||||||
|
[checked]="option.checked"
|
||||||
|
(change)="changeHandler($event, option)">
|
||||||
|
{{ option.name | translate }}
|
||||||
|
</mat-checkbox>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button mat-button color="primary" (click)="reset()">
|
||||||
|
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,8 @@
|
|||||||
|
.adf-search-check-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.mat-checkbox {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -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 = <any> { options: options };
|
||||||
|
component.ngOnInit();
|
||||||
|
|
||||||
|
expect(component.options).toEqual(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup operator from the settings', () => {
|
||||||
|
component.settings = <any> { operator: 'AND' };
|
||||||
|
component.ngOnInit();
|
||||||
|
expect(component.operator).toBe('AND');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use OR operator by default', () => {
|
||||||
|
component.settings = <any> { 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 = <any> {
|
||||||
|
queryFragments: {},
|
||||||
|
update() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
|
||||||
|
spyOn(component.context, 'update').and.stub();
|
||||||
|
|
||||||
|
component.changeHandler(
|
||||||
|
<any> { checked: true },
|
||||||
|
component.options[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component.context.queryFragments[component.id]).toEqual(`TYPE:'cm:folder'`);
|
||||||
|
|
||||||
|
component.changeHandler(
|
||||||
|
<any> { 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 = <any> {
|
||||||
|
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('');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
<form [formGroup]="form" novalidate (ngSubmit)="apply(form.value, form.valid)">
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput [formControl]="from" [errorStateMatcher]="matcher"
|
||||||
|
placeholder="{{ 'SEARCH.FILTER.RANGE.FROM-DATE' | translate }}"
|
||||||
|
[matDatepicker]="fromDatepicker"
|
||||||
|
[max]="maxFrom">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="fromDatepicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #fromDatepicker></mat-datepicker>
|
||||||
|
<mat-error *ngIf="from.hasError('required')">
|
||||||
|
{{ 'SEARCH.FILTER.VALIDATION.REQUIRED-VALUE' | translate }}
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput [formControl]="to" [errorStateMatcher]="matcher"
|
||||||
|
placeholder="{{ 'SEARCH.FILTER.RANGE.TO-DATE' | translate }}"
|
||||||
|
[matDatepicker]="toDatepicker"
|
||||||
|
[min]="from.value">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="toDatepicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #toDatepicker></mat-datepicker>
|
||||||
|
<mat-error *ngIf="!hasSelectedDays(from.value, to.value)">
|
||||||
|
{{ 'SEARCH.FILTER.VALIDATION.NO-DAYS' | translate }}
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button mat-button color="primary" type="button" (click)="reset()">
|
||||||
|
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||||
|
</button>
|
||||||
|
<button mat-button color="primary" type="submit" [disabled]="!form.valid">
|
||||||
|
{{ 'SEARCH.FILTER.ACTIONS.APPLY' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,8 @@
|
|||||||
|
.adf-search-date-range > form {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.mat-button {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<mat-expansion-panel
|
<mat-expansion-panel
|
||||||
*ngFor="let field of responseFacetFields"
|
*ngFor="let field of responseFacetFields"
|
||||||
[expanded]="field.$expanded"
|
[expanded]="field.expanded"
|
||||||
(opened)="onFacetFieldExpanded(field)"
|
(opened)="onFacetFieldExpanded(field)"
|
||||||
(closed)="onFacetFieldCollapsed(field)">
|
(closed)="onFacetFieldCollapsed(field)">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
@ -43,12 +43,19 @@
|
|||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<div class="checklist">
|
<div class="checklist">
|
||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
*ngFor="let bucket of field.buckets"
|
*ngFor="let bucket of field.getVisibleBuckets()"
|
||||||
[checked]="bucket.$checked"
|
[checked]="bucket.$checked"
|
||||||
(change)="onFacetToggle($event, field, bucket)">
|
(change)="onFacetToggle($event, field, bucket)">
|
||||||
{{ bucket.display || bucket.label }} ({{ bucket.count }})
|
{{ bucket.display || bucket.label }} ({{ bucket.count }})
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button mat-button
|
||||||
|
*ngIf="field.hasMoreItems()"
|
||||||
|
(click)="field.showMoreItems()">
|
||||||
|
Show more
|
||||||
|
<mat-icon>keyboard_arrow_down</mat-icon>
|
||||||
|
</button>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
@ -64,19 +64,19 @@ describe('SearchSettingsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update facet field model on expand', () => {
|
it('should update facet field model on expand', () => {
|
||||||
const field: any = { $expanded: false };
|
const field: any = { expanded: false };
|
||||||
|
|
||||||
component.onFacetFieldExpanded(field);
|
component.onFacetFieldExpanded(field);
|
||||||
|
|
||||||
expect(field.$expanded).toBeTruthy();
|
expect(field.expanded).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update facet field model on collapse', () => {
|
it('should update facet field model on collapse', () => {
|
||||||
const field: any = { $expanded: true };
|
const field: any = { expanded: true };
|
||||||
|
|
||||||
component.onFacetFieldCollapsed(field);
|
component.onFacetFieldCollapsed(field);
|
||||||
|
|
||||||
expect(field.$expanded).toBeFalsy();
|
expect(field.expanded).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update bucket model and query builder on facet toggle', () => {
|
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', () => {
|
it('should unselect facet query and update builder', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
|
categories: [],
|
||||||
facetQueries: [
|
facetQueries: [
|
||||||
{ label: 'q1', query: 'query1' }
|
{ label: 'q1', query: 'query1' }
|
||||||
]
|
]
|
||||||
@ -238,7 +239,7 @@ describe('SearchSettingsComponent', () => {
|
|||||||
it('should fetch facet fields from response payload', () => {
|
it('should fetch facet fields from response payload', () => {
|
||||||
component.responseFacetFields = [];
|
component.responseFacetFields = [];
|
||||||
|
|
||||||
const fields = [
|
const fields: any = [
|
||||||
{ label: 'f1', buckets: [] },
|
{ label: 'f1', buckets: [] },
|
||||||
{ label: 'f2', buckets: [] }
|
{ label: 'f2', buckets: [] }
|
||||||
];
|
];
|
||||||
@ -256,9 +257,9 @@ describe('SearchSettingsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should restore expanded state for new response facet fields', () => {
|
it('should restore expanded state for new response facet fields', () => {
|
||||||
component.responseFacetFields = [
|
component.responseFacetFields = <any> [
|
||||||
{ label: 'f1', buckets: [] },
|
{ label: 'f1', buckets: [] },
|
||||||
{ label: 'f2', buckets: [], $expanded: true }
|
{ label: 'f2', buckets: [], expanded: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
@ -276,8 +277,8 @@ describe('SearchSettingsComponent', () => {
|
|||||||
component.onDataLoaded(data);
|
component.onDataLoaded(data);
|
||||||
|
|
||||||
expect(component.responseFacetFields.length).toBe(2);
|
expect(component.responseFacetFields.length).toBe(2);
|
||||||
expect(component.responseFacetFields[0].$expanded).toBeFalsy();
|
expect(component.responseFacetFields[0].expanded).toBeFalsy();
|
||||||
expect(component.responseFacetFields[1].$expanded).toBeTruthy();
|
expect(component.responseFacetFields[1].expanded).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should restore checked buckets for new response facet fields', () => {
|
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' };
|
const bucket2 = { label: 'b2', $field: 'f2', count: 1, filterQuery: 'q2' };
|
||||||
|
|
||||||
component.selectedBuckets = [ bucket2 ];
|
component.selectedBuckets = [ bucket2 ];
|
||||||
component.responseFacetFields = [
|
component.responseFacetFields = <any> [
|
||||||
{ label: 'f2', buckets: [] }
|
{ label: 'f2', buckets: [] }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -63,11 +63,11 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFacetFieldExpanded(field: ResponseFacetField) {
|
onFacetFieldExpanded(field: ResponseFacetField) {
|
||||||
field.$expanded = true;
|
field.expanded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onFacetFieldCollapsed(field: ResponseFacetField) {
|
onFacetFieldCollapsed(field: ResponseFacetField) {
|
||||||
field.$expanded = false;
|
field.expanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onFacetQueryToggle(event: MatCheckboxChange, query: ResponseFacetQuery) {
|
onFacetQueryToggle(event: MatCheckboxChange, query: ResponseFacetQuery) {
|
||||||
@ -143,11 +143,13 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
return q;
|
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(
|
this.responseFacetFields = (context.facetsFields || []).map(
|
||||||
(field: ResponseFacetField) => {
|
(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 => {
|
(field.buckets || []).forEach(bucket => {
|
||||||
bucket.$field = field.label;
|
bucket.$field = field.label;
|
||||||
@ -160,6 +162,20 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
bucket.$checked = true;
|
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;
|
return field;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -168,5 +184,4 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
this.responseFacetFields = [];
|
this.responseFacetFields = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ import { SearchTextComponent } from '../search-text/search-text.component';
|
|||||||
import { SearchRadioComponent } from '../search-radio/search-radio.component';
|
import { SearchRadioComponent } from '../search-radio/search-radio.component';
|
||||||
import { SearchSliderComponent } from '../search-slider/search-slider.component';
|
import { SearchSliderComponent } from '../search-slider/search-slider.component';
|
||||||
import { SearchNumberRangeComponent } from '../search-number-range/search-number-range.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()
|
@Injectable()
|
||||||
export class SearchFilterService {
|
export class SearchFilterService {
|
||||||
@ -31,7 +33,9 @@ export class SearchFilterService {
|
|||||||
'text': SearchTextComponent,
|
'text': SearchTextComponent,
|
||||||
'radio': SearchRadioComponent,
|
'radio': SearchRadioComponent,
|
||||||
'slider': SearchSliderComponent,
|
'slider': SearchSliderComponent,
|
||||||
'number-range': SearchNumberRangeComponent
|
'number-range': SearchNumberRangeComponent,
|
||||||
|
'check-list': SearchCheckListComponent,
|
||||||
|
'date-range': SearchDateRangeComponent
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,21 @@
|
|||||||
<input
|
<input
|
||||||
matInput [formControl]="from" [errorStateMatcher]="matcher"
|
matInput [formControl]="from" [errorStateMatcher]="matcher"
|
||||||
placeholder="{{ 'SEARCH.FILTER.RANGE.FROM' | translate }}">
|
placeholder="{{ 'SEARCH.FILTER.RANGE.FROM' | translate }}">
|
||||||
<mat-error *ngIf="from.hasError('pattern')">Invalid format</mat-error>
|
<mat-error *ngIf="from.hasError('pattern')">
|
||||||
<mat-error *ngIf="from.hasError('required')">Required value</mat-error>
|
{{ 'SEARCH.FILTER.VALIDATION.INVALID-FORMAT' | translate }}
|
||||||
|
</mat-error>
|
||||||
|
<mat-error *ngIf="from.hasError('required')">
|
||||||
|
{{ 'SEARCH.FILTER.VALIDATION.REQUIRED-VALUE' | translate }}
|
||||||
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field >
|
<mat-form-field >
|
||||||
<input
|
<input
|
||||||
matInput [formControl]="to" [errorStateMatcher]="matcher"
|
matInput [formControl]="to" [errorStateMatcher]="matcher"
|
||||||
placeholder="{{ 'SEARCH.FILTER.RANGE.TO' | translate }}">
|
placeholder="{{ 'SEARCH.FILTER.RANGE.TO' | translate }}">
|
||||||
<mat-error *ngIf="to.invalid">Invalid format</mat-error>
|
<mat-error *ngIf="to.invalid">
|
||||||
|
{{ 'SEARCH.FILTER.VALIDATION.INVALID-FORMAT' | translate }}
|
||||||
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +19,9 @@ export interface FacetField {
|
|||||||
field: string;
|
field: string;
|
||||||
label: string;
|
label: string;
|
||||||
mincount?: number;
|
mincount?: number;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
prefix?: string;
|
||||||
|
|
||||||
$checked?: boolean;
|
$checked?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,11 @@ import { FacetFieldBucket } from './facet-field-bucket.interface';
|
|||||||
export interface ResponseFacetField {
|
export interface ResponseFacetField {
|
||||||
label: string;
|
label: string;
|
||||||
buckets: Array<FacetFieldBucket>;
|
buckets: Array<FacetFieldBucket>;
|
||||||
|
pageSize?: number;
|
||||||
|
currentPageSize?: number;
|
||||||
|
expanded?: boolean;
|
||||||
|
|
||||||
$expanded?: boolean;
|
hasMoreItems(): boolean;
|
||||||
|
showMoreItems(): void;
|
||||||
|
getVisibleBuckets(): Array<FacetFieldBucket>;
|
||||||
}
|
}
|
||||||
|
@ -23,12 +23,8 @@ import { SearchCategory } from './search-category.interface';
|
|||||||
export interface SearchConfiguration {
|
export interface SearchConfiguration {
|
||||||
include?: Array<string>;
|
include?: Array<string>;
|
||||||
fields?: Array<string>;
|
fields?: Array<string>;
|
||||||
query?: {
|
categories: Array<SearchCategory>;
|
||||||
categories: Array<SearchCategory>
|
|
||||||
};
|
|
||||||
filterQueries?: Array<FilterQuery>;
|
filterQueries?: Array<FilterQuery>;
|
||||||
facetQueries?: Array<FacetQuery>;
|
facetQueries?: Array<FacetQuery>;
|
||||||
facetFields?: {
|
facetFields?: Array<FacetField>;
|
||||||
facets: Array<FacetField>
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -37,13 +37,11 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should use only enabled categories', () => {
|
it('should use only enabled categories', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true },
|
||||||
<any> { id: 'cat1', enabled: true },
|
<any> { id: 'cat2', enabled: false },
|
||||||
<any> { id: 'cat2', enabled: false },
|
<any> { id: 'cat3', enabled: true }
|
||||||
<any> { id: 'cat3', enabled: true }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
|
|
||||||
@ -54,9 +52,7 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should fetch filter queries from config', () => {
|
it('should fetch filter queries from config', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [],
|
||||||
categories: []
|
|
||||||
},
|
|
||||||
filterQueries: [
|
filterQueries: [
|
||||||
{ query: 'query1' },
|
{ query: 'query1' },
|
||||||
{ query: 'query2' }
|
{ query: 'query2' }
|
||||||
@ -123,6 +119,7 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should fetch facet query from config', () => {
|
it('should fetch facet query from config', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
|
categories: [],
|
||||||
facetQueries: [
|
facetQueries: [
|
||||||
{ query: 'q1', label: 'query1' },
|
{ query: 'q1', label: 'query1' },
|
||||||
{ query: 'q2', label: 'query2' }
|
{ query: 'q2', label: 'query2' }
|
||||||
@ -137,6 +134,7 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should not fetch empty facet query from the config', () => {
|
it('should not fetch empty facet query from the config', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
|
categories: [],
|
||||||
facetQueries: [
|
facetQueries: [
|
||||||
{ query: 'q1', label: 'query1' }
|
{ query: 'q1', label: 'query1' }
|
||||||
]
|
]
|
||||||
@ -179,11 +177,9 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should require a query fragment to build query', () => {
|
it('should require a query fragment to build query', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true }
|
||||||
<any> { id: 'cat1', enabled: true }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
builder.queryFragments['cat1'] = null;
|
builder.queryFragments['cat1'] = null;
|
||||||
@ -194,11 +190,9 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should build query with single fragment', () => {
|
it('should build query with single fragment', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true }
|
||||||
<any> { id: 'cat1', enabled: true }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
|
|
||||||
@ -210,12 +204,10 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should build query with multiple fragments', () => {
|
it('should build query with multiple fragments', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true },
|
||||||
<any> { id: 'cat1', enabled: true },
|
<any> { id: 'cat2', enabled: true }
|
||||||
<any> { id: 'cat2', enabled: true }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
|
|
||||||
@ -231,12 +223,10 @@ describe('SearchQueryBuilder', () => {
|
|||||||
it('should build query with custom fields', () => {
|
it('should build query with custom fields', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
fields: ['field1', 'field2'],
|
fields: ['field1', 'field2'],
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true },
|
||||||
<any> { id: 'cat1', enabled: true },
|
<any> { id: 'cat2', enabled: true }
|
||||||
<any> { id: 'cat2', enabled: true }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
|
|
||||||
@ -249,12 +239,10 @@ describe('SearchQueryBuilder', () => {
|
|||||||
it('should build query with empty custom fields', () => {
|
it('should build query with empty custom fields', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
fields: [],
|
fields: [],
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true },
|
||||||
<any> { id: 'cat1', enabled: true },
|
<any> { id: 'cat2', enabled: true }
|
||||||
<any> { id: 'cat2', enabled: true }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
|
|
||||||
@ -266,11 +254,9 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should build query with custom filter queries', () => {
|
it('should build query with custom filter queries', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true }
|
||||||
<any> { id: 'cat1', enabled: true }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
builder.queryFragments['cat1'] = 'cm:name:test';
|
builder.queryFragments['cat1'] = 'cm:name:test';
|
||||||
@ -284,11 +270,9 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should build query with custom facet queries', () => {
|
it('should build query with custom facet queries', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true }
|
||||||
<any> { id: 'cat1', enabled: true }
|
],
|
||||||
]
|
|
||||||
},
|
|
||||||
facetQueries: [
|
facetQueries: [
|
||||||
{ query: 'q1', label: 'q2' }
|
{ query: 'q1', label: 'q2' }
|
||||||
]
|
]
|
||||||
@ -302,32 +286,26 @@ describe('SearchQueryBuilder', () => {
|
|||||||
|
|
||||||
it('should build query with custom facet fields', () => {
|
it('should build query with custom facet fields', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true }
|
||||||
<any> { id: 'cat1', enabled: true }
|
],
|
||||||
]
|
facetFields: [
|
||||||
},
|
{ field: 'field1', label: 'field1', mincount: 1, limit: null, offset: 0, prefix: null },
|
||||||
facetFields: {
|
{ field: 'field2', label: 'field2', mincount: 1, limit: null, offset: 0, prefix: null }
|
||||||
facets: [
|
]
|
||||||
{ field: 'field1', label: 'field1' },
|
|
||||||
{ field: 'field2', label: 'field2' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
builder.queryFragments['cat1'] = 'cm:name:test';
|
builder.queryFragments['cat1'] = 'cm:name:test';
|
||||||
|
|
||||||
const compiled = builder.buildQuery();
|
const compiled = builder.buildQuery();
|
||||||
expect(compiled.facetFields).toEqual(config.facetFields);
|
expect(compiled.facetFields.facets).toEqual(jasmine.objectContaining(config.facetFields));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use pagination settings', () => {
|
it('should use pagination settings', () => {
|
||||||
const config: SearchConfiguration = {
|
const config: SearchConfiguration = {
|
||||||
query: {
|
categories: [
|
||||||
categories: [
|
<any> { id: 'cat1', enabled: true }
|
||||||
<any> { id: 'cat1', enabled: true }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
const builder = new SearchQueryBuilderService(buildConfig(config), null);
|
||||||
builder.queryFragments['cat1'] = 'cm:name:test';
|
builder.queryFragments['cat1'] = 'cm:name:test';
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Subject } from 'rxjs/Subject';
|
import { Subject } from 'rxjs/Subject';
|
||||||
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
|
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 { SearchCategory } from './search-category.interface';
|
||||||
import { FilterQuery } from './filter-query.interface';
|
import { FilterQuery } from './filter-query.interface';
|
||||||
import { SearchRange } from './search-range.interface';
|
import { SearchRange } from './search-range.interface';
|
||||||
@ -45,8 +45,8 @@ export class SearchQueryBuilderService {
|
|||||||
throw new Error('Search configuration not found.');
|
throw new Error('Search configuration not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.query && this.config.query.categories) {
|
if (this.config.categories) {
|
||||||
this.categories = this.config.query.categories.filter(f => f.enabled);
|
this.categories = this.config.categories.filter(f => f.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.filterQueries = this.config.filterQueries || [];
|
this.filterQueries = this.config.filterQueries || [];
|
||||||
@ -116,7 +116,7 @@ export class SearchQueryBuilderService {
|
|||||||
fields: this.config.fields,
|
fields: this.config.fields,
|
||||||
filterQueries: this.filterQueries,
|
filterQueries: this.filterQueries,
|
||||||
facetQueries: this.config.facetQueries,
|
facetQueries: this.config.facetQueries,
|
||||||
facetFields: this.config.facetFields
|
facetFields: this.facetFields
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -124,4 +124,23 @@ export class SearchQueryBuilderService {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get facetFields(): RequestFacetFields {
|
||||||
|
const facetFields = this.config.facetFields;
|
||||||
|
|
||||||
|
if (facetFields && facetFields.length > 0) {
|
||||||
|
return {
|
||||||
|
facets: facetFields.map(facet => <RequestFacetField> {
|
||||||
|
field: facet.field,
|
||||||
|
mincount: facet.mincount,
|
||||||
|
label: facet.label,
|
||||||
|
limit: facet.limit,
|
||||||
|
offset: facet.offset,
|
||||||
|
prefix: facet.prefix
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,8 @@ import { SearchTextComponent } from './components/search-text/search-text.compon
|
|||||||
import { SearchRadioComponent } from './components/search-radio/search-radio.component';
|
import { SearchRadioComponent } from './components/search-radio/search-radio.component';
|
||||||
import { SearchSliderComponent } from './components/search-slider/search-slider.component';
|
import { SearchSliderComponent } from './components/search-slider/search-slider.component';
|
||||||
import { SearchNumberRangeComponent } from './components/search-number-range/search-number-range.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[] = [
|
export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
|
||||||
SearchComponent,
|
SearchComponent,
|
||||||
@ -60,7 +62,9 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
|
|||||||
SearchTextComponent,
|
SearchTextComponent,
|
||||||
SearchRadioComponent,
|
SearchRadioComponent,
|
||||||
SearchSliderComponent,
|
SearchSliderComponent,
|
||||||
SearchNumberRangeComponent
|
SearchNumberRangeComponent,
|
||||||
|
SearchCheckListComponent,
|
||||||
|
SearchDateRangeComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...ALFRESCO_SEARCH_DIRECTIVES,
|
...ALFRESCO_SEARCH_DIRECTIVES,
|
||||||
@ -68,14 +72,18 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
|
|||||||
SearchTextComponent,
|
SearchTextComponent,
|
||||||
SearchRadioComponent,
|
SearchRadioComponent,
|
||||||
SearchSliderComponent,
|
SearchSliderComponent,
|
||||||
SearchNumberRangeComponent
|
SearchNumberRangeComponent,
|
||||||
|
SearchCheckListComponent,
|
||||||
|
SearchDateRangeComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
SearchWidgetContainerComponent,
|
SearchWidgetContainerComponent,
|
||||||
SearchTextComponent,
|
SearchTextComponent,
|
||||||
SearchRadioComponent,
|
SearchRadioComponent,
|
||||||
SearchSliderComponent,
|
SearchSliderComponent,
|
||||||
SearchNumberRangeComponent
|
SearchNumberRangeComponent,
|
||||||
|
SearchCheckListComponent,
|
||||||
|
SearchDateRangeComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SearchModule {}
|
export class SearchModule {}
|
||||||
|
@ -480,21 +480,25 @@
|
|||||||
"search": {
|
"search": {
|
||||||
"description": "Search configuration parameters",
|
"description": "Search configuration parameters",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [ "categories" ],
|
||||||
"properties": {
|
"properties": {
|
||||||
"include": {
|
"include": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filterQueries": {
|
"filterQueries": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [ "query" ],
|
"required": [ "query" ],
|
||||||
@ -504,20 +508,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"facetFields": {
|
"facetFields": {
|
||||||
"type": "object",
|
"type": "array",
|
||||||
"required": [ "facets" ],
|
"minItems": 1,
|
||||||
"properties": {
|
"items": {
|
||||||
"facets": {
|
"type": "object",
|
||||||
"type": "array",
|
"required": [ "field", "mincount", "label" ],
|
||||||
"items": {
|
"properties": {
|
||||||
"type": "object",
|
"field": {
|
||||||
"required": [ "field", "mincount", "label" ],
|
"type": "string",
|
||||||
"properties": {
|
"description": "This specifies the facet field."
|
||||||
"field": { "type": "string" },
|
},
|
||||||
"mincount": { "type": "integer" },
|
"mincount": {
|
||||||
"label": { "type": "string" }
|
"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": {
|
"categories": {
|
||||||
"type": "object",
|
"type": "array",
|
||||||
"required": [ "categories" ],
|
"minItems": 1,
|
||||||
"properties": {
|
"items": {
|
||||||
"categories": {
|
"type": "object",
|
||||||
"type": "array",
|
"required": [ "id", "name" ],
|
||||||
"minItems": 1,
|
"properties": {
|
||||||
"items": {
|
"id": { "type": "string" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"expanded": { "type": "boolean" },
|
||||||
|
"component": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [ "id", "name" ],
|
"required": [ "selector", "settings" ],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": { "type": "string" },
|
"selector": { "type": "string" },
|
||||||
"name": { "type": "string" },
|
"settings": { "type": "object" }
|
||||||
"enabled": { "type": "boolean" },
|
|
||||||
"expanded": { "type": "boolean" },
|
|
||||||
"component": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [ "selector", "settings" ],
|
|
||||||
"properties": {
|
|
||||||
"selector": { "type": "string" },
|
|
||||||
"settings": { "type": "object" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|