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 put into Action which are identified by PHP
enum
\TYPO3\ and
differentiate groups of buttons into a "primary" and "secondary" group.
Also,
\TYPO3\ enhances
the ability to group multiple Button API Components into one data object
and manage its 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 need to
update the method signature as the $table parameter has been removed.
ModifyRecordListRecordActionsEvent
The method
set now requires a
Component object,
the method
get now returns null or a
Component
object.
Similar behavior can be for ActionGroup and the
$group parameter which
requires a
Action enum now and affects these methods:
hasAction () getAction () removeAction () getAction Group ()
The method
get no longer returns a raw data array but an instance
of the Record API.
A new method
get allows to access request context for the event.
Removed methods:
getActions () setActions () getTable ()
ProcessFileListActionsEvent
The
Process has received identical changes to its
API as the
Modify, allowing to modify
items in both supported ActionGroups (primary and secondary). Several new API
methods have been created:
setAction () getAction () removeAction () moveAction To () getAction Group () getRequest ()
Buttons can now also be internally relocated, or placed at specific before/after positions.
Removed methods:
getAction Items () setAction Items ()
Affected Installations
TYPO3 installations with custom PHP code that modifies these actions and buttons, or utilizes the mentioned PSR-14 events.
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 be obtained from
the
Record via
$record->get.
Adjust code that calls this (internal) method to drop the $table argument:
// ...
public function render(): string
{
$row = BackendUtility::getRecord($table, $someRowUid);
$databaseRecordList = GeneralUtility::makeInstance(DatabaseRecordList::class);
- return $databaseRecordList->makeControl($table, $row);
+ return $databaseRecordList->makeControl($row);
}
ProcessFileListActionsEvent
Due to the changes in those events, event listeners now need to
compose extra actions with the Button API and add each button (
$action)
via the event's
set method.
Internally, buttons are now put into the new
Action container
which can be retrieved via
TYPO3\ or
TYPO3\.
The replacement for the old event method
get thus needs the
context of which action group to retrieve, and can be done now via
get.
Instead of retrieving all items and modifying them, distinct event methods
remove,
move and
get are now available,
identifying each action button with a string like the former array key index.
Action buttons can now longer be submitted as raw HTML markup, but instead need to utilize either the Button API or the new ComponentFactory() (see forge#107823) for a convenience layer on top of the Button API.
// Before
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
class ProcessFileListActionsEventListener
{
public function __construct(
TYPO3\CMS\Backend\Template\Components\ComponentFactory $componentFactory,
TYPO3\CMS\Core\Imaging\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
As with the event above, event listeners now need to
compose extra actions with the Button API and add each button (
$action)
via the event's
set method. Buttons can no longer
contain raw HTML markup.
The signature of the existing event method
set has changed, so
that
$action needs to be an instance of
Component,
which is retrieved via the
Component, and no longer a string.
Since (as mentioned above) the action groups are managed via the
Action
container, the event methods
has,
get,
remove,
get now need to specify a
$group identifier like
Action
or
Action instead of a string.
The ability to inject multiple items at once with
set must be replaced
with distinct calls to
set.
Retrieving all action items can no longer be done with
get but must specifically
access either the primary or secondary action group with
get.
The
get method no longer returns an array with record data, but an object of the Record API.
The
get method can be replaced by retrieving the table name via
get
thanks to easily accessing the Record API object.
Modifying actions
// Before
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
class ModifyRecordListRecordActionsEventListener
{
public function __construct(
TYPO3\CMS\Backend\Template\Components\ComponentFactory $componentFactory,
TYPO3\CMS\Core\Imaging\IconFactory $iconFactory,
) {
}
public function __invoke(ModifyRecordListRecordActionsEventListener $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
$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
The create extensions or custom code that works in both TYPO3 v13 and v14, a version switch can be added within event listeners:
class ModifyRecordListRecordActionsEventListener
{
public function __invoke(ModifyRecordListRecordActionsEvent $event): void
{
if (new TYPO3\CMS\Core\Information\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);
}
}
}