Compare commits

...

8 Commits

Author SHA1 Message Date
Niklas Forsdahl 1ad6acbb84 Refactored grid field nested form link to be a button with aria-attributes,
for better accessibility.
2024-04-24 11:13:09 +03:00
Niklas Forsdahl 46e5cccdb4 Changed some PHPDoc return types to real typehings in nested gridfield. 2024-04-24 11:03:48 +03:00
Niklas Forsdahl bc1180b62c Throw exception in nested gridfields if the relation is invalid. 2024-04-24 10:42:55 +03:00
Niklas Forsdahl 5e6097214e Added phpdoc for nested grid field item request handler class. 2024-04-24 10:25:45 +03:00
Niklas Forsdahl 70b838ea8c Removed legacy disabling of security token and strict form method check
for nested gridfields, doesn't seem to be needed anymore.
2024-04-24 10:17:22 +03:00
Niklas Forsdahl c517c693f9 Don't assume records are DataObjects in nested gridfields.
Also don't assume the list is filterable.
2024-04-24 09:57:24 +03:00
Niklas Forsdahl f7b8aea3f8 PHPDoc additions and linting fixes for gridfield nested form 2024-04-24 09:40:16 +03:00
Niklas Forsdahl 847ce07ab0 Fixes and some refactoring for max nesting level handling in
nested gridfields.
2024-04-24 09:19:32 +03:00
4 changed files with 150 additions and 65 deletions

View File

