[ACS-8634] "Manage Searches" - a full page list of saved searches (#10307)

* [ACS-8634] "Manage Searches" - a full page list of saved searches

* Changes after CR

* [ACS-8634] Removed extra selector, drag&drop on hover

* [ACS-8634] Fix discovered bugs

* [ACS-8634] Unit test fixes

* [ACS-8634] Cleanup failing test case

* [ACS-8634] Unit test fixes

* [ACS-8634] Remove unused import

* [ACS-8634] Final cleanup

* [ACS-8634] Remove unused imports

---------

Co-authored-by: MichalKinas <michal.kinas@hyland.com>
This commit is contained in:
dominikiwanekhyland
2024-10-25 16:14:11 +02:00
committed by GitHub
parent ba52074bb5
commit a7911338c3
11 changed files with 308 additions and 55 deletions

View File

@@ -28,6 +28,7 @@ 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__';
@@ -59,6 +60,9 @@ describe('SavedSearchesService', () => {
authService = TestBed.inject(AuthenticationService);
spyOn(service.nodesApi, 'getNode').and.callFake(() => Promise.resolve({ entry: { id: testNodeId } } as NodeEntry));
spyOn(service.searchApi, 'search').and.callFake(() => Promise.resolve({ list: { entries: [] } }));
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(() => {
@@ -68,12 +72,11 @@ describe('SavedSearchesService', () => {
it('should retrieve saved searches from the saved-searches.json file', (done) => {
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
spyOn(localStorage, 'getItem').and.callFake(() => testNodeId);
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
service.innit();
service.getSavedSearches().subscribe((searches) => {
expect(localStorage.getItem).toHaveBeenCalledWith(SAVED_SEARCHES_NODE_ID + testUserName);
expect(service.nodesApi.getNodeContent).toHaveBeenCalledWith(testNodeId);
expect(getNodeContentSpy).toHaveBeenCalledWith(testNodeId);
expect(searches.length).toBe(2);
expect(searches[0].name).toBe('Search 1');
expect(searches[1].name).toBe('Search 2');
@@ -83,8 +86,7 @@ describe('SavedSearchesService', () => {
it('should create saved-searches.json file if it does not exist', (done) => {
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
spyOn(service.nodesApi, 'createNode').and.callFake(() => Promise.resolve({ entry: { id: 'new-node-id' } }));
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => Promise.resolve(new Blob([''])));
getNodeContentSpy.and.callFake(() => Promise.resolve(new Blob([''])));
service.innit();
service.getSavedSearches().subscribe((searches) => {
@@ -100,9 +102,7 @@ describe('SavedSearchesService', () => {
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
const nodeId = 'saved-searches-node-id';
spyOn(localStorage, 'getItem').and.callFake(() => nodeId);
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' };
spyOn(service.nodesApi, 'updateNodeContent').and.callFake(() => Promise.resolve({ entry: {} } as NodeEntry));
service.innit();
service.saveSearch(newSearch).subscribe(() => {
@@ -110,7 +110,7 @@ describe('SavedSearchesService', () => {
expect(service.savedSearches$).toBeDefined();
service.savedSearches$.subscribe((searches) => {
expect(searches.length).toBe(3);
expect(searches[2].name).toBe('Search 3');
expect(searches[2].name).toBe('Search 2');
expect(searches[2].order).toBe(2);
done();
});
@@ -120,7 +120,6 @@ describe('SavedSearchesService', () => {
it('should emit initial saved searches on subscription', (done) => {
const nodeId = 'saved-searches-node-id';
spyOn(localStorage, 'getItem').and.returnValue(nodeId);
spyOn(service.nodesApi, 'getNodeContent').and.returnValue(createBlob());
service.innit();
service.savedSearches$.pipe().subscribe((searches) => {
@@ -135,9 +134,7 @@ 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(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' };
spyOn(service.nodesApi, 'updateNodeContent').and.callFake(() => Promise.resolve({ entry: {} } as NodeEntry));
service.innit();
let emissionCount = 0;
@@ -149,11 +146,52 @@ describe('SavedSearchesService', () => {
}
if (emissionCount === 2) {
expect(searches.length).toBe(3);
expect(searches[2].name).toBe('Search 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.innit();
}
});

View File

@@ -76,23 +76,133 @@ export class SavedSearchesService {
}
/**
* Gets a list of saved searches by user.
* 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 Adds and saves search also updating current saved search state
* @returns NodeEntry
*/
saveSearch(newSaveSearch: Pick<SavedSearch, 'name' | 'description' | 'encodedUrl'>): Observable<NodeEntry> {
return this.getSavedSearches().pipe(
take(1),
switchMap((savedSearches: Array<SavedSearch>) => {
const updatedSavedSearches = [...savedSearches, { ...newSaveSearch, order: savedSearches.length }];
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);
})
);
}
/**
* Replace Save Search with new one and also updates the state.
*
* @param updatedSavedSearch - updated Save Search
* @returns NodeEntry
*/
editSavedSearch(updatedSavedSearch: SavedSearch): Observable<NodeEntry> {
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[]) => {
return from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches)));
}),
catchError((error) => {
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<NodeEntry> {
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[]) => {
return from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches)));
}),
catchError((error) => {
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$
.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[]) => {
return from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSearches)));
}),
catchError((error) => {
this.savedSearches$.next(previousSavedSearchesOrder);
return throwError(() => error);
})
)
.subscribe();
}
private getSavedSearchesNodeId(): Observable<string> {
const localStorageKey = this.getLocalStorageKey();
if (this.currentUserLocalStorageKey && this.currentUserLocalStorageKey !== localStorageKey) {

View File

@@ -596,6 +596,7 @@ export abstract class BaseQueryBuilderService {
* @param searchUrl search url to navigate to
*/
async navigateToSearch(query: string, searchUrl: string) {
this.update();
this.userQuery = query;
await this.execute();
await this.router.navigate([searchUrl], {