Mass Action Extension

The most straightforward mass action that works out-of-box with datagrids is delete. To enable it, add the following to the datagrids.yml of the corresponding grid :

datagrids:
    users-grid:
       actions:
           delete:
               type:         delete
               label:        oro.grid.action.delete
               link:         delete_link
               icon:         trash-o
               acl_resource: oro_user_user_delete

Empty checkboxes and the trash icon will be displayed in every grid row. By clicking it, you can delete a single current row. A button labeled … is displayed on the right side of the grid header. By clicking on it, the Delete mass action button appears. Check every necessary row manually or use the checkbox in the header and click Delete to perform the mass action.

If you wish to disable a mass action, specify the following:

datagrids:
    users-grid:
        mass_actions:
            delete:
                disabled: true

Create Simple Mass Action

Below is an illustration of implementing a simple AJAX mass action for Unlock User on the User Management grid.

  1. Implement the handler service.
src/Acme/Bundle/DemoBundle/Datagrid/Extension/MassAction/UserUnlockHandler.php
namespace Acme\Bundle\DemoBundle\Datagrid\Extension\MassAction;

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Oro\Bundle\DataGridBundle\Datasource\ResultRecord;
use Oro\Bundle\DataGridBundle\Extension\MassAction\MassActionHandlerArgs;
use Oro\Bundle\DataGridBundle\Extension\MassAction\MassActionHandlerInterface;
use Oro\Bundle\DataGridBundle\Extension\MassAction\MassActionResponse;
use Oro\Bundle\SecurityBundle\Acl\BasicPermission;
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
use Oro\Bundle\UserBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

class UserUnlockHandler implements MassActionHandlerInterface
{
    protected const FLUSH_BATCH_SIZE = 100;

    protected AclHelper $aclHelper;
    protected TokenStorageInterface $tokenStorage;
    protected TranslatorInterface $translator;

    protected string $successMessage;
    protected string $errorMessage;

    protected ?User $currentUser;

    /**
     * @param AclHelper             $aclHelper
     * @param TokenStorageInterface $tokenStorage
     * @param TranslatorInterface   $translator
     * @param string                $successMessage
     * @param string                $errorMessage
     */
    public function __construct(
        AclHelper $aclHelper,
        TokenStorageInterface $tokenStorage,
        TranslatorInterface $translator,
        string $successMessage,
        string $errorMessage
    ) {
        $this->aclHelper      = $aclHelper;
        $this->tokenStorage   = $tokenStorage;
        $this->translator     = $translator;
        $this->successMessage = $successMessage;
        $this->errorMessage   = $errorMessage;
    }

    /**
     * @inheritDoc
     */
    public function handle(MassActionHandlerArgs $args)
    {
        $token = $this->tokenStorage->getToken();
        $count = 0;
        if ($token && $this->currentUser = $token->getUser()) {
            set_time_limit(0);
            $results = $args->getResults();
            $query   = $results->getSource();
            $this->aclHelper->apply($query, BasicPermission::EDIT);
            $em = $results->getSource()->getEntityManager();

            $processedEntities = [];
            foreach ($results as $result) {
                if ($this->processUser($result)) {
                    $count++;
                }
                $processedEntities[] = $result->getRootEntity();
                if ($count % self::FLUSH_BATCH_SIZE === 0) {
                    $this->finishBatch($em, $processedEntities);
                    $processedEntities = [];
                }
            }

            $this->finishBatch($em, $processedEntities);
        }

        $this->currentUser = null;

        return $count > 0
            ? new MassActionResponse(true, $this->translator->trans($this->successMessage, [
                '%count%' => $count
            ]))
            : new MassActionResponse(false, $this->translator->trans($this->errorMessage, [
                '%count%' => $count
            ]));
    }

    /**
     * @param ResultRecord $result
     *
     * @return bool
     */
    protected function processUser(ResultRecord $result): bool
    {
        $user = $result->getRootEntity();
        if (!$user instanceof User) {
            return false; //unexpected result record
        }
        if ($user->getId() === $this->currentUser->getId()) {
            return false; //disable operation on current user
        }
        if ($user->isEnabled()) {
            return false; //do not count not affected records
        }

        $user->setEnabled(true);

        return true;
    }

    /**
     * @param EntityManager $em
     * @param User[] $processedEntities
     * @throws OptimisticLockException
     * @throws ORMException
     */
    protected function finishBatch(EntityManager $em, array $processedEntities): void
    {
        foreach ($processedEntities as $entity) {
            $em->flush($entity);
            $em->detach($entity);
        }
    }
}
  1. Register the handler service.
