diff --git a/demo-shell/resources/i18n/en.json b/demo-shell/resources/i18n/en.json index a9b1c809e2b..d0f5dc74c11 100644 --- a/demo-shell/resources/i18n/en.json +++ b/demo-shell/resources/i18n/en.json @@ -350,7 +350,5 @@ "ACTION_TYPE": "Action Type" } }, - "SEARCH_FORMS": { - "ALL": "All" - } + "DEFAULT_SEARCH": "Default" } diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index b5095858655..c5b322fcaa4 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -366,8 +366,258 @@ } ] }, - "name": "SEARCH_FORMS.ALL", + "name": "DEFAULT_SEARCH", "default": true + }, + { + "filterWithContains": true, + "app:fields": [ + "cm:name", + "cm:title", + "cm:description", + "ia:whatEvent", + "ia:descriptionEvent", + "lnk:title", + "lnk:description", + "TEXT", + "TAG" + ], + "include": [ + "path", + "allowableOperations", + "properties" + ], + "sorting": { + "options": [ + { + "key": "name", + "label": "Name", + "type": "FIELD", + "field": "cm:name", + "ascending": true + }, + { + "key": "content.sizeInBytes", + "label": "Size", + "type": "FIELD", + "field": "content.size", + "ascending": true + }, + { + "key": "createdByUser", + "label": "Author", + "type": "FIELD", + "field": "cm:creator", + "ascending": true + }, + { + "key": "createdAt", + "label": "Created", + "type": "FIELD", + "field": "cm:created", + "ascending": true + }, + { + "key": "score", + "label": "Relevance", + "type": "SCORE", + "field": "score", + "ascending": false + } + ], + "defaults": [ + { + "key": "score", + "type": "FIELD", + "field": "score", + "ascending": false + } + ] + }, + "resetButton": true, + "filterQueries": [ + { + "query": "TYPE:'cm:folder'" + }, + { + "query": "NOT cm:creator:System" + } + ], + "facetFields": { + "expanded": true, + "fields": [ + { + "field": "content.size", + "mincount": 1, + "label": "Folder Size", + "settings": { + "allowUpdateOnChange": false, + "hideDefaultAction": true, + "unit": "Bytes" + } + }, + { + "field": "creator", + "mincount": 1, + "label": "Field created", + "settings": { + "allowUpdateOnChange": false, + "hideDefaultAction": true + } + }, + { + "field": "modifier", + "mincount": 1, + "label": "Folder Modifier", + "settings": { + "allowUpdateOnChange": false, + "hideDefaultAction": true + } + }, + { + "field": "created", + "mincount": 1, + "label": "Folder Created", + "settings": { + "allowUpdateOnChange": false, + "hideDefaultAction": true + } + } + ] + }, + "facetQueries": { + "label": "SEARCH.FACET_QUERIES.MY_FACET_QUERIES", + "pageSize": 5, + "expanded": true, + "mincount": 1, + "queries": [ + { + "query": "created:2019", + "label": "SEARCH.FACET_QUERIES.CREATED_THIS_YEAR" + }, + { + "query": "content.mimetype:text/html", + "label": "SEARCH.FACET_QUERIES.MIMETYPE", + "group": "Type facet queries" + }, + { + "query": "content.size:[0 TO 10240]", + "label": "SEARCH.FACET_QUERIES.XTRASMALL", + "group": "Size facet queries" + }, + { + "query": "content.size:[10240 TO 102400]", + "label": "SEARCH.FACET_QUERIES.SMALL", + "group": "Size facet queries" + }, + { + "query": "content.size:[102400 TO 1048576]", + "label": "SEARCH.FACET_QUERIES.MEDIUM", + "group": "Size facet queries" + }, + { + "query": "content.size:[1048576 TO 16777216]", + "label": "SEARCH.FACET_QUERIES.LARGE", + "group": "Size facet queries" + }, + { + "query": "content.size:[16777216 TO 134217728]", + "label": "SEARCH.FACET_QUERIES.XTRALARGE", + "group": "Size facet queries" + }, + { + "query": "content.size:[134217728 TO MAX]", + "label": "SEARCH.FACET_QUERIES.XXTRALARGE", + "group": "Size facet queries" + } + ], + "settings": { + "allowUpdateOnChange": false, + "hideDefaultAction": true + } + }, + "facetIntervals": { + "expanded": true, + "intervals": [ + { + "label": "The Created", + "field": "cm:created", + "sets": [ + { + "label": "lastYear", + "start": "2018", + "end": "2019", + "endInclusive": false + }, + { + "label": "currentYear", + "start": "NOW/YEAR", + "end": "NOW/YEAR+1YEAR" + }, + { + "label": "earlier", + "start": "*", + "end": "2018", + "endInclusive": false + } + ], + "settings": { + "allowUpdateOnChange": false, + "hideDefaultAction": true + } + }, + { + "label": "The Modified", + "field": "cm:modified", + "sets": [ + { + "label": "2017", + "start": "2017", + "end": "2018", + "endInclusive": false + }, + { + "label": "2017-2018", + "start": "2017", + "end": "2018", + "endInclusive": true + }, + { + "label": "currentYear", + "start": "NOW/YEAR", + "end": "NOW/YEAR+1YEAR" + }, + { + "label": "earlierThan2017", + "start": "*", + "end": "2017", + "endInclusive": false + } + ], + "settings": { + "allowUpdateOnChange": false, + "hideDefaultAction": true + } + } + ] + }, + "categories": [], + "highlight": { + "prefix": " ", + "postfix": " ", + "mergeContiguous": true, + "fields": [ + { + "field": "cm:title" + }, + { + "field": "description", + "prefix": "(", + "postfix": ")" + } + ] + }, + "name": "Folder" }], "search-headers": { "filterWithContains": true, diff --git a/demo-shell/src/app/app.module.ts b/demo-shell/src/app/app.module.ts index feb9cfe531e..713e2d8a0ea 100644 --- a/demo-shell/src/app/app.module.ts +++ b/demo-shell/src/app/app.module.ts @@ -114,6 +114,7 @@ import localeDa from '@angular/common/locales/da'; import localeSv from '@angular/common/locales/sv'; import { setupAppNotifications } from './services/app-notifications-factory'; import { AppNotificationsService } from './services/app-notifications.service'; +import { SearchFilterChipsComponent } from './components/search/search-filter-chips.component'; registerLocaleData(localeFr); registerLocaleData(localeDe); @@ -204,7 +205,8 @@ registerLocaleData(localeSv); CustomEditorComponent, CustomWidgetComponent, ProcessCloudLayoutComponent, - ServiceTaskListCloudDemoComponent + ServiceTaskListCloudDemoComponent, + SearchFilterChipsComponent ], providers: [ { diff --git a/demo-shell/src/app/app.routes.ts b/demo-shell/src/app/app.routes.ts index c7e5caff1e8..21456908946 100644 --- a/demo-shell/src/app/app.routes.ts +++ b/demo-shell/src/app/app.routes.ts @@ -55,6 +55,7 @@ import { FilteredSearchComponent } from './components/files/filtered-search.comp import { ProcessCloudLayoutComponent } from './components/cloud/process-cloud-layout.component'; import { ServiceTaskListCloudDemoComponent } from './components/cloud/service-task-list-cloud-demo.component'; import { AspectListSampleComponent } from './components/aspect-list-sample/aspect-list-sample.component'; +import { SearchFilterChipsComponent } from './components/search/search-filter-chips.component'; export const appRoutes: Routes = [ { path: 'login', loadChildren: () => import('./components/login/login.module').then(m => m.AppLoginModule) }, @@ -325,6 +326,11 @@ export const appRoutes: Routes = [ component: SearchResultComponent, canActivate: [AuthGuardEcm] }, + { + path: 'search-filter-chips', + component: SearchFilterChipsComponent, + canActivate: [AuthGuardEcm] + }, { path: 'extendedSearch', component: SearchExtendedComponent, diff --git a/demo-shell/src/app/components/search/search-filter-chips.component.html b/demo-shell/src/app/components/search/search-filter-chips.component.html new file mode 100644 index 00000000000..2bb8461a9f3 --- /dev/null +++ b/demo-shell/src/app/components/search/search-filter-chips.component.html @@ -0,0 +1,39 @@ +
+
+ +
+
+ +
+ + + +
+ +
+ + refresh + + +
+ + +
+
diff --git a/demo-shell/src/app/components/search/search-filter-chips.component.scss b/demo-shell/src/app/components/search/search-filter-chips.component.scss new file mode 100644 index 00000000000..cf0453de25f --- /dev/null +++ b/demo-shell/src/app/components/search/search-filter-chips.component.scss @@ -0,0 +1,44 @@ +.app-search-results { + display: flex; + margin-left: 5px; + + .app-search-settings { + width: 260px; + border: 1px solid #eee; + } + + &__facets { + margin: 5px; + } + + &__content { + flex: 1; + } + + &__sorting { + padding-top: 16px; + padding-bottom: 16px; + display: flex; + } +} + +div.app-search-results-container { + padding: 0 20px 20px; +} + +.app-search-title { + font-size: 22px; + padding: 15px 0; +} + +@media screen and (max-width: 600px) { + :host .app-col-display-name { + min-width: 100px; + } + :host .app-col-modified-at, :host .app-col-modified-by { + display: none; + } + :host div.app-search-results-container table { + width: 100%; + } +} diff --git a/demo-shell/src/app/components/search/search-filter-chips.component.ts b/demo-shell/src/app/components/search/search-filter-chips.component.ts new file mode 100644 index 00000000000..2cb883fb846 --- /dev/null +++ b/demo-shell/src/app/components/search/search-filter-chips.component.ts @@ -0,0 +1,143 @@ +/*! + * @license + * Copyright 2019 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, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { Pagination, ResultSetPaging } from '@alfresco/js-api'; +import { SearchForm, SearchQueryBuilderService } from '@alfresco/adf-content-services'; +import { SearchService, ShowHeaderMode, UserPreferencesService } from '@alfresco/adf-core'; +import { combineLatest, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'app-search-filter-chips', + templateUrl: './search-filter-chips.component.html', + styleUrls: [ './search-filter-chips.component.scss' ], + providers: [SearchService] +}) +export class SearchFilterChipsComponent implements OnInit, OnDestroy { + + queryParamName = 'q'; + searchedWord = ''; + data: ResultSetPaging; + pagination: Pagination; + isLoading = true; + + sorting = ['name', 'asc']; + searchForms: SearchForm[]; + showHeader = ShowHeaderMode.Always; + + private onDestroy$ = new Subject(); + + constructor(public router: Router, + private preferences: UserPreferencesService, + private queryBuilder: SearchQueryBuilderService, + private route: ActivatedRoute) { + combineLatest([this.route.params, this.queryBuilder.configUpdated]) + .pipe(takeUntil(this.onDestroy$)) + .subscribe(([params, searchConfig]) => { + this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null; + const query = this.formatSearchQuery(this.searchedWord, searchConfig['app:fields']); + if (query) { + this.queryBuilder.userQuery = query; + } + }); + + queryBuilder.paging = { + maxItems: this.preferences.paginationSize, + skipCount: 0 + }; + } + + ngOnInit() { + this.queryBuilder.resetToDefaults(); + + this.sorting = this.getSorting(); + + this.queryBuilder.updated + .pipe(takeUntil(this.onDestroy$)) + .subscribe(() => { + this.sorting = this.getSorting(); + this.isLoading = true; + }); + + this.queryBuilder.executed + .pipe(takeUntil(this.onDestroy$)) + .subscribe((resultSetPaging: ResultSetPaging) => { + this.queryBuilder.paging.skipCount = 0; + + this.onSearchResultLoaded(resultSetPaging); + this.isLoading = false; + }); + + if (this.route) { + this.route.params.forEach((params: Params) => { + this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null; + if (this.searchedWord) { + this.queryBuilder.update(); + } else { + this.queryBuilder.userQuery = null; + this.queryBuilder.executed.next(new ResultSetPaging({ + list: { + pagination: { totalItems: 0 }, + entries: [] + } + })); + } + }); + } + } + + private formatSearchQuery(userInput: string, fields = ['cm:name']) { + if (!userInput) { + return null; + } + return fields.map((field) => `${field}:"${userInput}*"`).join(' OR '); + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } + + onSearchResultLoaded(resultSetPaging: ResultSetPaging) { + this.data = resultSetPaging; + this.pagination = { ...resultSetPaging.list.pagination }; + } + + onRefreshPagination(pagination: Pagination) { + this.queryBuilder.paging = { + maxItems: pagination.maxItems, + skipCount: pagination.skipCount + }; + this.queryBuilder.update(); + } + + onDeleteElementSuccess() { + this.queryBuilder.execute(); + } + + private getSorting(): string[] { + const primary = this.queryBuilder.getPrimarySorting(); + + if (primary) { + return [primary.key, primary.ascending ? 'asc' : 'desc']; + } + + return ['name', 'asc']; + } +} 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 41d1a3cf400..19d32193cbb 100644 --- a/demo-shell/src/app/components/search/search-result.component.html +++ b/demo-shell/src/app/components/search/search-result.component.html @@ -10,7 +10,9 @@
- +
+ +
+``` + +### Properties + +| Name | Type | Default value | Description | +| ---- | ---- | ------------- | ----------- | +| showContextFacets | `boolean` | true | Toggles whether to show or not the context facet filters | + +## Details + +This component is chip based layout for searching. it just alternate component for [expanded panel search filter](./search-filter.component.md) + +You may find it useful to check out the following resources for background information +before customizing the search UI: + +- [Search API](https://docs.alfresco.com/5.2/concepts/search-api.html) +- [Alfresco Full Text Search Reference](https://docs.alfresco.com/5.2/concepts/rm-searchsyntax-intro.html) +- [ACS API Explorer](https://api-explorer.alfresco.com/api-explorer/#!/search/search) + +## See also + +- [Search Filter Component](./search-filter.component.md) +- [Search Query Builder service](../services/search-query-builder.service.md) +- [Search Widget Interface](../interfaces/search-widget.interface.md) +- [Search check list component](search-check-list.component.md) +- [Search date range component](search-date-range.component.md) +- [Search number range component](search-number-range.component.md) +- [Search radio component](search-radio.component.md) +- [Search slider component](search-slider.component.md) +- [Search text component](search-text.component.md) diff --git a/docs/content-services/components/search-number-range.component.md b/docs/content-services/components/search-number-range.component.md index 387da57000a..cfde74960f0 100644 --- a/docs/content-services/components/search-number-range.component.md +++ b/docs/content-services/components/search-number-range.component.md @@ -40,6 +40,7 @@ Implements a number range [widget](../../../lib/testing/src/lib/core/pages/form/ | ---- | ---- | ----------- | | field | string | Field to to use | | format | string | Value format. Uses string substitution to allow all sorts of [range queries](https://docs.alfresco.com/5.2/concepts/rm-searchsyntax-ranges.html). | +| hideDefaultAction | boolean | Show/hide the widget actions. By default is false. ## Details diff --git a/docs/content-services/components/search-radio.component.md b/docs/content-services/components/search-radio.component.md index 22af04cdb60..29f59917650 100644 --- a/docs/content-services/components/search-radio.component.md +++ b/docs/content-services/components/search-radio.component.md @@ -45,6 +45,8 @@ Implements a radio button list [widget](../../../lib/testing/src/lib/core/pages/ | Name | Type | Description | | ---- | ---- | ----------- | | options | `array` | Array of objects with `name` and `value` properties. Each object defines a radio button, labelled with `name`, that adds the query fragment in `value` to the query when enabled. | +| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true. +| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false. ## Details diff --git a/docs/content-services/components/search-slider.component.md b/docs/content-services/components/search-slider.component.md index 713b7cec120..0f229506798 100644 --- a/docs/content-services/components/search-slider.component.md +++ b/docs/content-services/components/search-slider.component.md @@ -46,6 +46,8 @@ Implements a numeric slider [widget](../../../lib/testing/src/lib/core/pages/for | max | number | Maximum numeric value at the right end of the slider | | step | number | The step between adjacent positions on the slider | | thumbLabel | boolean | Toggles whether the "thumb" of the slider should show the current value | +| allowUpdateOnChange | boolean | Enable/Disable the update fire event when text has been changed. By default is true. +| hideDefaultAction | boolean | Show/hide the widget actions. By default is false. ## Details diff --git a/docs/content-services/components/search-text.component.md b/docs/content-services/components/search-text.component.md index e950e323d14..a34068c4470 100644 --- a/docs/content-services/components/search-text.component.md +++ b/docs/content-services/components/search-text.component.md @@ -49,6 +49,7 @@ Implements a text input [widget](../../../lib/testing/src/lib/core/pages/form/wi | searchSuffix | string | Text to append always in the search of a string| | searchPrefix | string | Text to prepend always in the search of a string| | allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true. +| hideDefaultAction | boolean | Show/hide the widget actions. By default is false. ## Details diff --git a/docs/docassets/images/search-filter-chip-widget.png b/docs/docassets/images/search-filter-chip-widget.png new file mode 100644 index 00000000000..3c2bbcc35ce Binary files /dev/null and b/docs/docassets/images/search-filter-chip-widget.png differ diff --git a/docs/docassets/images/search-filter-chips.png b/docs/docassets/images/search-filter-chips.png new file mode 100644 index 00000000000..5f7e2fb5d9f Binary files /dev/null and b/docs/docassets/images/search-filter-chips.png differ diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts index 35157f7c9c8..a8076730d00 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts @@ -50,7 +50,7 @@ import { CustomResourcesService } from '../document-list/services/custom-resourc import { NodeEntryEvent, ShareDataRow } from '../document-list'; import { Subject } from 'rxjs'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../search/search-query-service.token'; -import { SearchQueryBuilderService } from '../search/search-query-builder.service'; +import { SearchQueryBuilderService } from '../search/services/search-query-builder.service'; import { ContentNodeSelectorPanelService } from './content-node-selector-panel.service'; export type ValidationFunction = (entry: Node) => boolean; diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector.module.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector.module.ts index 096921b4828..6795339f8cb 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector.module.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector.module.ts @@ -29,7 +29,7 @@ import { CoreModule } from '@alfresco/adf-core'; import { DocumentListModule } from '../document-list/document-list.module'; import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component'; import { UploadModule } from '../upload/upload.module'; -import { SearchQueryBuilderService } from '../search/search-query-builder.service'; +import { SearchQueryBuilderService } from '../search/services/search-query-builder.service'; import { ContentDirectiveModule } from '../directives/content-directive.module'; @NgModule({ diff --git a/lib/content-services/src/lib/document-list/components/filter-header/filter-header.component.spec.ts b/lib/content-services/src/lib/document-list/components/filter-header/filter-header.component.spec.ts index 26755732fa9..20bff33a15b 100644 --- a/lib/content-services/src/lib/document-list/components/filter-header/filter-header.component.spec.ts +++ b/lib/content-services/src/lib/document-list/components/filter-header/filter-header.component.spec.ts @@ -21,7 +21,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { SearchService, setupTestBed, DataTableComponent, DataSorting } from '@alfresco/adf-core'; import { ContentTestingModule } from '../../../testing/content.testing.module'; import { SimpleChange } from '@angular/core'; -import { SearchHeaderQueryBuilderService } from './../../../search/search-header-query-builder.service'; +import { SearchHeaderQueryBuilderService } from './../../../search/services/search-header-query-builder.service'; import { SEARCH_QUERY_SERVICE_TOKEN } from './../../../search/search-query-service.token'; import { DocumentListComponent } from './../document-list.component'; import { FilterHeaderComponent } from './filter-header.component'; diff --git a/lib/content-services/src/lib/document-list/components/filter-header/filter-header.component.ts b/lib/content-services/src/lib/document-list/components/filter-header/filter-header.component.ts index 0e25b5f662e..8c348ac6479 100644 --- a/lib/content-services/src/lib/document-list/components/filter-header/filter-header.component.ts +++ b/lib/content-services/src/lib/document-list/components/filter-header/filter-header.component.ts @@ -19,7 +19,7 @@ import { Component, Inject, OnInit, OnChanges, SimpleChanges, Input, Output, Eve import { PaginationModel, DataSorting } from '@alfresco/adf-core'; import { DocumentListComponent } from '../document-list.component'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../../search/search-query-service.token'; -import { SearchHeaderQueryBuilderService } from '../../../search/search-header-query-builder.service'; +import { SearchHeaderQueryBuilderService } from '../../../search/services/search-header-query-builder.service'; import { FilterSearch } from './../../../search/models/filter-search.interface'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index d0c6687cdf7..54302f26551 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -223,14 +223,17 @@ }, "FILTER": { "ACTIONS": { + "SEARCH": "Search", "CLEAR": "Clear", "APPLY": "Apply", "CLEAR-ALL": "Clear all", "SHOW-MORE": "Show more", - "SHOW-LESS": "Show less", - "FILTER-CATEGORY": "Filter category" + "SHOW-LESS": "Show less" }, "BUTTONS": { + "CLOSE": "Close", + "REMOVE": "Remove", + "APPLY": "Apply", "CLEAR-ALL": { "LABEL": "Clear all", "TOOLTIP": "This will remove all selections" @@ -290,8 +293,7 @@ } } }, - "FORMS": "Search Forms", - "UNKNOWN_FORM": "Unknown Configuration", + "UNKNOWN_CONFIGURATION": "Unknown Configuration", "SEARCH_HEADER" : { "TITLE":"Filter", "TYPE": "Type", diff --git a/lib/content-services/src/lib/mock/search-filter-mock.ts b/lib/content-services/src/lib/mock/search-filter-mock.ts index 3b492c2585f..d886b5b9618 100644 --- a/lib/content-services/src/lib/mock/search-filter-mock.ts +++ b/lib/content-services/src/lib/mock/search-filter-mock.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { SearchCategory } from '../search'; + export const expandableCategories = [ { id: 'cat-1', @@ -68,7 +70,7 @@ export const expandedCategories = [ } ]; -export const simpleCategories = [ +export const simpleCategories: SearchCategory[] = [ { id: 'queryName', name: 'Name', @@ -76,7 +78,9 @@ export const simpleCategories = [ enabled: true, component: { selector: 'text', - settings: {} + settings: { + field: '' + } } }, { @@ -87,7 +91,7 @@ export const simpleCategories = [ component: { selector: 'check-list', settings: { - 'field': null, + 'field': 'check-list', 'pageSize': 5, 'options': [ { 'name': 'Folder', 'value': "TYPE:'cm:folder'" }, @@ -624,6 +628,7 @@ export const mockContentSizeResponseBucket = { }; export function getMockSearchResultWithResponseBucket() { - mockSearchResult.list.context.facets[3].buckets.push(mockContentSizeResponseBucket); - return mockSearchResult; + const cloneResult = JSON.parse(JSON.stringify( mockSearchResult)); + cloneResult.list.context.facets[3].buckets.push(mockContentSizeResponseBucket); + return cloneResult; } diff --git a/lib/content-services/src/lib/search/components/reset-search.directive.spec.ts b/lib/content-services/src/lib/search/components/reset-search.directive.spec.ts new file mode 100644 index 00000000000..4d592693c98 --- /dev/null +++ b/lib/content-services/src/lib/search/components/reset-search.directive.spec.ts @@ -0,0 +1,58 @@ +/*! + * @license + * Copyright 2019 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 } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { setupTestBed } from '@alfresco/adf-core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ContentTestingModule } from '../../testing/content.testing.module'; +import { SearchFacetFiltersService } from '../services/search-facet-filters.service'; +import { SearchQueryBuilderService } from '../services/search-query-builder.service'; + +@Component({ + template: `` +}) +class TestComponent { +} + +describe('Directive: ResetSearchDirective', () => { + let fixture: ComponentFixture; + let searchFacetFiltersService: SearchFacetFiltersService; + let queryBuilder: SearchQueryBuilderService; + + setupTestBed({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ], + declarations: [TestComponent] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService); + queryBuilder = TestBed.inject(SearchQueryBuilderService); + }); + + it('should reset the search on click', () => { + spyOn(queryBuilder, 'resetToDefaults'); + searchFacetFiltersService.responseFacets = [ { type: 'field', label: 'f1' } ] as any; + fixture.nativeElement.querySelector('button').click(); + expect(searchFacetFiltersService.responseFacets).toEqual([]); + expect(queryBuilder.resetToDefaults).toHaveBeenCalled(); + }); +}); diff --git a/lib/content-services/src/lib/search/components/reset-search.directive.ts b/lib/content-services/src/lib/search/components/reset-search.directive.ts new file mode 100644 index 00000000000..f3ca809b975 --- /dev/null +++ b/lib/content-services/src/lib/search/components/reset-search.directive.ts @@ -0,0 +1,31 @@ +/*! + * @license + * Copyright 2019 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 { Directive, HostListener } from '@angular/core'; +import { SearchFacetFiltersService } from '../services/search-facet-filters.service'; + +@Directive({ + selector: '[adf-reset-search]' +}) +export class ResetSearchDirective { + @HostListener('click') + onClick() { + this.filterService.reset(); + } + + constructor(private filterService: SearchFacetFiltersService) { } +} diff --git a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.html b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.html index c1c0c1ee9a9..e21a26f0461 100644 --- a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.html +++ b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.html @@ -6,8 +6,7 @@ [attr.data-automation-id]="'checkbox-' + (option.name)" (change)="changeHandler($event, option)" class="adf-facet-filter"> -
{{ option.name | translate }} @@ -15,9 +14,9 @@
- -
-
@@ -25,7 +24,7 @@
+ + + + +
+ + +
+ +
+ {{ bucket.display || bucket.label | translate }} {{ getBucketCountDisplay(bucket) }} +
+
+
+ +
+ +
+ +
+ + + +
+ diff --git a/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.scss b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.scss new file mode 100644 index 00000000000..38caf64086d --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.scss @@ -0,0 +1,89 @@ +@mixin adf-search-filter-field-theme($theme) { + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + + .adf-search-filter-facet { + .adf-checklist { + display: flex; + flex-direction: column; + + .mat-checkbox-label { + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + } + + .mat-checkbox-layout { + width: 100%; + } + + .adf-facet-label { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mat-checkbox { + margin: 5px; + + &.mat-checkbox-checked .mat-checkbox-label { + font-weight: bold; + } + } + } + + .adf-facet-result-filter { + padding-bottom: 16px; + + .adf-facet-search-container { + border-radius: 6px; + background: mat-color($background, background); + display: flex; + height: 32px; + + .adf-facet-search-icon { + width: 27px; + margin-top: -4px; + .mat-icon { + font-size: 15px; + } + } + + .adf-facet-search-field { + padding: 2px; + flex: 1; + margin-top: -16px; + font-size: 14px; + line-height: 24px; + letter-spacing: 0.25px; + + .mat-form-field-underline { + display: none; + } + + .mat-form-field-suffix { + padding-right: 1px; + } + } + } + } + + .adf-facet-buttons { + text-align: right; + + .mat-button { + text-transform: uppercase; + } + + &--topSpace { + padding-top: 15px; + } + } + + .mat-checkbox-label, + .mat-radio-label { + color: mat-color($foreground, text, 0.54); + } + + } +} diff --git a/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.spec.ts b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.spec.ts new file mode 100644 index 00000000000..f9c63e87dfd --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.spec.ts @@ -0,0 +1,195 @@ +/*! + * @license + * Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SearchFacetFieldComponent } from './search-facet-field.component'; +import { SearchFacetFiltersService } from '../../services/search-facet-filters.service'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { FacetField } from '../../models/facet-field.interface'; +import { FacetFieldBucket } from '../../models/facet-field-bucket.interface'; +import { SearchFilterList } from '../../models/search-filter-list.model'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('SearchFacetFieldComponent', () => { + let component: SearchFacetFieldComponent; + let fixture: ComponentFixture; + let searchFacetFiltersService: SearchFacetFiltersService; + let queryBuilder: SearchQueryBuilderService; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService); + queryBuilder = TestBed.inject(SearchQueryBuilderService); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchFacetFieldComponent); + component = fixture.componentInstance; + spyOn(searchFacetFiltersService, 'updateSelectedBuckets').and.stub(); + }); + + it('should update bucket model and query builder on facet toggle', () => { + spyOn(queryBuilder, 'update').and.stub(); + spyOn(queryBuilder, 'addUserFacetBucket').and.callThrough(); + + const event: any = { checked: true }; + const field: FacetField = { field: 'f1', label: 'f1', buckets: new SearchFilterList() }; + const bucket: FacetFieldBucket = { checked: false, filterQuery: 'q1', label: 'q1', count: 1 }; + component.field = field; + fixture.detectChanges(); + + component.onToggleBucket(event, field, bucket); + + expect(bucket.checked).toBeTruthy(); + expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(field, bucket); + expect(queryBuilder.update).toHaveBeenCalled(); + expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled(); + }); + + it('should update bucket model and query builder on facet un-toggle', () => { + spyOn(queryBuilder, 'update').and.stub(); + spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough(); + + const event: any = { checked: false }; + const field: FacetField = { field: 'f1', label: 'f1', buckets: new SearchFilterList() }; + const bucket: FacetFieldBucket = { checked: true, filterQuery: 'q1', label: 'q1', count: 1 }; + + component.field = field; + fixture.detectChanges(); + + component.onToggleBucket(event, field, bucket); + + expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, bucket); + expect(queryBuilder.update).toHaveBeenCalled(); + expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled(); + }); + + it('should unselect facet query and update builder', () => { + spyOn(queryBuilder, 'update').and.stub(); + spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough(); + + const event: any = { checked: false }; + const query = { checked: true, label: 'q1', filterQuery: 'query1' }; + const field = { field: 'q1', type: 'query', label: 'label1', buckets: new SearchFilterList([ query ] ) } as FacetField; + + component.field = field; + fixture.detectChanges(); + + component.onToggleBucket(event, field, query); + + expect(query.checked).toEqual(false); + expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, query); + expect(queryBuilder.update).toHaveBeenCalled(); + expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled(); + }); + + it('should update query builder only when has bucket to unselect', () => { + spyOn(queryBuilder, 'update').and.stub(); + + const field: FacetField = { field: 'f1', label: 'f1' }; + component.onToggleBucket( { checked: true }, field, null); + + expect(queryBuilder.update).not.toHaveBeenCalled(); + }); + + it('should allow to to reset selected buckets', () => { + const buckets: FacetFieldBucket[] = [ + { label: 'bucket1', checked: true, count: 1, filterQuery: 'q1' }, + { label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' } + ]; + + const field: FacetField = { + field: 'f1', + label: 'field1', + buckets: new SearchFilterList(buckets) + }; + + component.field = field; + fixture.detectChanges(); + + expect(component.canResetSelectedBuckets(field)).toBeTruthy(); + }); + + it('should not allow to reset selected buckets', () => { + const buckets: FacetFieldBucket[] = [ + { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, + { label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' } + ]; + + const field: FacetField = { + field: 'f1', + label: 'field1', + buckets: new SearchFilterList(buckets) + }; + + component.field = field; + fixture.detectChanges(); + + expect(component.canResetSelectedBuckets(field)).toEqual(false); + }); + + it('should reset selected buckets', () => { + spyOn(queryBuilder, 'execute').and.stub(); + const buckets: FacetFieldBucket[] = [ + { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, + { label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' } + ]; + + const field: FacetField = { + field: 'f1', + label: 'field1', + buckets: new SearchFilterList(buckets) + }; + + component.field = field; + fixture.detectChanges(); + + component.resetSelectedBuckets(field); + + expect(buckets[0].checked).toEqual(false); + expect(buckets[1].checked).toEqual(false); + }); + + it('should update query builder upon resetting buckets', () => { + spyOn(queryBuilder, 'update').and.stub(); + + const buckets: FacetFieldBucket[] = [ + { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, + { label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' } + ]; + + const field: FacetField = { + field: 'f1', + label: 'field1', + buckets: new SearchFilterList(buckets) + }; + + component.field = field; + fixture.detectChanges(); + + component.resetSelectedBuckets(field); + expect(queryBuilder.update).toHaveBeenCalled(); + }); + +}); diff --git a/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.ts b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.ts new file mode 100644 index 00000000000..6aa8460eee8 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.ts @@ -0,0 +1,129 @@ +/*! + * @license + * Copyright 2019 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, Inject, Input, ViewEncapsulation } from '@angular/core'; +import { FacetField } from '../../models/facet-field.interface'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { FacetFieldBucket } from '../../models/facet-field-bucket.interface'; +import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; +import { SearchFacetFiltersService } from '../../services/search-facet-filters.service'; +import { FacetWidget } from '../../models/facet-widget.interface'; +import { TranslationService } from '@alfresco/adf-core'; +import { Subject } from 'rxjs'; + +@Component({ + selector: 'adf-search-facet-field', + templateUrl: './search-facet-field.component.html', + encapsulation: ViewEncapsulation.None +}) +export class SearchFacetFieldComponent implements FacetWidget { + + @Input() + field!: FacetField; + + displayValue$: Subject = new Subject(); + + constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService, + private searchFacetFiltersService: SearchFacetFiltersService, + private translationService: TranslationService) { + } + + get canUpdateOnChange() { + return this.field.settings?.allowUpdateOnChange ?? true; + } + + onToggleBucket(event: MatCheckboxChange, field: FacetField, bucket: FacetFieldBucket) { + if (event && bucket) { + if (event.checked) { + this.selectFacetBucket(field, bucket); + } else { + this.unselectFacetBucket(field, bucket); + } + } + } + + selectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { + if (bucket) { + bucket.checked = true; + this.queryBuilder.addUserFacetBucket(field, bucket); + this.searchFacetFiltersService.updateSelectedBuckets(); + if (this.canUpdateOnChange) { + this.updateDisplayValue(); + this.queryBuilder.update(); + } + } + } + + unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { + if (bucket) { + bucket.checked = false; + this.queryBuilder.removeUserFacetBucket(field, bucket); + this.searchFacetFiltersService.updateSelectedBuckets(); + if (this.canUpdateOnChange) { + this.updateDisplayValue(); + this.queryBuilder.update(); + } + } + } + + canResetSelectedBuckets(field: FacetField): boolean { + if (field && field.buckets) { + return field.buckets.items.some((bucket) => bucket.checked); + } + return false; + } + + resetSelectedBuckets(field: FacetField) { + if (field && field.buckets) { + for (const bucket of field.buckets.items) { + bucket.checked = false; + this.queryBuilder.removeUserFacetBucket(field, bucket); + } + this.searchFacetFiltersService.updateSelectedBuckets(); + if (this.canUpdateOnChange) { + this.queryBuilder.update(); + } + } + } + + getBucketCountDisplay(bucket: FacetFieldBucket): string { + return bucket.count === null ? '' : `(${bucket.count})`; + } + + updateDisplayValue(): void { + if (!this.field.buckets?.items) { + this.displayValue$.next(''); + } else { + const displayValue = this.field.buckets?.items?.filter((item) => item.checked) + .map((item) => this.translationService.instant(item.display || item.label)) + .join(', '); + this.displayValue$.next(displayValue); + } + } + + reset(): void { + this.resetSelectedBuckets(this.field); + this.updateDisplayValue(); + this.queryBuilder.update(); + } + + submitValues(): void { + this.updateDisplayValue(); + this.queryBuilder.update(); + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.html b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.html new file mode 100644 index 00000000000..6d264b1da11 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.html @@ -0,0 +1,42 @@ + + + + {{ field.label | translate }} + : + + + +   {{ displayValue | translate }} + + keyboard_arrow_down + + + +
+ + + {{ field.label | translate }} + + + + + + + + + +
+
diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.spec.ts new file mode 100644 index 00000000000..daea4092d26 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.spec.ts @@ -0,0 +1,66 @@ +/*! + * @license + * Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchFacetChipComponent } from './search-facet-chip.component'; +import { ContentTestingModule } from '../../../../testing/content.testing.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { SearchQueryBuilderService } from '../../../services/search-query-builder.service'; +import { setupTestBed } from '@alfresco/adf-core'; +import { SearchFilterList } from '../../../models/search-filter-list.model'; + +describe('SearchFacetChipComponent', () => { + let component: SearchFacetChipComponent; + let fixture: ComponentFixture; + let queryBuilder: SearchQueryBuilderService; + + setupTestBed({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchFacetChipComponent); + component = fixture.componentInstance; + queryBuilder = TestBed.inject(SearchQueryBuilderService); + spyOn(queryBuilder, 'update').and.stub(); + + component.field = { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() }; + fixture.detectChanges(); + }); + + it('should update search query on apply click', () => { + const chip = fixture.debugElement.query(By.css('mat-chip')); + chip.triggerEventHandler('click', { stopPropagation: () => null }); + fixture.detectChanges(); + const applyButton = fixture.debugElement.query(By.css('#apply-filter-button')); + applyButton.triggerEventHandler('click', {}); + expect(queryBuilder.update).toHaveBeenCalled(); + }); + + it('should update search query on cancel click', () => { + const chip = fixture.debugElement.query(By.css('mat-chip')); + chip.triggerEventHandler('click', { stopPropagation: () => null }); + fixture.detectChanges(); + const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button')); + applyButton.triggerEventHandler('click', {}); + expect(queryBuilder.update).toHaveBeenCalled(); + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.ts new file mode 100644 index 00000000000..84fed678997 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip/search-facet-chip.component.ts @@ -0,0 +1,67 @@ +/*! + * @license + * Copyright 2019 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, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core'; +import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y'; +import { FacetField } from '../../../models/facet-field.interface'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { SearchFacetFieldComponent } from '../../search-facet-field/search-facet-field.component'; + +@Component({ + selector: 'adf-search-facet-chip', + templateUrl: './search-facet-chip.component.html', + encapsulation: ViewEncapsulation.None +}) +export class SearchFacetChipComponent { + @Input() + field: FacetField; + + @ViewChild('menuContainer', { static: false }) + menuContainer: ElementRef; + + @ViewChild('menuTrigger', { static: false }) + menuTrigger: MatMenuTrigger; + + @ViewChild(SearchFacetFieldComponent, { static: false }) + facetFieldComponent: SearchFacetFieldComponent; + + focusTrap: ConfigurableFocusTrap; + + constructor(private focusTrapFactory: ConfigurableFocusTrapFactory) {} + + onMenuOpen() { + if (this.menuContainer && !this.focusTrap) { + this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement); + this.focusTrap.focusInitialElement(); + } + } + + onClosed() { + this.focusTrap.destroy(); + this.focusTrap = null; + } + + onRemove() { + this.facetFieldComponent.reset(); + this.menuTrigger.closeMenu(); + } + + onApply() { + this.facetFieldComponent.submitValues(); + this.menuTrigger.closeMenu(); + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.html b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.html new file mode 100644 index 00000000000..daa3e1388e3 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.scss b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.scss new file mode 100644 index 00000000000..3f92fd20a66 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.scss @@ -0,0 +1,66 @@ +@mixin adf-search-filter-chips-theme($theme) { + $accent: map-get($theme, accent); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + $unselected-background: mat-color($background, unselected-chip); + $unselected-foreground: mat-color($foreground, text); + $selected-chip-background: mat-color($background, card); + $chip-placeholder: mat-color($foreground, disabled-text); + + .adf-search-filter-chip { + + &.mat-chip { + border: 2px solid transparent; + transition : border 500ms ease-in-out; + max-width: 320px; + text-overflow: ellipsis; + overflow: hidden; + background: $unselected-background; + + &:focus { + color: unset; + } + + &.mat-standard-chip::after { + background: $unselected-background; + color: unset; + } + + &.mat-chip-list-wrapper { + margin: 4px 6px; + } + } + + &.adf-search-toggle-chip { + background: $selected-chip-background; + border: 2px solid mat-color($accent); + + &.mat-chip::after { + background: unset; + } + } + + .adf-search-filter-placeholder { + flex: 1 1 auto; + white-space: nowrap; + color: $chip-placeholder; + } + + .adf-search-filter-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .mat-icon { + padding-top: 5px; + padding-left: 5px; + } + + &-menu + * .cdk-overlay-pane .mat-menu-panel { + min-width: 320px; + border-radius: 12px; + @include mat-elevation(2); + } + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.spec.ts new file mode 100644 index 00000000000..fcc2b274ecf --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.spec.ts @@ -0,0 +1,413 @@ +/*! + * @license + * Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchFilterChipsComponent } from './search-filter-chips.component'; +import { SearchFacetFiltersService } from '../../services/search-facet-filters.service'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { By } from '@angular/platform-browser'; +import { SearchFacetFieldComponent } from '../search-facet-field/search-facet-field.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { SearchFilterList } from '../../models/search-filter-list.model'; +import { + disabledCategories, + filteredResult, + mockSearchResult, + searchFilter, + simpleCategories, + stepOne, + stepThree, + stepTwo +} from '../../../mock'; +import { getAllMenus } from '../search-filter/search-filter.component.spec'; +import { AppConfigService } from '@alfresco/adf-core'; + +describe('SearchFilterChipsComponent', () => { + let fixture: ComponentFixture; + let searchFacetFiltersService: SearchFacetFiltersService; + let queryBuilder: SearchQueryBuilderService; + let appConfigService: AppConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + queryBuilder = TestBed.inject(SearchQueryBuilderService); + appConfigService = TestBed.inject(AppConfigService); + searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService); + fixture = TestBed.createComponent(SearchFilterChipsComponent); + }); + + it('should fetch facet fields from response payload and show the already checked items', () => { + spyOn(queryBuilder, 'execute').and.stub(); + queryBuilder.config = { + categories: [], + facetFields: { fields: [ + { label: 'f1', field: 'f1' }, + { label: 'f2', field: 'f2' } + ]}, + facetQueries: { + queries: [] + } + }; + + searchFacetFiltersService.responseFacets = [ + { type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([ + { label: 'b1', count: 10, filterQuery: 'filter', checked: true }, + { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, + { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList()} + ]; + searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + + const serverResponseFields: any = [ + { type: 'field', label: 'f1', field: 'f1', buckets: [ + { label: 'b1', metrics: [{value: {count: 6}}], filterQuery: 'filter' }, + { label: 'b2', metrics: [{value: {count: 1}}], filterQuery: 'filter2' }] }, + { type: 'field', label: 'f2', field: 'f2', buckets: [] } + ]; + const data = { + list: { + context: { + facets: serverResponseFields + } + } + }; + + fixture.detectChanges(); + + const facetChip = fixture.debugElement.query(By.css('[data-automation-id="search-fact-chip-f1"] mat-chip')); + facetChip.triggerEventHandler('click', { stopPropagation: () => null }); + + fixture.detectChanges(); + + const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance; + facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]); + + searchFacetFiltersService.onDataLoaded(data); + expect(searchFacetFiltersService.responseFacets.length).toEqual(2); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].checked).toEqual(true, 'should show the already checked item'); + }); + + it('should fetch facet fields from response payload and show the newly checked items', () => { + spyOn(queryBuilder, 'execute').and.stub(); + queryBuilder.config = { + categories: [], + facetFields: { fields: [ + { label: 'f1', field: 'f1' }, + { label: 'f2', field: 'f2' } + ]}, + facetQueries: { + queries: [] + } + }; + + searchFacetFiltersService.responseFacets = [ + { type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([ + { label: 'b1', count: 10, filterQuery: 'filter', checked: true }, + { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, + { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList()} + ]; + queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + + const serverResponseFields: any = [ + { type: 'field', label: 'f1', field: 'f1', buckets: [ + { label: 'b1', metrics: [{value: {count: 6}}], filterQuery: 'filter' }, + { label: 'b2', metrics: [{value: {count: 1}}], filterQuery: 'filter2' }] }, + { type: 'field', label: 'f2', field: 'f2', buckets: [] } + ]; + const data = { + list: { + context: { + facets: serverResponseFields + } + } + }; + fixture.detectChanges(); + + const facetChip = fixture.debugElement.query(By.css('[data-automation-id="search-fact-chip-f1"] mat-chip')); + facetChip.triggerEventHandler('click', { stopPropagation: () => null }); + + fixture.detectChanges(); + + const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance; + facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]); + + searchFacetFiltersService.onDataLoaded(data); + expect(searchFacetFiltersService.responseFacets.length).toEqual(2); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].checked).toEqual(true, 'should show the newly checked item'); + }); + + it('should show buckets with 0 values when there are no facet fields on the response payload', () => { + spyOn(queryBuilder, 'execute').and.stub(); + queryBuilder.config = { + categories: [], + facetFields: { fields: [ + { label: 'f1', field: 'f1' }, + { label: 'f2', field: 'f2' } + ]}, + facetQueries: { + queries: [] + } + }; + + searchFacetFiltersService.responseFacets = [ + { type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList( [ + { label: 'b1', count: 10, filterQuery: 'filter', checked: true }, + { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, + { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() } + ]; + queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + const data = { + list: { + context: {} + } + }; + fixture.detectChanges(); + + const facetChip = fixture.debugElement.query(By.css('[data-automation-id="search-fact-chip-f1"] mat-chip')); + facetChip.triggerEventHandler('click', { stopPropagation: () => null }); + + fixture.detectChanges(); + + const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance; + facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]); + searchFacetFiltersService.onDataLoaded(data); + + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(0); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(0); + }); + + it('should update query builder upon resetting selected queries', () => { + spyOn(queryBuilder, 'update').and.stub(); + spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough(); + + const queryResponse = { + field: 'query-response', + label: 'query response', + buckets: new SearchFilterList([ + { label: 'q1', query: 'q1', checked: true, metrics: [{value: {count: 1}}] }, + { label: 'q2', query: 'q2', checked: false, metrics: [{value: {count: 1}}] }, + { label: 'q3', query: 'q3', checked: true, metrics: [{value: {count: 1}}] }]) + } as any; + searchFacetFiltersService.responseFacets = [queryResponse]; + + fixture.detectChanges(); + + const facetChip = fixture.debugElement.query(By.css(`[data-automation-id="search-fact-chip-query-response"] mat-chip`)); + facetChip.triggerEventHandler('click', { stopPropagation: () => null }); + fixture.detectChanges(); + const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance; + + facetField.resetSelectedBuckets(queryResponse); + + expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledTimes(3); + expect(queryBuilder.update).toHaveBeenCalled(); + + for (const entry of searchFacetFiltersService.responseFacets[0].buckets.items) { + expect(entry.checked).toEqual(false); + } + }); + + describe('widgets', () => { + + it('should not show the disabled widget', async () => { + appConfigService.config.search = { categories: disabledCategories }; + queryBuilder.resetToDefaults(); + + fixture.detectChanges(); + await fixture.whenStable(); + const chips = fixture.debugElement.queryAll(By.css('mat-chip')); + expect(chips.length).toBe(0); + }); + + it('should show the widgets only if configured', async () => { + appConfigService.config.search = { categories: simpleCategories }; + queryBuilder.resetToDefaults(); + + fixture.detectChanges(); + await fixture.whenStable(); + + const chips = fixture.debugElement.queryAll(By.css('mat-chip')); + expect(chips.length).toBe(2); + + const titleElements = fixture.debugElement.queryAll(By.css('.adf-search-filter-placeholder')); + expect(titleElements.map(title => title.nativeElement.innerText.trim())).toEqual(['Name', 'Type']); + }); + + it('should be update the search query when name changed', async () => { + spyOn(queryBuilder, 'update').and.stub(); + appConfigService.config.search = searchFilter; + queryBuilder.resetToDefaults(); + + fixture.detectChanges(); + await fixture.whenStable(); + let chips = fixture.debugElement.queryAll(By.css('mat-chip')); + expect(chips.length).toBe(6); + + fixture.detectChanges(); + const searchChip = fixture.debugElement.query(By.css(`[data-automation-id="search-filter-chip-Name"]`)); + searchChip.triggerEventHandler('click', { stopPropagation: () => null }); + fixture.detectChanges(); + + const inputElement = fixture.debugElement.query(By.css('[data-automation-id="search-field-Name"] input')); + inputElement.triggerEventHandler('change', { target: { value: '*' } }); + expect(queryBuilder.update).toHaveBeenCalled(); + + queryBuilder.executed.next( mockSearchResult); + await fixture.whenStable(); + fixture.detectChanges(); + + chips = fixture.debugElement.queryAll(By.css('mat-chip')); + expect(chips.length).toBe(8); + }); + + it('should show the long facet options list with pagination', () => { + const field = `[data-automation-id="search-field-Size facet queries"]`; + appConfigService.config.search = searchFilter; + queryBuilder.resetToDefaults(); + + fixture.detectChanges(); + queryBuilder.executed.next( mockSearchResult); + fixture.detectChanges(); + + fixture.detectChanges(); + const searchChip = fixture.debugElement.query(By.css(`[data-automation-id="search-filter-chip-Size facet queries"]`)); + searchChip.triggerEventHandler('click', { stopPropagation: () => null }); + fixture.detectChanges(); + + let sizes = getAllMenus(`${field} mat-checkbox`, fixture); + expect(sizes).toEqual(stepOne); + + let moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`)); + let lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`)); + + expect(lessButton).toEqual(null); + expect(moreButton).toBeDefined(); + + moreButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + + sizes = getAllMenus(`${field} mat-checkbox`, fixture); + expect(sizes).toEqual(stepTwo); + + moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`)); + lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`)); + expect(lessButton).toBeDefined(); + expect(moreButton).toBeDefined(); + + moreButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + sizes = getAllMenus(`${field} mat-checkbox`, fixture); + + expect(sizes).toEqual(stepThree); + + moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`)); + lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`)); + expect(lessButton).toBeDefined(); + expect(moreButton).toEqual(null); + + lessButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + + sizes = getAllMenus(`${field} mat-checkbox`, fixture); + expect(sizes).toEqual(stepTwo); + + moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`)); + lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`)); + expect(lessButton).toBeDefined(); + expect(moreButton).toBeDefined(); + + lessButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + + sizes = getAllMenus(`${field} mat-checkbox`, fixture); + expect(sizes).toEqual(stepOne); + + moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`)); + lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`)); + expect(lessButton).toEqual(null); + expect(moreButton).toBeDefined(); + }); + + it('should not show facets if filter is not available', () => { + const chip = '[data-automation-id="search-filter-chip-Size facet queries"]'; + const filter = { ...searchFilter }; + delete filter.facetQueries; + + appConfigService.config.search = filter; + queryBuilder.resetToDefaults(); + + fixture.detectChanges(); + queryBuilder.executed.next( mockSearchResult); + fixture.detectChanges(); + + const facetElement = fixture.debugElement.query(By.css(chip)); + expect(facetElement).toEqual(null); + }); + + it('should search the facets options and select it', () => { + const field = `[data-automation-id="search-field-Size facet queries"]`; + appConfigService.config.search = searchFilter; + queryBuilder.resetToDefaults(); + fixture.detectChanges(); + queryBuilder.executed.next( mockSearchResult); + fixture.detectChanges(); + + spyOn(queryBuilder, 'update').and.stub(); + + const searchChip = fixture.debugElement.query(By.css(`[data-automation-id="search-filter-chip-Size facet queries"]`)); + searchChip.triggerEventHandler('click', { stopPropagation: () => null }); + fixture.detectChanges(); + + const inputElement = fixture.debugElement.query(By.css(`${field} input`)); + inputElement.nativeElement.value = 'Extra'; + inputElement.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + let filteredMenu = getAllMenus(`${field} mat-checkbox`, fixture); + expect(filteredMenu).toEqual(['Extra Small (10239)']); + + inputElement.nativeElement.value = 'my'; + inputElement.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + filteredMenu = getAllMenus(`${field} mat-checkbox`, fixture); + expect(filteredMenu).toEqual(filteredResult); + + const clearButton = fixture.debugElement.query(By.css(`${field} mat-form-field button`)); + clearButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + + filteredMenu = getAllMenus(`${field} mat-checkbox`, fixture); + expect(filteredMenu).toEqual(stepOne); + + const firstOption = fixture.debugElement.query(By.css(`${field} mat-checkbox`)); + firstOption.triggerEventHandler('change', { checked: true }); + fixture.detectChanges(); + + const checkedOption = fixture.debugElement.query(By.css(`${field} mat-checkbox.mat-checkbox-checked`)); + expect(checkedOption.nativeElement.innerText).toEqual('Extra Small (10239)'); + + expect(queryBuilder.update).toHaveBeenCalledTimes(1); + }); + + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts new file mode 100644 index 00000000000..3ccd9d3c810 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts @@ -0,0 +1,38 @@ +/*! + * @license + * Copyright 2019 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, Inject, Input, ViewEncapsulation } from '@angular/core'; +import { SearchFacetFiltersService } from '../../services/search-facet-filters.service'; +import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; + +@Component({ + selector: 'adf-search-filter-chips', + templateUrl: './search-filter-chips.component.html', + encapsulation: ViewEncapsulation.None +}) +export class SearchFilterChipsComponent { + /** Toggles whether to show or not the context facet filters. */ + @Input() + showContextFacets: boolean = true; + + constructor( + @Inject(SEARCH_QUERY_SERVICE_TOKEN) + public queryBuilder: SearchQueryBuilderService, + public facetFiltersService: SearchFacetFiltersService) {} + +} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.html b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.html new file mode 100644 index 00000000000..420b8ad7d5d --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.html @@ -0,0 +1,22 @@ +
+
+ + + close + +
+ + + +
+ +
+ + + +
+ +
+
diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.scss b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.scss new file mode 100644 index 00000000000..5a8f0c2be12 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.scss @@ -0,0 +1,39 @@ +@mixin adf-search-filter-menu-card($theme) { + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + + .adf-search-filter-menu-card { + color: mat-color($foreground, text); + background: mat-color($background, card); + + .adf-search-filter-title { + padding: 16px 12px; + height: 32px; + flex: 1 1 auto; + font-size: 14px; + letter-spacing: 0.15px; + line-height: 24px; + font-weight: bold; + font-style: inherit; + + &-action { + float: right; + } + } + + .adf-search-filter-content { + padding: 16px 12px; + overflow: auto; + } + + .adf-search-filter-actions { + padding: 16px 12px; + display: flex; + justify-content: space-between; + + .adf-search-action-button { + border-radius: 6px; + } + } + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.spec.ts new file mode 100644 index 00000000000..28e8caeeba3 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.spec.ts @@ -0,0 +1,46 @@ +/*! + * @license + * Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchFilterMenuCardComponent } from './search-filter-menu-card.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { ContentTestingModule } from '../../../../testing/content.testing.module'; +import { setupTestBed } from '@alfresco/adf-core'; + +describe('SearchFilterMenuComponent', () => { + let component: SearchFilterMenuCardComponent; + let fixture: ComponentFixture; + + setupTestBed({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchFilterMenuCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should emit on close click', (done) => { + component.close.subscribe(() => done()); + const closButton = fixture.debugElement.nativeElement.querySelector('.adf-search-filter-title-action'); + closButton.click(); + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.ts new file mode 100644 index 00000000000..42874e2d3f4 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.ts @@ -0,0 +1,31 @@ +/*! + * @license + * Copyright 2019 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, EventEmitter, Output } from '@angular/core'; + +@Component({ + selector: 'adf-search-filter-menu-card', + templateUrl: './search-filter-menu-card.component.html' +}) +export class SearchFilterMenuCardComponent { + @Output() + close = new EventEmitter(); + + onClose() { + this.close.emit(); + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.html b/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.html new file mode 100644 index 00000000000..1b076b0e1a0 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.html @@ -0,0 +1,47 @@ + + + {{ category.name | translate }} + : + + +  {{ displayValue | translate }} + + keyboard_arrow_down + + + +
+ + + + {{ category.name | translate }} ({{category.component.settings.unit}}) + + + + + + + + + + + + +
+
diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.spec.ts new file mode 100644 index 00000000000..3df03e62477 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.spec.ts @@ -0,0 +1,68 @@ +/*! + * @license + * Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchWidgetChipComponent } from './search-widget-chip.component'; +import { simpleCategories } from '../../../../mock'; +import { setupTestBed } from '@alfresco/adf-core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ContentTestingModule } from '../../../../testing/content.testing.module'; +import { MatMenuModule } from '@angular/material/menu'; +import { By } from '@angular/platform-browser'; +import { SearchQueryBuilderService } from '../../../services/search-query-builder.service'; + +describe('SearchWidgetChipComponent', () => { + let component: SearchWidgetChipComponent; + let fixture: ComponentFixture; + let queryBuilder: SearchQueryBuilderService; + + setupTestBed( { + imports: [ + MatMenuModule, + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + + beforeEach(() => { + queryBuilder = TestBed.inject(SearchQueryBuilderService); + fixture = TestBed.createComponent(SearchWidgetChipComponent); + component = fixture.componentInstance; + spyOn(queryBuilder, 'update').and.stub(); + + component.category = simpleCategories[1]; + fixture.detectChanges(); + }); + + it('should update search query on apply click', () => { + const chip = fixture.debugElement.query(By.css('mat-chip')); + chip.triggerEventHandler('click', { stopPropagation: () => null }); + fixture.detectChanges(); + const applyButton = fixture.debugElement.query(By.css('#apply-filter-button')); + applyButton.triggerEventHandler('click', {}); + expect(queryBuilder.update).toHaveBeenCalled(); + }); + + it('should update search query on cancel click', () => { + const chip = fixture.debugElement.query(By.css('mat-chip')); + chip.triggerEventHandler('click', { stopPropagation: () => null }); + fixture.detectChanges(); + const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button')); + applyButton.triggerEventHandler('click', {}); + expect(queryBuilder.update).toHaveBeenCalled(); + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.ts new file mode 100644 index 00000000000..f3703052913 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.ts @@ -0,0 +1,68 @@ +/*! + * @license + * Copyright 2019 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, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core'; +import { SearchCategory } from '../../../models/search-category.interface'; +import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { SearchWidgetContainerComponent } from '../../search-widget-container/search-widget-container.component'; + +@Component({ + selector: 'adf-search-widget-chip', + templateUrl: './search-widget-chip.component.html', + encapsulation: ViewEncapsulation.None +}) +export class SearchWidgetChipComponent { + + @Input() + category: SearchCategory; + + @ViewChild('menuContainer', { static: false }) + menuContainer: ElementRef; + + @ViewChild('menuTrigger', { static: false }) + menuTrigger: MatMenuTrigger; + + @ViewChild(SearchWidgetContainerComponent, { static: false }) + widgetContainerComponent: SearchWidgetContainerComponent; + + focusTrap: ConfigurableFocusTrap; + + constructor(private focusTrapFactory: ConfigurableFocusTrapFactory) {} + + onMenuOpen() { + if (this.menuContainer && !this.focusTrap) { + this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement); + this.focusTrap.focusInitialElement(); + } + } + + onClosed() { + this.focusTrap.destroy(); + this.focusTrap = null; + } + + onRemove() { + this.widgetContainerComponent.resetInnerWidget(); + this.menuTrigger.closeMenu(); + } + + onApply() { + this.widgetContainerComponent.applyInnerWidget(); + this.menuTrigger.closeMenu(); + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-container/search-filter-container.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-container/search-filter-container.component.spec.ts index 01f28e695ef..73040737d79 100644 --- a/lib/content-services/src/lib/search/components/search-filter-container/search-filter-container.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-filter-container/search-filter-container.component.spec.ts @@ -18,7 +18,7 @@ import { Subject } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { SearchService, setupTestBed, AlfrescoApiService } from '@alfresco/adf-core'; -import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service'; +import { SearchHeaderQueryBuilderService } from '../../services/search-header-query-builder.service'; import { ContentTestingModule } from '../../../testing/content.testing.module'; import { fakeNodePaging } from './../../../mock/document-list.component.mock'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; diff --git a/lib/content-services/src/lib/search/components/search-filter-container/search-filter-container.component.ts b/lib/content-services/src/lib/search/components/search-filter-container/search-filter-container.component.ts index 3ed55315a31..1794ff179d2 100644 --- a/lib/content-services/src/lib/search/components/search-filter-container/search-filter-container.component.ts +++ b/lib/content-services/src/lib/search/components/search-filter-container/search-filter-container.component.ts @@ -30,7 +30,7 @@ import { import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cdk/a11y'; import { DataColumn, TranslationService } from '@alfresco/adf-core'; import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component'; -import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service'; +import { SearchHeaderQueryBuilderService } from '../../services/search-header-query-builder.service'; import { SearchCategory } from '../../models/search-category.interface'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; import { Subject } from 'rxjs'; diff --git a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.html b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.html index bef56b8dc26..30ffd7dc9f7 100644 --- a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.html +++ b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.html @@ -1,11 +1,11 @@ - - - + {{ field.label | translate }} -
- - - - -
+ -
- -
- {{ bucket.display || bucket.label | translate }} {{ getBucketCountDisplay(bucket) }} -
-
-
- -
- -
- -
- - - -
diff --git a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.scss b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.scss index 77e11e249b8..f08bd8e1e8f 100644 --- a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.scss +++ b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.scss @@ -2,57 +2,6 @@ $foreground: map-get($theme, foreground); .adf-search-filter { - - .adf-checklist { - display: flex; - flex-direction: column; - - .mat-checkbox-label { - text-overflow: ellipsis; - overflow: hidden; - width: 100%; - } - - .mat-checkbox-layout { - width: 100%; - } - - .adf-facet-label { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - .mat-checkbox { - margin: 5px; - - &.mat-checkbox-checked .mat-checkbox-label { - font-weight: bold; - } - } - } - - .adf-facet-result-filter { - display: flex; - flex-direction: column; - - & > * { - width: 100%; - } - } - - .adf-facet-buttons { - text-align: right; - - .mat-button { - text-transform: uppercase; - } - - &--topSpace { - padding-top: 15px; - } - } - .mat-expansion-panel-header-title { font-size: 14px; color: mat-color($foreground, text, 0.87); diff --git a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.spec.ts index b2c14f42c81..18a498d8734 100644 --- a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.spec.ts @@ -16,11 +16,9 @@ */ import { SearchFilterComponent } from './search-filter.component'; -import { SearchQueryBuilderService } from '../../search-query-builder.service'; -import { AppConfigService, SearchService, setupTestBed, TranslationService } from '@alfresco/adf-core'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; +import { AppConfigService, SearchService, TranslationService } from '@alfresco/adf-core'; import { Subject } from 'rxjs'; -import { FacetFieldBucket } from '../../models/facet-field-bucket.interface'; -import { FacetField } from '../../models/facet-field.interface'; import { SearchFilterList } from '../../models/search-filter-list.model'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -39,6 +37,8 @@ import { stepTwo } from '../../../mock'; import { TranslateModule } from '@ngx-translate/core'; +import { SearchFacetFiltersService } from '../../services/search-facet-filters.service'; +import { SearchFacetFieldComponent } from '../search-facet-field/search-facet-field.component'; describe('SearchFilterComponent', () => { let fixture: ComponentFixture; @@ -48,18 +48,19 @@ describe('SearchFilterComponent', () => { const searchMock: any = { dataLoaded: new Subject() }; - - setupTestBed({ - imports: [ - TranslateModule.forRoot(), - ContentTestingModule - ], - providers: [ - { provide: SearchService, useValue: searchMock } - ] - }); + let searchFacetFiltersService: SearchFacetFiltersService; beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ], + providers: [ + { provide: SearchService, useValue: searchMock } + ] + }); + searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService); queryBuilder = TestBed.inject(SearchQueryBuilderService); fixture = TestBed.createComponent(SearchFilterComponent); appConfigService = TestBed.inject(AppConfigService); @@ -73,346 +74,6 @@ describe('SearchFilterComponent', () => { describe('component', () => { beforeEach(() => fixture.detectChanges()); - it('should subscribe to query builder executed event', () => { - spyOn(component, 'onDataLoaded').and.stub(); - const data = { list: {} }; - queryBuilder.executed.next(data); - - expect(component.onDataLoaded).toHaveBeenCalledWith(data); - }); - - it('should update bucket model and query builder on facet toggle', () => { - spyOn(queryBuilder, 'update').and.stub(); - spyOn(queryBuilder, 'addUserFacetBucket').and.callThrough(); - - const event: any = { checked: true }; - const field: FacetField = { field: 'f1', label: 'f1' }; - const bucket: FacetFieldBucket = { checked: false, filterQuery: 'q1', label: 'q1', count: 1 }; - - component.onToggleBucket(event, field, bucket); - - expect(bucket.checked).toBeTruthy(); - expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(field, bucket); - expect(queryBuilder.update).toHaveBeenCalled(); - }); - - it('should update bucket model and query builder on facet un-toggle', () => { - spyOn(queryBuilder, 'update').and.stub(); - spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough(); - - const event: any = { checked: false }; - const field: FacetField = { field: 'f1', label: 'f1' }; - const bucket: FacetFieldBucket = { checked: true, filterQuery: 'q1', label: 'q1', count: 1 }; - - component.onToggleBucket(event, field, bucket); - - expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, bucket); - expect(queryBuilder.update).toHaveBeenCalled(); - }); - - it('should unselect facet query and update builder', () => { - spyOn(queryBuilder, 'update').and.stub(); - spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough(); - - const event: any = { checked: false }; - const query = { checked: true, label: 'q1', filterQuery: 'query1' }; - const field = { type: 'query', label: 'label1', buckets: [ query ] }; - - component.onToggleBucket(event, field, query); - - expect(query.checked).toEqual(false); - expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, query); - expect(queryBuilder.update).toHaveBeenCalled(); - }); - - it('should fetch facet queries from response payload', () => { - component.responseFacets = null; - - queryBuilder.config = { - categories: [], - facetQueries: { - label: 'label1', - queries: [ - { label: 'q1', query: 'query1' }, - { label: 'q2', query: 'query2' } - ] - } - }; - - const queries = [ - { label: 'q1', filterQuery: 'query1', metrics: [{value: {count: 1}}] }, - { label: 'q2', filterQuery: 'query2', metrics: [{value: {count: 1}}] } - ]; - const data = { - list: { - context: { - facets: [{ - type: 'query', - label: 'label1', - buckets: queries - }] - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacets.length).toBe(1); - expect(component.responseFacets[0].buckets.length).toEqual(2); - }); - - it('should preserve order after response processing', () => { - component.responseFacets = null; - - queryBuilder.config = { - categories: [], - facetQueries: { - label: 'label1', - queries: [ - { label: 'q1', query: 'query1' }, - { label: 'q2', query: 'query2' }, - { label: 'q3', query: 'query3' } - ] - } - }; - - const queries = [ - { label: 'q2', filterQuery: 'query2', metrics: [{value: {count: 1}}] }, - { label: 'q1', filterQuery: 'query1', metrics: [{value: {count: 1}}] }, - { label: 'q3', filterQuery: 'query3', metrics: [{value: {count: 1}}] } - - ]; - const data = { - list: { - context: { - facets: [{ - type: 'query', - label: 'label1', - buckets: queries - }] - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacets.length).toBe(1); - expect(component.responseFacets[0].buckets.length).toBe(3); - expect(component.responseFacets[0].buckets.items[0].label).toBe('q1'); - expect(component.responseFacets[0].buckets.items[1].label).toBe('q2'); - expect(component.responseFacets[0].buckets.items[2].label).toBe('q3'); - }); - - it('should not fetch facet queries from response payload', () => { - component.responseFacets = null; - - queryBuilder.config = { - categories: [], - facetQueries: { - queries: [] - } - }; - - const data = { - list: { - context: { - facets: null - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacets).toBeNull(); - }); - - it('should fetch facet fields from response payload', () => { - component.responseFacets = null; - - queryBuilder.config = { - categories: [], - facetFields: { fields: [ - { label: 'f1', field: 'f1', mincount: 0 }, - { label: 'f2', field: 'f2', mincount: 0 } - ]}, - facetQueries: { - queries: [] - } - }; - - const fields: any = [ - { type: 'field', label: 'f1', buckets: [{ label: 'a1' }, { label: 'a2' }] }, - { type: 'field', label: 'f2', buckets: [{ label: 'b1' }, { label: 'b2' }] } - ]; - const data = { - list: { - context: { - facets: fields - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacets.length).toEqual(2); - expect(component.responseFacets[0].buckets.length).toEqual(2); - expect(component.responseFacets[1].buckets.length).toEqual(2); - }); - - it('should filter response facet fields based on search filter config method', () => { - queryBuilder.config = { - categories: [], - facetFields: { fields: [ - { label: 'f1', field: 'f1' } - ]}, - facetQueries: { - queries: [] - }, - filterWithContains: false - }; - - const initialFields: any = [ - { type: 'field', label: 'f1', buckets: [ - { label: 'firstLabel', display: 'firstLabel', metrics: [{value: {count: 5}}] }, - { label: 'secondLabel', display: 'secondLabel', metrics: [{value: {count: 5}}] }, - { label: 'thirdLabel', display: 'thirdLabel', metrics: [{value: {count: 5}}] } - ] - } - ]; - - const data = { - list: { - context: { - facets: initialFields - } - } - }; - - component.onDataLoaded(data); - expect(component.responseFacets.length).toBe(1); - expect(component.responseFacets[0].buckets.visibleItems.length).toBe(3); - - component.responseFacets[0].buckets.filterText = 'f'; - expect(component.responseFacets[0].buckets.visibleItems.length).toBe(1); - expect(component.responseFacets[0].buckets.visibleItems[0].label).toEqual('firstLabel'); - - component.responseFacets[0].buckets.filterText = 'label'; - expect(component.responseFacets[0].buckets.visibleItems.length).toBe(0); - - // Set filter method to use contains and test again - queryBuilder.config.filterWithContains = true; - component.responseFacets[0].buckets.filterText = 'f'; - expect(component.responseFacets[0].buckets.visibleItems.length).toBe(1); - component.responseFacets[0].buckets.filterText = 'label'; - expect(component.responseFacets[0].buckets.visibleItems.length).toBe(3); - }); - - it('should fetch facet fields from response payload and show the bucket values', () => { - component.responseFacets = null; - - queryBuilder.config = { - categories: [], - facetFields: { fields: [ - { label: 'f1', field: 'f1' }, - { label: 'f2', field: 'f2' } - ]}, - facetQueries: { - queries: [] - } - }; - - const serverResponseFields: any = [ - { - type: 'field', - label: 'f1', - buckets: [ - { label: 'b1', metrics: [{value: {count: 10}}] }, - { label: 'b2', metrics: [{value: {count: 1}}] } - ] - }, - { type: 'field', label: 'f2', buckets: [] } - ]; - const data = { - list: { - context: { - facets: serverResponseFields - } - } - }; - - component.onDataLoaded(data); - expect(component.responseFacets.length).toEqual(1); - expect(component.responseFacets[0].buckets.items[0].count).toEqual(10); - expect(component.responseFacets[0].buckets.items[1].count).toEqual(1); - }); - - it('should fetch facet fields from response payload and update the existing bucket values', () => { - queryBuilder.config = { - categories: [], - facetFields: { fields: [ - { label: 'f1', field: 'f1' }, - { label: 'f2', field: 'f2' } - ]}, - facetQueries: { - queries: [] - } - }; - - const initialFields: any = [ - { type: 'field', label: 'f1', buckets: { items: [{ label: 'b1', count: 10, filterQuery: 'filter' }, { label: 'b2', count: 1 }]} }, - { type: 'field', label: 'f2', buckets: [] } - ]; - component.responseFacets = initialFields; - expect(component.responseFacets[0].buckets.items[0].count).toEqual(10); - expect(component.responseFacets[0].buckets.items[1].count).toEqual(1); - - const serverResponseFields: any = [ - { type: 'field', label: 'f1', buckets: - [{ label: 'b1', metrics: [{value: {count: 6}}], filterQuery: 'filter' }, - { label: 'b2', metrics: [{value: {count: 0}}] }] }, - { type: 'field', label: 'f2', buckets: [] } - ]; - const data = { - list: { - context: { - facets: serverResponseFields - } - } - }; - - component.onDataLoaded(data); - expect(component.responseFacets[0].buckets.items[0].count).toEqual(6); - expect(component.responseFacets[0].buckets.items[1].count).toEqual(0); - }); - - it('should update correctly the existing facetFields bucket values', () => { - component.responseFacets = null; - - queryBuilder.config = { - categories: [], - facetFields: { fields: [{ label: 'f1', field: 'f1' }] }, - facetQueries: { queries: [] } - }; - - const firstCallFields: any = [{ - type: 'field', - label: 'f1', - buckets: [{ label: 'b1', metrics: [{value: {count: 10}}] }] - }]; - const firstCallData = { list: { context: { facets: firstCallFields }}}; - component.onDataLoaded(firstCallData); - expect(component.responseFacets[0].buckets.items[0].count).toEqual(10); - - const secondCallFields: any = [{ - type: 'field', - label: 'f1', - buckets: [{ label: 'b1', metrics: [{value: {count: 6}}] }] - }]; - const secondCallData = { list: { context: { facets: secondCallFields}}}; - component.onDataLoaded(secondCallData); - expect(component.responseFacets[0].buckets.items[0].count).toEqual(6); - }); - it('should fetch facet fields from response payload and show the already checked items', () => { spyOn(queryBuilder, 'execute').and.stub(); queryBuilder.config = { @@ -426,13 +87,13 @@ describe('SearchFilterComponent', () => { } }; - component.responseFacets = [ - { type: 'field', label: 'f1', field: 'f1', buckets: {items: [ + searchFacetFiltersService.responseFacets = [ + { type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([ { label: 'b1', count: 10, filterQuery: 'filter', checked: true }, - { label: 'b2', count: 1, filterQuery: 'filter2' }] }}, - { type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }} + { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, + { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList([]) } ]; - component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]); + queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); const serverResponseFields: any = [ { type: 'field', label: 'f1', field: 'f1', buckets: [ @@ -447,10 +108,14 @@ describe('SearchFilterComponent', () => { } } }; - component.selectFacetBucket({ field: 'f1', label: 'f1' }, component.responseFacets[0].buckets.items[1]); - component.onDataLoaded(data); - expect(component.responseFacets.length).toEqual(2); - expect(component.responseFacets[0].buckets.items[0].checked).toEqual(true, 'should show the already checked item'); + + fixture.detectChanges(); + const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance; + facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]); + + searchFacetFiltersService.onDataLoaded(data); + expect(searchFacetFiltersService.responseFacets.length).toEqual(2); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].checked).toEqual(true, 'should show the already checked item'); }); it('should fetch facet fields from response payload and show the newly checked items', () => { @@ -466,13 +131,13 @@ describe('SearchFilterComponent', () => { } }; - component.responseFacets = [ - { type: 'field', label: 'f1', field: 'f1', buckets: {items: [ + searchFacetFiltersService.responseFacets = [ + { type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([ { label: 'b1', count: 10, filterQuery: 'filter', checked: true }, - { label: 'b2', count: 1, filterQuery: 'filter2' }] }}, - { type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }} + { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, + { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList([]) } ]; - component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]); + searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); const serverResponseFields: any = [ { type: 'field', label: 'f1', field: 'f1', buckets: [ @@ -487,10 +152,14 @@ describe('SearchFilterComponent', () => { } } }; - component.selectFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[1]); - component.onDataLoaded(data); - expect(component.responseFacets.length).toEqual(2); - expect(component.responseFacets[0].buckets.items[1].checked).toEqual(true, 'should show the newly checked item'); + + fixture.detectChanges(); + const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance; + facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]); + + searchFacetFiltersService.onDataLoaded(data); + expect(searchFacetFiltersService.responseFacets.length).toEqual(2); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].checked).toEqual(true, 'should show the newly checked item'); }); it('should show buckets with 0 values when there are no facet fields on the response payload', () => { @@ -506,99 +175,26 @@ describe('SearchFilterComponent', () => { } }; - component.responseFacets = [ - { type: 'field', label: 'f1', field: 'f1', buckets: {items: [ + searchFacetFiltersService.responseFacets = [ + { type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([ { label: 'b1', count: 10, filterQuery: 'filter', checked: true }, - { label: 'b2', count: 1, filterQuery: 'filter2' }] }}, - { type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }} + { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, + { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() } ]; - component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]); + searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); const data = { list: { context: {} } }; - component.selectFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[1]); - component.onDataLoaded(data); - - expect(component.responseFacets[0].buckets.items[0].count).toEqual(0); - expect(component.responseFacets[0].buckets.items[1].count).toEqual(0); - }); - - it('should update query builder only when has bucket to unselect', () => { - spyOn(queryBuilder, 'update').and.stub(); - - const field: FacetField = { field: 'f1', label: 'f1' }; - component.onToggleBucket( { checked: true }, field, null); - - expect(queryBuilder.update).not.toHaveBeenCalled(); - }); - - it('should allow to to reset selected buckets', () => { - const buckets: FacetFieldBucket[] = [ - { label: 'bucket1', checked: true, count: 1, filterQuery: 'q1' }, - { label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' } - ]; - - const field: FacetField = { - field: 'f1', - label: 'field1', - buckets: new SearchFilterList(buckets) - }; - expect(component.canResetSelectedBuckets(field)).toBeTruthy(); - }); - - it('should not allow to reset selected buckets', () => { - const buckets: FacetFieldBucket[] = [ - { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, - { label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' } - ]; - - const field: FacetField = { - field: 'f1', - label: 'field1', - buckets: new SearchFilterList(buckets) - }; - - expect(component.canResetSelectedBuckets(field)).toEqual(false); - }); - - it('should reset selected buckets', () => { - spyOn(queryBuilder, 'execute').and.stub(); - const buckets: FacetFieldBucket[] = [ - { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, - { label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' } - ]; - - const field: FacetField = { - field: 'f1', - label: 'field1', - buckets: new SearchFilterList(buckets) - }; - - component.resetSelectedBuckets(field); - - expect(buckets[0].checked).toEqual(false); - expect(buckets[1].checked).toEqual(false); - }); - - it('should update query builder upon resetting buckets', () => { - spyOn(queryBuilder, 'update').and.stub(); - - const buckets: FacetFieldBucket[] = [ - { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, - { label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' } - ]; - - const field: FacetField = { - field: 'f1', - label: 'field1', - buckets: new SearchFilterList(buckets) - }; + fixture.detectChanges(); + const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance; + facetField.selectFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]); + searchFacetFiltersService.onDataLoaded(data); - component.resetSelectedBuckets(field); - expect(queryBuilder.update).toHaveBeenCalled(); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(0); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(0); }); it('should update query builder upon resetting selected queries', () => { @@ -607,110 +203,25 @@ describe('SearchFilterComponent', () => { const queryResponse = { label: 'query response', - buckets: { - items: [ + buckets: new SearchFilterList([ { label: 'q1', query: 'q1', checked: true, metrics: [{value: {count: 1}}] }, { label: 'q2', query: 'q2', checked: false, metrics: [{value: {count: 1}}] }, - { label: 'q3', query: 'q3', checked: true, metrics: [{value: {count: 1}}] }] - }}; - component.responseFacets = [queryResponse]; - component.resetSelectedBuckets(queryResponse); + { label: 'q3', query: 'q3', checked: true, metrics: [{value: {count: 1}}] }]) + }; + searchFacetFiltersService.responseFacets = [queryResponse]; + + fixture.detectChanges(); + const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance; + facetField.resetSelectedBuckets(queryResponse); expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledTimes(3); expect(queryBuilder.update).toHaveBeenCalled(); - for (const entry of component.responseFacets[0].buckets.items) { + for (const entry of searchFacetFiltersService.responseFacets[0].buckets.items) { expect(entry.checked).toEqual(false); } }); - - it('should fetch facet intervals from response payload', () => { - component.responseFacets = null; - queryBuilder.config = { - categories: [], - facetIntervals: { - intervals: [ - { label: 'test_intervals1', field: 'f1', sets: [ - { label: 'interval1', start: 's1', end: 'e1'}, - { label: 'interval2', start: 's2', end: 'e2'} - ]}, - { label: 'test_intervals2', field: 'f2', sets: [ - { label: 'interval3', start: 's3', end: 'e3'}, - { label: 'interval4', start: 's4', end: 'e4'} - ]} - ] - } - }; - - const response1 = [ - { label: 'interval1', filterQuery: 'query1', metrics: [{ value: { count: 1 }}]}, - { label: 'interval2', filterQuery: 'query2', metrics: [{ value: { count: 2 }}]} - ]; - const response2 = [ - { label: 'interval3', filterQuery: 'query3', metrics: [{ value: { count: 3 }}]}, - { label: 'interval4', filterQuery: 'query4', metrics: [{ value: { count: 4 }}]} - ]; - const data = { - list: { - context: { - facets: [ - { type: 'interval', label: 'test_intervals1', buckets: response1 }, - { type: 'interval', label: 'test_intervals2', buckets: response2 } - ] - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacets.length).toBe(2); - expect(component.responseFacets[0].buckets.length).toEqual(2); - expect(component.responseFacets[1].buckets.length).toEqual(2); - }); - - it('should filter out the fetched facet intervals that have bucket values less than their set mincount', () => { - component.responseFacets = null; - queryBuilder.config = { - categories: [], - facetIntervals: { - intervals: [ - { label: 'test_intervals1', field: 'f1', mincount: 2, sets: [ - { label: 'interval1', start: 's1', end: 'e1'}, - { label: 'interval2', start: 's2', end: 'e2'} - ]}, - { label: 'test_intervals2', field: 'f2', mincount: 5, sets: [ - { label: 'interval3', start: 's3', end: 'e3'}, - { label: 'interval4', start: 's4', end: 'e4'} - ]} - ] - } - }; - - const response1 = [ - { label: 'interval1', filterQuery: 'query1', metrics: [{ value: { count: 1 }}]}, - { label: 'interval2', filterQuery: 'query2', metrics: [{ value: { count: 2 }}]} - ]; - const response2 = [ - { label: 'interval3', filterQuery: 'query3', metrics: [{ value: { count: 3 }}]}, - { label: 'interval4', filterQuery: 'query4', metrics: [{ value: { count: 4 }}]} - ]; - const data = { - list: { - context: { - facets: [ - { type: 'interval', label: 'test_intervals1', buckets: response1 }, - { type: 'interval', label: 'test_intervals2', buckets: response2 } - ] - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacets.length).toBe(1); - expect(component.responseFacets[0].buckets.length).toEqual(1); - }); - }); + }); describe('widgets', () => { @@ -919,8 +430,6 @@ describe('SearchFilterComponent', () => { fixture.detectChanges(); spyOn(queryBuilder, 'update').and.stub(); - spyOn(component, 'selectFacetBucket').and.callThrough(); - spyOn(component, 'onToggleBucket').and.callThrough(); const inputElement = fixture.debugElement.query(By.css(`${panel} input`)); inputElement.nativeElement.value = 'Extra'; @@ -937,7 +446,7 @@ describe('SearchFilterComponent', () => { filteredMenu = getAllMenus(`${panel} mat-checkbox`, fixture); expect(filteredMenu).toEqual(filteredResult); - const clearButton = fixture.debugElement.query(By.css(`${panel} button`)); + const clearButton = fixture.debugElement.query(By.css(`${panel} mat-form-field button`)); clearButton.triggerEventHandler('click', {}); fixture.detectChanges(); @@ -951,8 +460,7 @@ describe('SearchFilterComponent', () => { const checkedOption = fixture.debugElement.query(By.css(`${panel} mat-checkbox.mat-checkbox-checked`)); expect(checkedOption.nativeElement.innerText).toEqual('Extra Small (10239)'); - expect(component.onToggleBucket).toHaveBeenCalledTimes(1); - expect(component.selectFacetBucket).toHaveBeenCalledTimes(1); + expect(queryBuilder.update).toHaveBeenCalledTimes(1); }); it('should preserve the filter state if other fields edited', () => { @@ -964,8 +472,6 @@ describe('SearchFilterComponent', () => { queryBuilder.executed.next( mockSearchResult); fixture.detectChanges(); spyOn(queryBuilder, 'update').and.stub(); - spyOn(component, 'selectFacetBucket').and.callThrough(); - spyOn(component, 'onToggleBucket').and.callThrough(); const inputElement = fixture.debugElement.query(By.css(`${panel1} input`)); inputElement.nativeElement.value = 'my'; @@ -995,15 +501,20 @@ describe('SearchFilterComponent', () => { panel1CheckedOption = fixture.debugElement.query(By.css(`${panel1} mat-checkbox.mat-checkbox-checked`)); expect(panel1CheckedOption.nativeElement.innerText).toEqual('my1 (806)'); - expect(component.onToggleBucket).toHaveBeenCalledTimes(2); - expect(component.selectFacetBucket).toHaveBeenCalledTimes(2); + expect(queryBuilder.update).toHaveBeenCalledTimes(2); }); it('should reset the query fragments when reset All is clicked', () => { component.queryBuilder.queryFragments = { 'fragment1' : 'value1'}; - component.responseFacets = []; - spyOn(queryBuilder, 'resetToDefaults').and.stub(); - component.resetAll(); + appConfigService.config.search = searchFilter; + searchFacetFiltersService.responseFacets = []; + component.displayResetButton = true; + fixture.detectChanges(); + spyOn(queryBuilder, 'resetToDefaults').and.callThrough(); + + const resetButton = fixture.debugElement.query(By.css('button')); + resetButton.nativeElement.click(); + expect(component.queryBuilder.queryFragments).toEqual({}); expect(queryBuilder.resetToDefaults).toHaveBeenCalled(); }); diff --git a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.ts b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.ts index 83e5ae7af0c..840f22d83ed 100644 --- a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.ts +++ b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.ts @@ -15,22 +15,12 @@ * limitations under the License. */ -import { Component, ViewEncapsulation, OnInit, OnDestroy, Inject, Input } from '@angular/core'; -import { MatCheckboxChange } from '@angular/material/checkbox'; -import { TranslationService, SearchService } from '@alfresco/adf-core'; -import { SearchQueryBuilderService } from '../../search-query-builder.service'; +import { Component, Inject, Input, ViewEncapsulation } from '@angular/core'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; import { FacetFieldBucket } from '../../models/facet-field-bucket.interface'; import { FacetField } from '../../models/facet-field.interface'; -import { SearchFilterList } from '../../models/search-filter-list.model'; -import { takeUntil } from 'rxjs/operators'; -import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api'; -import { Subject } from 'rxjs'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; - -export interface SelectedBucket { - field: FacetField; - bucket: FacetFieldBucket; -} +import { SearchFacetFiltersService } from '../../services/search-facet-filters.service'; @Component({ selector: 'adf-search-filter', @@ -38,36 +28,22 @@ export interface SelectedBucket { encapsulation: ViewEncapsulation.None, host: { class: 'adf-search-filter' } }) -export class SearchFilterComponent implements OnInit, OnDestroy { +export class SearchFilterComponent { /** Toggles whether to show or not the context facet filters. */ @Input() showContextFacets: boolean = true; - private DEFAULT_PAGE_SIZE = 5; - - /** 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; facetQueriesLabel: string = 'Facet Queries'; facetExpanded = { 'default': false }; displayResetButton: boolean; - selectedBuckets: SelectedBucket[] = []; - - private onDestroy$ = new Subject(); constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService, - private searchService: SearchService, - private translationService: TranslationService) { + public facetFiltersService: SearchFacetFiltersService) { if (queryBuilder.config && queryBuilder.config.facetQueries) { this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries'; - this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE; this.facetExpanded['query'] = queryBuilder.config.facetQueries.expanded; } if (queryBuilder.config && queryBuilder.config.facetFields) { @@ -77,350 +53,13 @@ export class SearchFilterComponent implements OnInit, OnDestroy { this.facetExpanded['interval'] = queryBuilder.config.facetIntervals.expanded; } this.displayResetButton = this.queryBuilder.config && !!this.queryBuilder.config.resetButton; - - this.queryBuilder.updated - .pipe(takeUntil(this.onDestroy$)) - .subscribe((query) => this.queryBuilder.execute(query)); - } - - ngOnInit() { - if (this.queryBuilder) { - this.queryBuilder.executed - .pipe(takeUntil(this.onDestroy$)) - .subscribe((resultSetPaging: ResultSetPaging) => { - this.onDataLoaded(resultSetPaging); - this.searchService.dataLoaded.next(resultSetPaging); - }); - } - } - - ngOnDestroy() { - this.onDestroy$.next(true); - this.onDestroy$.complete(); - } - - private updateSelectedBuckets() { - if (this.responseFacets) { - this.selectedBuckets = []; - for (const field of this.responseFacets) { - if (field.buckets) { - this.selectedBuckets.push( - ...this.queryBuilder.getUserFacetBuckets(field.field) - .filter((bucket) => bucket.checked) - .map((bucket) => { - return { field, bucket }; - }) - ); - } - } - } else { - this.selectedBuckets = []; - } - } - - onToggleBucket(event: MatCheckboxChange, field: FacetField, bucket: FacetFieldBucket) { - if (event && bucket) { - if (event.checked) { - this.selectFacetBucket(field, bucket); - } else { - this.unselectFacetBucket(field, bucket); - } - } - } - - selectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { - if (bucket) { - bucket.checked = true; - this.queryBuilder.addUserFacetBucket(field, bucket); - this.updateSelectedBuckets(); - this.queryBuilder.update(); - } - } - - unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { - if (bucket) { - bucket.checked = false; - this.queryBuilder.removeUserFacetBucket(field, bucket); - this.updateSelectedBuckets(); - this.queryBuilder.update(); - } - } - - canResetSelectedBuckets(field: FacetField): boolean { - if (field && field.buckets) { - return field.buckets.items.some((bucket) => bucket.checked); - } - return false; - } - - resetSelectedBuckets(field: FacetField) { - if (field && field.buckets) { - for (const bucket of field.buckets.items) { - bucket.checked = false; - this.queryBuilder.removeUserFacetBucket(field, bucket); - } - this.updateSelectedBuckets(); - this.queryBuilder.update(); - } - } - - resetAllSelectedBuckets() { - this.responseFacets.forEach((field) => { - if (field && field.buckets) { - for (const bucket of field.buckets.items) { - bucket.checked = false; - this.queryBuilder.removeUserFacetBucket(field, bucket); - } - this.updateSelectedBuckets(); - } - }); - this.queryBuilder.update(); - } - - resetQueryFragments() { - this.queryBuilder.queryFragments = {}; - this.queryBuilder.resetToDefaults(); - } - - resetAll() { - this.resetAllSelectedBuckets(); - this.resetQueryFragments(); - this.responseFacets = null; } shouldExpand(field: FacetField): boolean { return this.facetExpanded[field.type] || this.facetExpanded['default']; } - onDataLoaded(data: any) { - const context = data.list.context; - - if (context) { - this.parseFacets(context); - } else { - this.responseFacets = null; - } - } - - private parseFacets(context: ResultSetContext) { - this.parseFacetFields(context); - this.parseFacetIntervals(context); - this.parseFacetQueries(context); - } - - 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); - - if (alreadyExistingField) { - const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || []; - - this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets); - } else if (responseField && this.showContextFacets) { - if (responseBuckets.length > 0) { - const bucketList = new SearchFilterList(responseBuckets, field.pageSize); - bucketList.filter = this.getBucketFilterFunction(bucketList); - - if (!this.responseFacets) { - this.responseFacets = []; - } - 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) { - const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || []; - this.parseFacetItems(context, configFacetFields, 'field'); - } - - private parseFacetIntervals(context: ResultSetContext) { - const configFacetIntervals = this.queryBuilder.config.facetIntervals && this.queryBuilder.config.facetIntervals.intervals || []; - this.parseFacetItems(context, configFacetIntervals, 'interval'); - } - - 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); - if (acc[group]) { - acc[group].push(query); - } else { - acc[group] = [query]; - } - return acc; - }, []); - - const mincount = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.mincount; - const mincountFilter = this.getFilterByMinCount(mincount); - - Object.keys(configGroups).forEach((group) => { - const responseField = this.findFacet(context, 'query', group); - const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group]) - .filter(mincountFilter); - const alreadyExistingField = this.findResponseFacet('query', group); - - if (alreadyExistingField) { - const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || []; - - this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets); - } else if (responseField && this.showContextFacets) { - if (responseBuckets.length > 0) { - const bucketList = new SearchFilterList(responseBuckets, this.facetQueriesPageSize); - bucketList.filter = this.getBucketFilterFunction(bucketList); - - if (!this.responseFacets) { - this.responseFacets = []; - } - this.responseFacets.push( { - field: group, - type: responseField.type || 'query', - label: group, - pageSize: this.DEFAULT_PAGE_SIZE, - currentPageSize: this.DEFAULT_PAGE_SIZE, - buckets: bucketList - }); - } - } - }); - - } - - private getResponseBuckets(responseField: GenericFacetResponse, configField: FacetField): FacetFieldBucket[] { - return ((responseField && responseField.buckets) || []).map((respBucket) => { - - respBucket['count'] = this.getCountValue(respBucket); - respBucket.filterQuery = respBucket.filterQuery || this.getCorrespondingFilterQuery(configField, respBucket.label); - return { - ...respBucket, - checked: false, - display: respBucket.display, - label: respBucket.label - }; - }); - } - - private getResponseQueryBuckets(responseField: GenericFacetResponse, configGroup: any): FacetFieldBucket[] { - return (configGroup || []).map((query) => { - const respBucket = ((responseField && responseField.buckets) || []) - .find((bucket) => bucket.label === query.label) || {}; - - respBucket['count'] = this.getCountValue(respBucket); - return { - ...respBucket, - checked: false, - display: respBucket.display, - label: respBucket.label - }; - }); - } - - private getCountValue(bucket: GenericBucket): number { - return (!!bucket && !!bucket.metrics && bucket.metrics[0]?.value?.count) || 0; - } - getBucketCountDisplay(bucket: FacetFieldBucket): string { return bucket.count === null ? '' : `(${bucket.count})`; } - - private getFilterByMinCount(mincountInput: number) { - return (bucket) => { - let mincount = mincountInput; - if (mincount === undefined) { - mincount = 1; - } - return bucket.count >= mincount; - }; - } - - private getCorrespondingFilterQuery(configFacetItem: FacetField, bucketLabel: string): string { - let filterQuery = null; - - if (configFacetItem.field && bucketLabel) { - - if (configFacetItem.sets) { - const configSet = configFacetItem.sets.find((set) => bucketLabel === set.label); - - if (configSet) { - filterQuery = this.buildIntervalQuery(configFacetItem.field, configSet); - } - - } else { - filterQuery = `${configFacetItem.field}:"${bucketLabel}"`; - } - } - - return filterQuery; - } - - private buildIntervalQuery(fieldName: string, interval: any): string { - const start = interval.start; - const end = interval.end; - const startLimit = (interval.startInclusive === undefined || interval.startInclusive === true) ? '[' : '<'; - const endLimit = (interval.endInclusive === undefined || interval.endInclusive === true) ? ']' : '>'; - - 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/src/lib/search/components/search-form/search-form.component.html b/lib/content-services/src/lib/search/components/search-form/search-form.component.html index ebfc225a4c4..59027b9c708 100644 --- a/lib/content-services/src/lib/search/components/search-form/search-form.component.html +++ b/lib/content-services/src/lib/search/components/search-form/search-form.component.html @@ -1,8 +1,33 @@ - - {{ 'SEARCH.FORMS' | translate }} - - - {{form.name | translate}} - - - + + + + + + + + + + + + + + diff --git a/lib/content-services/src/lib/search/components/search-form/search-form.component.scss b/lib/content-services/src/lib/search/components/search-form/search-form.component.scss new file mode 100644 index 00000000000..6751ce1f57b --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-form/search-form.component.scss @@ -0,0 +1,49 @@ +@mixin adf-search-forms-theme($theme) { + $accent: map-get($theme, accent); + + .adf-search-form { + &.mat-button { + height: 35px; + max-width: 190px; + min-width: 190px; + align-content: center; + overflow: hidden; + + .mat-button-wrapper { + display: flex; + align-items: center; + } + } + + &-title { + max-width: 120px; + min-width: 120px; + font-weight: bold; + font-size: 14px; + line-height: 24px; + padding-right: 12px; + text-overflow: ellipsis; + overflow: hidden; + text-align: left; + } + + &-icon { + border: 2px solid transparent; + border-radius: 6px; + transition: border 500ms ease-out; + } + + &-icon-selected { + border-color: mat-color($accent); + } + + &-menu + * .mat-menu-panel { + @include mat-elevation(2); + border-radius: 6px; + + .mat-menu-content { + padding: 0; + } + } + } +} diff --git a/lib/content-services/src/lib/search/components/search-form/search-form.component.spec.ts b/lib/content-services/src/lib/search/components/search-form/search-form.component.spec.ts index e97486662d6..95cb5dec999 100644 --- a/lib/content-services/src/lib/search/components/search-form/search-form.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-form/search-form.component.spec.ts @@ -16,20 +16,19 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { SearchFormComponent } from './search-form.component'; import { setupTestBed } from '@alfresco/adf-core'; import { TranslateModule } from '@ngx-translate/core'; import { ContentTestingModule } from '../../../testing/content.testing.module'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; -import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; import { SearchForm } from '../../models/search-form.interface'; import { By } from '@angular/platform-browser'; describe('SearchFormComponent', () => { let fixture: ComponentFixture; let component: SearchFormComponent; - let queryBuilder: SearchHeaderQueryBuilderService; + let queryBuilder: SearchQueryBuilderService; const mockSearchForms: SearchForm[] = [ { default: false, index: 0, name: 'All', selected: false }, { default: true, index: 1, name: 'First', selected: true }, @@ -42,50 +41,59 @@ describe('SearchFormComponent', () => { ContentTestingModule ], providers: [ - { provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchHeaderQueryBuilderService } + { provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchQueryBuilderService } ] }); beforeEach(() => { fixture = TestBed.createComponent(SearchFormComponent); component = fixture.componentInstance; - queryBuilder = TestBed.inject(SEARCH_QUERY_SERVICE_TOKEN); - spyOn(queryBuilder, 'getSearchConfigurationDetails').and.returnValue(mockSearchForms); + queryBuilder = TestBed.inject(SEARCH_QUERY_SERVICE_TOKEN); + queryBuilder.searchForms.next(mockSearchForms); fixture.detectChanges(); }); - it('should show search forms', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - expect(component.selected).toBe(1); - const label = fixture.debugElement.query(By.css('.mat-form-field mat-label')); - expect(label.nativeElement.innerText).toContain('SEARCH.FORMS'); - const selectValue = fixture.debugElement.query(By.css('.mat-select-value')); - expect(selectValue.nativeElement.innerText).toContain('First'); + it('should show search forms', () => { + const title = fixture.debugElement.query(By.css('.adf-search-form-title')); + expect(title.nativeElement.innerText).toContain(mockSearchForms[1].name); }); - it('should emit on form change', async (done) => { + it('should emit on form change', (done) => { + spyOn(queryBuilder, 'updateSelectedConfiguration').and.stub(); component.formChange.subscribe((form) => { expect(form).toEqual(mockSearchForms[2]); + expect(queryBuilder.updateSelectedConfiguration).toHaveBeenCalled(); done(); }); - await fixture.whenStable(); + const button = fixture.debugElement.query(By.css('.adf-search-form')).nativeElement; + button.click(); fixture.detectChanges(); - const matSelect = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement; - matSelect.click(); + const matOption = fixture.debugElement.queryAll(By.css('.mat-menu-item'))[2].nativeElement; + matOption.click(); + }); + + it('should not show menu if only one config found', () => { + queryBuilder.searchForms.next([{ name: 'one', selected: true, default: true, index: 0 }]); fixture.detectChanges(); - const matOption = fixture.debugElement.queryAll(By.css('.mat-option'))[2].nativeElement; - matOption.click(); + const button = fixture.debugElement.query(By.css('.adf-search-form')).nativeElement; + button.click(); + + const title = fixture.debugElement.query(By.css('.adf-search-form-title')); + expect(title.nativeElement.innerText).toContain('one'); + + fixture.detectChanges(); + const matOption = fixture.debugElement.query(By.css('.mat-menu-item')); + expect(matOption).toBe(null, 'should not show mat menu'); }); - it('should not display search form if no form configured', async () => { - component.searchForms = []; - await fixture.whenStable(); + it('should not display search form if no form configured', () => { + queryBuilder.searchForms.next([]); fixture.detectChanges(); - const field = fixture.debugElement.query(By.css('.mat-form-field')); + + const field = fixture.debugElement.query(By.css('.adf-search-form-title')); expect(field).toEqual(null, 'search form displayed for empty configuration'); }); }); diff --git a/lib/content-services/src/lib/search/components/search-form/search-form.component.ts b/lib/content-services/src/lib/search/components/search-form/search-form.component.ts index 181fb56d712..61c21beeee7 100644 --- a/lib/content-services/src/lib/search/components/search-form/search-form.component.ts +++ b/lib/content-services/src/lib/search/components/search-form/search-form.component.ts @@ -15,30 +15,30 @@ * limitations under the License. */ -import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core'; -import { SearchQueryBuilderService } from '../../search-query-builder.service'; +import { Component, EventEmitter, Inject, Output, ViewEncapsulation } from '@angular/core'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; import { SearchForm } from '../../models/search-form.interface'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; @Component({ selector: 'adf-search-form', - templateUrl: './search-form.component.html' + templateUrl: './search-form.component.html', + styleUrls: ['./search-form.component.scss'], + encapsulation: ViewEncapsulation.None }) -export class SearchFormComponent implements OnInit { +export class SearchFormComponent { @Output() formChange: EventEmitter = new EventEmitter(); - selected: number; - searchForms: SearchForm[] = []; - - constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private queryBuilder: SearchQueryBuilderService) {} + constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService) { + } - ngOnInit(): void { - this.searchForms = this.queryBuilder.getSearchConfigurationDetails(); - this.selected = this.searchForms.find(form => form.selected)?.index; + onSelectionChange(form: SearchForm) { + this.queryBuilder.updateSelectedConfiguration(form.index); + this.formChange.emit(form); } - onSelectionChange(index: number) { - this.formChange.emit(this.searchForms[index]); + getSelected(forms: SearchForm[]): string { + return forms.find((form) => form.selected)?.name; } } diff --git a/lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.html b/lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.html index ad3b34f2b26..c3f499a37ce 100644 --- a/lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.html +++ b/lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.html @@ -29,8 +29,8 @@ -
-
diff --git a/lib/content-services/src/lib/search/components/search-slider/search-slider.component.spec.ts b/lib/content-services/src/lib/search/components/search-slider/search-slider.component.spec.ts index 233926e9136..39685920eb0 100644 --- a/lib/content-services/src/lib/search/components/search-slider/search-slider.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-slider/search-slider.component.spec.ts @@ -74,14 +74,13 @@ describe('SearchSliderComponent', () => { component.context = context; component.id = 'contentSize'; component.settings = { field: 'cm:content.size' }; + fixture.detectChanges(); component.onChangedHandler( { value: 10 }); - fixture.detectChanges(); expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 10]'); expect(context.update).toHaveBeenCalled(); component.onChangedHandler( { value: 20 }); - fixture.detectChanges(); expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 20]'); }); diff --git a/lib/content-services/src/lib/search/components/search-slider/search-slider.component.ts b/lib/content-services/src/lib/search/components/search-slider/search-slider.component.ts index da53ee455b5..1a7edbd6ec5 100644 --- a/lib/content-services/src/lib/search/components/search-slider/search-slider.component.ts +++ b/lib/content-services/src/lib/search/components/search-slider/search-slider.component.ts @@ -15,11 +15,12 @@ * limitations under the License. */ -import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core'; +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { SearchWidget } from '../../models/search-widget.interface'; import { SearchWidgetSettings } from '../../models/search-widget-settings.interface'; -import { SearchQueryBuilderService } from '../../search-query-builder.service'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; import { MatSliderChange } from '@angular/material/slider'; +import { Subject } from 'rxjs'; @Component({ selector: 'adf-search-slider', @@ -39,6 +40,8 @@ export class SearchSliderComponent implements SearchWidget, OnInit { min: number; max: number; thumbLabel = false; + enableChangeUpdate: boolean; + displayValue$: Subject = new Subject(); /** The numeric value represented by the slider. */ @Input() @@ -59,6 +62,7 @@ export class SearchSliderComponent implements SearchWidget, OnInit { } this.thumbLabel = this.settings['thumbLabel'] ? true : false; + this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true; } if (this.startValue) { @@ -66,6 +70,13 @@ export class SearchSliderComponent implements SearchWidget, OnInit { } } + clear() { + this.value = this.min || 0; + if (this.enableChangeUpdate) { + this.updateQuery(null); + } + } + reset() { this.value = this.min || 0; this.updateQuery(null); @@ -73,7 +84,9 @@ export class SearchSliderComponent implements SearchWidget, OnInit { onChangedHandler(event: MatSliderChange) { this.value = event.value; - this.updateQuery(this.value); + if (this.enableChangeUpdate) { + this.updateQuery(this.value); + } } submitValues() { @@ -94,6 +107,7 @@ export class SearchSliderComponent implements SearchWidget, OnInit { } private updateQuery(value: number | null) { + this.displayValue$.next( this.value ? `${this.value} ${this.settings.unit ?? ''}` : '' ); if (this.id && this.context && this.settings && this.settings.field) { if (value === null) { this.context.queryFragments[this.id] = ''; diff --git a/lib/content-services/src/lib/search/components/search-sorting-picker/search-sorting-picker.component.spec.ts b/lib/content-services/src/lib/search/components/search-sorting-picker/search-sorting-picker.component.spec.ts index 5fca98dd6d1..ad866f70035 100644 --- a/lib/content-services/src/lib/search/components/search-sorting-picker/search-sorting-picker.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-sorting-picker/search-sorting-picker.component.spec.ts @@ -16,7 +16,7 @@ */ import { SearchSortingPickerComponent } from './search-sorting-picker.component'; -import { SearchQueryBuilderService } from '../../search-query-builder.service'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; import { AppConfigService } from '@alfresco/adf-core'; import { SearchConfiguration } from '../../models/search-configuration.interface'; import { TestBed } from '@angular/core/testing'; diff --git a/lib/content-services/src/lib/search/components/search-sorting-picker/search-sorting-picker.component.ts b/lib/content-services/src/lib/search/components/search-sorting-picker/search-sorting-picker.component.ts index 02fe1c0c3bf..b9d7dea390d 100644 --- a/lib/content-services/src/lib/search/components/search-sorting-picker/search-sorting-picker.component.ts +++ b/lib/content-services/src/lib/search/components/search-sorting-picker/search-sorting-picker.component.ts @@ -16,7 +16,7 @@ */ import { Component, OnInit, ViewEncapsulation, Inject } from '@angular/core'; -import { SearchQueryBuilderService } from '../../search-query-builder.service'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; import { SearchSortingDefinition } from '../../models/search-sorting-definition.interface'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; diff --git a/lib/content-services/src/lib/search/components/search-text/search-text.component.html b/lib/content-services/src/lib/search/components/search-text/search-text.component.html index daec8a8aaa0..fedfd6393fd 100644 --- a/lib/content-services/src/lib/search/components/search-text/search-text.component.html +++ b/lib/content-services/src/lib/search/components/search-text/search-text.component.html @@ -4,7 +4,7 @@ placeholder="{{ settings?.placeholder | translate }}" [(ngModel)]="value" (change)="onChangedHandler($event)"> - diff --git a/lib/content-services/src/lib/search/components/search-text/search-text.component.ts b/lib/content-services/src/lib/search/components/search-text/search-text.component.ts index 88dbbe7850c..5547b8eb21a 100644 --- a/lib/content-services/src/lib/search/components/search-text/search-text.component.ts +++ b/lib/content-services/src/lib/search/components/search-text/search-text.component.ts @@ -15,10 +15,11 @@ * limitations under the License. */ -import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core'; +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { SearchWidget } from '../../models/search-widget.interface'; import { SearchWidgetSettings } from '../../models/search-widget-settings.interface'; -import { SearchQueryBuilderService } from '../../search-query-builder.service'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; +import { Subject } from 'rxjs'; @Component({ selector: 'adf-search-text', @@ -39,6 +40,7 @@ export class SearchTextComponent implements SearchWidget, OnInit { startValue: string; isActive = false; enableChangeUpdate = true; + displayValue$: Subject = new Subject(); ngOnInit() { if (this.context && this.settings && this.settings.pattern) { @@ -59,9 +61,15 @@ export class SearchTextComponent implements SearchWidget, OnInit { } } - reset() { + clear() { this.isActive = false; + this.value = ''; + if (this.enableChangeUpdate) { + this.updateQuery(null); + } + } + reset() { this.value = ''; this.updateQuery(null); } @@ -75,11 +83,11 @@ export class SearchTextComponent implements SearchWidget, OnInit { } private updateQuery(value: string) { + this.displayValue$.next(value); if (this.context && this.settings && this.settings.field) { this.context.queryFragments[this.id] = value ? `${this.settings.field}:'${this.getSearchPrefix()}${value}${this.getSearchSuffix()}'` : ''; this.context.update(); } - } submitValues() { @@ -96,6 +104,7 @@ export class SearchTextComponent implements SearchWidget, OnInit { setValue(value: string) { this.value = value; + this.displayValue$.next(this.value); this.submitValues(); } diff --git a/lib/content-services/src/lib/search/components/search-widget-container/search-widget-container.component.ts b/lib/content-services/src/lib/search/components/search-widget-container/search-widget-container.component.ts index 2b545a17313..baac0c421e7 100644 --- a/lib/content-services/src/lib/search/components/search-widget-container/search-widget-container.component.ts +++ b/lib/content-services/src/lib/search/components/search-widget-container/search-widget-container.component.ts @@ -15,10 +15,23 @@ * limitations under the License. */ -import { Component, Input, ViewChild, ViewContainerRef, OnInit, OnDestroy, ComponentRef, ComponentFactoryResolver, Inject, SimpleChanges, OnChanges } from '@angular/core'; -import { SearchFilterService } from '../search-filter/search-filter.service'; -import { BaseQueryBuilderService } from '../../base-query-builder.service'; +import { + Component, + Input, + ViewChild, + ViewContainerRef, + OnInit, + OnDestroy, + ComponentRef, + ComponentFactoryResolver, + Inject, + SimpleChanges, + OnChanges +} from '@angular/core'; +import { SearchFilterService } from '../../services/search-filter.service'; +import { BaseQueryBuilderService } from '../../services/base-query-builder.service'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; +import { Observable } from 'rxjs'; @Component({ selector: 'adf-search-widget-container', @@ -74,7 +87,7 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy, OnChan private setupWidget(ref: ComponentRef) { if (ref && ref.instance) { ref.instance.id = this.id; - ref.instance.settings = { ...this.settings }; + ref.instance.settings = {...this.settings}; ref.instance.context = this.queryBuilder; if (this.value) { ref.instance.isActive = true; @@ -107,6 +120,13 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy, OnChan return this.componentRef.instance.getCurrentValue(); } + getDisplayValue(): Observable | null { + if (!this.componentRef?.instance) { + return null; + } + return this.componentRef.instance.displayValue$; + } + resetInnerWidget() { if (this.componentRef && this.componentRef.instance) { this.componentRef.instance.reset(); diff --git a/lib/content-services/src/lib/search/models/facet-field.interface.ts b/lib/content-services/src/lib/search/models/facet-field.interface.ts index 961634c07da..54e5606fdb1 100644 --- a/lib/content-services/src/lib/search/models/facet-field.interface.ts +++ b/lib/content-services/src/lib/search/models/facet-field.interface.ts @@ -31,5 +31,13 @@ export interface FacetField { currentPageSize?: number; checked?: boolean; type?: string; + settings?: FacetFieldSettings; [propName: string]: any; } + +export interface FacetFieldSettings { + /* allow the user to update search in every change */ + allowUpdateOnChange?: boolean; + /* allow the user show/hide default search actions */ + hideDefaultAction?: boolean; +} diff --git a/lib/content-services/src/lib/search/models/facet-widget.interface.ts b/lib/content-services/src/lib/search/models/facet-widget.interface.ts new file mode 100644 index 00000000000..8df0676c2c7 --- /dev/null +++ b/lib/content-services/src/lib/search/models/facet-widget.interface.ts @@ -0,0 +1,27 @@ +/*! + * @license + * Copyright 2019 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 { Subject } from 'rxjs'; + +export interface FacetWidget { + /* provide the formatted selected value for chip */ + displayValue$: Subject; + /* reset the value and update the search */ + reset(): void; + /* update the search with field value */ + submitValues(): void; +} diff --git a/lib/content-services/src/lib/search/models/search-configuration.interface.ts b/lib/content-services/src/lib/search/models/search-configuration.interface.ts index f4ca9fec196..e6ebd764e6e 100644 --- a/lib/content-services/src/lib/search/models/search-configuration.interface.ts +++ b/lib/content-services/src/lib/search/models/search-configuration.interface.ts @@ -17,7 +17,7 @@ import { FilterQuery } from './filter-query.interface'; import { FacetQuery } from './facet-query.interface'; -import { FacetField } from './facet-field.interface'; +import { FacetField, FacetFieldSettings } from './facet-field.interface'; import { SearchCategory } from './search-category.interface'; import { SearchSortingDefinition } from './search-sorting-definition.interface'; import { RequestHighlight } from '@alfresco/js-api'; @@ -35,6 +35,7 @@ export interface SearchConfiguration { expanded?: boolean; mincount?: number; queries: FacetQuery[]; + settings?: FacetFieldSettings; }; facetFields?: { expanded?: boolean; diff --git a/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts b/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts index 0f2ef31cca5..d1b4c9d1793 100644 --- a/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts +++ b/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts @@ -17,5 +17,14 @@ export interface SearchWidgetSettings { field: string; + /* allow the user to update search in every change */ + allowUpdateOnChange?: boolean; + /* allow the user hide default search actions. So widget can have custom actions */ + hideDefaultAction?: boolean; + /* describes the unit of the value i.e byte for better display message */ + unit?: string; + /* describes query format */ + format?: string; + [indexer: string]: any; } diff --git a/lib/content-services/src/lib/search/models/search-widget.interface.ts b/lib/content-services/src/lib/search/models/search-widget.interface.ts index f4666ef416e..55dcca7b6b1 100644 --- a/lib/content-services/src/lib/search/models/search-widget.interface.ts +++ b/lib/content-services/src/lib/search/models/search-widget.interface.ts @@ -16,17 +16,23 @@ */ import { SearchWidgetSettings } from './search-widget-settings.interface'; -import { SearchQueryBuilderService } from '../search-query-builder.service'; +import { SearchQueryBuilderService } from '../services/search-query-builder.service'; +import { Subject } from 'rxjs'; export interface SearchWidget { id: string; + /* optional field control options */ settings?: SearchWidgetSettings; context?: SearchQueryBuilderService; isActive?: boolean; startValue: any; - reset(); - submitValues(); - hasValidValue(); - getCurrentValue(); + /* stream emit value on changes */ + displayValue$: Subject; + /* reset the value and update the search */ + reset(): void; + /* update the search with field value */ + submitValues(): void; + hasValidValue(): boolean; + getCurrentValue(): any; setValue(value: any); } diff --git a/lib/content-services/src/lib/search/public-api.ts b/lib/content-services/src/lib/search/public-api.ts index e013d31306d..3867657cb32 100644 --- a/lib/content-services/src/lib/search/public-api.ts +++ b/lib/content-services/src/lib/search/public-api.ts @@ -24,12 +24,12 @@ export * from './models/search-category.interface'; export * from './models/search-widget-settings.interface'; export * from './models/search-widget.interface'; export * from './models/search-configuration.interface'; -export * from './search-query-builder.service'; +export * from './services/search-query-builder.service'; export * from './models/search-range.interface'; export * from './models/search-form.interface'; export * from './search-query-service.token'; -export * from './search-header-query-builder.service'; +export * from './services/search-header-query-builder.service'; export * from './components/search.component'; export * from './components/search-control.component'; @@ -41,7 +41,7 @@ export * from './components/search-check-list/search-check-list.component'; export * from './components/search-chip-list/search-chip-list.component'; export * from './components/search-date-range/search-date-range.component'; export * from './components/search-filter/search-filter.component'; -export * from './components/search-filter/search-filter.service'; +export * from './services/search-filter.service'; export * from './components/search-filter-container/search-filter-container.component'; export * from './components/search-number-range/search-number-range.component'; export * from './components/search-radio/search-radio.component'; @@ -52,5 +52,10 @@ export * from './components/search-text/search-text.component'; export * from './components/search-widget-container/search-widget-container.component'; export * from './components/search-datetime-range/search-datetime-range.component'; export * from './components/search-form/search-form.component'; +export * from './services/search-facet-filters.service'; +export * from './components/search-filter-chips/search-filter-chips.component'; +export * from './components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component'; +export * from './components/search-facet-field/search-facet-field.component'; +export * from './components/reset-search.directive'; export * from './search.module'; diff --git a/lib/content-services/src/lib/search/search-query-service.token.ts b/lib/content-services/src/lib/search/search-query-service.token.ts index 13a2584d819..eb5c35998ea 100644 --- a/lib/content-services/src/lib/search/search-query-service.token.ts +++ b/lib/content-services/src/lib/search/search-query-service.token.ts @@ -16,6 +16,6 @@ */ import { InjectionToken } from '@angular/core'; -import { BaseQueryBuilderService } from './base-query-builder.service'; +import { BaseQueryBuilderService } from './services/base-query-builder.service'; export const SEARCH_QUERY_SERVICE_TOKEN = new InjectionToken('QueryService'); diff --git a/lib/content-services/src/lib/search/search.module.ts b/lib/content-services/src/lib/search/search.module.ts index 9e6add039a5..d9dcfedb4a9 100644 --- a/lib/content-services/src/lib/search/search.module.ts +++ b/lib/content-services/src/lib/search/search.module.ts @@ -37,10 +37,16 @@ import { SearchCheckListComponent } from './components/search-check-list/search- import { SearchDateRangeComponent } from './components/search-date-range/search-date-range.component'; import { SearchSortingPickerComponent } from './components/search-sorting-picker/search-sorting-picker.component'; import { SEARCH_QUERY_SERVICE_TOKEN } from './search-query-service.token'; -import { SearchQueryBuilderService } from './search-query-builder.service'; +import { SearchQueryBuilderService } from './services/search-query-builder.service'; import { SearchFilterContainerComponent } from './components/search-filter-container/search-filter-container.component'; import { SearchDatetimeRangeComponent } from './components/search-datetime-range/search-datetime-range.component'; import { SearchFormComponent } from './components/search-form/search-form.component'; +import { SearchFilterChipsComponent } from './components/search-filter-chips/search-filter-chips.component'; +import { SearchFilterMenuCardComponent } from './components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component'; +import { SearchFacetFieldComponent } from './components/search-facet-field/search-facet-field.component'; +import { SearchWidgetChipComponent } from './components/search-filter-chips/search-widget-chip/search-widget-chip.component'; +import { SearchFacetChipComponent } from './components/search-filter-chips/search-facet-chip/search-facet-chip.component'; +import { ResetSearchDirective } from './components/reset-search.directive'; @NgModule({ imports: [ @@ -67,7 +73,13 @@ import { SearchFormComponent } from './components/search-form/search-form.compon SearchDatetimeRangeComponent, SearchSortingPickerComponent, SearchFilterContainerComponent, - SearchFormComponent + SearchFormComponent, + SearchFilterChipsComponent, + SearchFilterMenuCardComponent, + SearchFacetFieldComponent, + SearchWidgetChipComponent, + SearchFacetChipComponent, + ResetSearchDirective ], exports: [ SearchComponent, @@ -86,11 +98,14 @@ import { SearchFormComponent } from './components/search-form/search-form.compon SearchDatetimeRangeComponent, SearchSortingPickerComponent, SearchFilterContainerComponent, - SearchFormComponent + SearchFormComponent, + SearchFilterChipsComponent, + SearchFilterMenuCardComponent, + SearchFacetFieldComponent, + ResetSearchDirective ], providers: [ - { provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService }, - SearchSortingPickerComponent + { provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService } ] }) export class SearchModule {} diff --git a/lib/content-services/src/lib/search/base-query-builder.service.ts b/lib/content-services/src/lib/search/services/base-query-builder.service.ts similarity index 92% rename from lib/content-services/src/lib/search/base-query-builder.service.ts rename to lib/content-services/src/lib/search/services/base-query-builder.service.ts index c886bb80645..a93b2a18c37 100644 --- a/lib/content-services/src/lib/search/base-query-builder.service.ts +++ b/lib/content-services/src/lib/search/services/base-query-builder.service.ts @@ -16,7 +16,7 @@ */ import { Injectable } from '@angular/core'; -import { Subject, Observable, from } from 'rxjs'; +import { Subject, Observable, from, ReplaySubject } from 'rxjs'; import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core'; import { QueryBody, @@ -27,15 +27,15 @@ import { RequestHighlight, RequestScope } from '@alfresco/js-api'; -import { SearchCategory } from './models/search-category.interface'; -import { FilterQuery } from './models/filter-query.interface'; -import { SearchRange } from './models/search-range.interface'; -import { SearchConfiguration } from './models/search-configuration.interface'; -import { FacetQuery } from './models/facet-query.interface'; -import { SearchSortingDefinition } from './models/search-sorting-definition.interface'; -import { FacetField } from './models/facet-field.interface'; -import { FacetFieldBucket } from './models/facet-field-bucket.interface'; -import { SearchForm } from './models/search-form.interface'; +import { SearchCategory } from '../models/search-category.interface'; +import { FilterQuery } from '../models/filter-query.interface'; +import { SearchRange } from '../models/search-range.interface'; +import { SearchConfiguration } from '../models/search-configuration.interface'; +import { FacetQuery } from '../models/facet-query.interface'; +import { SearchSortingDefinition } from '../models/search-sorting-definition.interface'; +import { FacetField } from '../models/facet-field.interface'; +import { FacetFieldBucket } from '../models/facet-field-bucket.interface'; +import { SearchForm } from '../models/search-form.interface'; @Injectable({ providedIn: 'root' @@ -54,6 +54,9 @@ export abstract class BaseQueryBuilderService { /* Stream that emits the error whenever user search */ error = new Subject(); + /* Stream that emits search forms */ + searchForms = new ReplaySubject(1); + categories: SearchCategory[] = []; queryFragments: { [id: string]: string } = {}; filterQueries: FilterQuery[] = []; @@ -92,14 +95,16 @@ export abstract class BaseQueryBuilderService { public resetToDefaults() { const currentConfig = this.getDefaultConfiguration(); + this.resetSearchOptions(); this.configUpdated.next(currentConfig); + this.searchForms.next(this.getSearchFormDetails()); this.setUpSearchConfiguration(currentConfig); } public getDefaultConfiguration(): SearchConfiguration | undefined { const configurations = this.loadConfiguration(); - if (this.selectedConfiguration >= 0) { + if (this.selectedConfiguration !== undefined) { return configurations[this.selectedConfiguration]; } @@ -112,8 +117,9 @@ export abstract class BaseQueryBuilderService { public updateSelectedConfiguration(index: number): void { const currentConfig = this.loadConfiguration(); if (Array.isArray(currentConfig) && currentConfig[index] !== undefined) { - this.configUpdated.next(currentConfig[index]); this.selectedConfiguration = index; + this.configUpdated.next(currentConfig[index]); + this.searchForms.next(this.getSearchFormDetails()); this.resetSearchOptions(); this.setUpSearchConfiguration(currentConfig[index]); this.update(); @@ -126,18 +132,21 @@ export abstract class BaseQueryBuilderService { this.filterQueries = []; this.sorting = []; this.sortingOptions = []; + this.userFacetBuckets = {}; this.scope = null; } - public getSearchConfigurationDetails(): SearchForm[] { + public getSearchFormDetails(): SearchForm[] { const configurations = this.loadConfiguration(); if (Array.isArray(configurations)) { return configurations.map((configuration, index) => ({ index, - name: configuration.name || 'SEARCH.UNKNOWN_FORM', + name: configuration.name || 'SEARCH.UNKNOWN_CONFIGURATION', default: configuration.default || false, selected: this.selectedConfiguration !== undefined ? index === this.selectedConfiguration : configuration.default })); + } else if (!!configurations) { + return [{ index: 0, name: configurations.name || 'SEARCH.UNKNOWN_CONFIGURATION', default: true, selected: true }]; } return []; } diff --git a/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts b/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts new file mode 100644 index 00000000000..4a772522f3f --- /dev/null +++ b/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts @@ -0,0 +1,419 @@ +/*! + * @license + * Copyright 2019 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 { TestBed } from '@angular/core/testing'; + +import { SearchFacetFiltersService } from './search-facet-filters.service'; +import { ContentTestingModule } from '../../testing/content.testing.module'; +import { SearchQueryBuilderService } from './search-query-builder.service'; + +describe('SearchFacetFiltersService', () => { + let searchFacetFiltersService: SearchFacetFiltersService; + let queryBuilder: SearchQueryBuilderService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ContentTestingModule] + }); + searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService); + queryBuilder = TestBed.inject(SearchQueryBuilderService); + }); + + it('should subscribe to query builder executed event', () => { + spyOn(searchFacetFiltersService, 'onDataLoaded').and.stub(); + const data = { list: {} }; + queryBuilder.executed.next(data); + + expect(searchFacetFiltersService.onDataLoaded).toHaveBeenCalledWith(data); + }); + + it('should fetch facet queries from response payload', () => { + searchFacetFiltersService.responseFacets = null; + + queryBuilder.config = { + categories: [], + facetQueries: { + label: 'label1', + queries: [ + { label: 'q1', query: 'query1' }, + { label: 'q2', query: 'query2' } + ] + } + }; + + const queries = [ + { label: 'q1', filterQuery: 'query1', metrics: [{value: {count: 1}}] }, + { label: 'q2', filterQuery: 'query2', metrics: [{value: {count: 1}}] } + ]; + const data = { + list: { + context: { + facets: [{ + type: 'query', + label: 'label1', + buckets: queries + }] + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + + expect(searchFacetFiltersService.responseFacets.length).toBe(1); + expect(searchFacetFiltersService.responseFacets[0].buckets.length).toEqual(2); + }); + + it('should preserve order after response processing', () => { + searchFacetFiltersService.responseFacets = null; + + queryBuilder.config = { + categories: [], + facetQueries: { + label: 'label1', + queries: [ + { label: 'q1', query: 'query1' }, + { label: 'q2', query: 'query2' }, + { label: 'q3', query: 'query3' } + ] + } + }; + + const queries = [ + { label: 'q2', filterQuery: 'query2', metrics: [{value: {count: 1}}] }, + { label: 'q1', filterQuery: 'query1', metrics: [{value: {count: 1}}] }, + { label: 'q3', filterQuery: 'query3', metrics: [{value: {count: 1}}] } + + ]; + const data = { + list: { + context: { + facets: [{ + type: 'query', + label: 'label1', + buckets: queries + }] + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + + expect(searchFacetFiltersService.responseFacets.length).toBe(1); + expect(searchFacetFiltersService.responseFacets[0].buckets.length).toBe(3); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].label).toBe('q1'); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].label).toBe('q2'); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[2].label).toBe('q3'); + }); + + it('should not fetch facet queries from response payload', () => { + searchFacetFiltersService.responseFacets = null; + + queryBuilder.config = { + categories: [], + facetQueries: { + queries: [] + } + }; + + const data = { + list: { + context: { + facets: null + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + + expect(searchFacetFiltersService.responseFacets).toBeNull(); + }); + + it('should fetch facet fields from response payload', () => { + searchFacetFiltersService.responseFacets = null; + + queryBuilder.config = { + categories: [], + facetFields: { fields: [ + { label: 'f1', field: 'f1', mincount: 0 }, + { label: 'f2', field: 'f2', mincount: 0 } + ]}, + facetQueries: { + queries: [] + } + }; + + const fields: any = [ + { type: 'field', label: 'f1', buckets: [{ label: 'a1' }, { label: 'a2' }] }, + { type: 'field', label: 'f2', buckets: [{ label: 'b1' }, { label: 'b2' }] } + ]; + const data = { + list: { + context: { + facets: fields + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + + expect(searchFacetFiltersService.responseFacets.length).toEqual(2); + expect(searchFacetFiltersService.responseFacets[0].buckets.length).toEqual(2); + expect(searchFacetFiltersService.responseFacets[1].buckets.length).toEqual(2); + }); + + it('should filter response facet fields based on search filter config method', () => { + queryBuilder.config = { + categories: [], + facetFields: { fields: [ + { label: 'f1', field: 'f1' } + ]}, + facetQueries: { + queries: [] + }, + filterWithContains: false + }; + + const initialFields: any = [ + { type: 'field', label: 'f1', buckets: [ + { label: 'firstLabel', display: 'firstLabel', metrics: [{value: {count: 5}}] }, + { label: 'secondLabel', display: 'secondLabel', metrics: [{value: {count: 5}}] }, + { label: 'thirdLabel', display: 'thirdLabel', metrics: [{value: {count: 5}}] } + ] + } + ]; + + const data = { + list: { + context: { + facets: initialFields + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + expect(searchFacetFiltersService.responseFacets.length).toBe(1); + expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(3); + + searchFacetFiltersService.responseFacets[0].buckets.filterText = 'f'; + expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(1); + expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems[0].label).toEqual('firstLabel'); + + searchFacetFiltersService.responseFacets[0].buckets.filterText = 'label'; + expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(0); + + // Set filter method to use contains and test again + queryBuilder.config.filterWithContains = true; + searchFacetFiltersService.responseFacets[0].buckets.filterText = 'f'; + expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(1); + searchFacetFiltersService.responseFacets[0].buckets.filterText = 'label'; + expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(3); + }); + + it('should fetch facet fields from response payload and show the bucket values', () => { + searchFacetFiltersService.responseFacets = null; + + queryBuilder.config = { + categories: [], + facetFields: { fields: [ + { label: 'f1', field: 'f1' }, + { label: 'f2', field: 'f2' } + ]}, + facetQueries: { + queries: [] + } + }; + + const serverResponseFields: any = [ + { + type: 'field', + label: 'f1', + buckets: [ + { label: 'b1', metrics: [{value: {count: 10}}] }, + { label: 'b2', metrics: [{value: {count: 1}}] } + ] + }, + { type: 'field', label: 'f2', buckets: [] } + ]; + const data = { + list: { + context: { + facets: serverResponseFields + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + expect(searchFacetFiltersService.responseFacets.length).toEqual(1); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(10); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(1); + }); + + it('should fetch facet fields from response payload and update the existing bucket values', () => { + queryBuilder.config = { + categories: [], + facetFields: { fields: [ + { label: 'f1', field: 'f1' }, + { label: 'f2', field: 'f2' } + ]}, + facetQueries: { + queries: [] + } + }; + + const initialFields: any = [ + { type: 'field', label: 'f1', buckets: { items: [{ label: 'b1', count: 10, filterQuery: 'filter' }, { label: 'b2', count: 1 }]} }, + { type: 'field', label: 'f2', buckets: [] } + ]; + searchFacetFiltersService.responseFacets = initialFields; + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(10); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(1); + + const serverResponseFields: any = [ + { type: 'field', label: 'f1', buckets: + [{ label: 'b1', metrics: [{value: {count: 6}}], filterQuery: 'filter' }, + { label: 'b2', metrics: [{value: {count: 0}}] }] }, + { type: 'field', label: 'f2', buckets: [] } + ]; + const data = { + list: { + context: { + facets: serverResponseFields + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(6); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(0); + }); + + it('should update correctly the existing facetFields bucket values', () => { + searchFacetFiltersService.responseFacets = null; + + queryBuilder.config = { + categories: [], + facetFields: { fields: [{ label: 'f1', field: 'f1' }] }, + facetQueries: { queries: [] } + }; + + const firstCallFields: any = [{ + type: 'field', + label: 'f1', + buckets: [{ label: 'b1', metrics: [{value: {count: 10}}] }] + }]; + const firstCallData = { list: { context: { facets: firstCallFields }}}; + searchFacetFiltersService.onDataLoaded(firstCallData); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(10); + + const secondCallFields: any = [{ + type: 'field', + label: 'f1', + buckets: [{ label: 'b1', metrics: [{value: {count: 6}}] }] + }]; + const secondCallData = { list: { context: { facets: secondCallFields}}}; + searchFacetFiltersService.onDataLoaded(secondCallData); + expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(6); + }); + + it('should fetch facet intervals from response payload', () => { + searchFacetFiltersService.responseFacets = null; + queryBuilder.config = { + categories: [], + facetIntervals: { + intervals: [ + { label: 'test_intervals1', field: 'f1', sets: [ + { label: 'interval1', start: 's1', end: 'e1'}, + { label: 'interval2', start: 's2', end: 'e2'} + ]}, + { label: 'test_intervals2', field: 'f2', sets: [ + { label: 'interval3', start: 's3', end: 'e3'}, + { label: 'interval4', start: 's4', end: 'e4'} + ]} + ] + } + }; + + const response1 = [ + { label: 'interval1', filterQuery: 'query1', metrics: [{ value: { count: 1 }}]}, + { label: 'interval2', filterQuery: 'query2', metrics: [{ value: { count: 2 }}]} + ]; + const response2 = [ + { label: 'interval3', filterQuery: 'query3', metrics: [{ value: { count: 3 }}]}, + { label: 'interval4', filterQuery: 'query4', metrics: [{ value: { count: 4 }}]} + ]; + const data = { + list: { + context: { + facets: [ + { type: 'interval', label: 'test_intervals1', buckets: response1 }, + { type: 'interval', label: 'test_intervals2', buckets: response2 } + ] + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + + expect(searchFacetFiltersService.responseFacets.length).toBe(2); + expect(searchFacetFiltersService.responseFacets[0].buckets.length).toEqual(2); + expect(searchFacetFiltersService.responseFacets[1].buckets.length).toEqual(2); + }); + + it('should filter out the fetched facet intervals that have bucket values less than their set mincount', () => { + searchFacetFiltersService.responseFacets = null; + queryBuilder.config = { + categories: [], + facetIntervals: { + intervals: [ + { label: 'test_intervals1', field: 'f1', mincount: 2, sets: [ + { label: 'interval1', start: 's1', end: 'e1'}, + { label: 'interval2', start: 's2', end: 'e2'} + ]}, + { label: 'test_intervals2', field: 'f2', mincount: 5, sets: [ + { label: 'interval3', start: 's3', end: 'e3'}, + { label: 'interval4', start: 's4', end: 'e4'} + ]} + ] + } + }; + + const response1 = [ + { label: 'interval1', filterQuery: 'query1', metrics: [{ value: { count: 1 }}]}, + { label: 'interval2', filterQuery: 'query2', metrics: [{ value: { count: 2 }}]} + ]; + const response2 = [ + { label: 'interval3', filterQuery: 'query3', metrics: [{ value: { count: 3 }}]}, + { label: 'interval4', filterQuery: 'query4', metrics: [{ value: { count: 4 }}]} + ]; + const data = { + list: { + context: { + facets: [ + { type: 'interval', label: 'test_intervals1', buckets: response1 }, + { type: 'interval', label: 'test_intervals2', buckets: response2 } + ] + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + + expect(searchFacetFiltersService.responseFacets.length).toBe(1); + expect(searchFacetFiltersService.responseFacets[0].buckets.length).toEqual(1); + }); + +}); diff --git a/lib/content-services/src/lib/search/services/search-facet-filters.service.ts b/lib/content-services/src/lib/search/services/search-facet-filters.service.ts new file mode 100644 index 00000000000..c5fa5612599 --- /dev/null +++ b/lib/content-services/src/lib/search/services/search-facet-filters.service.ts @@ -0,0 +1,370 @@ +/*! + * @license + * Copyright 2019 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 { Inject, Injectable, OnDestroy } from '@angular/core'; +import { FacetField } from '../models/facet-field.interface'; +import { Subject } from 'rxjs'; +import { SEARCH_QUERY_SERVICE_TOKEN } from '../search-query-service.token'; +import { SearchQueryBuilderService } from './search-query-builder.service'; +import { SearchService, TranslationService } from '@alfresco/adf-core'; +import { takeUntil } from 'rxjs/operators'; +import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api'; +import { SearchFilterList } from '../models/search-filter-list.model'; +import { FacetFieldBucket } from '../models/facet-field-bucket.interface'; + +export interface SelectedBucket { + field: FacetField; + bucket: FacetFieldBucket; +} + +@Injectable({ + providedIn: 'root' +}) +export class SearchFacetFiltersService implements OnDestroy { + + /** 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; + + /** shows the facet chips */ + selectedBuckets: SelectedBucket[] = []; + + private DEFAULT_PAGE_SIZE = 5; + private readonly facetQueriesPageSize = this.DEFAULT_PAGE_SIZE; + private readonly onDestroy$ = new Subject(); + + constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService, + private searchService: SearchService, + private translationService: TranslationService) { + if (queryBuilder.config && queryBuilder.config.facetQueries) { + this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE; + } + + this.queryBuilder.configUpdated + .pipe(takeUntil(this.onDestroy$)) + .subscribe(() => { + this.selectedBuckets = []; + this.responseFacets = null; + }); + + this.queryBuilder.updated + .pipe(takeUntil(this.onDestroy$)) + .subscribe((query) => this.queryBuilder.execute(query)); + + this.queryBuilder.executed + .pipe(takeUntil(this.onDestroy$)) + .subscribe((resultSetPaging: ResultSetPaging) => { + this.onDataLoaded(resultSetPaging); + this.searchService.dataLoaded.next(resultSetPaging); + }); + } + + onDataLoaded(data: any) { + const context = data.list.context; + + if (context) { + this.parseFacets(context); + } else { + this.responseFacets = null; + } + } + + private parseFacets(context: ResultSetContext) { + this.parseFacetFields(context); + this.parseFacetIntervals(context); + this.parseFacetQueries(context); + } + + 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); + + if (alreadyExistingField) { + const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || []; + + this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets); + } else if (responseField) { + if (responseBuckets.length > 0) { + const bucketList = new SearchFilterList(responseBuckets, field.pageSize); + bucketList.filter = this.getBucketFilterFunction(bucketList); + + if (!this.responseFacets) { + this.responseFacets = []; + } + 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) { + const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || []; + this.parseFacetItems(context, configFacetFields, 'field'); + } + + private parseFacetIntervals(context: ResultSetContext) { + const configFacetIntervals = this.queryBuilder.config.facetIntervals && this.queryBuilder.config.facetIntervals.intervals || []; + this.parseFacetItems(context, configFacetIntervals, 'interval'); + } + + private parseFacetQueries(context: ResultSetContext) { + const facetQuerySetting = this.queryBuilder.config.facetQueries?.settings || {}; + const configFacetQueries = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.queries || []; + const configGroups = configFacetQueries.reduce((acc, query) => { + const group = this.queryBuilder.getQueryGroup(query); + if (acc[group]) { + acc[group].push(query); + } else { + acc[group] = [query]; + } + return acc; + }, []); + + const mincount = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.mincount; + const mincountFilter = this.getFilterByMinCount(mincount); + + Object.keys(configGroups).forEach((group) => { + const responseField = this.findFacet(context, 'query', group); + const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group]) + .filter(mincountFilter); + const alreadyExistingField = this.findResponseFacet('query', group); + + if (alreadyExistingField) { + const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || []; + + this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets); + } else if (responseField) { + if (responseBuckets.length > 0) { + const bucketList = new SearchFilterList(responseBuckets, this.facetQueriesPageSize); + bucketList.filter = this.getBucketFilterFunction(bucketList); + + if (!this.responseFacets) { + this.responseFacets = []; + } + this.responseFacets.push( { + field: group, + type: responseField.type || 'query', + label: group, + pageSize: this.DEFAULT_PAGE_SIZE, + currentPageSize: this.DEFAULT_PAGE_SIZE, + buckets: bucketList, + settings: facetQuerySetting + }); + } + } + }); + + } + + private getResponseBuckets(responseField: GenericFacetResponse, configField: FacetField): FacetFieldBucket[] { + return ((responseField && responseField.buckets) || []).map((respBucket) => { + + respBucket['count'] = this.getCountValue(respBucket); + respBucket.filterQuery = respBucket.filterQuery || this.getCorrespondingFilterQuery(configField, respBucket.label); + return { + ...respBucket, + checked: false, + display: respBucket.display, + label: respBucket.label + }; + }); + } + + private getResponseQueryBuckets(responseField: GenericFacetResponse, configGroup: any): FacetFieldBucket[] { + return (configGroup || []).map((query) => { + const respBucket = ((responseField && responseField.buckets) || []) + .find((bucket) => bucket.label === query.label) || {}; + + respBucket['count'] = this.getCountValue(respBucket); + return { + ...respBucket, + checked: false, + display: respBucket.display, + label: respBucket.label + }; + }); + } + + private getCountValue(bucket: GenericBucket): number { + return (!!bucket && !!bucket.metrics && bucket.metrics[0]?.value?.count) || 0; + } + + getBucketCountDisplay(bucket: FacetFieldBucket): string { + return bucket.count === null ? '' : `(${bucket.count})`; + } + + private getFilterByMinCount(mincountInput: number) { + return (bucket) => { + let mincount = mincountInput; + if (mincount === undefined) { + mincount = 1; + } + return bucket.count >= mincount; + }; + } + + private getCorrespondingFilterQuery(configFacetItem: FacetField, bucketLabel: string): string { + let filterQuery = null; + + if (configFacetItem.field && bucketLabel) { + + if (configFacetItem.sets) { + const configSet = configFacetItem.sets.find((set) => bucketLabel === set.label); + + if (configSet) { + filterQuery = this.buildIntervalQuery(configFacetItem.field, configSet); + } + + } else { + filterQuery = `${configFacetItem.field}:"${bucketLabel}"`; + } + } + + return filterQuery; + } + + private buildIntervalQuery(fieldName: string, interval: any): string { + const start = interval.start; + const end = interval.end; + const startLimit = (interval.startInclusive === undefined || interval.startInclusive === true) ? '[' : '<'; + const endLimit = (interval.endInclusive === undefined || interval.endInclusive === true) ? ']' : '>'; + + 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; + }; + } + + unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { + if (bucket) { + bucket.checked = false; + this.queryBuilder.removeUserFacetBucket(field, bucket); + this.updateSelectedBuckets(); + this.queryBuilder.update(); + } + } + + /* update adf-search-chip-list component view */ + updateSelectedBuckets() { + if (this.responseFacets) { + this.selectedBuckets = []; + for (const field of this.responseFacets) { + if (field.buckets) { + this.selectedBuckets.push( + ...this.queryBuilder.getUserFacetBuckets(field.field) + .filter((bucket) => bucket.checked) + .map((bucket) => { + return {field, bucket}; + }) + ); + } + } + } else { + this.selectedBuckets = []; + } + } + + ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + resetAllSelectedBuckets() { + this.responseFacets.forEach((field) => { + if (field && field.buckets) { + for (const bucket of field.buckets.items) { + bucket.checked = false; + this.queryBuilder.removeUserFacetBucket(field, bucket); + } + this.updateSelectedBuckets(); + } + }); + this.queryBuilder.update(); + } + + resetQueryFragments() { + this.queryBuilder.queryFragments = {}; + this.queryBuilder.resetToDefaults(); + } + + reset() { + this.responseFacets = []; + this.selectedBuckets = []; + this.queryBuilder.resetToDefaults(); + this.queryBuilder.update(); + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter/search-filter.service.ts b/lib/content-services/src/lib/search/services/search-filter.service.ts similarity index 63% rename from lib/content-services/src/lib/search/components/search-filter/search-filter.service.ts rename to lib/content-services/src/lib/search/services/search-filter.service.ts index 743a39cbcaa..b45d1dca28c 100644 --- a/lib/content-services/src/lib/search/components/search-filter/search-filter.service.ts +++ b/lib/content-services/src/lib/search/services/search-filter.service.ts @@ -16,13 +16,13 @@ */ import { Injectable, Type } from '@angular/core'; -import { SearchTextComponent } from '../search-text/search-text.component'; -import { SearchRadioComponent } from '../search-radio/search-radio.component'; -import { SearchSliderComponent } from '../search-slider/search-slider.component'; -import { SearchNumberRangeComponent } from '../search-number-range/search-number-range.component'; -import { SearchCheckListComponent } from '../search-check-list/search-check-list.component'; -import { SearchDateRangeComponent } from '../search-date-range/search-date-range.component'; -import { SearchDatetimeRangeComponent } from '../search-datetime-range/search-datetime-range.component'; +import { SearchTextComponent } from '../components/search-text/search-text.component'; +import { SearchRadioComponent } from '../components/search-radio/search-radio.component'; +import { SearchSliderComponent } from '../components/search-slider/search-slider.component'; +import { SearchNumberRangeComponent } from '../components/search-number-range/search-number-range.component'; +import { SearchCheckListComponent } from '../components/search-check-list/search-check-list.component'; +import { SearchDateRangeComponent } from '../components/search-date-range/search-date-range.component'; +import { SearchDatetimeRangeComponent } from '../components/search-datetime-range/search-datetime-range.component'; @Injectable({ providedIn: 'root' diff --git a/lib/content-services/src/lib/search/search-header-query-builder.service.spec.ts b/lib/content-services/src/lib/search/services/search-header-query-builder.service.spec.ts similarity index 97% rename from lib/content-services/src/lib/search/search-header-query-builder.service.spec.ts rename to lib/content-services/src/lib/search/services/search-header-query-builder.service.spec.ts index 1def5e126b8..a4fa2d00e6b 100644 --- a/lib/content-services/src/lib/search/search-header-query-builder.service.spec.ts +++ b/lib/content-services/src/lib/search/services/search-header-query-builder.service.spec.ts @@ -15,11 +15,11 @@ * limitations under the License. */ -import { SearchConfiguration } from './models/search-configuration.interface'; +import { SearchConfiguration } from '../models/search-configuration.interface'; import { AppConfigService } from '@alfresco/adf-core'; import { SearchHeaderQueryBuilderService } from './search-header-query-builder.service'; import { TestBed } from '@angular/core/testing'; -import { ContentTestingModule } from '../testing/content.testing.module'; +import { ContentTestingModule } from '../../testing/content.testing.module'; describe('SearchHeaderQueryBuilderService', () => { diff --git a/lib/content-services/src/lib/search/search-header-query-builder.service.ts b/lib/content-services/src/lib/search/services/search-header-query-builder.service.ts similarity index 94% rename from lib/content-services/src/lib/search/search-header-query-builder.service.ts rename to lib/content-services/src/lib/search/services/search-header-query-builder.service.ts index 07e6d881049..e9f0344e589 100644 --- a/lib/content-services/src/lib/search/search-header-query-builder.service.ts +++ b/lib/content-services/src/lib/search/services/search-header-query-builder.service.ts @@ -17,14 +17,14 @@ import { Injectable } from '@angular/core'; import { AlfrescoApiService, AppConfigService, NodesApiService, DataSorting } from '@alfresco/adf-core'; -import { SearchConfiguration } from './models/search-configuration.interface'; +import { SearchConfiguration } from '../models/search-configuration.interface'; import { BaseQueryBuilderService } from './base-query-builder.service'; -import { SearchCategory } from './models/search-category.interface'; +import { SearchCategory } from '../models/search-category.interface'; import { MinimalNode, QueryBody } from '@alfresco/js-api'; import { filter } from 'rxjs/operators'; import { Observable } from 'rxjs'; -import { SearchSortingDefinition } from './models/search-sorting-definition.interface'; -import { FilterSearch } from './models/filter-search.interface'; +import { SearchSortingDefinition } from '../models/search-sorting-definition.interface'; +import { FilterSearch } from '../models/filter-search.interface'; @Injectable({ providedIn: 'root' diff --git a/lib/content-services/src/lib/search/search-query-builder.service.spec.ts b/lib/content-services/src/lib/search/services/search-query-builder.service.spec.ts similarity index 95% rename from lib/content-services/src/lib/search/search-query-builder.service.spec.ts rename to lib/content-services/src/lib/search/services/search-query-builder.service.spec.ts index 79396f15cb8..2850416ad38 100644 --- a/lib/content-services/src/lib/search/search-query-builder.service.spec.ts +++ b/lib/content-services/src/lib/search/services/search-query-builder.service.spec.ts @@ -16,11 +16,11 @@ */ import { SearchQueryBuilderService } from './search-query-builder.service'; -import { SearchConfiguration } from './models/search-configuration.interface'; +import { SearchConfiguration } from '../models/search-configuration.interface'; import { AppConfigService } from '@alfresco/adf-core'; -import { FacetField } from './models/facet-field.interface'; +import { FacetField } from '../models/facet-field.interface'; import { TestBed } from '@angular/core/testing'; -import { ContentTestingModule } from '../testing/content.testing.module'; +import { ContentTestingModule } from '../../testing/content.testing.module'; describe('SearchQueryBuilder', () => { @@ -670,10 +670,12 @@ describe('SearchQueryBuilder', () => { expect(queryBody.scope).toEqual(mockScope); }); - it('should return empty if array of search config not found', () => { - const builder = new SearchQueryBuilderService(buildConfig({}), null); - const forms = builder.getSearchConfigurationDetails(); - expect(forms).toEqual([]); + it('should return empty if array of search config not found', (done) => { + const builder = new SearchQueryBuilderService(buildConfig(null), null); + builder.searchForms.subscribe((forms) => { + expect(forms).toEqual([]); + done(); + }); }); describe('Multiple search configuration', () => { @@ -728,14 +730,15 @@ describe('SearchQueryBuilder', () => { expect(builder.filterQueries.length).toBe(2); }); - it('should list available search form names', () => { - const forms = builder.getSearchConfigurationDetails(); - - expect(forms).toEqual([ - { index: 0, name: 'config1', default: true, selected: true }, - { index: 1, name: 'config2', default: false, selected: false }, - { index: 2, name: 'SEARCH.UNKNOWN_FORM', default: false, selected: false } - ]); + it('should list available search form names', (done) => { + builder.searchForms.subscribe((forms) => { + expect(forms).toEqual([ + { index: 0, name: 'config1', default: true, selected: true }, + { index: 1, name: 'config2', default: false, selected: false }, + { index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false } + ]); + done(); + }); }); it('should allow the user switch the form', () => { @@ -745,15 +748,16 @@ describe('SearchQueryBuilder', () => { expect(builder.filterQueries.length).toBe(2); }); - it('should keep the selected configuration value', () => { + it('should keep the selected configuration value', (done) => { builder.updateSelectedConfiguration(1); - const forms = builder.getSearchConfigurationDetails(); - - expect(forms).toEqual([ - { index: 0, name: 'config1', default: true, selected: false }, - { index: 1, name: 'config2', default: false, selected: true }, - { index: 2, name: 'SEARCH.UNKNOWN_FORM', default: false, selected: false } - ]); + builder.searchForms.subscribe((forms) => { + expect(forms).toEqual([ + { index: 0, name: 'config1', default: true, selected: false }, + { index: 1, name: 'config2', default: false, selected: true }, + { index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false } + ]); + done(); + }); }); }); }); diff --git a/lib/content-services/src/lib/search/search-query-builder.service.ts b/lib/content-services/src/lib/search/services/search-query-builder.service.ts similarity index 93% rename from lib/content-services/src/lib/search/search-query-builder.service.ts rename to lib/content-services/src/lib/search/services/search-query-builder.service.ts index a8b88a663a3..05133764940 100644 --- a/lib/content-services/src/lib/search/search-query-builder.service.ts +++ b/lib/content-services/src/lib/search/services/search-query-builder.service.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core'; -import { SearchConfiguration } from './models/search-configuration.interface'; +import { SearchConfiguration } from '../models/search-configuration.interface'; import { BaseQueryBuilderService } from './base-query-builder.service'; @Injectable() diff --git a/lib/content-services/src/lib/styles/_index.scss b/lib/content-services/src/lib/styles/_index.scss index 07007c9bae6..f8672855b65 100644 --- a/lib/content-services/src/lib/styles/_index.scss +++ b/lib/content-services/src/lib/styles/_index.scss @@ -28,6 +28,10 @@ @import '../aspect-list/aspect-list.component'; @import '../permission-manager/components/user-icon-column/user-icon-column.component'; @import '../permission-manager/components/user-name-column/user-name-column.component'; +@import '../search/components/search-filter-chips/search-filter-chips.component'; +@import '../search/components/search-facet-field/search-facet-field.component'; +@import '../search/components/search-form/search-form.component'; +@import '../search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component'; @mixin adf-content-services-theme($theme) { @include adf-breadcrumb-theme($theme); @@ -56,4 +60,8 @@ @include adf-version-comparison-theme($theme); @include adf-content-type-dialog-theme($theme); @include adf-aspect-list-theme($theme); + @include adf-search-filter-chips-theme($theme); + @include adf-search-filter-field-theme($theme); + @include adf-search-forms-theme($theme); + @include adf-search-filter-menu-card($theme); } diff --git a/lib/core/app-config/schema.json b/lib/core/app-config/schema.json index 2028c987bf0..44ed4df3024 100644 --- a/lib/core/app-config/schema.json +++ b/lib/core/app-config/schema.json @@ -499,6 +499,26 @@ } ] }, + "search-widget-setting": { + "description": "Search widget setting", + "type": "object", + "properties": { + "allowUpdateOnChange": { + "type": "boolean", + "default": true, + "description": "update search query with every user changes on widget" + }, + "hideDefaultAction": { + "type": "boolean", + "default": false, + "description": "Hides the widget action i.e clear and submit" + }, + "unit": { + "type": "string", + "description": "unit type of the widget value" + } + } + }, "search-configuration": { "description": "Search configuration parameters", "type": "object", @@ -589,6 +609,9 @@ }, "offset": { "type": "integer" + }, + "settings": { + "$ref": "#/definitions/search-widget-setting" } } } @@ -660,6 +683,9 @@ "mincount": { "type": "number", "description": "This specifies the minimum count required for a facet interval to be displayed. The default value is 1." + }, + "settings": { + "$ref": "#/definitions/search-widget-setting" } } } @@ -716,6 +742,9 @@ } } } + }, + "settings": { + "$ref": "#/definitions/search-widget-setting" } } }, @@ -750,8 +779,7 @@ "type": "string" }, "settings": { - "description": "Component-specific settings", - "type": "object" + "$ref": "#/definitions/search-widget-setting" } } } diff --git a/lib/testing/src/lib/protractor/content-services/pages/search/date-range-filter.page.ts b/lib/testing/src/lib/protractor/content-services/pages/search/date-range-filter.page.ts index cb8166e5c75..377a5ad212c 100644 --- a/lib/testing/src/lib/protractor/content-services/pages/search/date-range-filter.page.ts +++ b/lib/testing/src/lib/protractor/content-services/pages/search/date-range-filter.page.ts @@ -139,4 +139,5 @@ export class DateRangeFilterPage { async checkClearButtonIsDisplayed(): Promise { await BrowserVisibility.waitUntilElementIsVisible(this.filter.element(this.clearButton)); } + } diff --git a/scripts/build/build-core.sh b/scripts/build/build-core.sh index 1ba573fb9b7..8aa97040153 100755 --- a/scripts/build/build-core.sh +++ b/scripts/build/build-core.sh @@ -22,6 +22,9 @@ echo "====== Copy i18n ======" mkdir -p ./lib/dist/core/bundles/assets/adf-core/i18n cp -R ./lib/core/i18n/* ./lib/dist/core/bundles/assets/adf-core/i18n +echo "====== Copy schema ======" +cp -R ./lib/core/app-config/schema.json lib/dist/core/app.config.schema.json + echo "====== Copy assets ======" cp -R ./lib/core/assets/* ./lib/dist/core/bundles/assets