Merge "Allow specifying item to use for actions in the actions directive"
This commit is contained in:
commit
54495c3185
@ -1,2 +1,2 @@
|
||||
<actions allowed-actions="actions" action-list-type="batch">
|
||||
<actions allowed="actions" type="batch">
|
||||
</actions>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
})();
|
||||
|
@ -1,2 +1,2 @@
|
||||
<actions allowed-actions="actions" action-list-type="row">
|
||||
<actions allowed="actions" type="row" item="rowItem">
|
||||
</actions>
|
||||
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user