Skip to content

Commit

Permalink
TPv2 Add Download Options Dialog to Generic Table (#7472)
Browse files Browse the repository at this point in the history
* Add download options dialog to generic table

* Better coverage

* Fix lint

* Fix rebase
  • Loading branch information
shamrickus authored Aug 26, 2024
1 parent b32cd0e commit 692e86a
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<h2 mat-dialog-title>Export Options</h2>
<form method="dialog" ngNativeValidate (ngSubmit)="onSubmit()">
<div class="content" mat-dialog-content>
<mat-form-field appearance="fill">
<mat-label>File Name (no extension)</mat-label>
<input name="fileName" matInput type="text" [(ngModel)]="fileName" required />
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Delimiter</mat-label>
<input name="delimiter" matInput type="text" [(ngModel)]="seperator" required />
</mat-form-field>
<mat-checkbox name="includeHeaders" [(ngModel)]="includeHeaders">Include Headers</mat-checkbox>
<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>
<mat-checkbox name="includeFiltered" *ngIf="visibleRows !== allRows" [(ngModel)]="includeFiltered">Include Filtered Rows ({{visibleRows}}/{{allRows}} visible)</mat-checkbox>
<mat-checkbox name="onlySelected" *ngIf="selectedRows" [(ngModel)]="onlySelected">Only Selected Rows ({{selectedRows}}/{{allRows}} selected)</mat-checkbox>
</div>
<div mat-dialog-actions>
<button mat-button type="submit">Confirm</button>
<button mat-button type="button" [mat-dialog-close]="undefined">Cancel</button>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


div.content {
display: grid;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { HarnessLoader } from "@angular/cdk/testing";
import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { MatCheckboxHarness } from "@angular/material/checkbox/testing";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";

import { AppUIModule } from "src/app/app.ui.module";
import {
DownloadOptionsDialogComponent,
DownloadOptionsDialogData
} from "src/app/shared/generic-table/download-options/download-options-dialog.component";

let loader: HarnessLoader;
describe("DownloadOptionsComponent", () => {
let component: DownloadOptionsDialogComponent;
let fixture: ComponentFixture<DownloadOptionsDialogComponent>;
const data: DownloadOptionsDialogData = {
allRows: 5,
columns: [{
hide: true
}, {
hide: false
}],
name: "test",
selectedRows: undefined,
visibleRows: 5
};
const spyRef = jasmine.createSpyObj("MatDialogRef", ["close"]);

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DownloadOptionsDialogComponent ],
imports: [
AppUIModule,
NoopAnimationsModule
],
providers: [
{provide: MatDialogRef, useValue: spyRef},
{provide: MAT_DIALOG_DATA, useValue: data}
]
}).compileComponents();

fixture = TestBed.createComponent(DownloadOptionsDialogComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
fixture.detectChanges();
});

it("should create", () => {
expect(component).toBeTruthy();
});

it("defaults set", async () => {
expect(fixture.componentInstance.allRows).toEqual(data.allRows);
expect(fixture.componentInstance.columns).toEqual(data.columns);
expect(fixture.componentInstance.fileName).toEqual(data.name);
expect(fixture.componentInstance.selectedRows).toEqual(data.selectedRows);
expect(fixture.componentInstance.visibleRows).toEqual(data.visibleRows);

expect(fixture.componentInstance.visibleColumns).toEqual(data.columns.filter(c => !c.hide));
expect(fixture.componentInstance.columns).toEqual(data.columns);
});

