maintain fixed-width columns when filtering cor-table

This commit is contained in:
alecmerdler 2017-05-27 01:15:28 -07:00 committed by Alec Merdler
parent 457f685952
commit 5e41de5370
9 changed files with 234 additions and 34 deletions

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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) {

View file

@ -0,0 +1,5 @@
.cor-table-element .co-top-bar {
display: flex;
justify-content: flex-end;
align-items: baseline;
}

View file

@ -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>

View 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();
});
});
});

View file

@ -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;
};

View file

@ -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"