[AAE-7819] Change column order - enable drag and drop for datatable - [1/3] (#7567)

* [AAE-7819] Enable drag and drop for datatable [1/3]

* [AAE-7819] Change column order - load and save columns order preferences for PROCESSES - [2/3] (#7568)

* [AAE-7819] Load and save column order preferences for processes

* [AAE-7819] Load and save column order preferences for tasks [3/3] (#7569)

* fix css

* fix icon module import

* Fix unit tests

* Fix test

* Fix e2e

* Fix C279927
This commit is contained in:
Bartosz Sekuła 2022-04-11 08:01:02 +02:00 committed by GitHub
parent e89cea79be
commit 48c3fac018
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 614 additions and 136 deletions

View File

@ -227,13 +227,13 @@ export class DataTableComponent {
], ],
[ [
{ type: 'image', key: 'icon', title: '', srTitle: 'Thumbnail' }, { type: 'image', key: 'icon', title: '', srTitle: 'Thumbnail' },
{ type: 'text', key: 'id', title: 'Id', sortable: true , cssClass: '' }, { type: 'text', key: 'id', title: 'Id', sortable: true , cssClass: '', draggable: true},
{ type: 'date', key: 'createdOn', title: 'Created On', sortable: true, cssClass: 'adf-ellipsis-cell adf-expand-cell-2' }, { type: 'date', key: 'createdOn', title: 'Created On', sortable: true, cssClass: 'adf-ellipsis-cell adf-expand-cell-2', draggable: true },
{ type: 'text', key: 'name', title: 'Name', cssClass: 'adf-ellipsis-cell', sortable: true }, { type: 'text', key: 'name', title: 'Name', cssClass: 'adf-ellipsis-cell', sortable: true, draggable: true },
{ type: 'text', key: 'createdBy.name', title: 'Created By', sortable: true, cssClass: ''}, { type: 'text', key: 'createdBy.name', title: 'Created By', sortable: true, cssClass: '', draggable: true},
{ type: 'json', key: 'json', title: 'Json', cssClass: 'adf-expand-cell-2'}, { type: 'json', key: 'json', title: 'Json', cssClass: 'adf-expand-cell-2', draggable: true},
{ type: 'text', key: 'users', title: 'Users', cssClass: 'adf-expand-cell-2'}, { type: 'text', key: 'users', title: 'Users', cssClass: 'adf-expand-cell-2', draggable: true},
{ type: 'json', key: 'status', title: 'Status', cssClass: 'adf-expand-cell-2'} { type: 'json', key: 'status', title: 'Status', cssClass: 'adf-expand-cell-2', draggable: true}
] ]
); );

View File

@ -43,6 +43,7 @@ Defines column properties for DataTable, Tasklist, Document List and other compo
| Name | Type | Default value | Description | | Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- | | ---- | ---- | ------------- | ----------- |
| id | `string` | '' | Column identifier. |
| copyContent | `boolean` | | Enables/disables a [Clipboard directive](../../core/directives/clipboard.directive.md) to allow copying of cell contents. | | copyContent | `boolean` | | Enables/disables a [Clipboard directive](../../core/directives/clipboard.directive.md) to allow copying of cell contents. |
| cssClass | `string` | | Additional CSS class to be applied to column (header and cells). | | cssClass | `string` | | Additional CSS class to be applied to column (header and cells). |
| editable | `boolean` | false | Toggles the editing support of the column data. | | editable | `boolean` | false | Toggles the editing support of the column data. |
@ -51,6 +52,7 @@ Defines column properties for DataTable, Tasklist, Document List and other compo
| formatTooltip | `Function` | | Custom tooltip formatter function. | | formatTooltip | `Function` | | Custom tooltip formatter function. |
| key | `string` | | Data source key. Can be either a column/property key like `title` or a property path like `createdBy.name`. | | key | `string` | | Data source key. Can be either a column/property key like `title` or a property path like `createdBy.name`. |
| sortable | `boolean` | true | Toggles ability to sort by this column, for example by clicking the column header. | | sortable | `boolean` | true | Toggles ability to sort by this column, for example by clicking the column header. |
| draggable | `boolean` | false | Toggles drag and drop for header column. |
| sortingKey | `string` | | When using server side sorting the column used by the api call where the sorting will be performed | | sortingKey | `string` | | When using server side sorting the column used by the api call where the sorting will be performed |
| srTitle | `string` | | Title to be used for screen readers. | | srTitle | `string` | | Title to be used for screen readers. |
| title | `string` | "" | Display title of the column, typically used for column headers. You can use the i18n resource key to get it translated automatically. | | title | `string` | "" | Display title of the column, typically used for column headers. You can use the i18n resource key to get it translated automatically. |

View File

