2015-09-21 12:49:02 +02:00
|
|
|
<?php
|
2016-09-09 09:00:05 +02:00
|
|
|
|
2017-06-16 06:07:09 +02:00
|
|
|
namespace Symbiote\GridFieldExtensions;
|
2016-11-29 18:20:15 +01:00
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
use Exception;
|
2016-09-09 09:00:05 +02:00
|
|
|
use SilverStripe\Control\Controller;
|
2018-09-28 17:38:38 +02:00
|
|
|
use SilverStripe\Control\HTTPRequest;
|
|
|
|
use SilverStripe\Control\HTTPResponse_Exception;
|
2016-09-09 09:00:05 +02:00
|
|
|
use SilverStripe\Control\RequestHandler;
|
|
|
|
use SilverStripe\Core\ClassInfo;
|
|
|
|
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;
|
2018-01-26 01:40:06 +01:00
|
|
|
use SilverStripe\Forms\GridField\GridFieldPaginator;
|
2016-09-09 09:00:05 +02:00
|
|
|
use SilverStripe\Forms\HiddenField;
|
|
|
|
use SilverStripe\ORM\ArrayList;
|
|
|
|
use SilverStripe\ORM\DataList;
|
2018-01-26 01:40:06 +01:00
|
|
|
use SilverStripe\ORM\DataObject;
|
2016-09-09 09:00:05 +02:00
|
|
|
use SilverStripe\ORM\DataObjectInterface;
|
2018-05-04 07:31:00 +02:00
|
|
|
use SilverStripe\ORM\DataObjectSchema;
|
2018-01-26 01:40:06 +01:00
|
|
|
use SilverStripe\ORM\DB;
|
2018-09-28 17:38:38 +02:00
|
|
|
use SilverStripe\ORM\FieldType\DBDatetime;
|
2016-09-09 09:00:05 +02:00
|
|
|
use SilverStripe\ORM\ManyManyList;
|
2018-05-04 07:31:00 +02:00
|
|
|
use SilverStripe\ORM\ManyManyThroughList;
|
|
|
|
use SilverStripe\ORM\ManyManyThroughQueryManipulator;
|
2016-09-09 09:00:05 +02:00
|
|
|
use SilverStripe\ORM\SS_List;
|
2018-01-30 21:40:59 +01:00
|
|
|
use SilverStripe\Versioned\Versioned;
|
2016-09-09 09:00:05 +02:00
|
|
|
use SilverStripe\View\ViewableData;
|
|
|
|
|
2015-09-21 12:49:02 +02:00
|
|
|
/**
|
|
|
|
* Allows grid field rows to be re-ordered via drag and drop. Both normal data
|
|
|
|
* lists and many many lists can be ordered.
|
|
|
|
*
|
|
|
|
* If the grid field has not been sorted, this component will sort the data by
|
|
|
|
* the sort field.
|
|
|
|
*/
|
|
|
|
class GridFieldOrderableRows extends RequestHandler implements
|
2016-12-21 03:34:58 +01:00
|
|
|
GridField_ColumnProvider,
|
|
|
|
GridField_DataManipulator,
|
|
|
|
GridField_HTMLProvider,
|
|
|
|
GridField_URLHandler,
|
|
|
|
GridField_SaveHandler
|
|
|
|
{
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see $immediateUpdate
|
|
|
|
* @var boolean
|
|
|
|
*/
|
|
|
|
private static $default_immediate_update = true;
|
|
|
|
|
|
|
|
private static $allowed_actions = array(
|
|
|
|
'handleReorder',
|
|
|
|
'handleMoveToPage'
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The database field which specifies the sort, defaults to "Sort".
|
|
|
|
*
|
|
|
|
* @see setSortField()
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $sortField;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If set to true, when an item is re-ordered, it will update on the
|
|
|
|
* database and refresh the gridfield. When set to false, it will only
|
|
|
|
* update the sort order when the record is saved.
|
|
|
|
*
|
|
|
|
* @var boolean
|
|
|
|
*/
|
|
|
|
protected $immediateUpdate;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extra sort fields to apply before the sort field.
|
|
|
|
*
|
|
|
|
* @see setExtraSortFields()
|
|
|
|
* @var string|array
|
|
|
|
*/
|
|
|
|
protected $extraSortFields = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The number of the column containing the reorder handles
|
|
|
|
*
|
|
|
|
* @see setReorderColumnNumber()
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
protected $reorderColumnNumber = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $sortField
|
|
|
|
*/
|
|
|
|
public function __construct($sortField = 'Sort')
|
|
|
|
{
|
|
|
|
parent::__construct();
|
|
|
|
$this->sortField = $sortField;
|
|
|
|
$this->immediateUpdate = $this->config()->default_immediate_update;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getSortField()
|
|
|
|
{
|
|
|
|
return $this->sortField;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the field used to specify the sort.
|
|
|
|
*
|
|
|
|
* @param string $sortField
|
|
|
|
* @return GridFieldOrderableRows $this
|
|
|
|
*/
|
|
|
|
public function setSortField($field)
|
|
|
|
{
|
|
|
|
$this->sortField = $field;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function getImmediateUpdate()
|
|
|
|
{
|
|
|
|
return $this->immediateUpdate;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see $immediateUpdate
|
|
|
|
* @param boolean $immediateUpdate
|
|
|
|
* @return GridFieldOrderableRows $this
|
|
|
|
*/
|
|
|
|
public function setImmediateUpdate($bool)
|
|
|
|
{
|
|
|
|
$this->immediateUpdate = $bool;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string|array
|
|
|
|
*/
|
|
|
|
public function getExtraSortFields()
|
|
|
|
{
|
|
|
|
return $this->extraSortFields;
|
|
|
|
}
|
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
/**
|
|
|
|
* Checks to see if the relationship list is for a type of many_many
|
|
|
|
*
|
|
|
|
* @param SS_List $list
|
2018-06-25 02:22:27 +02:00
|
|
|
*
|
|
|
|
* @return bool
|
2018-05-04 07:31:00 +02:00
|
|
|
*/
|
|
|
|
protected function isManyMany(SS_List $list)
|
|
|
|
{
|
|
|
|
return $list instanceof ManyManyList || $list instanceof ManyManyThroughList;
|
|
|
|
}
|
|
|
|
|
2016-12-21 03:34:58 +01:00
|
|
|
/**
|
|
|
|
* Sets extra sort fields to apply before the sort field.
|
|
|
|
*
|
|
|
|
* @param string|array $fields
|
|
|
|
* @return GridFieldOrderableRows $this
|
|
|
|
*/
|
|
|
|
public function setExtraSortFields($fields)
|
|
|
|
{
|
|
|
|
$this->extraSortFields = $fields;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
public function getReorderColumnNumber()
|
|
|
|
{
|
|
|
|
return $this->reorderColumnNumber;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the number of the column containing the reorder handles.
|
|
|
|
*
|
|
|
|
* @param int $colno
|
|
|
|
* @return GridFieldOrderableRows $this
|
|
|
|
*/
|
|
|
|
public function setReorderColumnNumber($colno)
|
|
|
|
{
|
|
|
|
$this->reorderColumnNumber = $colno;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2017-12-17 20:18:33 +01:00
|
|
|
/**
|
|
|
|
* Validates sortable list
|
|
|
|
*
|
|
|
|
* @param SS_List $list
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function validateSortField(SS_List $list)
|
|
|
|
{
|
|
|
|
$field = $this->getSortField();
|
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
// Check extra fields on many many relation types
|
2017-12-17 20:18:33 +01:00
|
|
|
if ($list instanceof ManyManyList) {
|
|
|
|
$extra = $list->getExtraFields();
|
|
|
|
|
|
|
|
if ($extra && array_key_exists($field, $extra)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-05-04 07:31:00 +02:00
|
|
|
} elseif ($list instanceof ManyManyThroughList) {
|
|
|
|
$manipulator = $this->getManyManyInspector($list);
|
|
|
|
$fieldTable = DataObject::getSchema()->tableForField($manipulator->getJoinClass(), $field);
|
|
|
|
if ($fieldTable) {
|
|
|
|
return;
|
|
|
|
}
|
2017-12-17 20:18:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
|
|
|
|
|
|
|
foreach ($classes as $class) {
|
|
|
|
if (singleton($class)->hasDataBaseField($field)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
throw new Exception("Couldn't find the sort field '" . $field . "'");
|
2017-12-17 20:18:33 +01:00
|
|
|
}
|
|
|
|
|
2016-12-21 03:34:58 +01:00
|
|
|
/**
|
|
|
|
* Gets the table which contains the sort field.
|
|
|
|
*
|
|
|
|
* @param DataList $list
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getSortTable(SS_List $list)
|
|
|
|
{
|
|
|
|
$field = $this->getSortField();
|
|
|
|
if ($list instanceof ManyManyList) {
|
|
|
|
$extra = $list->getExtraFields();
|
|
|
|
$table = $list->getJoinTable();
|
|
|
|
if ($extra && array_key_exists($field, $extra)) {
|
|
|
|
return $table;
|
|
|
|
}
|
2018-05-04 07:31:00 +02:00
|
|
|
} elseif ($list instanceof ManyManyThroughList) {
|
|
|
|
return $this->getManyManyInspector($list)->getJoinAlias();
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
|
|
|
foreach ($classes as $class) {
|
|
|
|
if (singleton($class)->hasDataBaseField($field)) {
|
2017-05-14 06:17:14 +02:00
|
|
|
return DataObject::getSchema()->tableName($class);
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
}
|
2018-05-04 07:31:00 +02:00
|
|
|
throw new Exception("Couldn't find the sort field '$field'");
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getURLHandlers($grid)
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
'POST reorder' => 'handleReorder',
|
|
|
|
'POST movetopage' => 'handleMoveToPage'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param GridField $field
|
|
|
|
*/
|
|
|
|
public function getHTMLFragments($field)
|
|
|
|
{
|
|
|
|
GridFieldExtensions::include_requirements();
|
|
|
|
|
|
|
|
$field->addExtraClass('ss-gridfield-orderable');
|
|
|
|
$field->setAttribute('data-immediate-update', (string)(int)$this->immediateUpdate);
|
|
|
|
$field->setAttribute('data-url-reorder', $field->Link('reorder'));
|
|
|
|
$field->setAttribute('data-url-movetopage', $field->Link('movetopage'));
|
|
|
|
}
|
|
|
|
|
|
|
|
public function augmentColumns($grid, &$cols)
|
|
|
|
{
|
|
|
|
if (!in_array('Reorder', $cols) && $grid->getState()->GridFieldOrderableRows->enabled) {
|
|
|
|
array_splice($cols, $this->reorderColumnNumber, 0, 'Reorder');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getColumnsHandled($grid)
|
|
|
|
{
|
|
|
|
return array('Reorder');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getColumnContent($grid, $record, $col)
|
|
|
|
{
|
|
|
|
// In case you are using GridFieldEditableColumns, this ensures that
|
|
|
|
// the correct sort order is saved. If you are not using that component,
|
|
|
|
// this will be ignored by other components, but will still work for this.
|
|
|
|
$sortFieldName = sprintf(
|
|
|
|
'%s[GridFieldEditableColumns][%s][%s]',
|
|
|
|
$grid->getName(),
|
|
|
|
$record->ID,
|
|
|
|
$this->getSortField()
|
|
|
|
);
|
2018-09-28 17:38:38 +02:00
|
|
|
|
|
|
|
// Default: Get the sort field directly from the current record
|
|
|
|
$currentSortValue = $record->getField($this->getSortField());
|
|
|
|
|
|
|
|
$list = $grid->getList();
|
|
|
|
if ($list instanceof ManyManyThroughList) {
|
|
|
|
// In a many many through list we should get the current sort order from the relationship
|
|
|
|
// if it exists, not directly from the record
|
|
|
|
$throughListSorts = $this->getSortValuesFromManyManyThroughList($list, $this->getSortField());
|
|
|
|
|
|
|
|
if (array_key_exists($record->ID, $throughListSorts)) {
|
|
|
|
$currentSortValue = $throughListSorts[$record->ID];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$sortField = HiddenField::create($sortFieldName, false, $currentSortValue);
|
2016-12-21 03:34:58 +01:00
|
|
|
$sortField->addExtraClass('ss-orderable-hidden-sort');
|
|
|
|
$sortField->setForm($grid->getForm());
|
|
|
|
|
|
|
|
return ViewableData::create()->customise(array(
|
|
|
|
'SortField' => $sortField
|
2017-06-16 06:07:09 +02:00
|
|
|
))->renderWith('Symbiote\\GridFieldExtensions\\GridFieldOrderableRowsDragHandle');
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getColumnAttributes($grid, $record, $col)
|
|
|
|
{
|
|
|
|
return array('class' => 'col-reorder');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getColumnMetadata($grid, $col)
|
|
|
|
{
|
|
|
|
if ($fieldLabels = singleton($grid->getModelClass())->fieldLabels()) {
|
|
|
|
return array('title' => isset($fieldLabels['Reorder']) ? $fieldLabels['Reorder'] : '');
|
|
|
|
}
|
|
|
|
|
|
|
|
return array('title' => '');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getManipulatedData(GridField $grid, SS_List $list)
|
|
|
|
{
|
|
|
|
$state = $grid->getState();
|
|
|
|
$sorted = (bool) ((string) $state->GridFieldSortableHeader->SortColumn);
|
|
|
|
|
|
|
|
// If the data has not been sorted by the user, then sort it by the
|
|
|
|
// sort column, otherwise disable reordering.
|
|
|
|
$state->GridFieldOrderableRows->enabled = !$sorted;
|
|
|
|
|
|
|
|
if (!$sorted) {
|
|
|
|
$sortterm = '';
|
|
|
|
if ($this->extraSortFields) {
|
|
|
|
if (is_array($this->extraSortFields)) {
|
|
|
|
foreach ($this->extraSortFields as $col => $dir) {
|
|
|
|
$sortterm .= "$col $dir, ";
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$sortterm = $this->extraSortFields.', ';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($list instanceof ArrayList) {
|
|
|
|
// Fix bug in 3.1.3+ where ArrayList doesn't account for quotes
|
|
|
|
$sortterm .= $this->getSortTable($list).'.'.$this->getSortField();
|
|
|
|
} else {
|
|
|
|
$sortterm .= '"'.$this->getSortTable($list).'"."'.$this->getSortField().'"';
|
|
|
|
}
|
|
|
|
return $list->sort($sortterm);
|
|
|
|
}
|
2018-09-28 17:38:38 +02:00
|
|
|
|
|
|
|
return $list;
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles requests to reorder a set of IDs in a specific order.
|
|
|
|
*
|
|
|
|
* @param GridField $grid
|
2018-09-28 17:38:38 +02:00
|
|
|
* @param HTTPRequest $request
|
|
|
|
* @return string
|
|
|
|
* @throws HTTPResponse_Exception
|
2016-12-21 03:34:58 +01:00
|
|
|
*/
|
|
|
|
public function handleReorder($grid, $request)
|
|
|
|
{
|
|
|
|
if (!$this->immediateUpdate) {
|
|
|
|
$this->httpError(400);
|
|
|
|
}
|
|
|
|
$list = $grid->getList();
|
|
|
|
$modelClass = $grid->getModelClass();
|
2018-05-04 07:31:00 +02:00
|
|
|
$isManyMany = $this->isManyMany($list);
|
|
|
|
if ($isManyMany && !singleton($modelClass)->canView()) {
|
2016-12-21 03:34:58 +01:00
|
|
|
$this->httpError(403);
|
2018-05-04 07:31:00 +02:00
|
|
|
} elseif (!$isManyMany && !singleton($modelClass)->canEdit()) {
|
2016-12-21 03:34:58 +01:00
|
|
|
$this->httpError(403);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save any un-committed changes to the gridfield
|
|
|
|
if (($form = $grid->getForm()) && ($record = $form->getRecord())) {
|
|
|
|
$form->loadDataFrom($request->requestVars(), true);
|
|
|
|
$grid->saveInto($record);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get records from the `GridFieldEditableColumns` column
|
|
|
|
$data = $request->postVar($grid->getName());
|
|
|
|
$sortedIDs = $this->getSortedIDs($data);
|
|
|
|
if (!$this->executeReorder($grid, $sortedIDs)) {
|
|
|
|
$this->httpError(400);
|
|
|
|
}
|
|
|
|
|
|
|
|
Controller::curr()->getResponse()->addHeader('X-Status', rawurlencode('Records reordered.'));
|
|
|
|
return $grid->FieldHolder();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-05-04 07:31:00 +02:00
|
|
|
* Get mapping of sort value to item ID from posted data (gridfield list state), ordered by sort value.
|
2016-12-21 03:34:58 +01:00
|
|
|
*
|
|
|
|
* @param array $data Raw posted data
|
2018-05-04 07:31:00 +02:00
|
|
|
* @return array [sortIndex => recordID]
|
2016-12-21 03:34:58 +01:00
|
|
|
*/
|
|
|
|
protected function getSortedIDs($data)
|
|
|
|
{
|
|
|
|
if (empty($data['GridFieldEditableColumns'])) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
$sortedIDs = array();
|
|
|
|
foreach ($data['GridFieldEditableColumns'] as $id => $recordData) {
|
|
|
|
$sortValue = $recordData[$this->sortField];
|
|
|
|
$sortedIDs[$sortValue] = $id;
|
|
|
|
}
|
|
|
|
ksort($sortedIDs);
|
|
|
|
return $sortedIDs;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles requests to move an item to the previous or next page.
|
|
|
|
*/
|
|
|
|
public function handleMoveToPage(GridField $grid, $request)
|
|
|
|
{
|
2018-01-26 01:40:06 +01:00
|
|
|
if (!$paginator = $grid->getConfig()->getComponentByType(GridFieldPaginator::class)) {
|
2016-12-21 03:34:58 +01:00
|
|
|
$this->httpError(404, 'Paginator component not found');
|
|
|
|
}
|
|
|
|
|
|
|
|
$move = $request->postVar('move');
|
|
|
|
$field = $this->getSortField();
|
|
|
|
|
|
|
|
$list = $grid->getList();
|
|
|
|
$manip = $grid->getManipulatedList();
|
|
|
|
|
|
|
|
$existing = $manip->map('ID', $field)->toArray();
|
|
|
|
$values = $existing;
|
|
|
|
$order = array();
|
|
|
|
|
|
|
|
$id = isset($move['id']) ? (int) $move['id'] : null;
|
|
|
|
$to = isset($move['page']) ? $move['page'] : null;
|
|
|
|
|
|
|
|
if (!isset($values[$id])) {
|
|
|
|
$this->httpError(400, 'Invalid item ID');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->populateSortValues($list);
|
|
|
|
|
|
|
|
$page = ((int) $grid->getState()->GridFieldPaginator->currentPage) ?: 1;
|
|
|
|
$per = $paginator->getItemsPerPage();
|
|
|
|
|
|
|
|
if ($to == 'prev') {
|
|
|
|
$swap = $list->limit(1, ($page - 1) * $per - 1)->first();
|
|
|
|
$values[$swap->ID] = $swap->$field;
|
|
|
|
|
|
|
|
$order[] = $id;
|
|
|
|
$order[] = $swap->ID;
|
|
|
|
|
|
|
|
foreach ($existing as $_id => $sort) {
|
|
|
|
if ($id != $_id) {
|
|
|
|
$order[] = $_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} elseif ($to == 'next') {
|
|
|
|
$swap = $list->limit(1, $page * $per)->first();
|
|
|
|
$values[$swap->ID] = $swap->$field;
|
|
|
|
|
|
|
|
foreach ($existing as $_id => $sort) {
|
|
|
|
if ($id != $_id) {
|
|
|
|
$order[] = $_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$order[] = $swap->ID;
|
|
|
|
$order[] = $id;
|
|
|
|
} else {
|
|
|
|
$this->httpError(400, 'Invalid page target');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->reorderItems($list, $values, $order);
|
|
|
|
|
|
|
|
return $grid->FieldHolder();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle saving when 'immediateUpdate' is disabled, otherwise this isn't
|
|
|
|
* necessary for the default sort mode.
|
|
|
|
*/
|
|
|
|
public function handleSave(GridField $grid, DataObjectInterface $record)
|
|
|
|
{
|
|
|
|
if (!$this->immediateUpdate) {
|
|
|
|
$value = $grid->Value();
|
|
|
|
$sortedIDs = $this->getSortedIDs($value);
|
|
|
|
if ($sortedIDs) {
|
|
|
|
$this->executeReorder($grid, $sortedIDs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param GridField $grid
|
|
|
|
* @param array $sortedIDs List of IDS, where the key is the sort field value to save
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
protected function executeReorder(GridField $grid, $sortedIDs)
|
|
|
|
{
|
2018-06-25 02:22:27 +02:00
|
|
|
if (!is_array($sortedIDs) || empty($sortedIDs)) {
|
2016-12-21 03:34:58 +01:00
|
|
|
return false;
|
|
|
|
}
|
2018-05-04 07:31:00 +02:00
|
|
|
$sortField = $this->getSortField();
|
2016-12-21 03:34:58 +01:00
|
|
|
|
|
|
|
$sortterm = '';
|
|
|
|
if ($this->extraSortFields) {
|
|
|
|
if (is_array($this->extraSortFields)) {
|
|
|
|
foreach ($this->extraSortFields as $col => $dir) {
|
|
|
|
$sortterm .= "$col $dir, ";
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$sortterm = $this->extraSortFields.', ';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$list = $grid->getList();
|
2018-05-04 07:31:00 +02:00
|
|
|
$sortterm .= '"'.$this->getSortTable($list).'"."'.$sortField.'"';
|
2016-12-21 03:34:58 +01:00
|
|
|
$items = $list->filter('ID', $sortedIDs)->sort($sortterm);
|
|
|
|
|
|
|
|
// Ensure that each provided ID corresponded to an actual object.
|
|
|
|
if (count($items) != count($sortedIDs)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate each object we are sorting with a sort value.
|
|
|
|
$this->populateSortValues($items);
|
|
|
|
|
|
|
|
// Generate the current sort values.
|
|
|
|
if ($items instanceof ManyManyList) {
|
|
|
|
$current = array();
|
|
|
|
foreach ($items->toArray() as $record) {
|
|
|
|
// NOTE: _SortColumn0 is the first ->sort() field
|
2017-07-05 17:15:20 +02:00
|
|
|
// used by SS when functions are detected in a SELECT
|
|
|
|
// or CASE WHEN.
|
2016-12-21 03:34:58 +01:00
|
|
|
if (isset($record->_SortColumn0)) {
|
|
|
|
$current[$record->ID] = $record->_SortColumn0;
|
|
|
|
} else {
|
2018-05-04 07:31:00 +02:00
|
|
|
$current[$record->ID] = $record->$sortField;
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
}
|
2018-05-04 07:31:00 +02:00
|
|
|
} elseif ($items instanceof ManyManyThroughList) {
|
2018-09-28 17:38:38 +02:00
|
|
|
$current = $this->getSortValuesFromManyManyThroughList($list, $sortField);
|
2016-12-21 03:34:58 +01:00
|
|
|
} else {
|
2018-05-04 07:31:00 +02:00
|
|
|
$current = $items->map('ID', $sortField)->toArray();
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Perform the actual re-ordering.
|
|
|
|
$this->reorderItems($list, $current, $sortedIDs);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
/**
|
|
|
|
* @param SS_List $list
|
|
|
|
* @param array $values **UNUSED** [listItemID => currentSortValue];
|
|
|
|
* @param array $sortedIDs [newSortValue => listItemID]
|
|
|
|
*/
|
2016-12-21 03:34:58 +01:00
|
|
|
protected function reorderItems($list, array $values, array $sortedIDs)
|
|
|
|
{
|
2018-05-04 07:31:00 +02:00
|
|
|
// setup
|
2016-12-21 03:34:58 +01:00
|
|
|
$sortField = $this->getSortField();
|
2018-05-04 07:31:00 +02:00
|
|
|
$class = $list->dataClass();
|
|
|
|
// The problem is that $sortedIDs is a list of the _related_ item IDs, which causes trouble
|
|
|
|
// with ManyManyThrough, where we need the ID of the _join_ item in order to set the value.
|
|
|
|
$itemToSortReference = ($list instanceof ManyManyThroughList) ? 'getJoin' : 'Me';
|
|
|
|
$currentSortList = $list->map('ID', $itemToSortReference)->toArray();
|
2016-12-21 03:34:58 +01:00
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
// sanity check.
|
2017-12-17 20:18:33 +01:00
|
|
|
$this->validateSortField($list);
|
2018-03-06 02:45:29 +01:00
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
$isVersioned = false;
|
2018-04-26 05:17:05 +02:00
|
|
|
// check if sort column is present on the model provided by dataClass() and if it's versioned
|
|
|
|
// cases:
|
|
|
|
// Model has sort column and is versioned - handle as versioned
|
|
|
|
// Model has sort column and is NOT versioned - handle as NOT versioned
|
|
|
|
// Model doesn't have sort column because sort column is on ManyManyList - handle as NOT versioned
|
2018-05-04 07:31:00 +02:00
|
|
|
// Model doesn't have sort column because sort column is on ManyManyThroughList...
|
|
|
|
// - Related item is not versioned:
|
|
|
|
// - Through object is versioned: THROW an error.
|
|
|
|
// - Through object is NOT versioned: handle as NOT versioned
|
|
|
|
// - Related item is versioned...
|
|
|
|
// - Through object is versioned: handle as versioned
|
|
|
|
// - Through object is NOT versioned: THROW an error.
|
2018-06-25 02:22:27 +02:00
|
|
|
if ($list instanceof ManyManyThroughList) {
|
2018-05-04 07:31:00 +02:00
|
|
|
$listClassVersioned = $class::create()->hasExtension(Versioned::class);
|
|
|
|
// We'll be updating the join class, not the relation class.
|
|
|
|
$class = $this->getManyManyInspector($list)->getJoinClass();
|
|
|
|
$isVersioned = $class::create()->hasExtension(Versioned::class);
|
|
|
|
|
2018-06-25 02:22:27 +02:00
|
|
|
// If one side of the relationship is versioned and the other is not, throw an error.
|
2018-05-04 07:31:00 +02:00
|
|
|
if ($listClassVersioned xor $isVersioned) {
|
|
|
|
throw new Exception(
|
|
|
|
'ManyManyThrough cannot mismatch Versioning between join class and related class'
|
|
|
|
);
|
2018-04-26 05:17:05 +02:00
|
|
|
}
|
2018-06-25 02:22:27 +02:00
|
|
|
} elseif (!$this->isManyMany($list)) {
|
2018-05-04 07:31:00 +02:00
|
|
|
$isVersioned = $class::create()->hasExtension(Versioned::class);
|
2018-03-06 02:45:29 +01:00
|
|
|
}
|
2016-12-21 03:34:58 +01:00
|
|
|
|
|
|
|
// Loop through each item, and update the sort values which do not
|
|
|
|
// match to order the objects.
|
|
|
|
if (!$isVersioned) {
|
|
|
|
$sortTable = $this->getSortTable($list);
|
2018-02-21 10:49:04 +01:00
|
|
|
$now = DBDatetime::now()->Rfc2822();
|
2017-07-05 17:15:20 +02:00
|
|
|
$additionalSQL = '';
|
2018-05-04 07:31:00 +02:00
|
|
|
$baseTable = DataObject::getSchema()->baseDataTable($class);
|
2018-02-21 12:58:48 +01:00
|
|
|
|
2017-07-05 17:15:20 +02:00
|
|
|
$isBaseTable = ($baseTable == $sortTable);
|
2017-09-01 03:02:18 +02:00
|
|
|
if (!$list instanceof ManyManyList && $isBaseTable) {
|
2018-02-21 12:44:42 +01:00
|
|
|
$additionalSQL = ", \"LastEdited\" = '$now'";
|
2017-07-05 17:15:20 +02:00
|
|
|
}
|
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
foreach ($sortedIDs as $newSortValue => $targetRecordID) {
|
|
|
|
if ($currentSortList[$targetRecordID]->$sortField != $newSortValue) {
|
2016-12-21 03:34:58 +01:00
|
|
|
DB::query(sprintf(
|
|
|
|
'UPDATE "%s" SET "%s" = %d%s WHERE %s',
|
|
|
|
$sortTable,
|
|
|
|
$sortField,
|
2018-05-04 07:31:00 +02:00
|
|
|
$newSortValue,
|
2016-12-21 03:34:58 +01:00
|
|
|
$additionalSQL,
|
2018-05-04 07:31:00 +02:00
|
|
|
$this->getSortTableClauseForIds($list, $targetRecordID)
|
2016-12-21 03:34:58 +01:00
|
|
|
));
|
2017-07-05 17:15:20 +02:00
|
|
|
|
2018-02-21 12:58:48 +01:00
|
|
|
if (!$isBaseTable && !$list instanceof ManyManyList) {
|
2017-07-05 17:15:20 +02:00
|
|
|
DB::query(sprintf(
|
2018-02-21 12:44:42 +01:00
|
|
|
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s',
|
2017-07-05 17:15:20 +02:00
|
|
|
$baseTable,
|
2018-02-21 12:44:42 +01:00
|
|
|
$now,
|
2018-05-04 07:31:00 +02:00
|
|
|
$this->getSortTableClauseForIds($list, $targetRecordID)
|
2017-07-05 17:15:20 +02:00
|
|
|
));
|
|
|
|
}
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// For versioned objects, modify them with the ORM so that the
|
2018-05-04 07:31:00 +02:00
|
|
|
// *_Versions table is updated. This ensures re-ordering works
|
2016-12-21 03:34:58 +01:00
|
|
|
// similar to the SiteTree where you change the position, and then
|
|
|
|
// you go into the record and publish it.
|
2018-05-04 07:31:00 +02:00
|
|
|
foreach ($sortedIDs as $newSortValue => $targetRecordID) {
|
|
|
|
// either the list data class (has_many, (belongs_)many_many)
|
|
|
|
// or the intermediary join class (many_many through)
|
|
|
|
$record = $currentSortList[$targetRecordID];
|
|
|
|
if ($record->$sortField != $newSortValue) {
|
|
|
|
$record->$sortField = $newSortValue;
|
2016-12-21 03:34:58 +01:00
|
|
|
$record->write();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-24 04:25:13 +02:00
|
|
|
$this->extend('onAfterReorderItems', $list, $values, $sortedIDs);
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function populateSortValues(DataList $list)
|
|
|
|
{
|
|
|
|
$list = clone $list;
|
|
|
|
$field = $this->getSortField();
|
|
|
|
$table = $this->getSortTable($list);
|
|
|
|
$clause = sprintf('"%s"."%s" = 0', $table, $this->getSortField());
|
2018-02-21 10:49:04 +01:00
|
|
|
$now = DBDatetime::now()->Rfc2822();
|
2017-07-05 17:15:20 +02:00
|
|
|
$additionalSQL = '';
|
2018-02-21 12:58:48 +01:00
|
|
|
$baseTable = DataObject::getSchema()->baseDataTable($list->dataClass());
|
|
|
|
|
2017-07-05 17:15:20 +02:00
|
|
|
$isBaseTable = ($baseTable == $table);
|
2017-09-01 03:02:18 +02:00
|
|
|
if (!$list instanceof ManyManyList && $isBaseTable) {
|
2018-02-21 12:44:42 +01:00
|
|
|
$additionalSQL = ", \"LastEdited\" = '$now'";
|
2017-07-05 17:15:20 +02:00
|
|
|
}
|
2016-12-21 03:34:58 +01:00
|
|
|
|
|
|
|
foreach ($list->where($clause)->column('ID') as $id) {
|
|
|
|
$max = DB::query(sprintf('SELECT MAX("%s") + 1 FROM "%s"', $field, $table));
|
|
|
|
$max = $max->value();
|
|
|
|
|
|
|
|
DB::query(sprintf(
|
|
|
|
'UPDATE "%s" SET "%s" = %d%s WHERE %s',
|
|
|
|
$table,
|
|
|
|
$field,
|
|
|
|
$max,
|
|
|
|
$additionalSQL,
|
|
|
|
$this->getSortTableClauseForIds($list, $id)
|
|
|
|
));
|
2017-07-05 17:15:20 +02:00
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
if (!$isBaseTable && !$this->isManyMany($list)) {
|
2017-07-05 17:15:20 +02:00
|
|
|
DB::query(sprintf(
|
2018-02-21 12:44:42 +01:00
|
|
|
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s',
|
2017-07-05 17:15:20 +02:00
|
|
|
$baseTable,
|
2018-02-21 12:44:42 +01:00
|
|
|
$now,
|
2017-07-05 17:15:20 +02:00
|
|
|
$this->getSortTableClauseForIds($list, $id)
|
|
|
|
));
|
|
|
|
}
|
2016-12-21 03:34:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
/**
|
|
|
|
* Forms a WHERE clause for the table the sort column is defined on.
|
|
|
|
* e.g. ID = 5
|
|
|
|
* e.g. ID IN(5, 8, 10)
|
|
|
|
* e.g. SortOrder = 5 AND RelatedThing.ID = 3
|
|
|
|
* e.g. SortOrder IN(5, 8, 10) AND RelatedThing.ID = 3
|
|
|
|
*
|
|
|
|
* @param DataList $list
|
|
|
|
* @param int|string|array $ids a single number, or array of numbers
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
2016-12-21 03:34:58 +01:00
|
|
|
protected function getSortTableClauseForIds(DataList $list, $ids)
|
|
|
|
{
|
|
|
|
if (is_array($ids)) {
|
|
|
|
$value = 'IN (' . implode(', ', array_map('intval', $ids)) . ')';
|
|
|
|
} else {
|
|
|
|
$value = '= ' . (int) $ids;
|
|
|
|
}
|
|
|
|
|
2018-05-04 07:31:00 +02:00
|
|
|
if ($this->isManyMany($list)) {
|
2018-06-25 02:22:27 +02:00
|
|
|
$introspector = $this->getManyManyInspector($list);
|
2018-05-04 07:31:00 +02:00
|
|
|
$extra = $list instanceof ManyManyList ?
|
2018-06-25 02:22:27 +02:00
|
|
|
$introspector->getExtraFields() :
|
|
|
|
DataObjectSchema::create()->fieldSpecs($introspector->getJoinClass(), DataObjectSchema::DB_ONLY);
|
|
|
|
$key = $introspector->getLocalKey();
|
|
|
|
$foreignKey = $introspector->getForeignKey();
|
2016-12-21 03:34:58 +01:00
|
|
|
$foreignID = (int) $list->getForeignID();
|
|
|
|
|
|
|
|
if ($extra && array_key_exists($this->getSortField(), $extra)) {
|
|
|
|
return sprintf(
|
|
|
|
'"%s" %s AND "%s" = %d',
|
|
|
|
$key,
|
|
|
|
$value,
|
|
|
|
$foreignKey,
|
|
|
|
$foreignID
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "\"ID\" $value";
|
|
|
|
}
|
2018-05-04 07:31:00 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A ManyManyList defines functions such as getLocalKey, however on ManyManyThroughList
|
|
|
|
* these functions are moved to ManyManyThroughQueryManipulator, but otherwise retain
|
|
|
|
* the same signature.
|
|
|
|
*
|
2018-06-25 02:22:27 +02:00
|
|
|
* @param ManyManyList|ManyManyThroughList $list
|
2018-05-04 07:31:00 +02:00
|
|
|
*
|
|
|
|
* @return ManyManyList|ManyManyThroughQueryManipulator
|
|
|
|
*/
|
|
|
|
protected function getManyManyInspector($list)
|
|
|
|
{
|
|
|
|
$inspector = $list;
|
|
|
|
if ($list instanceof ManyManyThroughList) {
|
|
|
|
foreach ($list->dataQuery()->getDataQueryManipulators() as $manipulator) {
|
|
|
|
if ($manipulator instanceof ManyManyThroughQueryManipulator) {
|
|
|
|
$inspector = $manipulator;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $inspector;
|
|
|
|
}
|
2018-09-28 17:38:38 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to get sort orders from a many many through list relationship record, rather than the current
|
|
|
|
* record itself.
|
|
|
|
*
|
|
|
|
* @param ManyManyList|ManyManyThroughList $list
|
|
|
|
* @return int[] Sort orders for the
|
|
|
|
*/
|
|
|
|
protected function getSortValuesFromManyManyThroughList($list, $sortField)
|
|
|
|
{
|
|
|
|
$manipulator = $this->getManyManyInspector($list);
|
|
|
|
|
|
|
|
// Find the foreign key name, ID and class to look up
|
|
|
|
$joinClass = $manipulator->getJoinClass();
|
|
|
|
$fromRelationName = $manipulator->getForeignKey();
|
|
|
|
$toRelationName = $manipulator->getLocalKey();
|
|
|
|
|
|
|
|
// Create a list of the MMTL relations
|
|
|
|
$sortlist = DataList::create($joinClass)->filter([
|
|
|
|
$toRelationName => $list->column('ID'),
|
|
|
|
// first() is safe as there are earlier checks to ensure our list to sort is valid
|
|
|
|
$fromRelationName => $list->first()->getJoin()->$fromRelationName,
|
|
|
|
]);
|
|
|
|
|
|
|
|
return $sortlist->map($toRelationName, $sortField)->toArray();
|
|
|
|
}
|
2015-09-21 12:49:02 +02:00
|
|
|
}
|