Merge "Improvements to hz-dynamic-table"

This commit is contained in:
Jenkins 2016-06-08 10:16:02 +00:00 committed by Gerrit Code Review
commit 377da87e3c
6 changed files with 209 additions and 100 deletions

View File

@ -38,7 +38,8 @@
*
* It should ideally be used within the context of the `hz-dynamic-table` directive.
* The params passed into `hz-dynamic-table` can be used in the custom template,
* including the 'table' scope.
* including the 'table' scope. 'table' can be referenced if you want to pass in an
* outside scope.
*
* @restrict E
*

View File

@ -29,24 +29,35 @@
*
* @param {object} config column definition used to generate the table (required)
* @param {object} items original collection, passed into 'st-safe-src' attribute (required)
* @param {object=} table any additional information that are
* passed down to child widgets (e.g hz-cell) (optional)
* @param {object=} batchActions batch actions for the table (optional)
* @param {object=} itemActions item actions for each item/row (optional)
* @param {object=} filterFacets Facets allowed for searching, if not provided,
* default to simple text search (optional)
* @param {object=} table is the name of a controller that should be passed
* down to child widgets (e.g hz-cell) for additional attribute access (optional)
* @param {object=} batchActions batch action-list actions for the table (optional)
* @param {object=} itemActions item action-list actions for each item/row (optional)
* @param {object=} filterFacets Facets used by hz-magic-search-context allowed for
* searching. Filter will not be shown if this is not supplied (optional)
* @param {function=} resultHandler function that is called with return value
* from a clicked actions perform function passed into `actions` directive (optional)
*
* @description
* The `hzDynamicTable` directive generates all the HTML content for a table.
* You will need to pass in two attributes: `config` and `items`.
* This directive is built off the Smart-table module, so `items`
* is passed into `st-table` attribute.
*
* This directive is built off the Smart-table module, so `items` is passed into
* `st-safe-src`.
* Note: `st-safe-src' is used for async data, to keep track of modifications to the
* original collection. Also, 'name' is the key used to retrieve cell data from base
* 'displayedCollection'.
* You can pass the following into `config` object:
* selectAll {boolean} set to true if you want to enable select all checkbox
* expand {boolean} set to true if you want to inline details
* trackId {string} passed into ngRepeat's track by to identify objects
* searchColumnSpan {number} is used to define the number of bootstrap grid columns the
* search box will occupy. If this is set to 12 (the default) then the search box
* and batch action buttons will be on separate rows.
* actionColumnSpan {number} is the number of bootstrap grid columns the action buttons
* should occupy. This defaults to 12, or the remainder of the row if searchColumnSpan
* is less than 12 columns.
* columns {Array} of objects to describe each column. Each object
* requires: 'id', 'title', 'priority' (responsive priority when table resized)
* optional: 'sortDefault', 'filters' (to apply to the column cells),
* 'template' (see hz-cell directive for details)
*
* @example
*
@ -54,6 +65,7 @@
* selectAll: true,
* expand: true,
* trackId: 'id',
* searchColumnSpan: 6,
* columns: [
* {id: 'a', title: 'A', priority: 1},
* {id: 'b', title: 'B', priority: 2},
@ -71,16 +83,21 @@
* config='config'
* items="items"
* table="table"
* batchActions="batchActions"
* itemActions="itemActions"
* filterFacets="filterFacets"
* resultHandler="resultHandler">
* batch-actions="batchActions"
* item-actions="itemActions"
* filter-facets="filterFacets"
* result-handler="resultHandler">
* </hz-dynamic-table>
* ```
*
*/
function hzDynamicTable(basePath) {
// <r1chardj0n3s>: there are some configuration items which are on the directive,
// and some on the "config" attribute of the directive. Those latter configuration
// items will be effectively "static" for the lifespan of the directive whereas
// angular will watch directive attributes for changes. This should be revisited
// at some point to make sure the split we've actually got here makes sense.
var directive = {
restrict: 'E',
scope: {
@ -109,6 +126,16 @@
if (angular.isUndefined(scope.config.expand)) {
scope.config.expand = true;
}
if (angular.isUndefined(scope.config.searchColumnSpan)) {
scope.config.searchColumnSpan = 12;
}
if (angular.isUndefined(scope.config.actionColumnSpan)) {
if (scope.config.searchColumnSpan < 12) {
scope.config.actionColumnSpan = 12 - scope.config.searchColumnSpan;
} else {
scope.config.actionColumnSpan = 12;
}
}
}
}
})();

