Revert "Revert "[ACS-9166] Migrate Saved Searches to preferences API from con…" (#10594)

This reverts commit fcd6e25dc65b83929c1dbed4121c929eef666910.
This commit is contained in:
MichalKinas 2025-01-28 08:41:23 +01:00 committed by GitHub
parent 78e527f7df
commit 35aec24c0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 130 deletions

View File

@ -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)`<SavedSearch[]>`
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)`<SavedSearch[]>` - 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);
});
```

View File

@ -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();
});
});
});
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.saveSearch(newSearch).subscribe(() => {
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');
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();
}
});

View File

@ -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<SavedSearch[]>(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<SavedSearch[]>(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<SavedSearch[]> {
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<string> {
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;
this.savedSearchFileNodeId = configNode.entry.id;
return configNode.entry.id;
}),
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;
})
);
localStorage.setItem(this.getLocalStorageKey(), 'true');
return '';
} else {
return throwError(() => error);
}
})
);
} else {
this.savedSearchFileNodeId = savedSearchesNodeId;
return of(savedSearchesNodeId);
}
}
private createSavedSearchesNode(parentNodeId: string): Observable<NodeEntry> {
return from(this.nodesApi.createNode(parentNodeId, { name: 'config.json', nodeType: 'cm:content' }));
}
private async mapFileContentToSavedSearches(blob: Blob): Promise<Array<SavedSearch>> {
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<SavedSearch[]> {
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 });
})
);
}
}

View File

@ -84,4 +84,25 @@ export class PreferencesApi extends BaseApi {
queryParams
});
}
updatePreference(personId: string, preferenceName: string, preferenceValue: string): Promise<PreferenceEntry> {
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
});
}
}

View File

@ -3,9 +3,10 @@
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 |
| [updatePreference](#updatePreference) | **POST** /people/{personId}/preferences/{preferenceName} | Update preference |
## getPreference
@ -71,6 +72,36 @@ preferencesApi.listPreferences(`<personId>`, opts).then((data) => {
});
```
## updatePreference
Update preference
You can use the `-me-` string in place of `<personId>` 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(`<personId>`, `<preferenceName>`, newPreferenceValue).then((data) => {
console.log('API called successfully. Returned data: ' + data);
});
```
# Models
## PreferencePaging