it("default submission", async () => {
const cbs = await loader.getAllHarnesses(MatCheckboxHarness);
expect(cbs.length).toBe(2);

fixture.componentInstance.onSubmit();
expect(spyRef.close.calls.count()).toBe(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Component, Inject } from "@angular/core";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { ColDef, CsvExportParams } from "ag-grid-community";

/**
* Data passed to DownloadOptionsComponent from the grid
*/
export interface DownloadOptionsDialogData {
name: string;

columns: ColDef<unknown>[];

/**
* Number of rows selected, should be undefined when only a single.
*/
selectedRows: number | undefined;

visibleRows: number;

allRows: number;
}

/**
* Controller for the DownloadOptions component.
*/
@Component({
selector: "tp-download-options",
styleUrls: ["./download-options-dialog.component.scss"],
templateUrl: "./download-options-dialog.component.html"
})
export class DownloadOptionsDialogComponent {
public fileName: string;

public visibleColumns: Array<ColDef<unknown>>;

public columns: Array<ColDef<unknown>>;

public includeHidden = false;
public includeHeaders = true;

public includeFiltered = false;

public onlySelected = false;

/**
* Number of selected rows, undefined if single selection.
*/
public selectedRows: number | undefined;
public allRows: number;
public visibleRows: number;

/** 'C'SV delimiter */
public seperator = ",";

constructor(private readonly dialogRef: MatDialogRef<DownloadOptionsDialogComponent,
CsvExportParams>, @Inject(MAT_DIALOG_DATA) data: DownloadOptionsDialogData) {
this.fileName = data.name;
this.selectedRows = data.selectedRows;
this.allRows = data.allRows;
this.visibleRows = data.visibleRows;
this.visibleColumns = [];
this.columns = [];
for(const col of data.columns) {
if(!col.hide) {
this.visibleColumns.push(col);
}
this.columns.push(col);
}
}

/**
* Called when submitting the form, converts data into export params.
*/
public onSubmit(): void {
const params: CsvExportParams = {
allColumns: this.includeHidden,
columnSeparator: this.seperator,
exportedRows: this.includeFiltered ? "all" : "filteredAndSorted",
fileName: `${this.fileName}.csv`,
onlySelected: this.onlySelected,
skipColumnHeaders: !this.includeHeaders,
};
this.dialogRef.close(params);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@
*/

import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { MatDialog, MatDialogModule } from "@angular/material/dialog";
import { MatMenuModule } from "@angular/material/menu";
import { Params } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { AgGridModule } from "ag-grid-angular";
import type { CellContextMenuEvent, ColDef, GridApi, RowNode, ValueGetterParams } from "ag-grid-community";
import { BehaviorSubject } from "rxjs";
import type {
CellContextMenuEvent,
ColDef,
GridApi,
RowNode,
ValueGetterParams
} from "ag-grid-community";
import { BehaviorSubject, of } from "rxjs";

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

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

beforeEach(async () => {
fuzzySearch = new BehaviorSubject("");
await TestBed.configureTestingModule({
declarations: [
GenericTableComponent,

],
imports: [
AgGridModule,
RouterTestingModule,
MatMenuModule
MatMenuModule,
MatDialogModule
],
providers: [
{ provide: MatDialog, useValue: dialogSpy }
]
}).compileComponents();

Expand Down Expand Up @@ -330,12 +341,11 @@ describe("GenericTableComponent", () => {
it("triggers a download of CSV data properly", async () => {
component.selected = {};
await fixture.whenStable();
const spy = spyOn(component.gridOptions.api as GridApi, "exportDataAsCsv");
component.download();
expect(spy).toHaveBeenCalledWith({onlySelected: false});
component.context = "test-context";
dialogSpy.open.and.returnValue({afterClosed: () => of({fileName: "test.csv"})});
const exportSpy = spyOn(component.gridOptions.api as GridApi, "exportDataAsCsv");
component.download();
expect(spy).toHaveBeenCalledWith({fileName: "test-context.csv", onlySelected: false});
expect(dialogSpy.open.calls.count()).toBe(1);
expect(exportSpy).toHaveBeenCalledWith({fileName: "test.csv"});
});

it("checks if a menu action is disabled", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import {
Output,
ViewChild
} from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { ActivatedRoute, type ParamMap, type Params, Router } from "@angular/router";
import type {
CellContextMenuEvent,
ColDef,
ColGroupDef,
Column,
ColumnApi,
CsvExportParams,
DateFilterModel,
FilterChangedEvent,
GridApi,
Expand All @@ -43,6 +43,7 @@ import type {
} from "ag-grid-community";
import type { BehaviorSubject, Subscription } from "rxjs";

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

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

constructor(private readonly router: Router, private readonly route: ActivatedRoute, private readonly log: LoggingService) {
constructor(private readonly router: Router,
private readonly route: ActivatedRoute,
private readonly dialog: MatDialog,
private readonly log: LoggingService) {
this.gridOptions = {
defaultColDef: {
filter: true,
Expand Down Expand Up @@ -829,15 +833,27 @@ export class GenericTableComponent<T> implements OnInit, OnDestroy {
* Downloads the table data as a CSV file.
*/
public download(): void {
const params: CsvExportParams = {
onlySelected: this.gridAPI.getSelectedNodes().length > 0,
};

if (this.context) {
params.fileName = `${this.context}.csv`;
}

this.gridAPI.exportDataAsCsv(params);
const nodes = this.gridAPI.getSelectedNodes();
const model = this.gridAPI.getModel();
let visible = 0;
let all = 0;
model.forEachNode(rowNode => {
if(rowNode.displayed) {
visible++;
}
all++;
});
this.dialog.open(DownloadOptionsDialogComponent, {
data: {
allRows: all,
columns: this.gridAPI.getColumnDefs() ?? [],
name: this.context,
selectedRows: nodes.length > 0 ? nodes.length : undefined,
visibleRows: visible
}
}).afterClosed().subscribe(value => {
this.gridAPI.exportDataAsCsv(value);
});
}

/**
Expand Down
4 changes: 3 additions & 1 deletion experimental/traffic-portal/src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";

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

Expand Down Expand Up @@ -64,7 +65,8 @@ import { CustomvalidityDirective } from "./validation/customvalidity.directive";
TextDialogComponent,
DecisionDialogComponent,
CollectionChoiceDialogComponent,
ImportJsonTxtComponent
ImportJsonTxtComponent,
DownloadOptionsDialogComponent
],
exports: [
AlertComponent,
Expand Down

0 comments on commit 692e86a

Please sign in to comment.