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
@ -420,7 +420,7 @@ Learn more about styling your datatable: [Customizing the component's styles](#c
|
||||
### Properties
|
||||
|
||||
| Name | Type | Default value | Description |
|
||||
|--------------------------|-------------------------------------------------------------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|----------------------------|-------------------------------------------------------------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| actions | `boolean` | false | Toggles the data actions column. |
|
||||
| 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. |
|
||||
@ -446,11 +446,12 @@ Learn more about styling your datatable: [Customizing the component's styles](#c
|
||||
| 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`] |
|
||||
| stickyHeader | `boolean` | false | Toggles the sticky header mode. |
|
||||
| enableDragRows | `boolean` | false | Flag that enables dragging rows. |
|
||||
|
||||
### Events
|
||||
|
||||
| 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. |
|
||||
| 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. |
|
||||
@ -459,6 +460,7 @@ Learn more about styling your datatable: [Customizing the component's styles](#c
|
||||
| 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. |
|
||||
| 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
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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], {
|
||||
|
@ -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>
|
||||
@ -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)"
|
||||
@ -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"
|
||||
|
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
@ -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);
|
||||
|
@ -2126,4 +2126,24 @@ describe('Column Resizing', () => {
|
||||
expect(dataTable.isResizing).toBeFalse();
|
||||
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()
|
||||
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>;
|
||||
noContentTemplate: TemplateRef<any>;
|
||||
noPermissionTemplate: TemplateRef<any>;
|
||||
@ -306,6 +316,7 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
||||
isDraggingHeaderColumn = false;
|
||||
hoveredHeaderColumnIndex = -1;
|
||||
resizingColumnIndex = -1;
|
||||
isDraggingRow = false;
|
||||
|
||||
private keyManager: FocusKeyManager<DataTableRowComponent>;
|
||||
private clickObserver: Observer<DataRowEvent>;
|
||||
@ -844,7 +855,8 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
||||
row.cssClass = row.cssClass ? row.cssClass : '';
|
||||
this.rowStyleClass = this.rowStyleClass ? this.rowStyleClass : '';
|
||||
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 {
|
||||
@ -1010,6 +1022,20 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
||||
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 {
|
||||
const allColumns = this.data.getColumns();
|
||||
|
||||
|
@ -381,6 +381,7 @@
|
||||
"ICON_TEXT": "Item type {{ type }}",
|
||||
"ICON_DISABLED": "Disabled",
|
||||
"ROW_OPTION_BUTTON": "Actions",
|
||||
"DRAG": "Drag button",
|
||||
"EMPTY_HEADER": "Empty header"
|
||||
},
|
||||
"FILE_TYPE": {
|
||||
|
@ -79,6 +79,8 @@ describe('SidenavLayoutComponent', () => {
|
||||
|
||||
fixture = TestBed.createComponent(SidenavLayoutComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.sidenavMin = 70;
|
||||
component.sidenavMax = 320;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -26,7 +26,8 @@ import {
|
||||
OnDestroy,
|
||||
TemplateRef,
|
||||
EventEmitter,
|
||||
ViewEncapsulation
|
||||
ViewEncapsulation,
|
||||
ChangeDetectorRef
|
||||
} from '@angular/core';
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { UserPreferencesService } from '../../../common/services/user-preferences.service';
|
||||
@ -100,7 +101,11 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -137,6 +142,7 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
} else {
|
||||
this.isMenuMinimized = false;
|
||||
}
|
||||
this.changeDetectorRef.detectChanges();
|
||||
|
||||
this.container.toggleMenu();
|
||||
this.expanded.emit(!this.isMenuMinimized);
|
||||
|
Loading…
x
Reference in New Issue
Block a user