mirror of
https://github.com/symbiote/silverstripe-gridfieldextensions.git
synced 2024-10-22 17:05:39 +02:00
Merge pull request #384 from creamarketing/nested-gridfield
NEW Nested gridfield
This commit is contained in:
commit
678ec6f7e1
@ -18,6 +18,8 @@ This module provides a number of useful grid field components:
|
|||||||
features.
|
features.
|
||||||
* `GridFieldTitleHeader` - a simple header which displays column titles.
|
* `GridFieldTitleHeader` - a simple header which displays column titles.
|
||||||
* `GridFieldConfigurablePaginator` - a paginator for GridField that allows customisable page sizes.
|
* `GridFieldConfigurablePaginator` - a paginator for GridField that allows customisable page sizes.
|
||||||
|
* `GridFieldNestedForm` - allows nesting of GridFields for managing relation records directly within
|
||||||
|
a parent GridField.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -223,3 +223,27 @@
|
|||||||
.grid-field-inline-new--multi-class-list__visible {
|
.grid-field-inline-new--multi-class-list__visible {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GridFieldNestedForm
|
||||||
|
*/
|
||||||
|
.grid-field tr.nested-gridfield td.gridfield-holder {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
@ -152,3 +152,46 @@ $paginator->setItemsPerPage(500);
|
|||||||
|
|
||||||
The first shown record will be maintained across page size changes, and the number of pages and current page will be
|
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.
|
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'));
|
||||||
|
```
|
||||||
|
|
||||||
|
You can define your own custom GridField config for the nested GridField configuration by implementing a `getNestedConfig`
|
||||||
|
on your nested model (should return a `GridField_Config` object).
|
||||||
|
```php
|
||||||
|
class NestedObject extends DataObject
|
||||||
|
{
|
||||||
|
private static $has_one = [
|
||||||
|
'Parent' => ParentObject::class
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getNestedConfig(): GridFieldConfig
|
||||||
|
{
|
||||||
|
$config = new GridFieldConfig_RecordViewer();
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also modify the default config (a `GridFieldConfig_RecordEditor`) via an extension to the nested model class, by implementing
|
||||||
|
`updateNestedConfig`, which will get the config object as the first parameter.
|
||||||
|
```php
|
||||||
|
class NestedObjectExtension extends DataExtension
|
||||||
|
{
|
||||||
|
public function updateNestedConfig(GridFieldConfig &$config)
|
||||||
|
{
|
||||||
|
$config->removeComponentsByType(GridFieldPaginator::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
(function($) {
|
(function($) {
|
||||||
|
let preventReorderUpdate = false;
|
||||||
|
let updateTimeouts = [];
|
||||||
|
|
||||||
$.entwine("ss", function($) {
|
$.entwine("ss", function($) {
|
||||||
/**
|
/**
|
||||||
* GridFieldAddExistingSearchButton
|
* GridFieldAddExistingSearchButton
|
||||||
@ -510,5 +513,173 @@
|
|||||||
this.parent().find('.ss-gridfield-pagesize-submit').trigger('click');
|
this.parent().find('.ss-gridfield-pagesize-submit').trigger('click');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GridFieldNestedForm
|
||||||
|
*/
|
||||||
|
$('.grid-field .col-listChildrenLink button').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) {
|
||||||
|
// add loading indicator until the nested gridfield is loaded
|
||||||
|
let colspan = gridField.find('.grid-field__title-row th').attr('colspan');
|
||||||
|
let loadingCell = $('<td />')
|
||||||
|
.addClass('ss-gridfield-item loading')
|
||||||
|
.attr('colspan', colspan);
|
||||||
|
$(this).closest('tr').after($('<tr class="nested-gridfield" />').append(loadingCell));
|
||||||
|
|
||||||
|
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('data-url'),
|
||||||
|
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');
|
||||||
|
$(this).attr('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
$(this).attr('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
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('<tr class="nested-gridfield '+evenOrOdd+'"><td class="gridfield-holder" colspan="'+nrOfColumns+'"></td></tr>');
|
||||||
|
// 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(`<div class="nested-container" data-pjax-fragment="${pjaxFragment}" style="display:none;"></div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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, 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);
|
})(jQuery);
|
||||||
|
491
src/GridFieldNestedForm.php
Normal file
491
src/GridFieldNestedForm.php
Normal file
@ -0,0 +1,491 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symbiote\GridFieldExtensions;
|
||||||
|
|
||||||
|
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\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;
|
||||||
|
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;
|
||||||
|
use SilverStripe\ORM\Filterable;
|
||||||
|
use SilverStripe\ORM\Hierarchy\Hierarchy;
|
||||||
|
use SilverStripe\ORM\SS_List;
|
||||||
|
use SilverStripe\Versioned\Versioned;
|
||||||
|
use SilverStripe\View\ViewableData;
|
||||||
|
use Symbiote\GridFieldExtensions\GridFieldOrderableRows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gridfield component for nesting GridFields
|
||||||
|
*/
|
||||||
|
class GridFieldNestedForm extends AbstractGridFieldComponent implements
|
||||||
|
GridField_URLHandler,
|
||||||
|
GridField_ColumnProvider,
|
||||||
|
GridField_SaveHandler,
|
||||||
|
GridField_HTMLProvider,
|
||||||
|
GridField_DataManipulator
|
||||||
|
{
|
||||||
|
use Configurable, GridFieldStateAware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key used in the post data to identify nested form data
|
||||||
|
*/
|
||||||
|
const POST_KEY = 'GridFieldNestedForm';
|
||||||
|
|
||||||
|
private static $allowed_actions = [
|
||||||
|
'handleNestedItem'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default max nesting level. Nesting further than this will throw an exception.
|
||||||
|
*/
|
||||||
|
private static int $default_max_nesting_level = 10;
|
||||||
|
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
private bool $expandNested = false;
|
||||||
|
|
||||||
|
private bool $forceCloseNested = false;
|
||||||
|
|
||||||
|
private GridField $gridField;
|
||||||
|
|
||||||
|
private string $relationName = 'Children';
|
||||||
|
|
||||||
|
private bool $inlineEditable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable|string
|
||||||
|
*/
|
||||||
|
private $canExpandCallback = null;
|
||||||
|
|
||||||
|
private int $maxNestingLevel = 0;
|
||||||
|
|
||||||
|
public function __construct($name = 'NestedForm')
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the grid field that this component is attached to
|
||||||
|
*/
|
||||||
|
public function getGridField(): GridField
|
||||||
|
{
|
||||||
|
return $this->gridField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the relation name to use for the nested grid fields
|
||||||
|
*/
|
||||||
|
public function getRelationName(): string
|
||||||
|
{
|
||||||
|
return $this->relationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the relation name to use for the nested grid fields
|
||||||
|
*/
|
||||||
|
public function setRelationName(string $relationName): static
|
||||||
|
{
|
||||||
|
$this->relationName = $relationName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the nested grid fields should be inline editable
|
||||||
|
*/
|
||||||
|
public function getInlineEditable(): bool
|
||||||
|
{
|
||||||
|
return $this->inlineEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether the nested grid fields should be inline editable
|
||||||
|
*/
|
||||||
|
public function setInlineEditable(bool $editable): static
|
||||||
|
{
|
||||||
|
$this->inlineEditable = $editable;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether the nested grid fields should be expanded by default
|
||||||
|
*/
|
||||||
|
public function setExpandNested(bool $expandNested): static
|
||||||
|
{
|
||||||
|
$this->expandNested = $expandNested;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether the nested grid fields should be forced closed on load
|
||||||
|
*/
|
||||||
|
public function setForceClosedNested(bool $forceClosed): static
|
||||||
|
{
|
||||||
|
$this->forceCloseNested = $forceClosed;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public function setCanExpandCallback(callable|string $callback): static
|
||||||
|
{
|
||||||
|
$this->canExpandCallback = $callback;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the maximum nesting level allowed for nested grid fields
|
||||||
|
*/
|
||||||
|
public function setMaxNestingLevel(int $level): static
|
||||||
|
{
|
||||||
|
$this->maxNestingLevel = $level;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the max nesting level allowed for this grid field.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
protected function atMaxNestingLevel(GridField $gridField): bool
|
||||||
|
{
|
||||||
|
$level = 0;
|
||||||
|
$controller = $gridField->getForm()->getController();
|
||||||
|
$maxLevel = $this->getMaxNestingLevel();
|
||||||
|
while ($level < $maxLevel && $controller && $controller instanceof GridFieldNestedFormItemRequest) {
|
||||||
|
$controller = $controller->getController();
|
||||||
|
$level++;
|
||||||
|
}
|
||||||
|
return $level >= $maxLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if ($gridField->getConfig()->getComponentsByType(GridFieldNestedForm::class)->count() > 1) {
|
||||||
|
throw new Exception('Only one GridFieldNestedForm component allowed per GridField');
|
||||||
|
}
|
||||||
|
if ($this->atMaxNestingLevel($gridField)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$gridField->addExtraClass('has-nested');
|
||||||
|
if ($record->ID && $record->exists()) {
|
||||||
|
$this->gridField = $gridField;
|
||||||
|
$relationName = $this->getRelationName();
|
||||||
|
if (!$record->hasMethod($relationName)) {
|
||||||
|
throw new Exception('Invalid relation name');
|
||||||
|
}
|
||||||
|
if ($this->canExpandCallback) {
|
||||||
|
if (is_callable($this->canExpandCallback)
|
||||||
|
&& !call_user_func($this->canExpandCallback, $record)
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
} elseif (is_string($this->canExpandCallback)
|
||||||
|
&& $record->hasMethod($this->canExpandCallback)
|
||||||
|
&& !$record->{$this->canExpandCallback}($record)
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$toggle = 'closed';
|
||||||
|
$className = str_replace('\\', '-', get_class($record));
|
||||||
|
$state = $gridField->State->GridFieldNestedForm;
|
||||||
|
$stateRelation = $className.'-'.$record->ID.'-'.$this->relationName;
|
||||||
|
$openState = $state && (int)$state->getData($stateRelation) === 1;
|
||||||
|
$forceExpand = $this->expandNested && $record->$relationName()->count() > 0;
|
||||||
|
if (!$this->forceCloseNested
|
||||||
|
&& ($forceExpand || $openState)
|
||||||
|
) {
|
||||||
|
$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'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
* Implemented in getHTMLFragments since this attribute needs to be added before any rendering happens.
|
||||||
|
*/
|
||||||
|
if (is_a($gridField->getModelClass(), DataObject::class, true)
|
||||||
|
&& DataObject::has_extension($gridField->getModelClass(), Hierarchy::class)
|
||||||
|
) {
|
||||||
|
$gridField->setAttribute('data-url-movetoparent', $gridField->Link('movetoparent'));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle moving a record to a new parent
|
||||||
|
*/
|
||||||
|
public function handleMoveToParent(GridField $gridField, $request): string
|
||||||
|
{
|
||||||
|
$move = $request->postVar('move');
|
||||||
|
/** @var DataList */
|
||||||
|
$list = $gridField->getList();
|
||||||
|
$id = isset($move['id']) ? (int) $move['id'] : null;
|
||||||
|
if (!$id) {
|
||||||
|
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
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the request to show a nested item
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
$list = $gridField->getList();
|
||||||
|
if (!$record && $request && $list instanceof Filterable) {
|
||||||
|
$recordID = $request->param('RecordID');
|
||||||
|
$record = $list->byID($recordID);
|
||||||
|
}
|
||||||
|
if (!$record) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$relationName = $this->getRelationName();
|
||||||
|
if (!$record->hasMethod($relationName)) {
|
||||||
|
throw new Exception('Invalid relation name');
|
||||||
|
}
|
||||||
|
$manager = $this->getStateManager();
|
||||||
|
$stateRequest = $request ?: $gridField->getForm()->getRequestHandler()->getRequest();
|
||||||
|
if ($gridStateStr = $manager->getStateFromRequest($gridField, $stateRequest)) {
|
||||||
|
$gridField->getState(false)->setValue($gridStateStr);
|
||||||
|
}
|
||||||
|
$this->gridField = $gridField;
|
||||||
|
$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;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the request to toggle a nested item in the gridfield state
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
$record = $list->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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the link for the nested grid field
|
||||||
|
*/
|
||||||
|
public function Link($action = null): string
|
||||||
|
{
|
||||||
|
$link = Director::absoluteURL(Controller::join_links($this->gridField->Link('nested'), $action));
|
||||||
|
$manager = $this->getStateManager();
|
||||||
|
return $manager->addStateToURL($this->gridField, $link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the link for the toggle action
|
||||||
|
*/
|
||||||
|
public function ToggleLink($action = null): string
|
||||||
|
{
|
||||||
|
$link = Director::absoluteURL(Controller::join_links($this->gridField->Link('toggle'), $action, '?toggle='));
|
||||||
|
$manager = $this->getStateManager();
|
||||||
|
return $manager->addStateToURL($this->gridField, $link);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleSave(GridField $gridField, DataObjectInterface $record)
|
||||||
|
{
|
||||||
|
$postKey = self::POST_KEY;
|
||||||
|
$value = $gridField->Value();
|
||||||
|
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();
|
||||||
|
$request = $gridField->getForm()->getRequestHandler()->getRequest();
|
||||||
|
if ($gridStateStr = $manager->getStateFromRequest($gridField, $request)) {
|
||||||
|
$gridField->getState(false)->setValue($gridStateStr);
|
||||||
|
}
|
||||||
|
foreach ($request->postVars() as $key => $val) {
|
||||||
|
$list = $gridField->getList();
|
||||||
|
if ($list instanceof Filterable
|
||||||
|
&& preg_match("/{$gridField->getName()}-{$postKey}-(\d+)/", $key, $matches)
|
||||||
|
) {
|
||||||
|
$recordID = $matches[1];
|
||||||
|
$nestedData = $val;
|
||||||
|
$record = $list->byID($recordID);
|
||||||
|
if ($record) {
|
||||||
|
/** @var GridField */
|
||||||
|
$nestedGridField = $this->handleNestedItem($gridField, null, $record);
|
||||||
|
$nestedGridField->setValue($nestedData);
|
||||||
|
$nestedGridField->saveInto($record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return $dataList;
|
||||||
|
}
|
||||||
|
}
|
176
src/GridFieldNestedFormItemRequest.php
Normal file
176
src/GridFieldNestedFormItemRequest.php
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
use SilverStripe\Forms\GridField\GridFieldPageCount;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request handler class for nested grid field forms.
|
||||||
|
*/
|
||||||
|
class GridFieldNestedFormItemRequest extends GridFieldDetailForm_ItemRequest
|
||||||
|
{
|
||||||
|
public function Link($action = null)
|
||||||
|
{
|
||||||
|
return Controller::join_links($this->component->Link($this->record->ID), $action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ItemEditForm()
|
||||||
|
{
|
||||||
|
$config = new GridFieldConfig_RecordEditor();
|
||||||
|
/** @var GridFieldDetailForm */
|
||||||
|
$detailForm = $config->getComponentByType(GridFieldDetailForm::class);
|
||||||
|
$detailForm->setItemEditFormCallback(function (Form $form, $itemRequest) {
|
||||||
|
$breadcrumbs = $itemRequest->Breadcrumbs(false);
|
||||||
|
if ($breadcrumbs && $breadcrumbs->exists()) {
|
||||||
|
$form->Backlink = $breadcrumbs->first()->Link;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$relationName = $this->component->getRelationName();
|
||||||
|
$list = $this->record->$relationName();
|
||||||
|
if ($relationName == 'Children' && $this->record->hasExtension(Hierarchy::class)) {
|
||||||
|
// we really need a HasManyList for Hierarchy objects,
|
||||||
|
// otherwise adding new items will not properly set the ParentID
|
||||||
|
$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->gridField->getConfig()->getComponentByType(GridFieldOrderableRows::class)) {
|
||||||
|
$config->addComponent(new GridFieldOrderableRows());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
/** @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);
|
||||||
|
|
||||||
|
$title = _t(get_class($this->record).'.'.strtoupper($relationName), ' ');
|
||||||
|
|
||||||
|
$fields = new FieldList(
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
$gridField->setModelClass($relationClass);
|
||||||
|
$gridField->setAttribute('data-class', str_replace('\\', '-', $relationClass));
|
||||||
|
$gridField->addExtraClass('nested');
|
||||||
|
$form = new Form($this, 'ItemEditForm', $fields, new FieldList());
|
||||||
|
|
||||||
|
$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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
12
templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss
Executable file
12
templates/Symbiote/GridFieldExtensions/GridFieldNestedForm.ss
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
<button
|
||||||
|
class="btn btn-secondary btn--no-text btn--icon-large <% if $Toggle == 'open' %>font-icon-down-dir<% else %>font-icon-right-dir<% end_if %> cms-panel-link list-children-link"
|
||||||
|
aria-expanded="<% if $Toggle == 'open' %>true<% else %>false<% end_if %>"
|
||||||
|
data-pjax-target="$PjaxFragment"
|
||||||
|
data-url="$Link"
|
||||||
|
data-toggle="$ToggleLink"
|
||||||
|
></button>
|
||||||
|
<% if $Toggle == 'open' %>
|
||||||
|
$NestedField
|
||||||
|
<% else %>
|
||||||
|
<div class="nested-container" data-pjax-fragment="$PjaxFragment" style="display:none;"></div>
|
||||||
|
<% end_if %>
|
88
tests/GridFieldNestedFormTest.php
Normal file
88
tests/GridFieldNestedFormTest.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symbiote\GridFieldExtensions\Tests;
|
||||||
|
|
||||||
|
use SilverStripe\Control\HTTPRequest;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\Forms\FieldList;
|
||||||
|
use SilverStripe\Forms\Form;
|
||||||
|
use SilverStripe\Forms\GridField\GridField;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
protected static $fixture_file = 'GridFieldNestedFormTest.yml';
|
||||||
|
|
||||||
|
protected static $extra_dataobjects = [
|
||||||
|
StubHierarchy::class,
|
||||||
|
StubParent::class,
|
||||||
|
StubOrdered::class
|
||||||
|
];
|
||||||
|
|
||||||
|
public function testHierarchy()
|
||||||
|
{
|
||||||
|
// test that GridFieldNestedForm works with hierarchy objects
|
||||||
|
$parent = $this->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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
19
tests/GridFieldNestedFormTest.yml
Normal file
19
tests/GridFieldNestedFormTest.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
|
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
|
20
tests/Stub/StubHierarchy.php
Normal file
20
tests/Stub/StubHierarchy.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symbiote\GridFieldExtensions\Tests\Stub;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\Hierarchy\Hierarchy;
|
||||||
|
|
||||||
|
class StubHierarchy extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static $table_name = 'StubHierarchy';
|
||||||
|
|
||||||
|
private static $extensions = [
|
||||||
|
Hierarchy::class
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $db = [
|
||||||
|
'Title' => 'Varchar'
|
||||||
|
];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user