2024-04-12 15:45:29 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Symbiote\GridFieldExtensions;
|
|
|
|
|
|
|
|
use Exception;
|
|
|
|
use SilverStripe\Admin\ModelAdmin;
|
|
|
|
use SilverStripe\Control\Controller;
|
|
|
|
use SilverStripe\Control\Director;
|
|
|
|
use SilverStripe\Control\HTTPResponse;
|
|
|
|
use SilverStripe\Control\HTTPResponse_Exception;
|
2024-04-18 09:56:35 +02:00
|
|
|
use SilverStripe\Core\Extensible;
|
|
|
|
use SilverStripe\Forms\GridField\AbstractGridFieldComponent;
|
2024-04-12 15:45:29 +02:00
|
|
|
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;
|
2024-04-18 09:56:35 +02:00
|
|
|
use SilverStripe\Forms\GridField\GridField_URLHandler;
|
|
|
|
use SilverStripe\Forms\GridField\GridFieldStateAware;
|
2024-04-12 15:45:29 +02:00
|
|
|
use SilverStripe\ORM\DataList;
|
|
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
use SilverStripe\ORM\DataObjectInterface;
|
|
|
|
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
|
|
|
|
*/
|
2024-04-18 09:56:35 +02:00
|
|
|
class GridFieldNestedForm extends AbstractGridFieldComponent implements
|
|
|
|
GridField_URLHandler,
|
2024-04-12 15:45:29 +02:00
|
|
|
GridField_ColumnProvider,
|
|
|
|
GridField_SaveHandler,
|
|
|
|
GridField_HTMLProvider,
|
|
|
|
GridField_DataManipulator
|
|
|
|
{
|
2024-04-18 09:56:35 +02:00
|
|
|
use Extensible, GridFieldStateAware;
|
2024-04-12 15:45:29 +02:00
|
|
|
|
|
|
|
const POST_KEY = 'GridFieldNestedForm';
|
|
|
|
|
|
|
|
private static $allowed_actions = [
|
|
|
|
'handleNestedItem'
|
|
|
|
];
|
|
|
|
|
2024-04-18 09:56:35 +02:00
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $name;
|
2024-04-12 15:45:29 +02:00
|
|
|
protected $expandNested = false;
|
|
|
|
protected $forceCloseNested = false;
|
|
|
|
protected $gridField = null;
|
|
|
|
protected $record = null;
|
|
|
|
protected $relationName = 'Children';
|
|
|
|
protected $inlineEditable = false;
|
|
|
|
protected $canExpandCheck = null;
|
|
|
|
|
|
|
|
public function __construct($name = 'NestedForm')
|
|
|
|
{
|
|
|
|
$this->name = $name;
|
|
|
|
}
|
|
|
|
|
2024-04-18 09:17:04 +02:00
|
|
|
/**
|
|
|
|
* Get the grid field that this component is attached to
|
|
|
|
* @return GridField
|
|
|
|
*/
|
2024-04-12 15:45:29 +02:00
|
|
|
public function getGridField()
|
|
|
|
{
|
|
|
|
return $this->gridField;
|
|
|
|
}
|
|
|
|
|
2024-04-18 09:17:04 +02:00
|
|
|
/**
|
|
|
|
* Get the relation name to use for the nested grid fields
|
|
|
|
* @return string
|
|
|
|
*/
|
2024-04-12 15:45:29 +02:00
|
|
|
public function getRelationName()
|
|
|
|
{
|
|
|
|
return $this->relationName;
|
|
|
|
}
|
|
|
|
|
2024-04-18 09:17:04 +02:00
|
|
|
/**
|
|
|
|
* Set the relation name to use for the nested grid fields
|
|
|
|
* @param string $relationName
|
|
|
|
*/
|
2024-04-12 15:45:29 +02:00
|
|
|
public function setRelationName($relationName)
|
|
|
|
{
|
|
|
|
$this->relationName = $relationName;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2024-04-18 09:17:04 +02:00
|
|
|
/**
|
|
|
|
* Get whether the nested grid fields should be inline editable
|
|
|
|
* @return boolean
|
|
|
|
*/
|
2024-04-12 15:45:29 +02:00
|
|
|
public function getInlineEditable()
|
|
|
|
{
|
|
|
|
return $this->inlineEditable;
|
|
|
|
}
|
|
|
|
|
2024-04-18 09:17:04 +02:00
|
|
|
/**
|
|
|
|
* Set whether the nested grid fields should be inline editable
|
|
|
|
* @param boolean $editable
|
|
|
|
*/
|
2024-04-12 15:45:29 +02:00
|
|
|
public function setInlineEditable($editable)
|
|
|
|
{
|
|
|
|
$this->inlineEditable = $editable;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2024-04-18 09:17:04 +02:00
|
|
|
/**
|
|
|
|
* Set whether the nested grid fields should be expanded by default
|
|
|
|
* @param boolean $expandNested
|
|
|
|
*/
|
2024-04-12 15:45:29 +02:00
|
|
|
public function setExpandNested($expandNested)
|
|
|
|
{
|
|
|
|
$this->expandNested = $expandNested;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2024-04-18 09:17:04 +02:00
|
|
|
/**
|
|
|
|
* Set whether the nested grid fields should be forced closed on load
|
|
|
|
* @param boolean $forceClosed
|
|
|
|
*/
|
2024-04-12 15:45:29 +02:00
|
|
|
public function setForceClosedNested($forceClosed)
|
|
|
|
{
|
|
|
|
$this->forceCloseNested = $forceClosed;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2024-04-18 09:17:04 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2024-04-12 15:45:29 +02:00
|
|
|
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) {
|
2024-04-18 11:41:58 +02:00
|
|
|
if (is_callable($this->canExpandCheck)
|
|
|
|
&& !call_user_func($this->canExpandCheck, $record)
|
|
|
|
) {
|
2024-04-12 15:45:29 +02:00
|
|
|
return '';
|
2024-04-18 11:41:58 +02:00
|
|
|
} elseif (is_string($this->canExpandCheck)
|
|
|
|
&& $record->hasMethod($this->canExpandCheck)
|
|
|
|
&& !$this->record->{$this->canExpandCheck}($record)
|
|
|
|
) {
|
2024-04-12 15:45:29 +02:00
|
|
|
return '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$toggle = 'closed';
|
|
|
|
$className = str_replace('\\', '-', get_class($record));
|
|
|
|
$state = $gridField->State->GridFieldNestedForm;
|
|
|
|
$stateRelation = $className.'-'.$record->ID.'-'.$this->relationName;
|
2024-04-18 11:41:58 +02:00
|
|
|
$openState = $state && (int)$state->getData($stateRelation) === 1;
|
|
|
|
$forceExpand = $this->expandNested && $record->$relationName()->count() > 0;
|
|
|
|
if (!$this->forceCloseNested
|
|
|
|
&& ($forceExpand || $openState)
|
|
|
|
) {
|
2024-04-12 15:45:29 +02:00
|
|
|
$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;
|
2024-04-18 11:41:58 +02:00
|
|
|
if (!$parent
|
|
|
|
&& $to
|
|
|
|
&& $gridField->getForm()->getController() instanceof GridFieldNestedFormItemRequest
|
|
|
|
&& $gridField->getForm()->getController()->getRecord()->ID == $to
|
|
|
|
) {
|
2024-04-12 15:45:29 +02:00
|
|
|
$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();
|
2024-04-18 11:41:58 +02:00
|
|
|
$stateRequest = $request ?: $gridField->getForm()->getRequestHandler()->getRequest();
|
|
|
|
if ($gridStateStr = $manager->getStateFromRequest($gridField, $stateRequest)) {
|
2024-04-12 15:45:29 +02:00
|
|
|
$gridField->getState(false)->setValue($gridStateStr);
|
|
|
|
}
|
|
|
|
$this->gridField = $gridField;
|
|
|
|
$this->record = $record;
|
2024-04-18 11:41:58 +02:00
|
|
|
$itemRequest = GridFieldNestedFormItemRequest::create(
|
|
|
|
$gridField,
|
|
|
|
$this,
|
|
|
|
$record,
|
|
|
|
$gridField->getForm()->getController(),
|
|
|
|
$this->name
|
|
|
|
);
|
2024-04-12 15:45:29 +02:00
|
|
|
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)
|
|
|
|
{
|
2024-04-18 13:39:25 +02:00
|
|
|
$postKey = self::POST_KEY;
|
2024-04-12 15:45:29 +02:00
|
|
|
$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();
|
2024-04-18 11:41:58 +02:00
|
|
|
$request = $gridField->getForm()->getRequestHandler()->getRequest();
|
|
|
|
if ($gridStateStr = $manager->getStateFromRequest($gridField, $request)) {
|
2024-04-12 15:45:29 +02:00
|
|
|
$gridField->getState(false)->setValue($gridStateStr);
|
|
|
|
}
|
2024-04-18 13:39:25 +02:00
|
|
|
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);
|
|
|
|
}
|
2024-04-12 15:45:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getManipulatedData(GridField $gridField, SS_List $dataList)
|
|
|
|
{
|
2024-04-18 11:41:58 +02:00
|
|
|
if ($this->relationName == 'Children'
|
|
|
|
&& DataObject::has_extension($gridField->getModelClass(), Hierarchy::class)
|
|
|
|
&& $gridField->getForm()->getController() instanceof ModelAdmin
|
|
|
|
) {
|
2024-04-12 15:45:29 +02:00
|
|
|
$dataList = $dataList->filter('ParentID', 0);
|
|
|
|
}
|
|
|
|
return $dataList;
|
|
|
|
}
|
|
|
|
}
|