diff --git a/projects/aca-content/src/lib/components/search/search-input/search-input.component.spec.ts b/projects/aca-content/src/lib/components/search/search-input/search-input.component.spec.ts index 3ee048959..5f18a7e36 100644 --- a/projects/aca-content/src/lib/components/search/search-input/search-input.component.spec.ts +++ b/projects/aca-content/src/lib/components/search/search-input/search-input.component.spec.ts @@ -29,7 +29,9 @@ import { AppStore } from '@alfresco/aca-shared/store'; import { AppTestingModule } from '../../../testing/app-testing.module'; import { SearchInputComponent } from './search-input.component'; import { Store } from '@ngrx/store'; -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; +import { NavigationStart, Router, RouterEvent } from '@angular/router'; +import { SearchConfiguration, SearchQueryBuilderService } from '@alfresco/adf-content-services'; import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatMenuHarness } from '@angular/material/menu/testing'; @@ -41,6 +43,9 @@ describe('SearchInputComponent', () => { let store: jasmine.SpyObj>; let unitTestingUtils: UnitTestingUtils; let loader: HarnessLoader; + let router: Router; + const routerEventsSubject = new Subject(); + const configUpdatedSubject = new Subject(); function getFirstError(): string { const error = unitTestingUtils.getByDirective(MatError); @@ -77,18 +82,31 @@ describe('SearchInputComponent', () => { beforeEach(async () => { const storeSpy = jasmine.createSpyObj>('Store', ['dispatch', 'pipe']); + const queryBuilderSpy = { + configUpdated: configUpdatedSubject, + removeFilterQuery: () => {} + } as Partial; + await TestBed.configureTestingModule({ imports: [AppTestingModule, SearchInputComponent], - providers: [{ provide: Store, useValue: storeSpy }] + providers: [ + { provide: Store, useValue: storeSpy }, + { provide: SearchQueryBuilderService, useValue: queryBuilderSpy } + ] }).compileComponents(); fixture = TestBed.createComponent(SearchInputComponent); component = fixture.componentInstance; store = TestBed.inject(Store) as jasmine.SpyObj>; + router = TestBed.inject(Router); store.pipe.and.returnValue(of([])); fixture.detectChanges(); unitTestingUtils = new UnitTestingUtils(fixture.debugElement); loader = TestbedHarnessEnvironment.loader(fixture); + + Object.defineProperty(router, 'events', { + get: () => routerEventsSubject.asObservable() + }); }); it('should show required error when field is empty and touched', async () => { @@ -147,4 +165,32 @@ describe('SearchInputComponent', () => { component.onSearchSubmit({ target: { value: '' } }); expect(store.dispatch).not.toHaveBeenCalled(); }); + + describe('queryBuilder configUpdated handling', () => { + it('should call searchByOption when searchedWord set and navigation has query params', () => { + spyOn(component, 'searchByOption').and.stub(); + + component.ngOnInit(); + + component.searchedWord = 'term'; + + routerEventsSubject.next(new NavigationStart(1, '/path?q=term')); + configUpdatedSubject.next({}); + + expect(component.searchByOption).toHaveBeenCalled(); + }); + + it('should NOT call searchByOption when searchedWord set and navigation has NO query params', () => { + component.searchedWord = 'term'; + + routerEventsSubject.next(new NavigationStart(1, '/path')); + + configUpdatedSubject.next({}); + spyOn(component, 'searchByOption').and.stub(); + + component.ngOnInit(); + + expect(component.searchByOption).not.toHaveBeenCalled(); + }); + }); }); diff --git a/projects/aca-content/src/lib/components/search/search-input/search-input.component.ts b/projects/aca-content/src/lib/components/search/search-input/search-input.component.ts index b69ae9ab8..6de96a4dc 100644 --- a/projects/aca-content/src/lib/components/search/search-input/search-input.component.ts +++ b/projects/aca-content/src/lib/components/search/search-input/search-input.component.ts @@ -28,7 +28,17 @@ import { SearchQueryBuilderService } from '@alfresco/adf-content-services'; import { AppConfigService, NotificationService } from '@alfresco/adf-core'; import { Component, DestroyRef, inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; -import { ActivatedRoute, NavigationSkipped, Params, PRIMARY_OUTLET, Router, UrlSegment, UrlSegmentGroup, UrlTree } from '@angular/router'; +import { + ActivatedRoute, + NavigationSkipped, + NavigationStart, + Params, + PRIMARY_OUTLET, + Router, + UrlSegment, + UrlSegmentGroup, + UrlTree +} from '@angular/router'; import { Store } from '@ngrx/store'; import { SearchInputControlComponent } from '../search-input-control/search-input-control.component'; import { SearchNavigationService } from '../search-navigation.service'; @@ -45,7 +55,7 @@ import { FormsModule } from '@angular/forms'; import { extractSearchedWordFromEncodedQuery } from '../../../utils/aca-search-utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { merge } from 'rxjs/internal/observable/merge'; -import { filter, map, withLatestFrom } from 'rxjs'; +import { filter, map, startWith, withLatestFrom } from 'rxjs'; @Component({ imports: [ @@ -140,6 +150,23 @@ export class SearchInputComponent implements OnInit, OnDestroy { } }); + this.queryBuilder.configUpdated + .pipe( + takeUntilDestroyed(this.destroyRef), + withLatestFrom( + this.router.events.pipe( + filter((event): event is NavigationStart => event instanceof NavigationStart), + startWith(null) + ) + ) + ) + .subscribe(([, navigationStartEvent]) => { + const hasQueryParams = navigationStartEvent?.url.includes('?'); + if (this.searchedWord && hasQueryParams) { + this.searchByOption(); + } + }); + this.appHookService.library400Error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { this.has400LibraryError = true; this.hasLibrariesConstraint = this.evaluateLibrariesConstraint(); diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.html b/projects/aca-content/src/lib/components/search/search-results/search-results.component.html index f1b63e145..0331c5fff 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.html +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.html @@ -1,8 +1,6 @@
- +
@@ -26,23 +24,25 @@

{{ 'APP.BROWSE.SEARCH.ADVANCED_FILTERS' | translate }}

- + @@ -62,16 +63,19 @@ [disabled]="!encodedQuery" class="aca-content__save-search-action" title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}" - [attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate "> + [attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate" + > {{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}
@@ -97,7 +101,13 @@ (node-dblclick)="handleNodeClick($event)" > - + diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts b/projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts index df8891e7e..f13b1997c 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts @@ -22,13 +22,13 @@ * from Hyland Software. If not, see . */ -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { SearchResultsComponent } from './search-results.component'; import { AppConfigService, NotificationService, TranslationService } from '@alfresco/adf-core'; import { Store } from '@ngrx/store'; import { NavigateToFolder } from '@alfresco/aca-shared/store'; import { Pagination, SearchRequest } from '@alfresco/js-api'; -import { SearchQueryBuilderService } from '@alfresco/adf-content-services'; +import { FacetFieldBucket, SearchQueryBuilderService } from '@alfresco/adf-content-services'; import { ActivatedRoute, Event, NavigationStart, Params, Router } from '@angular/router'; import { BehaviorSubject, Observable, of, Subject, throwError } from 'rxjs'; import { AppTestingModule } from '../../../testing/app-testing.module'; @@ -60,9 +60,11 @@ describe('SearchComponent', () => { let showErrorSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef>; let showInfoSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef>; let loader: HarnessLoader; + let updatedSubjectMock: Subject; const editSavedSearchesSpy = jasmine.createSpy('editSavedSearch'); const getSavedSearchButton = (): HTMLButtonElement => fixture.nativeElement.querySelector('.aca-content__save-search-action'); + const getResetSearchButton = (): HTMLButtonElement => fixture.nativeElement.querySelector('.aca-content__reset-action'); const encodeQuery = (query: any): string => { return Buffer.from(JSON.stringify(query)).toString('base64'); @@ -72,6 +74,7 @@ describe('SearchComponent', () => { params = new BehaviorSubject({ q: 'TYPE: "cm:folder" AND %28=cm: name: email OR cm: name: budget%29' }); queryParams = new Subject(); routerEvents = new Subject(); + updatedSubjectMock = new Subject(); const routerMock = jasmine.createSpyObj('Router', ['navigate'], { url: '/mock-search-url', @@ -92,9 +95,7 @@ describe('SearchComponent', () => { { provide: SavedSearchesContextService, useValue: { - getSavedSearches: jasmine - .createSpy('getSavedSearches') - .and.returnValue(of([{ name: 'test', encodedUrl: encodeQuery({ name: 'test' }), order: 0 }])), + savedSearches$: of([{ name: 'test', encodedUrl: encodeQuery({ name: 'test' }), order: 0 }]), editSavedSearch: editSavedSearchesSpy } }, @@ -125,6 +126,8 @@ describe('SearchComponent', () => { router = TestBed.inject(Router); route = TestBed.inject(ActivatedRoute); + queryBuilder.updated = updatedSubjectMock; + const notificationService = TestBed.inject(NotificationService); showErrorSpy = spyOn(notificationService, 'showError'); showInfoSpy = spyOn(notificationService, 'showInfo'); @@ -257,12 +260,6 @@ describe('SearchComponent', () => { }); }); - it('should update the user query whenever configuration changed', () => { - component.searchedWord = 'orange'; - queryBuilder.configUpdated.next({ 'app:fields': ['cm:tag'] } as any); - expect(queryBuilder.userQuery).toBe(`((cm:tag:"orange*"))`); - }); - it('should get initial saved search when url matches', () => { route.queryParams = of({ q: encodeQuery({ name: 'test' }) }); component.ngOnInit(); @@ -372,5 +369,73 @@ describe('SearchComponent', () => { expect(queryBuilder.userQuery).toBe('(test)'); }); + it('should set loading to true in updated stream for non-nullish query', fakeAsync(() => { + spyOn(queryBuilder, 'execute').and.stub(); + + expect(component.isLoading).toBeFalse(); + + updatedSubjectMock.next(null); + + tick(); + + expect(component.isLoading).toBeFalse(); + + updatedSubjectMock.next({} as SearchRequest); + + tick(); + + expect(component.isLoading).toBeTrue(); + + flush(); + })); + + describe('reset button', () => { + it('should enable the reset button when there are queryFragments', fakeAsync(() => { + queryBuilder.queryFragmentsUpdate.next({ test: 'test-value' }); + + tick(); + + fixture.detectChanges(); + + const resetBtn = getResetSearchButton(); + + expect(resetBtn).toBeDefined(); + expect(resetBtn.getAttribute('disabled')).toBeFalsy(); + + flush(); + })); + + it('should enable the reset button when there are userFacetBuckets', fakeAsync(() => { + queryBuilder.userFacetBucketsUpdate.next({ test: [{ label: 'test-value' }] as FacetFieldBucket[] }); + + tick(); + + fixture.detectChanges(); + + const resetBtn = getResetSearchButton(); + + expect(resetBtn).toBeDefined(); + expect(resetBtn.getAttribute('disabled')).toBeFalsy(); + + flush(); + })); + + it('should disable the reset button when there are no filters applied', fakeAsync(() => { + queryBuilder.queryFragmentsUpdate.next({}); + queryBuilder.userFacetBucketsUpdate.next({}); + + tick(); + + fixture.detectChanges(); + + const resetBtn = getResetSearchButton(); + + expect(resetBtn).toBeDefined(); + expect(resetBtn.getAttribute('disabled')).toBeTruthy(); + + flush(); + })); + }); + testHeader(SearchResultsComponent, false); }); diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts b/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts index fa9f99e5a..e22612b92 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts @@ -64,7 +64,7 @@ import { } from '@alfresco/aca-shared'; import { SearchSortingDefinition } from '@alfresco/adf-content-services/lib/search/models/search-sorting-definition.interface'; import { filter, first, map, startWith, switchMap, take, tap, toArray } from 'rxjs/operators'; -import { CommonModule } from '@angular/common'; +import { AsyncPipe, CommonModule } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; import { SearchInputComponent } from '../search-input/search-input.component'; import { MatProgressBarModule } from '@angular/material/progress-bar'; @@ -84,7 +84,7 @@ import { formatSearchTerm } from '../../../utils/aca-search-utils'; import { SaveSearchDirective } from '../search-save/directive/save-search.directive'; -import { combineLatest, of } from 'rxjs'; +import { combineLatest, merge, Observable, of } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatMenuModule } from '@angular/material/menu'; import { IsFeatureSupportedInCurrentAcsPipe } from '../../../pipes/is-feature-supported.pipe'; @@ -123,7 +123,8 @@ import { SavedSearchesContextService } from '../../../services/saved-searches-co BulkActionsDropdownComponent, SearchAiInputContainerComponent, SaveSearchDirective, - IsFeatureSupportedInCurrentAcsPipe + IsFeatureSupportedInCurrentAcsPipe, + AsyncPipe ], selector: 'aca-search-results', templateUrl: './search-results.component.html', @@ -135,6 +136,8 @@ export class SearchResultsComponent extends PageComponent implements OnInit { infoDrawerPreview$ = this.store.select(infoDrawerPreview); + protected readonly areFiltersActive$: Observable; + searchedWord: string; queryParamName = 'q'; data: ResultSetPaging; @@ -168,8 +171,14 @@ export class SearchResultsComponent extends PageComponent implements OnInit { this.queryBuilder.configUpdated.pipe(takeUntilDestroyed()).subscribe((searchConfig) => { this.searchConfig = searchConfig; - this.updateUserQuery(); }); + + this.areFiltersActive$ = merge(this.queryBuilder.queryFragmentsUpdate, this.queryBuilder.userFacetBucketsUpdate).pipe( + takeUntilDestroyed(), + map((v) => { + return Object.values(v).some((filterValue) => (Array.isArray(filterValue) ? filterValue.length > 0 : !!filterValue)); + }) + ); } ngOnInit() { @@ -179,12 +188,10 @@ export class SearchResultsComponent extends PageComponent implements OnInit { this.sorting = this.getSorting(); this.subscriptions.push( - this.queryBuilder.updated.subscribe((query) => { + this.queryBuilder.updated.pipe(filter(Boolean)).subscribe(() => { this.isLoading = true; - if (query) { - this.sorting = this.getSorting(); - this.changeDetectorRef.detectChanges(); - } + this.sorting = this.getSorting(); + this.changeDetectorRef.detectChanges(); }), this.queryBuilder.executed.subscribe((data) => { @@ -211,7 +218,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit { .pipe( takeUntilDestroyed(this.destroyRef), switchMap((params) => - this.savedSearchesService.getSavedSearches().pipe( + this.savedSearchesService.savedSearches$.pipe( first(), map((savedSearches) => savedSearches.find((savedSearch) => savedSearch.encodedUrl === encodeURIComponent(params[this.queryParamName]))) ) @@ -224,18 +231,18 @@ export class SearchResultsComponent extends PageComponent implements OnInit { combineLatest([ this.route.queryParams, this.router.events.pipe( - filter((e): e is NavigationStart => e instanceof NavigationStart), + filter((event): event is NavigationStart => event instanceof NavigationStart), startWith(null) ) ]) .pipe( takeUntilDestroyed(this.destroyRef), tap(([params]) => { + this.queryBuilder.userQuery = ''; this.encodedQuery = params[this.queryParamName]; this.isLoading = !!this.encodedQuery; this.searchedWord = extractSearchedWordFromEncodedQuery(this.encodedQuery); - this.updateUserQuery(); const filtersFromEncodedQuery = extractFiltersFromEncodedQuery(this.encodedQuery); this.queryBuilder.populateFilters.next(filtersFromEncodedQuery || {}); @@ -355,11 +362,6 @@ export class SearchResultsComponent extends PageComponent implements OnInit { }); } - private updateUserQuery(): void { - const updatedUserQuery = formatSearchTerm(this.searchedWord, this.searchConfig['app:fields']); - this.queryBuilder.userQuery = updatedUserQuery; - } - private shouldExecuteQuery(navigationStartEvent: NavigationStart | null, query: string | undefined): boolean { const hasQueryChanged = query !== this.previousEncodedQuery; this.previousEncodedQuery = query;