diff --git a/horizon/static/framework/widgets/action-list/actions-detail.template.html b/horizon/static/framework/widgets/action-list/actions-detail.template.html
new file mode 100644
index 0000000000..d5c7bdbc9a
--- /dev/null
+++ b/horizon/static/framework/widgets/action-list/actions-detail.template.html
@@ -0,0 +1,13 @@
+
+
+
+
$title$
+
+
+
$description$
+
+ $text$
+
+
+
+
\ No newline at end of file
diff --git a/horizon/static/framework/widgets/action-list/actions.detail.mock.html b/horizon/static/framework/widgets/action-list/actions.detail.mock.html
new file mode 100644
index 0000000000..9e2a33928b
--- /dev/null
+++ b/horizon/static/framework/widgets/action-list/actions.detail.mock.html
@@ -0,0 +1,2 @@
+
+
diff --git a/horizon/static/framework/widgets/action-list/actions.directive.js b/horizon/static/framework/widgets/action-list/actions.directive.js
index ea6ca05e49..9b36ea352d 100644
--- a/horizon/static/framework/widgets/action-list/actions.directive.js
+++ b/horizon/static/framework/widgets/action-list/actions.directive.js
@@ -28,21 +28,20 @@
* @name horizon.framework.widgets.action-list.directive:actions
* @element
* @description
- * The `actions` directive represents the actions to be
- * displayed in a Bootstrap button group or button
- * dropdown.
+ * The `actions` directive represents the actions to be displayed in a Bootstrap button
+ * group, button dropdown, or bootstrap panels.
*
*
* Attributes:
*
* @param {string} type
- * Type can be only be 'row' or 'batch'.
- * 'batch' actions are rendered as a button group, 'row' is rendered as a button dropdown menu.
- * 'batch' actions are typically used for actions across multiple items while
- * 'row' actions are used per item.
+ * Type can be 'row', 'batch', or 'detail'. 'batch' actions are rendered as a button group,
+ * 'row' actions are rendered as a button dropdown menu, 'detail' actions are rendered as
+ * bootstrap panels. 'batch' actions are typically used for actions across multiple items while
+ * 'row' and 'detail' actions are used per 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
* (Optional) A function that is called with the return value from a clicked actions perform
@@ -77,8 +76,8 @@
* 2. type: ''
* This creates an action button based off a 'known' button type.
* Currently supported values are
- * 1. 'delete' - Delete a single row. Only for 'row' type.
- * 2. 'danger' - For marking an Action as dangerous. 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' or 'detail' type.
* 3. 'delete-selected' - Delete multiple rows. 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.
* 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
* 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
* 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.
* 2. perform: is what gets called when the button is clicked. Also expected to return a
* 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 '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(
$parse,
diff --git a/horizon/static/framework/widgets/action-list/actions.directive.spec.js b/horizon/static/framework/widgets/action-list/actions.directive.spec.js
index bce0fc692a..5f258160cd 100644
--- a/horizon/static/framework/widgets/action-list/actions.directive.spec.js
+++ b/horizon/static/framework/widgets/action-list/actions.directive.spec.js
@@ -317,6 +317,36 @@
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) {
return {
template: {
@@ -392,13 +422,13 @@
return element;
}
- function rowElementFor(actions) {
+ function rowElementFor(actions, detail) {
$scope.rowItem = rowItem;
$scope.actions = function() {
return actions;
};
- var element = angular.element(getTemplate('actions.row'));
+ var element = angular.element(getTemplate(detail ? 'actions.detail' : 'actions.row'));
$compile(element)($scope);
$scope.$apply();
diff --git a/horizon/static/framework/widgets/action-list/actions.service.js b/horizon/static/framework/widgets/action-list/actions.service.js
index afd45f6b04..96760e0fc8 100644
--- a/horizon/static/framework/widgets/action-list/actions.service.js
+++ b/horizon/static/framework/widgets/action-list/actions.service.js
@@ -15,6 +15,8 @@
(function() {
'use strict';
+ var dangerTypes = { 'delete': 1, 'danger': 1, 'delete-selected': 1 };
+
angular
.module('horizon.framework.widgets.action-list')
.factory('horizon.framework.widgets.action-list.actions.service', actionsService);
@@ -83,7 +85,9 @@
if (permittedActions.pass.length > 0) {
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');
templateFetch.then(addButtons);
} else {
@@ -92,6 +96,16 @@
}
}
+ function addDetailActions(templates) {
+ var row = angular.element('');
+ 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
*/
@@ -195,6 +209,10 @@
'$action-classes$', getActionClasses(action, index, permittedActions.length)
)
.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);
defered.resolve({
template: template,
@@ -216,22 +234,29 @@
*/
function getActionClasses(action, index, numPermittedActions) {
var actionClassesParam = action.template.actionClasses || "";
+ var actionClasses = 'btn ';
if (listType === 'row') {
if (numPermittedActions === 1 || index === 0) {
- var actionClasses = "btn ";
- if (action.template.type === "delete" || action.template.type === 'danger') {
- actionClasses += "btn-danger ";
+ if (action.template.type in dangerTypes) {
+ actionClasses += 'btn-danger ';
} else {
- actionClasses += "btn-default ";
+ actionClasses += 'btn-default ';
}
return actionClasses + actionClassesParam;
} else {
- if (action.template.type === "delete" || action.template.type === 'danger') {
+ if (action.template.type in dangerTypes) {
return 'text-danger' + actionClassesParam;
} else {
return actionClassesParam;
}
}
+ } else if (listType === 'detail') {
+ if (action.template.type in dangerTypes) {
+ actionClasses += 'btn-danger';
+ } else {
+ actionClasses += 'btn-primary';
+ }
+ return actionClasses;
} else {
return actionClassesParam;
}
@@ -250,11 +275,11 @@
if (angular.isDefined(action.template.url)) {
// use the given 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
return basePath + 'action-list/actions-' + action.template.type + '.template.html';
} 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';
}
}
diff --git a/releasenotes/notes/bp-next-steps-4c7064e52d5abcf5.yaml b/releasenotes/notes/bp-next-steps-4c7064e52d5abcf5.yaml
new file mode 100644
index 0000000000..fb77a7cf82
--- /dev/null
+++ b/releasenotes/notes/bp-next-steps-4c7064e52d5abcf5.yaml
@@ -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.