diff --git a/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts b/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts index 859a4d68ea..e8f062f47e 100644 --- a/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts +++ b/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts @@ -1276,9 +1276,25 @@ describe('DocumentList', () => { documentList.onNodeDblClick(node); }); - it('should load folder by ID on init', async () => { + it('should load folder by ID on init if no filterValue is provided', async () => { spyOn(documentList, 'loadFolder').and.stub(); + documentList.filterValue = {}; + + fixture.detectChanges(); + + documentList.ngOnChanges({ currentFolderId: new SimpleChange(undefined, '1d26e465-dea3-42f3-b415-faa8364b9692', true) }); + + await fixture.whenStable(); + + expect(documentList.loadFolder).not.toHaveBeenCalled(); + }); + + it('should NOT load folder by ID on init if filterValue is provided', async () => { + spyOn(documentList, 'loadFolder').and.stub(); + + documentList.filterValue = undefined; + fixture.detectChanges(); documentList.ngOnChanges({ currentFolderId: new SimpleChange(undefined, '1d26e465-dea3-42f3-b415-faa8364b9692', true) }); diff --git a/lib/content-services/src/lib/document-list/components/document-list.component.ts b/lib/content-services/src/lib/document-list/components/document-list.component.ts index 8901fb6d23..a3c2f49b37 100644 --- a/lib/content-services/src/lib/document-list/components/document-list.component.ts +++ b/lib/content-services/src/lib/document-list/components/document-list.component.ts @@ -599,7 +599,7 @@ export class DocumentListComponent extends DataTableSchema implements OnInit, On } if (this.currentFolderId && changes['currentFolderId']?.currentValue !== changes['currentFolderId']?.previousValue) { - this.loadFolder(); + !this.filterValue && this.loadFolder(); } if (this.data) { @@ -816,7 +816,6 @@ export class DocumentListComponent extends DataTableSchema implements OnInit, On this.preserveExistingSelection(); } this.onPreselectNodes(); - this.setLoadingState(false); this.onDataReady(nodePaging); } } @@ -1023,6 +1022,7 @@ export class DocumentListComponent extends DataTableSchema implements OnInit, On private onDataReady(nodePaging: NodePaging) { this.ready.emit(nodePaging); this.pagination.next(nodePaging.list.pagination); + this.setLoadingState(false); } updatePagination(requestPaginationModel: RequestPaginationModel) { 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 49be25fdc9..e5c1322bb3 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 @@ -109,7 +109,35 @@ describe('FilterHeaderComponent', () => { expect(setCurrentRootFolderIdSpy).toHaveBeenCalled(); }); - it('should set active filters when an initial value is set', async () => { + it('should set filters if initial value is provided', async () => { + spyOn(queryBuilder, 'setCurrentRootFolderId'); + spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false); + spyOn(queryBuilder, 'setActiveFilter'); + + component.value = { name: 'pinocchio' }; + const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true); + component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(queryBuilder.setActiveFilter).toHaveBeenCalledWith('name', 'pinocchio'); + }); + + it('should NOT set filters if initial value is not provided', async () => { + spyOn(queryBuilder, 'setCurrentRootFolderId'); + spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false); + spyOn(queryBuilder, 'setActiveFilter'); + + component.value = undefined; + const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true); + component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(queryBuilder.setActiveFilter).not.toHaveBeenCalled(); + }); + + it('should set active filters correctly', async () => { spyOn(queryBuilder, 'setCurrentRootFolderId'); spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false); @@ -117,8 +145,7 @@ describe('FilterHeaderComponent', () => { await fixture.whenStable(); expect(queryBuilder.getActiveFilters().length).toBe(0); - const initialFilterValue = { name: 'pinocchio' }; - component.value = initialFilterValue; + component.value = { name: 'pinocchio' }; const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true); component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange }); fixture.detectChanges(); @@ -129,6 +156,25 @@ describe('FilterHeaderComponent', () => { expect(queryBuilder.getActiveFilters()[0].value).toBe('pinocchio'); }); + it('should update queryParams if initial value is provided', async () => { + spyOn(queryBuilder, 'setCurrentRootFolderId'); + spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false); + + fixture.detectChanges(); + await fixture.whenStable(); + expect(Object.keys(queryBuilder.filterRawParams).length).toBe(0); + + component.value = { name: 'pinocchio' }; + const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true); + component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(Object.keys(queryBuilder.filterRawParams).length).toBe(1); + expect(queryBuilder.filterRawParams['name']).toBe('pinocchio'); + expect(queryBuilder.queryFragments['name']).toBe('pinocchio'); + }); + it('should emit filterSelection when a filter is changed', (done) => { spyOn(queryBuilder, 'getActiveFilters').and.returnValue([{ key: 'name', value: 'pinocchio' }]); 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 c168652882..c118db40a1 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 @@ -46,7 +46,10 @@ export class FilterHeaderComponent implements OnInit, OnChanges { private readonly destroyRef = inject(DestroyRef); - constructor(@Inject(ADF_DOCUMENT_PARENT_COMPONENT) private documentList: any, private searchFilterQueryBuilder: SearchHeaderQueryBuilderService) { + constructor( + @Inject(ADF_DOCUMENT_PARENT_COMPONENT) private readonly documentList: any, + private readonly searchFilterQueryBuilder: SearchHeaderQueryBuilderService + ) { this.isFilterServiceActive = this.searchFilterQueryBuilder.isFilterServiceActive(); } @@ -102,11 +105,17 @@ export class FilterHeaderComponent implements OnInit, OnChanges { } private initSearchHeader(currentFolderId: string) { - this.searchFilterQueryBuilder.setCurrentRootFolderId(currentFolderId); if (this.value) { - Object.keys(this.value).forEach((columnKey) => { - this.searchFilterQueryBuilder.setActiveFilter(columnKey, this.value[columnKey]); + Object.keys(this.value).forEach((key) => { + this.searchFilterQueryBuilder.setActiveFilter(key, this.value[key]); + + const operator = this.searchFilterQueryBuilder.getOperatorForFilterId(key) || 'OR'; + this.searchFilterQueryBuilder.filterRawParams[key] = this.value[key]; + this.searchFilterQueryBuilder.queryFragments[key] = Array.isArray(this.value[key]) + ? this.value[key].join(` ${operator} `) + : this.value[key]; }); } + this.searchFilterQueryBuilder.setCurrentRootFolderId(currentFolderId); } } diff --git a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.spec.ts b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.spec.ts index 950d03d648..bdc9a461af 100644 --- a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.spec.ts @@ -203,18 +203,17 @@ describe('SearchCheckListComponent', () => { expect(checkedElements.length).toBe(0); }); - it('should update query with startValue on init, if provided', () => { + it('should check the checkbox with startValue on init, if provided', () => { component.id = 'checkList'; component.options = new SearchFilterList([ { name: 'Folder', value: `TYPE:'cm:folder'`, checked: false }, { name: 'Document', value: `TYPE:'cm:content'`, checked: false } ]); - component.startValue = `TYPE:'cm:folder'`; - component.context.queryFragments[component.id] = 'query'; + component.startValue = [`TYPE:'cm:folder'`]; fixture.detectChanges(); - - expect(component.context.queryFragments[component.id]).toBe(`TYPE:'cm:folder'`); - expect(component.context.update).toHaveBeenCalled(); + expect(component.options.items[0].checked).toBeTrue(); + expect(component.options.items[1].checked).toBeFalse(); + expect(component.isActive).toBeTrue(); }); it('should set query context as blank and not call query update, if no start value was provided', () => { @@ -231,6 +230,25 @@ describe('SearchCheckListComponent', () => { expect(component.context.update).not.toHaveBeenCalled(); }); + it('should handle initial populateFilters emission and no filter state properly', () => { + component.id = 'checkList'; + component.options = new SearchFilterList([ + { name: 'Folder', value: `TYPE:'cm:folder'`, checked: false }, + { name: 'Document', value: `TYPE:'cm:content'`, checked: false } + ]); + + component.context.filterLoaded = new ReplaySubject(1); + spyOn(component.context.filterLoaded, 'next').and.stub(); + spyOn(component.displayValue$, 'next').and.stub(); + fixture.detectChanges(); + + component.context.populateFilters.next({}); + component.context.populateFilters.next({ checkList: [`TYPE:'cm:content'`] }); + fixture.detectChanges(); + + expect(component.context.filterLoaded.next).toHaveBeenCalledTimes(1); + }); + it('should populate filter state when populate filters event has been observed', () => { component.id = 'checkList'; component.options = new SearchFilterList([ diff --git a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.ts b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.ts index 04f3a06a24..1eccfb0e47 100644 --- a/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.ts +++ b/lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.ts @@ -23,7 +23,7 @@ import { SearchQueryBuilderService } from '../../services/search-query-builder.s import { SearchFilterList } from '../../models/search-filter-list.model'; import { TranslationService } from '@alfresco/adf-core'; import { ReplaySubject } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; import { MatButtonModule } from '@angular/material/button'; @@ -50,7 +50,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit { context?: SearchQueryBuilderService; options: SearchFilterList; operator: string = 'OR'; - startValue: string; + startValue: string | string[]; pageSize = 5; isActive = false; enableChangeUpdate = true; @@ -81,9 +81,9 @@ export class SearchCheckListComponent implements SearchWidget, OnInit { } } this.context.populateFilters - .asObservable() .pipe( map((filtersQueries) => filtersQueries[this.id]), + filter((filterQuery) => filterQuery !== undefined), takeUntilDestroyed(this.destroyRef) ) .subscribe((filterQuery) => { @@ -160,8 +160,8 @@ export class SearchCheckListComponent implements SearchWidget, OnInit { } setValue(value: any) { - this.options.items.filter((item) => value.includes(item.value)).map((item) => (item.checked = true)); - this.submitValues(); + this.options.items.forEach((item) => (item.checked = value.includes(item.value))); + this.isActive = true; } private getCheckedValues() { 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 ec13684521..82e540f36d 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 @@ -94,7 +94,7 @@ describe('SearchFilterContainerComponent', () => { await applyButton.click(); expect(queryBuilder.getActiveFilters().length).toBe(1); - expect(queryBuilder.getActiveFilters()[0].key).toBe('name'); + expect(queryBuilder.getActiveFilters()[0].key).toBe('queryName'); expect(queryBuilder.getActiveFilters()[0].value).toBe('searchText'); await menu.open(); @@ -103,12 +103,12 @@ describe('SearchFilterContainerComponent', () => { await applyButton.click(); expect(queryBuilder.getActiveFilters().length).toBe(1); - expect(queryBuilder.getActiveFilters()[0].key).toBe('name'); + expect(queryBuilder.getActiveFilters()[0].key).toBe('queryName'); expect(queryBuilder.getActiveFilters()[0].value).toBe('updated text'); }); it('should remove active filter after the Clear button is clicked', async () => { - queryBuilder.setActiveFilter('name', 'searchText'); + queryBuilder.setActiveFilter('queryName', 'searchText'); const menu = await loader.getHarness(MatMenuHarness); await menu.open(); 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 2b01ba350a..d9349d98b6 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 @@ -76,7 +76,7 @@ export class SearchFilterContainerComponent implements OnInit { ngOnInit() { this.category = this.searchFilterQueryBuilder.getCategoryForColumn(this.col.key); - this.initialValue = this.value?.[this.col.key] ? this.value[this.col.key] : undefined; + this.initialValue = this.value?.[this.category?.id]; } onKeyPressed(event: KeyboardEvent, menuTrigger: MatMenuTrigger) { @@ -88,7 +88,7 @@ export class SearchFilterContainerComponent implements OnInit { onApply() { if (this.widgetContainer.hasValueSelected()) { - this.searchFilterQueryBuilder.setActiveFilter(this.category.columnKey, this.widgetContainer.getCurrentValue()); + this.searchFilterQueryBuilder.setActiveFilter(this.category.id, this.widgetContainer.getCurrentValue()); this.filterChange.emit(); this.widgetContainer.applyInnerWidget(); } else { @@ -103,7 +103,7 @@ export class SearchFilterContainerComponent implements OnInit { resetSearchFilter() { this.widgetContainer.resetInnerWidget(); - this.searchFilterQueryBuilder.removeActiveFilter(this.category.columnKey); + this.searchFilterQueryBuilder.removeActiveFilter(this.category.id); this.filterChange.emit(); } @@ -115,7 +115,7 @@ export class SearchFilterContainerComponent implements OnInit { } isActive(): boolean { - return this.searchFilterQueryBuilder.getActiveFilters().findIndex((f: FilterSearch) => f.key === this.category.columnKey) > -1; + return this.searchFilterQueryBuilder.getActiveFilters().findIndex((f: FilterSearch) => f.key === this.category.id) > -1; } onMenuOpen() { diff --git a/lib/content-services/src/lib/search/components/search-text/search-text.component.spec.ts b/lib/content-services/src/lib/search/components/search-text/search-text.component.spec.ts index 8813c8f65d..326d06f87e 100644 --- a/lib/content-services/src/lib/search/components/search-text/search-text.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-text/search-text.component.spec.ts @@ -123,7 +123,7 @@ describe('SearchTextComponent', () => { expect(component.value).toBe(''); expect(component.context.queryFragments[component.id]).toBe(''); - expect(component.context.filterRawParams[component.id]).toBeNull(); + expect(component.context.filterRawParams[component.id]).toBeUndefined(); }); it('should update query with startValue on init, if provided', () => { 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 f8dcb41b83..40b05fba20 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 @@ -99,6 +99,8 @@ export class SearchTextComponent implements SearchWidget, OnInit { reset(updateContext = true) { this.value = ''; + this.context.filterRawParams[this.id] = undefined; + this.context.queryFragments[this.id] = ''; this.updateQuery(null, updateContext); } @@ -111,7 +113,10 @@ export class SearchTextComponent implements SearchWidget, OnInit { } private updateQuery(value: string, updateContext = true) { - this.context.filterRawParams[this.id] = value; + if (value !== null) { + this.context.filterRawParams[this.id] = value; + } + 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()}'` : ''; diff --git a/lib/content-services/src/lib/search/services/search-header-query-builder.service.spec.ts b/lib/content-services/src/lib/search/services/search-header-query-builder.service.spec.ts index 49cf94bc8c..567ccbfeea 100644 --- a/lib/content-services/src/lib/search/services/search-header-query-builder.service.spec.ts +++ b/lib/content-services/src/lib/search/services/search-header-query-builder.service.spec.ts @@ -22,6 +22,7 @@ import { TestBed } from '@angular/core/testing'; import { ContentTestingModule } from '../../testing/content.testing.module'; import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { ActivatedRoute, Router } from '@angular/router'; +import { SearchCategory } from '../models'; describe('SearchHeaderQueryBuilderService', () => { let activatedRoute: ActivatedRoute; @@ -43,10 +44,13 @@ describe('SearchHeaderQueryBuilderService', () => { it('should load the configuration from app config', () => { TestBed.runInInjectionContext(() => { - const config: SearchConfiguration = { - categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any], + const config = { + categories: [ + { id: 'cat1', enabled: true }, + { id: 'cat2', enabled: true } + ], filterQueries: [{ query: 'query1' }, { query: 'query2' }] - }; + } as SearchConfiguration; const alfrescoApiService = TestBed.inject(AlfrescoApiService); const builder = new SearchHeaderQueryBuilderService(buildConfig(config), alfrescoApiService, null); @@ -66,13 +70,13 @@ describe('SearchHeaderQueryBuilderService', () => { it('should return the category assigned to a column key', () => { TestBed.runInInjectionContext(() => { - const config: SearchConfiguration = { + const config = { categories: [ - { id: 'cat1', columnKey: 'fake-key-1', enabled: true } as any, - { id: 'cat2', columnKey: 'fake-key-2', enabled: true } as any + { id: 'cat1', columnKey: 'fake-key-1', enabled: true }, + { id: 'cat2', columnKey: 'fake-key-2', enabled: true } ], filterQueries: [{ query: 'query1' }, { query: 'query2' }] - }; + } as SearchConfiguration; const alfrescoApiService = TestBed.inject(AlfrescoApiService); const service = new SearchHeaderQueryBuilderService(buildConfig(config), alfrescoApiService, null); @@ -84,6 +88,25 @@ describe('SearchHeaderQueryBuilderService', () => { }); }); + it('should return operator for a category by id', () => { + TestBed.runInInjectionContext(() => { + const config: SearchConfiguration = { + categories: [ + { id: 'cat1', columnKey: 'fake-key-1', enabled: true, component: { settings: { operator: 'operator' } } }, + { id: 'cat2', columnKey: 'fake-key-2', enabled: true } + ] as SearchCategory[] + }; + + const alfrescoApiService = TestBed.inject(AlfrescoApiService); + const service = new SearchHeaderQueryBuilderService(buildConfig(config), alfrescoApiService, null); + + const operator = service.getOperatorForFilterId('cat1'); + expect(operator).toBe('operator'); + const operator1 = service.getOperatorForFilterId('cat2'); + expect(operator1).toBeUndefined(); + }); + }); + it('should have empty user query by default', () => { TestBed.runInInjectionContext(() => { const alfrescoApiService = TestBed.inject(AlfrescoApiService); @@ -94,10 +117,13 @@ describe('SearchHeaderQueryBuilderService', () => { it('should add the extra filter for the parent node', () => { TestBed.runInInjectionContext(() => { - const config: SearchConfiguration = { - categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any], + const config = { + categories: [ + { id: 'cat1', enabled: true }, + { id: 'cat2', enabled: true } + ], filterQueries: [{ query: 'query1' }, { query: 'query2' }] - }; + } as SearchConfiguration; const expectedResult = [{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }]; @@ -114,10 +140,13 @@ describe('SearchHeaderQueryBuilderService', () => { TestBed.runInInjectionContext(() => { const expectedResult = [{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }]; - const config: SearchConfiguration = { - categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any], + const config = { + categories: [ + { id: 'cat1', enabled: true }, + { id: 'cat2', enabled: true } + ], filterQueries: expectedResult - }; + } as SearchConfiguration; const alfrescoApiService = TestBed.inject(AlfrescoApiService); const searchHeaderService = new SearchHeaderQueryBuilderService(buildConfig(config), alfrescoApiService, null); @@ -132,10 +161,10 @@ describe('SearchHeaderQueryBuilderService', () => { TestBed.runInInjectionContext(() => { const activeFilter = 'FakeColumn'; - const config: SearchConfiguration = { - categories: [{ id: 'cat1', enabled: true } as any], + const config = { + categories: [{ id: 'cat1', enabled: true }], filterQueries: [{ query: 'PARENT:"workspace://SpacesStore/fake-node-id' }] - }; + } as SearchConfiguration; const alfrescoApiService = TestBed.inject(AlfrescoApiService); const searchHeaderService = new SearchHeaderQueryBuilderService(buildConfig(config), alfrescoApiService, null); diff --git a/lib/content-services/src/lib/search/services/search-header-query-builder.service.ts b/lib/content-services/src/lib/search/services/search-header-query-builder.service.ts index 60ef9f7603..406345459d 100644 --- a/lib/content-services/src/lib/search/services/search-header-query-builder.service.ts +++ b/lib/content-services/src/lib/search/services/search-header-query-builder.service.ts @@ -36,7 +36,11 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService { activeFilters: FilterSearch[] = []; - constructor(appConfig: AppConfigService, alfrescoApiService: AlfrescoApiService, private nodeApiService: NodesApiService) { + constructor( + appConfig: AppConfigService, + alfrescoApiService: AlfrescoApiService, + private readonly nodeApiService: NodesApiService + ) { super(appConfig, alfrescoApiService); this.updated.pipe(filter((query) => !!query)).subscribe(() => { @@ -127,6 +131,12 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService { return foundCategory; } + getOperatorForFilterId(id: string): string | undefined { + const foundCategory = this.categories?.find((category) => category.id === id); + + return foundCategory?.component?.settings?.operator; + } + setCurrentRootFolderId(currentFolderId: string) { const alreadyAddedFilter = this.filterQueries.find((filterQueries) => filterQueries.query.includes(currentFolderId));