mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-05-19 17:14:57 +00:00
[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:
parent
ba52074bb5
commit
a7911338c3
@ -419,46 +419,48 @@ Learn more about styling your datatable: [Customizing the component's styles](#c
|
|||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
| Name | Type | Default value | Description |
|
| Name | Type | Default value | Description |
|
||||||
|--------------------------|-------------------------------------------------------------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------|-------------------------------------------------------------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| actions | `boolean` | false | Toggles the data actions column. |
|
| actions | `boolean` | false | Toggles the data actions column. |
|
||||||
| actionsPosition | `string` | "right" | Position of the actions dropdown menu. Can be "left" or "right". |
|
| actionsPosition | `string` | "right" | Position of the actions dropdown menu. Can be "left" or "right". |
|
||||||
| actionsVisibleOnHover | `boolean` | false | Toggles whether the actions dropdown should only be visible if the row is hovered over or the dropdown menu is open. |
|
| actionsVisibleOnHover | `boolean` | false | Toggles whether the actions dropdown should only be visible if the row is hovered over or the dropdown menu is open. |
|
||||||
| allowFiltering | `boolean` | false | Flag that indicate if the datatable allow the use [facet widget](../../../lib/content-services/src/lib/search/models/facet-widget.interface.ts) search for filtering. |
|
| allowFiltering | `boolean` | false | Flag that indicate if the datatable allow the use [facet widget](../../../lib/content-services/src/lib/search/models/facet-widget.interface.ts) search for filtering. |
|
||||||
| blurOnResize | `boolean` | true | Toggles blur when columns of the datatable are being resized. |
|
| blurOnResize | `boolean` | true | Toggles blur when columns of the datatable are being resized. |
|
||||||
| columns | `any[]` | \[] | The columns that the datatable will show. |
|
| columns | `any[]` | \[] | The columns that the datatable will show. |
|
||||||
| contextMenu | `boolean` | false | Toggles custom context menu for the component. |
|
| contextMenu | `boolean` | false | Toggles custom context menu for the component. |
|
||||||
| data | [`DataTableAdapter`](../../../lib/core/src/lib/datatable/data/datatable-adapter.ts) | | Data source for the table |
|
| data | [`DataTableAdapter`](../../../lib/core/src/lib/datatable/data/datatable-adapter.ts) | | Data source for the table |
|
||||||
| displayCheckboxesOnHover | `boolean` | false | Enables checkboxes in datatable rows being displayed on hover only. |
|
| displayCheckboxesOnHover | `boolean` | false | Enables checkboxes in datatable rows being displayed on hover only. |
|
||||||
| fallbackThumbnail | `string` | | Fallback image for rows where the thumbnail is missing. |
|
| fallbackThumbnail | `string` | | Fallback image for rows where the thumbnail is missing. |
|
||||||
| isResizingEnabled | `boolean` | false | Flag that indicates if the datatable allows column resizing. |
|
| isResizingEnabled | `boolean` | false | Flag that indicates if the datatable allows column resizing. |
|
||||||
| loading | `boolean` | false | Flag that indicates if the datatable is in loading state and needs to show the loading template (see the docs to learn how to configure a loading template). |
|
| loading | `boolean` | false | Flag that indicates if the datatable is in loading state and needs to show the loading template (see the docs to learn how to configure a loading template). |
|
||||||
| mainTableAction | `boolean` | true | Toggles main data table action column. |
|
| mainTableAction | `boolean` | true | Toggles main data table action column. |
|
||||||
| multiselect | `boolean` | false | Toggles multiple row selection, which renders checkboxes at the beginning of each row. |
|
| multiselect | `boolean` | false | Toggles multiple row selection, which renders checkboxes at the beginning of each row. |
|
||||||
| noPermission | `boolean` | false | Flag that indicates if the datatable should show the "no permission" template. |
|
| noPermission | `boolean` | false | Flag that indicates if the datatable should show the "no permission" template. |
|
||||||
| resolverFn | `Function` | null | Custom resolver function which is used to parse dynamic column objects see the docs to learn how to configure a resolverFn. |
|
| resolverFn | `Function` | null | Custom resolver function which is used to parse dynamic column objects see the docs to learn how to configure a resolverFn. |
|
||||||
| rowMenuCacheEnabled | `boolean` | true | Should the items for the row actions menu be cached for reuse after they are loaded the first time? |
|
| rowMenuCacheEnabled | `boolean` | true | Should the items for the row actions menu be cached for reuse after they are loaded the first time? |
|
||||||
| rowStyle | `Function` | | The inline style to apply to every row. See [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) docs for more details and usage examples. |
|
| rowStyle | `Function` | | The inline style to apply to every row. See [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) docs for more details and usage examples. |
|
||||||
| rowStyleClass | `string` | "" | The CSS class to apply to every row. |
|
| rowStyleClass | `string` | "" | The CSS class to apply to every row. |
|
||||||
| rows | `any[]` | \[] | The rows that the datatable will show. |
|
| rows | `any[]` | \[] | The rows that the datatable will show. |
|
||||||
| selectionMode | `string` | "single" | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode, you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. |
|
| selectionMode | `string` | "single" | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode, you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. |
|
||||||
| showHeader | `ShowHeaderMode` | | Toggles the header. |
|
| showHeader | `ShowHeaderMode` | | Toggles the header. |
|
||||||
| showMainDatatableActions | `boolean` | false | Toggles the main datatable action. |
|
| showMainDatatableActions | `boolean` | false | Toggles the main datatable action. |
|
||||||
| sorting | `any[]` | \[] | Define the sort order of the datatable. Possible values are : [`created`, `desc`], [`created`, `asc`], [`due`, `desc`], [`due`, `asc`] |
|
| sorting | `any[]` | \[] | Define the sort order of the datatable. Possible values are : [`created`, `desc`], [`created`, `asc`], [`due`, `desc`], [`due`, `asc`] |
|
||||||
| stickyHeader | `boolean` | false | Toggles the sticky header mode. |
|
| stickyHeader | `boolean` | false | Toggles the sticky header mode. |
|
||||||
|
| enableDragRows | `boolean` | false | Flag that enables dragging rows. |
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------|
|
|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------|
|
||||||
| columnOrderChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataColumn`](../../../lib/core/src/lib/datatable/data/data-column.model.ts)`<>[]>` | Emitted when the column order is changed. |
|
| columnOrderChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataColumn`](../../../lib/core/src/lib/datatable/data/data-column.model.ts)`<>[]>` | Emitted when the column order is changed. |
|
||||||
| columnsWidthChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataColumn`](../../../lib/core/src/lib/datatable/data/data-column.model.ts)`<>[]>` | Emitted when the column width is changed. |
|
| columnsWidthChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataColumn`](../../../lib/core/src/lib/datatable/data/data-column.model.ts)`<>[]>` | Emitted when the column width is changed. |
|
||||||
| selectedItemsCountChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<number>` | Emitted when the item row count is changed. |
|
| selectedItemsCountChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<number>` | Emitted when the item row count is changed. |
|
||||||
| executeRowAction | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataRowActionEvent`](../../../lib/core/src/lib/datatable/components/data-row-action.event.ts)`>` | Emitted when the user executes a row action. |
|
| executeRowAction | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataRowActionEvent`](../../../lib/core/src/lib/datatable/components/data-row-action.event.ts)`>` | Emitted when the user executes a row action. |
|
||||||
| rowClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataRowEvent`](../../../lib/core/src/lib/datatable/data/data-row-event.model.ts)`>` | Emitted when the user clicks a row. |
|
| rowClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataRowEvent`](../../../lib/core/src/lib/datatable/data/data-row-event.model.ts)`>` | Emitted when the user clicks a row. |
|
||||||
| rowDblClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataRowEvent`](../../../lib/core/src/lib/datatable/data/data-row-event.model.ts)`>` | Emitted when the user double-clicks a row. |
|
| rowDblClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataRowEvent`](../../../lib/core/src/lib/datatable/data/data-row-event.model.ts)`>` | Emitted when the user double-clicks a row. |
|
||||||
| showRowActionsMenu | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataCellEvent`](../../../lib/core/src/lib/datatable/components/data-cell.event.ts)`>` | Emitted before the actions menu is displayed for a row. |
|
| showRowActionsMenu | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataCellEvent`](../../../lib/core/src/lib/datatable/components/data-cell.event.ts)`>` | Emitted before the actions menu is displayed for a row. |
|
||||||
| showRowContextMenu | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataCellEvent`](../../../lib/core/src/lib/datatable/components/data-cell.event.ts)`>` | Emitted before the context menu is displayed for a row. |
|
| showRowContextMenu | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataCellEvent`](../../../lib/core/src/lib/datatable/components/data-cell.event.ts)`>` | Emitted before the context menu is displayed for a row. |
|
||||||
|
| dragDropped | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`{ previousIndex: number; currentIndex: number }]`>` | Emitted when dragged row is dropped. |
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ describe('SavedSearchesService', () => {
|
|||||||
let service: SavedSearchesService;
|
let service: SavedSearchesService;
|
||||||
let authService: AuthenticationService;
|
let authService: AuthenticationService;
|
||||||
let testUserName: string;
|
let testUserName: string;
|
||||||
|
let getNodeContentSpy: jasmine.Spy;
|
||||||
|
|
||||||
const testNodeId = 'test-node-id';
|
const testNodeId = 'test-node-id';
|
||||||
const SAVED_SEARCHES_NODE_ID = 'saved-searches-node-id__';
|
const SAVED_SEARCHES_NODE_ID = 'saved-searches-node-id__';
|
||||||
@ -59,6 +60,9 @@ describe('SavedSearchesService', () => {
|
|||||||
authService = TestBed.inject(AuthenticationService);
|
authService = TestBed.inject(AuthenticationService);
|
||||||
spyOn(service.nodesApi, 'getNode').and.callFake(() => Promise.resolve({ entry: { id: testNodeId } } as NodeEntry));
|
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.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(() => {
|
afterEach(() => {
|
||||||
@ -68,12 +72,11 @@ describe('SavedSearchesService', () => {
|
|||||||
it('should retrieve saved searches from the saved-searches.json file', (done) => {
|
it('should retrieve saved searches from the saved-searches.json file', (done) => {
|
||||||
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
|
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
|
||||||
spyOn(localStorage, 'getItem').and.callFake(() => testNodeId);
|
spyOn(localStorage, 'getItem').and.callFake(() => testNodeId);
|
||||||
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
|
|
||||||
service.innit();
|
service.innit();
|
||||||
|
|
||||||
service.getSavedSearches().subscribe((searches) => {
|
service.getSavedSearches().subscribe((searches) => {
|
||||||
expect(localStorage.getItem).toHaveBeenCalledWith(SAVED_SEARCHES_NODE_ID + testUserName);
|
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.length).toBe(2);
|
||||||
expect(searches[0].name).toBe('Search 1');
|
expect(searches[0].name).toBe('Search 1');
|
||||||
expect(searches[1].name).toBe('Search 2');
|
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) => {
|
it('should create saved-searches.json file if it does not exist', (done) => {
|
||||||
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
|
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
|
||||||
spyOn(service.nodesApi, 'createNode').and.callFake(() => Promise.resolve({ entry: { id: 'new-node-id' } }));
|
getNodeContentSpy.and.callFake(() => Promise.resolve(new Blob([''])));
|
||||||
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => Promise.resolve(new Blob([''])));
|
|
||||||
service.innit();
|
service.innit();
|
||||||
|
|
||||||
service.getSavedSearches().subscribe((searches) => {
|
service.getSavedSearches().subscribe((searches) => {
|
||||||
@ -100,9 +102,7 @@ describe('SavedSearchesService', () => {
|
|||||||
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
|
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
|
||||||
const nodeId = 'saved-searches-node-id';
|
const nodeId = 'saved-searches-node-id';
|
||||||
spyOn(localStorage, 'getItem').and.callFake(() => nodeId);
|
spyOn(localStorage, 'getItem').and.callFake(() => nodeId);
|
||||||
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
|
|
||||||
const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' };
|
const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' };
|
||||||
spyOn(service.nodesApi, 'updateNodeContent').and.callFake(() => Promise.resolve({ entry: {} } as NodeEntry));
|
|
||||||
service.innit();
|
service.innit();
|
||||||
|
|
||||||
service.saveSearch(newSearch).subscribe(() => {
|
service.saveSearch(newSearch).subscribe(() => {
|
||||||
@ -110,7 +110,7 @@ describe('SavedSearchesService', () => {
|
|||||||
expect(service.savedSearches$).toBeDefined();
|
expect(service.savedSearches$).toBeDefined();
|
||||||
service.savedSearches$.subscribe((searches) => {
|
service.savedSearches$.subscribe((searches) => {
|
||||||
expect(searches.length).toBe(3);
|
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);
|
expect(searches[2].order).toBe(2);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -120,7 +120,6 @@ describe('SavedSearchesService', () => {
|
|||||||
it('should emit initial saved searches on subscription', (done) => {
|
it('should emit initial saved searches on subscription', (done) => {
|
||||||
const nodeId = 'saved-searches-node-id';
|
const nodeId = 'saved-searches-node-id';
|
||||||
spyOn(localStorage, 'getItem').and.returnValue(nodeId);
|
spyOn(localStorage, 'getItem').and.returnValue(nodeId);
|
||||||
spyOn(service.nodesApi, 'getNodeContent').and.returnValue(createBlob());
|
|
||||||
service.innit();
|
service.innit();
|
||||||
|
|
||||||
service.savedSearches$.pipe().subscribe((searches) => {
|
service.savedSearches$.pipe().subscribe((searches) => {
|
||||||
@ -135,9 +134,7 @@ describe('SavedSearchesService', () => {
|
|||||||
it('should emit updated saved searches after saving a new search', (done) => {
|
it('should emit updated saved searches after saving a new search', (done) => {
|
||||||
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
|
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
|
||||||
spyOn(localStorage, 'getItem').and.callFake(() => testNodeId);
|
spyOn(localStorage, 'getItem').and.callFake(() => testNodeId);
|
||||||
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
|
|
||||||
const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' };
|
const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' };
|
||||||
spyOn(service.nodesApi, 'updateNodeContent').and.callFake(() => Promise.resolve({ entry: {} } as NodeEntry));
|
|
||||||
service.innit();
|
service.innit();
|
||||||
|
|
||||||
let emissionCount = 0;
|
let emissionCount = 0;
|
||||||
@ -149,11 +146,52 @@ describe('SavedSearchesService', () => {
|
|||||||
}
|
}
|
||||||
if (emissionCount === 2) {
|
if (emissionCount === 2) {
|
||||||
expect(searches.length).toBe(3);
|
expect(searches.length).toBe(3);
|
||||||
expect(searches[2].name).toBe('Search 3');
|
expect(searches[2].name).toBe('Search 2');
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
service.saveSearch(newSearch).subscribe();
|
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();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -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 }
|
* @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> {
|
saveSearch(newSaveSearch: Pick<SavedSearch, 'name' | 'description' | 'encodedUrl'>): Observable<NodeEntry> {
|
||||||
return this.getSavedSearches().pipe(
|
return this.getSavedSearches().pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((savedSearches: Array<SavedSearch>) => {
|
switchMap((savedSearches: SavedSearch[]) => {
|
||||||
const updatedSavedSearches = [...savedSearches, { ...newSaveSearch, order: savedSearches.length }];
|
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(
|
return from(this.nodesApi.updateNodeContent(this.savedSearchFileNodeId, JSON.stringify(updatedSavedSearches))).pipe(
|
||||||
tap(() => this.savedSearches$.next(updatedSavedSearches))
|
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> {
|
private getSavedSearchesNodeId(): Observable<string> {
|
||||||
const localStorageKey = this.getLocalStorageKey();
|
const localStorageKey = this.getLocalStorageKey();
|
||||||
if (this.currentUserLocalStorageKey && this.currentUserLocalStorageKey !== localStorageKey) {
|
if (this.currentUserLocalStorageKey && this.currentUserLocalStorageKey !== localStorageKey) {
|
||||||
|
@ -596,6 +596,7 @@ export abstract class BaseQueryBuilderService {
|
|||||||
* @param searchUrl search url to navigate to
|
* @param searchUrl search url to navigate to
|
||||||
*/
|
*/
|
||||||
async navigateToSearch(query: string, searchUrl: string) {
|
async navigateToSearch(query: string, searchUrl: string) {
|
||||||
|
this.update();
|
||||||
this.userQuery = query;
|
this.userQuery = query;
|
||||||
await this.execute();
|
await this.execute();
|
||||||
await this.router.navigate([searchUrl], {
|
await this.router.navigate([searchUrl], {
|
||||||
|
@ -15,6 +15,12 @@
|
|||||||
class="adf-datatable-row"
|
class="adf-datatable-row"
|
||||||
role="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) -->
|
<!-- Actions (left) -->
|
||||||
<div *ngIf="actions && actionsPosition === 'left'" class="adf-actions-column adf-datatable-cell-header">
|
<div *ngIf="actions && actionsPosition === 'left'" class="adf-actions-column adf-datatable-cell-header">
|
||||||
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
|
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
|
||||||
@ -148,10 +154,17 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="adf-datatable-body"
|
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">
|
role="rowgroup">
|
||||||
<ng-container *ngIf="!loading && !noPermission">
|
<ng-container *ngIf="!loading && !noPermission">
|
||||||
<adf-datatable-row *ngFor="let row of data.getRows(); let idx = index"
|
<adf-datatable-row *ngFor="let row of data.getRows(); let idx = index"
|
||||||
|
cdkDrag
|
||||||
|
[cdkDragDisabled]="!enableDragRows"
|
||||||
|
(cdkDragDropped)="onDragDrop($event)"
|
||||||
|
(cdkDragStarted)="onDragStart()"
|
||||||
|
(cdkDragEnded)="onDragEnd()"
|
||||||
[row]="row"
|
[row]="row"
|
||||||
(select)="onEnterKeyPressed(row, $event)"
|
(select)="onEnterKeyPressed(row, $event)"
|
||||||
(keyup)="onRowKeyUp(row, $event)"
|
(keyup)="onRowKeyUp(row, $event)"
|
||||||
@ -160,8 +173,19 @@
|
|||||||
[adf-upload-data]="row"
|
[adf-upload-data]="row"
|
||||||
[ngStyle]="rowStyle"
|
[ngStyle]="rowStyle"
|
||||||
[ngClass]="getRowStyle(row)"
|
[ngClass]="getRowStyle(row)"
|
||||||
|
[class.adf-datatable-row__dragging]="isDraggingRow"
|
||||||
[attr.data-automation-id]="'datatable-row-' + idx"
|
[attr.data-automation-id]="'datatable-row-' + idx"
|
||||||
(contextmenu)="markRowAsContextMenuSource(row)">
|
(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) -->
|
<!-- Actions (left) -->
|
||||||
<div *ngIf="actions && actionsPosition === 'left'" role="gridcell" class="adf-datatable-cell">
|
<div *ngIf="actions && actionsPosition === 'left'" role="gridcell" class="adf-datatable-cell">
|
||||||
<button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger"
|
<button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger"
|
||||||
|
@ -133,6 +133,14 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
||||||
|
&.adf-datatable-body__draggable {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.adf-datatable-body__dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
.adf-datatable-row {
|
.adf-datatable-row {
|
||||||
@include material-animation-default(0.28s);
|
@include material-animation-default(0.28s);
|
||||||
|
|
||||||
@ -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);
|
background-color: var(--adf-theme-background-selected-button-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.adf-drag-row {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: 1px solid var(--adf-theme-foreground-text-color-007);
|
border-bottom: 1px solid var(--adf-theme-foreground-text-color-007);
|
||||||
}
|
}
|
||||||
@ -284,6 +296,10 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
|
|||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.adf-drag-column {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.adf-datatable-cell-container {
|
.adf-datatable-cell-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: inherit;
|
min-height: inherit;
|
||||||
@ -585,6 +601,13 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
|
|||||||
}
|
}
|
||||||
|
|
||||||
#{$cdk-drag-preview} {
|
#{$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 {
|
&.adf-datatable-cell-header {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: var(--theme-background-color);
|
background-color: var(--theme-background-color);
|
||||||
|
@ -2126,4 +2126,24 @@ describe('Column Resizing', () => {
|
|||||||
expect(dataTable.isResizing).toBeFalse();
|
expect(dataTable.isResizing).toBeFalse();
|
||||||
expect(dataTable.columnsWidthChanged.emit).toHaveBeenCalled();
|
expect(dataTable.columnsWidthChanged.emit).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not have drag and drop directive enabled and not emit event when drag rows is disabled', () => {
|
||||||
|
spyOn(dataTable.dragDropped, 'emit');
|
||||||
|
dataTable.enableDragRows = false;
|
||||||
|
dataTable.showHeader = ShowHeaderMode.Never;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const dragAndDrop = fixture.debugElement.query(By.directive(CdkDropList)).injector.get(CdkDropList);
|
||||||
|
dataTable.onDragDrop({} as CdkDragDrop<any>);
|
||||||
|
expect(dataTable.dragDropped.emit).not.toHaveBeenCalled();
|
||||||
|
expect(dragAndDrop.disabled).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit event when drag rows is enabled', () => {
|
||||||
|
spyOn(dataTable.dragDropped, 'emit');
|
||||||
|
dataTable.enableDragRows = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const data = { previousIndex: 1, currentIndex: 0 };
|
||||||
|
dataTable.onDragDrop(data as CdkDragDrop<any>);
|
||||||
|
expect(dataTable.dragDropped.emit).toHaveBeenCalledWith(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -292,6 +292,16 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
|||||||
@Input()
|
@Input()
|
||||||
displayCheckboxesOnHover = false;
|
displayCheckboxesOnHover = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag that enables dragging rows
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
enableDragRows = false;
|
||||||
|
|
||||||
|
/** Emitted when dragged row is dropped. */
|
||||||
|
@Output()
|
||||||
|
dragDropped = new EventEmitter<{ previousIndex: number; currentIndex: number }>();
|
||||||
|
|
||||||
headerFilterTemplate: TemplateRef<any>;
|
headerFilterTemplate: TemplateRef<any>;
|
||||||
noContentTemplate: TemplateRef<any>;
|
noContentTemplate: TemplateRef<any>;
|
||||||
noPermissionTemplate: TemplateRef<any>;
|
noPermissionTemplate: TemplateRef<any>;
|
||||||
@ -306,6 +316,7 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
|||||||
isDraggingHeaderColumn = false;
|
isDraggingHeaderColumn = false;
|
||||||
hoveredHeaderColumnIndex = -1;
|
hoveredHeaderColumnIndex = -1;
|
||||||
resizingColumnIndex = -1;
|
resizingColumnIndex = -1;
|
||||||
|
isDraggingRow = false;
|
||||||
|
|
||||||
private keyManager: FocusKeyManager<DataTableRowComponent>;
|
private keyManager: FocusKeyManager<DataTableRowComponent>;
|
||||||
private clickObserver: Observer<DataRowEvent>;
|
private clickObserver: Observer<DataRowEvent>;
|
||||||
@ -844,7 +855,8 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
|||||||
row.cssClass = row.cssClass ? row.cssClass : '';
|
row.cssClass = row.cssClass ? row.cssClass : '';
|
||||||
this.rowStyleClass = this.rowStyleClass ? this.rowStyleClass : '';
|
this.rowStyleClass = this.rowStyleClass ? this.rowStyleClass : '';
|
||||||
const contextMenuSourceClass = row.isContextMenuSource ? 'adf-context-menu-source' : '';
|
const contextMenuSourceClass = row.isContextMenuSource ? 'adf-context-menu-source' : '';
|
||||||
return `${row.cssClass} ${this.rowStyleClass} ${contextMenuSourceClass}`;
|
const isDragEnabled = this.enableDragRows ? 'adf-drag-row' : '';
|
||||||
|
return `${row.cssClass} ${this.rowStyleClass} ${contextMenuSourceClass} ${isDragEnabled}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
markRowAsContextMenuSource(selectedRow: DataRow): void {
|
markRowAsContextMenuSource(selectedRow: DataRow): void {
|
||||||
@ -1010,6 +1022,20 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
|||||||
return !drop.getSortedItems()[index].disabled;
|
return !drop.getSortedItems()[index].disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDragDrop(droppedEvent: CdkDragDrop<any>): void {
|
||||||
|
if (this.enableDragRows) {
|
||||||
|
this.dragDropped.emit({ previousIndex: droppedEvent.previousIndex, currentIndex: droppedEvent.currentIndex });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragStart(): void {
|
||||||
|
this.isDraggingRow = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragEnd(): void {
|
||||||
|
this.isDraggingRow = false;
|
||||||
|
}
|
||||||
|
|
||||||
private updateColumnsWidths(): void {
|
private updateColumnsWidths(): void {
|
||||||
const allColumns = this.data.getColumns();
|
const allColumns = this.data.getColumns();
|
||||||
|
|
||||||
|
@ -381,6 +381,7 @@
|
|||||||
"ICON_TEXT": "Item type {{ type }}",
|
"ICON_TEXT": "Item type {{ type }}",
|
||||||
"ICON_DISABLED": "Disabled",
|
"ICON_DISABLED": "Disabled",
|
||||||
"ROW_OPTION_BUTTON": "Actions",
|
"ROW_OPTION_BUTTON": "Actions",
|
||||||
|
"DRAG": "Drag button",
|
||||||
"EMPTY_HEADER": "Empty header"
|
"EMPTY_HEADER": "Empty header"
|
||||||
},
|
},
|
||||||
"FILE_TYPE": {
|
"FILE_TYPE": {
|
||||||
|
@ -79,6 +79,8 @@ describe('SidenavLayoutComponent', () => {
|
|||||||
|
|
||||||
fixture = TestBed.createComponent(SidenavLayoutComponent);
|
fixture = TestBed.createComponent(SidenavLayoutComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
component.sidenavMin = 70;
|
||||||
|
component.sidenavMax = 320;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -26,7 +26,8 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
TemplateRef,
|
TemplateRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
ViewEncapsulation
|
ViewEncapsulation,
|
||||||
|
ChangeDetectorRef
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MediaMatcher } from '@angular/cdk/layout';
|
import { MediaMatcher } from '@angular/cdk/layout';
|
||||||
import { UserPreferencesService } from '../../../common/services/user-preferences.service';
|
import { UserPreferencesService } from '../../../common/services/user-preferences.service';
|
||||||
@ -100,7 +101,11 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
|
|
||||||
private onDestroy$ = new Subject<boolean>();
|
private onDestroy$ = new Subject<boolean>();
|
||||||
|
|
||||||
constructor(private mediaMatcher: MediaMatcher, private userPreferencesService: UserPreferencesService) {
|
constructor(
|
||||||
|
private readonly mediaMatcher: MediaMatcher,
|
||||||
|
private readonly userPreferencesService: UserPreferencesService,
|
||||||
|
private readonly changeDetectorRef: ChangeDetectorRef
|
||||||
|
) {
|
||||||
this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
|
this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +142,7 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
} else {
|
} else {
|
||||||
this.isMenuMinimized = false;
|
this.isMenuMinimized = false;
|
||||||
}
|
}
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
this.container.toggleMenu();
|
this.container.toggleMenu();
|
||||||
this.expanded.emit(!this.isMenuMinimized);
|
this.expanded.emit(!this.isMenuMinimized);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user