Skip to content

Commit 692e86a

Browse files
authored
TPv2 Add Download Options Dialog to Generic Table (apache#7472)
* Add download options dialog to generic table * Better coverage * Fix lint * Fix rebase
1 parent b32cd0e commit 692e86a

7 files changed

+288
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!--
2+
~ Licensed under the Apache License, Version 2.0 (the "License");
3+
~ you may not use this file except in compliance with the License.
4+
~ You may obtain a copy of the License at
5+
~
6+
~ http://www.apache.org/licenses/LICENSE-2.0
7+
~
8+
~ Unless required by applicable law or agreed to in writing, software
9+
~ distributed under the License is distributed on an "AS IS" BASIS,
10+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
~ See the License for the specific language governing permissions and
12+
~ limitations under the License.
13+
-->
14+
15+
<h2 mat-dialog-title>Export Options</h2>
16+
<form method="dialog" ngNativeValidate (ngSubmit)="onSubmit()">
17+
<div class="content" mat-dialog-content>
18+
<mat-form-field appearance="fill">
19+
<mat-label>File Name (no extension)</mat-label>
20+
<input name="fileName" matInput type="text" [(ngModel)]="fileName" required />
21+
</mat-form-field>
22+
<mat-form-field appearance="fill">
23+
<mat-label>Delimiter</mat-label>
24+
<input name="delimiter" matInput type="text" [(ngModel)]="seperator" required />
25+
</mat-form-field>
26+
<mat-checkbox name="includeHeaders" [(ngModel)]="includeHeaders">Include Headers</mat-checkbox>
27+
<mat-checkbox name="includeHidden" *ngIf="this.visibleColumns.length !== this.columns.length" [(ngModel)]="includeHidden">Include Hidden Columns ({{this.visibleColumns.length}}/{{this.columns.length}} visible)</mat-checkbox>
28+
<mat-checkbox name="includeFiltered" *ngIf="visibleRows !== allRows" [(ngModel)]="includeFiltered">Include Filtered Rows ({{visibleRows}}/{{allRows}} visible)</mat-checkbox>
29+
<mat-checkbox name="onlySelected" *ngIf="selectedRows" [(ngModel)]="onlySelected">Only Selected Rows ({{selectedRows}}/{{allRows}} selected)</mat-checkbox>
30+
</div>
31+
<div mat-dialog-actions>
32+
<button mat-button type="submit">Confirm</button>
33+
<button mat-button type="button" [mat-dialog-close]="undefined">Cancel</button>
34+
</div>
35+
</form>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
16+
div.content {
17+
display: grid;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
import { HarnessLoader } from "@angular/cdk/testing";
16+
import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
17+
import { ComponentFixture, TestBed } from "@angular/core/testing";
18+
import { MatCheckboxHarness } from "@angular/material/checkbox/testing";
19+
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
20+
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
21+
22+
import { AppUIModule } from "src/app/app.ui.module";
23+
import {
24+
DownloadOptionsDialogComponent,
25+
DownloadOptionsDialogData
26+
} from "src/app/shared/generic-table/download-options/download-options-dialog.component";
27+
28+
let loader: HarnessLoader;
29+
describe("DownloadOptionsComponent", () => {
30+
let component: DownloadOptionsDialogComponent;
31+
let fixture: ComponentFixture<DownloadOptionsDialogComponent>;
32+
const data: DownloadOptionsDialogData = {
33+
allRows: 5,
34+
columns: [{
35+
hide: true
36+
}, {
37+
hide: false
38+
}],
39+
name: "test",
40+
selectedRows: undefined,
41+
visibleRows: 5
42+
};
43+
const spyRef = jasmine.createSpyObj("MatDialogRef", ["close"]);
44+
45+
beforeEach(async () => {
46+
await TestBed.configureTestingModule({
47+
declarations: [ DownloadOptionsDialogComponent ],
48+
imports: [
49+
AppUIModule,
50+
NoopAnimationsModule
51+
],
52+
providers: [
53+
{provide: MatDialogRef, useValue: spyRef},
54+
{provide: MAT_DIALOG_DATA, useValue: data}
55+
]
56+
}).compileComponents();
57+
58+
fixture = TestBed.createComponent(DownloadOptionsDialogComponent);
59+
component = fixture.componentInstance;
60+
loader = TestbedHarnessEnvironment.loader(fixture);
61+
fixture.detectChanges();
62+
});
63+
64+
it("should create", () => {
65+
expect(component).toBeTruthy();
66+
});
67+
68+
it("defaults set", async () => {
69+
expect(fixture.componentInstance.allRows).toEqual(data.allRows);
70+
expect(fixture.componentInstance.columns).toEqual(data.columns);
71+
expect(fixture.componentInstance.fileName).toEqual(data.name);
72+
expect(fixture.componentInstance.selectedRows).toEqual(data.selectedRows);
73+
expect(fixture.componentInstance.visibleRows).toEqual(data.visibleRows);
74+
75+
expect(fixture.componentInstance.visibleColumns).toEqual(data.columns.filter(c => !c.hide));
76+
expect(fixture.componentInstance.columns).toEqual(data.columns);
77+
});
78+
79+
it("default submission", async () => {
80+
const cbs = await loader.getAllHarnesses(MatCheckboxHarness);
81+
expect(cbs.length).toBe(2);
82+
83+
fixture.componentInstance.onSubmit();
84+
expect(spyRef.close.calls.count()).toBe(1);
85+
});
86+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
import { Component, Inject } from "@angular/core";
16+
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
17+
import { ColDef, CsvExportParams } from "ag-grid-community";
18+
19+
/**
20+
* Data passed to DownloadOptionsComponent from the grid
21+
*/
22+
export interface DownloadOptionsDialogData {
23+
name: string;
24+
25+
columns: ColDef<unknown>[];
26+
27+
/**
28+
* Number of rows selected, should be undefined when only a single.
29+
*/
30+
selectedRows: number | undefined;
31+
32+
visibleRows: number;
33+
34+
allRows: number;
35+
}
36+
37+
/**
38+
* Controller for the DownloadOptions component.
39+
*/
40+
@Component({
41+
selector: "tp-download-options",
42+
styleUrls: ["./download-options-dialog.component.scss"],
43+
templateUrl: "./download-options-dialog.component.html"
44+
})
45+
export class DownloadOptionsDialogComponent {
46+
public fileName: string;
47+
48+
public visibleColumns: Array<ColDef<unknown>>;
49+
50+
public columns: Array<ColDef<unknown>>;
51+
52+
public includeHidden = false;
53+
public includeHeaders = true;
54+
55+
public includeFiltered = false;
56+
57+
public onlySelected = false;
58+
59+
/**
60+
* Number of selected rows, undefined if single selection.
61+
*/
62+
public selectedRows: number | undefined;
63+
public allRows: number;
64+
public visibleRows: number;
65+
66+
/** 'C'SV delimiter */
67+
public seperator = ",";
68+
69+
constructor(private readonly dialogRef: MatDialogRef<DownloadOptionsDialogComponent,
70+
CsvExportParams>, @Inject(MAT_DIALOG_DATA) data: DownloadOptionsDialogData) {
71+
this.fileName = data.name;
72+
this.selectedRows = data.selectedRows;
73+
this.allRows = data.allRows;
74+
this.visibleRows = data.visibleRows;
75+
this.visibleColumns = [];
76+
this.columns = [];
77+
for(const col of data.columns) {
78+
if(!col.hide) {
79+
this.visibleColumns.push(col);
80+
}
81+
this.columns.push(col);
82+
}
83+
}
84+
85+
/**
86+
* Called when submitting the form, converts data into export params.
87+
*/
88+
public onSubmit(): void {
89+
const params: CsvExportParams = {
90+
allColumns: this.includeHidden,
91+
columnSeparator: this.seperator,
92+
exportedRows: this.includeFiltered ? "all" : "filteredAndSorted",
93+
fileName: `${this.fileName}.csv`,
94+
onlySelected: this.onlySelected,
95+
skipColumnHeaders: !this.includeHeaders,
96+
};
97+
this.dialogRef.close(params);
98+
}
99+
100+
}

experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.spec.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@
1313
*/
1414

1515
import { type ComponentFixture, TestBed } from "@angular/core/testing";
16+
import { MatDialog, MatDialogModule } from "@angular/material/dialog";
1617
import { MatMenuModule } from "@angular/material/menu";
1718
import { Params } from "@angular/router";
1819
import { RouterTestingModule } from "@angular/router/testing";
1920
import { AgGridModule } from "ag-grid-angular";
20-
import type { CellContextMenuEvent, ColDef, GridApi, RowNode, ValueGetterParams } from "ag-grid-community";
21-
import { BehaviorSubject } from "rxjs";
21+
import type {
22+
CellContextMenuEvent,
23+
ColDef,
24+
GridApi,
25+
RowNode,
26+
ValueGetterParams
27+
} from "ag-grid-community";
28+
import { BehaviorSubject, of } from "rxjs";
2229

2330
import { type ContextMenuAction, GenericTableComponent, getColType, ContextMenuItem } from "./generic-table.component";
2431

@@ -136,18 +143,22 @@ describe("GenericTableComponent", () => {
136143
let component: GenericTableComponent<unknown>;
137144
let fixture: ComponentFixture<GenericTableComponent<unknown>>;
138145
let fuzzySearch: BehaviorSubject<string>;
146+
const dialogSpy = jasmine.createSpyObj("MatDialog", ["open", "afterClosed"]);
139147

140148
beforeEach(async () => {
141149
fuzzySearch = new BehaviorSubject("");
142150
await TestBed.configureTestingModule({
143151
declarations: [
144152
GenericTableComponent,
145-
146153
],
147154
imports: [
148155
AgGridModule,
149156
RouterTestingModule,
150-
MatMenuModule
157+
MatMenuModule,
158+
MatDialogModule
159+
],
160+
providers: [
161+
{ provide: MatDialog, useValue: dialogSpy }
151162
]
152163
}).compileComponents();
153164

@@ -330,12 +341,11 @@ describe("GenericTableComponent", () => {
330341
it("triggers a download of CSV data properly", async () => {
331342
component.selected = {};
332343
await fixture.whenStable();
333-
const spy = spyOn(component.gridOptions.api as GridApi, "exportDataAsCsv");
334-
component.download();
335-
expect(spy).toHaveBeenCalledWith({onlySelected: false});
336-
component.context = "test-context";
344+
dialogSpy.open.and.returnValue({afterClosed: () => of({fileName: "test.csv"})});
345+
const exportSpy = spyOn(component.gridOptions.api as GridApi, "exportDataAsCsv");
337346
component.download();
338-
expect(spy).toHaveBeenCalledWith({fileName: "test-context.csv", onlySelected: false});
347+
expect(dialogSpy.open.calls.count()).toBe(1);
348+
expect(exportSpy).toHaveBeenCalledWith({fileName: "test.csv"});
339349
});
340350

341351
it("checks if a menu action is disabled", async () => {

experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts

+27-11
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ import {
2323
Output,
2424
ViewChild
2525
} from "@angular/core";
26+
import { MatDialog } from "@angular/material/dialog";
2627
import { ActivatedRoute, type ParamMap, type Params, Router } from "@angular/router";
2728
import type {
2829
CellContextMenuEvent,
2930
ColDef,
3031
ColGroupDef,
3132
Column,
3233
ColumnApi,
33-
CsvExportParams,
3434
DateFilterModel,
3535
FilterChangedEvent,
3636
GridApi,
@@ -43,6 +43,7 @@ import type {
4343
} from "ag-grid-community";
4444
import type { BehaviorSubject, Subscription } from "rxjs";
4545

46+
import { DownloadOptionsDialogComponent } from "src/app/shared/generic-table/download-options/download-options-dialog.component";
4647
import { fuzzyScore } from "src/app/utils";
4748

4849
import { LoggingService } from "../logging.service";
@@ -406,7 +407,10 @@ export class GenericTableComponent<T> implements OnInit, OnDestroy {
406407
return (this.columnAPI.getColumns() ?? []).reverse();
407408
}
408409

409-
constructor(private readonly router: Router, private readonly route: ActivatedRoute, private readonly log: LoggingService) {
410+
constructor(private readonly router: Router,
411+
private readonly route: ActivatedRoute,
412+
private readonly dialog: MatDialog,
413+
private readonly log: LoggingService) {
410414
this.gridOptions = {
411415
defaultColDef: {
412416
filter: true,
@@ -829,15 +833,27 @@ export class GenericTableComponent<T> implements OnInit, OnDestroy {
829833
* Downloads the table data as a CSV file.
830834
*/
831835
public download(): void {
832-
const params: CsvExportParams = {
833-
onlySelected: this.gridAPI.getSelectedNodes().length > 0,
834-
};
835-
836-
if (this.context) {
837-
params.fileName = `${this.context}.csv`;
838-
}
839-
840-
this.gridAPI.exportDataAsCsv(params);
836+
const nodes = this.gridAPI.getSelectedNodes();
837+
const model = this.gridAPI.getModel();
838+
let visible = 0;
839+
let all = 0;
840+
model.forEachNode(rowNode => {
841+
if(rowNode.displayed) {
842+
visible++;
843+
}
844+
all++;
845+
});
846+
this.dialog.open(DownloadOptionsDialogComponent, {
847+
data: {
848+
allRows: all,
849+
columns: this.gridAPI.getColumnDefs() ?? [],
850+
name: this.context,
851+
selectedRows: nodes.length > 0 ? nodes.length : undefined,
852+
visibleRows: visible
853+
}
854+
}).afterClosed().subscribe(value => {
855+
this.gridAPI.exportDataAsCsv(value);
856+
});
841857
}
842858

843859
/**

experimental/traffic-portal/src/app/shared/shared.module.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { NgModule } from "@angular/core";
1717
import { RouterModule } from "@angular/router";
1818

1919
import { AppUIModule } from "src/app/app.ui.module";
20+
import { DownloadOptionsDialogComponent } from "src/app/shared/generic-table/download-options/download-options-dialog.component";
2021
import { TpHeaderComponent } from "src/app/shared/navigation/tp-header/tp-header.component";
2122
import { TpSidebarComponent } from "src/app/shared/navigation/tp-sidebar/tp-sidebar.component";
2223

@@ -64,7 +65,8 @@ import { CustomvalidityDirective } from "./validation/customvalidity.directive";
6465
TextDialogComponent,
6566
DecisionDialogComponent,
6667
CollectionChoiceDialogComponent,
67-
ImportJsonTxtComponent
68+
ImportJsonTxtComponent,
69+
DownloadOptionsDialogComponent
6870
],
6971
exports: [
7072
AlertComponent,

0 commit comments

Comments
 (0)