diff --git a/docs/core/services/saved-searches.service.md b/docs/core/services/saved-searches.service.md index 26ba0a7ffa..a5223cc959 100644 --- a/docs/core/services/saved-searches.service.md +++ b/docs/core/services/saved-searches.service.md @@ -1,7 +1,7 @@ # Saved Searches Service -Manages operations related to saving and retrieving user-defined searches in the Alfresco Process Services (APS) environment. +Manages operations related to saving and retrieving user-defined searches. ## Class members @@ -14,7 +14,7 @@ Manages operations related to saving and retrieving user-defined searches in the #### getSavedSearches(): [`Observable`](https://rxjs.dev/api/index/class/Observable)`` -Fetches the file with list of saved searches either from a locally cached node ID or by querying the APS server. Then it reads the file and maps JSON objects into SavedSearches +Fetches the file with list of saved searches either from a locally cached node ID or by querying the ACS server. Then it reads the file and maps JSON objects into SavedSearches - **Returns**: - [`Observable`](https://rxjs.dev/api/index/class/Observable)`` - An observable that emits the list of saved searches. @@ -51,14 +51,3 @@ this.savedSearchService.saveSearch(newSearch).subscribe((response) => { console.log('Saved new search:', response); }); ``` - -#### Creating Saved Searches Node - -When the saved searches file does not exist, it will be created: - -```typescript -this.savedSearchService.createSavedSearchesNode('parent-node-id').subscribe((node) => { - console.log('Created config.json node:', node); -}); -``` - 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 7f46da31fa..a683926a11 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 @@ -17,9 +17,9 @@ import { TestBed } from '@angular/core/testing'; import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { AlfrescoApiServiceMock } from '../../mock'; import { NodeEntry } from '@alfresco/js-api'; import { SavedSearchesService } from './saved-searches.service'; -import { AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { AuthenticationService } from '@alfresco/adf-core'; import { Subject } from 'rxjs'; @@ -28,10 +28,9 @@ describe('SavedSearchesService', () => { let service: SavedSearchesService; 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 LOCAL_STORAGE_KEY = 'saved-searches-test-user-migrated'; 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 } @@ -59,23 +58,28 @@ describe('SavedSearchesService', () => { service = TestBed.inject(SavedSearchesService); 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()); + spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob()); + spyOn(service.nodesApi, 'deleteNode').and.callFake(() => Promise.resolve()); + spyOn(service.preferencesApi, 'getPreference').and.callFake(() => + Promise.resolve({ entry: { id: 'saved-searches', value: SAVED_SEARCHES_CONTENT } }) + ); + spyOn(service.preferencesApi, 'updatePreference').and.callFake(() => + Promise.resolve({ entry: { id: 'saved-searches', value: SAVED_SEARCHES_CONTENT } }) + ); }); afterEach(() => { - localStorage.removeItem(SAVED_SEARCHES_NODE_ID + testUserName); + localStorage.removeItem(LOCAL_STORAGE_KEY); }); - it('should retrieve saved searches from the config.json file', (done) => { + it('should retrieve saved searches from the preferences API', (done) => { spyOn(authService, 'getUsername').and.callFake(() => testUserName); - spyOn(localStorage, 'getItem').and.callFake(() => testNodeId); - service.innit(); + spyOn(localStorage, 'getItem').and.callFake(() => 'true'); + service.init(); service.getSavedSearches().subscribe((searches) => { - expect(localStorage.getItem).toHaveBeenCalledWith(SAVED_SEARCHES_NODE_ID + testUserName); - expect(getNodeContentSpy).toHaveBeenCalledWith(testNodeId); + expect(localStorage.getItem).toHaveBeenCalledWith(LOCAL_STORAGE_KEY); + expect(service.preferencesApi.getPreference).toHaveBeenCalledWith('-me-', 'saved-searches'); expect(searches.length).toBe(2); expect(searches[0].name).toBe('Search 1'); expect(searches[1].name).toBe('Search 2'); @@ -83,48 +87,43 @@ describe('SavedSearchesService', () => { }); }); - it('should create config.json file if it does not exist', (done) => { - const error: Error = { name: 'test', message: '{ "error": { "statusCode": 404 } }' }; + it('should automatically migrate saved searches if config.json file exists', (done) => { + spyOn(localStorage, 'setItem'); spyOn(authService, 'getUsername').and.callFake(() => testUserName); - service.nodesApi.getNode = jasmine.createSpy().and.returnValue(Promise.reject(error)); - getNodeContentSpy.and.callFake(() => Promise.resolve(new Blob(['']))); - service.innit(); 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); + expect(service.nodesApi.getNodeContent).toHaveBeenCalledWith(testNodeId); + expect(localStorage.setItem).toHaveBeenCalledWith(LOCAL_STORAGE_KEY, 'true'); + expect(service.preferencesApi.updatePreference).toHaveBeenCalledWith('-me-', 'saved-searches', SAVED_SEARCHES_CONTENT); + expect(service.nodesApi.deleteNode).toHaveBeenCalledWith(testNodeId, { permanent: true }); + expect(searches.length).toBe(2); 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); + spyOn(localStorage, 'getItem').and.callFake(() => 'true'); const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' }; - service.innit(); + service.init(); service.saveSearch(newSearch).subscribe(() => { - expect(service.nodesApi.updateNodeContent).toHaveBeenCalledWith(nodeId, jasmine.any(String)); + expect(service.preferencesApi.updatePreference).toHaveBeenCalledWith('-me-', 'saved-searches', 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(); - }); + done(); }); }); it('should emit initial saved searches on subscription', (done) => { - const nodeId = 'saved-searches-node-id'; - spyOn(localStorage, 'getItem').and.returnValue(nodeId); - service.innit(); + spyOn(authService, 'getUsername').and.callFake(() => testUserName); + spyOn(localStorage, 'getItem').and.returnValue('true'); + service.init(); service.savedSearches$.pipe().subscribe((searches) => { expect(searches.length).toBe(2); expect(searches[0].name).toBe('Search 1'); + expect(service.preferencesApi.getPreference).toHaveBeenCalledWith('-me-', 'saved-searches'); done(); }); @@ -133,25 +132,18 @@ describe('SavedSearchesService', () => { it('should emit updated saved searches after saving a new search', (done) => { spyOn(authService, 'getUsername').and.callFake(() => testUserName); - spyOn(localStorage, 'getItem').and.callFake(() => testNodeId); + spyOn(localStorage, 'getItem').and.callFake(() => 'true'); const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' }; - service.innit(); - - let emissionCount = 0; + service.init(); - service.savedSearches$.subscribe((searches) => { - emissionCount++; - if (emissionCount === 1) { - expect(searches.length).toBe(2); - } - if (emissionCount === 2) { + service.saveSearch(newSearch).subscribe(() => { + service.savedSearches$.subscribe((searches) => { expect(searches.length).toBe(3); expect(searches[2].name).toBe('Search 2'); + expect(service.preferencesApi.updatePreference).toHaveBeenCalledWith('-me-', 'saved-searches', jasmine.any(String)); done(); - } + }); }); - - service.saveSearch(newSearch).subscribe(); }); it('should edit a search', (done) => { @@ -190,8 +182,7 @@ describe('SavedSearchesService', () => { */ function prepareDefaultMock(): void { spyOn(authService, 'getUsername').and.callFake(() => testUserName); - const nodeId = 'saved-searches-node-id'; - spyOn(localStorage, 'getItem').and.callFake(() => nodeId); - service.innit(); + spyOn(localStorage, 'getItem').and.callFake(() => 'true'); + service.init(); } }); 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 d1f3b88b8e..3b538a6d9a 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,7 +15,7 @@ * limitations under the License. */ -import { NodesApi, NodeEntry } from '@alfresco/js-api'; +import { NodesApi, NodeEntry, PreferencesApi } 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'; @@ -27,21 +27,25 @@ import { AuthenticationService } from '@alfresco/adf-core'; providedIn: 'root' }) export class SavedSearchesService { + private savedSearchFileNodeId: string; private _nodesApi: NodesApi; + private _preferencesApi: PreferencesApi; + get nodesApi(): NodesApi { this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance()); return this._nodesApi; } - readonly savedSearches$ = new ReplaySubject(1); + get preferencesApi(): PreferencesApi { + this._preferencesApi = this._preferencesApi ?? new PreferencesApi(this.apiService.getInstance()); + return this._preferencesApi; + } - private savedSearchFileNodeId: string; - private currentUserLocalStorageKey: string; - private createFileAttempt = false; + readonly savedSearches$ = new ReplaySubject(1); constructor(private readonly apiService: AlfrescoApiService, private readonly authService: AuthenticationService) {} - innit(): void { + init(): void { this.fetchSavedSearches(); } @@ -51,20 +55,27 @@ export class SavedSearchesService { * @returns SavedSearch list containing user saved searches */ 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); - }) - ) - ) - ); + const savedSearchesMigrated = localStorage.getItem(this.getLocalStorageKey()) ?? ''; + if (savedSearchesMigrated === 'true') { + return from(this.preferencesApi.getPreference('-me-', 'saved-searches')).pipe( + map((preference) => JSON.parse(preference.entry.value)), + catchError(() => of([])) + ); + } else { + return this.getSavedSearchesNodeId().pipe( + take(1), + concatMap(() => { + if (this.savedSearchFileNodeId !== '') { + return this.migrateSavedSearches(); + } else { + return from(this.preferencesApi.getPreference('-me-', 'saved-searches')).pipe( + map((preference) => JSON.parse(preference.entry.value)), + catchError(() => of([])) + ); + } + }) + ); + } } /** @@ -94,7 +105,8 @@ export class SavedSearchesService { order: index })); - return from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSavedSearches))).pipe( + return from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSavedSearches))).pipe( + map((preference) => JSON.parse(preference.entry.value)), tap(() => this.savedSearches$.next(updatedSavedSearches)) ); }), @@ -123,7 +135,9 @@ export class SavedSearchesService { this.savedSearches$.next(updatedSearches); }), switchMap((updatedSearches: SavedSearch[]) => - from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) + from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( + map((preference) => JSON.parse(preference.entry.value)) + ) ), catchError((error) => { this.savedSearches$.next(previousSavedSearches); @@ -154,7 +168,9 @@ export class SavedSearchesService { this.savedSearches$.next(updatedSearches); }), switchMap((updatedSearches: SavedSearch[]) => - from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) + from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( + map((preference) => JSON.parse(preference.entry.value)) + ) ), catchError((error) => { this.savedSearches$.next(previousSavedSearchesOrder); @@ -185,7 +201,9 @@ export class SavedSearchesService { }), tap((savedSearches: SavedSearch[]) => this.savedSearches$.next(savedSearches)), switchMap((updatedSearches: SavedSearch[]) => - from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches))) + from(this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(updatedSearches))).pipe( + map((preference) => JSON.parse(preference.entry.value)) + ) ), catchError((error) => { this.savedSearches$.next(previousSavedSearchesOrder); @@ -196,52 +214,33 @@ export class SavedSearchesService { } 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' })); + return from(this.nodesApi.getNode('-my-', { relativePath: 'config.json' })).pipe( + first(), + concatMap((configNode) => { + this.savedSearchFileNodeId = configNode.entry.id; + return configNode.entry.id; + }), + catchError((error) => { + const errorStatusCode = JSON.parse(error.message).error.statusCode; + if (errorStatusCode === 404) { + localStorage.setItem(this.getLocalStorageKey(), 'true'); + return ''; + } else { + return throwError(() => error); + } + }) + ); } private async mapFileContentToSavedSearches(blob: Blob): Promise> { - return blob.text().then((content) => (content ? JSON.parse(content) : [])); + return blob + .text() + .then((content) => (content ? JSON.parse(content) : [])) + .catch(() => []); } private getLocalStorageKey(): string { - return `saved-searches-node-id__${this.authService.getUsername()}`; + return `saved-searches-${this.authService.getUsername()}-migrated`; } private fetchSavedSearches(): void { @@ -249,4 +248,14 @@ export class SavedSearchesService { .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) => { + this.preferencesApi.updatePreference('-me-', 'saved-searches', JSON.stringify(savedSearches)); + localStorage.setItem(this.getLocalStorageKey(), 'true'); + this.nodesApi.deleteNode(this.savedSearchFileNodeId, { permanent: true }); + }) + ); + } } diff --git a/lib/js-api/src/api/content-rest-api/api/preferences.api.ts b/lib/js-api/src/api/content-rest-api/api/preferences.api.ts index 7f9f0be5bb..1d22e7e943 100644 --- a/lib/js-api/src/api/content-rest-api/api/preferences.api.ts +++ b/lib/js-api/src/api/content-rest-api/api/preferences.api.ts @@ -84,4 +84,25 @@ export class PreferencesApi extends BaseApi { queryParams }); } + + updatePreference(personId: string, preferenceName: string, preferenceValue: string): Promise { + throwIfNotDefined(personId, 'personId'); + throwIfNotDefined(preferenceName, 'preferenceName'); + throwIfNotDefined(preferenceValue, 'preferenceValue'); + + const pathParams = { + personId, + preferenceName + }; + + const bodyParam = { + value: preferenceValue + }; + + return this.put({ + path: '/people/{personId}/preferences/{preferenceName}', + pathParams, + bodyParam: bodyParam + }); + } } diff --git a/lib/js-api/src/api/content-rest-api/docs/PreferencesApi.md b/lib/js-api/src/api/content-rest-api/docs/PreferencesApi.md index aca7ff654d..27a1a7a637 100644 --- a/lib/js-api/src/api/content-rest-api/docs/PreferencesApi.md +++ b/lib/js-api/src/api/content-rest-api/docs/PreferencesApi.md @@ -2,10 +2,11 @@ All URIs are relative to *https://localhost/alfresco/api/-default-/public/alfresco/versions/1* -| Method | HTTP request | Description | -|-------------------------------------|---------------------------------------------------------|------------------| -| [getPreference](#getPreference) | **GET** /people/{personId}/preferences/{preferenceName} | Get a preference | -| [listPreferences](#listPreferences) | **GET** /people/{personId}/preferences | List preferences | +| Method | HTTP request | Description | +|---------------------------------------|----------------------------------------------------------|-------------------| +| [getPreference](#getPreference) | **GET** /people/{personId}/preferences/{preferenceName} | Get a preference | +| [listPreferences](#listPreferences) | **GET** /people/{personId}/preferences | List preferences | +| [updatePreference](#updatePreference) | **POST** /people/{personId}/preferences/{preferenceName} | Update preference | ## getPreference @@ -71,6 +72,36 @@ preferencesApi.listPreferences(``, opts).then((data) => { }); ``` +## updatePreference + +Update preference + +You can use the `-me-` string in place of `` to specify the currently authenticated user. + +### Parameters + +| Name | Type | Description | +|---------------------|--------|-----------------------------| +| **personId** | string | The identifier of a person. | +| **preferenceName** | string | The name of the preference. | +| **preferenceValue** | string | New preference value. | + +**Return type**: [PreferenceEntry](#PreferenceEntry) + +**Example** + +```javascript +import { AlfrescoApi, PreferencesApi } from '@alfresco/js-api'; + +const alfrescoApi = new AlfrescoApi(/*..*/); +const preferencesApi = new PreferencesApi(alfrescoApi); +const newPreferenceValue = 'test'; + +preferencesApi.updatePreference(``, ``, newPreferenceValue).then((data) => { + console.log('API called successfully. Returned data: ' + data); +}); +``` + # Models ## PreferencePaging @@ -105,4 +136,4 @@ preferencesApi.listPreferences(``, opts).then((data) => { | Name | Type | Description | |--------|--------|----------------------------------------------------------------------| | **id** | string | The unique id of the preference | -| value | string | The value of the preference. Note that this can be of any JSON type. | \ No newline at end of file +| value | string | The value of the preference. Note that this can be of any JSON type. |