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>
|
</actions>
|
||||||
|
@ -35,71 +35,111 @@
|
|||||||
*
|
*
|
||||||
* Attributes:
|
* Attributes:
|
||||||
*
|
*
|
||||||
* allowedActions: actions allowed that can be displayed
|
* @param {string} type
|
||||||
* actionListType: allow the buttons to be shown as a list or doropdown
|
* 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.
|
* @param {string=} item
|
||||||
* It's an array of objects of the form:
|
* The item to pass to the callback when using 'row' type.
|
||||||
* { template: {}, permissions: <promise to determine permissions>, callback: 'callback'}
|
* 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.
|
* This is an array that should contain objects with the following properties:
|
||||||
* Use this for complete extensibility and control over what is rendered.
|
* {
|
||||||
* The template will be responsible for binding the callback and styling.
|
* 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.
|
* template: is an object that can be any of
|
||||||
* Currently supported values are 'delete', 'delete-selected' and 'create'.
|
* 1. url: <full_path_to_template.html>
|
||||||
* `item` is optional and if provided will be used in the callback.
|
* This specifies the location of the template for the action button.
|
||||||
* The styling and binding of the callback is done by the template.
|
* 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.
|
* 2. type: '<action_button_type>'
|
||||||
* `item` is optional and if provided will be used in the callback.
|
* This uses a known action button type.
|
||||||
* The styling of the button will vary per the `actionListType` chosen.
|
* Currently supported values are
|
||||||
* For custom styling of the button, `actionClasses` can be included in
|
* 1. 'delete' - Delete a single row. Only for 'row' type.
|
||||||
* the template.
|
* 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
|
* The styling and binding of the callback is done by the template.
|
||||||
* if permitted and is rejected if not.
|
|
||||||
* `callback` is the method to call when the button is clicked.
|
|
||||||
*
|
*
|
||||||
* `actionListType` can be only be `row` or `batch`
|
* 3. text: 'text', actionClasses: 'custom-classes'
|
||||||
* `batch` is rendered as buttons, `row` is rendered as dropdown menu.
|
* 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
|
* @restrict E
|
||||||
* @scope
|
* @scope
|
||||||
* @example
|
* @example
|
||||||
*
|
*
|
||||||
* $scope.actions = [{
|
* batch:
|
||||||
* template: {
|
*
|
||||||
* text: gettext('Delete Image'),
|
* function actions() {
|
||||||
* type: 'delete',
|
* return [{
|
||||||
* item: 'image'
|
* callback: 'table.batchActions.delete.open',
|
||||||
* },
|
* template: {
|
||||||
* permissions: policy.ifAllowed({ rules: [['image', 'delete_image']] }),
|
* type: 'delete-selected',
|
||||||
* callback: deleteModalService.open
|
* text: gettext('Delete Images')
|
||||||
* }, {
|
* },
|
||||||
* template: {
|
* permissions: policy.ifAllowed({ rules: [['image', 'delete_image']] })
|
||||||
* text: gettext('Create Volume'),
|
* }, {
|
||||||
* item: 'image'
|
* callback: 'table.batchActions.create.open',
|
||||||
* },
|
* template: {
|
||||||
* permissions: policy.ifAllowed({rules: [['volume', 'volume:create']]}),
|
* type: 'create',
|
||||||
* callback: createVolumeModalService.open
|
* text: gettext('Create Image')
|
||||||
* }, {
|
* },
|
||||||
* template: {
|
* permissions: policy.ifAllowed({ rules: [['image', 'add_image']] })
|
||||||
* url: basePath + 'actions/my-custom-action.html'
|
* }];
|
||||||
* },
|
* }
|
||||||
* permissions: policy.ifAllowed({ rules: [['image', 'custom']] }),
|
|
||||||
* callback: customModalService.open
|
|
||||||
* }]
|
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* <actions allowed-actions="actions" action-list-type="row">
|
* <actions allowed="actions" type="batch">
|
||||||
* </actions>
|
|
||||||
*
|
|
||||||
* <actions allowed-actions="actions" action-list-type="batch">
|
|
||||||
* </actions>
|
* </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(
|
function actions(
|
||||||
$parse,
|
$parse,
|
||||||
@ -114,11 +154,21 @@
|
|||||||
return directive;
|
return directive;
|
||||||
|
|
||||||
function link(scope, element, attrs) {
|
function link(scope, element, attrs) {
|
||||||
var listType = attrs.actionListType;
|
var listType = attrs.type;
|
||||||
var allowedActions = $parse(attrs.allowedActions)(scope);
|
var item = attrs.item;
|
||||||
|
var service = actionsService({
|
||||||
actionsService({scope: scope, element: element, listType: listType})
|
scope: scope,
|
||||||
.renderActions(allowedActions);
|
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 () {
|
describe('actions directive', function () {
|
||||||
var $scope, $compile, $q, $templateCache, basePath;
|
var $scope, $compile, $q, $templateCache, basePath;
|
||||||
|
|
||||||
|
var rowItem = {id: 1};
|
||||||
|
|
||||||
beforeEach(module('templates'));
|
beforeEach(module('templates'));
|
||||||
beforeEach(module('horizon.framework'));
|
beforeEach(module('horizon.framework'));
|
||||||
|
|
||||||
@ -29,18 +31,12 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should have no buttons if there are no actions', function () {
|
it('should have no buttons if there are no actions', function () {
|
||||||
var element = angular.element(getTemplate('actions.batch'));
|
var element = batchElementFor([]);
|
||||||
$scope.actions = [];
|
|
||||||
$compile(element)($scope);
|
|
||||||
$scope.$apply();
|
|
||||||
expect(element.children().length).toBe(0);
|
expect(element.children().length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow for specifying action text', function () {
|
it('should allow for specifying action text', function () {
|
||||||
var element = angular.element(getTemplate('actions.batch'));
|
var element = batchElementFor([permittedActionWithText('Create Image')]);
|
||||||
$scope.actions = [permittedActionWithText('Create Image', 'image')];
|
|
||||||
$compile(element)($scope);
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -52,10 +48,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow for specifying by template for create', function () {
|
it('should allow for specifying by template for create', function () {
|
||||||
var element = angular.element(getTemplate('actions.batch'));
|
var element = batchElementFor([permittedActionWithType('create', 'Create Image')]);
|
||||||
$scope.actions = [permittedActionWithType('create', 'Create Image')];
|
|
||||||
$compile(element)($scope);
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -67,10 +60,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow for specifying by template for delete-selected', function () {
|
it('should allow for specifying by template for delete-selected', function () {
|
||||||
var element = angular.element(getTemplate('actions.batch'));
|
var element = batchElementFor([permittedActionWithType('delete-selected', 'Delete Images')]);
|
||||||
$scope.actions = [permittedActionWithType('delete-selected', 'Delete Images')];
|
|
||||||
$compile(element)($scope);
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -83,10 +73,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow for specifying by template for delete', function () {
|
it('should allow for specifying by template for delete', function () {
|
||||||
var element = angular.element(getTemplate('actions.row'));
|
$scope.callback = function(item) {
|
||||||
$scope.actions = [permittedActionWithType('delete', 'Delete Image')];
|
expect(item).toEqual(rowItem);
|
||||||
$compile(element)($scope);
|
};
|
||||||
$scope.$apply();
|
spyOn($scope, 'callback').and.callThrough();
|
||||||
|
|
||||||
|
var element = rowElementFor([permittedActionWithType('delete', 'Delete Image')]);
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -95,15 +87,14 @@
|
|||||||
expect(actionList.find('button').attr('class')).toEqual('text-danger');
|
expect(actionList.find('button').attr('class')).toEqual('text-danger');
|
||||||
expect(actionList.find('button').attr('ng-click')).toEqual('disabled || callback(item)');
|
expect(actionList.find('button').attr('ng-click')).toEqual('disabled || callback(item)');
|
||||||
expect(actionList.text().trim()).toEqual('Delete Image');
|
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 () {
|
it('should have one button if there is one action', function () {
|
||||||
var action = getTemplatePath('action-create', getTemplate());
|
var action = getTemplatePath('action-create', getTemplate());
|
||||||
|
var element = batchElementFor([permittedActionWithUrl(action)]);
|
||||||
var element = angular.element(getTemplate('actions.batch'));
|
|
||||||
$scope.actions = [permittedActionWithUrl(action)];
|
|
||||||
$compile(element)($scope);
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -115,10 +106,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have no buttons if not permitted', function () {
|
it('should have no buttons if not permitted', function () {
|
||||||
var element = angular.element(getTemplate('actions.batch'));
|
var element = batchElementFor([notPermittedAction()]);
|
||||||
$scope.actions = [notPermittedAction()];
|
|
||||||
$compile(element)($scope);
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(0);
|
expect(element.children().length).toBe(0);
|
||||||
});
|
});
|
||||||
@ -126,11 +114,10 @@
|
|||||||
it('should have multiple buttons for multiple actions as a list', function () {
|
it('should have multiple buttons for multiple actions as a list', function () {
|
||||||
var action1 = getTemplatePath('action-create');
|
var action1 = getTemplatePath('action-create');
|
||||||
var action2 = getTemplatePath('action-delete');
|
var action2 = getTemplatePath('action-delete');
|
||||||
|
var element = batchElementFor([
|
||||||
var element = angular.element(getTemplate('actions.batch'));
|
permittedActionWithUrl(action1),
|
||||||
$scope.actions = [permittedActionWithUrl(action1), permittedActionWithUrl(action2)];
|
permittedActionWithUrl(action2)
|
||||||
$compile(element)($scope);
|
]);
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(2);
|
expect(element.children().length).toBe(2);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -142,11 +129,10 @@
|
|||||||
|
|
||||||
it('should have as many buttons as permitted', function () {
|
it('should have as many buttons as permitted', function () {
|
||||||
var actionTemplate1 = getTemplatePath('action-create');
|
var actionTemplate1 = getTemplatePath('action-create');
|
||||||
|
var element = batchElementFor([
|
||||||
var element = angular.element(getTemplate('actions.batch'));
|
permittedActionWithUrl(actionTemplate1),
|
||||||
$scope.actions = [permittedActionWithUrl(actionTemplate1), notPermittedAction()];
|
notPermittedAction()
|
||||||
$compile(element)($scope);
|
]);
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -159,10 +145,10 @@
|
|||||||
var action1 = getTemplatePath('action-create');
|
var action1 = getTemplatePath('action-create');
|
||||||
var action2 = getTemplatePath('action-delete');
|
var action2 = getTemplatePath('action-delete');
|
||||||
|
|
||||||
var element = angular.element(getTemplate('actions.row'));
|
var element = rowElementFor([
|
||||||
$scope.actions = [permittedActionWithUrl(action1), permittedActionWithUrl(action2)];
|
permittedActionWithUrl(action1),
|
||||||
$compile(element)($scope);
|
permittedActionWithUrl(action2)
|
||||||
$scope.$apply();
|
]);
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -173,13 +159,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have multiple buttons as a dropdown for actions text', function () {
|
it('should have multiple buttons as a dropdown for actions text', function () {
|
||||||
var element = angular.element(getTemplate('actions.row'));
|
var element = rowElementFor([
|
||||||
$scope.actions = [
|
permittedActionWithText('Create Image'),
|
||||||
permittedActionWithText('Create Image', 'image'),
|
permittedActionWithText('Delete Image', 'text-danger')
|
||||||
permittedActionWithText('Delete Image', 'image', 'text-danger')
|
]);
|
||||||
];
|
|
||||||
$compile(element)($scope);
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -190,13 +173,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have one button if only one permitted for dropdown', function () {
|
it('should have one button if only one permitted for dropdown', function () {
|
||||||
var element = angular.element(getTemplate('actions.row'));
|
var element = rowElementFor([
|
||||||
$scope.actions = [
|
|
||||||
permittedActionWithUrl(getTemplatePath('action-create')),
|
permittedActionWithUrl(getTemplatePath('action-create')),
|
||||||
notPermittedAction()
|
notPermittedAction()
|
||||||
];
|
]);
|
||||||
$compile(element)($scope);
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(element.children().length).toBe(1);
|
expect(element.children().length).toBe(1);
|
||||||
var actionList = element.find('action-list');
|
var actionList = element.find('action-list');
|
||||||
@ -213,11 +193,10 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function permittedActionWithText(text, item, actionClasses) {
|
function permittedActionWithText(text, actionClasses) {
|
||||||
return {
|
return {
|
||||||
template: {
|
template: {
|
||||||
text: text,
|
text: text,
|
||||||
item: item,
|
|
||||||
actionClasses: actionClasses
|
actionClasses: actionClasses
|
||||||
},
|
},
|
||||||
permissions: getPermission(true),
|
permissions: getPermission(true),
|
||||||
@ -225,12 +204,11 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function permittedActionWithType(templateType, text, item) {
|
function permittedActionWithType(templateType, text) {
|
||||||
return {
|
return {
|
||||||
template: {
|
template: {
|
||||||
type: templateType,
|
type: templateType,
|
||||||
text: text,
|
text: text
|
||||||
item: item
|
|
||||||
},
|
},
|
||||||
permissions: getPermission(true),
|
permissions: getPermission(true),
|
||||||
callback: 'callback'
|
callback: 'callback'
|
||||||
@ -261,5 +239,33 @@
|
|||||||
return deferred.promise;
|
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>
|
</actions>
|
||||||
|
@ -30,12 +30,12 @@
|
|||||||
|
|
||||||
function actionsService($compile, $http, $q, $templateCache, basePath, $qExtensions) {
|
function actionsService($compile, $http, $q, $templateCache, basePath, $qExtensions) {
|
||||||
return function(spec) {
|
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 = {
|
var service = {
|
||||||
renderActions: renderActions
|
renderActions: renderActions
|
||||||
};
|
};
|
||||||
@ -157,9 +157,9 @@
|
|||||||
/**
|
/**
|
||||||
* Fetch the HTML Template for the Action
|
* Fetch the HTML Template for the Action
|
||||||
*/
|
*/
|
||||||
function getTemplate(permittedActionResponse) {
|
function getTemplate(permittedAction) {
|
||||||
var defered = $q.defer();
|
var defered = $q.defer();
|
||||||
var action = permittedActionResponse.context;
|
var action = permittedAction.context;
|
||||||
$http.get(getTemplateUrl(action), {cache: $templateCache}).then(onTemplateGet);
|
$http.get(getTemplateUrl(action), {cache: $templateCache}).then(onTemplateGet);
|
||||||
return defered.promise;
|
return defered.promise;
|
||||||
|
|
||||||
@ -167,7 +167,7 @@
|
|||||||
var template = response.data
|
var template = response.data
|
||||||
.replace('$action-classes$', action.template.actionClasses || '')
|
.replace('$action-classes$', action.template.actionClasses || '')
|
||||||
.replace('$text$', action.template.text)
|
.replace('$text$', action.template.text)
|
||||||
.replace('$item$', action.template.item);
|
.replace('$item$', item);
|
||||||
defered.resolve({template: template, callback: action.callback});
|
defered.resolve({template: template, callback: action.callback});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user