[ACS-10730] The ‘Save Changes’ button becomes disabled after modifying the search or filter. (#4929)

This commit is contained in:
dominikiwanekhyland
2025-12-19 07:18:37 +01:00
committed by GitHub
parent 9cd6c9154b
commit 7df4e84729
12 changed files with 246 additions and 82 deletions
@@ -60,6 +60,7 @@
mat-button mat-button
acaSaveSearch acaSaveSearch
[acaSaveSearchQuery]="encodedQuery" [acaSaveSearchQuery]="encodedQuery"
(searchSaved)="onSaveSearch()"
[disabled]="!encodedQuery" [disabled]="!encodedQuery"
class="aca-content__save-search-action" class="aca-content__save-search-action"
title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}" title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}"
@@ -266,6 +266,17 @@ describe('SearchComponent', () => {
expect(component.initialSavedSearch).toEqual({ name: 'test', encodedUrl: encodeQuery({ name: 'test' }), order: 0 }); expect(component.initialSavedSearch).toEqual({ name: 'test', encodedUrl: encodeQuery({ name: 'test' }), order: 0 });
}); });
it('should get initial saved search after creating a new one', () => {
route.queryParams = of({ q: encodeQuery({ name: 'test' }) });
component.onSaveSearch();
expect(component.initialSavedSearch).toEqual({ name: 'test', encodedUrl: encodeQuery({ name: 'test' }), order: 0 });
});
it('should clear context save search in service on component destroy', () => {
component.ngOnDestroy();
expect(TestBed.inject(SavedSearchesContextService).currentContextSavedSearch).toBeUndefined();
});
it('should render a menu with 2 options when initial saved search is found', async () => { it('should render a menu with 2 options when initial saved search is found', async () => {
route.queryParams = of({ q: encodeQuery({ name: 'test' }) }); route.queryParams = of({ q: encodeQuery({ name: 'test' }) });
component.ngOnInit(); component.ngOnInit();
@@ -22,7 +22,7 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>. * from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ChangeDetectorRef, Component, inject, OnInit, ViewEncapsulation } from '@angular/core'; import { ChangeDetectorRef, Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { NodeEntry, Pagination, ResultSetPaging } from '@alfresco/js-api'; import { NodeEntry, Pagination, ResultSetPaging } from '@alfresco/js-api';
import { ActivatedRoute, NavigationStart } from '@angular/router'; import { ActivatedRoute, NavigationStart } from '@angular/router';
import { import {
@@ -131,7 +131,7 @@ import { SavedSearchesContextService } from '../../../services/saved-searches-co
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
styleUrls: ['./search-results.component.scss'] styleUrls: ['./search-results.component.scss']
}) })
export class SearchResultsComponent extends PageComponent implements OnInit { export class SearchResultsComponent extends PageComponent implements OnInit, OnDestroy {
private notificationService = inject(NotificationService); private notificationService = inject(NotificationService);
infoDrawerPreview$ = this.store.select(infoDrawerPreview); infoDrawerPreview$ = this.store.select(infoDrawerPreview);
@@ -214,19 +214,9 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
this.columns = this.extensions.documentListPresets.searchResults || []; this.columns = this.extensions.documentListPresets.searchResults || [];
if (this.route) { if (this.route) {
this.route.queryParams this.selectInitialSavedSearch().subscribe((savedSearches) => {
.pipe( this.initialSavedSearch = savedSearches;
takeUntilDestroyed(this.destroyRef), });
switchMap((params) =>
this.savedSearchesService.savedSearches$.pipe(
first(),
map((savedSearches) => savedSearches.find((savedSearch) => savedSearch.encodedUrl === encodeURIComponent(params[this.queryParamName])))
)
)
)
.subscribe((savedSearches) => {
this.initialSavedSearch = savedSearches;
});
combineLatest([ combineLatest([
this.route.queryParams, this.route.queryParams,
@@ -269,6 +259,10 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
} }
} }
ngOnDestroy(): void {
this.savedSearchesService.currentContextSavedSearch = undefined;
}
onSearchError(error: { message: any }) { onSearchError(error: { message: any }) {
let message: string; let message: string;
try { try {
@@ -362,6 +356,15 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
}); });
} }
onSaveSearch(): void {
this.selectInitialSavedSearch()
.pipe(take(1))
.subscribe((savedSearch) => {
this.initialSavedSearch = savedSearch;
this.savedSearchesService.currentContextSavedSearch = savedSearch;
});
}
private shouldExecuteQuery(navigationStartEvent: NavigationStart | null, query: string | undefined): boolean { private shouldExecuteQuery(navigationStartEvent: NavigationStart | null, query: string | undefined): boolean {
const hasQueryChanged = query !== this.previousEncodedQuery; const hasQueryChanged = query !== this.previousEncodedQuery;
this.previousEncodedQuery = query; this.previousEncodedQuery = query;
@@ -374,4 +377,20 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
return !!query; return !!query;
} }
} }
private selectInitialSavedSearch(): Observable<SavedSearch> {
return this.route.queryParams.pipe(
takeUntilDestroyed(this.destroyRef),
switchMap((params) =>
this.savedSearchesService.savedSearches$.pipe(
first(),
map(
(savedSearches) =>
savedSearches.find((savedSearch) => savedSearch.encodedUrl === encodeURIComponent(params[this.queryParamName])) ||
this.savedSearchesService.currentContextSavedSearch
)
)
)
);
}
} }
@@ -74,12 +74,12 @@ describe('SaveSearchDialogComponent', () => {
expect(savedSearchesService.saveSearch).not.toHaveBeenCalled(); expect(savedSearchesService.saveSearch).not.toHaveBeenCalled();
}); });
it('should save search, show snackbar message and close modal if form is valid', fakeAsync(() => { it('should save search, show snackbar message and close modal with emitting true value if form is valid', fakeAsync(() => {
spyOn(savedSearchesService, 'saveSearch').and.callThrough(); spyOn(savedSearchesService, 'saveSearch').and.callThrough();
spyOn(notificationService, 'showInfo'); spyOn(notificationService, 'showInfo');
setFormValuesAndSubmit(); setFormValuesAndSubmit();
expect(notificationService.showInfo).toHaveBeenCalledWith('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS'); expect(notificationService.showInfo).toHaveBeenCalledWith('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS');
expect(dialogRef.close).toHaveBeenCalled(); expect(dialogRef.close).toHaveBeenCalledWith(true);
})); }));
it('should show snackbar error if there is save error', fakeAsync(() => { it('should show snackbar error if there is save error', fakeAsync(() => {
@@ -96,7 +96,7 @@ export class SaveSearchDialogComponent {
.pipe(take(1)) .pipe(take(1))
.subscribe({ .subscribe({
next: () => { next: () => {
this.dialog.close(); this.dialog.close(true);
this.notificationService.showInfo('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS'); this.notificationService.showInfo('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS');
this.disableSubmitButton = false; this.disableSubmitButton = false;
}, },
@@ -24,19 +24,24 @@
import { Component, DebugElement } from '@angular/core'; import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { of } from 'rxjs'; import { of, Subject } from 'rxjs';
import { SaveSearchDirective } from './save-search.directive'; import { SaveSearchDirective } from './save-search.directive';
import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component'; import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component';
@Component({ @Component({
selector: 'app-test-component', selector: 'app-test-component',
template: '<div acaSaveSearch="searchQuery" acaSaveSearchQuery="encodedQuery"></div>', template: '<div acaSaveSearch="searchQuery" (searchSaved)="onSaveSearchSuccess($event)" acaSaveSearchQuery="encodedQuery"></div>',
imports: [SaveSearchDirective] imports: [SaveSearchDirective]
}) })
class TestComponent { class TestComponent {
searchQuery = 'encodedQuery'; searchQuery = 'encodedQuery';
isSavedWithSuccess = false;
onSaveSearchSuccess(value: boolean): void {
this.isSavedWithSuccess = value;
}
} }
describe('SaveSearchDirective', () => { describe('SaveSearchDirective', () => {
@@ -57,7 +62,7 @@ describe('SaveSearchDirective', () => {
provide: MatDialog, provide: MatDialog,
useValue: { useValue: {
open: () => ({ open: () => ({
afterClosed: jasmine.createSpy('afterClosed').and.returnValue(of(null)) afterClosed: jasmine.createSpy('afterClosed').and.returnValue(of(true))
}) })
} }
} }
@@ -86,4 +91,20 @@ describe('SaveSearchDirective', () => {
expect(dialog.open).toHaveBeenCalledWith(SaveSearchDialogComponent, expectedConfig); expect(dialog.open).toHaveBeenCalledWith(SaveSearchDialogComponent, expectedConfig);
}); });
it('should emit event on save search success', () => {
const afterClosed$ = new Subject<boolean>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<SaveSearchDialogComponent>);
element.triggerEventHandler('click', event);
afterClosed$.next(true);
afterClosed$.complete();
fixture.detectChanges();
expect(fixture.componentInstance.isSavedWithSuccess).toBe(true);
});
}); });
@@ -22,9 +22,10 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>. * from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Directive, ElementRef, HostListener, Input } from '@angular/core'; import { DestroyRef, Directive, ElementRef, EventEmitter, HostListener, inject, Input, Output } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component'; import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
interface SaveSearchDirectiveDialogData { interface SaveSearchDirectiveDialogData {
searchUrl: string; searchUrl: string;
@@ -39,10 +40,13 @@ export class SaveSearchDirective {
@Input() @Input()
acaSaveSearchQuery: string; acaSaveSearchQuery: string;
constructor( /** Outputs a true value when search was successfully saved */
private readonly dialogRef: MatDialog, @Output()
private readonly elementRef: ElementRef<HTMLElement> searchSaved = new EventEmitter<boolean>();
) {}
private readonly destroyRef = inject(DestroyRef);
private readonly dialogRef = inject(MatDialog);
private readonly elementRef = inject(ElementRef<HTMLElement>);
@HostListener('click', ['$event']) @HostListener('click', ['$event'])
onClick(event: MouseEvent) { onClick(event: MouseEvent) {
@@ -52,7 +56,15 @@ export class SaveSearchDirective {
} }
private openDialog(): void { private openDialog(): void {
this.dialogRef.open(SaveSearchDialogComponent, { ...this.getDialogConfig(), restoreFocus: true }); const dialog = this.dialogRef.open(SaveSearchDialogComponent, { ...this.getDialogConfig(), restoreFocus: true });
dialog
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value: boolean) => {
if (value) {
this.searchSaved.emit(value);
}
});
} }
private getDialogConfig(): { data: SaveSearchDirectiveDialogData } { private getDialogConfig(): { data: SaveSearchDirectiveDialogData } {
@@ -1,3 +1,3 @@
@if (item) { @if (item) {
<app-expand-menu [item]="item" /> <app-expand-menu [item]="item" (actionClicked)="onActionClicked($event)" />
} }
@@ -28,6 +28,7 @@ import { AppTestingModule } from '../../../../testing/app-testing.module';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { SavedSearchesContextService } from '../../../../services/saved-searches-context.service'; import { SavedSearchesContextService } from '../../../../services/saved-searches-context.service';
import { SavedSearch } from '@alfresco/adf-content-services'; import { SavedSearch } from '@alfresco/adf-content-services';
import { NavBarLinkRef } from '@alfresco/adf-extensions';
describe('SaveSearchSidenavComponent', () => { describe('SaveSearchSidenavComponent', () => {
let fixture: ComponentFixture<SaveSearchSidenavComponent>; let fixture: ComponentFixture<SaveSearchSidenavComponent>;
@@ -36,8 +37,8 @@ describe('SaveSearchSidenavComponent', () => {
beforeEach(() => { beforeEach(() => {
const mockService: Partial<SavedSearchesContextService> = { const mockService: Partial<SavedSearchesContextService> = {
currentContextSavedSearch: undefined,
init: (): void => {}, init: (): void => {},
get savedSearches$(): Observable<SavedSearch[]> { get savedSearches$(): Observable<SavedSearch[]> {
return of([]); return of([]);
} }
@@ -94,4 +95,46 @@ describe('SaveSearchSidenavComponent', () => {
id: 'search1' id: 'search1'
}); });
})); }));
describe('onActionClicked', () => {
beforeEach(() => {
spyOnProperty(savedSearchesService, 'savedSearches$', 'get').and.returnValue(of([{ name: 'abc', order: 0, encodedUrl: 'abc' }]));
component.ngOnInit();
fixture.detectChanges();
});
it('should set currentContextSavedSearch when matching saved search is found', () => {
const selectedLinkRef: NavBarLinkRef = {
id: 'search-test',
icon: '',
title: 'abc',
description: 'test',
route: 'search?q=encoded',
url: 'search?q=encoded'
};
component.onActionClicked(selectedLinkRef);
expect(component.savedSearchesService.currentContextSavedSearch).toEqual({
name: 'abc',
encodedUrl: 'abc',
order: 0
});
});
it('should set currentContextSavedSearch to undefined when no matching saved search is found', () => {
const selectedLinkRef: NavBarLinkRef = {
id: 'search-unknown',
icon: '',
title: 'Unknown Search',
description: 'Unknown Search',
route: 'search?q=unknown',
url: 'search?q=unknown'
};
component.onActionClicked(selectedLinkRef);
expect(component.savedSearchesService.currentContextSavedSearch).toBeUndefined();
});
});
}); });
@@ -42,17 +42,19 @@ export class SaveSearchSidenavComponent implements OnInit {
translationService = inject(TranslationService); translationService = inject(TranslationService);
item: NavBarLinkRef; item: NavBarLinkRef;
private savedSearchCount = 0;
private savedSearches: SavedSearch[];
private readonly manageSearchesId = 'manage-saved-searches'; private readonly manageSearchesId = 'manage-saved-searches';
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly userPreferenceService = inject(UserPreferencesService); private readonly userPreferenceService = inject(UserPreferencesService);
private savedSearchCount = 0;
ngOnInit() { ngOnInit() {
this.savedSearchesService.init(); this.savedSearchesService.init();
this.savedSearchesService.savedSearches$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((savedSearches) => { this.savedSearchesService.savedSearches$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((savedSearches) => {
this.item = this.createNavBarLinkRef(savedSearches); this.item = this.createNavBarLinkRef(savedSearches);
this.savedSearchCount = savedSearches.length; this.savedSearchCount = savedSearches.length;
this.savedSearches = savedSearches;
}); });
this.userPreferenceService this.userPreferenceService
.select(UserPreferenceValues.Locale) .select(UserPreferenceValues.Locale)
@@ -64,6 +66,11 @@ export class SaveSearchSidenavComponent implements OnInit {
}); });
} }
onActionClicked(selectedLinkRef: NavBarLinkRef): void {
const selectedSavedSearch = this.savedSearches?.find((savedSearch) => savedSearch.name === selectedLinkRef.title);
this.savedSearchesService.currentContextSavedSearch = selectedSavedSearch;
}
private createNavBarLinkRef(children: SavedSearch[]): NavBarLinkRef { private createNavBarLinkRef(children: SavedSearch[]): NavBarLinkRef {
const mappedChildren = children const mappedChildren = children
.map((child) => ({ .map((child) => ({
@@ -23,11 +23,12 @@
*/ */
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Subject } from 'rxjs'; import { of, Subject } from 'rxjs';
import { SavedSearchesContextService } from './saved-searches-context.service'; import { SavedSearchesContextService } from './saved-searches-context.service';
import { SavedSearch, SavedSearchesLegacyService, SavedSearchesService } from '@alfresco/adf-content-services'; import { SavedSearch, SavedSearchesLegacyService, SavedSearchesService } from '@alfresco/adf-content-services';
import { IsFeatureSupportedInCurrentAcsPipe } from '../pipes/is-feature-supported.pipe'; import { IsFeatureSupportedInCurrentAcsPipe } from '../pipes/is-feature-supported.pipe';
import { NodeEntry } from '@alfresco/js-api';
describe('SavedSearchesContextService', () => { describe('SavedSearchesContextService', () => {
let legacySpy: jasmine.SpyObj<SavedSearchesLegacyService>; let legacySpy: jasmine.SpyObj<SavedSearchesLegacyService>;
@@ -38,7 +39,6 @@ describe('SavedSearchesContextService', () => {
beforeEach(() => { beforeEach(() => {
legacySpy = jasmine.createSpyObj('SavedSearchesLegacyService', [ legacySpy = jasmine.createSpyObj('SavedSearchesLegacyService', [
'savedSearches$',
'init', 'init',
'getSavedSearches', 'getSavedSearches',
'saveSearch', 'saveSearch',
@@ -46,9 +46,12 @@ describe('SavedSearchesContextService', () => {
'deleteSavedSearch', 'deleteSavedSearch',
'changeOrder' 'changeOrder'
]); ]);
legacySpy.getSavedSearches.and.returnValue(of([]));
legacySpy.saveSearch.and.returnValue(of({} as NodeEntry));
legacySpy.editSavedSearch.and.returnValue(of({} as NodeEntry));
legacySpy.deleteSavedSearch.and.returnValue(of({} as NodeEntry));
modernSpy = jasmine.createSpyObj('SavedSearchesService', [ modernSpy = jasmine.createSpyObj('SavedSearchesService', [
'savedSearches$',
'init', 'init',
'getSavedSearches', 'getSavedSearches',
'saveSearch', 'saveSearch',
@@ -56,6 +59,10 @@ describe('SavedSearchesContextService', () => {
'deleteSavedSearch', 'deleteSavedSearch',
'changeOrder' 'changeOrder'
]); ]);
modernSpy.getSavedSearches.and.returnValue(of([]));
modernSpy.saveSearch.and.returnValue(of({} as NodeEntry));
modernSpy.editSavedSearch.and.returnValue(of({} as NodeEntry));
modernSpy.deleteSavedSearch.and.returnValue(of({} as NodeEntry));
isSupported = new Subject<boolean>(); isSupported = new Subject<boolean>();
@@ -79,53 +86,70 @@ describe('SavedSearchesContextService', () => {
isSupported.next(true); isSupported.next(true);
}); });
it('should use modern service when feature is supported', () => { it('should use modern service when feature is supported', (done) => {
service.init(); service.init();
expect(legacySpy.init).not.toHaveBeenCalled(); setTimeout(() => {
expect(modernSpy.init).toHaveBeenCalled(); expect(legacySpy.init).not.toHaveBeenCalled();
expect(modernSpy.init).toHaveBeenCalled();
done();
});
}); });
it('should delegate init() call to a current strategy', () => { it('should delegate init() call to a current strategy', (done) => {
service.init(); service.init();
expect(modernSpy.init).toHaveBeenCalled(); setTimeout(() => {
expect(modernSpy.init).toHaveBeenCalled();
done();
});
}); });
it('should delegate getSavedSearches() call to a current strategy', () => { it('should delegate getSavedSearches() call to a current strategy', (done) => {
service.getSavedSearches(); service.getSavedSearches().subscribe(() => {
expect(modernSpy.getSavedSearches).toHaveBeenCalled(); expect(modernSpy.getSavedSearches).toHaveBeenCalled();
done();
});
}); });
it('should delegate saveSearch() call to a current strategy', () => { it('should delegate saveSearch() call to a current strategy', (done) => {
const newSavedSearch = { name: 'Test Search', description: 'Test Description', encodedUrl: 'http://example.com' }; const newSavedSearch = { name: 'Test Search', description: 'Test Description', encodedUrl: 'http://example.com' };
service.saveSearch(newSavedSearch); service.saveSearch(newSavedSearch).subscribe(() => {
expect(modernSpy.saveSearch).toHaveBeenCalledWith(newSavedSearch); expect(modernSpy.saveSearch).toHaveBeenCalledWith(newSavedSearch);
done();
});
}); });
it('should delegate editSavedSearch() call to a current strategy', () => { it('should delegate editSavedSearch() call to a current strategy', (done) => {
const updatedSavedSearch = { const updatedSavedSearch = {
name: 'Updated Search', name: 'Updated Search',
description: 'Updated Description', description: 'Updated Description',
encodedUrl: 'http://example.com', encodedUrl: 'http://example.com',
order: 1 order: 1
}; };
service.editSavedSearch(updatedSavedSearch); service.editSavedSearch(updatedSavedSearch).subscribe(() => {
expect(modernSpy.editSavedSearch).toHaveBeenCalledWith(updatedSavedSearch); expect(modernSpy.editSavedSearch).toHaveBeenCalledWith(updatedSavedSearch);
done();
});
}); });
it('should delegate deleteSavedSearch() call to a current strategy', () => { it('should delegate deleteSavedSearch() call to a current strategy', (done) => {
const deletedSavedSearch = { const deletedSavedSearch = {
name: 'Deleted Search', name: 'Deleted Search',
description: 'Deleted Description', description: 'Deleted Description',
encodedUrl: 'http://example.com', encodedUrl: 'http://example.com',
order: 2 order: 2
}; };
service.deleteSavedSearch(deletedSavedSearch); service.deleteSavedSearch(deletedSavedSearch).subscribe(() => {
expect(modernSpy.deleteSavedSearch).toHaveBeenCalledWith(deletedSavedSearch); expect(modernSpy.deleteSavedSearch).toHaveBeenCalledWith(deletedSavedSearch);
done();
});
}); });
it('should delegate changeOrder() call to a current strategy', () => { it('should delegate changeOrder() call to a current strategy', (done) => {
service.changeOrder(0, 1); service.changeOrder(0, 1);
expect(modernSpy.changeOrder).toHaveBeenCalledWith(0, 1); setTimeout(() => {
expect(modernSpy.changeOrder).toHaveBeenCalledWith(0, 1);
done();
});
}); });
}); });
@@ -134,53 +158,70 @@ describe('SavedSearchesContextService', () => {
isSupported.next(false); isSupported.next(false);
}); });
it('should use legacy service when feature is NOT supported', () => { it('should use legacy service when feature is NOT supported', (done) => {
service.init(); service.init();
expect(legacySpy.init).toHaveBeenCalled(); setTimeout(() => {
expect(modernSpy.init).not.toHaveBeenCalled(); expect(legacySpy.init).toHaveBeenCalled();
expect(modernSpy.init).not.toHaveBeenCalled();
done();
});
}); });
it('should delegate init() call to a current strategy', () => { it('should delegate init() call to a current strategy', (done) => {
service.init(); service.init();
expect(legacySpy.init).toHaveBeenCalled(); setTimeout(() => {
expect(legacySpy.init).toHaveBeenCalled();
done();
});
}); });
it('should delegate getSavedSearches() call to a current strategy', () => { it('should delegate getSavedSearches() call to a current strategy', (done) => {
service.getSavedSearches(); service.getSavedSearches().subscribe(() => {
expect(legacySpy.getSavedSearches).toHaveBeenCalled(); expect(legacySpy.getSavedSearches).toHaveBeenCalled();
done();
});
}); });
it('should delegate saveSearch() call to a current strategy', () => { it('should delegate saveSearch() call to a current strategy', (done) => {
const newSavedSearch = { name: 'Test Search', description: 'Test Description', encodedUrl: 'http://example.com' } as SavedSearch; const newSavedSearch = { name: 'Test Search', description: 'Test Description', encodedUrl: 'http://example.com' } as SavedSearch;
service.saveSearch(newSavedSearch); service.saveSearch(newSavedSearch).subscribe(() => {
expect(legacySpy.saveSearch).toHaveBeenCalledWith(newSavedSearch); expect(legacySpy.saveSearch).toHaveBeenCalledWith(newSavedSearch);
done();
});
}); });
it('should delegate editSavedSearch() call to a current strategy', () => { it('should delegate editSavedSearch() call to a current strategy', (done) => {
const updatedSavedSearch: SavedSearch = { const updatedSavedSearch: SavedSearch = {
name: 'Updated Search', name: 'Updated Search',
description: 'Updated Description', description: 'Updated Description',
encodedUrl: 'http://example.com', encodedUrl: 'http://example.com',
order: 1 order: 1
}; };
service.editSavedSearch(updatedSavedSearch); service.editSavedSearch(updatedSavedSearch).subscribe(() => {
expect(legacySpy.editSavedSearch).toHaveBeenCalledWith(updatedSavedSearch); expect(legacySpy.editSavedSearch).toHaveBeenCalledWith(updatedSavedSearch);
done();
});
}); });
it('should delegate deleteSavedSearch() call to a current strategy', () => { it('should delegate deleteSavedSearch() call to a current strategy', (done) => {
const deletedSavedSearch: SavedSearch = { const deletedSavedSearch: SavedSearch = {
name: 'Deleted Search', name: 'Deleted Search',
description: 'Deleted Description', description: 'Deleted Description',
encodedUrl: 'http://example.com', encodedUrl: 'http://example.com',
order: 2 order: 2
}; };
service.deleteSavedSearch(deletedSavedSearch); service.deleteSavedSearch(deletedSavedSearch).subscribe(() => {
expect(legacySpy.deleteSavedSearch).toHaveBeenCalledWith(deletedSavedSearch); expect(legacySpy.deleteSavedSearch).toHaveBeenCalledWith(deletedSavedSearch);
done();
});
}); });
it('should delegate changeOrder() call to a current strategy', () => { it('should delegate changeOrder() call to a current strategy', (done) => {
service.changeOrder(0, 1); service.changeOrder(0, 1);
expect(legacySpy.changeOrder).toHaveBeenCalledWith(0, 1); setTimeout(() => {
expect(legacySpy.changeOrder).toHaveBeenCalledWith(0, 1);
done();
});
}); });
}); });
}); });
@@ -33,8 +33,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
providedIn: 'root' providedIn: 'root'
}) })
export class SavedSearchesContextService implements SavedSearchStrategy { export class SavedSearchesContextService implements SavedSearchStrategy {
currentContextSavedSearch: SavedSearch;
private readonly strategy$ = new ReplaySubject<SavedSearchStrategy>(1); private readonly strategy$ = new ReplaySubject<SavedSearchStrategy>(1);
private strategy: SavedSearchStrategy;
constructor( constructor(
private readonly legacyService: SavedSearchesLegacyService, private readonly legacyService: SavedSearchesLegacyService,
@@ -45,8 +46,8 @@ export class SavedSearchesContextService implements SavedSearchStrategy {
.transform('isPreferencesApiAvailable') .transform('isPreferencesApiAvailable')
.pipe(takeUntilDestroyed()) .pipe(takeUntilDestroyed())
.subscribe((isSupported) => { .subscribe((isSupported) => {
this.strategy = isSupported ? this.modernService : this.legacyService; const strategy = isSupported ? this.modernService : this.legacyService;
this.strategy$.next(this.strategy); this.strategy$.next(strategy);
}); });
} }
@@ -55,26 +56,34 @@ export class SavedSearchesContextService implements SavedSearchStrategy {
} }
init(): void { init(): void {
this.strategy$.pipe(take(1)).subscribe((strategy) => strategy.init()); this.executeOnStrategyVoid((strategy) => strategy.init());
} }
getSavedSearches(): Observable<SavedSearch[]> { getSavedSearches(): Observable<SavedSearch[]> {
return this.strategy.getSavedSearches(); return this.executeOnStrategy((strategy) => strategy.getSavedSearches());
} }
saveSearch(newSaveSearch: Pick<SavedSearch, 'name' | 'description' | 'encodedUrl'>): Observable<NodeEntry> { saveSearch(newSaveSearch: Pick<SavedSearch, 'name' | 'description' | 'encodedUrl'>): Observable<NodeEntry> {
return this.strategy.saveSearch(newSaveSearch); return this.executeOnStrategy((strategy) => strategy.saveSearch(newSaveSearch));
} }
editSavedSearch(updatedSavedSearch: SavedSearch): Observable<NodeEntry> { editSavedSearch(updatedSavedSearch: SavedSearch): Observable<NodeEntry> {
return this.strategy.editSavedSearch(updatedSavedSearch); return this.executeOnStrategy((strategy) => strategy.editSavedSearch(updatedSavedSearch));
} }
deleteSavedSearch(deletedSavedSearch: SavedSearch): Observable<NodeEntry> { deleteSavedSearch(deletedSavedSearch: SavedSearch): Observable<NodeEntry> {
return this.strategy.deleteSavedSearch(deletedSavedSearch); return this.executeOnStrategy((strategy) => strategy.deleteSavedSearch(deletedSavedSearch));
} }
changeOrder(previousIndex: number, currentIndex: number): void { changeOrder(previousIndex: number, currentIndex: number): void {
this.strategy.changeOrder(previousIndex, currentIndex); this.executeOnStrategyVoid((strategy) => strategy.changeOrder(previousIndex, currentIndex));
}
private executeOnStrategy<T>(action: (strategy: SavedSearchStrategy) => Observable<T>): Observable<T> {
return this.strategy$.pipe(take(1), switchMap(action));
}
private executeOnStrategyVoid(action: (strategy: SavedSearchStrategy) => void): void {
this.strategy$.pipe(take(1)).subscribe(action);
} }
} }