initial import for Open Source 🎉
This commit is contained in:
parent
1898c361f3
commit
9c0dd3b722
2048 changed files with 218743 additions and 0 deletions
45
static/js/directives/ui/cor-table/cor-table-col.component.ts
Normal file
45
static/js/directives/ui/cor-table/cor-table-col.component.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { Input, Component, OnInit, Inject, Host } from 'ng-metadata/core';
|
||||
import { CorTableComponent } from './cor-table.component';
|
||||
|
||||
|
||||
/**
|
||||
* Defines a column (optionally sortable) in the table.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-table-col',
|
||||
template: '',
|
||||
})
|
||||
export class CorTableColumn implements OnInit {
|
||||
|
||||
@Input('@') public title: string;
|
||||
@Input('@') public templateurl: string;
|
||||
@Input('@') public datafield: string;
|
||||
@Input('@') public sortfield: string;
|
||||
@Input('@') public selected: string;
|
||||
@Input('=') public bindModel: any;
|
||||
@Input('@') public style: string;
|
||||
@Input('@') public class: string;
|
||||
@Input('@') public kindof: string;
|
||||
@Input('<') public itemLimit: number = 5;
|
||||
|
||||
constructor(@Host() @Inject(CorTableComponent) private parent: CorTableComponent,
|
||||
@Inject('TableService') private tableService: any) {
|
||||
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.parent.addColumn(this);
|
||||
}
|
||||
|
||||
public isNumeric(): boolean {
|
||||
return this.kindof == 'datetime';
|
||||
}
|
||||
|
||||
public processColumnForOrdered(value: any): any {
|
||||
if (this.kindof == 'datetime' && value) {
|
||||
return this.tableService.getReversedTimestamp(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.cor-table-element .co-top-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: baseline;
|
||||
}
|
67
static/js/directives/ui/cor-table/cor-table.component.html
Normal file
67
static/js/directives/ui/cor-table/cor-table.component.html
Normal file
|
@ -0,0 +1,67 @@
|
|||
<div class="cor-table-element">
|
||||
<span ng-transclude></span>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="co-top-bar" ng-if="!$ctrl.compact">
|
||||
<span class="co-filter-box with-options" ng-if="$ctrl.tableData.length && $ctrl.filterFields.length">
|
||||
<span class="page-controls"
|
||||
total-count="$ctrl.orderedData.entries.length"
|
||||
current-page="$ctrl.options.page"
|
||||
page-size="$ctrl.maxDisplayCount"></span>
|
||||
<span class="filter-message" ng-if="$ctrl.options.filter">
|
||||
Showing {{ $ctrl.orderedData.entries.length }} of {{ $ctrl.tableData.length }} {{ ::$ctrl.tableItemTitle }}
|
||||
</span>
|
||||
<input class="form-control" type="text"
|
||||
placeholder="Filter {{ ::$ctrl.tableItemTitle }}..."
|
||||
ng-model="$ctrl.options.filter"
|
||||
ng-change="$ctrl.refreshOrder()">
|
||||
</span>
|
||||
|
||||
<!-- Compact/expand rows toggle -->
|
||||
<div ng-if="!$ctrl.compact && $ctrl.canExpand" class="tab-header-controls">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn" ng-class="!$ctrl.expandRows ? 'btn-primary active' : 'btn-default'"
|
||||
ng-click="$ctrl.setExpanded(false)">
|
||||
Compact
|
||||
</button>
|
||||
<button class="btn" ng-class="$ctrl.expandRows ? 'btn-info active' : 'btn-default'"
|
||||
ng-click="$ctrl.setExpanded(true)">
|
||||
Expanded
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div class="empty" ng-if="!$ctrl.tableData.length && $ctrl.compact != 'true'">
|
||||
<div class="empty-primary-msg">No {{ ::$ctrl.tableItemTitle }} found.</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<table class="co-table co-fixed-table" ng-show="$ctrl.tableData.length">
|
||||
<thead>
|
||||
<td ng-repeat="col in $ctrl.columns"
|
||||
ng-class="$ctrl.tablePredicateClass(col)" style="{{ ::col.style }}"
|
||||
class="{{ ::col.class }}">
|
||||
<a ng-click="$ctrl.setOrder(col)">{{ ::col.title }}</a>
|
||||
</td>
|
||||
</thead>
|
||||
<tbody ng-repeat="item in $ctrl.orderedData.visibleEntries" ng-init="rowIndex = $index"
|
||||
ng-if="($index >= $ctrl.options.page * $ctrl.maxDisplayCount &&
|
||||
$index < ($ctrl.options.page + 1) * $ctrl.maxDisplayCount)">
|
||||
<tr>
|
||||
<td ng-repeat="col in $ctrl.columns"
|
||||
style="{{ ::col.style }}" class="{{ ::col.class }}">
|
||||
<div ng-if="col.templateurl" ng-include="col.templateurl"></div>
|
||||
<div ng-if="!col.templateurl">{{ item[col.datafield] }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="empty" ng-if="!$ctrl.orderedData.entries.length && $ctrl.tableData.length"
|
||||
style="margin-top: 20px;">
|
||||
<div class="empty-primary-msg">No matching {{ ::$ctrl.tableItemTitle }} found.</div>
|
||||
<div class="empty-secondary-msg">Try adjusting your filter above.</div>
|
||||
</div>
|
||||
</div>
|
126
static/js/directives/ui/cor-table/cor-table.component.spec.ts
Normal file
126
static/js/directives/ui/cor-table/cor-table.component.spec.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { Mock } from 'ts-mocks';
|
||||
import { CorTableComponent, CorTableOptions } from './cor-table.component';
|
||||
import { CorTableColumn } from './cor-table-col.component';
|
||||
import { SimpleChanges } from 'ng-metadata/core';
|
||||
import { ViewArray } from '../../../services/view-array/view-array';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTableComponent", () => {
|
||||
var component: CorTableComponent;
|
||||
var tableServiceMock: Mock<any>;
|
||||
var tableData: any[];
|
||||
var columnMocks: Mock<CorTableColumn>[];
|
||||
var orderedDataMock: Mock<ViewArray>;
|
||||
|
||||
beforeEach(() => {
|
||||
orderedDataMock = new Mock<ViewArray>();
|
||||
orderedDataMock.setup(mock => mock.visibleEntries).is([]);
|
||||
tableServiceMock = new Mock<any>();
|
||||
tableServiceMock.setup(mock => mock.buildOrderedItems)
|
||||
.is((items, options, filterFields, numericFields, extrafilter?) => orderedDataMock.Object);
|
||||
|
||||
tableData = [
|
||||
{name: "apple", last_modified: 1496068383000, version: "1.0.0"},
|
||||
{name: "pear", last_modified: 1496068383001, version: "1.1.0"},
|
||||
{name: "orange", last_modified: 1496068383002, version: "1.0.0"},
|
||||
{name: "banana", last_modified: 1496068383000, version: "2.0.0"},
|
||||
];
|
||||
|
||||
columnMocks = Object.keys(tableData[0])
|
||||
.map((key, index) => {
|
||||
const col = new Mock<CorTableColumn>();
|
||||
col.setup(mock => mock.isNumeric).is(() => index == 1 ? true : false);
|
||||
col.setup(mock => mock.processColumnForOrdered).is((value) => "dummy");
|
||||
col.setup(mock => mock.datafield).is(key);
|
||||
|
||||
return col;
|
||||
});
|
||||
|
||||
component = new CorTableComponent(tableServiceMock.Object);
|
||||
component.tableData = tableData;
|
||||
component.filterFields = ['name', 'version'];
|
||||
component.compact = false;
|
||||
component.tableItemTitle = "fruits";
|
||||
component.maxDisplayCount = 10;
|
||||
// Add columns
|
||||
columnMocks.forEach(colMock => component.addColumn(colMock.Object));
|
||||
(<Spy>tableServiceMock.Object.buildOrderedItems).calls.reset();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
|
||||
it("sets table options", () => {
|
||||
expect(component.options.filter).toEqual('');
|
||||
expect(component.options.reverse).toBe(false);
|
||||
expect(component.options.predicate).toEqual('');
|
||||
expect(component.options.page).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
var changes: SimpleChanges;
|
||||
|
||||
it("calls table service to build ordered items if table data is changed", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes processed table data to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.tableData = changes['tableData'].currentValue;
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[0]).not.toEqual(tableData);
|
||||
});
|
||||
|
||||
it("passes options to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[1]).toEqual(component.options);
|
||||
});
|
||||
|
||||
it("passes filter fields to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[2]).toEqual(component.filterFields);
|
||||
});
|
||||
|
||||
it("passes numeric fields to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
const expectedNumericCols: string[] = columnMocks.filter(colMock => colMock.Object.isNumeric())
|
||||
.map(colMock => colMock.Object.datafield);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[3]).toEqual(expectedNumericCols);
|
||||
});
|
||||
|
||||
it("resets to first page if table data is changed", () => {
|
||||
component.options.page = 1;
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect(component.options.page).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addColumn", () => {
|
||||
var columnMock: Mock<CorTableColumn>;
|
||||
|
||||
beforeEach(() => {
|
||||
columnMock = new Mock<CorTableColumn>();
|
||||
columnMock.setup(mock => mock.isNumeric).is(() => false);
|
||||
});
|
||||
|
||||
it("calls table service to build ordered items with new column", () => {
|
||||
component.addColumn(columnMock.Object);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
110
static/js/directives/ui/cor-table/cor-table.component.ts
Normal file
110
static/js/directives/ui/cor-table/cor-table.component.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { Input, Component, OnChanges, SimpleChanges, Inject } from 'ng-metadata/core';
|
||||
import { CorTableColumn } from './cor-table-col.component';
|
||||
import { ViewArray } from '../../../services/view-array/view-array';
|
||||
import './cor-table.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* A component that displays a table of information, with optional filtering and automatic sorting.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-table',
|
||||
templateUrl: '/static/js/directives/ui/cor-table/cor-table.component.html',
|
||||
legacy: {
|
||||
transclude: true
|
||||
}
|
||||
})
|
||||
export class CorTableComponent implements OnChanges {
|
||||
|
||||
@Input('<') public tableData: any[] = [];
|
||||
@Input('@') public tableItemTitle: string;
|
||||
@Input('<') public filterFields: string[];
|
||||
@Input('<') public compact: boolean = false;
|
||||
@Input('<') public maxDisplayCount: number = 10;
|
||||
@Input('<') public canExpand: boolean = false;
|
||||
@Input('<') public expandRows: boolean = false;
|
||||
|
||||
public orderedData: ViewArray;
|
||||
public options: CorTableOptions = {
|
||||
filter: '',
|
||||
reverse: false,
|
||||
predicate: '',
|
||||
page: 0,
|
||||
};
|
||||
|
||||
private rows: CorTableRow[] = [];
|
||||
private columns: CorTableColumn[] = [];
|
||||
|
||||
constructor(@Inject('TableService') private tableService: any) {
|
||||
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['tableData'] !== undefined) {
|
||||
this.refreshOrder();
|
||||
}
|
||||
}
|
||||
|
||||
public addColumn(col: CorTableColumn): void {
|
||||
this.columns.push(col);
|
||||
|
||||
if (col.selected == 'true') {
|
||||
this.options['predicate'] = col.datafield;
|
||||
}
|
||||
|
||||
this.refreshOrder();
|
||||
}
|
||||
|
||||
private setOrder(col: CorTableColumn): void {
|
||||
this.tableService.orderBy(col.datafield, this.options);
|
||||
this.refreshOrder();
|
||||
}
|
||||
|
||||
private setExpanded(isExpanded: boolean): void {
|
||||
this.expandRows = isExpanded;
|
||||
this.rows.forEach((row) => row.expanded = isExpanded);
|
||||
}
|
||||
|
||||
private tablePredicateClass(col: CorTableColumn, options: any) {
|
||||
return this.tableService.tablePredicateClass(col.datafield, this.options.predicate, this.options.reverse);
|
||||
}
|
||||
|
||||
private refreshOrder(): void {
|
||||
this.options.page = 0;
|
||||
|
||||
var columnMap: {[name: string]: CorTableColumn} = {};
|
||||
this.columns.forEach(function(col) {
|
||||
columnMap[col.datafield] = col;
|
||||
});
|
||||
|
||||
const numericCols: string[] = this.columns.filter(col => col.isNumeric())
|
||||
.map(col => col.datafield);
|
||||
|
||||
const processed: any[] = this.tableData.map((item) => {
|
||||
Object.keys(item).forEach((key) => {
|
||||
if (columnMap[key]) {
|
||||
item[key] = columnMap[key].processColumnForOrdered(item[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
this.orderedData = this.tableService.buildOrderedItems(processed, this.options, this.filterFields, numericCols);
|
||||
this.rows = this.orderedData.visibleEntries.map((item) => Object.assign({}, {expanded: false, rowData: item}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type CorTableOptions = {
|
||||
filter: string;
|
||||
reverse: boolean;
|
||||
predicate: string;
|
||||
page: number;
|
||||
};
|
||||
|
||||
|
||||
export type CorTableRow = {
|
||||
expanded: boolean;
|
||||
rowData: any;
|
||||
};
|
Reference in a new issue