View File

@ -2,87 +2,97 @@
Dynamic table template
-->
<hz-magic-search-context filter-facets="filterFacets">
<hz-magic-search-bar ng-if="filterFacets">
</hz-magic-search-bar>
<actions ng-if="filterFacets && batchActions" allowed="batchActions" type="batch" result-handler="resultHandler"></actions>
<table
hz-table ng-cloak
<div hz-table
track-rows-by="{$ config.trackId $}"
ng-cloak
st-magic-search
st-table="items"
st-safe-src="safeSrcItems"
class="table table-striped table-rsp table-detail">
<thead>
<!--
Table-column-headers:
Set selectAll to True if you want to enable select all checkbox.
Set expand to True if you want to inline details.
-->
<tr>
<th ng-show="config.selectAll" class="multi_select_column">
<input type="checkbox" hz-select-all="items">
</th>
<th ng-show="config.expand" class="expander"></th>
<th ng-repeat="column in config.columns"
class="rsp-p{$ column.priority $}"
st-sort="{$ column.id $}"
ng-attr-st-sort-default="{$ column.sortDefault $}"
translate>
{$ column.title $}
</th>
</tr>
</thead>
class="container-fluid">
<tbody>
<!--
Table-rows:
classes rsp-p1 rsp-p2 are responsive priority as user resizes window.
-->
<tr ng-repeat-start="item in items track by item[config.trackId]"
ng-class="{'st-selected': checked[item[config.trackId]]}">
<div class="row hz-dynamic-table-preamble">
<hz-magic-search-bar ng-if="filterFacets" class="col-md-{$ config.searchColumnSpan $}">
</hz-magic-search-bar>
<actions ng-if="batchActions" class="col-md-{$ config.actionColumnSpan $}"
allowed="batchActions" type="batch" result-handler="resultHandler">
</actions>
</div>
<td ng-show="config.selectAll" class="multi_select_column">
<input type="checkbox"
ng-model="tCtrl.selections[item[config.trackId]].checked"
hz-select="item">
</td>
<td ng-show="config.expand" class="expander">
<span class="fa fa-chevron-right"
hz-expand-detail
duration="200">
</span>
</td>
<td ng-repeat="column in config.columns"
class="rsp-p{$ column.priority $}">
<hz-cell></hz-cell>
</td>
<td class="action_column">
<!--
Table-row-action-column:
Actions taken here apply to a single item/row.
-->
<actions ng-if="itemActions" allowed="itemActions" type="row" item="item" result-handler="resultHandler"></actions>
</td>
</tr>
<table class="table table-striped table-rsp table-detail">
<thead>
<!--
Table-column-headers:
Set selectAll to True if you want to enable select all checkbox.
Set expand to True if you want to inline details.
-->
<tr>
<th ng-show="config.selectAll" class="multi_select_column">
<input type="checkbox" hz-select-all="items">
</th>
<th ng-show="config.expand" class="expander"></th>
<th ng-repeat="column in config.columns"
class="rsp-p{$ column.priority $}"
st-sort="{$ column.id $}"
ng-attr-st-sort-default="{$ column.sortDefault $}"
translate>
{$ column.title $}
</th>
<th ng-if="itemActions"></th>
</tr>
</thead>
<tbody>
<!--
Table-rows:
classes rsp-p1 rsp-p2 are responsive priority as user resizes window.
-->
<tr ng-repeat-start="item in items track by item[config.trackId]"
ng-class="{'st-selected': checked[item[config.trackId]]}">
<td ng-show="config.selectAll" class="multi_select_column">
<input type="checkbox"
ng-model="tCtrl.selections[item[config.trackId]].checked"
hz-select="item">
</td>
<td ng-show="config.expand" class="expander">
<span class="fa fa-chevron-right"
hz-expand-detail
duration="200">
</span>
</td>
<td ng-repeat="column in config.columns"
class="rsp-p{$ column.priority $}">
<hz-cell></hz-cell>
</td>
<td ng-if="itemActions" class="actions_column">
<!--
Table-row-action-column:
Actions taken here apply to a single item/row.
-->
<actions allowed="itemActions" type="row" item="item" result-handler="resultHandler"></actions>
</td>
</tr>
<!--
Detail-row:
Contains detailed information on this item.
Can be toggled using the chevron button.
Ensure colspan is greater or equal to number of column-headers.
-->
<tr ng-if="config.expand" ng-repeat-end class="detail-row">
<td class="detail" colspan="100">
<hz-detail-row template-url="config.detailsTemplateUrl">
</hz-detail-row>
</td>
</tr>
<tr hz-no-items items="items"></tr>
</tbody>
<!--
Detail-row:
Contains detailed information on this item.
Can be toggled using the chevron button.
Ensure colspan is greater or equal to number of column-headers.
Table-footer:
This is where we display number of items and pagination controls.
-->
<tr ng-if="config.expand" ng-repeat-end class="detail-row">
<td class="detail" colspan="100">
<hz-detail-row template-url="config.detailsTemplateUrl">
</hz-detail-row>
</td>
</tr>
<tr hz-no-items items="items"></tr>
</tbody>
<!--
Table-footer:
This is where we display number of items and pagination controls.
-->
<tfoot hz-table-footer items="items"></tfoot>
</table>
<tfoot hz-table-footer items="items"></tfoot>
</table>
</div>
</hz-magic-search-context>

