Create GridField Actions Menu component (#8083)

* WIP GridField action menu work, the gist of the idea is using a new gridfield component

* Add delete action to actions menu

* Actions are added automatically to action menu (allows for extension)

* Add test and minor changes

* Add docs and minor changes

* Refactor ActionMenuItem into distinct types, general ActionMenu cleanup

* Add icons and fix title

* Pass columnName, so it can be used by components

* Update test to open and find action menu buttons

* Add section in changelog upgrade section for GridField_ActionMenu
This commit is contained in:
Luke Edwards 2018-05-29 16:10:52 +12:00 committed by Aaron Carlino
parent a0d0564369
commit 385e9e105c
17 changed files with 532 additions and 59 deletions

View File

@ -157,6 +157,7 @@ $component = $config->getComponentByType(GridFieldFilterHeader::class)
Here is a list of components for use bundled with the core framework. Many more components are provided by third-party
modules and extensions.
- [GridField_ActionMenu](api:SilverStripe\Forms\GridField\GridField_ActionMenu)
- [GridFieldToolbarHeader](api:SilverStripe\Forms\GridField\GridFieldToolbarHeader)
- [GridFieldSortableHeader](api:SilverStripe\Forms\GridField\GridFieldSortableHeader)
- [GridFieldFilterHeader](api:SilverStripe\Forms\GridField\GridFieldFilterHeader)
@ -266,6 +267,33 @@ This configuration adds the ability to searched for existing records and add a r
Records created or deleted through the `GridFieldConfig_RelationEditor` automatically update the relationship in the
database.
## GridField_ActionMenu
The `GridField_ActionMenu` component provides a dropdown menu which automatically bundles GridField actions into a react based dropdown. It is included by default on `GridFieldConfig_RecordEditor` and `GridFieldConfig_RelationEditor` configs.
To add it to a GridField, add the `GridField_ActionMenu` component and any action(s) that implement `GridField_ActionMenuItem` (such as `GridFieldEditButton` or `GridFieldDeleteAction`) to the `GridFieldConfig`.
```php
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
// `GridFieldConfig::create()` will create an empty configuration (no components).
$config = GridFieldConfig::create();
// add a component
$config->addComponent();
$config->addComponents(
new GridFieldDataColumns(),
new GridFieldEditButton(),
new GridField_ActionMenu()
);
// Update the GridField with our custom configuration
$gridField->setConfig($config);
```
## GridFieldDetailForm
The `GridFieldDetailForm` component drives the record viewing and editing form. It takes its' fields from

View File

@ -153,6 +153,51 @@ called method is available as a parameter.
To finish off our basic example, the `handleAction` method simply returns a
message to the user interface indicating a successful message.
## Add the GridFieldCustomAction to the `GridField_ActionMenu`
For an action to be included in the action menu dropdown, which appears on each row if `GridField_ActionMenu` is included in the `GridFieldConfig`, it must implement `GridField_ActionMenuItem` and relevant `get` functions to provide information to the frontend react action menu component.
## Basic GridFieldCustomAction boilerplate implementing GridField_ActionMenuItem
```php
use SilverStripe\Forms\GridField\GridField_ColumnProvider;
use SilverStripe\Forms\GridField\GridField_ActionProvider;
use SilverStripe\Forms\GridField\GridField_ActionMenuItem;
use SilverStripe\Forms\GridField\GridField_FormAction;
use SilverStripe\Control\Controller;
class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_ActionProvider, GridField_ActionMenuItem
{
public function augmentColumns($gridField, &$columns)
{
if(!in_array('Actions', $columns)) {
$columns[] = 'Actions';
}
}
public function getTitle($gridField, $record)
{
return _t(__CLASS__ . '.Delete', "Delete");
}
public function getGroup($gridField, $record)
{
return GridField_ActionMenuItem::DEFAULT_GROUP;
}
public function getExtraData($gridField, $record, $columnName)
{
if ($field) {
return $field->getAttributes();
}
return null;
}
// ...rest of boilerplate code
```
## Related
* [GridField Reference](/developer_guides/forms/field_types/gridfield)

View File

@ -148,3 +148,26 @@ cd ~/my-project-root
upgrade-code environment --write
upgrade-code reorganise --write
```
### New GridField Action Menu
A new `GridField_ActionMenu` is included by default in GridFields configured with `GridFieldConfig_RecordEditor`
or `GridFieldConfig_RelationEditor`.
In addition to this `GridFieldDeleteAction` and `GridFieldEditButton` now implement `GridField_ActionMenuItem`,
this means that any GridField that uses a config of or based on `GridFieldConfig_RecordEditor`
or `GridFieldConfig_RelationEditor` will have an action menu on each item row with
the 'Delete/Unlink' and 'Edit' actions moved into it.
If you wish to opt out of having this menu and the respective actions moved into it, you can remove the `GridField_ActionMenu`
component from the config that is passed into your GridField.
```php
// method 1: removing GridField_ActionMenu from a new GridField
$config = GridFieldConfig_RecordEditor::create();
$config->removeComponentsByType(GridField_ActionMenu);
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
// method 2: removing GridField_ActionMenu from an existing GridField
$gridField->getConfig()->removeComponentsByType(GridField_ActionMenu);
```

View File

@ -98,7 +98,7 @@ en:
DeletePermissionsFailure: 'No delete permissions'
Deleted: 'Deleted {type} {name}'
Save: Save
SilverStripe\Forms\GridField\GridFieldEditButton_ss:
SilverStripe\Forms\GridField\GridFieldEditButton:
EDIT: Edit
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Cannot remove yourself from this group, you will lose admin rights'

View File

@ -22,7 +22,8 @@ class GridFieldConfig_RecordEditor extends GridFieldConfig
$this->addComponent($filter = new GridFieldFilterHeader());
$this->addComponent(new GridFieldDataColumns());
$this->addComponent(new GridFieldEditButton());
$this->addComponent(new GridFieldDeleteAction());
$this->addComponent(new GridFieldDeleteAction(true));
$this->addComponent(new GridField_ActionMenu());
$this->addComponent(new GridFieldPageCount('toolbar-header-right'));
$this->addComponent($pagination = new GridFieldPaginator($itemsPerPage));
$this->addComponent(new GridFieldDetailForm());

View File

@ -38,6 +38,7 @@ class GridFieldConfig_RelationEditor extends GridFieldConfig
$this->addComponent(new GridFieldDataColumns());
$this->addComponent(new GridFieldEditButton());
$this->addComponent(new GridFieldDeleteAction(true));
$this->addComponent(new GridField_ActionMenu());
$this->addComponent(new GridFieldPageCount('toolbar-header-right'));
$this->addComponent($pagination = new GridFieldPaginator($itemsPerPage));
$this->addComponent(new GridFieldDetailForm());

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Forms\GridField;
use SilverStripe\Control\Controller;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationException;
@ -21,7 +22,7 @@ use SilverStripe\ORM\ValidationException;
* $action = new GridFieldDeleteAction(true);
* </code>
*/
class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_ActionProvider
class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_ActionProvider, GridField_ActionMenuItem
{
/**
@ -44,6 +45,47 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
$this->setRemoveRelation($removeRelation);
}
/**
* @inheritdoc
*/
public function getTitle($gridField, $record, $columnName)
{
$field = $this->getRemoveAction($gridField, $record, $columnName);
if ($field) {
return $field->getAttribute('title');
}
return _t(__CLASS__ . '.Delete', "Delete");
}
/**
* @inheritdoc
*/
public function getGroup($gridField, $record, $columnName)
{
return GridField_ActionMenuItem::DEFAULT_GROUP;
}
/**
*
* @param GridField $gridField
* @param DataObject $record
* @param string $columnName
* @return string|null the attribles for the action
*/
public function getExtraData($gridField, $record, $columnName)
{
$field = $this->getRemoveAction($gridField, $record, $columnName);
if ($field) {
return $field->getAttributes();
}
return null;
}
/**
* Add a column 'Delete'
*
@ -67,7 +109,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
*/
public function getColumnAttributes($gridField, $record, $columnName)
{
return array('class' => 'grid-field__col-compact');
return ['class' => 'grid-field__col-compact'];
}
/**
@ -80,7 +122,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
public function getColumnMetadata($gridField, $columnName)
{
if ($columnName == 'Actions') {
return array('title' => '');
return ['title' => ''];
}
}
@ -92,7 +134,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
*/
public function getColumnsHandled($gridField)
{
return array('Actions');
return ['Actions'];
}
/**
@ -103,7 +145,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
*/
public function getActions($gridField)
{
return array('deleterecord', 'unlinkrelation');
return ['deleterecord', 'unlinkrelation'];
}
/**
@ -111,43 +153,17 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
* @param GridField $gridField
* @param DataObject $record
* @param string $columnName
* @return string the HTML for the column
* @return string|null the HTML for the column
*/
public function getColumnContent($gridField, $record, $columnName)
{
if ($this->getRemoveRelation()) {
if (!$record->canEdit()) {
return null;
}
$title = _t(__CLASS__ . '.UnlinkRelation', "Unlink");
$field = $this->getRemoveAction($gridField, $record, $columnName);
$field = GridField_FormAction::create(
$gridField,
'UnlinkRelation' . $record->ID,
false,
"unlinkrelation",
array('RecordID' => $record->ID)
)
->addExtraClass('btn btn--no-text btn--icon-md font-icon-link-broken grid-field__icon-action gridfield-button-unlink')
->setAttribute('title', $title)
->setAttribute('aria-label', $title);
} else {
if (!$record->canDelete()) {
return null;
}
$field = GridField_FormAction::create(
$gridField,
'DeleteRecord' . $record->ID,
false,
"deleterecord",
array('RecordID' => $record->ID)
)
->addExtraClass('gridfield-button-delete btn--icon-md font-icon-trash-bin btn--no-text grid-field__icon-action')
->setAttribute('title', _t(__CLASS__ . '.Delete', "Delete"))
->setDescription(_t(__CLASS__ . '.DELETE_DESCRIPTION', 'Delete'));
if ($field) {
return $field->Field();
}
return $field->Field();
return null;
}
/**
@ -188,6 +204,54 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
}
}
/**
*
* @param GridField $gridField
* @param DataObject $record
* @param string $columnName
* @return GridField_FormAction|null
*/
private function getRemoveAction($gridField, $record, $columnName)
{
if ($this->getRemoveRelation()) {
if (!$record->canEdit()) {
return null;
}
$title = _t(__CLASS__ . '.UnlinkRelation', "Unlink");
$field = GridField_FormAction::create(
$gridField,
'UnlinkRelation' . $record->ID,
false,
"unlinkrelation",
['RecordID' => $record->ID]
)
->addExtraClass('btn btn--no-text btn--icon-md font-icon-link-broken grid-field__icon-action gridfield-button-unlink action-menu--handled')
->setAttribute('classNames', 'gridfield-button-unlink font-icon-link-broken')
->setDescription($title)
->setAttribute('aria-label', $title);
} else {
if (!$record->canDelete()) {
return null;
}
$title = _t(__CLASS__ . '.Delete', "Delete");
$field = GridField_FormAction::create(
$gridField,
'DeleteRecord' . $record->ID,
false,
"deleterecord",
['RecordID' => $record->ID]
)
->addExtraClass('gridfield-button-delete btn--icon-md font-icon-trash-bin btn--no-text grid-field__icon-action action-menu--handled')
->setAttribute('classNames', 'gridfield-button-delete font-icon-trash')
->setDescription($title)
->setAttribute('aria-label', $title);
}
return $field;
}
/**
* Get whether to remove or delete the relation
*

View File

@ -17,7 +17,7 @@ use SilverStripe\View\SSViewer;
* The default routing applies to the {@link GridFieldDetailForm} component,
* which has to be added separately to the {@link GridField} configuration.
*/
class GridFieldEditButton implements GridField_ColumnProvider
class GridFieldEditButton implements GridField_ColumnProvider, GridField_ActionProvider, GridField_ActionMenuLink
{
/**
* HTML classes to be added to GridField edit buttons
@ -27,9 +27,44 @@ class GridFieldEditButton implements GridField_ColumnProvider
protected $extraClass = [
'grid-field__icon-action--hidden-on-hover' => true,
'font-icon-edit' => true,
'btn--icon-large' => true
'btn--icon-large' => true,
'action-menu--handled' => true
];
/**
* @inheritdoc
*/
public function getTitle($gridField, $record, $columnName)
{
return _t(__CLASS__ . '.EDIT', "Edit");
}
/**
* @inheritdoc
*/
public function getGroup($gridField, $record, $columnName)
{
return GridField_ActionMenuItem::DEFAULT_GROUP;
}
/**
* @inheritdoc
*/
public function getExtraData($gridField, $record, $columnName)
{
return [
"classNames" => "font-icon-edit action-detail edit-link"
];
}
/**
* @inheritdoc
*/
public function getUrl($gridField, $record, $columnName)
{
return Controller::join_links($gridField->Link('item'), $record->ID, 'edit');
}
/**
* Add a column 'Delete'
*
@ -53,7 +88,7 @@ class GridFieldEditButton implements GridField_ColumnProvider
*/
public function getColumnAttributes($gridField, $record, $columnName)
{
return array('class' => 'grid-field__col-compact');
return ['class' => 'grid-field__col-compact'];
}
/**
@ -66,7 +101,7 @@ class GridFieldEditButton implements GridField_ColumnProvider
public function getColumnMetadata($gridField, $columnName)
{
if ($columnName == 'Actions') {
return array('title' => '');
return ['title' => ''];
}
return [];
}
@ -79,7 +114,7 @@ class GridFieldEditButton implements GridField_ColumnProvider
*/
public function getColumnsHandled($gridField)
{
return array('Actions');
return ['Actions'];
}
/**
@ -90,7 +125,7 @@ class GridFieldEditButton implements GridField_ColumnProvider
*/
public function getActions($gridField)
{
return array();
return [];
}
/**
@ -104,10 +139,10 @@ class GridFieldEditButton implements GridField_ColumnProvider
// No permission checks, handled through GridFieldDetailForm,
// which can make the form readonly if no edit permissions are available.
$data = new ArrayData(array(
$data = new ArrayData([
'Link' => Controller::join_links($gridField->Link('item'), $record->ID, 'edit'),
'ExtraClass' => $this->getExtraClass()
));
]);
$template = SSViewer::get_templates_by_class($this, '', __CLASS__);
return $data->renderWith($template);

View File

@ -14,16 +14,16 @@ use SilverStripe\View\SSViewer;
class GridFieldViewButton implements GridField_ColumnProvider
{
public function augmentColumns($field, &$cols)
public function augmentColumns($field, &$columns)
{
if (!in_array('Actions', $cols)) {
$cols[] = 'Actions';
if (!in_array('Actions', $columns)) {
$columns[] = 'Actions';
}
}
public function getColumnsHandled($field)
{
return array('Actions');
return ['Actions'];
}
public function getColumnContent($field, $record, $col)
@ -31,20 +31,20 @@ class GridFieldViewButton implements GridField_ColumnProvider
if (!$record->canView()) {
return null;
}
$data = new ArrayData(array(
$data = new ArrayData([
'Link' => Controller::join_links($field->Link('item'), $record->ID, 'view')
));
]);
$template = SSViewer::get_templates_by_class($this, '', __CLASS__);
return $data->renderWith($template);
}
public function getColumnAttributes($field, $record, $col)
{
return array('class' => 'grid-field__col-compact');
return ['class' => 'grid-field__col-compact'];
}
public function getColumnMetadata($gridField, $col)
{
return array('title' => null);
return ['title' => null];
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace SilverStripe\Forms\GridField;
use SilverStripe\Core\Convert;
use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer;
/**
* Groups exiting actions in the Actions column in to a menu
*/
class GridField_ActionMenu implements GridField_ColumnProvider, GridField_ActionProvider
{
public function augmentColumns($gridField, &$columns)
{
if (!in_array('Actions', $columns)) {
$columns[] = 'Actions';
}
}
public function getColumnsHandled($gridField)
{
return ['Actions'];
}
public function getColumnContent($gridField, $record, $columnName)
{
$items = $this->getItems($gridField);
if (!$items) {
return null;
}
$schema = array_map(function (GridField_ActionMenuItem $item) use ($gridField, $record, $columnName) {
return [
'type' => $item instanceof GridField_ActionMenuLink ? 'link' : 'submit',
'title' => $item->getTitle($gridField, $record, $columnName),
'url' => $item instanceof GridField_ActionMenuLink ? $item->getUrl($gridField, $record, $columnName) : null,
'group' => $item->getGroup($gridField, $record, $columnName),
'data' => $item->getExtraData($gridField, $record, $columnName),
];
}, $items);
$templateData = ArrayData::create([
'Schema' => Convert::raw2json($schema),
]);
$template = SSViewer::get_templates_by_class($this, '', static::class);
return $templateData->renderWith($template);
}
public function getColumnAttributes($gridField, $record, $columnName)
{
return ['class' => 'grid-field__col-compact action-menu'];
}
public function getColumnMetadata($gridField, $columnName)
{
return ['title' => null];
}
public function getActions($gridField)
{
$actions = [];
foreach ($this->getItems($gridField) as $item) {
if ($item instanceof GridField_ActionProvider) {
$actions = array_merge($actions, $item->getActions($gridField));
}
}
return $actions;
}
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
{
foreach ($this->getItems($gridField) as $item) {
$actions = [];
if ($item instanceof GridField_ActionProvider) {
$actions = $item->getActions($gridField);
}
if (in_array($actionName, $actions)) {
$item->handleAction($gridField, $actionName, $arguments, $data);
}
}
}
/**
* Gets the list of items setup
*
* @return array
*/
public function getItems($gridfield)
{
$items = $gridfield->config->getComponentsByType(GridField_ActionMenuItem::class)->items;
return $items;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace SilverStripe\Forms\GridField;
use SilverStripe\ORM\DataObject;
/**
* GridField action menu item interface, this provides data so the action
* will be included if there is a {@see GridField_ActionMenu}
*/
interface GridField_ActionMenuItem extends GridFieldComponent
{
/**
* Default group name
*/
const DEFAULT_GROUP = 'Default';
/**
* Gets the title for this menu item
*
* @see {@link GridField_ActionMenu->getColumnContent()}
*
* @param GridField $gridField
* @param DataObject $record
*
* @return string $title
*/
public function getTitle($gridField, $record, $columnName);
/**
* Gets any extra data that could go in to the schema that the menu generates
*
* @see {@link GridField_ActionMenu->getColumnContent()}
*
* @param GridField $gridField
* @param DataObject $record
*
* @return array $data
*/
public function getExtraData($gridField, $record, $columnName);
/**
* Gets the group this menu item will belong to
*
* @see {@link GridField_ActionMenu->getColumnContent()}
*
* @param GridField $gridField
* @param DataObject $record
*
* @return string $group
*/
public function getGroup($gridField, $record, $columnName);
}

View File

@ -0,0 +1,21 @@
<?php
namespace SilverStripe\Forms\GridField;
/**
* Allows GridField_ActionMenuItem to act as a link
*/
interface GridField_ActionMenuLink extends GridField_ActionMenuItem
{
/**
* Gets the action url for this menu item
*
* @see {@link GridField_ActionMenu->getColumnContent()}
*
* @param GridField $gridField
* @param DataObject $record
*
* @return string $url
*/
public function getUrl($gridField, $record, $columnName);
}

View File

@ -1,6 +1,6 @@
<a
class="grid-field__icon-action {$ExtraClass} action action-detail edit-link"
href="$Link" title="<%t SilverStripe\\Forms\\GridField\\GridFieldEditButton_ss.EDIT 'Edit' %>"
href="$Link" title="<%t SilverStripe\\Forms\\GridField\\GridFieldEditButton.EDIT 'Edit' %>"
>
<span class="sr-only"><%t SilverStripe\\Forms\\GridField\\GridFieldEditButton_ss.EDIT 'Edit' %></span>
<span class="sr-only"><%t SilverStripe\\Forms\\GridField\\GridFieldEditButton.EDIT 'Edit' %></span>
</a>

View File

@ -1 +1,3 @@
<a class="grid-field__icon-action font-icon-right-open btn--icon-large action action-detail view-link" href="$Link"><span class="sr-only">View</span></a>
<a class="grid-field__icon-action font-icon-right-open btn--icon-large action action-detail view-link" href="$Link">
<span class="sr-only">View</span>
</a>

View File

@ -0,0 +1 @@
<div class="gridfield-actionmenu__container" data-schema="$Schema"></div>

View File

@ -416,7 +416,11 @@ JS;
return null;
}
$button = $name->getParent()->find('xpath', sprintf('//*[@aria-label="%s"]', $buttonLabel));
if ($dropdownButton = $name->getParent()->find('css', '.action-menu__toggle')) {
$dropdownButton->click();
}
$button = $name->getParent()->find('named', array('link_or_button', $buttonLabel));
return $button;
}

View File

@ -0,0 +1,94 @@
<?php
namespace SilverStripe\Forms\Tests\GridField;
use SilverStripe\Control\Controller;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridField_ActionMenu;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldEditButton;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
class GridFieldActionMenuTest extends SapphireTest
{
/**
* @var ArrayList
*/
protected $list;
/**
* @var GridField
*/
protected $gridField;
/**
* @var Form
*/
protected $form;
/**
* @var string
*/
protected static $fixture_file = 'GridFieldActionTest.yml';
/**
* @var array
*/
protected static $extra_dataobjects = array(
Team::class,
Cheerleader::class,
Player::class,
Permissions::class,
);
protected function setUp()
{
parent::setUp();
$this->list = new DataList(Team::class);
$config = GridFieldConfig::create()
->addComponent(new GridFieldEditButton())
->addComponent(new GridFieldDeleteAction())
->addComponent(new GridField_ActionMenu());
$this->gridField = new GridField('testfield', 'testfield', $this->list, $config);
$this->form = new Form(null, 'mockform', new FieldList(array($this->gridField)), new FieldList());
}
public function testShowActionMenu()
{
if (Security::getCurrentUser()) {
Security::setCurrentUser(null);
}
$content = new CSSContentParser($this->gridField->FieldHolder());
// Check that there are content
$this->assertEquals(4, count($content->getBySelector('.ss-gridfield-item')));
// Make sure that there are edit links, even though the user doesn't have "edit" permissions
// (they can still view the records)
$this->assertEquals(
3,
count($content->getBySelector('.gridfield-actionmenu__container')),
'Edit links should show when not logged in.'
);
}
public function testShowEditLinksWithAdminPermission()
{
$this->logInWithPermission('ADMIN');
$content = new CSSContentParser($this->gridField->FieldHolder());
$editLinks = $content->getBySelector('.gridfield-actionmenu__container');
$this->assertEquals(3, count($editLinks), 'Edit links should show when logged in.');
}
}