Merge "Allow specifying item to use for actions in the actions directive"

This commit is contained in:
Jenkins 2015-12-17 05:29:58 +00:00 committed by Gerrit Code Review
commit 54495c3185
5 changed files with 177 additions and 121 deletions

View File

@ -1,2 +1,2 @@
<actions allowed-actions="actions" action-list-type="batch">
<actions allowed="actions" type="batch">
</actions>

View File

@ -35,71 +35,111 @@
*
* Attributes:
*
* allowedActions: actions allowed that can be displayed
* actionListType: allow the buttons to be shown as a list or doropdown
* @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.
*
* `allowedActions` is a list of allowed actions on the service.
* It's an array of objects of the form:
* { template: {}, permissions: <promise to determine permissions>, callback: 'callback'}
* @param {string=} item
* The item to pass to the callback when using 'row' type.
* The variable is evaluated and passed as an argument when evaluating 'allowed'.
* 'item' is not used when row type is 'batch'.
*
* `template` is an object that can be
* @param {function} allowed
* Returns an array of actions that can be performed on the item(s).
* When using 'row' type, the current 'item' will be passed to the function.
* When using 'batch' type, no arguments are provided.
*
* {url: 'template.html'} the location of the template for the action button.
* Use this for complete extensibility and control over what is rendered.
* The template will be responsible for binding the callback and styling.
* This is an array that should contain objects with the following properties:
* {
* template: <template object - described below>,
* permissions: <a promise to determine if action is allowed>,
* callback: 'callback for the action'
* }
*
* {type: 'type', item: 'item'} use a known action button type.
* Currently supported values are 'delete', 'delete-selected' and 'create'.
* `item` is optional and if provided will be used in the callback.
* The styling and binding of the callback is done by the template.
* template: is an object that can be any of
* 1. url: <full_path_to_template.html>
* This specifies the location of the template for the action button.
* Use this for complete extensibility and control over what is rendered.
* The template will be responsible for binding the callback and styling the button.
*
* {text: 'text', item: 'item'} use an unstyled button with given text.
* `item` is optional and if provided will be used in the callback.
* The styling of the button will vary per the `actionListType` chosen.
* For custom styling of the button, `actionClasses` can be included in
* the template.
* 2. type: '<action_button_type>'
* This uses a known action button type.
* Currently supported values are
* 1. 'delete' - Delete a single row. Only for 'row' type.
* 2. 'delete-selected' - Delete multiple rows. Only for 'batch' type.
* 3. 'create' - Create a new entity. Only for 'batch' type.
*
* `permissions` is expected to be a promise that resolves
* if permitted and is rejected if not.
* `callback` is the method to call when the button is clicked.
* The styling and binding of the callback is done by the template.
*
* `actionListType` can be only be `row` or `batch`
* `batch` is rendered as buttons, `row` is rendered as dropdown menu.
* 3. text: 'text', actionClasses: 'custom-classes'
* This creates a button with the given text.
* For custom styling of the button, `actionClasses` can be optionally included.
*
* permissions: is expected to be 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.
*
* callback: is the method to call when the button is clicked.
* When using 'row' 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.
*
* @restrict E
* @scope
* @example
*
* $scope.actions = [{
* template: {
* text: gettext('Delete Image'),
* type: 'delete',
* item: 'image'
* },
* permissions: policy.ifAllowed({ rules: [['image', 'delete_image']] }),
* callback: deleteModalService.open
* }, {
* template: {
* text: gettext('Create Volume'),
* item: 'image'
* },
* permissions: policy.ifAllowed({rules: [['volume', 'volume:create']]}),
* callback: createVolumeModalService.open
* }, {
* template: {
* url: basePath + 'actions/my-custom-action.html'
* },
* permissions: policy.ifAllowed({ rules: [['image', 'custom']] }),
* callback: customModalService.open
* }]
* batch:
*
* function actions() {
* return [{
* callback: 'table.batchActions.delete.open',
* template: {
* type: 'delete-selected',
* text: gettext('Delete Images')
* },
* permissions: policy.ifAllowed({ rules: [['image', 'delete_image']] })
* }, {
* callback: 'table.batchActions.create.open',
* template: {
* type: 'create',
* text: gettext('Create Image')
* },
* permissions: policy.ifAllowed({ rules: [['image', 'add_image']] })
* }];
* }
*
* ```
* <actions allowed-actions="actions" action-list-type="row">
* </actions>
*
* <actions allowed-actions="actions" action-list-type="batch">
* <actions allowed="actions" type="batch">
* </actions>
* ```
*
* row:
*
* function actions(image) {
* return [{
* callback: 'table.rowActions.deleteImage.open',
* template: {
* text: gettext('Delete Image'),
* type: 'delete'
* },
* permissions: imageDeletePermitted(image)
* }, {
* callback: 'table.rowActions.createVolume.open',
* template: {
* text: gettext('Create Volume')
* },
* permissions: createVolumeFromImagePermitted(image)
* }];
* }
*
* ```
* <actions allowed="actions" type="row" item="image">
* </actions>
*
* ```
*
*/
function actions(
$parse,
@ -114,11 +154,21 @@
return directive;
function link(scope, element, attrs) {
var listType = attrs.actionListType;
var allowedActions = $parse(attrs.allowedActions)(scope);
actionsService({scope: scope, element: element, listType: listType})
.renderActions(allowedActions);
var listType = attrs.type;
var item = attrs.item;
var service = actionsService({
scope: scope,
element: element,
listType: listType,
item: item
});
var allowedActions = $parse(attrs.allowed)(scope);
if (listType === 'row') {
var itemVal = $parse(item)(scope);
service.renderActions(allowedActions(itemVal));
} else {
service.renderActions(allowedActions());
}
}
}
})();

