Breaking: #107884 - Rework actions to use Buttons API with Components
See forge#107884
Description
The record list and file list action system (the "button bar" in every row of the table-like display) has been completely reworked to use the Buttons API, utilizing proper component objects instead of plain HTML strings.
This modernization improves type safety, provides better extensibility, and enables more structured manipulation of action buttons through PSR-14 events.
The following components have been affected by this change:
\TYPO3\CMS\ Backend\ Record List\ Event\ Modify Record List Record Actions Event \TYPO3\CMS\ Filelist\ Event\ Process File List Actions Event \TYPO3\CMS\ Backend\ Record List\ Database Record List:: make Control ()
Buttons can now be placed into ActionGroups, which are identified by the
PHP enum
\TYPO3\ and
distinguish between a "primary" and a "secondary" group.
In addition,
\TYPO3\
enhances the ability to group multiple Button API Components into a single data
object and manage their state.
Impact
Extensions that listen to the
Modify
or
Process to modify
record or file actions need to be updated.
The events no longer work with HTML strings but with
Component objects
(see forge#107823).
Extensions that directly call
Database must
update their code, as the
$table parameter has been removed.
ModifyRecordListRecordActionsEvent
The method
set now requires a
Component object, and
get now returns either null or a
Component instance.
The following methods now expect an
Action enum value as the
$group parameter:
hasAction () getAction () removeAction () getAction Group ()
The method
get no longer returns a raw array but an instance of
the Record API.
A new method
get has been added to access the current PSR-7
request context.
Removed methods:
getActions () setActions () getTable ()
ProcessFileListActionsEvent
The
\TYPO3\ has received
identical API changes to
Modify,
allowing manipulation of items in both supported
Action contexts
(primary and secondary).
New methods:
setAction () getAction () removeAction () moveAction To () getAction Group () getRequest ()
Buttons can now also be repositioned or inserted at specific before/after locations within an action group.
Removed methods:
getAction Items () setAction Items ()
Affected installations
TYPO3 installations with custom PHP code that modifies record or file list actions, or utilizes the mentioned PSR-14 events, are affected.
Migration
DatabaseRecordList::makeControl()
// Before
public function makeControl($table, RecordInterface $record): string
// After
public function makeControl(RecordInterface $record): string
The
$table parameter has been removed, as the table name can now be
retrieved from the
Record via
$record->get.
Adjust calls accordingly:
// ...
public function render(): string
{
$row = BackendUtility::getRecord($table, $someRowUid);
$databaseRecordList = GeneralUtility::makeInstance(DatabaseRecordList::class);
- return $databaseRecordList->makeControl($table, $row);
+ return $databaseRecordList->makeControl($row);
}
ProcessFileListActionsEvent
Event listeners must now compose buttons via the Button API and add each
component using the event’s
set method.
Internally, buttons are placed into an
Action container,
retrieved via
Action or
Action.
The previous
get logic is replaced with
get to fetch the corresponding button group.
Instead of manipulating raw HTML, you must now create components using the
Component.
// Before
use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;
class ProcessFileListActionsEventListener
{
public function __invoke(ProcessFileListActionsEvent $event): void
{
$items = $event->getActionItems();
$items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
unset($items['some-other-action']);
$event->setActionItems($items);
}
}
// After
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;
class ProcessFileListActionsEventListener
{
public function __construct(
private readonly ComponentFactory $componentFactory,
private readonly IconFactory $iconFactory,
) {}
public function __invoke(ProcessFileListActionsEvent $event): void
{
$viewButton = $this->componentFactory->createGenericButton()
->setIcon($this->iconFactory->getIcon('actions-view'))
->setTitle('My title');
$event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
$event->removeAction('some-other-action', ActionGroup::primary);
}
}
ModifyRecordListRecordActionsEvent
This event now behaves identically to the file list event:
actions must be created via the Button API and added as
Component instances using
set.
The
set and
get methods are removed and must be
replaced by distinct calls to
set or use
get to access existing actions.
The
get method now returns a Record API object instead of an
array.
get can be replaced with
get.
Modifying actions example:
// Before
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
class ModifyRecordListRecordActionsEventListener
{
public function __invoke(ModifyRecordListRecordActionsEvent $event): void
{
$items = $event->getActions();
unset($items['my-own-action']);
$items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
unset($existing['some-other-action']);
$event->setActions($items);
$event->setAction('<button ...></button>', 'my-other-own-action', 'secondary');
}
}
// After
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Core\Imaging\IconFactory;
class ModifyRecordListRecordActionsEventListener
{
public function __construct(
private readonly ComponentFactory $componentFactory,
private readonly IconFactory $iconFactory,
) {}
public function __invoke(ModifyRecordListRecordActionsEvent $event): void
{
$viewButton = $this->componentFactory->createGenericButton()
->setIcon($this->iconFactory->getIcon('actions-view'))
->setTitle('My title');
$event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
$event->removeAction('some-other-action', ActionGroup::primary);
$inputButton = $this->componentFactory->createInputButton()
->setTitle('My Button');
$event->setAction($inputButton, 'my-other-own-action', ActionGroup::secondary);
}
}
Accessing groups
// Before
$event->getAction('my-button', 'primary');
$event->hasAction('my-button', 'primary');
$event->removeAction('my-button', 'primary');
$event->getActionGroup('primary');
// After
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
$event->getAction('my-button', ActionGroup::primary);
$event->hasAction('my-button', ActionGroup::primary);
$event->removeAction('my-button', ActionGroup::primary);
$event->getActionGroup(ActionGroup::primary);
Accessing record
// Before
$uid = $event->getRecord()['uid'];
$title = $event->getRecord()['title'];
// After
$uid = $event->getRecord()->getUid();
$title = $event->getRecord()->getRawRecord()['title'];
Dual-version compatibility
To support both TYPO3 v13 and v14, extensions can use a version check within event listeners:
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Core\Information\Typo3Version;
class ModifyRecordListRecordActionsEventListener
{
public function __invoke(ModifyRecordListRecordActionsEvent $event): void
{
if ((new Typo3Version())->getMajorVersion() >= 14) {
$viewButton = $this->componentFactory->createGenericButton()
->setIcon($this->iconFactory->getIcon('actions-view'))
->setTitle('My title');
$event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
$event->removeAction('some-other-action', ActionGroup::primary);
} else {
$items = $event->getActions();
unset($items['my-own-action']);
$items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
unset($existing['some-other-action']);
$event->setActions($items);
}
}
}