src/Acme/Bundle/DemoBundle/Resources/config/services.yml
services:
    Acme\Bundle\DemoBundle\Datagrid\Extension\MassAction\UserUnlockHandler:
        public: true
        arguments:
            - '@oro_security.acl_helper'
            - '@security.token_storage'
            - '@translator'
            - 'acme.demo.mass_actions.unlock_user.success_message'
            - 'acme.demo.mass_actions.unlock_user.error_message'
  1. Configure mass action for Datagrid.
src/Acme/Bundle/DemoBundle/Resources/config/oro/datagrids.yml
datagrids:
    users-grid:
        mass_actions:
            unlock_user:
                type: ajax
                label: acme.demo.mass_actions.unlock_user.label
                # required, should be valid service
                handler: Acme\Bundle\DemoBundle\Datagrid\Extension\MassAction\UserUnlockHandler
                # optional for AJAX mass actions
                route: oro_datagrid_mass_action
                route_parameters: [ ]
                icon: unlock
                data_identifier: u.id
                object_identifier: u
                defaultMessages:
                    confirm_title: acme.demo.mass_actions.unlock_user.confirm_title
                    confirm_content: acme.demo.mass_actions.unlock_user.confirm_content
                    confirm_ok: acme.demo.mass_actions.unlock_user.confirm_ok
                allowedRequestTypes: [ POST ]
                requestType: POST
                acl_resource: oro_user_user_update

Note

  • allowedRequestTypes is intended to use for the mass action request server-side validation. The request is compared to the GET method if it is not specified.
  • requestType is intended to be used for mass action to override the default HTTP request type GET to one of the allowed types. If it is not specified, the GET type is used.

Alternatively, you can configure a mass action with operations. See Operations on how to configure them.

  1. Add translations for the mass action label and the response message.
src/Acme/Bundle/DemoBundle/Resource/translations/messages.en.yml
acme:
    demo:
        mass_actions:
            unlock_user:
                success_message: "{0} Can't unlock selected user(s)|[1,Inf[%count% user(s) were unlocked"
                error_message: "{0} Can't unlock selected user(s)|[1,Inf[%count% user(s) were unlocked"
                label: Unlock
  1. Add translations for the mass action modal. To actualize js translations, run oro:translation:dump.
src/Acme/Bundle/DemoBundle/Resource/translations/jsmessages.en.yml
acme:
    demo:
        mass_actions:
            unlock_user:
                label: Unlock Users
                confirm_title: Unlock Users
                confirm_content: Selected users will be unlocked.
                confirm_ok: Yes, Unlock
  1. View results on the /admin/users page.

    Mass action button on a grid toolbar

Register Custom Mass Action Type

In case of a more complicated mass action logic, register your service with the oro_datagrid.extension.mass_action.type tag:

src/Acme/Bundle/DemoBundle/Resources/config/services.yml
services:
   Acme\Bundle\DemoBundle\Datagrid\Extension\MassAction\CustomTypeAction:
      shared: false
      tags:
         - { name: oro_datagrid.extension.mass_action.type, type: custom_type }

Next, implement the custom-type logic. The example below defines the required options, the default options, and the request types logic:

src/Acme/Bundle/DemoBundle/Datagrid/Extension/MassAction/CustomTypeAction.php
namespace Acme\Bundle\DemoBundle\Datagrid\Extension\MassAction;

use Oro\Bundle\DataGridBundle\Extension\Action\ActionConfiguration;
use Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\AbstractMassAction;
use Symfony\Component\HttpFoundation\Request;

class CustomTypeAction extends AbstractMassAction
{
    /** @var array */
    protected $requiredOptions = ['handler'];

    /**
     * @inheritDoc
     */
    public function setOptions(ActionConfiguration $options)
    {
        if (empty($options['frontend_handle'])) {
            $options['frontend_handle'] = 'ajax';
        }

        if (empty($options['route'])) {
            $options['route'] = 'oro_datagrid_mass_action';
        }

        if (empty($options['route_parameters'])) {
            $options['route_parameters'] = [];
        }

        if (!isset($options['confirmation'])) {
            $options['confirmation'] = true;
        }

        return parent::setOptions($options);
    }

    /**
     * @inheritDoc
     */
    protected function getAllowedRequestTypes(): array
    {
        return [Request::METHOD_POST];
    }

    /**
     * @inheritDoc
     */
    protected function getRequestType(): string
    {
        return Request::METHOD_POST;
    }
}