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 @@ -84,6 +84,7 @@ Welcome, 🤖 AI assistant! Please follow these guidelines when contributing to
- Services configuration must use PHP format (`config/services.php`)
- Translations must be in PHP format (`translations/*.php`)
- Handle exceptions explicitly and avoid silent catch blocks
- In tests, use simple descriptive names like 'Action 1', 'Action 2', 'Group 1', etc. instead of realistic examples

## Twig Templates

Expand Down
26 changes: 17 additions & 9 deletions assets/css/easyadmin-theme/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -575,12 +575,15 @@ a.user-menu-wrapper .user-details:hover {
.content-header .datagrid-filters {
margin-inline-end: 10px;
}
.content-header .global-actions,
.content-header .page-actions {
justify-content: right;
flex-wrap: wrap;
row-gap: 1em;
gap: 10px;
display: flex;
flex-direction: row;
}
.content-header .page-actions {
margin: 10px 0 15px;
}
@media (min-width: 768px) {
Expand All @@ -592,14 +595,6 @@ a.user-menu-wrapper .user-details:hover {
display: none;
}

.content-header .page-actions :is(.btn, form:has(.btn)) + :is(.btn, form:has(.btn)) {
margin-inline-start: 10px;
}

.content-header .page-actions .btn-group .btn + .btn {
margin-inline-start: 0;
}

.batch-actions form {
display: flex;
}
Expand Down Expand Up @@ -718,6 +713,9 @@ a.user-menu-wrapper .user-details:hover {
max-inline-size: 240px;
padding: 5px;
}
.dropdown-menu.dropdown-has-submenus {
padding-inline-start: 25px;
}
.dropdown-menu li {
border-radius: var(--border-radius);
}
Expand Down Expand Up @@ -785,6 +783,16 @@ a.user-menu-wrapper .user-details:hover {
outline: none;
padding: 0 4px;
}
.dropdown-menu .dropdown-submenu .dropdown-item.dropdown-toggle {
border: 0;
display: flex;
padding: 0 12px 0 6px;
position: relative;
}
.dropdown-menu .dropdown-submenu .dropdown-item.dropdown-toggle:not(.dropdown-toggle-split):hover {
/* unlike the toggle split, this dropdown item is not clickable, so make that very clear */
cursor: default;
}

.list-pagination {
background: var(--table-footer-bg);
Expand Down
43 changes: 9 additions & 34 deletions assets/css/easyadmin-theme/buttons.css
Original file line number Diff line number Diff line change
Expand Up @@ -261,43 +261,18 @@ fieldset:disabled a.btn {
}

/* Button groups */
.btn-group {
display: inline-flex;
position: relative;
vertical-align: middle;
}

.btn-group > .btn {
flex: 1 1 auto;
position: relative;
.btn-group > .btn-group:not(:first-child),
.btn-group > :not(.btn-check:first-child) + .btn {
margin-inline-start: 0;
}

.btn-group > .btn:not(:first-child) {
border-start-start-radius: 0;
border-end-start-radius: 0;
.btn-group > .btn.dropdown-toggle.dropdown-toggle-split {
margin-inline-start: -1px;
padding-inline-end: .5625rem;
padding-inline-start: .5625rem;
}

.btn-group > .btn:not(:last-child) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}

/* Dropdown toggle for split buttons */
.btn-group > .dropdown-toggle-split {
--button-padding-x: calc(var(--button-padding-x-md) * 0.75);
}

.btn-group > .btn-sm + .dropdown-toggle-split {
--button-padding-x: calc(var(--button-padding-x-sm) * 0.75);
}

.btn-group > .btn-lg + .dropdown-toggle-split {
--button-padding-x: calc(var(--button-padding-x-lg) * 0.75);
}

.dropdown-toggle-split::after {
margin-inline-start: 0;
.dropdown-menu .dropdown-submenu .dropdown-toggle.dropdown-toggle-split {
border: 0;
inline-size: auto;
}

/* Block buttons (full width) */
Expand Down
72 changes: 69 additions & 3 deletions assets/css/easyadmin-theme/datagrids.css
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dro
font-size: var(--font-size-base);
margin-inline-end: 2px;
}
.datagrid td.actions .dropdown-item-variant-danger:hover {
.datagrid td.actions .dropdown-item-variant-danger:hover,
.page-actions .dropdown-item-variant-danger:hover {
--dropdown-icon-color: var(--dropdown-item-danger-color);

background: var(--dropdown-item-danger-bg);
Expand All @@ -194,13 +195,14 @@ table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dro
.datagrid td.actions-as-dropdown-table-head {
inline-size: 10px;
}
.datagrid tr:not(.selected-row):hover .dropdown-toggle {
.datagrid tr:not(.selected-row):hover .actions-as-dropdown .dropdown-actions > .dropdown-toggle {
background: var(--dropdown-toggle-bg);
border-color: var(--dropdown-toggle-border-color);
}
.datagrid tr:hover .dropdown-toggle:hover {
.datagrid tr:hover .actions-as-dropdown .dropdown-actions > .dropdown-toggle:hover {
border-color: var(--dropdown-toggle-hover-border-color);
}

.datagrid tr:hover .dropdown-toggle:focus,
.datagrid tr:hover .dropdown-toggle:active,
.datagrid tr:hover .dropdown-toggle:active:focus,
Expand All @@ -224,6 +226,7 @@ table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dro
color: var(--dropdown-toggle-color);
display: block;
padding: 1px 5px;
overflow: visible;
}

.datagrid .dropdown-actions .dropdown-toggle .icon {
Expand All @@ -236,6 +239,69 @@ table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dro
z-index: var(--zindex-900);
}

/* Nested dropdown menus (action groups) - always open to the left for better UX */
.datagrid .dropdown-actions .dropdown-menu .dropstart {
position: relative;
}

.datagrid .dropdown-actions .dropstart .dropdown-toggle:before {
margin-inline-start: -20px;
position: absolute;
}

/* Position nested dropdown menu to the left */
.datagrid .dropdown-actions .dropdown-menu .dropstart > .dropdown-menu {
inset-block-start: 0;
inset-inline-end: 100%;
inset-inline-start: auto;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: -0.125rem;
margin-inline-start: 0;
}
.datagrid .dropdown-actions .dropdown-menu .dropstart:has(.dropdown-toggle-split) > .dropdown-menu {
margin-inline-end: 1.125rem;
}

/* Submenus without toggle-split */
.datagrid .dropdown-actions .dropdown-menu .dropstart:not(:has(.dropdown-toggle-split)):hover > .dropdown-menu,
.datagrid
.dropdown-actions
.dropdown-menu
.dropstart:not(:has(.dropdown-toggle-split))
> .dropdown-toggle:focus
.dropdown-menu,
.datagrid .dropdown-actions .dropdown-menu .dropstart:not(:has(.dropdown-toggle-split)) > .dropdown-menu:hover,
/* Submenus with toggle-split */
.datagrid
.dropdown-actions
.dropdown-menu
.dropstart:has(.dropdown-toggle-split:hover)
> .dropdown-menu,
.datagrid .dropdown-actions .dropdown-menu .dropstart:has(.dropdown-toggle-split) > .dropdown-menu:hover {
display: block;
}

/* Split button submenu styling */
.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split {
padding-inline-start: 0.5rem;
padding-inline-end: 0.5rem;
position: absolute;
inset-inline-start: -22px;
}
.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split:before {
display: none;
}

.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split .dropdown-toggle-marker {
/* this is a chevron-like icon made out of text with CSS border tricks */
border-block-end: .3em solid transparent;
border-inline-end: .3em solid;
border-block-start: .3em solid transparent;
content: "";
display: inline-block;
}

.datagrid .ea-lightbox-thumbnail img {
background: var(--white);
border: 1px solid transparent;
Expand Down
159 changes: 159 additions & 0 deletions doc/actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,157 @@ contents on each row. If you prefer to display all the actions *inline*
}
}

Grouping Actions
----------------

In addition to individual actions, you can group multiple related actions into
a single button. This is useful when you have many actions and want to organize
them better or save space in the interface. Use the ``ActionGroup`` class
to create these grouped actions::

use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\ActionGroup;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;

public function configureActions(Actions $actions): Actions
{
$publishActions = ActionGroup::new('publish', 'Publish')
->addAction(Action::new('publishNow', 'Publish Now')->linkToCrudAction('...'))
->addAction(Action::new('schedule', 'Schedule...')->linkToCrudAction('...'))
->addAction(Action::new('publishDraft', 'Save as Draft')->linkToCrudAction('...'));

return $actions
// ...
->add(Crud::PAGE_EDIT, $publishActions)
;
}

This is how the action group looks in practice:

.. image:: images/easyadmin-action-group.gif
:alt: An action group that includes three different actions into a single button

Similar to standalone actions, on the index page there are two types of action
groups: those associated with each entity and those associated with the entire page::

public function configureActions(Actions $actions): Actions
{
$createActions = ActionGroup::new('create')
->createAsGlobalActionGroup()
->addAction(Action::new('new', 'Create Post')->linkToCrudAction('...'))
->addAction(Action::new('draft', 'Draft Post')->linkToCrudAction('...'))
->addAction(Action::new('template', 'Create from Template')->linkToCrudAction('...'));

$sendActions = ActionGroup::new('send', 'Send ...')
->addAction(Action::new('sendEmail', 'Send by Email')->linkToCrudAction('...'))
->addAction(Action::new('sendSlack', 'Send to Slack')->linkToCrudAction('...'))
->addAction(Action::new('sendTelegram', 'Send via Telegram')->linkToCrudAction('...'));

return $actions
// ...
->add(Crud::PAGE_INDEX, $createActions)
->add(Crud::PAGE_INDEX, $sendActions)
;
}

The ``createAsGlobalActionGroup()`` method creates an action group associated
with the entire page rather than any specific entity. It appears like the image
shown above for action groups.

When not using the ``createAsGlobalActionGroup()`` method on the index page, the
action group is displayed as a nested dropdown on each entity row (see the image
in the next section below).

Split Button Dropdowns
~~~~~~~~~~~~~~~~~~~~~~

If one of the grouped actions is more common than the others, you can render the
group as a "split button". This displays the **main action** as a clickable button,
with the other actions available in the dropdown::

$publishActions = ActionGroup::new('publish', 'Publish')
->addMainAction(Action::new('publishNow', 'Publish Now')->linkToCrudAction('...'))
->addAction(Action::new('schedule', 'Schedule...')->linkToCrudAction('...'))
->addAction(Action::new('publishDraft', 'Save as Draft')->linkToCrudAction('...'));

Now, the action group will look as follows:

.. image:: images/easyadmin-action-group-split-button.gif
:alt: An action group that defines a main action and a list of secondary actions

On the index page, if the action group is associated with each entity, it's
displayed as a dropdown. In the following image, the first action group is a
simple dropdown because it doesn't define a main action. The second action
group is a split dropdown, where the main action is a clickable element and the
remaining actions appear when hovering over the submenu marker:

.. image:: images/easyadmin-action-group-entity-dropdown.gif
:alt: An action group inside an entity dropdown. The second group defines a main action.

Headers and Dividers
~~~~~~~~~~~~~~~~~~~~

For better organization, especially with many actions in a dropdown, you can add
headers and dividers to create logical groups::

$actionsGroup = ActionGroup::new('actions', 'Actions', 'fa fa-cog')
->addHeader('Quick Actions')
->addAction(Action::new('approve', 'Approve')->linkToCrudAction('approve'))
->addAction(Action::new('reject', 'Reject')->linkToCrudAction('reject'))
->addDivider()
->addHeader('Advanced')
->addAction(Action::new('archive', 'Archive')->linkToCrudAction('archive'))
->addAction(Action::new('delete', 'Delete')->linkToCrudAction('delete')
->addCssClass('text-danger'));

Headers help users understand the purpose of each group, while dividers provide
visual separation between different sections.

Conditional Dropdown Display
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Like regular actions, dropdowns can be displayed conditionally based on the
entity state or user permissions::

$moderationGroup = ActionGroup::new('moderation', 'Moderation')
// the callable receives the current entity instance or null (in the index page)
->displayIf(static function ($entity) {
return null !== $entity && 'pending' === $entity->getStatus();
})
->addAction(Action::new('approve', 'Approve')->linkToCrudAction('approve'))
->addAction(Action::new('reject', 'Reject')->linkToCrudAction('reject'));

The dropdown will only appear when the condition is met. Individual actions
within the dropdown can also have their own display conditions.

Customizing Dropdown Appearance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Dropdowns support the same customization options as regular actions for styling
and HTML attributes::

$customGroup = ActionGroup::new('custom', 'Options')
// use only an icon, no label
->setLabel(false)
->setIcon('fa fa-ellipsis-v')

// create different variants of action groups
->asPrimaryActionGroup()
->asDefaultActionGroup()
->asSuccessActionGroup()
->asWarningActionGroup()
->asDangerActionGroup()

// add custom CSS classes
->addCssClass('my-custom-dropdown')

// add HTML attributes
->setHtmlAttributes(['data-foo' => 'bar']);

You can also customize individual actions within the dropdown using the standard
action configuration methods.

.. _actions-custom:

Adding Custom Actions
Expand Down Expand Up @@ -414,6 +565,14 @@ that will represent the action::
// useful when customizing a built-in action, which already has CSS classes)
->addCssClass('some-custom-css-class text-danger')

This is how the different button style variants look in light and dark mode:

.. image:: images/easyadmin-buttons-light-mode.gif
:alt: EasyAdmin button variants in light mode

.. image:: images/easyadmin-buttons-dark-mode.gif
:alt: EasyAdmin button variants in dark mode

.. note::

When using ``setCssClass()`` or ``addCssClass()`` methods, the action loses
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/easyadmin-action-group.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/easyadmin-buttons-dark-mode.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/easyadmin-buttons-light-mode.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading