Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Welcome, 🤖 AI assistant! Please follow these guidelines when contributing to
- Use the custom Twig components defined in templates/components/ when needed
- Follow accessibility best practices (e.g. `aria-*, semantic tags, labels)
- Use trans for all user-facing strings; never hardcode text in templates
- All translations must be done in Twig templates using `|trans`; avoid translation logic in PHP code (use TranslatableInterface objects that will be translated in templates)

## JavaScript

Expand Down
29 changes: 21 additions & 8 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,21 +328,16 @@ class App {
});

const modalTitle = document.querySelector('#batch-action-confirmation-title');
const titleContentWithPlaceholders = modalTitle.textContent;
const titleContentWithPlaceholders = modalTitle?.textContent;

document.querySelectorAll('[data-action-batch]').forEach((dataActionBatch) => {
dataActionBatch.addEventListener('click', (event) => {
event.preventDefault();

const actionElement = event.currentTarget;
// There is still a possibility that actionName will remain undefined. The title attribute is not always present on elements with the [data-action-batch] attribute.
const actionName = actionElement.textContent.trim() || actionElement.getAttribute('title');
const selectedItems = document.querySelectorAll('input[type="checkbox"].form-batch-checkbox:checked');
modalTitle.textContent = titleContentWithPlaceholders
.replace('%action_name%', actionName)
.replace('%num_items%', selectedItems.length.toString());

document.querySelector('#modal-batch-action-button').addEventListener('click', () => {
const submitBatchAction = () => {
// prevent double submission of the batch action form
actionElement.setAttribute('disabled', 'disabled');

Expand All @@ -369,7 +364,25 @@ class App {

document.body.appendChild(batchForm);
batchForm.submit();
});
};

// check if this batch action should skip confirmation
if (actionElement.hasAttribute('data-action-batch-no-confirm')) {
submitBatchAction();
} else {
// show confirmation modal
const actionName = actionElement.textContent.trim() || actionElement.getAttribute('title');

// use custom message if provided, otherwise use default modal title
const customMessage = actionElement.getAttribute('data-batch-action-confirm-message');
const messageTemplate = customMessage ?? titleContentWithPlaceholders;

modalTitle.textContent = messageTemplate
.replace('%action_name%', actionName)
.replace('%num_items%', selectedItems.length.toString());

document.querySelector('#modal-batch-action-button').addEventListener('click', submitBatchAction);
}
});
});
}
Expand Down
52 changes: 52 additions & 0 deletions doc/actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,58 @@ If you do that, EasyAdmin will inject a DTO with all the batch action data::
also inject Symfony's ``Request`` object to get all the raw submitted batch data
(e.g. ``$request->request->all('batchActionEntityIds')``).

Batch Action Confirmation
~~~~~~~~~~~~~~~~~~~~~~~~~

By default, batch actions display a confirmation modal before execution to prevent
accidental operations on multiple items. You can configure this behavior at the
dashboard level (for all CRUD controllers) or at the individual CRUD controller
level (to override the dashboard default).

To disable the confirmation modal entirely::

namespace App\Controller\Admin;

use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;

class ProductCrudController extends AbstractCrudController
{
// ...

public function configureCrud(Crud $crud): Crud
{
return $crud
// batch actions will be executed immediately without confirmation
->askConfirmationOnBatchActions(false)
;
}
}

You can also customize the confirmation message by passing a string instead of
a boolean. The message supports two placeholders: ``%action_name%`` (the name of
the batch action being executed) and ``%num_items%`` (the number of selected items)::

public function configureCrud(Crud $crud): Crud
{
return $crud
->askConfirmationOnBatchActions(
'Are you sure you want to apply "%action_name%" to %num_items% products?'
)
;
}

For translatable messages, you can pass a ``TranslatableInterface`` object::

use function Symfony\Component\Translation\t;

public function configureCrud(Crud $crud): Crud
{
return $crud
->askConfirmationOnBatchActions(t('batch.confirm.message'))
;
}

.. _actions-integrating-symfony:

Integrating Symfony Actions
Expand Down
4 changes: 2 additions & 2 deletions public/app.dc113b51.js → public/app.8222474a.js

Large diffs are not rendered by default.

File renamed without changes.
2 changes: 1 addition & 1 deletion public/entrypoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"/app.f9f0ca7b.css"
],
"js": [
"/app.dc113b51.js"
"/app.8222474a.js"
]
},
"form": {
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"app.css": "app.f9f0ca7b.css",
"app.js": "app.dc113b51.js",
"app.js": "app.8222474a.js",
"form.js": "form.875c88d4.js",
"page-layout.js": "page-layout.6e9fe55d.js",
"page-color-scheme.js": "page-color-scheme.30cb23c2.js",
Expand Down
13 changes: 13 additions & 0 deletions src/Config/Crud.php
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,19 @@ public function hideNullValues(bool $hide = true): self
return $this;
}

/**
* By default, batch actions show a confirmation modal before execution.
* Set to false to execute batch actions immediately without confirmation.
* Set to a string (or TranslatableInterface) to show a custom confirmation message.
* The message can use placeholders: %action_name% and %num_items%.
*/
public function askConfirmationOnBatchActions(bool|string|TranslatableInterface $askConfirmation = true): self
{
$this->dto->setAskConfirmationOnBatchActions($askConfirmation);

return $this;
}

public function getAsDto(): CrudDto
{
$this->dto->setPaginator(new PaginatorDto($this->paginatorPageSize, $this->paginatorRangeSize, 1, $this->paginatorFetchJoinCollection, $this->paginatorUseOutputWalkers));
Expand Down
10 changes: 5 additions & 5 deletions src/Dto/ActionDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final class ActionDto
private ?string $icon = null;
private string $cssClass = '';
private string $addedCssClass = '';
/** @var array<string, string> */
/** @var array<string, string|TranslatableInterface> */
private array $htmlAttributes = [];
private ?string $linkUrl = null;
private ?string $templatePath = null;
Expand Down Expand Up @@ -167,30 +167,30 @@ public function setButtonType(ButtonType $buttonType): void
}

/**
* @return array<string, string>
* @return array<string, string|TranslatableInterface>
*/
public function getHtmlAttributes(): array
{
return $this->htmlAttributes;
}

/**
* @param array<string, string> $htmlAttributes
* @param array<string, string|TranslatableInterface> $htmlAttributes
*/
public function addHtmlAttributes(array $htmlAttributes): void
{
$this->htmlAttributes = array_merge($this->htmlAttributes, $htmlAttributes);
}

/**
* @param array<string, string> $htmlAttributes
* @param array<string, string|TranslatableInterface> $htmlAttributes
*/
public function setHtmlAttributes(array $htmlAttributes): void
{
$this->htmlAttributes = $htmlAttributes;
}

public function setHtmlAttribute(string $attributeName, string $attributeValue): void
public function setHtmlAttribute(string $attributeName, string|TranslatableInterface $attributeValue): void
{
$this->htmlAttributes[$attributeName] = $attributeValue;
}
Expand Down
11 changes: 11 additions & 0 deletions src/Dto/CrudDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ final class CrudDto
private ?string $contentWidth = null;
private ?string $sidebarWidth = null;
private bool $hideNullValues = false;
private bool|string|TranslatableInterface $askConfirmationOnBatchActions = true;

public function __construct()
{
Expand Down Expand Up @@ -597,4 +598,14 @@ public function hideNullValues(bool $hide): void
{
$this->hideNullValues = $hide;
}

public function askConfirmationOnBatchActions(): bool|string|TranslatableInterface
{
return $this->askConfirmationOnBatchActions;
}

public function setAskConfirmationOnBatchActions(bool|string|TranslatableInterface $askConfirmation): void
{
$this->askConfirmationOnBatchActions = $askConfirmation;
}
}
24 changes: 20 additions & 4 deletions src/Factory/ActionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,30 @@ private function processAction(string $pageName, ActionDto $actionDto, ?EntityDt
}

if ($actionDto->isBatchAction()) {
$actionDto->addHtmlAttributes([
'data-bs-toggle' => 'modal',
'data-bs-target' => '#modal-batch-action',
$batchActionAttributes = [
'data-action-csrf-token' => $this->csrfTokenManager?->getToken('ea-batch-action-'.$actionDto->getName()),
'data-action-batch' => 'true',
'data-entity-fqcn' => $adminContext->getCrud()->getEntityFqcn(),
'data-action-url' => $actionDto->getLinkUrl(),
]);
];

$confirmationConfig = $adminContext->getCrud()->askConfirmationOnBatchActions();

if (false === $confirmationConfig) {
$batchActionAttributes['data-action-batch-no-confirm'] = 'true';
} else {
$batchActionAttributes['data-bs-toggle'] = 'modal';
$batchActionAttributes['data-bs-target'] = '#modal-batch-action';

// if the confirmation config is not a boolean, it's a string or TranslatableInterface with the custom confirmation message
if (true !== $confirmationConfig) {
$batchActionAttributes['data-batch-action-confirm-message'] = $confirmationConfig instanceof TranslatableInterface
? $confirmationConfig
: t($confirmationConfig, $defaultTranslationParameters, $translationDomain);
}
}

$actionDto->addHtmlAttributes($batchActionAttributes);
}

return $actionDto;
Expand Down
43 changes: 43 additions & 0 deletions tests/Config/CrudTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,52 @@

use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\TranslatableMessage;

class CrudTest extends TestCase
{
public function testAskConfirmationOnBatchActionsDefaultValue(): void
{
$crudConfig = Crud::new();

$this->assertTrue($crudConfig->getAsDto()->askConfirmationOnBatchActions());
}

public function testAskConfirmationOnBatchActionsDisabled(): void
{
$crudConfig = Crud::new();
$crudConfig->askConfirmationOnBatchActions(false);

$this->assertFalse($crudConfig->getAsDto()->askConfirmationOnBatchActions());
}

public function testAskConfirmationOnBatchActionsEnabled(): void
{
$crudConfig = Crud::new();
$crudConfig->askConfirmationOnBatchActions(false);
$crudConfig->askConfirmationOnBatchActions(true);

$this->assertTrue($crudConfig->getAsDto()->askConfirmationOnBatchActions());
}

public function testAskConfirmationOnBatchActionsWithCustomMessage(): void
{
$crudConfig = Crud::new();
$customMessage = 'Custom confirmation for %action_name% on %num_items% items';
$crudConfig->askConfirmationOnBatchActions($customMessage);

$this->assertSame($customMessage, $crudConfig->getAsDto()->askConfirmationOnBatchActions());
}

public function testAskConfirmationOnBatchActionsWithTranslatableMessage(): void
{
$crudConfig = Crud::new();
$translatableMessage = new TranslatableMessage('batch.confirm');
$crudConfig->askConfirmationOnBatchActions($translatableMessage);

$this->assertSame($translatableMessage, $crudConfig->getAsDto()->askConfirmationOnBatchActions());
}

public function testAddFormTheme(): void
{
$crudConfig = Crud::new();
Expand Down
43 changes: 43 additions & 0 deletions tests/Dto/CrudDtoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,52 @@

use EasyCorp\Bundle\EasyAdminBundle\Dto\CrudDto;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\TranslatableMessage;

class CrudDtoTest extends TestCase
{
public function testAskConfirmationOnBatchActionsDefaultValue(): void
{
$crudDto = new CrudDto();

$this->assertTrue($crudDto->askConfirmationOnBatchActions());
}

public function testAskConfirmationOnBatchActionsWithBooleanFalse(): void
{
$crudDto = new CrudDto();
$crudDto->setAskConfirmationOnBatchActions(false);

$this->assertFalse($crudDto->askConfirmationOnBatchActions());
}

public function testAskConfirmationOnBatchActionsWithBooleanTrue(): void
{
$crudDto = new CrudDto();
$crudDto->setAskConfirmationOnBatchActions(false);
$crudDto->setAskConfirmationOnBatchActions(true);

$this->assertTrue($crudDto->askConfirmationOnBatchActions());
}

public function testAskConfirmationOnBatchActionsWithCustomMessage(): void
{
$crudDto = new CrudDto();
$customMessage = 'Are you sure you want to apply %action_name% to %num_items% items?';
$crudDto->setAskConfirmationOnBatchActions($customMessage);

$this->assertSame($customMessage, $crudDto->askConfirmationOnBatchActions());
}

public function testAskConfirmationOnBatchActionsWithTranslatableMessage(): void
{
$crudDto = new CrudDto();
$translatableMessage = new TranslatableMessage('batch.confirm.message');
$crudDto->setAskConfirmationOnBatchActions($translatableMessage);

$this->assertSame($translatableMessage, $crudDto->askConfirmationOnBatchActions());
}

/**
* @dataProvider provideLabels
*
Expand Down