Skip to content

Commit

Permalink
[ACS-8634] "Manage Searches" - a full page list of saved searches (#1…
Browse files Browse the repository at this point in the history
…0307)

* [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 <[email protected]>
  • Loading branch information
dominikiwanekhyland and MichalKinas authored Oct 25, 2024
1 parent ba52074 commit a791133
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 55 deletions.
74 changes: 38 additions & 36 deletions docs/core/components/datatable.component.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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__';
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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');
Expand All @@ -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) => {
Expand All @@ -100,17 +102,15 @@ 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(() => {
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 3');
expect(searches[2].name).toBe('Search 2');
expect(searches[2].order).toBe(2);
done();
});
Expand All @@ -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) => {
Expand All @@ -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;
Expand All @@ -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();
}
});
118 changes: 114 additions & 4 deletions lib/content-services/src/lib/common/services/saved-searches.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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], {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
class="adf-datatable-row"
role="row">


<!-- Drag -->
<div *ngIf="enableDragRows" class="adf-datatable-cell-header adf-drag-column">
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.DRAG' | translate }}</span>
</div>

<!-- Actions (left) -->
<div *ngIf="actions && actionsPosition === 'left'" class="adf-actions-column adf-datatable-cell-header">
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
Expand Down Expand Up @@ -148,10 +154,17 @@

<div
class="adf-datatable-body"
[ngClass]="{ 'adf-blur-datatable-body': blurOnResize && (isDraggingHeaderColumn || isResizing) }"
[ngClass]="{ 'adf-blur-datatable-body': blurOnResize && (isDraggingHeaderColumn || isResizing), 'adf-datatable-body__draggable': enableDragRows && !isDraggingRow, 'adf-datatable-body__dragging': isDraggingRow }"
cdkDropList
[cdkDropListDisabled]="!enableDragRows"
role="rowgroup">
<ng-container *ngIf="!loading && !noPermission">
<adf-datatable-row *ngFor="let row of data.getRows(); let idx = index"
cdkDrag
[cdkDragDisabled]="!enableDragRows"
(cdkDragDropped)="onDragDrop($event)"
(cdkDragStarted)="onDragStart()"
(cdkDragEnded)="onDragEnd()"
[row]="row"
(select)="onEnterKeyPressed(row, $event)"
(keyup)="onRowKeyUp(row, $event)"
Expand All @@ -160,8 +173,19 @@
[adf-upload-data]="row"
[ngStyle]="rowStyle"
[ngClass]="getRowStyle(row)"
[class.adf-datatable-row__dragging]="isDraggingRow"
[attr.data-automation-id]="'datatable-row-' + idx"
(contextmenu)="markRowAsContextMenuSource(row)">
<!-- Drag button -->
<div *ngIf="enableDragRows"
role="gridcell"
class="adf-datatable-cell adf-datatable__actions-cell adf-datatable-hover-only">
<button mat-icon-button
[attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.DRAG' | translate">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>

<!-- Actions (left) -->
<div *ngIf="actions && actionsPosition === 'left'" role="gridcell" class="adf-datatable-cell">
<button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
width: 100%;
min-width: 100%;

&.adf-datatable-body__draggable {
cursor: grab;
}

&.adf-datatable-body__dragging {
cursor: grabbing;
}

.adf-datatable-row {
@include material-animation-default(0.28s);

Expand All @@ -148,6 +156,10 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
background-color: var(--adf-theme-background-selected-button-color);
}

&.adf-drag-row {
cursor: grab;
}

&:last-child {
border-bottom: 1px solid var(--adf-theme-foreground-text-color-007);
}
Expand Down Expand Up @@ -284,6 +296,10 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
box-sizing: content-box;
}

&.adf-drag-column {
flex: 0;
}

.adf-datatable-cell-container {
overflow: hidden;
min-height: inherit;
Expand Down Expand Up @@ -585,6 +601,13 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
}

#{$cdk-drag-preview} {
min-height: $data-table-row-height;
display: flex;
align-items: center;
background-color: var(--theme-background-color);
border-top: 2px solid var(--theme-selected-background-color);
opacity: 1;

&.adf-datatable-cell-header {
border-radius: 6px;
background-color: var(--theme-background-color);
Expand Down
Loading

0 comments on commit a791133

Please sign in to comment.