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.