maintain fixed-width columns when filtering cor-table
This commit is contained in:
parent
457f685952
commit
5e41de5370
9 changed files with 234 additions and 34 deletions
|
@ -973,13 +973,18 @@ a:focus {
|
|||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.co-fixed-table .co-flowing-col{
|
||||
.co-fixed-table .co-flowing-col {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.co-fixed-table .nowrap-col {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.co-table td {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px;
|
||||
|
|
|
@ -75,13 +75,16 @@
|
|||
</div>
|
||||
|
||||
<div ng-show="$ctrl.repository.releases.length || !$ctrl.repository.can_write">
|
||||
<cor-table table-data="$ctrl.repository.releases" table-item-title="releases" filter-fields="['name']">
|
||||
<cor-table table-data="$ctrl.repository.releases"
|
||||
table-item-title="releases"
|
||||
filter-fields="['name']"
|
||||
can-expand="true">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"></cor-table-col>
|
||||
<cor-table-col datafield="last_modified" sortfield="last_modified"
|
||||
title="Created"
|
||||
selected="true" kindof="datetime"
|
||||
templateurl="/static/js/directives/ui/app-public-view/last-modified.html"></cor-table-col>
|
||||
<cor-table-col datafield="channels" title="Channels"
|
||||
<cor-table-col datafield="channels" title="Channels" item-limit="6"
|
||||
templateurl="/static/js/directives/ui/app-public-view/channels-list.html"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
<span ng-repeat="channel_name in item.channels">
|
||||
<channel-icon name="channel_name"></channel-icon>
|
||||
</span>
|
||||
<span ng-if="!item.channels.length" class="empty">(None)</span>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="display: flex; flex-wrap: wrap; width: 70%;">
|
||||
<!-- TODO(alecmerdler): Move repeat logic to separate component -->
|
||||
<span ng-repeat="channel_name in item.channels | limitTo : ($ctrl.rows[rowIndex].expanded ? item.channels.length : col.itemLimit)"
|
||||
ng-style="{
|
||||
'width': (100 / col.itemLimit) + '%',
|
||||
'margin-bottom': $ctrl.rows[rowIndex].expanded && $index < (item.channels.length - col.itemLimit) ? '5px' : ''
|
||||
}">
|
||||
<channel-icon name="channel_name"></channel-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a ng-if="item.channels.length > col.itemLimit"
|
||||
ng-click="$ctrl.rows[rowIndex].expanded = !$ctrl.rows[rowIndex].expanded">
|
||||
{{ $ctrl.rows[rowIndex].expanded ? 'show less...' : item.channels.length - col.itemLimit + ' more...' }}
|
||||
</a>
|
||||
<span ng-if="!item.channels.length" class="empty">(None)</span>
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { CorTableComponent } from './cor-table.component';
|
|||
template: '',
|
||||
})
|
||||
export class CorTableColumn implements OnInit {
|
||||
|
||||
@Input('@') public title: string;
|
||||
@Input('@') public templateurl: string;
|
||||
@Input('@') public datafield: string;
|
||||
|
@ -19,6 +20,7 @@ export class CorTableColumn implements OnInit {
|
|||
@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) {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.cor-table-element .co-top-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: baseline;
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
<div class="cor-table-element">
|
||||
<span ng-transclude/>
|
||||
<span ng-transclude></span>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="co-top-bar" ng-if="$ctrl.compact != 'true'">
|
||||
<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"
|
||||
|
@ -16,6 +16,20 @@
|
|||
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 -->
|
||||
|
@ -24,20 +38,20 @@
|
|||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<table class="co-table" ng-show="$ctrl.tableData.length">
|
||||
<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 }}">
|
||||
<a ng-click="$ctrl.setOrder(col)">{{ ::col.title }}</a>
|
||||
</td>
|
||||
</thead>
|
||||
<tbody ng-repeat="item in $ctrl.orderedData.visibleEntries"
|
||||
<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 }} nowrap-col">
|
||||
<div ng-include="col.templateurl" ng-if="col.templateurl"></div>
|
||||
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>
|
||||
|
@ -49,4 +63,4 @@
|
|||
<div class="empty-primary-msg">No matching {{ ::$ctrl.tableItemTitle }} found.</div>
|
||||
<div class="empty-secondary-msg">Try adjusting your filter above.</div>
|
||||
</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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
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';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -13,23 +15,28 @@ import { CorTableColumn } from './cor-table-col.component';
|
|||
}
|
||||
})
|
||||
export class CorTableComponent implements OnChanges {
|
||||
|
||||
@Input('<') public tableData: any[] = [];
|
||||
@Input('@') public tableItemTitle: string;
|
||||
@Input('<') public filterFields: string[];
|
||||
@Input('@') public compact: string;
|
||||
@Input('<') public compact: boolean = false;
|
||||
@Input('<') public maxDisplayCount: number = 10;
|
||||
private columns: CorTableColumn[];
|
||||
private orderedData: any;
|
||||
private options: any;
|
||||
@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) {
|
||||
this.columns = [];
|
||||
this.options = {
|
||||
'filter': '',
|
||||
'reverse': false,
|
||||
'predicate': '',
|
||||
'page': 0,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
|
@ -53,6 +60,11 @@ export class CorTableComponent implements OnChanges {
|
|||
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);
|
||||
}
|
||||
|
@ -60,15 +72,15 @@ export class CorTableComponent implements OnChanges {
|
|||
private refreshOrder(): void {
|
||||
this.options.page = 0;
|
||||
|
||||
var columnMap = {};
|
||||
var columnMap: {[name: string]: CorTableColumn} = {};
|
||||
this.columns.forEach(function(col) {
|
||||
columnMap[col.datafield] = col;
|
||||
});
|
||||
|
||||
const numericCols = this.columns.filter(col => col.isNumeric())
|
||||
const numericCols: string[] = this.columns.filter(col => col.isNumeric())
|
||||
.map(col => col.datafield);
|
||||
|
||||
const processed = this.tableData.map((item) => {
|
||||
const processed: any[] = this.tableData.map((item) => {
|
||||
Object.keys(item).forEach((key) => {
|
||||
if (columnMap[key]) {
|
||||
item[key] = columnMap[key].processColumnForOrdered(item[key]);
|
||||
|
@ -79,5 +91,20 @@ export class CorTableComponent implements OnChanges {
|
|||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
max-display-count="$ctrl.namespacesPerPage"
|
||||
filter-fields="::['title', 'id']">
|
||||
<cor-table-col datafield="title"
|
||||
style="width: 20px;"
|
||||
style="width: 30px;"
|
||||
sortfield="title"
|
||||
bind-model="$ctrl.local.selectedNamespace"
|
||||
templateurl="/static/js/directives/ui/manage-trigger-githost/namespace-radio-input.html">
|
||||
|
@ -100,7 +100,8 @@
|
|||
max-display-count="$ctrl.repositoriesPerPage"
|
||||
filter-fields="['name', 'description']">
|
||||
<cor-table-col bind-model="$ctrl.local.selectedRepository"
|
||||
templateurl="/static/js/directives/ui/manage-trigger-githost/repository-radio-input.html">
|
||||
templateurl="/static/js/directives/ui/manage-trigger-githost/repository-radio-input.html"
|
||||
style="width: 30px;">
|
||||
<script type="text/ng-template" id="/static/js/directives/ui/manage-trigger-githost/repository-radio-input.html">
|
||||
<span ng-if="!item.has_admin_permissions">
|
||||
<i class="fa fa-exclamation-triangle"
|
||||
|
@ -135,10 +136,13 @@
|
|||
<cor-table-col title="Description"
|
||||
datafield="description"
|
||||
sortfield="description"
|
||||
templateurl="/static/js/directives/ui/manage-trigger-githost/repository-description.html">
|
||||
templateurl="/static/js/directives/ui/manage-trigger-githost/repository-description.html"
|
||||
class="co-flowing-col">
|
||||
<script type="text/ng-template" id="/static/js/directives/ui/manage-trigger-githost/repository-description.html">
|
||||
<span ng-if="item.description">{{ item.description }}</span>
|
||||
<span class="empty-description" ng-if="!item.description">(None)</span>
|
||||
<span ng-if="item.description" class="repo-description">
|
||||
{{ item.description }}
|
||||
</span>
|
||||
<span ng-if="!item.description" class="empty-description">(None)</span>
|
||||
</script>
|
||||
</cor-table-col>
|
||||
</cor-table>
|
||||
|
@ -376,7 +380,7 @@
|
|||
max-display-count="$ctrl.robotsPerPage">
|
||||
<cor-table-col datafield="name"
|
||||
bind-model="$ctrl.local.robotAccount"
|
||||
style="width: 20px;"
|
||||
style="width: 30px;"
|
||||
templateurl="/static/js/directives/ui/manage-trigger-custom-git/robot-radio-input.html">
|
||||
<script type="text/ng-template" id="/static/js/directives/ui/manage-trigger-custom-git/robot-radio-input.html">
|
||||
<input type="radio"
|
||||
|
|
Reference in a new issue