@ -517,7 +517,7 @@
/** /**
* GridFieldNestedForm * GridFieldNestedForm
*/ */
$('.grid-field .col-listChildrenLink a').entwine({ $('.grid-field .col-listChildrenLink button').entwine({
onclick: function(e) { onclick: function(e) {
let gridField = $(this).closest('.grid-field'); let gridField = $(this).closest('.grid-field');
let currState = gridField.getState(); let currState = gridField.getState();
@ -552,7 +552,7 @@
} }
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: $(this).attr('href'), url: $(this).attr('data-url'),
data: data, data: data,
headers: { headers: {
'X-Pjax': pjaxTarget 'X-Pjax': pjaxTarget
@ -572,6 +572,7 @@
} }
$(this).removeClass('font-icon-right-dir'); $(this).removeClass('font-icon-right-dir');
$(this).addClass('font-icon-down-dir'); $(this).addClass('font-icon-down-dir');
$(this).attr('aria-expanded', 'true');
} }
else { else {
$.ajax({ $.ajax({
@ -580,6 +581,7 @@
$(this).closest('tr').next('.nested-gridfield').hide(); $(this).closest('tr').next('.nested-gridfield').hide();
$(this).removeClass('font-icon-down-dir'); $(this).removeClass('font-icon-down-dir');
$(this).addClass('font-icon-right-dir'); $(this).addClass('font-icon-right-dir');
$(this).attr('aria-expanded', 'false');
} }
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@ -6,8 +6,10 @@ use Exception;
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Forms\GridField\AbstractGridFieldComponent; use SilverStripe\Forms\GridField\AbstractGridFieldComponent;
use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField;
@ -16,11 +18,11 @@ use SilverStripe\Forms\GridField\GridField_DataManipulator;
use SilverStripe\Forms\GridField\GridField_HTMLProvider; use SilverStripe\Forms\GridField\GridField_HTMLProvider;
use SilverStripe\Forms\GridField\GridField_SaveHandler; use SilverStripe\Forms\GridField\GridField_SaveHandler;
use SilverStripe\Forms\GridField\GridField_URLHandler; use SilverStripe\Forms\GridField\GridField_URLHandler;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\Forms\GridField\GridFieldStateAware; use SilverStripe\Forms\GridField\GridFieldStateAware;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\Filterable;
use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
@ -39,26 +41,61 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
{ {
use Configurable, GridFieldStateAware; use Configurable, GridFieldStateAware;
/**
* The key used in the post data to identify nested form data
*/
const POST_KEY = 'GridFieldNestedForm'; const POST_KEY = 'GridFieldNestedForm';
private static $allowed_actions = [ private static $allowed_actions = [
'handleNestedItem' 'handleNestedItem'
]; ];
private static $max_nesting_level = 10; /**
* The default max nesting level. Nesting further than this will throw an exception.
*
* @var boolean
*/
private static $default_max_nesting_level = 10;
/** /**
* @var string * @var string
*/ */
protected $name; private $name;
protected $expandNested = false;
protected $forceCloseNested = false; /**
protected $gridField = null; * @var bool
protected $record = null; */
protected $relationName = 'Children'; private $expandNested = false;
protected $inlineEditable = false;
protected $canExpandCheck = null; /**
protected $maxNestingLevel = null; * @var bool
*/
private $forceCloseNested = false;
/**
* @var GridField
*/
private $gridField = null;
/**
* @var string
*/
private $relationName = 'Children';
/**
* @var bool
*/
private $inlineEditable = false;
/**
* @var callable|string
*/
private $canExpandCheck = null;
/**
* @var int
*/
private $maxNestingLevel = null;
public function __construct($name = 'NestedForm') public function __construct($name = 'NestedForm')
{ {
@ -67,27 +104,24 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
/** /**
* Get the grid field that this component is attached to * Get the grid field that this component is attached to
* @return GridField
*/ */
public function getGridField() public function getGridField(): GridField
{ {
return $this->gridField; return $this->gridField;
} }
/** /**
* Get the relation name to use for the nested grid fields * Get the relation name to use for the nested grid fields
* @return string
*/ */
public function getRelationName() public function getRelationName(): string
{ {
return $this->relationName; return $this->relationName;
} }
/** /**
* Set the relation name to use for the nested grid fields * Set the relation name to use for the nested grid fields
* @param string $relationName
*/ */
public function setRelationName($relationName) public function setRelationName(string $relationName)
{ {
$this->relationName = $relationName; $this->relationName = $relationName;
return $this; return $this;
@ -95,18 +129,16 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
/** /**
* Get whether the nested grid fields should be inline editable * Get whether the nested grid fields should be inline editable
* @return boolean
*/ */
public function getInlineEditable() public function getInlineEditable(): bool
{ {
return $this->inlineEditable; return $this->inlineEditable;
} }
/** /**
* Set whether the nested grid fields should be inline editable * Set whether the nested grid fields should be inline editable
* @param boolean $editable
*/ */
public function setInlineEditable($editable) public function setInlineEditable(bool $editable)
{ {
$this->inlineEditable = $editable; $this->inlineEditable = $editable;
return $this; return $this;
@ -114,9 +146,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
/** /**
* Set whether the nested grid fields should be expanded by default * Set whether the nested grid fields should be expanded by default
* @param boolean $expandNested
*/ */
public function setExpandNested($expandNested) public function setExpandNested(bool $expandNested)
{ {
$this->expandNested = $expandNested; $this->expandNested = $expandNested;
return $this; return $this;
@ -124,49 +155,54 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
/** /**
* Set whether the nested grid fields should be forced closed on load * Set whether the nested grid fields should be forced closed on load
* @param boolean $forceClosed
*/ */
public function setForceClosedNested($forceClosed) public function setForceClosedNested(bool $forceClosed)
{ {
$this->forceCloseNested = $forceClosed; $this->forceCloseNested = $forceClosed;
return $this; return $this;
} }
/** /**
* Set a callback to check which items in this grid that should show the expand link * Set a callback function to check which items in this grid that should show the expand link
* for nested gridfields. The callback should return a boolean value. * for nested gridfields. The callback should return a boolean value.
* @param callable $checkFunction * You can either pass a callable or a method name as a string.
*/ */
public function setCanExpandCheck($checkFunction) public function setCanExpandCheck(callable|string $callback)
{ {
$this->canExpandCheck = $checkFunction; $this->canExpandCheck = $callback;
return $this; return $this;
} }
/** /**
* Set the maximum nesting level allowed for nested grid fields * Set the maximum nesting level allowed for nested grid fields
* @param int $level
*/ */
public function setMaxNestingLevel($level) public function setMaxNestingLevel(int $level)
{ {
$this->maxNestingLevel = $level; $this->maxNestingLevel = $level;
return $this; return $this;
} }
public function getMaxNestingLevel() /**
* Get the max nesting level allowed for this grid field.
*/
public function getMaxNestingLevel(): int
{ {
return $this->maxNestingLevel ?: $this->config()->max_nesting_level; return $this->maxNestingLevel ?: static::config()->get('default_max_nesting_level');
} }
protected function getNestingLevel($gridField) /**
* Check if we are currently at the max nesting level allowed.
*/
protected function atMaxNestingLevel(GridField $gridField): bool
{ {
$level = 0; $level = 0;
$c = $gridField->getForm()->getController(); $controller = $gridField->getForm()->getController();
while ($c && $c instanceof GridFieldDetailForm_ItemRequest) { $maxLevel = $this->getMaxNestingLevel();
$c = $c->getController(); while ($level < $maxLevel && $controller && $controller instanceof GridFieldNestedFormItemRequest) {
$controller = $controller->getController();
$level++; $level++;
} }
return $level; return $level >= $maxLevel;
} }
public function getColumnMetadata($gridField, $columnName) public function getColumnMetadata($gridField, $columnName)
@ -193,14 +229,12 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
public function getColumnContent($gridField, $record, $columnName) public function getColumnContent($gridField, $record, $columnName)
{ {
$nestingLevel = $this->getNestingLevel($gridField); if ($this->atMaxNestingLevel($gridField)) {
if ($nestingLevel >= $this->getMaxNestingLevel()) {
return ''; return '';
} }
$gridField->addExtraClass('has-nested'); $gridField->addExtraClass('has-nested');
if ($record->ID && $record->exists()) { if ($record->ID && $record->exists()) {
$this->gridField = $gridField; $this->gridField = $gridField;
$this->record = $record;
$relationName = $this->getRelationName(); $relationName = $this->getRelationName();
if (!$record->hasMethod($relationName)) { if (!$record->hasMethod($relationName)) {
return ''; return '';
@ -212,7 +246,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
return ''; return '';
} elseif (is_string($this->canExpandCheck) } elseif (is_string($this->canExpandCheck)
&& $record->hasMethod($this->canExpandCheck) && $record->hasMethod($this->canExpandCheck)
&& !$this->record->{$this->canExpandCheck}($record) && !$record->{$this->canExpandCheck}($record)
) { ) {
return ''; return '';
} }
@ -251,15 +285,30 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
} }
/** /**
* @param GridField $field * @param GridField $gridField
* @return array
*/ */
public function getHTMLFragments($field) public function getHTMLFragments($gridField)
{ {
if (DataObject::has_extension($field->getModelClass(), Hierarchy::class)) { /**
$field->setAttribute('data-url-movetoparent', $field->Link('movetoparent')); * If we have a DataObject with the hierarchy extension, we want to allow moving items to a new parent.
* This is enabled by setting the data-url-movetoparent attribute on the grid field, so that the client
* javascript can handle the move.
* Implemented in getHTMLFragments since this attribute needs to be added before any rendering happens.
*/
if (is_a($gridField->getModelClass(), DataObject::class, true)
&& DataObject::has_extension($gridField->getModelClass(), Hierarchy::class)
) {
$gridField->setAttribute('data-url-movetoparent', $gridField->Link('movetoparent'));
} }
return [];
} }
/**
* Handle moving a record to a new parent
*
* @return string
*/
public function handleMoveToParent(GridField $gridField, $request) public function handleMoveToParent(GridField $gridField, $request)
{ {
$move = $request->postVar('move'); $move = $request->postVar('move');
@ -326,22 +375,30 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
return $gridField->FieldHolder(); return $gridField->FieldHolder();
} }
/**
* Handle the request to show a nested item
*
* @param GridField $gridField
* @param HTTPRequest|null $request
* @param ViewableData|null $record
* @return HTTPResponse|RequestHandler
*/
public function handleNestedItem(GridField $gridField, $request = null, $record = null) public function handleNestedItem(GridField $gridField, $request = null, $record = null)
{ {
$nestingLevel = $this->getNestingLevel($gridField); if ($this->atMaxNestingLevel($gridField)) {
if ($nestingLevel >= $this->getMaxNestingLevel()) {
throw new Exception('Max nesting level reached'); throw new Exception('Max nesting level reached');
} }
if (!$record && $request) { $list = $gridField->getList();
if (!$record && $request && $list instanceof Filterable) {
$recordID = $request->param('RecordID'); $recordID = $request->param('RecordID');
$record = $gridField->getList()->byID($recordID); $record = $list->byID($recordID);
} }
if (!$record) { if (!$record) {
return ''; return '';
} }
$relationName = $this->getRelationName(); $relationName = $this->getRelationName();
if (!$record->hasMethod($relationName)) { if (!$record->hasMethod($relationName)) {
return ''; throw new Exception('Invalid relation name');
} }
$manager = $this->getStateManager(); $manager = $this->getStateManager();
$stateRequest = $request ?: $gridField->getForm()->getRequestHandler()->getRequest(); $stateRequest = $request ?: $gridField->getForm()->getRequestHandler()->getRequest();
@ -349,7 +406,6 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
$gridField->getState(false)->setValue($gridStateStr); $gridField->getState(false)->setValue($gridStateStr);
} }
$this->gridField = $gridField; $this->gridField = $gridField;
$this->record = $record;
$itemRequest = GridFieldNestedFormItemRequest::create( $itemRequest = GridFieldNestedFormItemRequest::create(
$gridField, $gridField,
$this, $this,
@ -373,11 +429,19 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
} }
} }
/**
* Handle the request to toggle a nested item in the gridfield state
*
* @param GridField $gridField
* @param HTTPRequest|null $request
* @param ViewableData|null $record
*/
public function toggleNestedItem(GridField $gridField, $request = null, $record = null) public function toggleNestedItem(GridField $gridField, $request = null, $record = null)
{ {
if (!$record) { $list = $gridField->getList();
if (!$record && $request && $list instanceof Filterable) {
$recordID = $request->param('RecordID'); $recordID = $request->param('RecordID');
$record = $gridField->getList()->byID($recordID); $record = $list->byID($recordID);
} }
$manager = $this->getStateManager(); $manager = $this->getStateManager();
if ($gridStateStr = $manager->getStateFromRequest($gridField, $request)) { if ($gridStateStr = $manager->getStateFromRequest($gridField, $request)) {
@ -389,14 +453,20 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
$state->$stateRelation = (int)$request->getVar('toggle'); $state->$stateRelation = (int)$request->getVar('toggle');
} }
public function Link($action = null) /**
* Get the link for the nested grid field
*/
public function Link($action = null): string
{ {
$link = Director::absoluteURL(Controller::join_links($this->gridField->Link('nested'), $action)); $link = Director::absoluteURL(Controller::join_links($this->gridField->Link('nested'), $action));
$manager = $this->getStateManager(); $manager = $this->getStateManager();
return $manager->addStateToURL($this->gridField, $link); return $manager->addStateToURL($this->gridField, $link);
} }
public function ToggleLink($action = null) /**
* Get the link for the toggle action
*/
public function ToggleLink($action = null): string
{ {
$link = Director::absoluteURL(Controller::join_links($this->gridField->Link('toggle'), $action, '?toggle=')); $link = Director::absoluteURL(Controller::join_links($this->gridField->Link('toggle'), $action, '?toggle='));
$manager = $this->getStateManager(); $manager = $this->getStateManager();
@ -417,11 +487,13 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
$gridField->getState(false)->setValue($gridStateStr); $gridField->getState(false)->setValue($gridStateStr);
} }
foreach ($request->postVars() as $key => $val) { foreach ($request->postVars() as $key => $val) {
if (preg_match("/{$gridField->getName()}-{$postKey}-(\d+)/", $key, $matches)) { $list = $gridField->getList();
if ($list instanceof Filterable && preg_match("/{$gridField->getName()}-{$postKey}-(\d+)/", $key, $matches)) {
$recordID = $matches[1]; $recordID = $matches[1];
$nestedData = $val; $nestedData = $val;
$record = $gridField->getList()->byID($recordID); $record = $list->byID($recordID);
if ($record) { if ($record) {
/** @var GridField */
$nestedGridField = $this->handleNestedItem($gridField, null, $record); $nestedGridField = $this->handleNestedItem($gridField, null, $record);
$nestedGridField->setValue($nestedData); $nestedGridField->setValue($nestedData);
$nestedGridField->saveInto($record); $nestedGridField->saveInto($record);
@ -433,8 +505,10 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements
public function getManipulatedData(GridField $gridField, SS_List $dataList) public function getManipulatedData(GridField $gridField, SS_List $dataList)
{ {
if ($this->relationName == 'Children' if ($this->relationName == 'Children'
&& is_a($gridField->getModelClass(), DataObject::class, true)
&& DataObject::has_extension($gridField->getModelClass(), Hierarchy::class) && DataObject::has_extension($gridField->getModelClass(), Hierarchy::class)
&& $gridField->getForm()->getController() instanceof ModelAdmin && $gridField->getForm()->getController() instanceof ModelAdmin
&& $dataList instanceof Filterable
) { ) {
$dataList = $dataList->filter('ParentID', 0); $dataList = $dataList->filter('ParentID', 0);
} }

View File

@ -24,9 +24,11 @@ use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
use Symbiote\GridFieldExtensions\GridFieldEditableColumns; use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
use Symbiote\GridFieldExtensions\GridFieldOrderableRows; use Symbiote\GridFieldExtensions\GridFieldOrderableRows;
/**
* Request handler class for nested grid field forms.
*/
class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest
{ {
public function Link($action = null) public function Link($action = null)
{ {
return Controller::join_links($this->component->Link($this->record->ID), $action); return Controller::join_links($this->component->Link($this->record->ID), $action);
@ -72,7 +74,10 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest
} }
if ($this->record->hasExtension(Hierarchy::class)) { if ($this->record->hasExtension(Hierarchy::class)) {
$config->addComponent(new GridFieldNestedForm(), GridFieldOrderableRows::class); $config->addComponent($nestedForm = new GridFieldNestedForm(), GridFieldOrderableRows::class);
// use max nesting level from parent component
$nestedForm->setMaxNestingLevel($this->component->getMaxNestingLevel());
/** @var GridFieldOrderableRows */ /** @var GridFieldOrderableRows */
$orderableRows = $config->getComponentByType(GridFieldOrderableRows::class); $orderableRows = $config->getComponentByType(GridFieldOrderableRows::class);
if ($orderableRows) { if ($orderableRows) {
@ -117,8 +122,6 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest
$gridField->setAttribute('data-class', str_replace('\\', '-', $relationClass)); $gridField->setAttribute('data-class', str_replace('\\', '-', $relationClass));
$gridField->addExtraClass('nested'); $gridField->addExtraClass('nested');
$form = new Form($this, 'ItemEditForm', $fields, new FieldList()); $form = new Form($this, 'ItemEditForm', $fields, new FieldList());
$form->setStrictFormMethodCheck(false);
$form->disableSecurityToken();
$className = str_replace('\\', '-', get_class($this->record)); $className = str_replace('\\', '-', get_class($this->record));
$state = $this->gridField->getState()->GridFieldNestedForm; $state = $this->gridField->getState()->GridFieldNestedForm;

View File

@ -1,4 +1,10 @@
<a class="btn btn-secondary btn--no-text btn--icon-large <% if $Toggle == 'open' %>font-icon-down-dir<% else %>font-icon-right-dir<% end_if %> cms-panel-link list-children-link" data-pjax-target="$PjaxFragment" href="$Link" data-toggle="$ToggleLink"></a> <button
class="btn btn-secondary btn--no-text btn--icon-large <% if $Toggle == 'open' %>font-icon-down-dir<% else %>font-icon-right-dir<% end_if %> cms-panel-link list-children-link"
aria-expanded="<% if $Toggle == 'open' %>true<% else %>false<% end_if %>"
data-pjax-target="$PjaxFragment"
data-url="$Link"
data-toggle="$ToggleLink"
></button>
<% if $Toggle == 'open' %> <% if $Toggle == 'open' %>
$NestedField $NestedField
<% else %> <% else %>