Compare commits

...

5 Commits

Author SHA1 Message Date
Niklas Forsdahl fc40420daa Added configurable max nesting level for nested GridFields 2024-04-18 18:04:56 +03:00
Niklas Forsdahl 4fc20fb771 Added one more unit test for GridFieldNestedForm 2024-04-18 17:51:27 +03:00
Niklas Forsdahl f8c777dcc5 Changed naming schema for nested GridFields, to not include [ or ]
characters. This makes them work also with the search component.
2024-04-18 14:39:25 +03:00
Niklas Forsdahl c415d43731 Fixed linting issues 2024-04-18 12:41:58 +03:00
Niklas Forsdahl c043220949 Changed base-class of GridFieldNestedForm, it doesn't share much with
GridFieldDetailForm anymore.
Also changed how Breadcrumbs work.
2024-04-18 10:56:35 +03:00
5 changed files with 193 additions and 26 deletions

View File

@ -612,6 +612,8 @@
$('.ss-gridfield-orderable.has-nested > .grid-field__table > tbody, .ss-gridfield-orderable.nested > .grid-field__table > tbody').entwine({
onadd: function() {
this._super();
let preventReorderUpdate = false;
let updateTimeouts = [];
let gridField = this.getGridField();
if (gridField.data("url-movetoparent")) {
let parentID = 0;
@ -638,7 +640,7 @@
window.clearTimeout(timeout);
}
let childID = ui.item.attr('data-id');
let parentIntoChild = $(e.target).closest('.grid-field[data-name*="[GridFieldNestedForm]['+childID+']"]').length;
let parentIntoChild = $(e.target).closest('.grid-field[data-name*="-GridFieldNestedForm-'+childID+'"]').length;
if (parentIntoChild) {
// parent dragged into child, cancel sorting
ui.sender.sortable("cancel");

View File

@ -8,12 +8,16 @@ use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Forms\GridField\AbstractGridFieldComponent;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridField_ColumnProvider;
use SilverStripe\Forms\GridField\GridField_DataManipulator;
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
use SilverStripe\Forms\GridField\GridField_SaveHandler;
use SilverStripe\Forms\GridField\GridField_URLHandler;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\Forms\GridField\GridFieldStateAware;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface;
@ -26,19 +30,27 @@ use Symbiote\GridFieldExtensions\GridFieldOrderableRows;
/**
* Gridfield component for nesting GridFields
*/
class GridFieldNestedForm extends GridFieldDetailForm implements
class GridFieldNestedForm extends AbstractGridFieldComponent implements
GridField_URLHandler,
GridField_ColumnProvider,
GridField_SaveHandler,
GridField_HTMLProvider,
GridField_DataManipulator
{
use Configurable, GridFieldStateAware;
const POST_KEY = 'GridFieldNestedForm';
private static $allowed_actions = [
'handleNestedItem'
];
private static $max_nesting_level = 10;
/**
* @var string
*/
protected $name;
protected $expandNested = false;
protected $forceCloseNested = false;
protected $gridField = null;
@ -46,6 +58,7 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
protected $relationName = 'Children';
protected $inlineEditable = false;
protected $canExpandCheck = null;
protected $maxNestingLevel = null;
public function __construct($name = 'NestedForm')
{
@ -130,6 +143,32 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
return $this;
}
/**
* Set the maximum nesting level allowed for nested grid fields
* @param int $level
*/
public function setMaxNestingLevel($level)
{
$this->maxNestingLevel = $level;
return $this;
}
public function getMaxNestingLevel()
{
return $this->maxNestingLevel ?: $this->config()->max_nesting_level;
}
protected function getNestingLevel($gridField)
{
$level = 0;
$c = $gridField->getForm()->getController();
while ($c && $c instanceof GridFieldDetailForm_ItemRequest) {
$c = $c->getController();
$level++;
}
return $level;
}
public function getColumnMetadata($gridField, $columnName)
{
return ['title' => ''];
@ -154,6 +193,10 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
public function getColumnContent($gridField, $record, $columnName)
{
$nestingLevel = $this->getNestingLevel($gridField);
if ($nestingLevel >= $this->getMaxNestingLevel()) {
return '';
}
$gridField->addExtraClass('has-nested');
if ($record->ID && $record->exists()) {
$this->gridField = $gridField;
@ -163,9 +206,14 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
return '';
}
if ($this->canExpandCheck) {
if (is_callable($this->canExpandCheck) && !call_user_func($this->canExpandCheck, $record)) {
if (is_callable($this->canExpandCheck)
&& !call_user_func($this->canExpandCheck, $record)
) {
return '';
} elseif (is_string($this->canExpandCheck) && $record->hasMethod($this->canExpandCheck) && !$this->record->{$this->canExpandCheck}($record)) {
} elseif (is_string($this->canExpandCheck)
&& $record->hasMethod($this->canExpandCheck)
&& !$this->record->{$this->canExpandCheck}($record)
) {
return '';
}
}
@ -173,7 +221,11 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
$className = str_replace('\\', '-', get_class($record));
$state = $gridField->State->GridFieldNestedForm;
$stateRelation = $className.'-'.$record->ID.'-'.$this->relationName;
if (!$this->forceCloseNested && (($this->expandNested && $record->$relationName()->count() > 0) || ($state && (int)$state->getData($stateRelation) === 1))) {
$openState = $state && (int)$state->getData($stateRelation) === 1;
$forceExpand = $this->expandNested && $record->$relationName()->count() > 0;
if (!$this->forceCloseNested
&& ($forceExpand || $openState)
) {
$toggle = 'open';
}
@ -219,7 +271,11 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
if ($id) {
// should be possible either on parent or child grid field, or nested grid field from parent
$parent = $to ? $list->byID($to) : null;
if (!$parent && $to && $gridField->getForm()->getController() instanceof GridFieldNestedFormItemRequest && $gridField->getForm()->getController()->getRecord()->ID == $to) {
if (!$parent
&& $to
&& $gridField->getForm()->getController() instanceof GridFieldNestedFormItemRequest
&& $gridField->getForm()->getController()->getRecord()->ID == $to
) {
$parent = $gridField->getForm()->getController()->getRecord();
}
$child = $list->byID($id);
@ -264,6 +320,10 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
public function handleNestedItem(GridField $gridField, $request = null, $record = null)
{
$nestingLevel = $this->getNestingLevel($gridField);
if ($nestingLevel >= $this->getMaxNestingLevel()) {
throw new Exception('Max nesting level reached');
}
if (!$record && $request) {
$recordID = $request->param('RecordID');
$record = $gridField->getList()->byID($recordID);
@ -276,12 +336,19 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
return '';
}
$manager = $this->getStateManager();
if ($gridStateStr = $manager->getStateFromRequest($gridField, $request ?: $gridField->getForm()->getRequestHandler()->getRequest())) {
$stateRequest = $request ?: $gridField->getForm()->getRequestHandler()->getRequest();
if ($gridStateStr = $manager->getStateFromRequest($gridField, $stateRequest)) {
$gridField->getState(false)->setValue($gridStateStr);
}
$this->gridField = $gridField;
$this->record = $record;
$itemRequest = GridFieldNestedFormItemRequest::create($gridField, $this, $record, $gridField->getForm()->getController(), $this->name);
$itemRequest = GridFieldNestedFormItemRequest::create(
$gridField,
$this,
$record,
$gridField->getForm()->getController(),
$this->name
);
if ($request) {
$pjaxFragment = $request->getHeader('X-Pjax');
$targetPjaxFragment = str_replace('\\', '-', get_class($record)).'-'.$record->ID.'-'.$this->relationName;
@ -330,32 +397,37 @@ class GridFieldNestedForm extends GridFieldDetailForm implements
public function handleSave(GridField $gridField, DataObjectInterface $record)
{
$postKey = self::POST_KEY;
$value = $gridField->Value();
if (!isset($value[self::POST_KEY]) || !is_array($value[self::POST_KEY])) {
return;
}
if (isset($value['GridState']) && $value['GridState']) {
// set grid state from value, to store open/closed toggle state for nested forms
$gridField->getState(false)->setValue($value['GridState']);
}
$manager = $this->getStateManager();
if ($gridStateStr = $manager->getStateFromRequest($gridField, $gridField->getForm()->getRequestHandler()->getRequest())) {
$request = $gridField->getForm()->getRequestHandler()->getRequest();
if ($gridStateStr = $manager->getStateFromRequest($gridField, $request)) {
$gridField->getState(false)->setValue($gridStateStr);
}
foreach ($value[self::POST_KEY] as $recordID => $nestedData) {
$record = $gridField->getList()->byID($recordID);
if ($record) {
$nestedGridField = $this->handleNestedItem($gridField, null, $record);
$nestedGridField->setValue($nestedData);
$nestedGridField->saveInto($record);
foreach ($request->postVars() as $key => $val) {
if (preg_match("/{$gridField->getName()}-{$postKey}-(\d+)/", $key, $matches)) {
$recordID = $matches[1];
$nestedData = $val;
$record = $gridField->getList()->byID($recordID);
if ($record) {
$nestedGridField = $this->handleNestedItem($gridField, null, $record);
$nestedGridField->setValue($nestedData);
$nestedGridField->saveInto($record);
}
}
}
}
public function getManipulatedData(GridField $gridField, SS_List $dataList)
{
if ($this->relationName == 'Children' && DataObject::has_extension($gridField->getModelClass(), Hierarchy::class) && $gridField->getForm()->getController() instanceof ModelAdmin) {
if ($this->relationName == 'Children'
&& DataObject::has_extension($gridField->getModelClass(), Hierarchy::class)
&& $gridField->getForm()->getController() instanceof ModelAdmin
) {
$dataList = $dataList->filter('ParentID', 0);
}
return $dataList;

View File

@ -3,12 +3,14 @@
namespace Symbiote\GridFieldExtensions;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldAddNewButton;
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\Forms\GridField\GridFieldEditButton;
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
@ -17,6 +19,7 @@ use SilverStripe\Forms\GridField\GridFieldSortableHeader;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\View\ArrayData;
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
use Symbiote\GridFieldExtensions\GridFieldOrderableRows;
@ -32,10 +35,19 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest
public function ItemEditForm()
{
$config = new GridFieldConfig_RecordEditor();
/** @var GridFieldDetailForm */
$detailForm = $config->getComponentByType(GridFieldDetailForm::class);
$detailForm->setItemEditFormCallback(function (Form $form, $itemRequest) {
$breadcrumbs = $itemRequest->Breadcrumbs(false);
if ($breadcrumbs && $breadcrumbs->exists()) {
$form->Backlink = $breadcrumbs->first()->Link;
}
});
$relationName = $this->component->getRelationName();
$list = $this->record->$relationName();
if ($relationName == 'Children' && $this->record->hasExtension(Hierarchy::class)) {
// we really need a HasManyList for Hierarchy objects, otherwise adding new items will not properly set the ParentID
// we really need a HasManyList for Hierarchy objects,
// otherwise adding new items will not properly set the ParentID
$list = HasManyList::create(get_class($this->record), 'ParentID')
->setDataQueryParam($this->record->getInheritableQueryParams())
->forForeignID($this->record->ID);
@ -53,6 +65,10 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest
if ($relationClass == get_class($this->record)) {
$config->removeComponentsByType(GridFieldSortableHeader::class);
$config->removeComponentsByType(GridFieldFilterHeader::class);
if ($this->gridField->getConfig()->getComponentByType(GridFieldOrderableRows::class)) {
$config->addComponent(new GridFieldOrderableRows());
}
}
if ($this->record->hasExtension(Hierarchy::class)) {
@ -82,7 +98,17 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest
$title = _t(get_class($this->record).'.'.strtoupper($relationName), ' ');
$fields = new FieldList(
$gridField = new GridField($this->component->getGridField()->getName().'['.GridFieldNestedForm::POST_KEY.']['.$this->record->ID.']', $title, $list, $config)
$gridField = new GridField(
sprintf(
'%s-%s-%s',
$this->component->getGridField()->getName(),
GridFieldNestedForm::POST_KEY,
$this->record->ID
),
$title,
$list,
$config
)
);
if (!trim($title)) {
$gridField->addExtraClass('empty-title');
@ -113,7 +139,34 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest
/** @var ArrayList $items */
$items = $this->popupController->Breadcrumbs($unlinked);
if (!$items) {
$items = ArrayList::create();
}
if ($this->record && $this->record->ID) {
$title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
$items->push(ArrayData::create([
'Title' => $title,
'Link' => parent::Link()
]));
} else {
$items->push(ArrayData::create([
'Title' => _t(
'SilverStripe\\Forms\\GridField\\GridField.NewRecord',
'New {type}',
['type' => $this->record->i18n_singular_name()]
),
'Link' => false
]));
}
foreach ($items as $item) {
if ($item->Link) {
$item->Link = $this->gridField->addAllStateToUrl(Director::absoluteURL($item->Link));
}
}
$this->extend('updateBreadcrumbs', $items);
return $items;
}

View File

@ -11,6 +11,8 @@ use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
use SilverStripe\ORM\ArrayList;
use Symbiote\GridFieldExtensions\GridFieldNestedForm;
use Symbiote\GridFieldExtensions\Tests\Stub\StubHierarchy;
use Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered;
use Symbiote\GridFieldExtensions\Tests\Stub\StubParent;
use Symbiote\GridFieldExtensions\Tests\Stub\TestController;
class GridFieldNestedFormTest extends SapphireTest
@ -18,7 +20,9 @@ class GridFieldNestedFormTest extends SapphireTest
protected static $fixture_file = 'GridFieldNestedFormTest.yml';
protected static $extra_dataobjects = [
StubHierarchy::class
StubHierarchy::class,
StubParent::class,
StubOrdered::class
];
public function testHierarchy()
@ -55,4 +59,30 @@ class GridFieldNestedFormTest extends SapphireTest
$child2 = $this->objFromFixture(StubHierarchy::class, 'item1_1_1');
$this->assertEquals($child2->ID, $list->first()->ID);
}
public function testHasManyRelation()
{
// test that GridFieldNestedForm works with HasMany relations
$parent = $this->objFromFixture(StubParent::class, 'parent1');
$list = new ArrayList([$parent]);
$config = new GridFieldConfig_RecordEditor();
$config->addComponent($nestedForm = new GridFieldNestedForm());
$nestedForm->setRelationName('MyHasMany');
$controller = new TestController('Test');
$form = new Form($controller, 'TestForm', new FieldList(
$gridField = new GridField(__FUNCTION__, 'test', $list, $config)
), new FieldList());
$request = new HTTPRequest('GET', '/');
$itemEditForm = $nestedForm->handleNestedItem($gridField, $request, $parent);
$this->assertNotNull($itemEditForm);
$nestedGridField = $itemEditForm->Fields()->first();
$this->assertNotNull($nestedGridField);
$list = $nestedGridField->getList();
$this->assertEquals(2, $list->count());
$child1 = $this->objFromFixture(StubOrdered::class, 'child1');
$this->assertEquals($child1->ID, $list->first()->ID);
}
}

View File

@ -6,4 +6,14 @@ Symbiote\GridFieldExtensions\Tests\Stub\StubHierarchy:
ParentID: =>Symbiote\GridFieldExtensions\Tests\Stub\StubHierarchy.item1
item1_1_1:
Title: 'Item 1.1.1'
ParentID: =>Symbiote\GridFieldExtensions\Tests\Stub\StubHierarchy.item1_1
ParentID: =>Symbiote\GridFieldExtensions\Tests\Stub\StubHierarchy.item1_1
Symbiote\GridFieldExtensions\Tests\Stub\StubParent:
parent1:
Title: 'Parent 1'
Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered:
child1:
Title: 'Child 1'
ParentID: =>Symbiote\GridFieldExtensions\Tests\Stub\StubParent.parent1
child2:
Title: 'Child 2'
ParentID: =>Symbiote\GridFieldExtensions\Tests\Stub\StubParent.parent1