TSK-843 designed report table and made single row collapsable

This commit is contained in:
Mustapha Zorgati 2019-04-03 17:06:42 +02:00 committed by Holger Hagen
parent b75be882f1
commit d8b1829a53
10 changed files with 101 additions and 45 deletions

View File

@ -38,12 +38,10 @@ import pro.taskana.rest.RestConfiguration;
@SpringBootTest(classes = RestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT) @SpringBootTest(classes = RestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class MonitorControllerRestDocumentation { public class MonitorControllerRestDocumentation {
@LocalServerPort
int port;
@Rule @Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@LocalServerPort
int port;
@Autowired @Autowired
private WebApplicationContext context; private WebApplicationContext context;
@ -70,6 +68,8 @@ public class MonitorControllerRestDocumentation {
fieldWithPath("meta.name").description("Name of the report"), fieldWithPath("meta.name").description("Name of the report"),
fieldWithPath("meta.date").description("Date of the report creation"), fieldWithPath("meta.date").description("Date of the report creation"),
fieldWithPath("meta.header").description("Column-headers of the report"), fieldWithPath("meta.header").description("Column-headers of the report"),
fieldWithPath("meta.expHeader").description(
"Expandable Column-headers which match the depth of the foldable rows within the report."),
fieldWithPath("meta.rowDesc").description("Descriptions for the rows the report"), fieldWithPath("meta.rowDesc").description("Descriptions for the rows the report"),
fieldWithPath("meta.totalDesc").description("Description for the report itself"), fieldWithPath("meta.totalDesc").description("Description for the report itself"),
subsectionWithPath("rows").description("Object holding the rows of the report.\n" subsectionWithPath("rows").description("Object holding the rows of the report.\n"
@ -77,7 +77,7 @@ public class MonitorControllerRestDocumentation {
fieldWithPath("sumRow").description("Object holding the sums in the columns over all rows"), fieldWithPath("sumRow").description("Object holding the sums in the columns over all rows"),
subsectionWithPath("sumRow.cells") subsectionWithPath("sumRow.cells")
.description("Contains the accumulated numbers over all columns defined in meta.header.\n" .description("Contains the accumulated numbers over all columns defined in meta.header.\n"
+ "For the exact structure please check the example response above"), + "For the exact structure please check the example response above"),
fieldWithPath("sumRow.total").description("Total number of tasks"), fieldWithPath("sumRow.total").description("Total number of tasks"),
fieldWithPath("_links.self.href").ignored() fieldWithPath("_links.self.href").ignored()
}; };

View File

@ -8,4 +8,5 @@ export class ReportInfoDataIterable {
key: string; key: string;
val: ReportInfoData; val: ReportInfoData;
depth: number; depth: number;
display = false;
} }

View File

@ -14,12 +14,12 @@ import {MonitorComponent} from './monitor.component';
import {TasksComponent} from './tasks/tasks.component'; import {TasksComponent} from './tasks/tasks.component';
import {WorkbasketComponent} from './workbasket/workbasket.component'; import {WorkbasketComponent} from './workbasket/workbasket.component';
import {ClassificationTasksComponent} from './classification-tasks/classification-tasks.component'; import {ClassificationTasksComponent} from './classification-tasks/classification-tasks.component';
import {ReportRowComponent} from "./report/row/row.component"; import {ReportRowComponent} from './report/row/row.component';
import {TimestampComponent} from "./timestamp/timestamp.component"; import {TimestampComponent} from './timestamp/timestamp.component';
import {RestConnectorService} from './services/restConnector/rest-connector.service'; import {RestConnectorService} from './services/restConnector/rest-connector.service';
import {MapToIterable} from "../shared/pipes/mapToIterable/mapToIterable"; import {MapToIterable} from '../shared/pipes/mapToIterable/mapToIterable';
const MODULES = [ const MODULES = [
CommonModule, CommonModule,

View File

@ -1,26 +1,30 @@
<div *ngIf="reportData" class="report table table-body-striped"> <div *ngIf="reportData" class="report table table-body-striped">
<div class="table-header"> <div class="table-header">
<div class="table-row"> <div class="table-row">
<div class="table-cell table-cell--bold table-cell--justify">{{reportData.meta.rowDesc}}</div> <div
[ngClass]="{'table-cell--border-right': currentExpHeaders === 0}"
class="table-cell table-cell--bold table-cell--justify">{{reportData.meta.rowDesc}}</div>
<ng-container *ngFor="let header of reportData.meta.expHeader; let i = index"> <ng-container *ngFor="let header of reportData.meta.expHeader; let i = index">
<ng-container *ngIf="i < currentExpHeaders"> <ng-container *ngIf="i < currentExpHeaders">
<div class="table-cell table-cell--bold table-cell--justify">{{header}}</div> <div [ngClass]="{'table-cell--border-right': currentExpHeaders - 1 === i}"
class="table-cell table-cell--bold table-cell--justify">{{header}}</div>
</ng-container> </ng-container>
</ng-container> </ng-container>
<div *ngFor="let header of reportData.meta.header" class="table-cell table-cell--bold">{{header}}</div> <div *ngFor="let header of reportData.meta.header"
<div class="table-cell table-cell--bold">{{reportData.meta.totalDesc}}</div> class="table-cell table-cell--bold">{{header}}</div>
<div class="table-cell table-cell--bold table-cell--border-left">{{reportData.meta.totalDesc}}</div>
</div> </div>
</div> </div>
<monitor-report-row (expandedDepth)="expandHeader($event, i)" <taskana-report-row (expandedDepth)="expandHeader($event, i)"
*ngFor="let row of reportData.rows | mapToIterable | orderBy:['key']; let i = index" *ngFor="let row of reportData.rows | mapToIterable | orderBy:['key']; let i = index"
[headers]="reportData.meta.header" [headers]="reportData.meta.header"
[maxTableDepth]="currentExpHeaders" [maxTableDepth]="currentExpHeaders"
[row]="row" [row]="row"
class="table-body"></monitor-report-row> class="table-body"></taskana-report-row>
<monitor-report-row (expandedDepth)="expandHeader($event, expHeaders.length - 1)" <taskana-report-row (expandedDepth)="expandHeader($event, expHeaders.length - 1)"
[headers]="reportData.meta.header" [headers]="reportData.meta.header"
[maxTableDepth]="currentExpHeaders" [maxTableDepth]="currentExpHeaders"
[row]="_sumRow" [row]="_sumRow"
bold="true" bold="true"
class="table-footer"></monitor-report-row> class="table-footer"></taskana-report-row>
</div> </div>

View File

@ -1,6 +1,6 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {ReportData} from 'app/monitor/models/report-data'; import {ReportData} from 'app/monitor/models/report-data';
import {ReportInfoDataIterable} from "../models/report-info-data"; import {ReportInfoDataIterable} from '../models/report-info-data';
@Component({ @Component({
selector: 'taskana-report', selector: 'taskana-report',
@ -11,12 +11,9 @@ export class ReportComponent implements OnInit {
expHeaders: Array<number>; expHeaders: Array<number>;
currentExpHeaders: number = 0; currentExpHeaders = 0;
_sumRow: ReportInfoDataIterable; _sumRow: ReportInfoDataIterable;
constructor() {
}
private _reportData: ReportData; private _reportData: ReportData;
get reportData(): ReportData { get reportData(): ReportData {
@ -32,6 +29,9 @@ export class ReportComponent implements OnInit {
this._sumRow.key = reportData.meta.totalDesc; this._sumRow.key = reportData.meta.totalDesc;
} }
constructor() {
}
ngOnInit(): void { ngOnInit(): void {
} }

View File

@ -1,19 +1,28 @@
<ng-container *ngIf="headers && flatRows"> <ng-container *ngIf="headers && flatRows">
<ng-container *ngFor="let row of flatRows; let i = index"> <ng-container *ngFor="let row of flatRows; let i = index">
<div *ngIf="currentDepth >= row.depth" class="table-row"> <div *ngIf="row.display"
[ngClass]="{'table-row--highlight': row.depth === 0 && currentDepth > 0,
'table-row--hover': row.depth > 0 && currentDepth > 0,
'table-row--white': row.depth > 0 && currentDepth > 0}"
class="table-row">
<div *ngFor="let _ of range(row.depth)" class="table-cell"></div> <div *ngFor="let _ of range(row.depth)" class="table-cell"></div>
<div (click)="toggleFold(row.depth)" <div (click)="toggleFold(i)"
[ngClass]="{'table-cell--clickable': maxDepth > row.depth, 'table-cell--bold' : bold}" [ngClass]="{'table-cell--clickable': maxDepth > row.depth,
'table-cell--bold' : bold || row.depth === 0 && currentDepth > 0,
'table-cell--border-right': row.depth === maxTableDepth}"
class="table-cell table-cell--justify"> class="table-cell table-cell--justify">
{{row.key}} <span *ngIf="maxDepth > row.depth"
class="material-icons md-18">{{ canRowCollapse(i) ? "expand_more" : "expand_less"}}</span>{{row.key}}
</div> </div>
<div *ngFor="let _ of range(maxTableDepth - row.depth)" class="table-cell"></div> <div *ngFor="let _ of range(maxTableDepth - row.depth); let i = index"
<div *ngFor="let header of headers" [ngClass]="{'table-cell--bold' : bold}" class="table-cell"> [ngClass]="{'table-cell--border-right': i === maxTableDepth - row.depth - 1}" class="table-cell"></div>
<div *ngFor="let header of headers"
[ngClass]="{'table-cell--bold' : bold || row.depth === 0 && currentDepth > 0}" class="table-cell">
{{row.val.cells[header]}} {{row.val.cells[header]}}
</div> </div>
<div class="table-cell table-cell--bold"> <div class="table-cell table-cell--bold table-cell--border-left">
{{row.val.total}} {{row.val.total}}
</div> </div>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@ -1,4 +1,19 @@
@import './src/assets/_colors';
.table-cell--clickable { .table-cell--clickable {
cursor: pointer; cursor: pointer;
}
.table-row--highlight,
.table-row--hover:hover {
background-color: #f9f9f9;
}
.table-row--highlight > .table-cell {
border-bottom: 2px solid #ddd;
}
.table-row--white {
background-color: white;
} }

View File

@ -1,9 +1,9 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {ReportInfoDataIterable} from "../../models/report-info-data"; import {ReportInfoDataIterable} from '../../models/report-info-data';
import {MapToIterable} from "../../../shared/pipes/mapToIterable/mapToIterable"; import {MapToIterable} from '../../../shared/pipes/mapToIterable/mapToIterable';
@Component({ @Component({
selector: 'monitor-report-row', selector: 'taskana-report-row',
templateUrl: './row.component.html', templateUrl: './row.component.html',
styleUrls: ['./row.component.scss'] styleUrls: ['./row.component.scss']
}) })
@ -12,19 +12,15 @@ export class ReportRowComponent implements OnInit {
@Input() @Input()
headers: Array<string>; headers: Array<string>;
@Input() @Input()
bold: boolean = false; bold = false;
@Input() @Input()
maxTableDepth: number = 0; maxTableDepth = 0;
@Output() @Output()
expandedDepth: EventEmitter<number> = new EventEmitter<number>(); expandedDepth: EventEmitter<number> = new EventEmitter<number>();
currentDepth = 0;
maxDepth: number; maxDepth: number;
currentDepth: number = 0;
flatRows: Array<ReportInfoDataIterable>; flatRows: Array<ReportInfoDataIterable>;
constructor(private mapToIterable: MapToIterable) {
}
private _row: ReportInfoDataIterable; private _row: ReportInfoDataIterable;
get row(): ReportInfoDataIterable { get row(): ReportInfoDataIterable {
@ -38,25 +34,47 @@ export class ReportRowComponent implements OnInit {
this.maxDepth = this.flatten(row, 0); this.maxDepth = this.flatten(row, 0);
} }
constructor(private mapToIterable: MapToIterable) {
}
ngOnInit() { ngOnInit() {
} }
toggleFold(depth: number) { toggleFold(index: number): void {
this.currentDepth = depth == this.currentDepth && depth < this.maxDepth ? depth + 1 : depth; const toggleRow = this.flatRows[index++];
this.expandedDepth.emit(this.currentDepth); if (toggleRow.depth < this.maxDepth) {
const firstChildRow = this.flatRows[index++];
firstChildRow.display = !firstChildRow.display;
let end = false;
for (let i = index; i < this.flatRows.length && !end; i++) {
const row = this.flatRows[i];
end = row.depth <= toggleRow.depth;
if (!end) {
row.display = firstChildRow.display && row.depth === firstChildRow.depth;
}
}
this.currentDepth = Math.max(...this.flatRows.filter(r => r.display).map(r => r.depth));
this.expandedDepth.emit(this.currentDepth);
}
} }
range(depth: number): Array<null> { range(depth: number): Array<null> {
return new Array<null>(Math.max(depth, 0)); return new Array<null>(Math.max(depth, 0));
} }
canRowCollapse(index: number) {
return !this.flatRows[index + 1].display;
}
private flatten(row: ReportInfoDataIterable, depth: number): number { private flatten(row: ReportInfoDataIterable, depth: number): number {
row.depth = depth; row.depth = depth;
row.display = depth === 0;
this.flatRows.push(row); this.flatRows.push(row);
if (row.val.foldableRows) { if (row.val.foldableRows) {
depth = Math.max(...this.mapToIterable.transform(row.val.foldableRows) depth = Math.max(...this.mapToIterable.transform(row.val.foldableRows)
.sort((a, b) => a.key.localeCompare(b.key)) .sort((a, b) => a.key.localeCompare(b.key))
.map(r => this.flatten(r, depth + 1))) .map(r => this.flatten(r, depth + 1)));
} }
return depth; return depth;
} }

View File

@ -1,6 +1,6 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ReportData} from "../models/report-data"; import {ReportData} from '../models/report-data';
import {RestConnectorService} from "../services/restConnector/rest-connector.service"; import {RestConnectorService} from '../services/restConnector/rest-connector.service';
@Component({ @Component({
selector: 'taskana-monitor-timestamp', selector: 'taskana-monitor-timestamp',

View File

@ -72,3 +72,12 @@
min-width: 0; min-width: 0;
} }
.table-cell--border-right {
border-right: 2px solid #ddd;
}
.table-cell--border-left {
border-left: 2px solid #ddd;
}