diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json
index e2d2501839..99d0a1e7bd 100644
--- a/demo-shell/src/app.config.json
+++ b/demo-shell/src/app.config.json
@@ -102,6 +102,7 @@
}
]
},
+ "resetButton": true,
"filterQueries": [
{ "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
{ "query": "NOT cm:creator:System" }
diff --git a/demo-shell/src/app/components/search/search-result.component.html b/demo-shell/src/app/components/search/search-result.component.html
index 4a97f107cd..9f05f5954a 100644
--- a/demo-shell/src/app/components/search/search-result.component.html
+++ b/demo-shell/src/app/components/search/search-result.component.html
@@ -1,5 +1,5 @@
diff --git a/docs/content-services/search-chip-list.component.md b/docs/content-services/search-chip-list.component.md
index 170c29c523..05823061be 100644
--- a/docs/content-services/search-chip-list.component.md
+++ b/docs/content-services/search-chip-list.component.md
@@ -25,3 +25,4 @@ Displays search criteria as a set of "chips".
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| searchFilter | [`SearchFilterComponent`](../content-services/search-filter.component.md) | | Search filter to supply the data for the chips. |
+| clearAll | boolean | false | Enables or disables the display of a clear-all-filters button. |
diff --git a/docs/content-services/search-filter.component.md b/docs/content-services/search-filter.component.md
index 9666a0f693..59394b2f74 100644
--- a/docs/content-services/search-filter.component.md
+++ b/docs/content-services/search-filter.component.md
@@ -133,6 +133,17 @@ You can choose to filter facet field results using 'contains' instead of 'starts
}
```
+You can choose to display a reset button by setting the 'resetButton' value to true.
+This 'clean up' button would make it easier for the final user to remove all bucket selections and all search filtering.
+
+```json
+{
+ "search": {
+ "resetButton": true
+ }
+}
+```
+
You can also provide a set of queries that are always executed alongside the user-defined
settings:
diff --git a/lib/content-services/i18n/en.json b/lib/content-services/i18n/en.json
index 25624894c6..17022cb032 100644
--- a/lib/content-services/i18n/en.json
+++ b/lib/content-services/i18n/en.json
@@ -198,6 +198,16 @@
"SHOW-LESS": "Show less",
"FILTER-CATEGORY": "Filter category"
},
+ "BUTTONS": {
+ "CLEAR-ALL": {
+ "LABEL": "Clear all",
+ "TOOLTIP": "This will remove all selections"
+ },
+ "RESET-ALL": {
+ "LABEL": "Reset all",
+ "TOOLTIP": "This will reset all selections and all filters"
+ }
+ },
"RANGE": {
"FROM": "From",
"TO": "To",
diff --git a/lib/content-services/search/components/search-chip-list/search-chip-list.component.html b/lib/content-services/search/components/search-chip-list/search-chip-list.component.html
index dd5fa52184..88c98c1013 100644
--- a/lib/content-services/search/components/search-chip-list/search-chip-list.component.html
+++ b/lib/content-services/search/components/search-chip-list/search-chip-list.component.html
@@ -1,5 +1,14 @@
+ 1"
+ color="primary"
+ selected
+ matTooltip="{{ 'SEARCH.FILTER.BUTTONS.CLEAR-ALL.TOOLTIP' | translate }}"
+ matTooltipPosition="right"
+ (click)="searchFilter.resetAllSelectedBuckets()">
+ {{ 'SEARCH.FILTER.BUTTONS.CLEAR-ALL.LABEL' | translate }}
+
+
implements Iterable {
this.filterText = '';
}
+ addItem(item: T) {
+ if (!item) {
+ return;
+ }
+ this.items.push(item);
+ this.applyFilter();
+ }
+
+ deleteItem(item: T) {
+ const removeIndex = this.items.indexOf(item);
+ if (removeIndex > -1) {
+ this.items.splice(removeIndex, 1);
+ this.filteredItems.splice(removeIndex, 1);
+ }
+ }
+
[Symbol.iterator](): Iterator {
let pointer = 0;
let items = this.visibleItems;
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 cb20027fa0..591abd8bfb 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
@@ -1,5 +1,13 @@
+
- {{ bucket.display || bucket.label | translate }}
- (
- {{ bucket.count }}
- )
+ {{ bucket.display || bucket.label | translate }} {{ getBucketCountDisplay(bucket) }}
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 e33fc432df..265b7adf3c 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
@@ -193,7 +193,7 @@ describe('SearchFilterComponent', () => {
component.onDataLoaded(data);
- expect(component.responseFacets.length).toBe(0);
+ expect(component.responseFacets).toBeNull();
});
it('should fetch facet fields from response payload', () => {
@@ -397,10 +397,10 @@ describe('SearchFilterComponent', () => {
};
component.responseFacets = [
- { label: 'f1', field: 'f1', buckets: {items: [
+ { type: 'field', label: 'f1', field: 'f1', buckets: {items: [
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
- { label: 'f2', field: 'f2', buckets: {items: [] }}
+ { type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
];
component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]);
@@ -437,10 +437,10 @@ describe('SearchFilterComponent', () => {
};
component.responseFacets = [
- { label: 'f1', field: 'f1', buckets: {items: [
+ { type: 'field', label: 'f1', field: 'f1', buckets: {items: [
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
- { label: 'f2', field: 'f2', buckets: {items: [] }}
+ { type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
];
component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]);
@@ -477,10 +477,10 @@ describe('SearchFilterComponent', () => {
};
component.responseFacets = [
- { label: 'f1', field: 'f1', buckets: {items: [
+ { type: 'field', label: 'f1', field: 'f1', buckets: {items: [
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
- { label: 'f2', field: 'f2', buckets: {items: [] }}
+ { type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
];
component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]);
const data = {
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 58ea6042db..178323e9aa 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
@@ -37,6 +37,11 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
private DEFAULT_PAGE_SIZE = 5;
isAlive = true;
+
+ /** All facet field items to be displayed in the component. These are updated according to the response.
+ * When a new search is performed, the already existing items are updated with the new bucket count values and
+ * the newly received items are added to the responseFacets.
+ */
responseFacets: FacetField[] = null;
private facetQueriesPageSize = this.DEFAULT_PAGE_SIZE;
@@ -44,7 +49,7 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
facetExpanded = {
'default': false
};
-
+ displayResetButton: boolean;
selectedBuckets: Array<{ field: FacetField, bucket: FacetFieldBucket }> = [];
constructor(public queryBuilder: SearchQueryBuilderService,
@@ -61,6 +66,7 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
if (queryBuilder.config && queryBuilder.config.facetIntervals) {
this.facetExpanded['interval'] = queryBuilder.config.facetIntervals.expanded;
}
+ this.displayResetButton = this.queryBuilder.config && !!this.queryBuilder.config.resetButton;
this.queryBuilder.updated.pipe(
takeWhile(() => this.isAlive)
@@ -149,6 +155,24 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
}
}
+ resetAllSelectedBuckets() {
+ this.responseFacets.forEach((field) => {
+ if (field && field.buckets) {
+ for (let bucket of field.buckets.items) {
+ bucket.checked = false;
+ this.queryBuilder.removeUserFacetBucket(field, bucket);
+ }
+ this.updateSelectedBuckets();
+ }
+ });
+ this.queryBuilder.update();
+ }
+
+ resetAll() {
+ this.resetAllSelectedBuckets();
+ this.responseFacets = null;
+ }
+
shouldExpand(field: FacetField): boolean {
return this.facetExpanded[field.type] || this.facetExpanded['default'];
}
@@ -164,69 +188,53 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
}
private parseFacets(context: ResultSetContext) {
- if (!this.responseFacets) {
- const responseFacetFields = this.parseFacetFields(context);
- const responseFacetIntervals = this.parseFacetIntervals(context);
- const responseGroupedFacetQueries = this.parseFacetQueries(context);
- this.responseFacets = responseFacetFields.concat(...responseGroupedFacetQueries, ...responseFacetIntervals);
-
- } else {
- this.responseFacets = this.responseFacets
- .map((field) => {
-
- let responseField = (context.facets || []).find((response) => response.label === field.label && response.type === field.type);
-
- (field && field.buckets && field.buckets.items || [])
- .map((bucket) => {
- const responseBucket = ((responseField && responseField.buckets) || []).find((respBucket) => respBucket.label === bucket.label);
-
- bucket.count = responseBucket ? this.getCountValue(responseBucket) : 0;
- return bucket;
- });
-
- return field;
- });
- }
+ this.parseFacetFields(context);
+ this.parseFacetIntervals(context);
+ this.parseFacetQueries(context);
}
- private parseFacetItems(context: ResultSetContext, configFacetFields: FacetField[], itemType: string): FacetField[] {
- return configFacetFields.map((field) => {
- const responseField = (context.facets || []).find((response) => response.type === itemType && response.label === field.label) || {};
+ private parseFacetItems(context: ResultSetContext, configFacetFields: FacetField[], itemType: string) {
+ configFacetFields.forEach((field) => {
+ const responseField = this.findFacet(context, itemType, field.label);
const responseBuckets = this.getResponseBuckets(responseField, field)
.filter(this.getFilterByMinCount(field.mincount));
+ const alreadyExistingField = this.findResponseFacet(itemType, field.label);
- const bucketList = new SearchFilterList(responseBuckets, field.pageSize);
- bucketList.filter = (bucket: FacetFieldBucket): boolean => {
- if (bucket && bucketList.filterText) {
- const pattern = (bucketList.filterText || '').toLowerCase();
- const label = (this.translationService.instant(bucket.display) || this.translationService.instant(bucket.label)).toLowerCase();
- return this.queryBuilder.config.filterWithContains ? label.indexOf(pattern) !== -1 : label.startsWith(pattern);
+ if (alreadyExistingField) {
+ const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || [];
+
+ this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets);
+ } else if (responseField) {
+
+ const bucketList = new SearchFilterList(responseBuckets, field.pageSize);
+ bucketList.filter = this.getBucketFilterFunction(bucketList);
+
+ if (!this.responseFacets) {
+ this.responseFacets = [];
}
- return true;
- };
-
- return {
- ...field,
- type: responseField.type,
- label: field.label,
- pageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
- currentPageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
- buckets: bucketList
- };
+ this.responseFacets.push( {
+ ...field,
+ type: responseField.type || itemType,
+ label: field.label,
+ pageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
+ currentPageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
+ buckets: bucketList
+ });
+ }
});
}
- private parseFacetFields(context: ResultSetContext): FacetField[] {
+ private parseFacetFields(context: ResultSetContext) {
const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || [];
- return this.parseFacetItems(context, configFacetFields, 'field');
+ this.parseFacetItems(context, configFacetFields, 'field');
}
- private parseFacetIntervals(context: ResultSetContext): FacetField[] {
+ private parseFacetIntervals(context: ResultSetContext) {
const configFacetIntervals = this.queryBuilder.config.facetIntervals && this.queryBuilder.config.facetIntervals.intervals || [];
- return this.parseFacetItems(context, configFacetIntervals, 'interval');
+ this.parseFacetItems(context, configFacetIntervals, 'interval');
}
- private parseFacetQueries(context: ResultSetContext): FacetField[] {
+ private parseFacetQueries(context: ResultSetContext) {
const configFacetQueries = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.queries || [];
const configGroups = configFacetQueries.reduce((acc, query) => {
const group = this.queryBuilder.getQueryGroup(query);
@@ -238,36 +246,38 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
return acc;
}, []);
- const result = [];
const mincount = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.mincount;
const mincountFilter = this.getFilterByMinCount(mincount);
Object.keys(configGroups).forEach((group) => {
- const responseField = (context.facets || []).find((response) => response.type === 'query' && response.label === group) || {};
+ const responseField = this.findFacet(context, 'query', group);
const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group])
.filter(mincountFilter);
+ const alreadyExistingField = this.findResponseFacet('query', group);
- const bucketList = new SearchFilterList(responseBuckets, this.facetQueriesPageSize);
- bucketList.filter = (bucket: FacetFieldBucket): boolean => {
- if (bucket && bucketList.filterText) {
- const pattern = (bucketList.filterText || '').toLowerCase();
- const label = (this.translationService.instant(bucket.display) || this.translationService.instant(bucket.label)).toLowerCase();
- return this.queryBuilder.config.filterWithContains ? label.indexOf(pattern) !== -1 : label.startsWith(pattern);
+ if (alreadyExistingField) {
+ const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || [];
+
+ this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets);
+ } else if (responseField) {
+
+ const bucketList = new SearchFilterList(responseBuckets, this.facetQueriesPageSize);
+ bucketList.filter = this.getBucketFilterFunction(bucketList);
+
+ if (!this.responseFacets) {
+ this.responseFacets = [];
}
- return true;
- };
-
- result.push( {
- field: group,
- type: responseField.type,
- label: group,
- pageSize: this.DEFAULT_PAGE_SIZE,
- currentPageSize: this.DEFAULT_PAGE_SIZE,
- buckets: bucketList
- });
+ this.responseFacets.push( {
+ field: group,
+ type: responseField.type || 'query',
+ label: group,
+ pageSize: this.DEFAULT_PAGE_SIZE,
+ currentPageSize: this.DEFAULT_PAGE_SIZE,
+ buckets: bucketList
+ });
+ }
});
- return result;
}
private getResponseBuckets(responseField: GenericFacetResponse, configField: FacetField): FacetFieldBucket[] {
@@ -304,6 +314,10 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
|| 0;
}
+ getBucketCountDisplay(bucket: FacetFieldBucket): string {
+ return bucket.count === null ? '' : `(${bucket.count})`;
+ }
+
private getFilterByMinCount(mincountInput: number) {
return (bucket) => {
let mincount = mincountInput;
@@ -342,4 +356,55 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
return `${fieldName}:${startLimit}"${start}" TO "${end}"${endLimit}`;
}
+
+ private findFacet(context: ResultSetContext, itemType: string, fieldLabel: string): GenericFacetResponse {
+ return (context.facets || []).find((response) => response.type === itemType && response.label === fieldLabel) || {};
+ }
+
+ private findResponseFacet(itemType: string, fieldLabel: string): FacetField {
+ return (this.responseFacets || []).find((response) => response.type === itemType && response.label === fieldLabel);
+ }
+
+ private updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets) {
+ const bucketsToDelete = [];
+
+ alreadyExistingBuckets
+ .map((bucket) => {
+ const responseBucket = ((responseField && responseField.buckets) || []).find((respBucket) => respBucket.label === bucket.label);
+
+ if (!responseBucket) {
+ bucketsToDelete.push(bucket);
+ }
+ bucket.count = this.getCountValue(responseBucket);
+ return bucket;
+ });
+
+ const hasSelection = this.selectedBuckets
+ .find((selBuckets) => alreadyExistingField.label === selBuckets.field.label && alreadyExistingField.type === selBuckets.field.type);
+
+ if (!hasSelection && bucketsToDelete.length) {
+ bucketsToDelete.forEach((bucket) => {
+ alreadyExistingField.buckets.deleteItem(bucket);
+ });
+ }
+
+ responseBuckets.forEach((respBucket) => {
+ const existingBucket = alreadyExistingBuckets.find((oldBucket) => oldBucket.label === respBucket.label);
+
+ if (!existingBucket) {
+ alreadyExistingField.buckets.addItem(respBucket);
+ }
+ });
+ }
+
+ private getBucketFilterFunction (bucketList) {
+ return (bucket: FacetFieldBucket): boolean => {
+ if (bucket && bucketList.filterText) {
+ const pattern = (bucketList.filterText || '').toLowerCase();
+ const label = (this.translationService.instant(bucket.display) || this.translationService.instant(bucket.label)).toLowerCase();
+ return this.queryBuilder.config.filterWithContains ? label.indexOf(pattern) !== -1 : label.startsWith(pattern);
+ }
+ return true;
+ };
+ }
}
diff --git a/lib/content-services/search/components/search-radio/search-radio.component.ts b/lib/content-services/search/components/search-radio/search-radio.component.ts
index 2842e8d88e..18146ae9fb 100644
--- a/lib/content-services/search/components/search-radio/search-radio.component.ts
+++ b/lib/content-services/search/components/search-radio/search-radio.component.ts
@@ -62,9 +62,10 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
}
}
- this.setValue(
- this.getSelectedValue()
- );
+ const initialValue = this.getSelectedValue();
+ if (initialValue !== null) {
+ this.setValue(initialValue);
+ }
}
private getSelectedValue(): string {
diff --git a/lib/content-services/search/search-configuration.interface.ts b/lib/content-services/search/search-configuration.interface.ts
index bd2e249276..7dff04b770 100644
--- a/lib/content-services/search/search-configuration.interface.ts
+++ b/lib/content-services/search/search-configuration.interface.ts
@@ -27,6 +27,7 @@ export interface SearchConfiguration {
categories: SearchCategory[];
filterQueries?: FilterQuery[];
filterWithContains?: boolean;
+ resetButton?: boolean;
facetQueries?: {
label?: string;
pageSize?: number;
diff --git a/lib/core/app-config/schema.json b/lib/core/app-config/schema.json
index c29f531bb1..33eddb0584 100644
--- a/lib/core/app-config/schema.json
+++ b/lib/core/app-config/schema.json
@@ -926,6 +926,9 @@
"filterWithContains": {
"type": "boolean"
},
+ "resetButton": {
+ "type": "boolean"
+ },
"facetFields": {
"type": "object",
"required": [