From db3edd7f1644788cfd3717b89ebcdce6d21565fa Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Fri, 12 Apr 2024 16:45:29 +0300 Subject: [PATCH 01/24] NEW Initial nested gridfield component --- css/GridFieldExtensions.css | 20 ++ javascript/GridFieldExtensions.js | 159 +++++++++ src/GridFieldNestedForm.php | 330 ++++++++++++++++++ src/GridFieldNestedForm_ItemRequest.php | 115 ++++++ .../GridFieldNestedForm.ss | 6 + 5 files changed, 630 insertions(+) create mode 100644 src/GridFieldNestedForm.php create mode 100644 src/GridFieldNestedForm_ItemRequest.php create mode 100755 templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss diff --git a/css/GridFieldExtensions.css b/css/GridFieldExtensions.css index 7aec1b7..2bd2760 100644 --- a/css/GridFieldExtensions.css +++ b/css/GridFieldExtensions.css @@ -223,3 +223,23 @@ .grid-field-inline-new--multi-class-list__visible { display: block; } + +/** + * GridFieldNestedForm + */ +.grid-field tr.nested-gridfield td.gridfield-holder { + padding-left: 60px; +} + +.grid-field.nested table tbody tr:not(.nested-gridfield) { + border-left: 1px solid #dbe0e9; +} + +.grid-field.nested table tbody tr:not(.nested-gridfield).last { + border-bottom: 1px solid #dbe0e9; +} + +.ss-gridfield-orderable.has-nested > .grid-field__table > .ss-gridfield-items > .ss-gridfield-item.ui-droppable-active.ui-state-highlight { + border: 0; + background-color: #fbf9ee; +} diff --git a/javascript/GridFieldExtensions.js b/javascript/GridFieldExtensions.js index 2d7accf..886308f 100644 --- a/javascript/GridFieldExtensions.js +++ b/javascript/GridFieldExtensions.js @@ -510,5 +510,164 @@ this.parent().find('.ss-gridfield-pagesize-submit').trigger('click'); } }); + + /** + * GridFieldNestedForm + */ + $('.grid-field .col-listChildrenLink a').entwine({ + onclick: function(e) { + let gridField = $(this).closest('.grid-field'); + let currState = gridField.getState(); + let toggleState = false; + let pjaxTarget = $(this).attr('data-pjax-target'); + if ($(this).hasClass('font-icon-right-dir')) { + toggleState = true; + } + if (typeof currState['GridFieldNestedForm'] == 'undefined' || currState['GridFieldNestedForm'] == null) { + currState['GridFieldNestedForm'] = {}; + } + currState['GridFieldNestedForm'][$(this).attr('data-pjax-target')] = toggleState; + gridField.setState('GridFieldNestedForm', currState['GridFieldNestedForm']); + if (toggleState) { + if (!$(this).closest('tr').next('.nested-gridfield').length) { + let data = {}; + let stateInput = gridField.find('input.gridstate').first(); + data[stateInput.attr('name')] = JSON.stringify(currState); + if (window.location.search) { + let searchParams = window.location.search.replace('?', '').split('&'); + for (let i = 0; i < searchParams.length; i++) { + let parts = searchParams[i].split('='); + data[parts[0]] = parts[1]; + } + } + $.ajax({ + type: 'POST', + url: $(this).attr('href'), + data: data, + headers: { + 'X-Pjax': pjaxTarget + }, + success: function(data) { + if (data && data[pjaxTarget]) { + gridField.find(`[data-pjax-fragment="${pjaxTarget}"]`).replaceWith(data[pjaxTarget]); + } + } + }); + } + else { + $(this).closest('tr').next('.nested-gridfield').show(); + $.ajax({ + url: $(this).attr('data-toggle')+'1' + }); + } + $(this).removeClass('font-icon-right-dir'); + $(this).addClass('font-icon-down-dir'); + } + else { + $.ajax({ + url: $(this).attr('data-toggle')+'0' + }); + $(this).closest('tr').next('.nested-gridfield').hide(); + $(this).removeClass('font-icon-down-dir'); + $(this).addClass('font-icon-right-dir'); + } + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); + + // move nested gridfields onto their own rows below this row, to make it look nicer + $('.col-listChildrenLink > .grid-field.nested').entwine({ + onadd: function() { + let nrOfColumns = $(this).closest('tr').children('td').length; + let evenOrOdd = 'even'; + if ($(this).closest('tr').hasClass('odd')) { + evenOrOdd = 'odd'; + } + if ($(this).closest('.grid-field').hasClass('editable-gridfield')) { + $(this).find('tr').removeClass('even').removeClass('odd').addClass(evenOrOdd); + } + + if ($(this).closest('tr').next('tr.nested-gridfield').length) { + $(this).closest('tr').next('tr.nested-gridfield').remove(); + } + + // add a new table row, with one table cell which spans all columns + $(this).closest('tr').after(''); + // move this field into the newly created row + $(this).appendTo($(this).closest('tr').next('tr').find('td').first()); + $(this).show(); + this._super(); + } + }); + + $('.ss-gridfield-orderable.has-nested > .grid-field__table > tbody, .ss-gridfield-orderable.nested > .grid-field__table > tbody').entwine({ + onadd: function() { + this._super(); + let gridField = this.getGridField(); + if (gridField.data("url-movetoparent")) { + let parentID = 0; + let parentItem = gridField.closest('.nested-gridfield').prev('.ss-gridfield-item'); + if (parentItem && parentItem.length) { + parentID = parentItem.attr('data-id'); + } + this.sortable('option', 'connectWith', '.ss-gridfield-orderable tbody'); + this.sortable('option', 'start', function(e, ui) { + if (ui.item.find('.col-listChildrenLink').length && ui.item.next('.ui-sortable-placeholder').next('.nested-gridfield').length) { + if (ui.item.find('.col-listChildrenLink a').hasClass('font-icon-down-dir')) { + ui.item.find('.col-listChildrenLink a').removeClass('font-icon-down-dir'); + ui.item.find('.col-listChildrenLink a').addClass('font-icon-right-dir'); + } + ui.item.next('.ui-sortable-placeholder').next('.nested-gridfield').remove(); + let pjaxFragment = ui.item.find('.col-listChildrenLink a').attr('data-pjax-target'); + ui.item.find('.col-listChildrenLink').append(``); + } + }); + this.sortable('option', 'receive', function(e, ui) { + preventReorderUpdate = true; + while (updateTimeouts.length) { + let timeout = updateTimeouts.shift(); + window.clearTimeout(timeout); + } + let childID = ui.item.attr('data-id'); + let parentIntoChild = $(e.target).closest('.grid-field[data-name*="[GridFieldNestedForm]['+childID+']"]').length; + if (parentIntoChild) { + // parent dragged into child, or widget dragged to root, cancel sorting + ui.sender.sortable("cancel"); + e.preventDefault(); + e.stopPropagation(); + window.setTimeout(function() { + preventReorderUpdate = false; + }, 500); + return false; + } + let sortInput = ui.item.find('input.ss-orderable-hidden-sort'); + let sortName = sortInput.attr('name'); + let index = sortName.indexOf('[GridFieldEditableColumns]'); + sortInput.attr('name', gridField.attr('data-name')+sortName.substring(index)); + gridField.find('> .grid-field__table > tbody').rebuildSort(); + gridField.reload({ + url: gridField.data("url-movetoparent"), + data: [ + { name: "move[id]", value: childID}, + { name: "move[parent]", value: parentID} + ] + }, function() { + preventReorderUpdate = false; + }); + }); + let updateCallback = this.sortable('option', 'update'); + this.sortable('option', 'update', function(e, ui) { + if (!preventReorderUpdate) { + let timeout = window.setTimeout(function() { + updateCallback(e, ui); + }, 500); + updateTimeouts.push(timeout); + } + }); + } + } + }); }); })(jQuery); diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php new file mode 100644 index 0000000..ee424dd --- /dev/null +++ b/src/GridFieldNestedForm.php @@ -0,0 +1,330 @@ +name = $name; + } + + public function getGridField() + { + return $this->gridField; + } + + public function getRelationName() + { + return $this->relationName; + } + + public function setRelationName($relationName) + { + $this->relationName = $relationName; + return $this; + } + + public function getInlineEditable() + { + return $this->inlineEditable; + } + + public function setInlineEditable($editable) + { + $this->inlineEditable = $editable; + return $this; + } + + public function setExpandNested($expandNested) + { + $this->expandNested = $expandNested; + return $this; + } + + public function setForceClosedNested($forceClosed) + { + $this->forceCloseNested = $forceClosed; + return $this; + } + + public function setCanExpandCheck($checkFunction) + { + $this->canExpandCheck = $checkFunction; + return $this; + } + + public function getColumnMetadata($gridField, $columnName) + { + return ['title' => '']; + } + + public function getColumnsHandled($gridField) + { + return ['ToggleNested']; + } + + public function getColumnAttributes($gridField, $record, $columnName) + { + return ['class' => 'col-listChildrenLink grid-field__col-compact']; + } + + public function augmentColumns($gridField, &$columns) + { + if (!in_array('ToggleNested', $columns)) { + array_splice($columns, 0, 0, 'ToggleNested'); + } + } + + public function getColumnContent($gridField, $record, $columnName) + { + $gridField->addExtraClass('has-nested'); + if ($record->ID && $record->exists()) { + $this->gridField = $gridField; + $this->record = $record; + $relationName = $this->getRelationName(); + if (!$record->hasMethod($relationName)) { + return ''; + } + if ($this->canExpandCheck) { + 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)) { + return ''; + } + } + $toggle = 'closed'; + $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))) { + $toggle = 'open'; + } + + GridFieldExtensions::include_requirements(); + + return ViewableData::create()->customise([ + 'Toggle' => $toggle, + 'Link' => $this->Link($record->ID), + 'ToggleLink' => $this->ToggleLink($record->ID), + 'PjaxFragment' => $stateRelation, + 'NestedField' => ($toggle == 'open') ? $this->handleNestedItem($gridField, null, $record): ' ' + ])->renderWith('Symbiote\GridFieldExtensions\GridFieldNestedForm'); + } + } + + public function getURLHandlers($gridField) + { + return [ + 'nested/$RecordID/$NestedAction' => 'handleNestedItem', + 'toggle/$RecordID' => 'toggleNestedItem', + 'POST movetoparent' => 'handleMoveToParent' + ]; + } + + /** + * @param GridField $field + */ + public function getHTMLFragments($field) + { + if (DataObject::has_extension($field->getModelClass(), Hierarchy::class)) { + $field->setAttribute('data-url-movetoparent', $field->Link('movetoparent')); + } + } + + public function handleMoveToParent(GridField $gridField, $request) + { + $move = $request->postVar('move'); + /** @var DataList */ + $list = $gridField->getList(); + $id = isset($move['id']) ? (int) $move['id'] : null; + $to = isset($move['parent']) ? (int)$move['parent'] : null; + $parent = null; + 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 GridFieldNestedForm_ItemRequest && $gridField->getForm()->getController()->getRecord()->ID == $to) { + $parent = $gridField->getForm()->getController()->getRecord(); + } + $child = $list->byID($id); + if ($parent || $child || $to === 0) { + if (!$parent && $to) { + $parent = DataList::create($gridField->getModelClass())->byID($to); + } + if (!$child) { + $child = DataList::create($gridField->getModelClass())->byID($id); + } + if ($child) { + if ($child->hasExtension(Hierarchy::class)) { + $child->ParentID = $parent ? $parent->ID : 0; + } + $validationResult = $child->validate(); + if ($validationResult->isValid()) { + if ($child->hasExtension(Versioned::class)) { + $child->writeToStage(Versioned::DRAFT); + } else { + $child->write(); + } + + /** @var GridFieldOrderableRows */ + $orderableRows = $gridField->getConfig()->getComponentByType(GridFieldOrderableRows::class); + if ($orderableRows) { + $orderableRows->setImmediateUpdate(true); + try { + $orderableRows->handleReorder($gridField, $request); + } catch (Exception $e) { + } + } + } else { + $messages = $validationResult->getMessages(); + $message = array_pop($messages); + throw new HTTPResponse_Exception($message['message'], 400); + } + } + } + } + return $gridField->FieldHolder(); + } + + public function handleNestedItem(GridField $gridField, $request = null, $record = null) + { + if (!$record && $request) { + $recordID = $request->param('RecordID'); + $record = $gridField->getList()->byID($recordID); + } + if (!$record) { + return ''; + } + $relationName = $this->getRelationName(); + if (!$record->hasMethod($relationName)) { + return ''; + } + $manager = $this->getStateManager(); + if ($gridStateStr = $manager->getStateFromRequest($gridField, $request ?: $gridField->getForm()->getRequestHandler()->getRequest())) { + $gridField->getState(false)->setValue($gridStateStr); + } + $this->gridField = $gridField; + $this->record = $record; + $itemRequest = new GridFieldNestedForm_ItemRequest($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; + if ($pjaxFragment == $targetPjaxFragment) { + $pjaxReturn = [$pjaxFragment => $itemRequest->ItemEditForm()->Fields()->first()->forTemplate()]; + $response = new HTTPResponse(json_encode($pjaxReturn)); + $response->addHeader('Content-Type', 'text/json'); + return $response; + } else { + return $itemRequest->ItemEditForm(); + } + } else { + return $itemRequest->ItemEditForm()->Fields()->first(); + } + } + + public function toggleNestedItem(GridField $gridField, $request = null, $record = null) + { + if (!$record) { + $recordID = $request->param('RecordID'); + $record = $gridField->getList()->byID($recordID); + } + $manager = $this->getStateManager(); + if ($gridStateStr = $manager->getStateFromRequest($gridField, $request)) { + $gridField->getState(false)->setValue($gridStateStr); + } + $className = str_replace('\\', '-', get_class($record)); + $state = $gridField->getState()->GridFieldNestedForm; + $stateRelation = $className.'-'.$record->ID.'-'.$this->getRelationName(); + $state->$stateRelation = (int)$request->getVar('toggle'); + } + + public function Link($action = null) + { + $link = Director::absoluteURL(Controller::join_links($this->gridField->Link('nested'), $action)); + $manager = $this->getStateManager(); + return $manager->addStateToURL($this->gridField, $link); + } + + public function ToggleLink($action = null) + { + $link = Director::absoluteURL(Controller::join_links($this->gridField->Link('toggle'), $action, '?toggle=')); + $manager = $this->getStateManager(); + return $manager->addStateToURL($this->gridField, $link); + } + + public function handleSave(GridField $gridField, DataObjectInterface $record) + { + $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())) { + $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); + } + } + } + + public function getManipulatedData(GridField $gridField, SS_List $dataList) + { + if ($this->relationName == 'Children' && DataObject::has_extension($gridField->getModelClass(), Hierarchy::class) && $gridField->getForm()->getController() instanceof ModelAdmin) { + $dataList = $dataList->filter('ParentID', 0); + } + return $dataList; + } +} diff --git a/src/GridFieldNestedForm_ItemRequest.php b/src/GridFieldNestedForm_ItemRequest.php new file mode 100644 index 0000000..fcdc5bd --- /dev/null +++ b/src/GridFieldNestedForm_ItemRequest.php @@ -0,0 +1,115 @@ +component->Link($this->record->ID), $action); + } + + public function ItemEditForm() + { + $config = new GridFieldConfig_RecordEditor(); + $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 + $list = HasManyList::create(get_class($this->record), 'ParentID') + ->setDataQueryParam($this->record->getInheritableQueryParams()) + ->forForeignID($this->record->ID); + } + $relationClass = $list->dataClass(); + + if ($this->record->hasMethod('getNestedConfig')) { + $config = $this->record->getNestedConfig(); + } else { + $canEdit = $this->record->canEdit(); + if (!$canEdit) { + $config->removeComponentsByType(GridFieldAddNewButton::class); + } + $config->removeComponentsByType(GridFieldPageCount::class); + if ($relationClass == get_class($this->record)) { + $config->removeComponentsByType(GridFieldSortableHeader::class); + $config->removeComponentsByType(GridFieldFilterHeader::class); + } + + if ($this->record->hasExtension(Hierarchy::class)) { + $config->addComponent(new GridFieldNestedForm(), GridFieldOrderableRows::class); + /** @var GridFieldOrderableRows */ + $orderableRows = $config->getComponentByType(GridFieldOrderableRows::class); + if ($orderableRows) { + $orderableRows->setReorderColumnNumber(1); + } + } + + if ($this->component->getInlineEditable() && $canEdit) { + $config->removeComponentsByType(GridFieldDataColumns::class); + $config->addComponent(new GridFieldEditableColumns(), GridFieldEditButton::class); + $config->addComponent(new GridFieldAddNewInlineButton('buttons-before-left')); + $config->removeComponentsByType(GridFieldAddNewButton::class); + /** @var GridFieldNestedForm */ + $nestedForm = $config->getComponentByType(GridFieldNestedForm::class); + if ($nestedForm) { + $nestedForm->setInlineEditable(true); + } + } + } + + $this->record->invokeWithExtensions('updateNestedConfig', $config); + + $fields = new FieldList( + $gridField = new GridField($this->component->getGridField()->getName().'['.GridFieldNestedForm::POST_KEY.']['.$this->record->ID.']', _t(get_class($this->record).'.'.strtoupper($relationName), ' '), $list, $config) + ); + $gridField->setModelClass($relationClass); + $gridField->setAttribute('data-class', str_replace('\\', '-', $relationClass)); + $gridField->addExtraClass('nested'); + $form = new Form($this, 'ItemEditForm', $fields, new FieldList()); + $form->setStrictFormMethodCheck(false); + $form->disableSecurityToken(); + + $className = str_replace('\\', '-', get_class($this->record)); + $state = $this->gridField->getState()->GridFieldNestedForm; + if ($state) { + $stateRelation = $className.'-'.$this->record->ID.'-'.$relationName; + $state->$stateRelation = 1; + } + + $this->record->extend('updateNestedForm', $form); + return $form; + } + + public function Breadcrumbs($unlinked = false) + { + if (!$this->popupController->hasMethod('Breadcrumbs')) { + return null; + } + + /** @var ArrayList $items */ + $items = $this->popupController->Breadcrumbs($unlinked); + + $this->extend('updateBreadcrumbs', $items); + return $items; + } +} diff --git a/templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss b/templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss new file mode 100755 index 0000000..0e2f09d --- /dev/null +++ b/templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss @@ -0,0 +1,6 @@ + +<% if $Toggle == 'open' %> + $NestedField +<% else %> + +<% end_if %> \ No newline at end of file From 27dc6360ae2bd1eeb6ca3ed89ce538dfb22eaf18 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Mon, 15 Apr 2024 09:52:33 +0300 Subject: [PATCH 02/24] Styling for nested GridFields without title. Added loading indicator while nested GridField is being fetched. --- css/GridFieldExtensions.css | 4 ++++ javascript/GridFieldExtensions.js | 9 ++++++++- src/GridFieldNestedForm_ItemRequest.php | 7 ++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/css/GridFieldExtensions.css b/css/GridFieldExtensions.css index 2bd2760..785faa7 100644 --- a/css/GridFieldExtensions.css +++ b/css/GridFieldExtensions.css @@ -231,6 +231,10 @@ padding-left: 60px; } +.grid-field.nested.empty-title .grid-field__title-row th { + padding: 0; +} + .grid-field.nested table tbody tr:not(.nested-gridfield) { border-left: 1px solid #dbe0e9; } diff --git a/javascript/GridFieldExtensions.js b/javascript/GridFieldExtensions.js index 886308f..c7bd02c 100644 --- a/javascript/GridFieldExtensions.js +++ b/javascript/GridFieldExtensions.js @@ -530,6 +530,13 @@ gridField.setState('GridFieldNestedForm', currState['GridFieldNestedForm']); if (toggleState) { if (!$(this).closest('tr').next('.nested-gridfield').length) { + // add loading indicator until the nested gridfield is loaded + let colspan = gridField.find('.grid-field__title-row th').attr('colspan'); + let loadingCell = $('') + .addClass('ss-gridfield-item loading') + .attr('colspan', colspan); + $(this).closest('tr').after($('').append(loadingCell)); + let data = {}; let stateInput = gridField.find('input.gridstate').first(); data[stateInput.attr('name')] = JSON.stringify(currState); @@ -633,7 +640,7 @@ let childID = ui.item.attr('data-id'); let parentIntoChild = $(e.target).closest('.grid-field[data-name*="[GridFieldNestedForm]['+childID+']"]').length; if (parentIntoChild) { - // parent dragged into child, or widget dragged to root, cancel sorting + // parent dragged into child, cancel sorting ui.sender.sortable("cancel"); e.preventDefault(); e.stopPropagation(); diff --git a/src/GridFieldNestedForm_ItemRequest.php b/src/GridFieldNestedForm_ItemRequest.php index fcdc5bd..9488ea5 100644 --- a/src/GridFieldNestedForm_ItemRequest.php +++ b/src/GridFieldNestedForm_ItemRequest.php @@ -78,10 +78,15 @@ class GridFieldNestedForm_ItemRequest extends GridFieldDetailForm_ItemRequest } $this->record->invokeWithExtensions('updateNestedConfig', $config); + + $title = _t(get_class($this->record).'.'.strtoupper($relationName), ' '); $fields = new FieldList( - $gridField = new GridField($this->component->getGridField()->getName().'['.GridFieldNestedForm::POST_KEY.']['.$this->record->ID.']', _t(get_class($this->record).'.'.strtoupper($relationName), ' '), $list, $config) + $gridField = new GridField($this->component->getGridField()->getName().'['.GridFieldNestedForm::POST_KEY.']['.$this->record->ID.']', $title, $list, $config) ); + if (!trim($title)) { + $gridField->addExtraClass('empty-title'); + } $gridField->setModelClass($relationClass); $gridField->setAttribute('data-class', str_replace('\\', '-', $relationClass)); $gridField->addExtraClass('nested'); From 19a7cffa88f31e1877d240ecb6e1634dc31cdeff Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Mon, 15 Apr 2024 10:57:05 +0300 Subject: [PATCH 03/24] Added initial unit test for GridFieldNestedForm --- tests/GridFieldNestedFormTest.php | 58 +++++++++++++++++++++++++++++++ tests/GridFieldNestedFormTest.yml | 9 +++++ tests/Stub/StubHierarchy.php | 20 +++++++++++ 3 files changed, 87 insertions(+) create mode 100644 tests/GridFieldNestedFormTest.php create mode 100644 tests/GridFieldNestedFormTest.yml create mode 100644 tests/Stub/StubHierarchy.php diff --git a/tests/GridFieldNestedFormTest.php b/tests/GridFieldNestedFormTest.php new file mode 100644 index 0000000..d364314 --- /dev/null +++ b/tests/GridFieldNestedFormTest.php @@ -0,0 +1,58 @@ +objFromFixture(StubHierarchy::class, 'item1'); + $list = new ArrayList([$parent]); + $config = new GridFieldConfig_RecordEditor(); + $config->addComponent($nestedForm = new GridFieldNestedForm()); + + $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(1, $list->count()); + + $child1 = $this->objFromFixture(StubHierarchy::class, 'item1_1'); + $this->assertEquals($child1->ID, $list->first()->ID); + $nestedForm = $nestedGridField->getConfig()->getComponentByType(GridFieldNestedForm::class); + $itemEditForm = $nestedForm->handleNestedItem($gridField, $request, $child1); + $this->assertNotNull($itemEditForm); + + $nestedGridField = $itemEditForm->Fields()->first(); + $this->assertNotNull($nestedGridField); + $list = $nestedGridField->getList(); + $this->assertEquals(1, $list->count()); + $child2 = $this->objFromFixture(StubHierarchy::class, 'item1_1_1'); + $this->assertEquals($child2->ID, $list->first()->ID); + } +} diff --git a/tests/GridFieldNestedFormTest.yml b/tests/GridFieldNestedFormTest.yml new file mode 100644 index 0000000..aa5235f --- /dev/null +++ b/tests/GridFieldNestedFormTest.yml @@ -0,0 +1,9 @@ +Symbiote\GridFieldExtensions\Tests\Stub\StubHierarchy: + item1: + Title: 'Item 1' + item1_1: + Title: 'Item 1.1' + ParentID: =>Symbiote\GridFieldExtensions\Tests\Stub\StubHierarchy.item1 + item1_1_1: + Title: 'Item 1.1.1' + ParentID: =>Symbiote\GridFieldExtensions\Tests\Stub\StubHierarchy.item1_1 \ No newline at end of file diff --git a/tests/Stub/StubHierarchy.php b/tests/Stub/StubHierarchy.php new file mode 100644 index 0000000..2b48e47 --- /dev/null +++ b/tests/Stub/StubHierarchy.php @@ -0,0 +1,20 @@ + 'Varchar' + ]; +} From bcc4ea14bbf49088d708987451fa9800bc2eb3bf Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Mon, 15 Apr 2024 11:02:31 +0300 Subject: [PATCH 04/24] Added initial user documentation for Nested GridFields. --- docs/en/index.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/en/index.md b/docs/en/index.md index c565963..e5eb80c 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -152,3 +152,17 @@ $paginator->setItemsPerPage(500); The first shown record will be maintained across page size changes, and the number of pages and current page will be recalculated on each request, based on the current first shown record and page size. + +Nested GridFields +----------------- + +The `GridFieldNestedForm` component allows you to nest GridFields in the UI. It can be used with `DataObject` subclasses +with the `Hierarchy` extension, or by specifying the relation used for nesting. + +```php +// Basic usage, defaults to the Children-method for Hierarchy objects. +$grid->getConfig()->addComponent(GridFieldNestedForm::create()); + +// Usage with custom relation +$grid->getConfig()->addComponent(GridFieldNestedForm::create()->setRelationName('MyRelation')); +``` From 654b7294fa565f983ebd12198bb8e9b2999bd30a Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Thu, 18 Apr 2024 10:17:04 +0300 Subject: [PATCH 05/24] Renamed GridFieldNestedForm_ItemRequest, to conform with coding styles. Also added some php-docs for some public functions. --- src/GridFieldNestedForm.php | 37 ++++++++++++++++++- ...php => GridFieldNestedFormItemRequest.php} | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) rename src/{GridFieldNestedForm_ItemRequest.php => GridFieldNestedFormItemRequest.php} (98%) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index ee424dd..5a88017 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -52,45 +52,78 @@ class GridFieldNestedForm extends GridFieldDetailForm implements $this->name = $name; } + /** + * Get the grid field that this component is attached to + * @return GridField + */ public function getGridField() { return $this->gridField; } + /** + * Get the relation name to use for the nested grid fields + * @return string + */ public function getRelationName() { return $this->relationName; } + /** + * Set the relation name to use for the nested grid fields + * @param string $relationName + */ public function setRelationName($relationName) { $this->relationName = $relationName; return $this; } + /** + * Get whether the nested grid fields should be inline editable + * @return boolean + */ public function getInlineEditable() { return $this->inlineEditable; } + /** + * Set whether the nested grid fields should be inline editable + * @param boolean $editable + */ public function setInlineEditable($editable) { $this->inlineEditable = $editable; return $this; } + /** + * Set whether the nested grid fields should be expanded by default + * @param boolean $expandNested + */ public function setExpandNested($expandNested) { $this->expandNested = $expandNested; return $this; } + /** + * Set whether the nested grid fields should be forced closed on load + * @param boolean $forceClosed + */ public function setForceClosedNested($forceClosed) { $this->forceCloseNested = $forceClosed; return $this; } + /** + * Set a callback to check which items in this grid that should show the expand link + * for nested gridfields. The callback should return a boolean value. + * @param callable $checkFunction + */ public function setCanExpandCheck($checkFunction) { $this->canExpandCheck = $checkFunction; @@ -186,7 +219,7 @@ 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 GridFieldNestedForm_ItemRequest && $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); @@ -248,7 +281,7 @@ class GridFieldNestedForm extends GridFieldDetailForm implements } $this->gridField = $gridField; $this->record = $record; - $itemRequest = new GridFieldNestedForm_ItemRequest($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; diff --git a/src/GridFieldNestedForm_ItemRequest.php b/src/GridFieldNestedFormItemRequest.php similarity index 98% rename from src/GridFieldNestedForm_ItemRequest.php rename to src/GridFieldNestedFormItemRequest.php index 9488ea5..4e5357f 100644 --- a/src/GridFieldNestedForm_ItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -21,7 +21,7 @@ use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton; use Symbiote\GridFieldExtensions\GridFieldEditableColumns; use Symbiote\GridFieldExtensions\GridFieldOrderableRows; -class GridFieldNestedForm_ItemRequest extends GridFieldDetailForm_ItemRequest +class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest { public function Link($action = null) From c043220949b2fc8bb97eb8189843e6515b344f8a Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Thu, 18 Apr 2024 10:56:35 +0300 Subject: [PATCH 06/24] Changed base-class of GridFieldNestedForm, it doesn't share much with GridFieldDetailForm anymore. Also changed how Breadcrumbs work. --- src/GridFieldNestedForm.php | 13 ++++++++-- src/GridFieldNestedFormItemRequest.php | 36 +++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index 5a88017..11560ea 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -8,12 +8,15 @@ use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse_Exception; +use SilverStripe\Core\Extensible; +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\GridFieldStateAware; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; @@ -26,12 +29,14 @@ 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 Extensible, GridFieldStateAware; const POST_KEY = 'GridFieldNestedForm'; @@ -39,6 +44,10 @@ class GridFieldNestedForm extends GridFieldDetailForm implements 'handleNestedItem' ]; + /** + * @var string + */ + protected $name; protected $expandNested = false; protected $forceCloseNested = false; protected $gridField = null; diff --git a/src/GridFieldNestedFormItemRequest.php b/src/GridFieldNestedFormItemRequest.php index 4e5357f..3927425 100644 --- a/src/GridFieldNestedFormItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -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,6 +35,14 @@ 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)) { @@ -113,7 +124,30 @@ 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; } From c415d43731a8ef3d5e58106b88bfe13a323a9795 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Thu, 18 Apr 2024 12:41:58 +0300 Subject: [PATCH 07/24] Fixed linting issues --- src/GridFieldNestedForm.php | 40 ++++++++++++++++++++------ src/GridFieldNestedFormItemRequest.php | 21 ++++++++++++-- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index 11560ea..faf7bf6 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -172,9 +172,14 @@ class GridFieldNestedForm extends AbstractGridFieldComponent 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 ''; } } @@ -182,7 +187,11 @@ class GridFieldNestedForm extends AbstractGridFieldComponent 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'; } @@ -228,7 +237,11 @@ class GridFieldNestedForm extends AbstractGridFieldComponent 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); @@ -285,12 +298,19 @@ class GridFieldNestedForm extends AbstractGridFieldComponent 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; @@ -349,7 +369,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements $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) { @@ -364,7 +385,10 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements 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; diff --git a/src/GridFieldNestedFormItemRequest.php b/src/GridFieldNestedFormItemRequest.php index 3927425..5734184 100644 --- a/src/GridFieldNestedFormItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -46,7 +46,8 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest $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); @@ -93,7 +94,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'); @@ -137,7 +148,11 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest ])); } else { $items->push(ArrayData::create([ - 'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->record->i18n_singular_name()]), + 'Title' => _t( + 'SilverStripe\\Forms\\GridField\\GridField.NewRecord', + 'New {type}', + ['type' => $this->record->i18n_singular_name()] + ), 'Link' => false ])); } From f8c777dcc5d93e4b44ae249476dd92dea6b923e7 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Thu, 18 Apr 2024 14:39:25 +0300 Subject: [PATCH 08/24] Changed naming schema for nested GridFields, to not include [ or ] characters. This makes them work also with the search component. --- javascript/GridFieldExtensions.js | 4 +++- src/GridFieldNestedForm.php | 21 +++++++++++---------- src/GridFieldNestedFormItemRequest.php | 6 +++++- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/javascript/GridFieldExtensions.js b/javascript/GridFieldExtensions.js index c7bd02c..5041f02 100644 --- a/javascript/GridFieldExtensions.js +++ b/javascript/GridFieldExtensions.js @@ -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"); diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index faf7bf6..4f1f007 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -359,11 +359,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent 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']); @@ -373,12 +370,16 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements 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); + } } } } diff --git a/src/GridFieldNestedFormItemRequest.php b/src/GridFieldNestedFormItemRequest.php index 5734184..8d8fb28 100644 --- a/src/GridFieldNestedFormItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -65,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)) { @@ -96,7 +100,7 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest $fields = new FieldList( $gridField = new GridField( sprintf( - '%s[%s][%s]', + '%s-%s-%s', $this->component->getGridField()->getName(), GridFieldNestedForm::POST_KEY, $this->record->ID From 4fc20fb771fc9914fc91d10786887df7ef089e94 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Thu, 18 Apr 2024 17:51:27 +0300 Subject: [PATCH 09/24] Added one more unit test for GridFieldNestedForm --- tests/GridFieldNestedFormTest.php | 32 ++++++++++++++++++++++++++++++- tests/GridFieldNestedFormTest.yml | 12 +++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/GridFieldNestedFormTest.php b/tests/GridFieldNestedFormTest.php index d364314..c7b975f 100644 --- a/tests/GridFieldNestedFormTest.php +++ b/tests/GridFieldNestedFormTest.php @@ -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); + } } diff --git a/tests/GridFieldNestedFormTest.yml b/tests/GridFieldNestedFormTest.yml index aa5235f..f0a6be4 100644 --- a/tests/GridFieldNestedFormTest.yml +++ b/tests/GridFieldNestedFormTest.yml @@ -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 \ No newline at end of file + 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 From fc40420daaa460ce3dc27a819c0085d5c7859430 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Thu, 18 Apr 2024 18:04:56 +0300 Subject: [PATCH 10/24] Added configurable max nesting level for nested GridFields --- src/GridFieldNestedForm.php | 42 +++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index 4f1f007..81b83ef 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -8,7 +8,7 @@ use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse_Exception; -use SilverStripe\Core\Extensible; +use SilverStripe\Core\Config\Configurable; use SilverStripe\Forms\GridField\AbstractGridFieldComponent; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField_ColumnProvider; @@ -16,6 +16,7 @@ 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; @@ -36,13 +37,15 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements GridField_HTMLProvider, GridField_DataManipulator { - use Extensible, GridFieldStateAware; + use Configurable, GridFieldStateAware; const POST_KEY = 'GridFieldNestedForm'; private static $allowed_actions = [ 'handleNestedItem' ]; + + private static $max_nesting_level = 10; /** * @var string @@ -55,6 +58,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements protected $relationName = 'Children'; protected $inlineEditable = false; protected $canExpandCheck = null; + protected $maxNestingLevel = null; public function __construct($name = 'NestedForm') { @@ -139,6 +143,32 @@ class GridFieldNestedForm extends AbstractGridFieldComponent 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' => '']; @@ -163,6 +193,10 @@ class GridFieldNestedForm extends AbstractGridFieldComponent 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; @@ -286,6 +320,10 @@ class GridFieldNestedForm extends AbstractGridFieldComponent 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); From 32d980e13c35128703b1a70c86d5a635a5aa30e3 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Tue, 23 Apr 2024 16:35:10 +0300 Subject: [PATCH 11/24] Fixed moving nested gridfield items to other gridfields --- javascript/GridFieldExtensions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/javascript/GridFieldExtensions.js b/javascript/GridFieldExtensions.js index 5041f02..644fb13 100644 --- a/javascript/GridFieldExtensions.js +++ b/javascript/GridFieldExtensions.js @@ -1,4 +1,7 @@ (function($) { + let preventReorderUpdate = false; + let updateTimeouts = []; + $.entwine("ss", function($) { /** * GridFieldAddExistingSearchButton @@ -612,8 +615,6 @@ $('.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; From cfcf8d2e8e9ec2b669fabff2eb3d3119b4237a9d Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Tue, 23 Apr 2024 17:03:00 +0300 Subject: [PATCH 12/24] Refactored nested GridField move to parent functionality. --- src/GridFieldNestedForm.php | 90 ++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index 81b83ef..ca32a20 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -266,53 +266,61 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** @var DataList */ $list = $gridField->getList(); $id = isset($move['id']) ? (int) $move['id'] : null; + if (!$id) { + throw new HTTPResponse_Exception('Missing ID', 400); + } $to = isset($move['parent']) ? (int)$move['parent'] : null; - $parent = null; - 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 - ) { - $parent = $gridField->getForm()->getController()->getRecord(); + // 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 + ) { + $parent = $gridField->getForm()->getController()->getRecord(); + } + $child = $list->byID($id); + // we need either a parent or a child, or a move to top level at this stage + if (!($parent || $child || $to === 0)) { + throw new HTTPResponse_Exception('Invalid request', 400); + } + // parent or child might be from another grid field, so we need to search via DataList in some cases + if (!$parent && $to) { + $parent = DataList::create($gridField->getModelClass())->byID($to); + } + if (!$child) { + $child = DataList::create($gridField->getModelClass())->byID($id); + } + if ($child) { + if (!$child->canEdit()) { + throw new HTTPResponse_Exception('Not allowed', 403); } - $child = $list->byID($id); - if ($parent || $child || $to === 0) { - if (!$parent && $to) { - $parent = DataList::create($gridField->getModelClass())->byID($to); + if ($child->hasExtension(Hierarchy::class)) { + $child->ParentID = $parent ? $parent->ID : 0; + } + // validate that the record is still valid + $validationResult = $child->validate(); + if ($validationResult->isValid()) { + if ($child->hasExtension(Versioned::class)) { + $child->writeToStage(Versioned::DRAFT); + } else { + $child->write(); } - if (!$child) { - $child = DataList::create($gridField->getModelClass())->byID($id); - } - if ($child) { - if ($child->hasExtension(Hierarchy::class)) { - $child->ParentID = $parent ? $parent->ID : 0; - } - $validationResult = $child->validate(); - if ($validationResult->isValid()) { - if ($child->hasExtension(Versioned::class)) { - $child->writeToStage(Versioned::DRAFT); - } else { - $child->write(); - } - /** @var GridFieldOrderableRows */ - $orderableRows = $gridField->getConfig()->getComponentByType(GridFieldOrderableRows::class); - if ($orderableRows) { - $orderableRows->setImmediateUpdate(true); - try { - $orderableRows->handleReorder($gridField, $request); - } catch (Exception $e) { - } - } - } else { - $messages = $validationResult->getMessages(); - $message = array_pop($messages); - throw new HTTPResponse_Exception($message['message'], 400); + // reorder items at the same time, if applicable + /** @var GridFieldOrderableRows */ + $orderableRows = $gridField->getConfig()->getComponentByType(GridFieldOrderableRows::class); + if ($orderableRows) { + $orderableRows->setImmediateUpdate(true); + try { + $orderableRows->handleReorder($gridField, $request); + } catch (Exception $e) { } } + } else { + $messages = $validationResult->getMessages(); + $message = array_pop($messages); + throw new HTTPResponse_Exception($message['message'], 400); } } return $gridField->FieldHolder(); From 847ce07ab0273732ca8cfa50aede8ad8213c1ee5 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Wed, 24 Apr 2024 09:19:32 +0300 Subject: [PATCH 13/24] Fixes and some refactoring for max nesting level handling in nested gridfields. --- src/GridFieldNestedForm.php | 35 +++++++++++++++++--------- src/GridFieldNestedFormItemRequest.php | 5 +++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index ca32a20..b9f8b02 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -16,7 +16,6 @@ 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; @@ -45,7 +44,12 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements '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 @@ -153,20 +157,29 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements return $this; } + /** + * Get the max nesting level allowed for this grid field. + * @return int + */ public function getMaxNestingLevel() { - 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. + * @return bool + */ + protected function atMaxNestingLevel(GridField $gridField): bool { $level = 0; - $c = $gridField->getForm()->getController(); - while ($c && $c instanceof GridFieldDetailForm_ItemRequest) { - $c = $c->getController(); + $controller = $gridField->getForm()->getController(); + $maxLevel = $this->getMaxNestingLevel(); + while ($level < $maxLevel && $controller && $controller instanceof GridFieldNestedFormItemRequest) { + $controller = $controller->getController(); $level++; } - return $level; + return $level >= $maxLevel; } public function getColumnMetadata($gridField, $columnName) @@ -193,8 +206,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements public function getColumnContent($gridField, $record, $columnName) { - $nestingLevel = $this->getNestingLevel($gridField); - if ($nestingLevel >= $this->getMaxNestingLevel()) { + if ($this->atMaxNestingLevel($gridField)) { return ''; } $gridField->addExtraClass('has-nested'); @@ -328,8 +340,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements public function handleNestedItem(GridField $gridField, $request = null, $record = null) { - $nestingLevel = $this->getNestingLevel($gridField); - if ($nestingLevel >= $this->getMaxNestingLevel()) { + if ($this->atMaxNestingLevel($gridField)) { throw new Exception('Max nesting level reached'); } if (!$record && $request) { diff --git a/src/GridFieldNestedFormItemRequest.php b/src/GridFieldNestedFormItemRequest.php index 8d8fb28..18b46ea 100644 --- a/src/GridFieldNestedFormItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -72,7 +72,10 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest } 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 */ $orderableRows = $config->getComponentByType(GridFieldOrderableRows::class); if ($orderableRows) { From f7b8aea3f8dc3de09b282fb6927dcdb45209a508 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Wed, 24 Apr 2024 09:40:16 +0300 Subject: [PATCH 14/24] PHPDoc additions and linting fixes for gridfield nested form --- src/GridFieldNestedForm.php | 94 +++++++++++++++++++++----- src/GridFieldNestedFormItemRequest.php | 3 +- 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index b9f8b02..a323fbd 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -6,6 +6,7 @@ use Exception; use SilverStripe\Admin\ModelAdmin; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; +use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Core\Config\Configurable; @@ -38,6 +39,9 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements { use Configurable, GridFieldStateAware; + /** + * The key used in the post data to identify nested form data + */ const POST_KEY = 'GridFieldNestedForm'; private static $allowed_actions = [ @@ -54,15 +58,42 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * @var string */ - protected $name; - protected $expandNested = false; - protected $forceCloseNested = false; - protected $gridField = null; - protected $record = null; - protected $relationName = 'Children'; - protected $inlineEditable = false; - protected $canExpandCheck = null; - protected $maxNestingLevel = null; + private $name; + + /** + * @var bool + */ + private $expandNested = false; + + /** + * @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') { @@ -137,13 +168,14 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements } /** - * 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. - * @param callable $checkFunction + * You can either pass a callable or a method name as a string. + * @param callable|string $callback */ - public function setCanExpandCheck($checkFunction) + public function setCanExpandCheck($callback) { - $this->canExpandCheck = $checkFunction; + $this->canExpandCheck = $callback; return $this; } @@ -212,7 +244,6 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements $gridField->addExtraClass('has-nested'); if ($record->ID && $record->exists()) { $this->gridField = $gridField; - $this->record = $record; $relationName = $this->getRelationName(); if (!$record->hasMethod($relationName)) { return ''; @@ -224,7 +255,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements return ''; } elseif (is_string($this->canExpandCheck) && $record->hasMethod($this->canExpandCheck) - && !$this->record->{$this->canExpandCheck}($record) + && !$record->{$this->canExpandCheck}($record) ) { return ''; } @@ -272,6 +303,11 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements } } + /** + * Handle moving a record to a new parent + * + * @return string + */ public function handleMoveToParent(GridField $gridField, $request) { $move = $request->postVar('move'); @@ -338,6 +374,14 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements return $gridField->FieldHolder(); } + /** + * Handle the request to show a nested item + * + * @param GridField $gridField + * @param HTTPRequest|null $request + * @param ViewableData|null $record + * @return HTTPResponse + */ public function handleNestedItem(GridField $gridField, $request = null, $record = null) { if ($this->atMaxNestingLevel($gridField)) { @@ -360,7 +404,6 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements $gridField->getState(false)->setValue($gridStateStr); } $this->gridField = $gridField; - $this->record = $record; $itemRequest = GridFieldNestedFormItemRequest::create( $gridField, $this, @@ -384,6 +427,13 @@ 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) { if (!$record) { @@ -400,6 +450,12 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements $state->$stateRelation = (int)$request->getVar('toggle'); } + /** + * Get the link for the nested grid field + * + * @param string $action + * @return string + */ public function Link($action = null) { $link = Director::absoluteURL(Controller::join_links($this->gridField->Link('nested'), $action)); @@ -407,6 +463,12 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements return $manager->addStateToURL($this->gridField, $link); } + /** + * Get the link for the toggle action + * + * @param string $action + * @return string + */ public function ToggleLink($action = null) { $link = Director::absoluteURL(Controller::join_links($this->gridField->Link('toggle'), $action, '?toggle=')); diff --git a/src/GridFieldNestedFormItemRequest.php b/src/GridFieldNestedFormItemRequest.php index 18b46ea..4282334 100644 --- a/src/GridFieldNestedFormItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -26,7 +26,6 @@ use Symbiote\GridFieldExtensions\GridFieldOrderableRows; class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest { - public function Link($action = null) { return Controller::join_links($this->component->Link($this->record->ID), $action); @@ -75,7 +74,7 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest $config->addComponent($nestedForm = new GridFieldNestedForm(), GridFieldOrderableRows::class); // use max nesting level from parent component $nestedForm->setMaxNestingLevel($this->component->getMaxNestingLevel()); - + /** @var GridFieldOrderableRows */ $orderableRows = $config->getComponentByType(GridFieldOrderableRows::class); if ($orderableRows) { From c517c693f9b4ed1a98df27ace83ed53eb34d8c51 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Wed, 24 Apr 2024 09:57:24 +0300 Subject: [PATCH 15/24] Don't assume records are DataObjects in nested gridfields. Also don't assume the list is filterable. --- src/GridFieldNestedForm.php | 40 +++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index a323fbd..3399d77 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -9,6 +9,7 @@ use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse_Exception; +use SilverStripe\Control\RequestHandler; use SilverStripe\Core\Config\Configurable; use SilverStripe\Forms\GridField\AbstractGridFieldComponent; use SilverStripe\Forms\GridField\GridField; @@ -21,6 +22,7 @@ use SilverStripe\Forms\GridField\GridFieldStateAware; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; +use SilverStripe\ORM\Filterable; use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\ORM\SS_List; use SilverStripe\Versioned\Versioned; @@ -294,13 +296,23 @@ 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 []; } /** @@ -380,16 +392,17 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements * @param GridField $gridField * @param HTTPRequest|null $request * @param ViewableData|null $record - * @return HTTPResponse + * @return HTTPResponse|RequestHandler */ public function handleNestedItem(GridField $gridField, $request = null, $record = null) { if ($this->atMaxNestingLevel($gridField)) { throw new Exception('Max nesting level reached'); } - if (!$record && $request) { + $list = $gridField->getList(); + if (!$record && $request && $list instanceof Filterable) { $recordID = $request->param('RecordID'); - $record = $gridField->getList()->byID($recordID); + $record = $list->byID($recordID); } if (!$record) { return ''; @@ -436,9 +449,10 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements */ public function toggleNestedItem(GridField $gridField, $request = null, $record = null) { - if (!$record) { + $list = $gridField->getList(); + if (!$record && $request && $list instanceof Filterable) { $recordID = $request->param('RecordID'); - $record = $gridField->getList()->byID($recordID); + $record = $list->byID($recordID); } $manager = $this->getStateManager(); if ($gridStateStr = $manager->getStateFromRequest($gridField, $request)) { @@ -490,11 +504,13 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements $gridField->getState(false)->setValue($gridStateStr); } 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]; $nestedData = $val; - $record = $gridField->getList()->byID($recordID); + $record = $list->byID($recordID); if ($record) { + /** @var GridField */ $nestedGridField = $this->handleNestedItem($gridField, null, $record); $nestedGridField->setValue($nestedData); $nestedGridField->saveInto($record); @@ -506,8 +522,10 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements public function getManipulatedData(GridField $gridField, SS_List $dataList) { if ($this->relationName == 'Children' + && is_a($gridField->getModelClass(), DataObject::class, true) && DataObject::has_extension($gridField->getModelClass(), Hierarchy::class) && $gridField->getForm()->getController() instanceof ModelAdmin + && $dataList instanceof Filterable ) { $dataList = $dataList->filter('ParentID', 0); } From 70b838ea8c4ffb54528b3342397b003331445fe0 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Wed, 24 Apr 2024 10:17:22 +0300 Subject: [PATCH 16/24] Removed legacy disabling of security token and strict form method check for nested gridfields, doesn't seem to be needed anymore. --- src/GridFieldNestedFormItemRequest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/GridFieldNestedFormItemRequest.php b/src/GridFieldNestedFormItemRequest.php index 4282334..f0bbd47 100644 --- a/src/GridFieldNestedFormItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -119,8 +119,6 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest $gridField->setAttribute('data-class', str_replace('\\', '-', $relationClass)); $gridField->addExtraClass('nested'); $form = new Form($this, 'ItemEditForm', $fields, new FieldList()); - $form->setStrictFormMethodCheck(false); - $form->disableSecurityToken(); $className = str_replace('\\', '-', get_class($this->record)); $state = $this->gridField->getState()->GridFieldNestedForm; From 5e6097214e1140d9899af0ae1247363a4171e39a Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Wed, 24 Apr 2024 10:25:45 +0300 Subject: [PATCH 17/24] Added phpdoc for nested grid field item request handler class. --- src/GridFieldNestedFormItemRequest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/GridFieldNestedFormItemRequest.php b/src/GridFieldNestedFormItemRequest.php index f0bbd47..cf52867 100644 --- a/src/GridFieldNestedFormItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -24,6 +24,9 @@ use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton; use Symbiote\GridFieldExtensions\GridFieldEditableColumns; use Symbiote\GridFieldExtensions\GridFieldOrderableRows; +/** + * Request handler class for nested grid field forms. + */ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest { public function Link($action = null) From bc1180b62cd32284c25db9061656615acf0c6367 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Wed, 24 Apr 2024 10:42:55 +0300 Subject: [PATCH 18/24] Throw exception in nested gridfields if the relation is invalid. --- src/GridFieldNestedForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index 3399d77..953fbf0 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -409,7 +409,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements } $relationName = $this->getRelationName(); if (!$record->hasMethod($relationName)) { - return ''; + throw new Exception('Invalid relation name'); } $manager = $this->getStateManager(); $stateRequest = $request ?: $gridField->getForm()->getRequestHandler()->getRequest(); From 46e5cccdb47be6d4a9e8fa85585768dfab066184 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Wed, 24 Apr 2024 11:03:48 +0300 Subject: [PATCH 19/24] Changed some PHPDoc return types to real typehings in nested gridfield. --- src/GridFieldNestedForm.php | 41 +++++++++++-------------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index 953fbf0..200ed40 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -104,27 +104,24 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Get the grid field that this component is attached to - * @return GridField */ - public function getGridField() + public function getGridField(): GridField { return $this->gridField; } /** * Get the relation name to use for the nested grid fields - * @return string */ - public function getRelationName() + public function getRelationName(): string { return $this->relationName; } /** * 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; return $this; @@ -132,18 +129,16 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Get whether the nested grid fields should be inline editable - * @return boolean */ - public function getInlineEditable() + public function getInlineEditable(): bool { return $this->inlineEditable; } /** * 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; return $this; @@ -151,9 +146,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * 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; return $this; @@ -161,9 +155,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * 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; return $this; @@ -173,9 +166,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements * 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. * You can either pass a callable or a method name as a string. - * @param callable|string $callback */ - public function setCanExpandCheck($callback) + public function setCanExpandCheck(callable|string $callback) { $this->canExpandCheck = $callback; return $this; @@ -183,9 +175,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * 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; return $this; @@ -193,16 +184,14 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Get the max nesting level allowed for this grid field. - * @return int */ - public function getMaxNestingLevel() + public function getMaxNestingLevel(): int { return $this->maxNestingLevel ?: static::config()->get('default_max_nesting_level'); } /** * Check if we are currently at the max nesting level allowed. - * @return bool */ protected function atMaxNestingLevel(GridField $gridField): bool { @@ -466,11 +455,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Get the link for the nested grid field - * - * @param string $action - * @return string */ - public function Link($action = null) + public function Link($action = null): string { $link = Director::absoluteURL(Controller::join_links($this->gridField->Link('nested'), $action)); $manager = $this->getStateManager(); @@ -479,11 +465,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Get the link for the toggle action - * - * @param string $action - * @return string */ - public function ToggleLink($action = null) + public function ToggleLink($action = null): string { $link = Director::absoluteURL(Controller::join_links($this->gridField->Link('toggle'), $action, '?toggle=')); $manager = $this->getStateManager(); From 1ad6acbb84f235e29fc364e28fe2ee332a590eb9 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Wed, 24 Apr 2024 11:13:09 +0300 Subject: [PATCH 20/24] Refactored grid field nested form link to be a button with aria-attributes, for better accessibility. --- javascript/GridFieldExtensions.js | 6 ++++-- .../Symbiote/GridFieldExtensions/GridFieldNestedForm.ss | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/javascript/GridFieldExtensions.js b/javascript/GridFieldExtensions.js index 644fb13..3500cc9 100644 --- a/javascript/GridFieldExtensions.js +++ b/javascript/GridFieldExtensions.js @@ -517,7 +517,7 @@ /** * GridFieldNestedForm */ - $('.grid-field .col-listChildrenLink a').entwine({ + $('.grid-field .col-listChildrenLink button').entwine({ onclick: function(e) { let gridField = $(this).closest('.grid-field'); let currState = gridField.getState(); @@ -552,7 +552,7 @@ } $.ajax({ type: 'POST', - url: $(this).attr('href'), + url: $(this).attr('data-url'), data: data, headers: { 'X-Pjax': pjaxTarget @@ -572,6 +572,7 @@ } $(this).removeClass('font-icon-right-dir'); $(this).addClass('font-icon-down-dir'); + $(this).attr('aria-expanded', 'true'); } else { $.ajax({ @@ -580,6 +581,7 @@ $(this).closest('tr').next('.nested-gridfield').hide(); $(this).removeClass('font-icon-down-dir'); $(this).addClass('font-icon-right-dir'); + $(this).attr('aria-expanded', 'false'); } e.preventDefault(); e.stopPropagation(); diff --git a/templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss b/templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss index 0e2f09d..bd5c4f1 100755 --- a/templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss +++ b/templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss @@ -1,4 +1,10 @@ - + <% if $Toggle == 'open' %> $NestedField <% else %> From 1993acba3a1bf94e4e4440362620fb16e6e172f5 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Tue, 7 May 2024 12:35:16 +0300 Subject: [PATCH 21/24] Linting and typehinting fixes for nested grid field --- src/GridFieldNestedForm.php | 105 +++++++++++++----------------------- 1 file changed, 38 insertions(+), 67 deletions(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index 200ed40..3e0c13d 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -11,6 +11,7 @@ use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\RequestHandler; use SilverStripe\Core\Config\Configurable; +use SilverStripe\Forms\Form; use SilverStripe\Forms\GridField\AbstractGridFieldComponent; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField_ColumnProvider; @@ -52,50 +53,27 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * The default max nesting level. Nesting further than this will throw an exception. - * - * @var boolean */ - private static $default_max_nesting_level = 10; + private static int $default_max_nesting_level = 10; - /** - * @var string - */ - private $name; + private string $name; - /** - * @var bool - */ - private $expandNested = false; + private bool $expandNested = false; - /** - * @var bool - */ - private $forceCloseNested = false; + private bool $forceCloseNested = false; - /** - * @var GridField - */ - private $gridField = null; + private GridField $gridField; - /** - * @var string - */ - private $relationName = 'Children'; + private string $relationName = 'Children'; - /** - * @var bool - */ - private $inlineEditable = false; + private bool $inlineEditable = false; /** * @var callable|string */ - private $canExpandCheck = null; + private $canExpandCallback = null; - /** - * @var int - */ - private $maxNestingLevel = null; + private int $maxNestingLevel = 0; public function __construct($name = 'NestedForm') { @@ -121,7 +99,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Set the relation name to use for the nested grid fields */ - public function setRelationName(string $relationName) + public function setRelationName(string $relationName): static { $this->relationName = $relationName; return $this; @@ -138,7 +116,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Set whether the nested grid fields should be inline editable */ - public function setInlineEditable(bool $editable) + public function setInlineEditable(bool $editable): static { $this->inlineEditable = $editable; return $this; @@ -147,7 +125,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Set whether the nested grid fields should be expanded by default */ - public function setExpandNested(bool $expandNested) + public function setExpandNested(bool $expandNested): static { $this->expandNested = $expandNested; return $this; @@ -156,7 +134,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Set whether the nested grid fields should be forced closed on load */ - public function setForceClosedNested(bool $forceClosed) + public function setForceClosedNested(bool $forceClosed): static { $this->forceCloseNested = $forceClosed; return $this; @@ -167,16 +145,16 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements * for nested gridfields. The callback should return a boolean value. * You can either pass a callable or a method name as a string. */ - public function setCanExpandCheck(callable|string $callback) + public function setCanExpandCallback(callable|string $callback): static { - $this->canExpandCheck = $callback; + $this->canExpandCallback = $callback; return $this; } /** * Set the maximum nesting level allowed for nested grid fields */ - public function setMaxNestingLevel(int $level) + public function setMaxNestingLevel(int $level): static { $this->maxNestingLevel = $level; return $this; @@ -239,14 +217,14 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements if (!$record->hasMethod($relationName)) { return ''; } - if ($this->canExpandCheck) { - if (is_callable($this->canExpandCheck) - && !call_user_func($this->canExpandCheck, $record) + if ($this->canExpandCallback) { + if (is_callable($this->canExpandCallback) + && !call_user_func($this->canExpandCallback, $record) ) { return ''; - } elseif (is_string($this->canExpandCheck) - && $record->hasMethod($this->canExpandCheck) - && !$record->{$this->canExpandCheck}($record) + } elseif (is_string($this->canExpandCallback) + && $record->hasMethod($this->canExpandCallback) + && !$record->{$this->canExpandCallback}($record) ) { return ''; } @@ -284,13 +262,9 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements ]; } - /** - * @param GridField $gridField - * @return array - */ public function getHTMLFragments($gridField) { - /** + /* * 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. @@ -306,10 +280,8 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * Handle moving a record to a new parent - * - * @return string */ - public function handleMoveToParent(GridField $gridField, $request) + public function handleMoveToParent(GridField $gridField, $request): string { $move = $request->postVar('move'); /** @var DataList */ @@ -377,14 +349,12 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements /** * 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, + HTTPRequest|null $request = null, + ViewableData|null $record = null + ): HTTPResponse|RequestHandler|Form { if ($this->atMaxNestingLevel($gridField)) { throw new Exception('Max nesting level reached'); } @@ -431,13 +401,12 @@ 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, + HTTPRequest|null $request = null, + ViewableData|null $record = null + ) { $list = $gridField->getList(); if (!$record && $request && $list instanceof Filterable) { $recordID = $request->param('RecordID'); @@ -488,7 +457,9 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements } foreach ($request->postVars() as $key => $val) { $list = $gridField->getList(); - if ($list instanceof Filterable && preg_match("/{$gridField->getName()}-{$postKey}-(\d+)/", $key, $matches)) { + if ($list instanceof Filterable + && preg_match("/{$gridField->getName()}-{$postKey}-(\d+)/", $key, $matches) + ) { $recordID = $matches[1]; $nestedData = $val; $record = $list->byID($recordID); From 9ab3ed67a75c0060e9303abd9822a9751bf6afe4 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Tue, 7 May 2024 12:36:44 +0300 Subject: [PATCH 22/24] Throw an exception in Nesed Gridfield if an invalid relation is configured. --- src/GridFieldNestedForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index 3e0c13d..a569b3c 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -215,7 +215,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements $this->gridField = $gridField; $relationName = $this->getRelationName(); if (!$record->hasMethod($relationName)) { - return ''; + throw new Exception('Invalid relation name'); } if ($this->canExpandCallback) { if (is_callable($this->canExpandCallback) From 8f505659f06e65bf6881d050c6cdb022f5f539b6 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Tue, 7 May 2024 18:02:06 +0300 Subject: [PATCH 23/24] Only add nested form to nested gridfield child if that child is of the same class as the parent record. --- src/GridFieldNestedFormItemRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GridFieldNestedFormItemRequest.php b/src/GridFieldNestedFormItemRequest.php index cf52867..fdd104a 100644 --- a/src/GridFieldNestedFormItemRequest.php +++ b/src/GridFieldNestedFormItemRequest.php @@ -73,7 +73,7 @@ class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest } } - if ($this->record->hasExtension(Hierarchy::class)) { + if ($this->record->hasExtension(Hierarchy::class) && $relationClass == get_class($this->record)) { $config->addComponent($nestedForm = new GridFieldNestedForm(), GridFieldOrderableRows::class); // use max nesting level from parent component $nestedForm->setMaxNestingLevel($this->component->getMaxNestingLevel()); From a9b0a70155eb46fd659893fa6f42cdf28aad5bf7 Mon Sep 17 00:00:00 2001 From: Niklas Forsdahl Date: Tue, 7 May 2024 18:20:56 +0300 Subject: [PATCH 24/24] Throw 404 error on grid field nested form move-to-parent action, if no record is found. --- src/GridFieldNestedForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GridFieldNestedForm.php b/src/GridFieldNestedForm.php index a569b3c..cb3015c 100644 --- a/src/GridFieldNestedForm.php +++ b/src/GridFieldNestedForm.php @@ -288,7 +288,7 @@ class GridFieldNestedForm extends AbstractGridFieldComponent implements $list = $gridField->getList(); $id = isset($move['id']) ? (int) $move['id'] : null; if (!$id) { - throw new HTTPResponse_Exception('Missing ID', 400); + throw new HTTPResponse_Exception('Missing ID', 404); } $to = isset($move['parent']) ? (int)$move['parent'] : null; // should be possible either on parent or child grid field, or nested grid field from parent