@ -63,10 +63,10 @@ export class ContentServicesPage {
errorSnackBar = $('simple-snack-bar[class*="mat-simple-snackbar"]'); errorSnackBar = $('simple-snack-bar[class*="mat-simple-snackbar"]');
emptyPagination = $('adf-pagination[class*="adf-pagination__empty"]'); emptyPagination = $('adf-pagination[class*="adf-pagination__empty"]');
dragAndDrop = $$('adf-upload-drag-area div').first(); dragAndDrop = $$('adf-upload-drag-area div').first();
nameHeader = $$('div[data-automation-id="auto_id_name"] > span').first(); nameHeader = $$('div[data-automation-id="auto_header_content_id_name"] > span').first();
sizeHeader = $$('div[data-automation-id="auto_id_content.sizeInBytes"] > span').first(); sizeHeader = $$('div[data-automation-id="auto_header_content_id_content.sizeInBytes"] > span').first();
createdByHeader = $$('div[data-automation-id="auto_id_createdByUser.displayName"] > span').first(); createdByHeader = $$('div[data-automation-id="auto_header_content_id_createdByUser.displayName"] > span').first();
createdHeader = $$('div[data-automation-id="auto_id_createdAt"] > span').first(); createdHeader = $$('div[data-automation-id="auto_header_content_id_createdAt"] > span').first();
recentFiles = $('.app-container-recent'); recentFiles = $('.app-container-recent');
recentFilesExpanded = $('.app-container-recent mat-expansion-panel-header.mat-expanded'); recentFilesExpanded = $('.app-container-recent mat-expansion-panel-header.mat-expanded');
recentFilesClosed = $('.app-container-recent mat-expansion-panel-header'); recentFilesClosed = $('.app-container-recent mat-expansion-panel-header');

View File

@ -59,6 +59,8 @@ import { DocumentListModule } from '../document-list.module';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ShareDataRow } from '../data/share-data-row.model'; import { ShareDataRow } from '../data/share-data-row.model';
import { DocumentLoaderNode } from '../models/document-folder.model'; import { DocumentLoaderNode } from '../models/document-folder.model';
import { matIconRegistryMock } from '../../testing/mat-icon-registry-mock';
import { domSanitizerMock } from '../../testing/dom-sanitizer-mock';
describe('DocumentList', () => { describe('DocumentList', () => {
@ -1139,7 +1141,7 @@ describe('DocumentList', () => {
it('should display [empty folder] template ', () => { it('should display [empty folder] template ', () => {
fixture.detectChanges(); fixture.detectChanges();
documentList.dataTable = new DataTableComponent(null, null); documentList.dataTable = new DataTableComponent(null, null, matIconRegistryMock, domSanitizerMock);
expect(documentList.dataTable).toBeDefined(); expect(documentList.dataTable).toBeDefined();
expect(fixture.debugElement.query(By.css('adf-empty-list'))).not.toBeNull(); expect(fixture.debugElement.query(By.css('adf-empty-list'))).not.toBeNull();
}); });
@ -1157,7 +1159,7 @@ describe('DocumentList', () => {
}); });
it('should empty folder NOT show the pagination', () => { it('should empty folder NOT show the pagination', () => {
documentList.dataTable = new DataTableComponent(null, null); documentList.dataTable = new DataTableComponent(null, null, matIconRegistryMock, domSanitizerMock);
expect(documentList.isEmpty()).toBeTruthy(); expect(documentList.isEmpty()).toBeTruthy();
expect(element.querySelector('alfresco-pagination')).toBe(null); expect(element.querySelector('alfresco-pagination')).toBe(null);

View File

@ -0,0 +1,5 @@
import { DomSanitizer } from '@angular/platform-browser';
export const domSanitizerMock = {
bypassSecurityTrustResourceUrl: () => {}
} as any as DomSanitizer;

View File

@ -0,0 +1,5 @@
import { MatIconRegistry } from '@angular/material/icon';
export const matIconRegistryMock = {
addSvgIconInNamespace: () => {}
} as any as MatIconRegistry;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@ -25,6 +25,9 @@ import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/co
}) })
export class DataColumnComponent implements OnInit { export class DataColumnComponent implements OnInit {
@Input()
id: string = '';
/** Data source key. Can be either a column/property key like `title` /** Data source key. Can be either a column/property key like `title`
* or a property path like `createdBy.name`. * or a property path like `createdBy.name`.
*/ */
@ -45,6 +48,10 @@ export class DataColumnComponent implements OnInit {
@Input() @Input()
sortable: boolean = true; sortable: boolean = true;
/* Enable drag and drop for header column */
@Input()
draggable: boolean = false;
/** Display title of the column, typically used for column headers. You can use the /** Display title of the column, typically used for column headers. You can use the
* i18n resource key to get it translated automatically. * i18n resource key to get it translated automatically.
*/ */

View File

@ -8,21 +8,26 @@
[class.adf-datatable--empty--header-visible]="isEmpty() && isHeaderVisible()"> [class.adf-datatable--empty--header-visible]="isEmpty() && isHeaderVisible()">
<div *ngIf="isHeaderVisible()" class="adf-datatable-header" role="rowgroup" [ngClass]="{ 'adf-sr-only': !isHeaderVisible() }"> <div *ngIf="isHeaderVisible()" class="adf-datatable-header" role="rowgroup" [ngClass]="{ 'adf-sr-only': !isHeaderVisible() }">
<adf-datatable-row <adf-datatable-row
cdkDropList
cdkDropListOrientation="horizontal"
data-automation-id="datatable-row-header" data-automation-id="datatable-row-header"
[disabled]="!isHeaderVisible()" [disabled]="!isHeaderVisible()"
class="adf-datatable-row" class="adf-datatable-row"
*ngIf="display === 'list'" *ngIf="display === 'list'"
role="row"> role="row">
<!-- 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>
</div> </div>
<!-- Columns --> <!-- Columns -->
<div *ngIf="multiselect" class="adf-datatable-cell-header adf-datatable-checkbox"> <div *ngIf="multiselect" class="adf-datatable-cell-header adf-datatable-checkbox">
<mat-checkbox [indeterminate]="isSelectAllIndeterminate" [checked]="isSelectAllChecked" (change)="onSelectAllClick($event)" class="adf-checkbox-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_ALL' | translate }}</mat-checkbox> <mat-checkbox [indeterminate]="isSelectAllIndeterminate" [checked]="isSelectAllChecked" (change)="onSelectAllClick($event)" class="adf-checkbox-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_ALL' | translate }}</mat-checkbox>
</div> </div>
<div class="adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}} adf-datatable-cell-header" <div class="adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}} adf-datatable-cell-header"
*ngFor="let col of data.getColumns()" *ngFor="let col of data.getColumns(); let columnIndex = index"
[class.adf-sortable]="col.sortable" [class.adf-sortable]="col.sortable"
[attr.data-automation-id]="'auto_id_' + col.key" [attr.data-automation-id]="'auto_id_' + col.key"
[class.adf-datatable__header--sorted-asc]="isColumnSorted(col, 'asc')" [class.adf-datatable__header--sorted-asc]="isColumnSorted(col, 'asc')"
@ -32,14 +37,59 @@
role="columnheader" role="columnheader"
[attr.tabindex]="isHeaderVisible() ? 0 : null" [attr.tabindex]="isHeaderVisible() ? 0 : null"
[attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null" [attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null"
cdkDrag
cdkDragBoundary="adf-datatable-row"
cdkDragLockAxis="x"
(cdkDragStarted)="isDraggingHeaderColumn = true"
(cdkDragDropped)="onDropHeaderColumn($event)"
[cdkDragDisabled]="!col.draggable"
(mouseenter)="hoveredHeaderColumnIndex = columnIndex"
(mouseleave)="hoveredHeaderColumnIndex = -1"
adf-drop-zone dropTarget="header" [dropColumn]="col"> adf-drop-zone dropTarget="header" [dropColumn]="col">
<ng-container *ngIf="!col.header">
<span *ngIf="col.title" class="adf-datatable-cell-value">{{ col.title | translate}}</span> <div
<span *ngIf="col.title && col.sortable" class="adf-sr-only" aria-live="polite">{{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }}</span> *ngIf="!col.header"
[attr.data-automation-id]="'auto_header_content_id_' + col.key"
class="adf-datatable-cell-header-content"
[class.adf-datatable-cell-header-content--hovered]="hoveredHeaderColumnIndex === columnIndex && !isDraggingHeaderColumn"
>
<span
*ngIf="hoveredHeaderColumnIndex === columnIndex && col.draggable"
class="adf-datatable-cell-header-drag-icon-placeholder"
[attr.data-automation-id]="'adf-datatable-cell-header-drag-icon-placeholder-'+col.key"
></span>
<span *ngIf="col.title" class="adf-datatable-cell-value"> {{col.title | translate}}</span>
<span *ngIf="col.title && col.sortable && isDraggingHeaderColumn" class="adf-sr-only" aria-live="polite">
{{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }}
</span>
<ng-template *ngIf="allowFiltering" [ngTemplateOutlet]="headerFilterTemplate" [ngTemplateOutletContext]="{$implicit: col}"></ng-template> <ng-template *ngIf="allowFiltering" [ngTemplateOutlet]="headerFilterTemplate" [ngTemplateOutletContext]="{$implicit: col}"></ng-template>
</ng-container>
<span
[class.adf-datatable__header--sorted-asc]="isColumnSorted(col, 'asc')"
[class.adf-datatable__header--sorted-desc]="isColumnSorted(col, 'desc')">
</span>
</div>
<ng-template *ngIf="col.header" [ngTemplateOutlet]="col.header" [ngTemplateOutletContext]="{$implicit: col}"></ng-template> <ng-template *ngIf="col.header" [ngTemplateOutlet]="col.header" [ngTemplateOutletContext]="{$implicit: col}"></ng-template>
<div
*ngIf="col.draggable"
cdkDragHandle
class="adf-datatable-cell-header-drag-icon"
>
<adf-icon
*ngIf="hoveredHeaderColumnIndex === columnIndex"
value="adf:drag_indicator"
[attr.data-automation-id]="'adf-datatable-cell-header-drag-icon-'+col.key">
</adf-icon>
</div>
<div class="adf-drop-header-cell-placeholder" *cdkDragPlaceholder></div>
</div> </div>
<!-- Actions (right) --> <!-- Actions (right) -->
<div *ngIf="actions && actionsPosition === 'right'" class="adf-actions-column adf-datatable-cell-header adf-datatable__actions-cell"> <div *ngIf="actions && actionsPosition === 'right'" class="adf-actions-column adf-datatable-cell-header adf-datatable__actions-cell">
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span> <span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
@ -58,7 +108,10 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="adf-datatable-body" role="rowgroup"> <div
class="adf-datatable-body"
[class.adf-blur-datatable-body]="isDraggingHeaderColumn"
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"
[row]="row" [row]="row"
@ -171,7 +224,7 @@
[tooltip]="getCellTooltip(row, col)"> [tooltip]="getCellTooltip(row, col)">
</adf-filesize-cell> </adf-filesize-cell>
</div> </div>
<div *ngSwitchCase="'text'" [attr.tabindex]="data.getValue(row, col, resolverFn)? 0 : -1" class="adf-cell-value" <div *ngSwitchCase="'text'" [attr.tabindex]="data.getValue(row, col, resolverFn)? 0 : -1" class="adf-cell-value"
[attr.data-automation-id]="'text_' + data.getValue(row, col, resolverFn)"> [attr.data-automation-id]="'text_' + data.getValue(row, col, resolverFn)">
<adf-datatable-cell <adf-datatable-cell
[copyContent]="col.copyContent" [copyContent]="col.copyContent"

View File

@ -9,8 +9,8 @@ $data-table-column-spacing: 36px !default;
$data-table-column-padding: 18px !default; $data-table-column-padding: 18px !default;
$data-table-card-padding: var(--theme-headline-line-height) !default; $data-table-card-padding: var(--theme-headline-line-height) !default;
$data-table-cell-top: calc($data-table-card-padding / 2); $data-table-cell-top: calc($data-table-card-padding / 2);
$data-table-thumbnail-width: 50px !default; $data-table-thumbnail-width: 65px !default;
$data-table-cell-min-width: 50px !default; $data-table-cell-min-width: 65px !default;
$data-table-cell-min-width-no-grow: 100px !default; $data-table-cell-min-width-no-grow: 100px !default;
$data-table-cell-min-width-file-size: $data-table-cell-min-width !default; $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
@ -208,6 +208,13 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
width: fit-content; width: fit-content;
min-width: 100%; min-width: 100%;
box-sizing: border-box; box-sizing: border-box;
.adf-datatable-row {
&:hover,
&:focus {
background-color: inherit;
}
}
} }
.adf-datatable-cell { .adf-datatable-cell {
@ -234,6 +241,10 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
} }
} }
.adf-blur-datatable-body {
filter: blur(3px);
}
.adf-datatable-body { .adf-datatable-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -315,66 +326,6 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
} }
} }
.adf-datatable-cell-header {
@include adf-no-select;
cursor: pointer;
position: relative;
vertical-align: bottom;
text-overflow: ellipsis;
font-weight: bold;
line-height: 24px;
letter-spacing: 0;
min-height: $data-table-row-height !important;
font-size: $data-table-header-font-size;
color: var(--theme-text-fg-color);
padding-bottom: 8px;
box-sizing: border-box;
padding-top: 12px !important;
&.adf-sortable {
@include adf-no-select;
&:hover {
cursor: pointer;
}
display: flex;
align-items: center;
}
&.adf-datatable__header--sorted-asc,
&.adf-datatable__header--sorted-desc {
color: var(--theme-text-fg-color);
&::before {
@include typo-icon;
font-size: $data-table-header-sort-icon-size;
content: '\e5d8';
left: 5px;
right: 5px;
position: relative;
vertical-align: sub;
}
}
&.adf-datatable__header--sorted-desc::before {
content: '\e5db';
}
&.adf-datatable-cell--fileSize.adf-datatable__header--sorted-asc::before,
&.adf-datatable-cell--fileSize.adf-datatable__header--sorted-desc::before {
left: -3px;
right: -3px;
}
&.adf-datatable-checkbox {
display: flex;
align-items: center;
}
}
.adf-datatable-cell-header.adf-expand-cell-1, .adf-datatable-cell-header.adf-expand-cell-1,
.adf-datatable-cell.adf-expand-cell-1 { .adf-datatable-cell.adf-expand-cell-1 {
flex-grow: 1; flex-grow: 1;
@ -421,11 +372,12 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
align-items: center; align-items: center;
display: flex; display: flex;
width: 100%; width: 100%;
padding: 0 10px;
} }
.adf-datatable-cell-value { .adf-datatable-cell-value {
word-break: break-word; word-break: break-word;
padding: 10px; padding-right: 10px;
display: block; display: block;
@media screen and (-ms-high-contrast: active), screen and (-ms-high-contrast: none) { @media screen and (-ms-high-contrast: active), screen and (-ms-high-contrast: none) {
@ -519,7 +471,6 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
padding: 0 4px;
&.adf-datatable-cell-header, &.adf-datatable-cell-header,
.adf-datatable-content-cell { .adf-datatable-content-cell {
@ -606,6 +557,136 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
} }
} }
.adf-datatable-cell-header {
@include adf-no-select;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
text-overflow: ellipsis;
font-weight: bold;
line-height: 24px;
letter-spacing: 0;
min-height: $data-table-row-height !important;
font-size: $data-table-header-font-size;
color: var(--theme-text-fg-color);
box-sizing: border-box;
&.adf-sortable {
@include adf-no-select;
&:hover {
cursor: pointer;
}
display: flex;
align-items: center;
}
.adf-datatable__header--sorted-asc,
.adf-datatable__header--sorted-desc {
color: var(--theme-text-fg-color);
&::after {
@include typo-icon;
font-size: $data-table-header-sort-icon-size;
content: '\e5d8';
left: 5px;
right: 5px;
position: relative;
vertical-align: sub;
}
}
.adf-datatable__header--sorted-desc::after {
content: '\e5db';
}
.adf-datatable-cell--fileSize.adf-datatable__header--sorted-asc::before,
.adf-datatable-cell--fileSize.adf-datatable__header--sorted-desc::before {
left: -3px;
right: -3px;
}
&.adf-datatable-checkbox {
display: flex;
align-items: center;
}
.adf-datatable-cell-header-content {
display: flex;
flex-grow: 1;
align-items: center;
margin: 0 2px;
padding: 0 8px;
column-gap: 5px;
.adf-datatable-cell-header-drag-icon-placeholder {
min-width: 15px;
}
&--hovered {
background-color: var(--theme-bg-hover-color);
border-radius: 6px;
}
}
.adf-datatable-cell-header-drag-icon {
position: absolute;
top: 0;
bottom: 0;
left: 3px;
margin: auto;
color: #ccc;
cursor: move;
width: 24px;
height: 24px;
}
.adf-datatable-cell-value {
padding: 10px 0 !important;
line-height: 24px;
word-break: break-word;
}
}
.adf-drop-header-cell-placeholder {
display: flex;
flex: 1;
flex-grow: 1;
background: var(--theme-bg-hover-color);
border: dotted 1px rgba(0, 0, 0, 0.25);
min-height: 55px;
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.adf-datatable-cell-header.adf-ellipsis-cell {
.adf-datatable-cell-header-content {
white-space: nowrap;
overflow: hidden;
}
.adf-datatable-cell-header-content--hovered {
.adf-datatable-cell-value {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.cdk-drag-preview {
&.adf-datatable-cell-header {
border-radius: 6px;
background-color: var(--theme-background-color);
@include mat-elevation-transition;
@include mat-overridable-elevation(4);
}
}
/* [Accessibility] Material checkbox labels */ /* [Accessibility] Material checkbox labels */
.adf-checkbox-sr-only .mat-checkbox-label { .adf-checkbox-sr-only .mat-checkbox-label {
position: absolute; position: absolute;

View File

@ -29,6 +29,9 @@ import { CoreTestingModule } from '../../../testing/core.testing.module';
import { DataColumnListComponent } from '../../../data-column/data-column-list.component'; import { DataColumnListComponent } from '../../../data-column/data-column-list.component';
import { DataColumnComponent } from '../../../data-column/data-column.component'; import { DataColumnComponent } from '../../../data-column/data-column.component';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { domSanitizerMock } from 'content-services/src/lib/testing/dom-sanitizer-mock';
import { matIconRegistryMock } from 'content-services/src/lib/testing/mat-icon-registry-mock';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
@Component({selector: 'adf-custom-column-template-component', template: ` @Component({selector: 'adf-custom-column-template-component', template: `
<ng-template #tmplRef></ng-template> <ng-template #tmplRef></ng-template>
@ -722,7 +725,7 @@ describe('DataTable', () => {
}); });
it('should initialize default adapter', () => { it('should initialize default adapter', () => {
const table = new DataTableComponent(null, null); const table = new DataTableComponent(null, null, matIconRegistryMock, domSanitizerMock);
expect(table.data).toBeUndefined(); expect(table.data).toBeUndefined();
table.ngOnChanges({ data: new SimpleChange('123', {}, true) }); table.ngOnChanges({ data: new SimpleChange('123', {}, true) });
expect(table.data).toEqual(jasmine.any(ObjectDataTableAdapter)); expect(table.data).toEqual(jasmine.any(ObjectDataTableAdapter));
@ -1605,3 +1608,115 @@ describe('Accesibility', () => {
expect(cell.getAttribute('tabindex')).toBe('0'); expect(cell.getAttribute('tabindex')).toBe('0');
}); });
}); });
describe('Drag&Drop column header', () => {
let fixture: ComponentFixture<DataTableComponent>;
let dataTable: DataTableComponent;
let data: { id: number; name: string }[] = [];
let dataTableSchema: DataColumn[] = [];
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
declarations: [CustomColumnTemplateComponent],
schemas: [NO_ERRORS_SCHEMA]
});
beforeEach(() => {
fixture = TestBed.createComponent(DataTableComponent);
dataTable = fixture.componentInstance;
data = [
{ id: 1, name: 'name1' },
{ id: 2, name: 'name2' }
];
dataTableSchema = [
new ObjectDataColumn({ key: 'id', title: 'ID', draggable: false }),
new ObjectDataColumn({ key: 'name', title: 'Name', draggable: true })
];
dataTable.data = new ObjectDataTableAdapter(
[...data],
[...dataTableSchema]
);
});
it('should show/hide drag indicator icon', () => {
fixture.detectChanges();
const hedaderColumn = fixture.debugElement.nativeElement.querySelector('[data-automation-id="auto_id_name"]');
hedaderColumn.dispatchEvent(new MouseEvent('mouseenter'));
fixture.detectChanges();
let dragIcon = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-datatable-cell-header-drag-icon-name"]');
let dragIconPlaceholder = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-datatable-cell-header-drag-icon-placeholder-name"]');
expect(dragIcon).toBeTruthy();
expect(dragIconPlaceholder).toBeTruthy();
hedaderColumn.dispatchEvent(new MouseEvent('mouseleave'));
fixture.detectChanges();
dragIcon = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-datatable-cell-header-drag-icon-name"]');
dragIconPlaceholder = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-datatable-cell-header-drag-icon-placeholder-name"]');
expect(dragIcon).toBeFalsy();
expect(dragIconPlaceholder).toBeFalsy();
});
it('should not show drag indicator icon, when drag and drop is disabled', () => {
fixture.detectChanges();
const hedaderColumn = fixture.debugElement.nativeElement.querySelector('[data-automation-id="auto_id_id"]');
hedaderColumn.dispatchEvent(new MouseEvent('mouseenter'));
fixture.detectChanges();
const dragIcon = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-datatable-cell-header-drag-icon-id"]');
const dragIconPlaceholder = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-datatable-cell-header-drag-icon-placeholder-id"]');
expect(dragIcon).toBeFalsy();
expect(dragIconPlaceholder).toBeFalsy();
});
it('should emit on change column order', () => {
const columnOrderChangedSpy = spyOn(dataTable.columnOrderChanged, 'emit');
const dropEvent: CdkDragDrop<unknown> = {
previousIndex: 0,
currentIndex: 1,
item: undefined,
container: undefined,
previousContainer: undefined,
isPointerOverContainer: true,
distance: { x: 0, y: 0 }
};
dataTable.onDropHeaderColumn(dropEvent);
expect(columnOrderChangedSpy).toHaveBeenCalledWith(dataTableSchema.reverse());
});
it('should change columns order', () => {
const dropEvent: CdkDragDrop<unknown> = {
previousIndex: 0,
currentIndex: 1,
item: undefined,
container: undefined,
previousContainer: undefined,
isPointerOverContainer: true,
distance: { x: 0, y: 0 }
};
dataTable.onDropHeaderColumn(dropEvent);
fixture.detectChanges();
const columns = dataTable.data.getColumns();
const headerCells = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell--text.adf-datatable-cell-header');
expect(columns[0].key).toEqual(dataTableSchema[1].key);
expect(columns[1].key).toEqual(dataTableSchema[0].key);
expect(headerCells[0].innerText).toBe(dataTableSchema[1].title);
expect(headerCells[1].innerText).toBe(dataTableSchema[0].title);
});
});

View File

@ -20,7 +20,7 @@
import { import {
ViewChildren, QueryList, HostListener, ViewChildren, QueryList, HostListener,
AfterContentInit, Component, ContentChild, DoCheck, ElementRef, EventEmitter, Input, AfterContentInit, Component, ContentChild, DoCheck, ElementRef, EventEmitter, Input,
IterableDiffers, OnChanges, Output, SimpleChange, SimpleChanges, TemplateRef, ViewEncapsulation, OnDestroy, AfterViewInit IterableDiffers, OnChanges, Output, SimpleChange, SimpleChanges, TemplateRef, ViewEncapsulation, OnDestroy, AfterViewInit, OnInit
} from '@angular/core'; } from '@angular/core';
import { FocusKeyManager } from '@angular/cdk/a11y'; import { FocusKeyManager } from '@angular/cdk/a11y';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
@ -40,6 +40,9 @@ import { ObjectDataTableAdapter } from '../../data/object-datatable-adapter';
import { DataCellEvent } from '../data-cell.event'; import { DataCellEvent } from '../data-cell.event';
import { DataRowActionEvent } from '../data-row-action.event'; import { DataRowActionEvent } from '../data-row-action.event';
import { share, buffer, map, filter, debounceTime } from 'rxjs/operators'; import { share, buffer, map, filter, debounceTime } from 'rxjs/operators';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
export enum DisplayMode { export enum DisplayMode {
@ -61,7 +64,7 @@ export enum ShowHeaderMode {
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'adf-datatable' } host: { class: 'adf-datatable' }
}) })
export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck, OnDestroy, AfterViewInit { export class DataTableComponent implements OnInit, AfterContentInit, OnChanges, DoCheck, OnDestroy, AfterViewInit {
@ViewChildren(DataTableRowComponent) @ViewChildren(DataTableRowComponent)
rowsList: QueryList<DataTableRowComponent>; rowsList: QueryList<DataTableRowComponent>;
@ -160,6 +163,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
@Output() @Output()
executeRowAction = new EventEmitter<DataRowActionEvent>(); executeRowAction = new EventEmitter<DataRowActionEvent>();
@Output()
columnOrderChanged = new EventEmitter<DataColumn[]>();
/** Flag that indicates if the datatable is in loading state and needs to show the /** 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 template (see the docs to learn how to configure a loading template).
*/ */
@ -199,6 +205,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
isSelectAllChecked: boolean = false; isSelectAllChecked: boolean = false;
selection = new Array<DataRow>(); selection = new Array<DataRow>();
isDraggingHeaderColumn = false;
hoveredHeaderColumnIndex = -1;
/** This array of fake rows fix the flex layout for the gallery view */ /** This array of fake rows fix the flex layout for the gallery view */
fakeRows = []; fakeRows = [];
@ -220,14 +229,21 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
} }
constructor(private elementRef: ElementRef, constructor(private elementRef: ElementRef,
differs: IterableDiffers) { differs: IterableDiffers,
private matIconRegistry: MatIconRegistry,
private sanitizer: DomSanitizer) {
if (differs) { if (differs) {
this.differ = differs.find([]).create(null); this.differ = differs.find([]).create(null);
} }
this.click$ = new Observable<DataRowEvent>((observer) => this.clickObserver = observer) this.click$ = new Observable<DataRowEvent>((observer) => this.clickObserver = observer)
.pipe(share()); .pipe(share());
} }
ngOnInit(): void {
this.registerDragHandleIcon();
}
ngAfterContentInit() { ngAfterContentInit() {
if (this.columnList) { if (this.columnList) {
this.subscriptions.push( this.subscriptions.push(
@ -260,13 +276,18 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
if (dataChanges) { if (dataChanges) {
this.data = changes['data'].currentValue; this.data = changes['data'].currentValue;
this.resetSelection(); this.resetSelection();
} else if (rowChanges) { }
if (rowChanges) {
this.setTableRows(changes['rows'].currentValue); this.setTableRows(changes['rows'].currentValue);
this.setTableSorting(this.sorting); this.setTableSorting(this.sorting);
} else { }
if (columnChanges) {
this.setTableColumns(changes['columns'].currentValue); this.setTableColumns(changes['columns'].currentValue);
} }
} }
return; return;
} }
@ -291,6 +312,14 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
return column.key === this.data.getSorting().key; return column.key === this.data.getSorting().key;
} }
onDropHeaderColumn(event: CdkDragDrop<unknown>): void {
const columns = this.data.getColumns();
moveItemInArray(columns, event.previousIndex, event.currentIndex);
this.columnOrderChanged.emit(columns);
this.isDraggingHeaderColumn = false;
}
ngDoCheck() { ngDoCheck() {
const changes = this.differ.diff(this.rows); const changes = this.differ.diff(this.rows);
if (changes) { if (changes) {
@ -849,6 +878,18 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
'ADF-DATATABLE.ACCESSIBILITY.SORT_ASCENDING_BY' : 'ADF-DATATABLE.ACCESSIBILITY.SORT_ASCENDING_BY' :
'ADF-DATATABLE.ACCESSIBILITY.SORT_DESCENDING_BY'; 'ADF-DATATABLE.ACCESSIBILITY.SORT_DESCENDING_BY';
} }
private registerDragHandleIcon(): void {
const iconUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
'./assets/images/drag_indicator_24px.svg'
);
this.matIconRegistry.addSvgIconInNamespace(
'adf',
'drag_indicator',
iconUrl
);
}
} }
export interface DataTableDropEvent { export interface DataTableDropEvent {

View File

@ -30,6 +30,7 @@ export interface DataColumnTypes {
export type DataColumnType = keyof DataColumnTypes; export type DataColumnType = keyof DataColumnTypes;
export interface DataColumn { export interface DataColumn {
id?: string;
key: string; key: string;
type: DataColumnType; type: DataColumnType;
format?: string; format?: string;
@ -44,4 +45,5 @@ export interface DataColumn {
focus?: boolean; focus?: boolean;
sortingKey?: string; sortingKey?: string;
header?: TemplateRef<any>; header?: TemplateRef<any>;
draggable?: boolean;
} }

View File

@ -16,6 +16,7 @@
*/ */
import { ContentChild, Input, Directive } from '@angular/core'; import { ContentChild, Input, Directive } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { AppConfigService } from '../../app-config/app-config.service'; import { AppConfigService } from '../../app-config/app-config.service';
import { DataColumnListComponent } from '../../data-column/data-column-list.component'; import { DataColumnListComponent } from '../../data-column/data-column-list.component';
import { DataColumn } from './data-column.model'; import { DataColumn } from './data-column.model';
@ -34,19 +35,34 @@ export abstract class DataTableSchema {
columns: any; columns: any;
protected columnsOrder: string[] | undefined;
protected columnsOrderedByKey: string = 'id';
private layoutPresets = {}; private layoutPresets = {};
private columnsSchemaSubject$ = new ReplaySubject<boolean>();
isColumnSchemaCreated$ = this.columnsSchemaSubject$.asObservable();
constructor(private appConfigService: AppConfigService, constructor(private appConfigService: AppConfigService,
protected presetKey: string, protected presetKey: string,
protected presetsModel: any) { } protected presetsModel: any) { }
public createDatatableSchema(): void { public createDatatableSchema(): void {
this.loadLayoutPresets(); this.loadLayoutPresets();
if (!this.columns || this.columns.length === 0) { if (!this.columns || this.columns.length === 0) {
this.columns = this.mergeJsonAndHtmlSchema(); this.createColumns();
this.columnsSchemaSubject$.next(true);
} else {
this.columnsSchemaSubject$.next(false);
} }
} }
public createColumns(): void {
const columns = this.mergeJsonAndHtmlSchema();
this.columns = this.sortColumnsByKey(columns);
}
public loadLayoutPresets(): void { public loadLayoutPresets(): void {
const externalSettings = this.appConfigService.get(this.presetKey, null); const externalSettings = this.appConfigService.get(this.presetKey, null);
if (externalSettings) { if (externalSettings) {
@ -57,10 +73,18 @@ export abstract class DataTableSchema {
} }
public mergeJsonAndHtmlSchema(): any { public mergeJsonAndHtmlSchema(): any {
let customSchemaColumns = this.getSchemaFromConfig(this.presetColumn).concat(this.getSchemaFromHtml(this.columnList)); const configSchemaColumns = this.getSchemaFromConfig(this.presetColumn);
const htmlSchemaColumns = this.getSchemaFromHtml(this.columnList);
let customSchemaColumns = [
...configSchemaColumns,
...htmlSchemaColumns
];
if (customSchemaColumns.length === 0) { if (customSchemaColumns.length === 0) {
customSchemaColumns = this.getDefaultLayoutPreset(); customSchemaColumns = this.getDefaultLayoutPreset();
} }
return customSchemaColumns; return customSchemaColumns;
} }
@ -87,4 +111,20 @@ export abstract class DataTableSchema {
public setPresetsModel(presetsModel: any) { public setPresetsModel(presetsModel: any) {
this.presetsModel = presetsModel; this.presetsModel = presetsModel;
} }
private sortColumnsByKey(columns: any[]): any[] {
const defaultColumns = [...columns];
const columnsWithProperOrder = [];
(this.columnsOrder ?? []).forEach(columnKey => {
const originalColumnIndex = defaultColumns.findIndex(defaultColumn => defaultColumn[this.columnsOrderedByKey] === columnKey);
if (originalColumnIndex > -1) {
columnsWithProperOrder.push(defaultColumns[originalColumnIndex]);
defaultColumns.splice(originalColumnIndex, 1);
}
});
return [...columnsWithProperOrder, ...defaultColumns];
}
} }

View File

@ -20,7 +20,7 @@ import { DataColumn, DataColumnType } from './data-column.model';
// Simple implementation of the DataColumn interface. // Simple implementation of the DataColumn interface.
export class ObjectDataColumn implements DataColumn { export class ObjectDataColumn implements DataColumn {
id?: string;
key: string; key: string;
type: DataColumnType; type: DataColumnType;
format: string; format: string;
@ -33,8 +33,10 @@ export class ObjectDataColumn implements DataColumn {
focus?: boolean; focus?: boolean;
sortingKey?: string; sortingKey?: string;
header?: TemplateRef<any>; header?: TemplateRef<any>;
draggable: boolean;
constructor(input: any) { constructor(input: any) {
this.id = input.id ?? '';
this.key = input.key; this.key = input.key;
this.type = input.type || 'text'; this.type = input.type || 'text';
this.format = input.format; this.format = input.format;
@ -47,5 +49,6 @@ export class ObjectDataColumn implements DataColumn {
this.focus = input.focus; this.focus = input.focus;
this.sortingKey = input.sortingKey; this.sortingKey = input.sortingKey;
this.header = input.header; this.header = input.header;
this.draggable = input.draggable ?? false;
} }
} }

View File

@ -46,6 +46,8 @@ import { JsonCellComponent } from './components/json-cell/json-cell.component';
import { ClipboardModule } from '../clipboard/clipboard.module'; import { ClipboardModule } from '../clipboard/clipboard.module';
import { DropZoneDirective } from './directives/drop-zone.directive'; import { DropZoneDirective } from './directives/drop-zone.directive';
import { DataColumnModule } from '../data-column/data-column.module'; import { DataColumnModule } from '../data-column/data-column.module';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { IconModule } from '../icon/icon.module';
@NgModule({ @NgModule({
imports: [ imports: [
@ -57,7 +59,9 @@ import { DataColumnModule } from '../data-column/data-column.module';
ContextMenuModule, ContextMenuModule,
PipeModule, PipeModule,
DirectiveModule, DirectiveModule,
ClipboardModule ClipboardModule,
DragDropModule,
IconModule
], ],
declarations: [ declarations: [
DataTableComponent, DataTableComponent,
@ -101,6 +105,5 @@ import { DataColumnModule } from '../data-column/data-column.module';
CustomNoPermissionTemplateDirective, CustomNoPermissionTemplateDirective,
DropZoneDirective DropZoneDirective
] ]
}) })
export class DataTableModule {} export class DataTableModule {}

View File

@ -27,7 +27,9 @@ import {
LocalPreferenceCloudService, LocalPreferenceCloudService,
PreferenceCloudServiceInterface, PreferenceCloudServiceInterface,
PROCESS_FILTERS_SERVICE_TOKEN, PROCESS_FILTERS_SERVICE_TOKEN,
TASK_FILTERS_SERVICE_TOKEN TASK_FILTERS_SERVICE_TOKEN,
PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN,
TASK_LIST_PREFERENCES_SERVICE_TOKEN
} from './services/public-api'; } from './services/public-api';
import { PeopleCloudModule } from './people/people-cloud.module'; import { PeopleCloudModule } from './people/people-cloud.module';
import { CloudFormRenderingService } from './form/components/cloud-form-rendering.service'; import { CloudFormRenderingService } from './form/components/cloud-form-rendering.service';
@ -67,7 +69,10 @@ import { ProcessServicesCloudPipeModule } from './pipes/process-services-cloud-p
] ]
}) })
export class ProcessServicesCloudModule { export class ProcessServicesCloudModule {
static forRoot(preferenceServiceInstance?: PreferenceCloudServiceInterface): ModuleWithProviders<ProcessServicesCloudModule> { static forRoot(
filterPreferenceServiceInstance?: PreferenceCloudServiceInterface,
listPreferenceServiceInstance?: PreferenceCloudServiceInterface
): ModuleWithProviders<ProcessServicesCloudModule> {
return { return {
ngModule: ProcessServicesCloudModule, ngModule: ProcessServicesCloudModule,
providers: [ providers: [
@ -79,8 +84,10 @@ export class ProcessServicesCloudModule {
source: 'assets/adf-process-services-cloud' source: 'assets/adf-process-services-cloud'
} }
}, },
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useExisting: preferenceServiceInstance ?? LocalPreferenceCloudService }, { provide: PROCESS_FILTERS_SERVICE_TOKEN, useExisting: filterPreferenceServiceInstance ?? LocalPreferenceCloudService },
{ provide: TASK_FILTERS_SERVICE_TOKEN, useExisting: preferenceServiceInstance ?? LocalPreferenceCloudService }, { provide: TASK_FILTERS_SERVICE_TOKEN, useExisting: filterPreferenceServiceInstance ?? LocalPreferenceCloudService },
{ provide: PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN, useExisting: listPreferenceServiceInstance ?? LocalPreferenceCloudService },
{ provide: TASK_LIST_PREFERENCES_SERVICE_TOKEN, useExisting: listPreferenceServiceInstance ?? LocalPreferenceCloudService },
FormRenderingService, FormRenderingService,
{ provide: FormRenderingService, useClass: CloudFormRenderingService } { provide: FormRenderingService, useClass: CloudFormRenderingService }
] ]

View File

@ -16,7 +16,8 @@
(row-select)="onRowSelect($any($event))" (row-select)="onRowSelect($any($event))"
(row-unselect)="onRowUnselect($any($event))" (row-unselect)="onRowUnselect($any($event))"
(row-keyup)="onRowKeyUp($any($event))" (row-keyup)="onRowKeyUp($any($event))"
(sorting-changed)="onSortingChanged($any($event))"> (sorting-changed)="onSortingChanged($any($event))"
(columnOrderChanged)="onColumnOrderChanged($event)">
<adf-loading-content-template> <adf-loading-content-template>
<ng-template> <ng-template>
<mat-progress-spinner <mat-progress-spinner

View File

@ -27,7 +27,7 @@ import { ProcessListCloudService } from '../services/process-list-cloud.service'
import { ProcessListCloudComponent } from './process-list-cloud.component'; import { ProcessListCloudComponent } from './process-list-cloud.component';
import { fakeCustomSchema, fakeProcessCloudList, processListSchemaMock } from '../mock/process-list-service.mock'; import { fakeCustomSchema, fakeProcessCloudList, processListSchemaMock } from '../mock/process-list-service.mock';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { skip } from 'rxjs/operators'; import { shareReplay, skip } from 'rxjs/operators';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ProcessListCloudSortingModel } from '../models/process-list-sorting.model'; import { ProcessListCloudSortingModel } from '../models/process-list-sorting.model';
@ -102,6 +102,8 @@ describe('ProcessListCloudComponent', () => {
} }
} }
}); });
component.isColumnSchemaCreated$ = of(true).pipe(shareReplay(1));
}); });
afterEach(() => fixture.destroy()); afterEach(() => fixture.destroy());