View File

@ -25,7 +25,7 @@
}
describe('hzDynamicTable directive', function () {
var $scope, $compile, markup;
var $scope, $compile, $qExtensions, markup;
beforeEach(module('templates'));
beforeEach(module('smart-table'));
@ -34,6 +34,7 @@
beforeEach(inject(function ($injector) {
$compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
$qExtensions = $injector.get('horizon.framework.util.q.extensions');
$scope.config = {
selectAll: true,
@ -107,7 +108,7 @@
it('has the correct responsive priority classes', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('tbody tr').length).toBe(7);
expect($element.find('tbody tr:eq(0) td').length).toBe(6);
expect($element.find('tbody tr:eq(0) td').length).toBe(5);
expect($element.find('tbody tr:eq(2) td:eq(2)').hasClass('rsp-p1')).toBe(true);
expect($element.find('tbody tr:eq(2) td:eq(3)').hasClass('rsp-p2')).toBe(true);
expect($element.find('tbody tr:eq(2) td:eq(4)').hasClass('rsp-p1')).toBe(true);
@ -116,12 +117,63 @@
it('has the correct number of rows (including detail rows and no items row)', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('tbody tr').length).toBe(7);
expect($element.find('tbody tr:eq(0) td').length).toBe(6);
expect($element.find('tbody tr:eq(0) td').length).toBe(5);
expect($element.find('tbody tr:eq(2) td:eq(2)').text()).toContain('snake');
expect($element.find('tbody tr:eq(2) td:eq(3)').text()).toContain('reptile');
expect($element.find('tbody tr:eq(2) td:eq(4)').text()).toContain('mice');
});
it('has no search or action buttons if none configured', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('.hz-dynamic-table-preamble').length).toBe(1);
expect($element.find('.hz-dynamic-table-preamble').text().trim()).toBe('');
});
describe('search & action button preamble', function () {
beforeEach(function() {
$scope.filterFacets = [{ label: 'Name', name: 'name' }];
$scope.batchActions = [
{
id: 'action',
service: {allowed: function () {
return $qExtensions.booleanAsPromise(false);
}},
template: { type: 'create' }}
];
markup =
'<hz-dynamic-table config="config" items="safeTableData" ' +
'filter-facets="filterFacets" batch-actions="batchActions">' +
'</hz-dynamic-table>';
});
it('has the correct number of default columns', function() {
var $element = digestMarkup($scope, $compile, markup);
var preamble = $element.find('.hz-dynamic-table-preamble');
expect(preamble.length).toBe(1);
expect(preamble.find('hz-magic-search-bar').hasClass('col-md-12')).toBe(true);
expect(preamble.find('actions').hasClass('col-md-12')).toBe(true);
});
it('has the configured number of columns calculated', function() {
$scope.config.searchColumnSpan = 7;
var $element = digestMarkup($scope, $compile, markup);
var preamble = $element.find('.hz-dynamic-table-preamble');
expect(preamble.length).toBe(1);
expect(preamble.find('hz-magic-search-bar').hasClass('col-md-7')).toBe(true);
expect(preamble.find('actions').hasClass('col-md-5')).toBe(true);
});
it('has the configured number of columns', function() {
$scope.config.searchColumnSpan = 8;
$scope.config.actionColumnSpan = 4;
var $element = digestMarkup($scope, $compile, markup);
var preamble = $element.find('.hz-dynamic-table-preamble');
expect(preamble.length).toBe(1);
expect(preamble.find('hz-magic-search-bar').hasClass('col-md-8')).toBe(true);
expect(preamble.find('actions').hasClass('col-md-4')).toBe(true);
});
});
describe('hzDetailRow directive', function() {
it('compiles default detail row template', function() {

View File

@ -32,13 +32,18 @@
* row collection and `st-safe-src` attribute to pass in the
* safe row collection.
*
* If rows are identified by some property other than "id" (for the
* purposes of selection) then use the track-rows-by attribute to
* identify the property that should be used. In the example below,
* the unique property for the rows is "name", not "id" (the default).
*
* @restrict A
* @scope true
* @example
*
* ```
* <table st-table='displayedCollection' st-safe-src='rowCollection'
* hz-table default-sort="email">
* hz-table track-rows-by="name">
* <thead>
* <tr>
* <th>
@ -61,13 +66,21 @@
*
*/
function hzTable() {
var directive = {
return {
restrict: 'A',
require: 'stTable',
scope: true,
controller: 'TableController',
controllerAs: 'tCtrl'
controllerAs: 'tCtrl',
link: link
};
return directive;
///////////////////
function link(scope, element, attrs) {
if (attrs.trackRowsBy) {
scope.tCtrl.trackId = attrs.trackRowsBy;
}
}
}
})();

View File

@ -38,6 +38,7 @@
function TableController($scope) {
var ctrl = this;
ctrl.trackId = 'id';
ctrl.isSelected = isSelected;
ctrl.toggleSelect = toggleSelect;
ctrl.broadcastExpansion = broadcastExpansion;
@ -57,7 +58,7 @@
* return true if the row is selected
*/
function isSelected(row) {
var rowState = ctrl.selections[row.id];
var rowState = ctrl.selections[row[ctrl.trackId]];
return angular.isDefined(rowState) && rowState.checked;
}
@ -76,7 +77,12 @@
* Toggle the row selection state
*/
function toggleSelect(row, checkedState, broadcast) {
ctrl.selections[row.id] = { checked: checkedState, item: row };
var key = row[ctrl.trackId];
if (angular.isDefined(ctrl.selections[key])) {
ctrl.selections[key].checked = checkedState;
} else {
ctrl.selections[key] = { checked: checkedState, item: row };
}
ctrl.selected = getSelected(ctrl.selections);
if (broadcast) {
/*