From 3b7f3a576257d4b2ca2040c702bafb745681f350 Mon Sep 17 00:00:00 2001 From: Vito Date: Mon, 20 Jul 2020 11:39:51 +0100 Subject: [PATCH] [ACA-3506] - Filter are kept when reloaded (#5885) * [ADF] - saving in the url the filter values * Fixed filter status on refresh * Fixed filter status on refresh * [ACA-3506] - added url filtering save * [ACA-3506] - fixed spellcheck * improve log * more log * fix scripts * Added documentation for allowUpdateOnChange setting * Added default value in description for docs Co-authored-by: Vito Albano Co-authored-by: Eugenio Romano --- demo-shell/src/app.config.json | 6 ++- .../app/components/files/files.component.html | 4 +- .../app/components/files/files.component.ts | 23 +++++++++ .../files/filtered-search.component.html | 3 +- .../files/filtered-search.component.ts | 7 +++ .../components/search-check-list.component.md | 2 + .../components/search-text.component.md | 4 +- .../search-check-list.component.html | 2 +- .../search-check-list.component.ts | 31 ++++++++++-- .../search-date-range.component.ts | 44 ++++++++++++++--- .../search-header.component.html | 5 +- .../search-header.component.spec.ts | 3 +- .../search-header/search-header.component.ts | 36 +++++++++++--- .../search-number-range.component.ts | 19 ++++++- .../search-radio.component.spec.ts | 17 ++++--- .../search-radio/search-radio.component.ts | 10 +++- .../search-slider/search-slider.component.ts | 15 ++++++ .../search-text/search-text.component.ts | 26 ++++++++-- .../search-widget-container.component.ts | 16 ++++++ ...earch-header-query-builder.service.spec.ts | 41 ++-------------- .../search-header-query-builder.service.ts | 49 ++++++++++--------- .../src/lib/search/search-widget.interface.ts | 3 ++ scripts/travis/build/build.sh | 2 +- scripts/travis/e2e/content-services-e2e.sh | 10 ++-- scripts/travis/e2e/core-e2e.sh | 10 +++- scripts/travis/e2e/insights-e2e.sh | 9 +++- .../travis/e2e/process-services-cloud-e2e.sh | 9 +++- scripts/travis/e2e/process-services-e2e.sh | 9 +++- scripts/travis/e2e/search-e2e.sh | 9 +++- scripts/travis/release/release-npm.sh | 2 +- scripts/travis/unit-test/content.sh | 2 +- scripts/travis/unit-test/core-extension.sh | 4 +- scripts/travis/unit-test/process.sh | 6 +-- 33 files changed, 316 insertions(+), 122 deletions(-) diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index 7e967686ff..19e574b21b 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -460,7 +460,8 @@ "pattern": "cm:name:'(.*?)'", "field": "cm:name", "placeholder": "SEARCH.SEARCH_HEADER.FILTERS.NAME.PLACEHOLDER", - "searchSuffix" : "*" + "searchSuffix" : "*", + "allowUpdateOnChange" : false } } }, @@ -473,6 +474,7 @@ "selector": "check-list", "settings": { "pageSize": 5, + "allowUpdateOnChange" : false, "operator": "OR", "options": [ { @@ -495,6 +497,7 @@ "component": { "selector": "check-list", "settings": { + "allowUpdateOnChange" : false, "options": [ { "name": "SEARCH.SEARCH_HEADER.FILTERS.SIZE.SMALL", @@ -524,6 +527,7 @@ "component": { "selector": "date-range", "settings": { + "allowUpdateOnChange" : false, "field": "cm:created", "dateFormat": "DD-MMM-YY", "maxDate": "today" diff --git a/demo-shell/src/app/components/files/files.component.html b/demo-shell/src/app/components/files/files.component.html index 6a7877a0fe..3b8079742f 100644 --- a/demo-shell/src/app/components/files/files.component.html +++ b/demo-shell/src/app/components/files/files.component.html @@ -240,11 +240,13 @@ + (clear)="onAllFilterCleared()" + (selection)="onFilterSelected($event)"> diff --git a/demo-shell/src/app/components/files/files.component.ts b/demo-shell/src/app/components/files/files.component.ts index 0614066625..3e6c3321b6 100644 --- a/demo-shell/src/app/components/files/files.component.ts +++ b/demo-shell/src/app/components/files/files.component.ts @@ -163,6 +163,9 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { @Input() enableCustomHeaderFilter = false; + @Input() + paramValues: Map = null; + @Output() documentListReady: EventEmitter = new EventEmitter(); @@ -655,7 +658,27 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { onAllFilterCleared() { this.documentList.node = null; + if (this.currentFolderId === '-my-') { + this.router.navigate([this.navigationRoute, '']); + } else { + this.router.navigate([this.navigationRoute, this.currentFolderId, 'display', this.displayMode]); + } this.documentList.reload(); } + onFilterSelected(currentActiveFilters: Map) { + const objectFromMap = {}; + currentActiveFilters.forEach((value: any, key) => { + let paramValue = null; + if (value && value.from && value.to) { + paramValue = `${value.from}||${value.to}`; + } else { + paramValue = value; + } + objectFromMap[key] = paramValue; + }); + + this.router.navigate([], { relativeTo: this.route, queryParams: objectFromMap }); + } + } diff --git a/demo-shell/src/app/components/files/filtered-search.component.html b/demo-shell/src/app/components/files/filtered-search.component.html index 56eaa79e5b..e45851c28f 100644 --- a/demo-shell/src/app/components/files/filtered-search.component.html +++ b/demo-shell/src/app/components/files/filtered-search.component.html @@ -4,5 +4,6 @@ [showSettingsPanel]="false" [navigationRoute]="navigationRoute" [currentFolderId]="currentFolderId" - [enableCustomHeaderFilter]="true"> + [enableCustomHeaderFilter]="true" + [paramValues]="queryParams"> diff --git a/demo-shell/src/app/components/files/filtered-search.component.ts b/demo-shell/src/app/components/files/filtered-search.component.ts index 70fb4e29f7..2806f99891 100644 --- a/demo-shell/src/app/components/files/filtered-search.component.ts +++ b/demo-shell/src/app/components/files/filtered-search.component.ts @@ -29,13 +29,20 @@ export class FilteredSearchComponent { navigationRoute = '/filtered-search'; currentFolderId = '-my-'; + queryParams = null; + constructor(@Optional() private route: ActivatedRoute) { + if (this.route) { this.route.params.forEach((params: Params) => { if (params['id'] && this.currentFolderId !== params['id']) { this.currentFolderId = params['id']; } }); + + this.route.queryParamMap.subscribe((queryMap: Params) => { + this.queryParams = queryMap.params; + }); } } diff --git a/docs/content-services/components/search-check-list.component.md b/docs/content-services/components/search-check-list.component.md index 237c881466..eef5bdc112 100644 --- a/docs/content-services/components/search-check-list.component.md +++ b/docs/content-services/components/search-check-list.component.md @@ -26,6 +26,7 @@ Implements a checklist [widget](../../../lib/testing/src/lib/core/pages/form/wid "pageSize": 5, "settings": { "operator": "OR", + "allowUpdateOnChange": true, "options": [ { "name": "Folder", "value": "TYPE:'cm:folder'" }, { "name": "Document", "value": "TYPE:'cm:content'" } @@ -44,6 +45,7 @@ Implements a checklist [widget](../../../lib/testing/src/lib/core/pages/form/wid | ---- | ---- | ----------- | | operator | `string` | Logical operator to combine query fragments. Can be 'AND' or 'OR'. | | options | `array` | Array of objects with `name` and `value` properties. Each object defines a checkbox, 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. ## Details diff --git a/docs/content-services/components/search-text.component.md b/docs/content-services/components/search-text.component.md index aa8e26489a..e950e323d1 100644 --- a/docs/content-services/components/search-text.component.md +++ b/docs/content-services/components/search-text.component.md @@ -29,7 +29,8 @@ Implements a text input [widget](../../../lib/testing/src/lib/core/pages/form/wi "searchSuffix": "", "pattern": "cm:name:'(.*?)'", "field": "cm:name", - "placeholder": "Enter the name" + "placeholder": "Enter the name", + "allowUpdateOnChange": true } } } @@ -47,6 +48,7 @@ Implements a text input [widget](../../../lib/testing/src/lib/core/pages/form/wi | placeholder | string | Text displayed in the [widget](../../../lib/testing/src/lib/core/pages/form/widgets/widget.ts) when the input string is empty | | 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. ## Details 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 85aabc4adb..c1c0c1ee9a 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 @@ -15,7 +15,7 @@ - +
diff --git a/lib/content-services/src/lib/search/components/search-header/search-header.component.spec.ts b/lib/content-services/src/lib/search/components/search-header/search-header.component.spec.ts index d37ccfd5d2..7e8222c670 100644 --- a/lib/content-services/src/lib/search/components/search-header/search-header.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-header/search-header.component.spec.ts @@ -173,7 +173,8 @@ describe('SearchHeaderComponent', () => { spyOn(queryBuilder, 'isNoFilterActive').and.returnValue(false); spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging)); spyOn(queryBuilder, 'buildQuery').and.returnValue({}); - spyOn(component.widgetContainer, 'resetInnerWidget').and.stub(); + queryBuilder.queryFragments['fake'] = 'test'; + spyOn(component.widgetContainer, 'resetInnerWidget').and.callThrough(); const fakeEvent = jasmine.createSpyObj('event', ['stopPropagation']); const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button'); component.update.subscribe((newNodePaging) => { diff --git a/lib/content-services/src/lib/search/components/search-header/search-header.component.ts b/lib/content-services/src/lib/search/components/search-header/search-header.component.ts index 6c0bcde183..88c974d716 100644 --- a/lib/content-services/src/lib/search/components/search-header/search-header.component.ts +++ b/lib/content-services/src/lib/search/components/search-header/search-header.component.ts @@ -33,7 +33,7 @@ import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cd 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 { NodePaging } from '@alfresco/js-api'; +import { NodePaging, MinimalNode } from '@alfresco/js-api'; import { SearchCategory } from '../../search-category.interface'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; import { Subject } from 'rxjs'; @@ -52,6 +52,9 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy { @Input() col: DataColumn; + @Input() + value: any; + /** The id of the current folder of the document list. */ @Input() currentFolderNodeId: string; @@ -72,6 +75,10 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy { @Output() clear: EventEmitter = new EventEmitter(); + /** Emitted when a filter value is selected */ + @Output() + selection: EventEmitter> = new EventEmitter(); + @ViewChild(SearchWidgetContainerComponent) widgetContainer: SearchWidgetContainerComponent; @@ -80,6 +87,7 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy { category: SearchCategory; isFilterServiceActive: boolean; + initialValue: any; focusTrap: ConfigurableFocusTrap; private onDestroy$ = new Subject(); @@ -100,11 +108,18 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy { .subscribe((newNodePaging: NodePaging) => { this.update.emit(newNodePaging); }); + + if (this.searchHeaderQueryBuilder.isCustomSourceNode(this.currentFolderNodeId)) { + this.searchHeaderQueryBuilder.getNodeIdForCustomSource(this.currentFolderNodeId).subscribe((node: MinimalNode) => { + this.initSearchHeader(node.id); + }); + } else { + this.initSearchHeader(this.currentFolderNodeId); + } } ngOnChanges(changes: SimpleChanges) { - if (changes['currentFolderNodeId'] && changes['currentFolderNodeId'].currentValue !== changes['currentFolderNodeId'].previousValue) { - this.searchHeaderQueryBuilder.setCurrentRootFolderId(changes['currentFolderNodeId'].currentValue); + if (changes['currentFolderNodeId'] && changes['currentFolderNodeId'].currentValue) { this.clearHeader(); } @@ -138,8 +153,8 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy { onApply() { if (this.widgetContainer.hasValueSelected()) { this.widgetContainer.applyInnerWidget(); - this.searchHeaderQueryBuilder.setActiveFilter(this.category.columnKey); - this.searchHeaderQueryBuilder.execute(); + this.searchHeaderQueryBuilder.setActiveFilter(this.category.columnKey, this.widgetContainer.getCurrentValue()); + this.selection.emit(this.searchHeaderQueryBuilder.getActiveFilters()); } else { this.clearHeader(); } @@ -154,10 +169,9 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy { if (this.widgetContainer) { this.widgetContainer.resetInnerWidget(); this.searchHeaderQueryBuilder.removeActiveFilter(this.category.columnKey); + this.selection.emit(this.searchHeaderQueryBuilder.getActiveFilters()); if (this.searchHeaderQueryBuilder.isNoFilterActive()) { this.clear.emit(); - } else { - this.searchHeaderQueryBuilder.execute(); } } } @@ -173,6 +187,14 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy { return this.widgetContainer && this.widgetContainer.componentRef && this.widgetContainer.componentRef.instance.isActive; } + private initSearchHeader(currentFolderId: string) { + this.searchHeaderQueryBuilder.setCurrentRootFolderId(currentFolderId); + if (this.value) { + this.searchHeaderQueryBuilder.setActiveFilter(this.category.columnKey, this.initialValue); + this.initialValue = this.value; + } + } + onMenuOpen() { if (this.filterContainer && !this.focusTrap) { this.focusTrap = this.focusTrapFactory.create(this.filterContainer.nativeElement); diff --git a/lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.ts b/lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.ts index dbc90b94c1..993ae92ef6 100644 --- a/lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.ts +++ b/lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.ts @@ -45,6 +45,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit { format = '[{FROM} TO {TO}]'; isActive = false; + startValue: any; validators: Validators; @@ -61,8 +62,13 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit { Validators.min(0) ]); - this.from = new FormControl('', this.validators); - this.to = new FormControl('', this.validators); + if (this.startValue) { + this.from = new FormControl(this.startValue['from'], this.validators); + this.to = new FormControl(this.startValue['to'], this.validators); + } else { + this.from = new FormControl('', this.validators); + this.to = new FormControl('', this.validators); + } this.form = new FormGroup({ from: this.from, @@ -108,6 +114,15 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit { return this.form.valid; } + getCurrentValue() { + return this.form.value; + } + + setValue(value: any) { + this.form['from'].setValue(value); + this.form['to'].setValue(value); + } + reset() { this.isActive = false; diff --git a/lib/content-services/src/lib/search/components/search-radio/search-radio.component.spec.ts b/lib/content-services/src/lib/search/components/search-radio/search-radio.component.spec.ts index 97e1578dfe..e942a6f43b 100644 --- a/lib/content-services/src/lib/search/components/search-radio/search-radio.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-radio/search-radio.component.spec.ts @@ -41,10 +41,10 @@ describe('SearchRadioComponent', () => { describe('Pagination', () => { it('should show 5 items when pageSize not defined', () => { - component.id = 'checklist'; + component.id = 'radio'; component.context = { queryFragments: { - 'checklist': 'query' + 'radio': 'query' }, update() {} }; @@ -60,10 +60,10 @@ describe('SearchRadioComponent', () => { }); it('should show all items when pageSize is high', () => { - component.id = 'checklist'; + component.id = 'radio'; component.context = { queryFragments: { - 'checklist': 'query' + 'radio': 'query' }, update() {} }; @@ -78,21 +78,22 @@ describe('SearchRadioComponent', () => { }); }); - it('should able to check the radio button', () => { - component.id = 'checklist'; + it('should able to check the radio button', async () => { + component.id = 'radio'; component.context = { queryFragments: { - 'checklist': 'query' + 'radio': 'query' }, update: () => {} }; component.settings = { options: sizeOptions }; spyOn(component.context, 'update').and.stub(); - component.ngOnInit(); fixture.detectChanges(); + await fixture.whenStable(); const optionElements = fixture.debugElement.query(By.css('mat-radio-button')); optionElements.triggerEventHandler('change', { checked: true }); + fixture.detectChanges(); expect(component.context.update).toHaveBeenCalled(); expect(component.context.queryFragments[component.id]).toBe(sizeOptions[0].value); diff --git a/lib/content-services/src/lib/search/components/search-radio/search-radio.component.ts b/lib/content-services/src/lib/search/components/search-radio/search-radio.component.ts index 277b27fb5a..30df70c983 100644 --- a/lib/content-services/src/lib/search/components/search-radio/search-radio.component.ts +++ b/lib/content-services/src/lib/search/components/search-radio/search-radio.component.ts @@ -47,6 +47,7 @@ export class SearchRadioComponent implements SearchWidget, OnInit { options: SearchFilterList; pageSize = 5; isActive = false; + startValue: any; constructor() { this.options = new SearchFilterList(); @@ -64,8 +65,11 @@ export class SearchRadioComponent implements SearchWidget, OnInit { } const initialValue = this.getSelectedValue(); + if (initialValue !== null) { this.setValue(initialValue); + } else if (this.startValue !== null) { + this.setValue(initialValue); } } @@ -93,12 +97,16 @@ export class SearchRadioComponent implements SearchWidget, OnInit { return !!currentValue; } - private setValue(newValue: string) { + setValue(newValue: string) { this.value = newValue; this.context.queryFragments[this.id] = newValue; this.context.update(); } + getCurrentValue() { + return this.getSelectedValue(); + } + changeHandler(event: MatRadioChange) { this.setValue(event.value); } 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 1debea4c42..a76397dc05 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 @@ -29,6 +29,8 @@ import { MatSliderChange } from '@angular/material/slider'; host: { class: 'adf-search-slider' } }) export class SearchSliderComponent implements SearchWidget, OnInit { + isActive?: boolean; + startValue: any; id: string; settings: SearchWidgetSettings; @@ -58,6 +60,10 @@ export class SearchSliderComponent implements SearchWidget, OnInit { this.thumbLabel = this.settings['thumbLabel'] ? true : false; } + + if (this.startValue) { + this.setValue(this.startValue); + } } reset() { @@ -78,6 +84,15 @@ export class SearchSliderComponent implements SearchWidget, OnInit { return !!this.value; } + getCurrentValue() { + return this.value; + } + + setValue(value: any) { + this.value = value; + this.submitValues(); + } + private updateQuery(value: number | null) { if (this.id && this.context && this.settings && this.settings.field) { if (value === null) { 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 99533efcf7..dda4c4d454 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 @@ -36,16 +36,26 @@ export class SearchTextComponent implements SearchWidget, OnInit { id: string; settings: SearchWidgetSettings; context: SearchQueryBuilderService; + startValue: string; isActive = false; + enableChangeUpdate = true; ngOnInit() { if (this.context && this.settings && this.settings.pattern) { const pattern = new RegExp(this.settings.pattern, 'g'); const match = pattern.exec(this.context.queryFragments[this.id] || ''); + if (this.settings.allowUpdateOnChange !== undefined && + this.settings.allowUpdateOnChange !== null) { + this.enableChangeUpdate = this.settings.allowUpdateOnChange; + } if (match && match.length > 1) { this.value = match[1]; } + + if (this.startValue) { + this.setValue(this.startValue); + } } } @@ -58,12 +68,13 @@ export class SearchTextComponent implements SearchWidget, OnInit { onChangedHandler(event) { this.value = event.target.value; - this.updateQuery(this.value); + this.isActive = !!this.value; + if (this.enableChangeUpdate) { + this.updateQuery(this.value); + } } private updateQuery(value: string) { - this.isActive = !!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(); @@ -79,6 +90,15 @@ export class SearchTextComponent implements SearchWidget, OnInit { return !!this.value; } + getCurrentValue() { + return this.value; + } + + setValue(value: string) { + this.value = value; + this.submitValues(); + } + private getSearchPrefix(): string { return this.settings.searchPrefix ? this.settings.searchPrefix : ''; } 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 446ab4e30f..984c8c71b3 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 @@ -41,6 +41,9 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy { @Input() config: any; + @Input() + value: any; + componentRef: ComponentRef; constructor( @@ -66,6 +69,10 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy { ref.instance.id = this.id; ref.instance.settings = { ...this.settings }; ref.instance.context = this.queryBuilder; + if (this.value) { + ref.instance.isActive = true; + ref.instance.startValue = this.value; + } } } @@ -80,10 +87,19 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy { this.componentRef.instance.submitValues(); } + setValue(currentValue: string | Object) { + this.componentRef.instance.isActive = true; + this.componentRef.instance.setValue(currentValue); + } + hasValueSelected() { return this.componentRef.instance.hasValidValue(); } + getCurrentValue() { + return this.componentRef.instance.getCurrentValue(); + } + resetInnerWidget() { if (this.componentRef && this.componentRef.instance) { this.componentRef.instance.reset(); diff --git a/lib/content-services/src/lib/search/search-header-query-builder.service.spec.ts b/lib/content-services/src/lib/search/search-header-query-builder.service.spec.ts index 662affa94e..c0f2dfdcab 100644 --- a/lib/content-services/src/lib/search/search-header-query-builder.service.spec.ts +++ b/lib/content-services/src/lib/search/search-header-query-builder.service.spec.ts @@ -136,39 +136,6 @@ describe('SearchHeaderQueryBuilder', () => { ); }); - it('should replace the new query filter for the old parent node with the new one', () => { - const expectedResult = [ - { query: 'PARENT:"workspace://SpacesStore/fake-next-node-id"' } - ]; - - const config: SearchConfiguration = { - categories: [ - { id: 'cat1', enabled: true }, - { id: 'cat2', enabled: true } - ], - filterQueries: [ - { query: 'PARENT:"workspace://SpacesStore/fake-node-id' } - ] - }; - - const searchHeaderService = new SearchHeaderQueryBuilderService( - buildConfig(config), - null, - null - ); - - searchHeaderService.currentParentFolderId = 'fake-node-id'; - - searchHeaderService.setCurrentRootFolderId( - 'fake-next-node-id' - ); - - expect(searchHeaderService.filterQueries).toEqual( - expectedResult, - 'Filters are not as expected' - ); - }); - it('should not add duplicate column names in activeFilters', () => { const activeFilter = 'FakeColumn'; @@ -187,11 +154,11 @@ describe('SearchHeaderQueryBuilder', () => { null ); - expect(searchHeaderService.activeFilters.length).toBe(0); + expect(searchHeaderService.activeFilters.size).toBe(0); - searchHeaderService.setActiveFilter(activeFilter); - searchHeaderService.setActiveFilter(activeFilter); + searchHeaderService.setActiveFilter(activeFilter, 'fake-value'); + searchHeaderService.setActiveFilter(activeFilter, 'fake-value'); - expect(searchHeaderService.activeFilters.length).toBe(1); + expect(searchHeaderService.activeFilters.size).toBe(1); }); }); diff --git a/lib/content-services/src/lib/search/search-header-query-builder.service.ts b/lib/content-services/src/lib/search/search-header-query-builder.service.ts index 7b85e69ae1..239a686c38 100644 --- a/lib/content-services/src/lib/search/search-header-query-builder.service.ts +++ b/lib/content-services/src/lib/search/search-header-query-builder.service.ts @@ -20,7 +20,9 @@ import { AlfrescoApiService, AppConfigService, NodesApiService } from '@alfresco import { SearchConfiguration } from './search-configuration.interface'; import { BaseQueryBuilderService } from './base-query-builder.service'; import { SearchCategory } from './search-category.interface'; -import { MinimalNode } from '@alfresco/js-api'; +import { MinimalNode, QueryBody } from '@alfresco/js-api'; +import { filter } from 'rxjs/operators'; +import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' @@ -29,11 +31,15 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService { private customSources = ['-trashcan-', '-sharedlinks-', '-sites-', '-mysites-', '-favorites-', '-recent-', '-my-']; - activeFilters: string[] = []; - currentParentFolderId: string; + activeFilters: Map = new Map(); constructor(appConfig: AppConfigService, alfrescoApiService: AlfrescoApiService, private nodeApiService: NodesApiService) { super(appConfig, alfrescoApiService); + + this.updated.pipe( + filter((query: QueryBody) => !!query)).subscribe(() => { + this.execute(); + }); } public isFilterServiceActive(): boolean { @@ -53,18 +59,22 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService { } } - setActiveFilter(columnActivated: string) { - if (!this.activeFilters.includes(columnActivated)) { - this.activeFilters.push(columnActivated); - } + setActiveFilter(columnActivated: string, filterValue: string) { + this.activeFilters.set(columnActivated, filterValue); + } + + getActiveFilters(): Map { + return this.activeFilters; } isNoFilterActive(): boolean { - return this.activeFilters.length === 0; + return this.activeFilters.size === 0; } removeActiveFilter(columnRemoved: string) { - this.activeFilters = this.activeFilters.filter((column) => column !== columnRemoved); + if (this.activeFilters.get(columnRemoved) !== null) { + this.activeFilters.delete(columnRemoved); + } } getCategoryForColumn(columnKey: string): SearchCategory { @@ -78,19 +88,6 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService { } setCurrentRootFolderId(currentFolderId: string) { - if (currentFolderId !== this.currentParentFolderId) { - if (this.customSources.includes(currentFolderId)) { - this.nodeApiService.getNode(currentFolderId).subscribe((nodeEntity: MinimalNode) => { - this.updateCurrentParentFilter(nodeEntity.id); - }); - } else { - this.currentParentFolderId = currentFolderId; - this.updateCurrentParentFilter(currentFolderId); - } - } - } - - private updateCurrentParentFilter(currentFolderId: string) { const alreadyAddedFilter = this.filterQueries.find(filterQueries => filterQueries.query.includes(currentFolderId) ); @@ -104,4 +101,12 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService { }]; } + isCustomSourceNode(currentNodeId: string): boolean { + return this.customSources.includes(currentNodeId); + } + + getNodeIdForCustomSource(customSourceId: string): Observable { + return this.nodeApiService.getNode(customSourceId); + } + } diff --git a/lib/content-services/src/lib/search/search-widget.interface.ts b/lib/content-services/src/lib/search/search-widget.interface.ts index 59933e9f27..6ec816e522 100644 --- a/lib/content-services/src/lib/search/search-widget.interface.ts +++ b/lib/content-services/src/lib/search/search-widget.interface.ts @@ -23,7 +23,10 @@ export interface SearchWidget { settings?: SearchWidgetSettings; context?: SearchQueryBuilderService; isActive?: boolean; + startValue: any; reset(); submitValues(); hasValidValue(); + getCurrentValue(); + setValue(value: any); } diff --git a/scripts/travis/build/build.sh b/scripts/travis/build/build.sh index 40586e6cb8..429d14a3f5 100755 --- a/scripts/travis/build/build.sh +++ b/scripts/travis/build/build.sh @@ -8,7 +8,7 @@ rm -rf tmp && mkdir tmp; npx @alfresco/adf-cli@alpha update-commit-sha --pointer "HEAD" --pathPackage "$(pwd)" -if [[ $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ "${TRAVIS_EVENT_TYPE}" == "push" ]]; then if [[ $TRAVIS_BRANCH == "develop" ]]; then diff --git a/scripts/travis/e2e/content-services-e2e.sh b/scripts/travis/e2e/content-services-e2e.sh index f3c92dcef4..dfd7ef7e60 100755 --- a/scripts/travis/e2e/content-services-e2e.sh +++ b/scripts/travis/e2e/content-services-e2e.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +echo "Start Content service e2e" + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $DIR/../../../ @@ -8,18 +10,20 @@ export CONTEXT_ENV="content-services" export PROVIDER='ECM' export AUTH_TYPE='BASIC' - -if [[ $TRAVIS_PULL_REQUEST == "true" ]]; then +if [ "${TRAVIS_EVENT_TYPE}" == "pull_request" ]; then + echo "Calculate affected e2e $BASE_HASH $HEAD_HASH" ./scripts/git-util/check-branch-updated.sh -b $TRAVIS_BRANCH || exit 1; AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" + echo "Affected libs ${AFFECTED_LIBS}" AFFECTED_E2E="$(./scripts/git-util/affected-folder.sh -b $TRAVIS_BRANCH -f "e2e/$CONTEXT_ENV")"; + echo "Affected e2e ${AFFECTED_E2E}" fi; ./node_modules/@alfresco/adf-cli/bin/adf-cli check-cs-env --host "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" || exit 1 #-b is needed to run the Folder upload test that are not workin in Headless chrome RUN_E2E=$(echo ./scripts/test-e2e-lib.sh -host http://localhost:4200 -proxy "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" -e "$E2E_EMAIL" --use-dist -b -save -m 4 || exit 1) -if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "$CONTEXT_ENV" || $TRAVIS_PULL_REQUEST == "false" ]]; then +if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "$CONTEXT_ENV" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then $RUN_CHECK $RUN_E2E --folder $CONTEXT_ENV else if [[ $AFFECTED_E2E = "e2e/$CONTEXT_ENV" ]]; diff --git a/scripts/travis/e2e/core-e2e.sh b/scripts/travis/e2e/core-e2e.sh index 0ae8908ef8..427dbe2039 100755 --- a/scripts/travis/e2e/core-e2e.sh +++ b/scripts/travis/e2e/core-e2e.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +echo "Start Core e2e" +echo "Start Core e2e $TRAVIS_PULL_REQUEST" + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $DIR/../../../ @@ -8,17 +11,20 @@ export CONTEXT_ENV="core" export PROVIDER='ALL' export AUTH_TYPE='BASIC' -if [[ $TRAVIS_PULL_REQUEST == "true" ]]; then +if [ "${TRAVIS_EVENT_TYPE}" == "pull_request" ]; then + echo "Calculate affected e2e $BASE_HASH $HEAD_HASH" ./scripts/git-util/check-branch-updated.sh -b $TRAVIS_BRANCH || exit 1; AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" + echo "Affected libs ${AFFECTED_LIBS}" AFFECTED_E2E="$(./scripts/git-util/affected-folder.sh -b $TRAVIS_BRANCH -f "e2e/$CONTEXT_ENV")"; + echo "Affected e2e ${AFFECTED_E2E}" fi; ./node_modules/@alfresco/adf-cli/bin/adf-cli check-ps-env --host "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" || exit 1 ./node_modules/@alfresco/adf-cli/bin/adf-cli check-cs-env --host "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" || exit 1 RUN_E2E=$(echo ./scripts/test-e2e-lib.sh -host http://localhost:4200 -proxy "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" -e "$E2E_EMAIL" --use-dist -m 2 || exit 1) -if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "$CONTEXT_ENV" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "$CONTEXT_ENV" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then $RUN_E2E --folder $CONTEXT_ENV || exit 1 else if [[ $AFFECTED_E2E = "e2e/$CONTEXT_ENV" ]]; diff --git a/scripts/travis/e2e/insights-e2e.sh b/scripts/travis/e2e/insights-e2e.sh index ed66759001..1b34eb7142 100755 --- a/scripts/travis/e2e/insights-e2e.sh +++ b/scripts/travis/e2e/insights-e2e.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +echo "Start insight e2e" + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export PROVIDER='BPM' @@ -7,12 +9,15 @@ export AUTH_TYPE='BASIC' cd $DIR/../../../ -if [[ $TRAVIS_PULL_REQUEST == "true" ]]; then +if [ "${TRAVIS_EVENT_TYPE}" == "pull_request" ]; then + echo "Calculate affected e2e $BASE_HASH $HEAD_HASH" ./scripts/git-util/check-branch-updated.sh -b $TRAVIS_BRANCH || exit 1; + echo "Affected libs ${AFFECTED_LIBS}" AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" + echo "Affected e2e ${AFFECTED_E2E}" fi; -if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "insight" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "insight" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then ./node_modules/@alfresco/adf-cli/bin/adf-cli check-ps-env --host "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" || exit 1; ./scripts/test-e2e-lib.sh -host http://localhost:4200 -proxy "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" -e "$E2E_EMAIL" --folder insights --use-dist || exit 1; diff --git a/scripts/travis/e2e/process-services-cloud-e2e.sh b/scripts/travis/e2e/process-services-cloud-e2e.sh index 9a552acd9b..1da81c6282 100755 --- a/scripts/travis/e2e/process-services-cloud-e2e.sh +++ b/scripts/travis/e2e/process-services-cloud-e2e.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +echo "Start process services cloud e2e" + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $DIR/../../../ @@ -8,10 +10,13 @@ export CONTEXT_ENV="process-services-cloud" export PROVIDER="ALL" export AUTH_TYPE="OAUTH" -if [[ $TRAVIS_PULL_REQUEST == "true" ]]; then +if [ "${TRAVIS_EVENT_TYPE}" == "pull_request" ]; then + echo "Calculate affected e2e $BASE_HASH $HEAD_HASH" ./scripts/git-util/check-branch-updated.sh -b $TRAVIS_BRANCH || exit 1; AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" + echo "Affected libs ${AFFECTED_LIBS}" AFFECTED_E2E="$(./scripts/git-util/affected-folder.sh -b $TRAVIS_BRANCH -f "e2e/$CONTEXT_ENV")"; + echo "Affected e2e ${AFFECTED_E2E}" fi; RUN_E2E=$(echo ./scripts/test-e2e-lib.sh -host http://localhost:4200 -proxy "$E2E_HOST_BPM" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" -e "$E2E_EMAIL" -host_sso "$E2E_HOST_SSO" -identity_admin_email "$E2E_ADMIN_EMAIL_IDENTITY" -identity_admin_password "$E2E_ADMIN_PASSWORD_IDENTITY" -prefix $TRAVIS_BUILD_NUMBER --use-dist -m 2 -save -b ) @@ -22,7 +27,7 @@ check_env(){ ./node_modules/@alfresco/adf-cli/bin/adf-cli check-cs-env --host "$E2E_HOST_BPM" -u "$E2E_ADMIN_EMAIL_IDENTITY" -p "$E2E_ADMIN_PASSWORD_IDENTITY" || exit 1 } -if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "$CONTEXT_ENV" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "$CONTEXT_ENV" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then echo "Case 1 - adf-testing has been changed"; check_env; diff --git a/scripts/travis/e2e/process-services-e2e.sh b/scripts/travis/e2e/process-services-e2e.sh index c136987a6e..fb549621ca 100755 --- a/scripts/travis/e2e/process-services-e2e.sh +++ b/scripts/travis/e2e/process-services-e2e.sh @@ -8,16 +8,21 @@ export CONTEXT_ENV="process-services" export PROVIDER='BPM' export AUTH_TYPE='BASIC' -if [[ $TRAVIS_PULL_REQUEST == "true" ]]; then +echo "Start process services e2e" + +if [ "${TRAVIS_EVENT_TYPE}" == "pull_request" ]; then + echo "Calculate affected e2e $BASE_HASH $HEAD_HASH" ./scripts/git-util/check-branch-updated.sh -b $TRAVIS_BRANCH || exit 1; AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" + echo "Affected libs ${AFFECTED_LIBS}" AFFECTED_E2E="$(./scripts/git-util/affected-folder.sh -b $TRAVIS_BRANCH -f "e2e/$CONTEXT_ENV")"; + echo "Affected e2e ${AFFECTED_E2E}" fi; ./node_modules/@alfresco/adf-cli/bin/adf-cli check-ps-env --host "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" || exit 1 RUN_E2E=$(echo ./scripts/test-e2e-lib.sh -host http://localhost:4200 -proxy "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" -e "$E2E_EMAIL" --use-dist -m 2 || exit 1) -if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "$CONTEXT_ENV" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "$CONTEXT_ENV" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then $RUN_E2E --folder $CONTEXT_ENV else if [[ $AFFECTED_E2E = "e2e/$CONTEXT_ENV" ]]; diff --git a/scripts/travis/e2e/search-e2e.sh b/scripts/travis/e2e/search-e2e.sh index 3e32e92ef0..e2450f8849 100755 --- a/scripts/travis/e2e/search-e2e.sh +++ b/scripts/travis/e2e/search-e2e.sh @@ -2,22 +2,27 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +echo "Start search e2e" + cd $DIR/../../../ export CONTEXT_ENV="search" export PROVIDER='ECM' export AUTH_TYPE='BASIC' -if [[ $TRAVIS_PULL_REQUEST == "true" ]]; then +if [ "${TRAVIS_EVENT_TYPE}" == "pull_request" ];then + echo "Calculate affected e2e $BASE_HASH $HEAD_HASH" ./scripts/git-util/check-branch-updated.sh -b $TRAVIS_BRANCH || exit 1; AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" + echo "Affected libs ${AFFECTED_LIBS}" AFFECTED_E2E="$(./scripts/git-util/affected-folder.sh -b $TRAVIS_BRANCH -f "e2e/$CONTEXT_ENV")"; + echo "Affected e2e ${AFFECTED_E2E}" fi; ./node_modules/@alfresco/adf-cli/bin/adf-cli check-cs-env --host "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" || exit 1 RUN_E2E=$(echo ./scripts/test-e2e-lib.sh -host http://localhost:4200 -proxy "$E2E_HOST" -u "$E2E_USERNAME" -p "$E2E_PASSWORD" -e "$E2E_EMAIL" --use-dist -m 2 || exit 1) -if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "content-services" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "testing" || $AFFECTED_LIBS =~ "content-services" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then $RUN_E2E --folder $CONTEXT_ENV else if [[ $AFFECTED_E2E = "e2e/$CONTEXT_ENV" ]]; diff --git a/scripts/travis/release/release-npm.sh b/scripts/travis/release/release-npm.sh index 910ba5f82a..8f4974d113 100755 --- a/scripts/travis/release/release-npm.sh +++ b/scripts/travis/release/release-npm.sh @@ -4,7 +4,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $DIR/../../../ -if [[ $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ "${TRAVIS_EVENT_TYPE}" == "push" ]]; then TAG_NPM=latest if [[ $TRAVIS_BRANCH == "develop" ]]; diff --git a/scripts/travis/unit-test/content.sh b/scripts/travis/unit-test/content.sh index 655ac10456..e49e6c57d0 100755 --- a/scripts/travis/unit-test/content.sh +++ b/scripts/travis/unit-test/content.sh @@ -9,7 +9,7 @@ ng test content-services --watch=false || exit 1; # echo "================== content-services unit ===================" AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" -if [[ $AFFECTED_LIBS =~ "content-services" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "content-services" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then ng test content-services --watch=false || exit 1; fi; diff --git a/scripts/travis/unit-test/core-extension.sh b/scripts/travis/unit-test/core-extension.sh index fb6f2e49b8..bdb40d3a6b 100755 --- a/scripts/travis/unit-test/core-extension.sh +++ b/scripts/travis/unit-test/core-extension.sh @@ -9,14 +9,14 @@ AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" echo "================== core unit ===================" -if [[ $AFFECTED_LIBS =~ "core" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "core" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then ng test core --watch=false || exit 1; fi; echo "================== extensions unit ===================" -if [[ $AFFECTED_LIBS =~ "extensions" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "extensions" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then ng test extensions --watch=false || exit 1; fi; diff --git a/scripts/travis/unit-test/process.sh b/scripts/travis/unit-test/process.sh index 1dcd5336b6..4ac22eb37a 100755 --- a/scripts/travis/unit-test/process.sh +++ b/scripts/travis/unit-test/process.sh @@ -10,19 +10,19 @@ cd $DIR/../../../ AFFECTED_LIBS="$(nx affected:libs --base=$BASE_HASH --head=$HEAD_HASH --plain)" echo "================== process-services unit ===================" -if [[ $AFFECTED_LIBS =~ "process-services" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "process-services" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then ng test process-services --watch=false || exit 1; fi; echo "================== insights unit ===================" -if [[ $AFFECTED_LIBS =~ "insights" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "insights" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then ng test insights --watch=false || exit 1; fi; echo "================== process-services-cloud unit ===================" -if [[ $AFFECTED_LIBS =~ "process-services-cloud" || $TRAVIS_PULL_REQUEST == "false" ]]; +if [[ $AFFECTED_LIBS =~ "process-services-cloud" || "${TRAVIS_EVENT_TYPE}" == "push" ]]; then ng test process-services-cloud --watch=false || exit 1; fi;