View File

@ -15,16 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, ViewEncapsulation, OnChanges, AfterContentInit, ContentChild, Output, EventEmitter, SimpleChanges, Input, ViewChild } from '@angular/core'; import { Component, ViewEncapsulation, OnChanges, AfterContentInit, ContentChild, Output, EventEmitter, SimpleChanges, Input, ViewChild, Inject } from '@angular/core';
import { DataTableSchema, PaginatedComponent, import { DataTableSchema, PaginatedComponent,
CustomEmptyContentTemplateDirective, AppConfigService, CustomEmptyContentTemplateDirective, AppConfigService,
UserPreferencesService, PaginationModel, UserPreferencesService, PaginationModel,
UserPreferenceValues, DataRowEvent, CustomLoadingContentTemplateDirective, DataCellEvent, DataRowActionEvent, DataTableComponent } from '@alfresco/adf-core'; UserPreferenceValues, DataRowEvent, CustomLoadingContentTemplateDirective, DataCellEvent, DataRowActionEvent, DataTableComponent, DataColumn } from '@alfresco/adf-core';
import { ProcessListCloudService } from '../services/process-list-cloud.service'; import { ProcessListCloudService } from '../services/process-list-cloud.service';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject, combineLatest } from 'rxjs';
import { processCloudPresetsDefaultModel } from '../models/process-cloud-preset.model'; import { processCloudPresetsDefaultModel } from '../models/process-cloud-preset.model';
import { ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model'; import { ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model';
import { ProcessListCloudSortingModel } from '../models/process-list-sorting.model'; import { ProcessListCloudSortingModel } from '../models/process-list-sorting.model';
import { map, take } from 'rxjs/operators';
import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
import { PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { ProcessListCloudPreferences } from '../models/process-cloud-preferences';
const PRESET_KEY = 'adf-cloud-process-list.presets'; const PRESET_KEY = 'adf-cloud-process-list.presets';
@ -189,14 +193,17 @@ export class ProcessListCloudComponent extends DataTableSchema implements OnChan
currentInstanceId: string; currentInstanceId: string;
selectedInstances: any[]; selectedInstances: any[];
isLoading = true; isLoading = true;
rows: any[] = []; rows: any[] = [];
formattedSorting: any[]; formattedSorting: any[];
requestNode: ProcessQueryCloudRequestModel; requestNode: ProcessQueryCloudRequestModel;
private defaultSorting = { key: 'startDate', direction: 'desc' }; private defaultSorting = { key: 'startDate', direction: 'desc' };
constructor(private processListCloudService: ProcessListCloudService, constructor(private processListCloudService: ProcessListCloudService,
appConfigService: AppConfigService, appConfigService: AppConfigService,
private userPreferences: UserPreferencesService) { private userPreferences: UserPreferencesService,
@Inject(PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN) private cloudPreferenceService: PreferenceCloudServiceInterface) {
super(appConfigService, PRESET_KEY, processCloudPresetsDefaultModel); super(appConfigService, PRESET_KEY, processCloudPresetsDefaultModel);
this.size = userPreferences.paginationSize; this.size = userPreferences.paginationSize;
this.userPreferences.select(UserPreferenceValues.PaginationSize).subscribe((pageSize) => { this.userPreferences.select(UserPreferenceValues.PaginationSize).subscribe((pageSize) => {
@ -210,7 +217,23 @@ export class ProcessListCloudComponent extends DataTableSchema implements OnChan
} }
ngAfterContentInit() { ngAfterContentInit() {
this.createDatatableSchema(); this.cloudPreferenceService.getPreferences(this.appName)
.pipe(
take(1),
map((preferences => {
const preferencesList = preferences?.list?.entries ?? [];
const columnsOrder = preferencesList.find(preference => preference.entry.key === ProcessListCloudPreferences.columnOrder);
return {
columnsOrder: columnsOrder ? JSON.parse(columnsOrder.entry.value) : undefined
};
}))
)
.subscribe(({ columnsOrder }) => {
this.columnsOrder = columnsOrder;
this.createDatatableSchema();
});
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
@ -237,16 +260,21 @@ export class ProcessListCloudComponent extends DataTableSchema implements OnChan
private load(requestNode: ProcessQueryCloudRequestModel) { private load(requestNode: ProcessQueryCloudRequestModel) {
this.isLoading = true; this.isLoading = true;
this.processListCloudService.getProcessByRequest(requestNode).subscribe(
(processes) => { combineLatest([
this.rows = processes.list.entries; this.processListCloudService.getProcessByRequest(requestNode),
this.success.emit(processes); this.isColumnSchemaCreated$
this.isLoading = false; ]).pipe(
this.pagination.next(processes.list.pagination); take(1)
}, (error) => { ).subscribe(([processes]) => {
this.error.emit(error); this.rows = processes.list.entries;
this.isLoading = false; this.success.emit(processes);
}); this.isLoading = false;
this.pagination.next(processes.list.pagination);
}, (error) => {
this.error.emit(error);
this.isLoading = false;
});
} }
private isAnyPropertyChanged(changes: SimpleChanges): boolean { private isAnyPropertyChanged(changes: SimpleChanges): boolean {
@ -297,6 +325,17 @@ export class ProcessListCloudComponent extends DataTableSchema implements OnChan
this.reload(); this.reload();
} }
onColumnOrderChanged(columnsWithNewOrder: DataColumn[]): void {
if (this.appName) {
const newColumnsOrder = columnsWithNewOrder.map(column => column.id);
this.cloudPreferenceService.updatePreference(
this.appName,
ProcessListCloudPreferences.columnOrder,
newColumnsOrder
);
}
}
onRowClick(item: DataRowEvent) { onRowClick(item: DataRowEvent) {
this.currentInstanceId = item.value.getValue('id'); this.currentInstanceId = item.value.getValue('id');
this.rowClick.emit(this.currentInstanceId); this.rowClick.emit(this.currentInstanceId);

View File

@ -0,0 +1,4 @@
// eslint-disable-next-line no-shadow
export enum ProcessListCloudPreferences {
columnOrder = 'processes-cloud-list-columns-order'
}

View File

@ -20,6 +20,7 @@ export * from './components/process-list-cloud.component';
export * from './models/process-cloud-query-request.model'; export * from './models/process-cloud-query-request.model';
export * from './models/process-cloud-preset.model'; export * from './models/process-cloud-preset.model';
export * from './models/process-list-sorting.model'; export * from './models/process-list-sorting.model';
export * from './models/process-cloud-preferences';
export * from './services/process-list-cloud.service'; export * from './services/process-list-cloud.service';
export * from './services/process-task-list-cloud.service'; export * from './services/process-task-list-cloud.service';

View File

@ -19,6 +19,10 @@ import { InjectionToken } from '@angular/core';
import { PreferenceCloudServiceInterface } from './preference-cloud.interface'; import { PreferenceCloudServiceInterface } from './preference-cloud.interface';
import { TaskListCloudServiceInterface } from './task-list-cloud.service.interface'; import { TaskListCloudServiceInterface } from './task-list-cloud.service.interface';
export const PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN = new InjectionToken<PreferenceCloudServiceInterface>('proccesses-list-preferences-cloud');
export const TASK_LIST_PREFERENCES_SERVICE_TOKEN = new InjectionToken<PreferenceCloudServiceInterface>('tasks-list-preferences-cloud');
export const PROCESS_FILTERS_SERVICE_TOKEN = new InjectionToken<PreferenceCloudServiceInterface>('proccess-filters-cloud'); export const PROCESS_FILTERS_SERVICE_TOKEN = new InjectionToken<PreferenceCloudServiceInterface>('proccess-filters-cloud');
export const TASK_FILTERS_SERVICE_TOKEN = new InjectionToken<PreferenceCloudServiceInterface>('task-filters-cloud'); export const TASK_FILTERS_SERVICE_TOKEN = new InjectionToken<PreferenceCloudServiceInterface>('task-filters-cloud');

View File

@ -18,11 +18,9 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
export interface PreferenceCloudServiceInterface { export interface PreferenceCloudServiceInterface {
getPreferences(appName: string, key?: string): Observable<any>; getPreferences(appName: string, key?: string): Observable<any>;
getPreferenceByKey(appName: string, key: string): Observable<any>; getPreferenceByKey(appName: string, key: string): Observable<any>;
createPreference(appName: string, key: string, newPreference: any): Observable<any>; createPreference(appName: string, key: string, newPreference: any): Observable<any>;
updatePreference(appName: string, key: string, updatedPreference: any): Observable<any>; updatePreference(appName: string, key: string, updatedPreference: any): Observable<any>;
deletePreference(appName: string, key: any): Observable<any>; deletePreference(appName: string, key: string): Observable<any>;
} }

View File

@ -19,7 +19,8 @@
(row-unselect)="onRowUnselect($any($event))" (row-unselect)="onRowUnselect($any($event))"
(rowClick)="onRowClick($any($event))" (rowClick)="onRowClick($any($event))"
(row-keyup)="onRowKeyUp($any($event))" (row-keyup)="onRowKeyUp($any($event))"
(sorting-changed)="onSortingChanged($any($event))"> (sorting-changed)="onSortingChanged($any($event))"
(columnOrderChanged)="onColumnOrderChanged($event)">
<adf-loading-content-template> <adf-loading-content-template>
<ng-template> <ng-template>
<!-- Add your custom loading template here --> <!-- Add your custom loading template here -->

View File

@ -26,8 +26,10 @@ import { taskPresetsCloudDefaultModel } from '../models/task-preset-cloud.model'
import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model';
import { BehaviorSubject, Subject } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs';
import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model'; import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model';
import { takeUntil } from 'rxjs/operators'; import { map, take, takeUntil } from 'rxjs/operators';
import { TaskCloudService } from '../../services/task-cloud.service'; import { TaskCloudService } from '../../services/task-cloud.service';
import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
import { TasksListCloudPreferences } from '../models/tasks-cloud-preferences';
@Directive() @Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix // eslint-disable-next-line @angular-eslint/directive-class-suffix
@ -120,7 +122,8 @@ export abstract class BaseTaskListCloudComponent extends DataTableSchema impleme
constructor(appConfigService: AppConfigService, constructor(appConfigService: AppConfigService,
private taskCloudService: TaskCloudService, private taskCloudService: TaskCloudService,
private userPreferences: UserPreferencesService, private userPreferences: UserPreferencesService,
presetKey: string) { presetKey: string,
private cloudPreferenceService: PreferenceCloudServiceInterface) {
super(appConfigService, presetKey, taskPresetsCloudDefaultModel); super(appConfigService, presetKey, taskPresetsCloudDefaultModel);
this.size = userPreferences.paginationSize; this.size = userPreferences.paginationSize;
@ -153,7 +156,18 @@ export abstract class BaseTaskListCloudComponent extends DataTableSchema impleme
} }
ngAfterContentInit() { ngAfterContentInit() {
this.createDatatableSchema(); this.cloudPreferenceService.getPreferences(this.appName).pipe(
take(1),
map((preferences => {
const preferencesList = preferences?.list?.entries ?? [];
const searchedPreferences = preferencesList.find(preference => preference.entry.key === TasksListCloudPreferences.columnOrder);
return searchedPreferences ? JSON.parse(searchedPreferences.entry.value) : null;
}))
).subscribe(columnsOrder => {
this.columnsOrder = columnsOrder;
this.createDatatableSchema();
}
);
} }
reload() { reload() {
@ -235,6 +249,18 @@ export abstract class BaseTaskListCloudComponent extends DataTableSchema impleme
this.executeRowAction.emit(row); this.executeRowAction.emit(row);
} }
onColumnOrderChanged(columnsWithNewOrder: DataColumn[]): void {
this.columnsOrder = columnsWithNewOrder.map(column => column.id);
if (this.appName) {
this.cloudPreferenceService.updatePreference(
this.appName,
TasksListCloudPreferences.columnOrder,
this.columnsOrder
);
}
}
setSorting(sortDetail) { setSorting(sortDetail) {
const sorting = sortDetail ? { const sorting = sortDetail ? {
orderBy: sortDetail.key, orderBy: sortDetail.key,

View File

@ -25,7 +25,7 @@ import { of } from 'rxjs';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model'; import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model';
import { skip } from 'rxjs/operators'; import { shareReplay, skip } from 'rxjs/operators';
import { ServiceTaskListCloudService } from '../services/service-task-list-cloud.service'; import { ServiceTaskListCloudService } from '../services/service-task-list-cloud.service';
@Component({ @Component({
@ -111,6 +111,8 @@ describe('ServiceTaskListCloudComponent', () => {
} }
} }
}); });
component.isColumnSchemaCreated$ = of(true).pipe(shareReplay(1));
}); });
afterEach(() => { afterEach(() => {
@ -384,6 +386,8 @@ describe('ServiceTaskListCloudComponent', () => {
componentCustom = fixtureCustom.componentInstance; componentCustom = fixtureCustom.componentInstance;
customCopyComponent = copyFixture.componentInstance; customCopyComponent = copyFixture.componentInstance;
element = copyFixture.debugElement.nativeElement; element = copyFixture.debugElement.nativeElement;
customCopyComponent.taskList.isColumnSchemaCreated$ = of(true);
}); });
afterEach(() => { afterEach(() => {

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, ViewEncapsulation, Input } from '@angular/core'; import { Component, ViewEncapsulation, Input, Inject } from '@angular/core';
import { import {
AppConfigService, UserPreferencesService AppConfigService, UserPreferencesService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
@ -23,6 +23,9 @@ import { ServiceTaskQueryCloudRequestModel } from '../models/service-task-cloud.
import { BaseTaskListCloudComponent } from './base-task-list-cloud.component'; import { BaseTaskListCloudComponent } from './base-task-list-cloud.component';
import { ServiceTaskListCloudService } from '../services/service-task-list-cloud.service'; import { ServiceTaskListCloudService } from '../services/service-task-list-cloud.service';
import { TaskCloudService } from '../../services/task-cloud.service'; import { TaskCloudService } from '../../services/task-cloud.service';
import { combineLatest } from 'rxjs';
import { PreferenceCloudServiceInterface, TASK_LIST_PREFERENCES_SERVICE_TOKEN } from '../../../services/public-api';
import { take } from 'rxjs/operators';
const PRESET_KEY = 'adf-cloud-service-task-list.presets'; const PRESET_KEY = 'adf-cloud-service-task-list.presets';
@ -39,14 +42,21 @@ export class ServiceTaskListCloudComponent extends BaseTaskListCloudComponent {
constructor(private serviceTaskListCloudService: ServiceTaskListCloudService, constructor(private serviceTaskListCloudService: ServiceTaskListCloudService,
appConfigService: AppConfigService, appConfigService: AppConfigService,
taskCloudService: TaskCloudService, taskCloudService: TaskCloudService,
userPreferences: UserPreferencesService) { userPreferences: UserPreferencesService,
super(appConfigService, taskCloudService, userPreferences, PRESET_KEY); @Inject(TASK_LIST_PREFERENCES_SERVICE_TOKEN) cloudPreferenceService: PreferenceCloudServiceInterface) {
super(appConfigService, taskCloudService, userPreferences, PRESET_KEY, cloudPreferenceService);
} }
load(requestNode: ServiceTaskQueryCloudRequestModel) { load(requestNode: ServiceTaskQueryCloudRequestModel) {
this.isLoading = true; this.isLoading = true;
this.serviceTaskListCloudService.getServiceTaskByRequest(requestNode).subscribe(
(tasks) => { combineLatest([
this.serviceTaskListCloudService.getServiceTaskByRequest(requestNode),
this.isColumnSchemaCreated$
]).pipe(
take(1)
).subscribe(
([tasks]) => {
this.rows = tasks.list.entries; this.rows = tasks.list.entries;
this.success.emit(tasks); this.success.emit(tasks);
this.isLoading = false; this.isLoading = false;

View File

@ -26,7 +26,7 @@ import { of } from 'rxjs';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model'; import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model';
import { skip } from 'rxjs/operators'; import { shareReplay, skip } from 'rxjs/operators';
import { TaskListCloudServiceInterface } from '../../../services/task-list-cloud.service.interface'; import { TaskListCloudServiceInterface } from '../../../services/task-list-cloud.service.interface';
import { TASK_LIST_CLOUD_TOKEN } from '../../../services/cloud-token.service'; import { TASK_LIST_CLOUD_TOKEN } from '../../../services/cloud-token.service';
@ -124,6 +124,8 @@ describe('TaskListCloudComponent', () => {
} }
} }
}); });
component.isColumnSchemaCreated$ = of(true).pipe(shareReplay(1));
}); });
afterEach(() => { afterEach(() => {
@ -419,6 +421,8 @@ describe('TaskListCloudComponent', () => {
componentCustom = fixtureCustom.componentInstance; componentCustom = fixtureCustom.componentInstance;
customCopyComponent = copyFixture.componentInstance; customCopyComponent = copyFixture.componentInstance;
element = copyFixture.debugElement.nativeElement; element = copyFixture.debugElement.nativeElement;
customCopyComponent.taskList.isColumnSchemaCreated$ = of(true);
}); });
afterEach(() => { afterEach(() => {
@ -542,7 +546,9 @@ describe('TaskListCloudComponent', () => {
element = fixture.debugElement.nativeElement; element = fixture.debugElement.nativeElement;
taskSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTask)); taskSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTask));
component.isColumnSchemaCreated$ = of(true);
}); });
afterEach(() => { afterEach(() => {
fixture.destroy(); fixture.destroy();
}); });

View File

@ -20,8 +20,11 @@ import { AppConfigService, UserPreferencesService } from '@alfresco/adf-core';
import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model';
import { BaseTaskListCloudComponent } from './base-task-list-cloud.component'; import { BaseTaskListCloudComponent } from './base-task-list-cloud.component';
import { TaskCloudService } from '../../services/task-cloud.service'; import { TaskCloudService } from '../../services/task-cloud.service';
import { TASK_LIST_CLOUD_TOKEN } from '../../../services/cloud-token.service'; import { TASK_LIST_CLOUD_TOKEN, TASK_LIST_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
import { TaskListCloudServiceInterface } from '../../../services/task-list-cloud.service.interface'; import { TaskListCloudServiceInterface } from '../../../services/task-list-cloud.service.interface';
import { combineLatest } from 'rxjs';
import { take } from 'rxjs/operators';
const PRESET_KEY = 'adf-cloud-task-list.presets'; const PRESET_KEY = 'adf-cloud-task-list.presets';
@ -135,14 +138,21 @@ export class TaskListCloudComponent extends BaseTaskListCloudComponent {
constructor(@Inject(TASK_LIST_CLOUD_TOKEN) public taskListCloudService: TaskListCloudServiceInterface, constructor(@Inject(TASK_LIST_CLOUD_TOKEN) public taskListCloudService: TaskListCloudServiceInterface,
appConfigService: AppConfigService, appConfigService: AppConfigService,
taskCloudService: TaskCloudService, taskCloudService: TaskCloudService,
userPreferences: UserPreferencesService) { userPreferences: UserPreferencesService,
super(appConfigService, taskCloudService, userPreferences, PRESET_KEY); @Inject(TASK_LIST_PREFERENCES_SERVICE_TOKEN) cloudPreferenceService: PreferenceCloudServiceInterface) {
super(appConfigService, taskCloudService, userPreferences, PRESET_KEY, cloudPreferenceService);
} }
load(requestNode: TaskQueryCloudRequestModel) { load(requestNode: TaskQueryCloudRequestModel) {
this.isLoading = true; this.isLoading = true;
this.taskListCloudService.getTaskByRequest(requestNode).subscribe(
(tasks) => { combineLatest([
this.taskListCloudService.getTaskByRequest(requestNode),
this.isColumnSchemaCreated$
]).pipe(
take(1)
).subscribe(
([tasks]) => {
this.rows = tasks.list.entries; this.rows = tasks.list.entries;
this.success.emit(tasks); this.success.emit(tasks);
this.isLoading = false; this.isLoading = false;

View File

@ -0,0 +1,4 @@
// eslint-disable-next-line no-shadow
export enum TasksListCloudPreferences {
columnOrder = 'tasks-list-cloud-columns-order'
}

View File

@ -20,6 +20,7 @@ export * from './components/service-task-list-cloud.component';
export * from './models/service-task-cloud.model'; export * from './models/service-task-cloud.model';
export * from './models/task-preset-cloud.model'; export * from './models/task-preset-cloud.model';
export * from './models/tasks-cloud-preferences';
export * from './services/task-list-cloud.service'; export * from './services/task-list-cloud.service';
export * from './services/service-task-list-cloud.service'; export * from './services/service-task-list-cloud.service';

View File

@ -22,7 +22,7 @@ import { TaskListCloudComponent } from './components/task-list-cloud.component';
import { ServiceTaskListCloudComponent } from './components/service-task-list-cloud.component'; import { ServiceTaskListCloudComponent } from './components/service-task-list-cloud.component';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
import { TASK_LIST_CLOUD_TOKEN } from '../../services/cloud-token.service'; import { TASK_LIST_CLOUD_TOKEN } from '../../services/cloud-token.service';
import { TaskListCloudService } from './public-api'; import { TaskListCloudService } from './services/task-list-cloud.service';
@NgModule({ @NgModule({
imports: [ imports: [