From e0779033544710d4951dfb2f443f69d984e55279 Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Mon, 6 Jun 2016 16:20:04 +1000 Subject: [PATCH] Improvements to hz-dynamic-table These changes were prompted by the patch to migrate Swift UI to use hz-dynamic-table (now a follow-on patch). hz-table and its select-all assumes the use of an "id" property for selection, which isn't always present (Swift uses "name") and hz-dynamic-table makes it configurable anyway, so now it's also configurable on hz-table. The various directives in hz-dynamic-table on the tag were moved up to an enclosing
so as to allow other content to remain outside of the table while still access the "table" contexts (st-table, hz-table). The search bar and action buttons may now occupy the same row, though by default they still split over two rows. The column share is configurable. Change-Id: I6f687ea9f605674d123e07e9f6e0dc2866f933b2 Partially-Implements: blueprint swift-ui-functionality --- .../widgets/table/hz-cell.directive.js | 3 +- .../table/hz-dynamic-table.directive.js | 57 +++++-- .../widgets/table/hz-dynamic-table.html | 160 ++++++++++-------- .../widgets/table/hz-dynamic-table.spec.js | 58 ++++++- .../widgets/table/hz-table.directive.js | 21 ++- .../widgets/table/table.controller.js | 10 +- 6 files changed, 209 insertions(+), 100 deletions(-) diff --git a/horizon/static/framework/widgets/table/hz-cell.directive.js b/horizon/static/framework/widgets/table/hz-cell.directive.js index fa10a6d01e..10d854ebee 100644 --- a/horizon/static/framework/widgets/table/hz-cell.directive.js +++ b/horizon/static/framework/widgets/table/hz-cell.directive.js @@ -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 * diff --git a/horizon/static/framework/widgets/table/hz-dynamic-table.directive.js b/horizon/static/framework/widgets/table/hz-dynamic-table.directive.js index 92afb6675c..a2f672b3e1 100644 --- a/horizon/static/framework/widgets/table/hz-dynamic-table.directive.js +++ b/horizon/static/framework/widgets/table/hz-dynamic-table.directive.js @@ -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"> * * ``` * */ function hzDynamicTable(basePath) { + // : 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; + } + } } } })(); diff --git a/horizon/static/framework/widgets/table/hz-dynamic-table.html b/horizon/static/framework/widgets/table/hz-dynamic-table.html index 7427db71d6..e4af29d5cb 100644 --- a/horizon/static/framework/widgets/table/hz-dynamic-table.html +++ b/horizon/static/framework/widgets/table/hz-dynamic-table.html @@ -2,87 +2,97 @@ Dynamic table template --> - - - -
- - - - - - - - + class="container-fluid"> - - - +
+ + + + +
- - - - - +
- - - {$ column.title $} -
- - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - -
+ + + {$ column.title $} +
+ + + + + + + + + +
+ + +
- - -
+ + + diff --git a/horizon/static/framework/widgets/table/hz-dynamic-table.spec.js b/horizon/static/framework/widgets/table/hz-dynamic-table.spec.js index 293bb9cfb1..4e74336ebe 100644 --- a/horizon/static/framework/widgets/table/hz-dynamic-table.spec.js +++ b/horizon/static/framework/widgets/table/hz-dynamic-table.spec.js @@ -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 = + '' + + ''; + }); + + 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() { diff --git a/horizon/static/framework/widgets/table/hz-table.directive.js b/horizon/static/framework/widgets/table/hz-table.directive.js index c24d62eea2..e49db0cd62 100644 --- a/horizon/static/framework/widgets/table/hz-table.directive.js +++ b/horizon/static/framework/widgets/table/hz-table.directive.js @@ -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 * * ``` * + * hz-table track-rows-by="name"> * * *
@@ -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; + } + } } })(); diff --git a/horizon/static/framework/widgets/table/table.controller.js b/horizon/static/framework/widgets/table/table.controller.js index 8db371a8b7..92d3580aa4 100644 --- a/horizon/static/framework/widgets/table/table.controller.js +++ b/horizon/static/framework/widgets/table/table.controller.js @@ -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) { /*