Add support for detail actions

This updates the angular actions directive to support the "detail"
list type to render actions as tiles with a title and description.

To see it in action:
Update some row actions so the template attribute has a title and
description property. Then add these actions to an array in the
detail page controller and add the actions directive to the content
of the detail page, something like this:

  <actions allowed="ctrl.detailActions" type="detail"
           item="ctrl.item" ng-if="ctrl.item"></actions>

Implements blueprint next-steps
Change-Id: I5e85004255e351c1fdd121251030e0697b3b9b9f
This commit is contained in:
Justin Pomeroy 2016-03-15 09:49:17 -05:00 committed by Rob Cresswell
parent caa5e91059
commit 0c5c57542b
6 changed files with 106 additions and 22 deletions

View File

@ -0,0 +1,13 @@
<div class="col-lg-4 col-md-6">
<div class="panel $panel-classes$">
<div class="panel-heading">
<h3 class="panel-title">$title$</h3>
</div>
<div class="panel-body">
<p>$description$</p>
<action action-classes="'$action-classes$'" item="$item$">
$text$
</action>
</div>
</div>
</div>

View File

@ -0,0 +1,2 @@
<actions allowed="actions" type="detail" item="rowItem">
</actions>

View File

@ -28,21 +28,20 @@
* @name horizon.framework.widgets.action-list.directive:actions * @name horizon.framework.widgets.action-list.directive:actions
* @element * @element
* @description * @description
* The `actions` directive represents the actions to be * The `actions` directive represents the actions to be displayed in a Bootstrap button
* displayed in a Bootstrap button group or button * group, button dropdown, or bootstrap panels.
* dropdown.
* *
* *
* Attributes: * Attributes:
* *
* @param {string} type * @param {string} type
* Type can be only be 'row' or 'batch'. * Type can be 'row', 'batch', or 'detail'. 'batch' actions are rendered as a button group,
* 'batch' actions are rendered as a button group, 'row' is rendered as a button dropdown menu. * 'row' actions are rendered as a button dropdown menu, 'detail' actions are rendered as
* 'batch' actions are typically used for actions across multiple items while * bootstrap panels. 'batch' actions are typically used for actions across multiple items while
* 'row' actions are used per item. * 'row' and 'detail' actions are used per item.
* *
* @param {string=} item * @param {string=} item
* The item to pass to the 'service' when using 'row' type. * The item to pass to the 'service' when using 'row' or 'detail' type.
* *
* @param {function} result-handler * @param {function} result-handler
* (Optional) A function that is called with the return value from a clicked actions perform * (Optional) A function that is called with the return value from a clicked actions perform
@ -77,8 +76,8 @@
* 2. type: '<action_button_type>' * 2. type: '<action_button_type>'
* This creates an action button based off a 'known' button type. * This creates an action button based off a 'known' button type.
* Currently supported values are * Currently supported values are
* 1. 'delete' - Delete a single row. Only for 'row' type. * 1. 'delete' - Delete a single row. Only for 'row' or 'detail' type.
* 2. 'danger' - For marking an Action as dangerous. Only for 'row' type. * 2. 'danger' - For marking an Action as dangerous. Only for 'row' or 'detail' type.
* 3. 'delete-selected' - Delete multiple rows. Only for 'batch' type. * 3. 'delete-selected' - Delete multiple rows. Only for 'batch' type.
* 4. 'create' - Create a new entity. Only for 'batch' type. * 4. 'create' - Create a new entity. Only for 'batch' type.
* *
@ -90,15 +89,20 @@
* For custom styling of the button, `actionClasses` can be optionally included. * For custom styling of the button, `actionClasses` can be optionally included.
* The directive will be responsible for binding the correct callback. * The directive will be responsible for binding the correct callback.
* *
* 4. title: 'title', description: 'description'
* A title and description must be provided for the 'detail' type. These are used as
* the title and description to display in the bootstrap panel.
*
* service: is the service expected to have two functions * service: is the service expected to have two functions
* 1. allowed: is expected to return a promise that resolves * 1. allowed: is expected to return a promise that resolves
* if the action is permitted and is rejected if not. If there are multiple promises that * if the action is permitted and is rejected if not. If there are multiple promises that
* need to be resolved, you can $q.all to combine multiple promises into a single promise. * need to be resolved, you can $q.all to combine multiple promises into a single promise.
* When using 'row' type, the current 'item' will be passed to the function. * When using 'row' or 'detail' type, the current 'item' will be passed to the function.
* When using 'batch' type, no arguments are provided. * When using 'batch' type, no arguments are provided.
* 2. perform: is what gets called when the button is clicked. Also expected to return a * 2. perform: is what gets called when the button is clicked. Also expected to return a
* promise that resolves when the action completes. * promise that resolves when the action completes.
* When using 'row' type, the current 'item' is evaluated and passed to the function. * When using 'row' or 'detail' type, the current 'item' is evaluated and passed to the
* function.
* When using 'batch' type, 'item' is not passed. * When using 'batch' type, 'item' is not passed.
* When using 'delete-selected' for 'batch' type, all selected rows are passed. * When using 'delete-selected' for 'batch' type, all selected rows are passed.
* *
@ -222,6 +226,10 @@
* *
* ``` * ```
* *
* detail:
*
* The 'detail' type actions are identical to the 'row' type actions except that the template
* property for each action should have a title and description property.
*/ */
function actions( function actions(
$parse, $parse,

View File

@ -317,6 +317,36 @@
expect(callbacks.first).toHaveBeenCalled(); expect(callbacks.first).toHaveBeenCalled();
}); });
it('should render detail actions', function () {
var actions = [{
template: {
text: 'Action 1',
title: 'Do something cool',
description: 'This describes what that cool thing is you can do.'
},
service: getService(getPermission(true), callback)
},{
template: {
text: 'Action 2',
title: 'Do something dangerous',
type: 'danger',
description: 'This describes what that dangerous thing is you can do.'
},
service: getService(getPermission(true), callback)
}];
var element = rowElementFor(actions, true);
expect(element.find('.panel').length).toBe(2);
expect(element.find('.panel-title').first().text().trim()).toBe('Do something cool');
expect(element.find('.panel-title').last().text().trim()).toBe('Do something dangerous');
expect(element.find('.panel-body button').first().text().trim()).toBe('Action 1');
expect(element.find('.panel-body button').last().text().trim()).toBe('Action 2');
expect(element.find('.panel').first().hasClass('panel-info')).toBe(true);
expect(element.find('.panel').last().hasClass('panel-danger')).toBe(true);
expect(element.find('.panel-body button').first().hasClass('btn-primary')).toBe(true);
expect(element.find('.panel-body button').last().hasClass('btn-danger')).toBe(true);
});
function permittedActionWithUrl(templateName) { function permittedActionWithUrl(templateName) {
return { return {
template: { template: {
@ -392,13 +422,13 @@
return element; return element;
} }
function rowElementFor(actions) { function rowElementFor(actions, detail) {
$scope.rowItem = rowItem; $scope.rowItem = rowItem;
$scope.actions = function() { $scope.actions = function() {
return actions; return actions;
}; };
var element = angular.element(getTemplate('actions.row')); var element = angular.element(getTemplate(detail ? 'actions.detail' : 'actions.row'));
$compile(element)($scope); $compile(element)($scope);
$scope.$apply(); $scope.$apply();

View File

@ -15,6 +15,8 @@
(function() { (function() {
'use strict'; 'use strict';
var dangerTypes = { 'delete': 1, 'danger': 1, 'delete-selected': 1 };
angular angular
.module('horizon.framework.widgets.action-list') .module('horizon.framework.widgets.action-list')
.factory('horizon.framework.widgets.action-list.actions.service', actionsService); .factory('horizon.framework.widgets.action-list.actions.service', actionsService);
@ -83,7 +85,9 @@
if (permittedActions.pass.length > 0) { if (permittedActions.pass.length > 0) {
var templateFetch = $q.all(permittedActions.pass.map(getTemplate)); var templateFetch = $q.all(permittedActions.pass.map(getTemplate));
if (listType === 'batch' || permittedActions.pass.length === 1) { if (listType === 'detail') {
templateFetch.then(addDetailActions);
} else if (listType === 'batch' || permittedActions.pass.length === 1) {
element.addClass('btn-addon'); element.addClass('btn-addon');
templateFetch.then(addButtons); templateFetch.then(addButtons);
} else { } else {
@ -92,6 +96,16 @@
} }
} }
function addDetailActions(templates) {
var row = angular.element('<div class="row"></div>');
element.append(row);
templates.forEach(function renderDetailAction(template) {
var templateElement = angular.element(template.template);
templateElement.find('action').attr('callback', template.callback);
row.append($compile(templateElement)(scope));
});
}
/** /**
* Add all the buttons as a list of buttons * Add all the buttons as a list of buttons
*/ */
@ -195,6 +209,10 @@
'$action-classes$', getActionClasses(action, index, permittedActions.length) '$action-classes$', getActionClasses(action, index, permittedActions.length)
) )
.replace('$text$', action.template.text) .replace('$text$', action.template.text)
.replace('$title$', action.template.title)
.replace('$description$', action.template.description)
.replace('$panel-classes$',
action.template.type in dangerTypes ? 'panel-danger' : 'panel-info')
.replace('$item$', item); .replace('$item$', item);
defered.resolve({ defered.resolve({
template: template, template: template,
@ -216,22 +234,29 @@
*/ */
function getActionClasses(action, index, numPermittedActions) { function getActionClasses(action, index, numPermittedActions) {
var actionClassesParam = action.template.actionClasses || ""; var actionClassesParam = action.template.actionClasses || "";
var actionClasses = 'btn ';
if (listType === 'row') { if (listType === 'row') {
if (numPermittedActions === 1 || index === 0) { if (numPermittedActions === 1 || index === 0) {
var actionClasses = "btn "; if (action.template.type in dangerTypes) {
if (action.template.type === "delete" || action.template.type === 'danger') { actionClasses += 'btn-danger ';
actionClasses += "btn-danger ";
} else { } else {
actionClasses += "btn-default "; actionClasses += 'btn-default ';
} }
return actionClasses + actionClassesParam; return actionClasses + actionClassesParam;
} else { } else {
if (action.template.type === "delete" || action.template.type === 'danger') { if (action.template.type in dangerTypes) {
return 'text-danger' + actionClassesParam; return 'text-danger' + actionClassesParam;
} else { } else {
return actionClassesParam; return actionClassesParam;
} }
} }
} else if (listType === 'detail') {
if (action.template.type in dangerTypes) {
actionClasses += 'btn-danger';
} else {
actionClasses += 'btn-primary';
}
return actionClasses;
} else { } else {
return actionClassesParam; return actionClassesParam;
} }
@ -250,11 +275,11 @@
if (angular.isDefined(action.template.url)) { if (angular.isDefined(action.template.url)) {
// use the given URL // use the given URL
return action.template.url; return action.template.url;
} else if (angular.isDefined(action.template.type)) { } else if (angular.isDefined(action.template.type) && listType !== 'detail') {
// determine the template by the given type // determine the template by the given type
return basePath + 'action-list/actions-' + action.template.type + '.template.html'; return basePath + 'action-list/actions-' + action.template.type + '.template.html';
} else { } else {
// determine the template by `listType` which can be row or batch // determine the template by `listType` which can be row, batch, or detail
return basePath + 'action-list/actions-' + listType + '.template.html'; return basePath + 'action-list/actions-' + listType + '.template.html';
} }
} }

View File

@ -0,0 +1,6 @@
---
features:
- Added ability to render angular row actions with additional details that
explain the purpose of the action. These are rendered as tiles and are
meant to depict the next steps a user might want to take for a given
resource.