View File

@ -17,6 +17,8 @@
describe('actions directive', function () {
var $scope, $compile, $q, $templateCache, basePath;
var rowItem = {id: 1};
beforeEach(module('templates'));
beforeEach(module('horizon.framework'));
@ -29,18 +31,12 @@
}));
it('should have no buttons if there are no actions', function () {
var element = angular.element(getTemplate('actions.batch'));
$scope.actions = [];
$compile(element)($scope);
$scope.$apply();
var element = batchElementFor([]);
expect(element.children().length).toBe(0);
});
it('should allow for specifying action text', function () {
var element = angular.element(getTemplate('actions.batch'));
$scope.actions = [permittedActionWithText('Create Image', 'image')];
$compile(element)($scope);
$scope.$apply();
var element = batchElementFor([permittedActionWithText('Create Image')]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -52,10 +48,7 @@
});
it('should allow for specifying by template for create', function () {
var element = angular.element(getTemplate('actions.batch'));
$scope.actions = [permittedActionWithType('create', 'Create Image')];
$compile(element)($scope);
$scope.$apply();
var element = batchElementFor([permittedActionWithType('create', 'Create Image')]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -67,10 +60,7 @@
});
it('should allow for specifying by template for delete-selected', function () {
var element = angular.element(getTemplate('actions.batch'));
$scope.actions = [permittedActionWithType('delete-selected', 'Delete Images')];
$compile(element)($scope);
$scope.$apply();
var element = batchElementFor([permittedActionWithType('delete-selected', 'Delete Images')]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -83,10 +73,12 @@
});
it('should allow for specifying by template for delete', function () {
var element = angular.element(getTemplate('actions.row'));
$scope.actions = [permittedActionWithType('delete', 'Delete Image')];
$compile(element)($scope);
$scope.$apply();
$scope.callback = function(item) {
expect(item).toEqual(rowItem);
};
spyOn($scope, 'callback').and.callThrough();
var element = rowElementFor([permittedActionWithType('delete', 'Delete Image')]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -95,15 +87,14 @@
expect(actionList.find('button').attr('class')).toEqual('text-danger');
expect(actionList.find('button').attr('ng-click')).toEqual('disabled || callback(item)');
expect(actionList.text().trim()).toEqual('Delete Image');
actionList.find('button').click();
expect($scope.callback).toHaveBeenCalled();
});
it('should have one button if there is one action', function () {
var action = getTemplatePath('action-create', getTemplate());
var element = angular.element(getTemplate('actions.batch'));
$scope.actions = [permittedActionWithUrl(action)];
$compile(element)($scope);
$scope.$apply();
var element = batchElementFor([permittedActionWithUrl(action)]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -115,10 +106,7 @@
});
it('should have no buttons if not permitted', function () {
var element = angular.element(getTemplate('actions.batch'));
$scope.actions = [notPermittedAction()];
$compile(element)($scope);
$scope.$apply();
var element = batchElementFor([notPermittedAction()]);
expect(element.children().length).toBe(0);
});
@ -126,11 +114,10 @@
it('should have multiple buttons for multiple actions as a list', function () {
var action1 = getTemplatePath('action-create');
var action2 = getTemplatePath('action-delete');
var element = angular.element(getTemplate('actions.batch'));
$scope.actions = [permittedActionWithUrl(action1), permittedActionWithUrl(action2)];
$compile(element)($scope);
$scope.$apply();
var element = batchElementFor([
permittedActionWithUrl(action1),
permittedActionWithUrl(action2)
]);
expect(element.children().length).toBe(2);
var actionList = element.find('action-list');
@ -142,11 +129,10 @@
it('should have as many buttons as permitted', function () {
var actionTemplate1 = getTemplatePath('action-create');
var element = angular.element(getTemplate('actions.batch'));
$scope.actions = [permittedActionWithUrl(actionTemplate1), notPermittedAction()];
$compile(element)($scope);
$scope.$apply();
var element = batchElementFor([
permittedActionWithUrl(actionTemplate1),
notPermittedAction()
]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -159,10 +145,10 @@
var action1 = getTemplatePath('action-create');
var action2 = getTemplatePath('action-delete');
var element = angular.element(getTemplate('actions.row'));
$scope.actions = [permittedActionWithUrl(action1), permittedActionWithUrl(action2)];
$compile(element)($scope);
$scope.$apply();
var element = rowElementFor([
permittedActionWithUrl(action1),
permittedActionWithUrl(action2)
]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -173,13 +159,10 @@
});
it('should have multiple buttons as a dropdown for actions text', function () {
var element = angular.element(getTemplate('actions.row'));
$scope.actions = [
permittedActionWithText('Create Image', 'image'),
permittedActionWithText('Delete Image', 'image', 'text-danger')
];
$compile(element)($scope);
$scope.$apply();
var element = rowElementFor([
permittedActionWithText('Create Image'),
permittedActionWithText('Delete Image', 'text-danger')
]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -190,13 +173,10 @@
});
it('should have one button if only one permitted for dropdown', function () {
var element = angular.element(getTemplate('actions.row'));
$scope.actions = [
var element = rowElementFor([
permittedActionWithUrl(getTemplatePath('action-create')),
notPermittedAction()
];
$compile(element)($scope);
$scope.$apply();
]);
expect(element.children().length).toBe(1);
var actionList = element.find('action-list');
@ -213,11 +193,10 @@
};
}
function permittedActionWithText(text, item, actionClasses) {
function permittedActionWithText(text, actionClasses) {
return {
template: {
text: text,
item: item,
actionClasses: actionClasses
},
permissions: getPermission(true),
@ -225,12 +204,11 @@
};
}
function permittedActionWithType(templateType, text, item) {
function permittedActionWithType(templateType, text) {
return {
template: {
type: templateType,
text: text,
item: item
text: text
},
permissions: getPermission(true),
callback: 'callback'
@ -261,5 +239,33 @@
return deferred.promise;
}
function batchElementFor(actions) {
$scope.actions = function() {
return actions;
};
var element = angular.element(getTemplate('actions.batch'));
$compile(element)($scope);
$scope.$apply();
return element;
}
function rowElementFor(actions) {
$scope.rowItem = rowItem;
$scope.actions = function(item) {
expect(item).toEqual(rowItem);
return actions;
};
var element = angular.element(getTemplate('actions.row'));
$compile(element)($scope);
$scope.$apply();
return element;
}
});
})();

View File

@ -1,2 +1,2 @@
<actions allowed-actions="actions" action-list-type="row">
<actions allowed="actions" type="row" item="rowItem">
</actions>

View File

@ -30,12 +30,12 @@
function actionsService($compile, $http, $q, $templateCache, basePath, $qExtensions) {
return function(spec) {
return createService(spec.scope, spec.element, spec.listType);
return createService(spec.scope, spec.element, spec.listType, spec.item);
};
///////////////
function createService(scope, element, listType) {
function createService(scope, element, listType, item) {
var service = {
renderActions: renderActions
};
@ -157,9 +157,9 @@
/**
* Fetch the HTML Template for the Action
*/
function getTemplate(permittedActionResponse) {
function getTemplate(permittedAction) {
var defered = $q.defer();
var action = permittedActionResponse.context;
var action = permittedAction.context;
$http.get(getTemplateUrl(action), {cache: $templateCache}).then(onTemplateGet);
return defered.promise;
@ -167,7 +167,7 @@
var template = response.data
.replace('$action-classes$', action.template.actionClasses || '')
.replace('$text$', action.template.text)
.replace('$item$', action.template.item);
.replace('$item$', item);
defered.resolve({template: template, callback: action.callback});
}
}