horizon/horizon/static/framework/widgets/action-list/actions.directive.js

310 lines
12 KiB
JavaScript

/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
angular
.module('horizon.framework.widgets.action-list')
.directive('actions', actions);
actions.$inject = [
'$parse',
'horizon.framework.widgets.action-list.actions.service'
];
/**
* @ngdoc directive
* @name horizon.framework.widgets.action-list.directive:actions
* @element
* @description
* 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 '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' or 'detail' type.
*
* @param {function} result-handler
* (Optional) A function that is called with the return value from a clicked actions perform
* function. Ideally the action perform function returns a promise that resolves to some data
* on success, but it may return just data, or no return at all, depending on the specific action
* implementation. It is recommended to use the actionResultService to manage the results of your
* actions, and also to have them generate results which are more broadly usable than a custom
* result value.
*
* @param {function} allowed
* Returns an array of actions that can be performed on the item(s).
*
* This is an array that should contain objects with the following properties:
* {
* template: <template object - described below>,
* service: <service to use - described below>
* }
*
* template: the Template used for the Action Button.
* It is an object that can be any of
* 1. url: <full_path_to_template.html>
* This allows for specification of the template for the action button.
* Use this option for complete extensibility and control over what is rendered.
* The directive will be responsible for binding the callback but not for styling the button.
* The template should include the 'item' attribute for the 'action' button,
* if the action needs an item to act upon even for 'row' type. Specifying an 'item' other
* than the current row 'item' is supported'.
*
* The 'scope' in use for the 'actions' directive can be used in the custom template.
*
* Refer to tests that exercise this functionality with some sample templates at
* - 'actions.custom.mock.html' and 'actions.custom.mock2.html'.
*
* 2. type: '<action_button_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' 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.
*
* The styling of the action button is done based on the 'listType'.
* The directive will be responsible for binding the correct callback.
*
* 3. text: 'text', actionClasses: 'custom-classes'
* This creates an unstyled button with the given text.
* 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 defined. Both of these
* functions take two arguments: the relevant item or items (if any) and the angular
* scope in which the action button appears.
*
* 1. allowed(item[s], scope): 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' or 'detail' type, the current 'item' will be passed to the function.
* - When using 'batch' type, the item argument will be undefined.
*
* 2. perform(item[s], scope): is what gets called when the button is clicked. It is also
* expected to return a promise that resolves when the action completes.
*
* - When using 'row' or 'detail' type, the current 'item' is evaluated and passed
* to the function.
* - When using 'batch' type, 'item' will be undefined.
* - When using 'delete-selected' for 'batch' type, all selected rows are passed as
* an array.
*
* There is a third optional function, initAction (which was previously called initScope)
* Actions may perform post-config (in the angular sense) initialization by
* providing an initAction method. This might be typically invoked by initActions()
* on a ResourceType. Actions should not perform blocking operations in their
* construction, for example API calls, because as injectables their constructor
* is run during injection, meaning those calls would be executed as the module
* is initialized. This would mean those calls would be blocking on any
* Angular context initialization, such as going to the login page.
*
*
* @restrict E
* @scope
* @example
*
* batch:
*
* Create the services that will implement the actions.
* Each service must have an allowed function and a perform function.
*
* var batchDeleteService = {
* allowed: function() {
* // This function ignores the "items" and scope arguments because
* // they're not needed for this action.
* return policy.ifAllowed({ rules: [['image', 'delete_image']] });
* },
* perform: function(images) {
* // This function ignores the second scope argument because there's
* // no context information interesting to this action.
* return $q.all(images.map(function(image){
* return glanceAPI.deleteImage(image.id);
* }));
* }
* };
*
* In the following example we also send off an async check that the image
* service is enabled, the resultant promise being checked in the allowed
* function. This saves us checking that enabled flag every time allowed
* is executed.
*
* var createService = {
* allowed: function(ignored, scope) {
* // This function may use the information in the scope to check whether
* // an image may be created in a certain context.
* // We ignore the undefined "item" value passed in because as a "batch"
* // action undefined will be passed in.
* return imageServiceEnabledPromise;
* },
* perform: function(ignored, scope) {
* // Open the modal to create volume and return the modal's result promise.
* // This function may use the information in the scope to create the
* // image in a certain context. Again, we ignore the undefined "item"
* // value passed in.
* },
* initAction: function() {
* imageServiceEnabledPromise = serviceCatalog.ifTypeEnabled('image');
* }
* };
*
* Then create the Service to use in the HTML which lists
* all allowed actions with the templates to use.
*
* function actions() {
* return [{
* template: {
* type: 'delete-selected',
* text: gettext('Delete Images')
* },
* service: batchDeleteService
* }, {
* template: {
* type: 'create',
* text: gettext('Create Image')
* },
* service: createService
* }];
* }
*
* Finally, in your HTML, reference the "actions" function and pass
* in the list of actions that will be allowed.
*
* ```
* <actions allowed="actions" type="batch" result-handler="onResult">
* </actions>
* ```
*
* row:
*
* Create the services that will implement the actions.
* Each service must have an allowed function and a perform function.
*
* var deleteService = {
* allowed: function(image) {
* return $q.all([
* notProtected(image),
* policy.ifAllowed({ rules: [['image', 'delete_image']] }),
* ownedByUser(image),
* notDeleted(image)
* ]);
* },
* perform: function(image) {
* return glanceAPI.deleteImage(image.id);
* }
* };
*
* var createVolumeService = {
* allowed: function(image) {
* return createVolumeFromImagePermitted(image);
* },
* perform: function(image) {
* //open the modal to create volume and return the modal's result promise
* }
* };
*
* var downloadService = {
* allowed: function(image) {
* return isPublic(image);
* },
* perform: function(image) {
* return generateUrlFor(image);
* }
* };
*
* Then create the Service to use in the HTML which lists
* all allowed actions with the templates to use.
*
* function actions(image) {
* return [{
* template: {
* text: gettext('Delete Image'),
* type: 'delete'
* },
* service: deleteService
* }, {
* template: {
* text: gettext('Create Volume')
* },
* service: createVolumeService
* }];
* }
*
* Finally, in your HTML, reference the "actions" function and pass
* in the list of actions that will be allowed.
*
* ```
* <actions allowed="actions" type="row" item="image" result-handler="onResult">
* </actions>
*
* ```
*
* 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,
actionsService
) {
var directive = {
link: link,
restrict: 'E',
scope: true,
controller: 'horizon.framework.widgets.action-list.ActionsController as actionsCtrl'
};
return directive;
function link(scope, element, attrs, actionsController) {
var listType = attrs.type;
var item = attrs.item;
var allowedActions;
var resultHandler = $parse(attrs.resultHandler)(scope);
var actionsParam = $parse(attrs.allowed)(scope);
if (angular.isFunction(actionsParam)) {
allowedActions = actionsParam();
} else {
allowedActions = actionsParam;
}
var service = actionsService({
scope: scope,
element: element,
ctrl: actionsController,
listType: listType,
item: item,
resultHandler: resultHandler
});
service.renderActions(allowedActions);
}
}
})();