From dc2e73e9a7113afcfe151b628e0732422b45f7ce Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Wed, 22 Oct 2025 11:29:59 +0200 Subject: [PATCH 01/11] [ACS-9166]: introduces strategy interface --- .../saved-searches-strategy.interface.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 lib/content-services/src/lib/common/interfaces/saved-searches-strategy.interface.ts diff --git a/lib/content-services/src/lib/common/interfaces/saved-searches-strategy.interface.ts b/lib/content-services/src/lib/common/interfaces/saved-searches-strategy.interface.ts new file mode 100644 index 00000000000..779925f4fc6 --- /dev/null +++ b/lib/content-services/src/lib/common/interfaces/saved-searches-strategy.interface.ts @@ -0,0 +1,71 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Observable } from 'rxjs'; +import { NodeEntry } from '@alfresco/js-api'; +import { SavedSearch } from './saved-search.interface'; + +/** + * Contract that describes the public API for saved searches strategy. + * Implemented by both the new and legacy SavedSearches services so callers + * can depend on the same shape. + */ +export interface SavedSearchStrategy { + savedSearches$: Observable; + + init(): void; + + /** + * Gets a list of saved searches by user. + * + * @returns SavedSearch list containing user saved searches + */ + getSavedSearches(): Observable; + + /** + * Saves a new search into state and updates state. If there are less than 5 searches, + * it will be pushed on first place, if more it will be pushed to 6th place. + * + * @param newSaveSearch object { name: string, description: string, encodedUrl: string } + * @returns NodeEntry + */ + saveSearch(newSaveSearch: Pick): Observable; + + /** + * Replace Save Search with new one and also updates the state. + * + * @param updatedSavedSearch - updated Save Search + * @returns NodeEntry + */ + editSavedSearch(updatedSavedSearch: SavedSearch): Observable; + + /** + * Deletes Save Search and update state. + * + * @param deletedSavedSearch - Save Search to delete + * @returns NodeEntry + */ + deleteSavedSearch(deletedSavedSearch: SavedSearch): Observable; + + /** + * Reorders saved search place + * + * @param previousIndex - previous index of saved search + * @param currentIndex - new index of saved search + */ + changeOrder(previousIndex: number, currentIndex: number): void; +} From 53bb04a3d7a613ba5adbe17f62dfefb055470203 Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Wed, 22 Oct 2025 11:31:43 +0200 Subject: [PATCH 02/11] [ACS-9166]: restores config-based saved searches approach --- .../src/lib/common/public-api.ts | 2 + .../saved-searches-legacy.service.spec.ts | 197 +++++++++++++++ .../services/saved-searches-legacy.service.ts | 227 ++++++++++++++++++ 3 files changed, 426 insertions(+) create mode 100644 lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts create mode 100644 lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts diff --git a/lib/content-services/src/lib/common/public-api.ts b/lib/content-services/src/lib/common/public-api.ts index 5b561d4501f..cca99f28e47 100644 --- a/lib/content-services/src/lib/common/public-api.ts +++ b/lib/content-services/src/lib/common/public-api.ts @@ -25,6 +25,7 @@ export * from './services/discovery-api.service'; export * from './services/people-content.service'; export * from './services/content.service'; export * from './services/saved-searches.service'; +export * from './services/saved-searches-legacy.service'; export * from './events/file.event'; @@ -38,4 +39,5 @@ export * from './models/allowable-operations.enum'; export * from './interfaces/search-configuration.interface'; export * from './interfaces/saved-search.interface'; +export * from './interfaces/saved-searches-strategy.interface'; export * from './mocks/ecm-user.service.mock'; diff --git a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts new file mode 100644 index 00000000000..7331f2ad256 --- /dev/null +++ b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts @@ -0,0 +1,197 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { NodeEntry } from '@alfresco/js-api'; +import { AlfrescoApiServiceMock } from '../../mock'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AuthenticationService } from '@alfresco/adf-core'; +import { Subject } from 'rxjs'; +import { SavedSearchesLegacyService } from './saved-searches-legacy.service'; + +describe('SavedSearchesLegacyService', () => { + let service: SavedSearchesLegacyService; + let authService: AuthenticationService; + let testUserName: string; + let getNodeContentSpy: jasmine.Spy; + + const testNodeId = 'test-node-id'; + const SAVED_SEARCHES_NODE_ID = 'saved-searches-node-id__'; + const SAVED_SEARCHES_CONTENT = JSON.stringify([ + { name: 'Search 1', description: 'Description 1', encodedUrl: 'url1', order: 0 }, + { name: 'Search 2', description: 'Description 2', encodedUrl: 'url2', order: 1 } + ]); + + /** + * Creates a stub with Promise returning a Blob + * + * @returns Promise with Blob + */ + function createBlob(): Promise { + return Promise.resolve(new Blob([SAVED_SEARCHES_CONTENT])); + } + + beforeEach(() => { + testUserName = 'test-user'; + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, + { provide: AuthenticationService, useValue: { getUsername: () => {}, onLogin: new Subject() } }, + SavedSearchesLegacyService + ] + }); + service = TestBed.inject(SavedSearchesLegacyService); + authService = TestBed.inject(AuthenticationService); + spyOn(service.nodesApi, 'getNode').and.callFake(() => Promise.resolve({ entry: { id: testNodeId } } as NodeEntry)); + spyOn(service.nodesApi, 'createNode').and.callFake(() => Promise.resolve({ entry: { id: 'new-node-id' } })); + spyOn(service.nodesApi, 'updateNodeContent').and.callFake(() => Promise.resolve({ entry: {} } as NodeEntry)); + getNodeContentSpy = spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob()); + }); + + afterEach(() => { + localStorage.removeItem(SAVED_SEARCHES_NODE_ID + testUserName); + }); + + it('should retrieve saved searches from the config.json file', (done) => { + spyOn(authService, 'getUsername').and.callFake(() => testUserName); + spyOn(localStorage, 'getItem').and.callFake(() => testNodeId); + service.init(); + + service.getSavedSearches().subscribe((searches) => { + expect(localStorage.getItem).toHaveBeenCalledWith(SAVED_SEARCHES_NODE_ID + testUserName); + expect(getNodeContentSpy).toHaveBeenCalledWith(testNodeId); + expect(searches.length).toBe(2); + expect(searches[0].name).toBe('Search 1'); + expect(searches[1].name).toBe('Search 2'); + done(); + }); + }); + + it('should create config.json file if it does not exist', (done) => { + const error: Error = { name: 'test', message: '{ "error": { "statusCode": 404 } }' }; + spyOn(authService, 'getUsername').and.callFake(() => testUserName); + service.nodesApi.getNode = jasmine.createSpy().and.returnValue(Promise.reject(error)); + getNodeContentSpy.and.callFake(() => Promise.resolve(new Blob(['']))); + service.init(); + + service.getSavedSearches().subscribe((searches) => { + expect(service.nodesApi.getNode).toHaveBeenCalledWith('-my-', { relativePath: 'config.json' }); + expect(service.nodesApi.createNode).toHaveBeenCalledWith('-my-', jasmine.objectContaining({ name: 'config.json' })); + expect(searches.length).toBe(0); + done(); + }); + }); + + it('should save a new search', (done) => { + spyOn(authService, 'getUsername').and.callFake(() => testUserName); + const nodeId = 'saved-searches-node-id'; + spyOn(localStorage, 'getItem').and.callFake(() => nodeId); + const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' }; + service.init(); + + service.saveSearch(newSearch).subscribe(() => { + expect(service.nodesApi.updateNodeContent).toHaveBeenCalledWith(nodeId, jasmine.any(String)); + expect(service.savedSearches$).toBeDefined(); + service.savedSearches$.subscribe((searches) => { + expect(searches.length).toBe(3); + expect(searches[2].name).toBe('Search 2'); + expect(searches[2].order).toBe(2); + done(); + }); + }); + }); + + it('should emit initial saved searches on subscription', (done) => { + const nodeId = 'saved-searches-node-id'; + spyOn(localStorage, 'getItem').and.returnValue(nodeId); + service.init(); + + service.savedSearches$.pipe().subscribe((searches) => { + expect(searches.length).toBe(2); + expect(searches[0].name).toBe('Search 1'); + done(); + }); + + service.getSavedSearches().subscribe(); + }); + + it('should emit updated saved searches after saving a new search', (done) => { + spyOn(authService, 'getUsername').and.callFake(() => testUserName); + spyOn(localStorage, 'getItem').and.callFake(() => testNodeId); + const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' }; + service.init(); + + let emissionCount = 0; + + service.savedSearches$.subscribe((searches) => { + emissionCount++; + if (emissionCount === 1) { + expect(searches.length).toBe(2); + } + if (emissionCount === 2) { + expect(searches.length).toBe(3); + expect(searches[2].name).toBe('Search 2'); + done(); + } + }); + + service.saveSearch(newSearch).subscribe(); + }); + + it('should edit a search', (done) => { + const updatedSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3', order: 0 }; + prepareDefaultMock(); + + service.editSavedSearch(updatedSearch).subscribe(() => { + service.savedSearches$.subscribe((searches) => { + expect(searches.length).toBe(2); + expect(searches[0].name).toBe('Search 3'); + expect(searches[0].order).toBe(0); + + expect(searches[1].name).toBe('Search 2'); + expect(searches[1].order).toBe(1); + done(); + }); + }); + }); + + it('should delete a search', (done) => { + const searchToDelete = { name: 'Search 1', description: 'Description 1', encodedUrl: 'url1', order: 0 }; + prepareDefaultMock(); + + service.deleteSavedSearch(searchToDelete).subscribe(() => { + service.savedSearches$.subscribe((searches) => { + expect(searches.length).toBe(1); + expect(searches[0].name).toBe('Search 2'); + expect(searches[0].order).toBe(0); + done(); + }); + }); + }); + + /** + * Prepares default mocks for service + */ + function prepareDefaultMock(): void { + spyOn(authService, 'getUsername').and.callFake(() => testUserName); + const nodeId = 'saved-searches-node-id'; + spyOn(localStorage, 'getItem').and.callFake(() => nodeId); + service.init(); + } +}); diff --git a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts new file mode 100644 index 00000000000..e558606218a --- /dev/null +++ b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts @@ -0,0 +1,227 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NodesApi, NodeEntry } from '@alfresco/js-api'; +import { Injectable } from '@angular/core'; +import { Observable, of, from, ReplaySubject, throwError } from 'rxjs'; +import { catchError, concatMap, first, map, switchMap, take, tap } from 'rxjs/operators'; +import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { SavedSearch } from '../interfaces/saved-search.interface'; +import { AuthenticationService } from '@alfresco/adf-core'; +import { SavedSearchStrategy } from '../interfaces/saved-searches-strategy.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class SavedSearchesLegacyService implements SavedSearchStrategy { + private _nodesApi: NodesApi; + get nodesApi(): NodesApi { + this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance()); + return this._nodesApi; + } + + private readonly _savedSearches$ = new ReplaySubject(1); + readonly savedSearches$ = this._savedSearches$.asObservable(); + + private savedSearchFileNodeId: string; + private currentUserLocalStorageKey: string; + private createFileAttempt = false; + + constructor( + private readonly apiService: AlfrescoApiService, + private readonly authService: AuthenticationService + ) {} + + init(): void { + this.fetchSavedSearches(); + } + + getSavedSearches(): Observable { + return this.getSavedSearchesNodeId().pipe( + concatMap(() => + from(this.nodesApi.getNodeContent(this.savedSearchFileNodeId).then((content) => this.mapFileContentToSavedSearches(content))).pipe( + catchError((error) => { + if (!this.createFileAttempt) { + this.createFileAttempt = true; + localStorage.removeItem(this.getLocalStorageKey()); + return this.getSavedSearches(); + } + return throwError(() => error); + }) + ) + ) + ); + } + + saveSearch(newSaveSearch: Pick): Observable { + return this.getSavedSearches().pipe( + take(1), + switchMap((savedSearches: SavedSearch[]) => { + let updatedSavedSearches: SavedSearch[] = []; + + if (savedSearches.length < 5) { + updatedSavedSearches = [{ ...newSaveSearch, order: 0 }, ...savedSearches]; + } else { + const firstFiveSearches = savedSearches.slice(0, 5); + const restOfSearches = savedSearches.slice(5); + + updatedSavedSearches = [...firstFiveSearches, { ...newSaveSearch, order: 5 }, ...restOfSearches]; + } + + updatedSavedSearches = updatedSavedSearches.map((search, index) => ({ + ...search, + order: index + })); + + return from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSavedSearches))).pipe( + tap(() => this._savedSearches$.next(updatedSavedSearches)) + ); + }), + catchError((error) => { + console.error('Error saving new search:', error); + return throwError(() => error); + }) + ); + } + + editSavedSearch(updatedSavedSearch: SavedSearch): Observable { + let previousSavedSearches: SavedSearch[]; + return this.savedSearches$.pipe( + take(1), + map((savedSearches: SavedSearch[]) => { + previousSavedSearches = [...savedSearches]; + return savedSearches.map((search) => (search.order === updatedSavedSearch.order ? updatedSavedSearch : search)); + }), + tap((updatedSearches: SavedSearch[]) => { + this._savedSearches$.next(updatedSearches); + }), + switchMap((updatedSearches: SavedSearch[]) => + from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) + ), + catchError((error) => { + this._savedSearches$.next(previousSavedSearches); + return throwError(() => error); + }) + ); + } + + deleteSavedSearch(deletedSavedSearch: SavedSearch): Observable { + let previousSavedSearchesOrder: SavedSearch[]; + return this._savedSearches$.pipe( + take(1), + map((savedSearches: SavedSearch[]) => { + previousSavedSearchesOrder = [...savedSearches]; + const updatedSearches = savedSearches.filter((search) => search.order !== deletedSavedSearch.order); + return updatedSearches.map((search, index) => ({ + ...search, + order: index + })); + }), + tap((updatedSearches: SavedSearch[]) => { + this._savedSearches$.next(updatedSearches); + }), + switchMap((updatedSearches: SavedSearch[]) => + from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) + ), + catchError((error) => { + this._savedSearches$.next(previousSavedSearchesOrder); + return throwError(() => error); + }) + ); + } + + changeOrder(previousIndex: number, currentIndex: number): void { + let previousSavedSearchesOrder: SavedSearch[]; + this.savedSearches$ + .pipe( + take(1), + map((savedSearches: SavedSearch[]) => { + previousSavedSearchesOrder = [...savedSearches]; + const [movedSearch] = savedSearches.splice(previousIndex, 1); + savedSearches.splice(currentIndex, 0, movedSearch); + return savedSearches.map((search, index) => ({ + ...search, + order: index + })); + }), + tap((savedSearches: SavedSearch[]) => this._savedSearches$.next(savedSearches)), + switchMap((updatedSearches: SavedSearch[]) => + from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) + ), + catchError((error) => { + this._savedSearches$.next(previousSavedSearchesOrder); + return throwError(() => error); + }) + ) + .subscribe(); + } + + private getSavedSearchesNodeId(): Observable { + const localStorageKey = this.getLocalStorageKey(); + if (this.currentUserLocalStorageKey && this.currentUserLocalStorageKey !== localStorageKey) { + this._savedSearches$.next([]); + } + this.currentUserLocalStorageKey = localStorageKey; + let savedSearchesNodeId = localStorage.getItem(this.currentUserLocalStorageKey) ?? ''; + if (savedSearchesNodeId === '') { + return from(this.nodesApi.getNode('-my-', { relativePath: 'config.json' })).pipe( + first(), + concatMap((configNode) => { + savedSearchesNodeId = configNode.entry.id; + localStorage.setItem(this.currentUserLocalStorageKey, savedSearchesNodeId); + this.savedSearchFileNodeId = savedSearchesNodeId; + return savedSearchesNodeId; + }), + catchError((error) => { + const errorStatusCode = JSON.parse(error.message).error.statusCode; + if (errorStatusCode === 404) { + return this.createSavedSearchesNode('-my-').pipe( + first(), + map((node) => { + localStorage.setItem(this.currentUserLocalStorageKey, node.entry.id); + return node.entry.id; + }) + ); + } else { + return throwError(() => error); + } + }) + ); + } else { + this.savedSearchFileNodeId = savedSearchesNodeId; + return of(savedSearchesNodeId); + } + } + + private createSavedSearchesNode(parentNodeId: string): Observable { + return from(this.nodesApi.createNode(parentNodeId, { name: 'config.json', nodeType: 'cm:content' })); + } + + private async mapFileContentToSavedSearches(blob: Blob): Promise> { + return blob.text().then((content) => (content ? JSON.parse(content) : [])); + } + + private getLocalStorageKey(): string { + return `saved-searches-node-id__${this.authService.getUsername()}`; + } + + private fetchSavedSearches(): void { + this.getSavedSearches() + .pipe(take(1)) + .subscribe((searches) => this._savedSearches$.next(searches)); + } +} From 6fc92b2c3b55a5cd7c6d746d571b995a68631178 Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Wed, 22 Oct 2025 11:32:56 +0200 Subject: [PATCH 03/11] [ACS-9166]: updates existing modern service --- .../common/services/saved-searches.service.ts | 52 +++++-------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/lib/content-services/src/lib/common/services/saved-searches.service.ts b/lib/content-services/src/lib/common/services/saved-searches.service.ts index 4a30eff2499..02ef7409e24 100644 --- a/lib/content-services/src/lib/common/services/saved-searches.service.ts +++ b/lib/content-services/src/lib/common/services/saved-searches.service.ts @@ -22,6 +22,7 @@ import { catchError, concatMap, first, map, switchMap, take, tap } from 'rxjs/op import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { SavedSearch } from '../interfaces/saved-search.interface'; import { AuthenticationService } from '@alfresco/adf-core'; +import { SavedSearchStrategy } from '../interfaces/saved-searches-strategy.interface'; export interface SavedSearchesPreferencesApiService { getPreference: (personId: string, preferenceName: string, opts?: ContentFieldsQuery) => Promise | Observable; @@ -33,7 +34,7 @@ export const SAVED_SEARCHES_SERVICE_PREFERENCES = new InjectionToken(1); + private readonly _savedSearches$ = new ReplaySubject(1); + readonly savedSearches$ = this._savedSearches$.asObservable(); constructor( private readonly apiService: AlfrescoApiService, @@ -65,11 +67,6 @@ export class SavedSearchesService { this.fetchSavedSearches(); } - /** - * Gets a list of saved searches by user. - * - * @returns SavedSearch list containing user saved searches - */ getSavedSearches(): Observable { const savedSearchesMigrated = localStorage.getItem(this.getLocalStorageKey()) ?? ''; if (savedSearchesMigrated === 'true') { @@ -89,13 +86,6 @@ export class SavedSearchesService { } } - /** - * Saves a new search into state and updates state. If there are less than 5 searches, - * it will be pushed on first place, if more it will be pushed to 6th place. - * - * @param newSaveSearch object { name: string, description: string, encodedUrl: string } - * @returns NodeEntry - */ saveSearch(newSaveSearch: Pick): Observable { return this.getSavedSearches().pipe( take(1), @@ -118,7 +108,7 @@ export class SavedSearchesService { return from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSavedSearches))).pipe( map((preference) => JSON.parse(preference.entry.value)), - tap(() => this.savedSearches$.next(updatedSavedSearches)) + tap(() => this._savedSearches$.next(updatedSavedSearches)) ); }), catchError((error) => { @@ -128,12 +118,6 @@ export class SavedSearchesService { ); } - /** - * Replace Save Search with new one and also updates the state. - * - * @param updatedSavedSearch - updated Save Search - * @returns NodeEntry - */ editSavedSearch(updatedSavedSearch: SavedSearch): Observable { let previousSavedSearches: SavedSearch[]; return this.savedSearches$.pipe( @@ -143,7 +127,7 @@ export class SavedSearchesService { return savedSearches.map((search) => (search.order === updatedSavedSearch.order ? updatedSavedSearch : search)); }), tap((updatedSearches: SavedSearch[]) => { - this.savedSearches$.next(updatedSearches); + this._savedSearches$.next(updatedSearches); }), switchMap((updatedSearches: SavedSearch[]) => from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( @@ -151,18 +135,12 @@ export class SavedSearchesService { ) ), catchError((error) => { - this.savedSearches$.next(previousSavedSearches); + this._savedSearches$.next(previousSavedSearches); return throwError(() => error); }) ); } - /** - * Deletes Save Search and update state. - * - * @param deletedSavedSearch - Save Search to delete - * @returns NodeEntry - */ deleteSavedSearch(deletedSavedSearch: SavedSearch): Observable { let previousSavedSearchesOrder: SavedSearch[]; return this.savedSearches$.pipe( @@ -176,7 +154,7 @@ export class SavedSearchesService { })); }), tap((updatedSearches: SavedSearch[]) => { - this.savedSearches$.next(updatedSearches); + this._savedSearches$.next(updatedSearches); }), switchMap((updatedSearches: SavedSearch[]) => from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( @@ -184,18 +162,12 @@ export class SavedSearchesService { ) ), catchError((error) => { - this.savedSearches$.next(previousSavedSearchesOrder); + this._savedSearches$.next(previousSavedSearchesOrder); return throwError(() => error); }) ); } - /** - * Reorders saved search place - * - * @param previousIndex - previous index of saved search - * @param currentIndex - new index of saved search - */ changeOrder(previousIndex: number, currentIndex: number): void { let previousSavedSearchesOrder: SavedSearch[]; this.savedSearches$ @@ -210,14 +182,14 @@ export class SavedSearchesService { order: index })); }), - tap((savedSearches: SavedSearch[]) => this.savedSearches$.next(savedSearches)), + tap((savedSearches: SavedSearch[]) => this._savedSearches$.next(savedSearches)), switchMap((updatedSearches: SavedSearch[]) => from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( map((preference) => JSON.parse(preference.entry.value)) ) ), catchError((error) => { - this.savedSearches$.next(previousSavedSearchesOrder); + this._savedSearches$.next(previousSavedSearchesOrder); return throwError(() => error); }) ) @@ -255,7 +227,7 @@ export class SavedSearchesService { private fetchSavedSearches(): void { this.getSavedSearches() .pipe(take(1)) - .subscribe((searches) => this.savedSearches$.next(searches)); + .subscribe((searches) => this._savedSearches$.next(searches)); } private migrateSavedSearches(): Observable { From a68de54759fce71c2b018a27fa350c8b8a0db44a Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Fri, 24 Oct 2025 22:58:38 +0200 Subject: [PATCH 04/11] [ACS-9166]: introduces saved search base class --- .../saved-searches-base.service.spec.ts | 139 ++++++++++++++++ .../services/saved-searches-base.service.ts | 148 ++++++++++++++++++ .../lib/mock/saved-searches-derived.mock.ts | 47 ++++++ 3 files changed, 334 insertions(+) create mode 100644 lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts create mode 100644 lib/content-services/src/lib/common/services/saved-searches-base.service.ts create mode 100644 lib/content-services/src/lib/mock/saved-searches-derived.mock.ts diff --git a/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts new file mode 100644 index 00000000000..e8d822dc784 --- /dev/null +++ b/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts @@ -0,0 +1,139 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { AlfrescoApiServiceMock } from '../../mock'; +import { MockSavedSearchesService } from '../../mock/saved-searches-derived.mock'; +import { SavedSearch } from '../interfaces/saved-search.interface'; +import { Subject } from 'rxjs'; +import { AuthenticationService } from '@alfresco/adf-core'; + +describe('SavedSearchesBaseService', () => { + let service: MockSavedSearchesService; + + const SAVED_SEARCHES_CONTENT = [ + { name: 'Search 1', description: 'Description 1', encodedUrl: 'url1', order: 0 }, + { name: 'Search 2', description: 'Description 2', encodedUrl: 'url2', order: 1 } + ]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, + { provide: AuthenticationService, useValue: { getUsername: () => {}, onLogin: new Subject() } }, + MockSavedSearchesService + ] + }); + service = TestBed.inject(MockSavedSearchesService); + }); + + it('should emit loaded data in savedSearches$ on init', (done) => { + service.savedSearches$.subscribe((value) => { + expect(value).toEqual(SAVED_SEARCHES_CONTENT); + done(); + }); + service.mockFetch(SAVED_SEARCHES_CONTENT); + service.init(); + }); + + it('should emit updated searches with correct order if total of saved searches is less than 5', (done) => { + service.mockFetch(SAVED_SEARCHES_CONTENT); + const newSearch = { name: 'new-search' } as SavedSearch; + service.saveSearch(newSearch).subscribe(() => { + const args = (service.updateSpy as jasmine.Spy).calls.mostRecent().args[0]; + + expect(args.length).toBe(1 + SAVED_SEARCHES_CONTENT.length); + + expect(args[0]).toEqual({ ...newSearch, order: 0 }); + + service.savedSearches$.subscribe((v) => { + expect(v).toEqual(args); + done(); + }); + }); + }); + + it('should emit updated searches with correct order if total of saved searches is more than 5', (done) => { + const moreSavedSearches = [...SAVED_SEARCHES_CONTENT, ...SAVED_SEARCHES_CONTENT, ...SAVED_SEARCHES_CONTENT]; + service.mockFetch(moreSavedSearches); + const newSearch = { name: 'new-search' } as SavedSearch; + service.saveSearch(newSearch).subscribe(() => { + const args = (service.updateSpy as jasmine.Spy).calls.mostRecent().args[0]; + + expect(args.length).toBe(1 + moreSavedSearches.length); + + expect(args[5]).toEqual({ ...newSearch, order: 5 }); + + service.savedSearches$.subscribe((v) => { + expect(v).toEqual(args); + done(); + }); + }); + }); + + it('should edit a search and emit updated saved searches', (done) => { + service.mockFetch(SAVED_SEARCHES_CONTENT); + service.init(); + const updatedSearch = { name: 'updated-search', order: 0 } as SavedSearch; + service.editSavedSearch(updatedSearch).subscribe(() => { + const args = (service.updateSpy as jasmine.Spy).calls.mostRecent().args[0]; + expect(args[0]).toEqual(updatedSearch); + + service.savedSearches$.subscribe((searches) => { + expect(searches[0]).toEqual(updatedSearch); + done(); + }); + }); + }); + + it('should delete a search and emit updated saved searches', (done) => { + service.mockFetch(SAVED_SEARCHES_CONTENT); + service.init(); + const searchToDelete = { name: 'Search 1', order: 0 } as SavedSearch; + service.deleteSavedSearch(searchToDelete).subscribe(() => { + const args = (service.updateSpy as jasmine.Spy).calls.mostRecent().args[0]; + expect(args.find((s: SavedSearch) => s.name === 'Search 1')).toBeUndefined(); + + service.savedSearches$.subscribe((searches) => { + expect(searches.length).toBe(1); + expect(searches[0].name).toBe('Search 2'); + expect(searches[0].order).toBe(0); + done(); + }); + }); + }); + + it('should change order of saved searches and emit updated saved searches', (done) => { + const updatedOrder = [ + { ...SAVED_SEARCHES_CONTENT[1], order: 0 }, + { ...SAVED_SEARCHES_CONTENT[0], order: 1 } + ]; + service.mockFetch(SAVED_SEARCHES_CONTENT); + service.init(); + service.changeOrder(1, 0); + + service.savedSearches$.subscribe((searches) => { + expect(service.updateSpy).toHaveBeenCalledWith(updatedOrder); + + expect(searches.length).toBe(SAVED_SEARCHES_CONTENT.length); + expect(searches[0]).toEqual(updatedOrder[0]); + expect(searches[1]).toEqual(updatedOrder[1]); + done(); + }); + }); +}); diff --git a/lib/content-services/src/lib/common/services/saved-searches-base.service.ts b/lib/content-services/src/lib/common/services/saved-searches-base.service.ts new file mode 100644 index 00000000000..3bf4bc53330 --- /dev/null +++ b/lib/content-services/src/lib/common/services/saved-searches-base.service.ts @@ -0,0 +1,148 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { SavedSearchStrategy } from '../interfaces/saved-searches-strategy.interface'; +import { AuthenticationService } from '@alfresco/adf-core'; +import { ReplaySubject, Observable, catchError, switchMap, take, tap, throwError, map } from 'rxjs'; +import { NodeEntry, NodesApi } from '@alfresco/js-api'; +import { SavedSearch } from '../interfaces/saved-search.interface'; +import { AlfrescoApiService } from '../../services/alfresco-api.service'; + +@Injectable() +export abstract class SavedSearchesBaseService implements SavedSearchStrategy { + private _nodesApi: NodesApi; + + protected readonly _savedSearches$ = new ReplaySubject(1); + readonly savedSearches$: Observable = this._savedSearches$.asObservable(); + + get nodesApi(): NodesApi { + this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance()); + return this._nodesApi; + } + + constructor( + protected readonly apiService: AlfrescoApiService, + protected readonly authService: AuthenticationService + ) {} + + protected abstract fetchAllSavedSearches(): Observable; + protected abstract updateSavedSearches(searches: SavedSearch[]): Observable; + + init(): void { + this.fetchSavedSearches(); + } + + getSavedSearches(): Observable { + return this.fetchAllSavedSearches(); + } + + saveSearch(newSaveSearch: Pick): Observable { + return this.fetchAllSavedSearches().pipe( + take(1), + switchMap((savedSearches: SavedSearch[]) => { + let updatedSavedSearches: SavedSearch[] = []; + + if (savedSearches.length < 5) { + updatedSavedSearches = [{ ...newSaveSearch, order: 0 }, ...savedSearches]; + } else { + const firstFiveSearches = savedSearches.slice(0, 5); + const restOfSearches = savedSearches.slice(5); + updatedSavedSearches = [...firstFiveSearches, { ...newSaveSearch, order: 5 }, ...restOfSearches]; + } + + updatedSavedSearches = updatedSavedSearches.map((search, index) => ({ ...search, order: index })); + + return this.updateSavedSearches(updatedSavedSearches).pipe(tap(() => this._savedSearches$.next(updatedSavedSearches))); + }), + catchError((error) => { + console.error('Error saving new search:', error); + return throwError(() => error); + }) + ); + } + + editSavedSearch(updatedSavedSearch: SavedSearch): Observable { + let previousSavedSearches: SavedSearch[]; + return this.savedSearches$.pipe( + take(1), + map((savedSearches: SavedSearch[]) => { + previousSavedSearches = [...savedSearches]; + return savedSearches.map((search) => (search.order === updatedSavedSearch.order ? updatedSavedSearch : search)); + }), + tap((updatedSearches: SavedSearch[]) => { + this._savedSearches$.next(updatedSearches); + }), + switchMap((updatedSearches: SavedSearch[]) => this.updateSavedSearches(updatedSearches)), + catchError((error) => { + this._savedSearches$.next(previousSavedSearches); + return throwError(() => error); + }) + ); + } + + deleteSavedSearch(deletedSavedSearch: SavedSearch): Observable { + let previousSavedSearchesOrder: SavedSearch[]; + return this.savedSearches$.pipe( + take(1), + map((savedSearches: SavedSearch[]) => { + previousSavedSearchesOrder = [...savedSearches]; + const updatedSearches = savedSearches.filter((search) => search.order !== deletedSavedSearch.order); + return updatedSearches.map((search, index) => ({ ...search, order: index })); + }), + tap((updatedSearches: SavedSearch[]) => { + this._savedSearches$.next(updatedSearches); + }), + switchMap((updatedSearches: SavedSearch[]) => this.updateSavedSearches(updatedSearches)), + catchError((error) => { + this._savedSearches$.next(previousSavedSearchesOrder); + return throwError(() => error); + }) + ); + } + + changeOrder(previousIndex: number, currentIndex: number): void { + let previousSavedSearchesOrder: SavedSearch[]; + this.savedSearches$ + .pipe( + take(1), + map((savedSearches: SavedSearch[]) => { + previousSavedSearchesOrder = [...savedSearches]; + const [movedSearch] = savedSearches.splice(previousIndex, 1); + savedSearches.splice(currentIndex, 0, movedSearch); + return savedSearches.map((search, index) => ({ ...search, order: index })); + }), + tap((savedSearches: SavedSearch[]) => this._savedSearches$.next(savedSearches)), + switchMap((updatedSearches: SavedSearch[]) => this.updateSavedSearches(updatedSearches)), + catchError((error) => { + this._savedSearches$.next(previousSavedSearchesOrder); + return throwError(() => error); + }) + ) + .subscribe(); + } + + protected resetSavedSearchesStream(): void { + this._savedSearches$.next([]); + } + + private fetchSavedSearches(): void { + this.getSavedSearches() + .pipe(take(1)) + .subscribe((searches) => this._savedSearches$.next(searches)); + } +} diff --git a/lib/content-services/src/lib/mock/saved-searches-derived.mock.ts b/lib/content-services/src/lib/mock/saved-searches-derived.mock.ts new file mode 100644 index 00000000000..adb49555562 --- /dev/null +++ b/lib/content-services/src/lib/mock/saved-searches-derived.mock.ts @@ -0,0 +1,47 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NodeEntry } from '@alfresco/js-api'; +import { SavedSearchesBaseService } from '../common/services/saved-searches-base.service'; +import { of, Observable, ReplaySubject } from 'rxjs'; +import { AlfrescoApiService } from '../services/alfresco-api.service'; +import { AuthenticationService } from '@alfresco/adf-core'; +import { SavedSearch } from '../common/interfaces/saved-search.interface'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class MockSavedSearchesService extends SavedSearchesBaseService { + public fetchSubject = new ReplaySubject(); + + public updateSpy = jasmine.createSpy('updateSavedSearches').and.returnValue(of({} as NodeEntry)); + + constructor(apiService: AlfrescoApiService, authService: AuthenticationService) { + super(apiService, authService); + } + + protected fetchAllSavedSearches(): Observable { + return this.fetchSubject.asObservable(); + } + + protected updateSavedSearches(searches: SavedSearch[]): Observable { + return this.updateSpy(searches); + } + + public mockFetch(searches: SavedSearch[]): void { + this.fetchSubject.next(searches); + } +} From af66fecfae467891f09efb80b34629ee2118c78e Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Fri, 24 Oct 2025 23:00:05 +0200 Subject: [PATCH 05/11] [ACS-9166]: migrates both saved searches to inheritance model --- .../services/saved-searches-legacy.service.ts | 143 ++--------------- .../common/services/saved-searches.service.ts | 146 ++---------------- 2 files changed, 23 insertions(+), 266 deletions(-) diff --git a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts index e558606218a..8fb9196b996 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts @@ -15,42 +15,28 @@ * limitations under the License. */ -import { NodesApi, NodeEntry } from '@alfresco/js-api'; +import { NodeEntry } from '@alfresco/js-api'; import { Injectable } from '@angular/core'; -import { Observable, of, from, ReplaySubject, throwError } from 'rxjs'; -import { catchError, concatMap, first, map, switchMap, take, tap } from 'rxjs/operators'; +import { Observable, of, from, throwError } from 'rxjs'; +import { catchError, concatMap, first, map } from 'rxjs/operators'; import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { SavedSearch } from '../interfaces/saved-search.interface'; import { AuthenticationService } from '@alfresco/adf-core'; -import { SavedSearchStrategy } from '../interfaces/saved-searches-strategy.interface'; +import { SavedSearchesBaseService } from './saved-searches-base.service'; @Injectable({ providedIn: 'root' }) -export class SavedSearchesLegacyService implements SavedSearchStrategy { - private _nodesApi: NodesApi; - get nodesApi(): NodesApi { - this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance()); - return this._nodesApi; - } - - private readonly _savedSearches$ = new ReplaySubject(1); - readonly savedSearches$ = this._savedSearches$.asObservable(); - +export class SavedSearchesLegacyService extends SavedSearchesBaseService { private savedSearchFileNodeId: string; private currentUserLocalStorageKey: string; private createFileAttempt = false; - constructor( - private readonly apiService: AlfrescoApiService, - private readonly authService: AuthenticationService - ) {} - - init(): void { - this.fetchSavedSearches(); + constructor(apiService: AlfrescoApiService, authService: AuthenticationService) { + super(apiService, authService); } - getSavedSearches(): Observable { + protected fetchAllSavedSearches(): Observable { return this.getSavedSearchesNodeId().pipe( concatMap(() => from(this.nodesApi.getNodeContent(this.savedSearchFileNodeId).then((content) => this.mapFileContentToSavedSearches(content))).pipe( @@ -58,7 +44,7 @@ export class SavedSearchesLegacyService implements SavedSearchStrategy { if (!this.createFileAttempt) { this.createFileAttempt = true; localStorage.removeItem(this.getLocalStorageKey()); - return this.getSavedSearches(); + return this.fetchAllSavedSearches(); } return throwError(() => error); }) @@ -67,113 +53,14 @@ export class SavedSearchesLegacyService implements SavedSearchStrategy { ); } - saveSearch(newSaveSearch: Pick): Observable { - return this.getSavedSearches().pipe( - take(1), - switchMap((savedSearches: SavedSearch[]) => { - let updatedSavedSearches: SavedSearch[] = []; - - if (savedSearches.length < 5) { - updatedSavedSearches = [{ ...newSaveSearch, order: 0 }, ...savedSearches]; - } else { - const firstFiveSearches = savedSearches.slice(0, 5); - const restOfSearches = savedSearches.slice(5); - - updatedSavedSearches = [...firstFiveSearches, { ...newSaveSearch, order: 5 }, ...restOfSearches]; - } - - updatedSavedSearches = updatedSavedSearches.map((search, index) => ({ - ...search, - order: index - })); - - return from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSavedSearches))).pipe( - tap(() => this._savedSearches$.next(updatedSavedSearches)) - ); - }), - catchError((error) => { - console.error('Error saving new search:', error); - return throwError(() => error); - }) - ); - } - - editSavedSearch(updatedSavedSearch: SavedSearch): Observable { - let previousSavedSearches: SavedSearch[]; - return this.savedSearches$.pipe( - take(1), - map((savedSearches: SavedSearch[]) => { - previousSavedSearches = [...savedSearches]; - return savedSearches.map((search) => (search.order === updatedSavedSearch.order ? updatedSavedSearch : search)); - }), - tap((updatedSearches: SavedSearch[]) => { - this._savedSearches$.next(updatedSearches); - }), - switchMap((updatedSearches: SavedSearch[]) => - from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) - ), - catchError((error) => { - this._savedSearches$.next(previousSavedSearches); - return throwError(() => error); - }) - ); - } - - deleteSavedSearch(deletedSavedSearch: SavedSearch): Observable { - let previousSavedSearchesOrder: SavedSearch[]; - return this._savedSearches$.pipe( - take(1), - map((savedSearches: SavedSearch[]) => { - previousSavedSearchesOrder = [...savedSearches]; - const updatedSearches = savedSearches.filter((search) => search.order !== deletedSavedSearch.order); - return updatedSearches.map((search, index) => ({ - ...search, - order: index - })); - }), - tap((updatedSearches: SavedSearch[]) => { - this._savedSearches$.next(updatedSearches); - }), - switchMap((updatedSearches: SavedSearch[]) => - from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) - ), - catchError((error) => { - this._savedSearches$.next(previousSavedSearchesOrder); - return throwError(() => error); - }) - ); - } - - changeOrder(previousIndex: number, currentIndex: number): void { - let previousSavedSearchesOrder: SavedSearch[]; - this.savedSearches$ - .pipe( - take(1), - map((savedSearches: SavedSearch[]) => { - previousSavedSearchesOrder = [...savedSearches]; - const [movedSearch] = savedSearches.splice(previousIndex, 1); - savedSearches.splice(currentIndex, 0, movedSearch); - return savedSearches.map((search, index) => ({ - ...search, - order: index - })); - }), - tap((savedSearches: SavedSearch[]) => this._savedSearches$.next(savedSearches)), - switchMap((updatedSearches: SavedSearch[]) => - from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) - ), - catchError((error) => { - this._savedSearches$.next(previousSavedSearchesOrder); - return throwError(() => error); - }) - ) - .subscribe(); + protected updateSavedSearches(searches: SavedSearch[]): Observable { + return from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(searches))); } private getSavedSearchesNodeId(): Observable { const localStorageKey = this.getLocalStorageKey(); if (this.currentUserLocalStorageKey && this.currentUserLocalStorageKey !== localStorageKey) { - this._savedSearches$.next([]); + this.resetSavedSearchesStream(); } this.currentUserLocalStorageKey = localStorageKey; let savedSearchesNodeId = localStorage.getItem(this.currentUserLocalStorageKey) ?? ''; @@ -218,10 +105,4 @@ export class SavedSearchesLegacyService implements SavedSearchStrategy { private getLocalStorageKey(): string { return `saved-searches-node-id__${this.authService.getUsername()}`; } - - private fetchSavedSearches(): void { - this.getSavedSearches() - .pipe(take(1)) - .subscribe((searches) => this._savedSearches$.next(searches)); - } } diff --git a/lib/content-services/src/lib/common/services/saved-searches.service.ts b/lib/content-services/src/lib/common/services/saved-searches.service.ts index 02ef7409e24..32fc945e6e7 100644 --- a/lib/content-services/src/lib/common/services/saved-searches.service.ts +++ b/lib/content-services/src/lib/common/services/saved-searches.service.ts @@ -15,14 +15,14 @@ * limitations under the License. */ -import { NodesApi, NodeEntry, PreferencesApi, ContentFieldsQuery, PreferenceEntry } from '@alfresco/js-api'; +import { NodeEntry, PreferencesApi, ContentFieldsQuery, PreferenceEntry } from '@alfresco/js-api'; import { inject, Injectable, InjectionToken } from '@angular/core'; -import { Observable, of, from, ReplaySubject, throwError } from 'rxjs'; +import { Observable, of, from, throwError } from 'rxjs'; import { catchError, concatMap, first, map, switchMap, take, tap } from 'rxjs/operators'; import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { SavedSearch } from '../interfaces/saved-search.interface'; import { AuthenticationService } from '@alfresco/adf-core'; -import { SavedSearchStrategy } from '../interfaces/saved-searches-strategy.interface'; +import { SavedSearchesBaseService } from './saved-searches-base.service'; export interface SavedSearchesPreferencesApiService { getPreference: (personId: string, preferenceName: string, opts?: ContentFieldsQuery) => Promise | Observable; @@ -34,17 +34,11 @@ export const SAVED_SEARCHES_SERVICE_PREFERENCES = new InjectionToken(1); - readonly savedSearches$ = this._savedSearches$.asObservable(); - - constructor( - private readonly apiService: AlfrescoApiService, - private readonly authService: AuthenticationService - ) {} - - init(): void { - this.fetchSavedSearches(); + constructor(apiService: AlfrescoApiService, authService: AuthenticationService) { + super(apiService, authService); } - getSavedSearches(): Observable { + protected fetchAllSavedSearches(): Observable { const savedSearchesMigrated = localStorage.getItem(this.getLocalStorageKey()) ?? ''; if (savedSearchesMigrated === 'true') { return this.getSavedSearchesFromPreferenceApi(); } else { return this.getSavedSearchesNodeId().pipe( take(1), - concatMap(() => { + switchMap(() => { if (this.savedSearchFileNodeId !== '') { return this.migrateSavedSearches(); } else { @@ -86,116 +72,12 @@ export class SavedSearchesService implements SavedSearchStrategy { } } - saveSearch(newSaveSearch: Pick): Observable { - return this.getSavedSearches().pipe( - take(1), - switchMap((savedSearches: SavedSearch[]) => { - let updatedSavedSearches: SavedSearch[] = []; - - if (savedSearches.length < 5) { - updatedSavedSearches = [{ ...newSaveSearch, order: 0 }, ...savedSearches]; - } else { - const firstFiveSearches = savedSearches.slice(0, 5); - const restOfSearches = savedSearches.slice(5); - - updatedSavedSearches = [...firstFiveSearches, { ...newSaveSearch, order: 5 }, ...restOfSearches]; - } - - updatedSavedSearches = updatedSavedSearches.map((search, index) => ({ - ...search, - order: index - })); - - return from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSavedSearches))).pipe( - map((preference) => JSON.parse(preference.entry.value)), - tap(() => this._savedSearches$.next(updatedSavedSearches)) - ); - }), - catchError((error) => { - console.error('Error saving new search:', error); - return throwError(() => error); - }) + protected updateSavedSearches(updatedSavedSearches: SavedSearch[]): Observable { + return from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSavedSearches))).pipe( + map((preference) => JSON.parse(preference.entry.value)) ); } - editSavedSearch(updatedSavedSearch: SavedSearch): Observable { - let previousSavedSearches: SavedSearch[]; - return this.savedSearches$.pipe( - take(1), - map((savedSearches: SavedSearch[]) => { - previousSavedSearches = [...savedSearches]; - return savedSearches.map((search) => (search.order === updatedSavedSearch.order ? updatedSavedSearch : search)); - }), - tap((updatedSearches: SavedSearch[]) => { - this._savedSearches$.next(updatedSearches); - }), - switchMap((updatedSearches: SavedSearch[]) => - from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( - map((preference) => JSON.parse(preference.entry.value)) - ) - ), - catchError((error) => { - this._savedSearches$.next(previousSavedSearches); - return throwError(() => error); - }) - ); - } - - deleteSavedSearch(deletedSavedSearch: SavedSearch): Observable { - let previousSavedSearchesOrder: SavedSearch[]; - return this.savedSearches$.pipe( - take(1), - map((savedSearches: SavedSearch[]) => { - previousSavedSearchesOrder = [...savedSearches]; - const updatedSearches = savedSearches.filter((search) => search.order !== deletedSavedSearch.order); - return updatedSearches.map((search, index) => ({ - ...search, - order: index - })); - }), - tap((updatedSearches: SavedSearch[]) => { - this._savedSearches$.next(updatedSearches); - }), - switchMap((updatedSearches: SavedSearch[]) => - from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( - map((preference) => JSON.parse(preference.entry.value)) - ) - ), - catchError((error) => { - this._savedSearches$.next(previousSavedSearchesOrder); - return throwError(() => error); - }) - ); - } - - changeOrder(previousIndex: number, currentIndex: number): void { - let previousSavedSearchesOrder: SavedSearch[]; - this.savedSearches$ - .pipe( - take(1), - map((savedSearches: SavedSearch[]) => { - previousSavedSearchesOrder = [...savedSearches]; - const [movedSearch] = savedSearches.splice(previousIndex, 1); - savedSearches.splice(currentIndex, 0, movedSearch); - return savedSearches.map((search, index) => ({ - ...search, - order: index - })); - }), - tap((savedSearches: SavedSearch[]) => this._savedSearches$.next(savedSearches)), - switchMap((updatedSearches: SavedSearch[]) => - from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( - map((preference) => JSON.parse(preference.entry.value)) - ) - ), - catchError((error) => { - this._savedSearches$.next(previousSavedSearchesOrder); - return throwError(() => error); - }) - ) - .subscribe(); - } - private getSavedSearchesNodeId(): Observable { return from(this.nodesApi.getNode('-my-', { relativePath: 'config.json' })).pipe( first(), @@ -224,12 +106,6 @@ export class SavedSearchesService implements SavedSearchStrategy { return `saved-searches-${this.authService.getUsername()}-migrated`; } - private fetchSavedSearches(): void { - this.getSavedSearches() - .pipe(take(1)) - .subscribe((searches) => this._savedSearches$.next(searches)); - } - private migrateSavedSearches(): Observable { return from(this.nodesApi.getNodeContent(this.savedSearchFileNodeId).then((content) => this.mapFileContentToSavedSearches(content))).pipe( tap((savedSearches) => { From 65b18bbdfbcfb705db54fb06dafa8d328591af20 Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Fri, 24 Oct 2025 23:00:35 +0200 Subject: [PATCH 06/11] [ACS-9166]: tests clean up --- .../saved-searches-legacy.service.spec.ts | 41 ------------------- .../services/saved-searches.service.spec.ts | 39 ------------------ 2 files changed, 80 deletions(-) diff --git a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts index 7331f2ad256..4b643fcafa9 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts @@ -153,45 +153,4 @@ describe('SavedSearchesLegacyService', () => { service.saveSearch(newSearch).subscribe(); }); - - it('should edit a search', (done) => { - const updatedSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3', order: 0 }; - prepareDefaultMock(); - - service.editSavedSearch(updatedSearch).subscribe(() => { - service.savedSearches$.subscribe((searches) => { - expect(searches.length).toBe(2); - expect(searches[0].name).toBe('Search 3'); - expect(searches[0].order).toBe(0); - - expect(searches[1].name).toBe('Search 2'); - expect(searches[1].order).toBe(1); - done(); - }); - }); - }); - - it('should delete a search', (done) => { - const searchToDelete = { name: 'Search 1', description: 'Description 1', encodedUrl: 'url1', order: 0 }; - prepareDefaultMock(); - - service.deleteSavedSearch(searchToDelete).subscribe(() => { - service.savedSearches$.subscribe((searches) => { - expect(searches.length).toBe(1); - expect(searches[0].name).toBe('Search 2'); - expect(searches[0].order).toBe(0); - done(); - }); - }); - }); - - /** - * Prepares default mocks for service - */ - function prepareDefaultMock(): void { - spyOn(authService, 'getUsername').and.callFake(() => testUserName); - const nodeId = 'saved-searches-node-id'; - spyOn(localStorage, 'getItem').and.callFake(() => nodeId); - service.init(); - } }); diff --git a/lib/content-services/src/lib/common/services/saved-searches.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches.service.spec.ts index 9f025a94223..2b9e03bb989 100644 --- a/lib/content-services/src/lib/common/services/saved-searches.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches.service.spec.ts @@ -148,36 +148,6 @@ describe('SavedSearchesService', () => { }); }); }); - - it('should edit a search', (done) => { - const updatedSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3', order: 0 }; - prepareDefaultMock(); - - service.editSavedSearch(updatedSearch).subscribe(() => { - service.savedSearches$.subscribe((searches) => { - expect(searches.length).toBe(2); - expect(searches[0].name).toBe('Search 3'); - expect(searches[0].order).toBe(0); - expect(searches[1].name).toBe('Search 2'); - expect(searches[1].order).toBe(1); - done(); - }); - }); - }); - - it('should delete a search', (done) => { - const searchToDelete = { name: 'Search 1', description: 'Description 1', encodedUrl: 'url1', order: 0 }; - prepareDefaultMock(); - - service.deleteSavedSearch(searchToDelete).subscribe(() => { - service.savedSearches$.subscribe((searches) => { - expect(searches.length).toBe(1); - expect(searches[0].name).toBe('Search 2'); - expect(searches[0].order).toBe(0); - done(); - }); - }); - }); }); describe('Saved searches error handling', () => { @@ -209,13 +179,4 @@ describe('SavedSearchesService', () => { }); }); }); - - /** - * Prepares default mocks for service - */ - function prepareDefaultMock(): void { - spyOn(authService, 'getUsername').and.callFake(() => testUserName); - spyOn(localStorage, 'getItem').and.callFake(() => 'true'); - service.init(); - } }); From 504615d6d0262b6b8e468f82cfe798ec98ea4241 Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Mon, 27 Oct 2025 12:03:18 +0100 Subject: [PATCH 07/11] [ACS-9166]: imports fixes --- lib/content-services/src/lib/common/public-api.ts | 1 + .../lib/common/services/saved-searches-base.service.spec.ts | 2 +- .../src/lib/common/services/saved-searches-base.service.ts | 2 +- .../common/services/saved-searches-legacy.service.spec.ts | 2 +- .../src/lib/common/services/saved-searches-legacy.service.ts | 2 +- .../src/lib/mock/saved-searches-derived.mock.ts | 5 ++--- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/content-services/src/lib/common/public-api.ts b/lib/content-services/src/lib/common/public-api.ts index cca99f28e47..d66f9fcbd6d 100644 --- a/lib/content-services/src/lib/common/public-api.ts +++ b/lib/content-services/src/lib/common/public-api.ts @@ -26,6 +26,7 @@ export * from './services/people-content.service'; export * from './services/content.service'; export * from './services/saved-searches.service'; export * from './services/saved-searches-legacy.service'; +export * from './services/saved-searches-base.service'; export * from './events/file.event'; diff --git a/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts index e8d822dc784..939d2c49507 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts @@ -16,7 +16,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { AlfrescoApiService } from '../../services'; import { AlfrescoApiServiceMock } from '../../mock'; import { MockSavedSearchesService } from '../../mock/saved-searches-derived.mock'; import { SavedSearch } from '../interfaces/saved-search.interface'; diff --git a/lib/content-services/src/lib/common/services/saved-searches-base.service.ts b/lib/content-services/src/lib/common/services/saved-searches-base.service.ts index 3bf4bc53330..6a4e1da4bbb 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-base.service.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-base.service.ts @@ -21,7 +21,7 @@ import { AuthenticationService } from '@alfresco/adf-core'; import { ReplaySubject, Observable, catchError, switchMap, take, tap, throwError, map } from 'rxjs'; import { NodeEntry, NodesApi } from '@alfresco/js-api'; import { SavedSearch } from '../interfaces/saved-search.interface'; -import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { AlfrescoApiService } from '../../services'; @Injectable() export abstract class SavedSearchesBaseService implements SavedSearchStrategy { diff --git a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts index 4b643fcafa9..9e4ab654643 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts @@ -16,7 +16,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { AlfrescoApiService } from '../../services'; import { NodeEntry } from '@alfresco/js-api'; import { AlfrescoApiServiceMock } from '../../mock'; import { HttpClientTestingModule } from '@angular/common/http/testing'; diff --git a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts index 8fb9196b996..1cfb8f4657f 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.ts @@ -19,7 +19,7 @@ import { NodeEntry } from '@alfresco/js-api'; import { Injectable } from '@angular/core'; import { Observable, of, from, throwError } from 'rxjs'; import { catchError, concatMap, first, map } from 'rxjs/operators'; -import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { AlfrescoApiService } from '../../services'; import { SavedSearch } from '../interfaces/saved-search.interface'; import { AuthenticationService } from '@alfresco/adf-core'; import { SavedSearchesBaseService } from './saved-searches-base.service'; diff --git a/lib/content-services/src/lib/mock/saved-searches-derived.mock.ts b/lib/content-services/src/lib/mock/saved-searches-derived.mock.ts index adb49555562..a2da535b652 100644 --- a/lib/content-services/src/lib/mock/saved-searches-derived.mock.ts +++ b/lib/content-services/src/lib/mock/saved-searches-derived.mock.ts @@ -16,11 +16,10 @@ */ import { NodeEntry } from '@alfresco/js-api'; -import { SavedSearchesBaseService } from '../common/services/saved-searches-base.service'; import { of, Observable, ReplaySubject } from 'rxjs'; -import { AlfrescoApiService } from '../services/alfresco-api.service'; +import { AlfrescoApiService } from '../services'; import { AuthenticationService } from '@alfresco/adf-core'; -import { SavedSearch } from '../common/interfaces/saved-search.interface'; +import { SavedSearch, SavedSearchesBaseService } from '../common'; import { Injectable } from '@angular/core'; @Injectable() From 93998d56a9f7b0f76470e6e816b503c5c667cb65 Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Mon, 27 Oct 2025 12:19:11 +0100 Subject: [PATCH 08/11] [ACS-9166]: types and naming fixes --- .../saved-searches-base.service.spec.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts index 939d2c49507..1986eff3213 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts @@ -26,7 +26,7 @@ import { AuthenticationService } from '@alfresco/adf-core'; describe('SavedSearchesBaseService', () => { let service: MockSavedSearchesService; - const SAVED_SEARCHES_CONTENT = [ + const SAVED_SEARCHES_CONTENT: SavedSearch[] = [ { name: 'Search 1', description: 'Description 1', encodedUrl: 'url1', order: 0 }, { name: 'Search 2', description: 'Description 2', encodedUrl: 'url2', order: 1 } ]; @@ -61,8 +61,8 @@ describe('SavedSearchesBaseService', () => { expect(args[0]).toEqual({ ...newSearch, order: 0 }); - service.savedSearches$.subscribe((v) => { - expect(v).toEqual(args); + service.savedSearches$.subscribe((savedSearches) => { + expect(savedSearches).toEqual(args); done(); }); }); @@ -79,8 +79,8 @@ describe('SavedSearchesBaseService', () => { expect(args[5]).toEqual({ ...newSearch, order: 5 }); - service.savedSearches$.subscribe((v) => { - expect(v).toEqual(args); + service.savedSearches$.subscribe((savedSearches) => { + expect(savedSearches).toEqual(args); done(); }); }); @@ -94,8 +94,8 @@ describe('SavedSearchesBaseService', () => { const args = (service.updateSpy as jasmine.Spy).calls.mostRecent().args[0]; expect(args[0]).toEqual(updatedSearch); - service.savedSearches$.subscribe((searches) => { - expect(searches[0]).toEqual(updatedSearch); + service.savedSearches$.subscribe((savedSearches) => { + expect(savedSearches[0]).toEqual(updatedSearch); done(); }); }); @@ -107,19 +107,19 @@ describe('SavedSearchesBaseService', () => { const searchToDelete = { name: 'Search 1', order: 0 } as SavedSearch; service.deleteSavedSearch(searchToDelete).subscribe(() => { const args = (service.updateSpy as jasmine.Spy).calls.mostRecent().args[0]; - expect(args.find((s: SavedSearch) => s.name === 'Search 1')).toBeUndefined(); + expect(args.find((savedSearch: SavedSearch) => savedSearch.name === 'Search 1')).toBeUndefined(); - service.savedSearches$.subscribe((searches) => { - expect(searches.length).toBe(1); - expect(searches[0].name).toBe('Search 2'); - expect(searches[0].order).toBe(0); + service.savedSearches$.subscribe((savedSearches) => { + expect(savedSearches.length).toBe(1); + expect(savedSearches[0].name).toBe('Search 2'); + expect(savedSearches[0].order).toBe(0); done(); }); }); }); it('should change order of saved searches and emit updated saved searches', (done) => { - const updatedOrder = [ + const updatedOrder: SavedSearch[] = [ { ...SAVED_SEARCHES_CONTENT[1], order: 0 }, { ...SAVED_SEARCHES_CONTENT[0], order: 1 } ]; @@ -127,12 +127,12 @@ describe('SavedSearchesBaseService', () => { service.init(); service.changeOrder(1, 0); - service.savedSearches$.subscribe((searches) => { + service.savedSearches$.subscribe((savedSearches) => { expect(service.updateSpy).toHaveBeenCalledWith(updatedOrder); - expect(searches.length).toBe(SAVED_SEARCHES_CONTENT.length); - expect(searches[0]).toEqual(updatedOrder[0]); - expect(searches[1]).toEqual(updatedOrder[1]); + expect(savedSearches.length).toBe(SAVED_SEARCHES_CONTENT.length); + expect(savedSearches[0]).toEqual(updatedOrder[0]); + expect(savedSearches[1]).toEqual(updatedOrder[1]); done(); }); }); From 53d3076a6ff592f40222a831630c8555fa157caa Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Mon, 27 Oct 2025 16:34:03 +0100 Subject: [PATCH 09/11] [ACS-9166]: redundant types --- .../common/services/saved-searches-base.service.ts | 12 ++++++------ .../services/saved-searches-legacy.service.spec.ts | 4 +--- .../common/services/saved-searches.service.spec.ts | 2 -- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/content-services/src/lib/common/services/saved-searches-base.service.ts b/lib/content-services/src/lib/common/services/saved-searches-base.service.ts index 6a4e1da4bbb..f209d50c573 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-base.service.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-base.service.ts @@ -54,7 +54,7 @@ export abstract class SavedSearchesBaseService implements SavedSearchStrategy { saveSearch(newSaveSearch: Pick): Observable { return this.fetchAllSavedSearches().pipe( take(1), - switchMap((savedSearches: SavedSearch[]) => { + switchMap((savedSearches) => { let updatedSavedSearches: SavedSearch[] = []; if (savedSearches.length < 5) { @@ -80,14 +80,14 @@ export abstract class SavedSearchesBaseService implements SavedSearchStrategy { let previousSavedSearches: SavedSearch[]; return this.savedSearches$.pipe( take(1), - map((savedSearches: SavedSearch[]) => { + map((savedSearches) => { previousSavedSearches = [...savedSearches]; return savedSearches.map((search) => (search.order === updatedSavedSearch.order ? updatedSavedSearch : search)); }), - tap((updatedSearches: SavedSearch[]) => { + tap((updatedSearches) => { this._savedSearches$.next(updatedSearches); }), - switchMap((updatedSearches: SavedSearch[]) => this.updateSavedSearches(updatedSearches)), + switchMap((updatedSearches) => this.updateSavedSearches(updatedSearches)), catchError((error) => { this._savedSearches$.next(previousSavedSearches); return throwError(() => error); @@ -99,7 +99,7 @@ export abstract class SavedSearchesBaseService implements SavedSearchStrategy { let previousSavedSearchesOrder: SavedSearch[]; return this.savedSearches$.pipe( take(1), - map((savedSearches: SavedSearch[]) => { + map((savedSearches) => { previousSavedSearchesOrder = [...savedSearches]; const updatedSearches = savedSearches.filter((search) => search.order !== deletedSavedSearch.order); return updatedSearches.map((search, index) => ({ ...search, order: index })); @@ -120,7 +120,7 @@ export abstract class SavedSearchesBaseService implements SavedSearchStrategy { this.savedSearches$ .pipe( take(1), - map((savedSearches: SavedSearch[]) => { + map((savedSearches) => { previousSavedSearchesOrder = [...savedSearches]; const [movedSearch] = savedSearches.splice(previousIndex, 1); savedSearches.splice(currentIndex, 0, movedSearch); diff --git a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts index 9e4ab654643..4c5a893a619 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts @@ -19,7 +19,6 @@ import { TestBed } from '@angular/core/testing'; import { AlfrescoApiService } from '../../services'; import { NodeEntry } from '@alfresco/js-api'; import { AlfrescoApiServiceMock } from '../../mock'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { AuthenticationService } from '@alfresco/adf-core'; import { Subject } from 'rxjs'; import { SavedSearchesLegacyService } from './saved-searches-legacy.service'; @@ -28,7 +27,7 @@ describe('SavedSearchesLegacyService', () => { let service: SavedSearchesLegacyService; let authService: AuthenticationService; let testUserName: string; - let getNodeContentSpy: jasmine.Spy; + let getNodeContentSpy: jasmine.Spy; const testNodeId = 'test-node-id'; const SAVED_SEARCHES_NODE_ID = 'saved-searches-node-id__'; @@ -49,7 +48,6 @@ describe('SavedSearchesLegacyService', () => { beforeEach(() => { testUserName = 'test-user'; TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], providers: [ { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, { provide: AuthenticationService, useValue: { getUsername: () => {}, onLogin: new Subject() } }, diff --git a/lib/content-services/src/lib/common/services/saved-searches.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches.service.spec.ts index 2b9e03bb989..00fca12d300 100644 --- a/lib/content-services/src/lib/common/services/saved-searches.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches.service.spec.ts @@ -20,7 +20,6 @@ import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { AlfrescoApiServiceMock } from '../../mock'; import { NodeEntry } from '@alfresco/js-api'; import { SavedSearchesService } from './saved-searches.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { AuthenticationService } from '@alfresco/adf-core'; import { Subject } from 'rxjs'; @@ -48,7 +47,6 @@ describe('SavedSearchesService', () => { beforeEach(() => { testUserName = 'test-user'; TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], providers: [ { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, { provide: AuthenticationService, useValue: { getUsername: () => {}, onLogin: new Subject() } }, From 34cb40ccf383a2a64025fd33b3853114d2e8958e Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Mon, 27 Oct 2025 17:53:25 +0100 Subject: [PATCH 10/11] [ACS-9166]: introduces variable for search save mode threshold --- .../services/saved-searches-base.service.spec.ts | 4 ++-- .../common/services/saved-searches-base.service.ts | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts index 1986eff3213..480b2ea5695 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-base.service.spec.ts @@ -51,7 +51,7 @@ describe('SavedSearchesBaseService', () => { service.init(); }); - it('should emit updated searches with correct order if total of saved searches is less than 5', (done) => { + it('should emit updated searches with correct order if total of saved searches is less than limit (5)', (done) => { service.mockFetch(SAVED_SEARCHES_CONTENT); const newSearch = { name: 'new-search' } as SavedSearch; service.saveSearch(newSearch).subscribe(() => { @@ -68,7 +68,7 @@ describe('SavedSearchesBaseService', () => { }); }); - it('should emit updated searches with correct order if total of saved searches is more than 5', (done) => { + it('should emit updated searches with correct order if total of saved searches is more than limit (5)', (done) => { const moreSavedSearches = [...SAVED_SEARCHES_CONTENT, ...SAVED_SEARCHES_CONTENT, ...SAVED_SEARCHES_CONTENT]; service.mockFetch(moreSavedSearches); const newSearch = { name: 'new-search' } as SavedSearch; diff --git a/lib/content-services/src/lib/common/services/saved-searches-base.service.ts b/lib/content-services/src/lib/common/services/saved-searches-base.service.ts index f209d50c573..6c32ec47aa6 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-base.service.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-base.service.ts @@ -27,6 +27,8 @@ import { AlfrescoApiService } from '../../services'; export abstract class SavedSearchesBaseService implements SavedSearchStrategy { private _nodesApi: NodesApi; + private static readonly SAVE_MODE_THRESHOLD = 5; + protected readonly _savedSearches$ = new ReplaySubject(1); readonly savedSearches$: Observable = this._savedSearches$.asObservable(); @@ -35,7 +37,7 @@ export abstract class SavedSearchesBaseService implements SavedSearchStrategy { return this._nodesApi; } - constructor( + protected constructor( protected readonly apiService: AlfrescoApiService, protected readonly authService: AuthenticationService ) {} @@ -52,17 +54,18 @@ export abstract class SavedSearchesBaseService implements SavedSearchStrategy { } saveSearch(newSaveSearch: Pick): Observable { + const limit = SavedSearchesBaseService.SAVE_MODE_THRESHOLD; return this.fetchAllSavedSearches().pipe( take(1), switchMap((savedSearches) => { let updatedSavedSearches: SavedSearch[] = []; - if (savedSearches.length < 5) { + if (savedSearches.length < limit) { updatedSavedSearches = [{ ...newSaveSearch, order: 0 }, ...savedSearches]; } else { - const firstFiveSearches = savedSearches.slice(0, 5); - const restOfSearches = savedSearches.slice(5); - updatedSavedSearches = [...firstFiveSearches, { ...newSaveSearch, order: 5 }, ...restOfSearches]; + const upToLimitSearches = savedSearches.slice(0, limit); + const restSearches = savedSearches.slice(limit); + updatedSavedSearches = [...upToLimitSearches, { ...newSaveSearch, order: limit }, ...restSearches]; } updatedSavedSearches = updatedSavedSearches.map((search, index) => ({ ...search, order: index })); From 18596837e2c0e7e280e37daeb8c1f9ba2bf3478a Mon Sep 17 00:00:00 2001 From: Anton Ramanovich Date: Tue, 28 Oct 2025 11:32:07 +0100 Subject: [PATCH 11/11] [ACS-9166]: minor fix --- .../lib/common/services/saved-searches-legacy.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts index 4c5a893a619..e2449548f32 100644 --- a/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts +++ b/lib/content-services/src/lib/common/services/saved-searches-legacy.service.spec.ts @@ -27,7 +27,7 @@ describe('SavedSearchesLegacyService', () => { let service: SavedSearchesLegacyService; let authService: AuthenticationService; let testUserName: string; - let getNodeContentSpy: jasmine.Spy; + let getNodeContentSpy: jasmine.Spy<(nodeId: string) => Promise>; const testNodeId = 'test-node-id'; const SAVED_SEARCHES_NODE_ID = 'saved-searches-node-id__';