Merge pull request #7206 from open-sausages/pulls/4.0/select-them-all-again

Enhancement add support for TreeMultiselectField in react
This commit is contained in:
Damian Mooyman 2017-07-27 13:41:18 +12:00 committed by GitHub
commit 697798b464
3 changed files with 258 additions and 103 deletions

View File

@ -94,12 +94,19 @@ class TreeDropdownField extends FormField
protected $keyField = null;
/**
* Name of lavel field on underlying object
* Name of label field on underlying object
*
* @var string
*/
protected $labelField = null;
/**
* Similar to labelField but for non-html equivalent of field
*
* @var string
*/
protected $titleField = 'Title';
/**
* Callback for filtering records
*
@ -159,7 +166,7 @@ class TreeDropdownField extends FormField
* @var array
*/
protected $searchIds = [];
/**
* List of ids which matches the search result
* This excludes parents of search result children
@ -210,15 +217,15 @@ class TreeDropdownField extends FormField
if (!DataObject::has_extension($sourceObject, Hierarchy::class)) {
throw new InvalidArgumentException("SourceObject must have Hierarchy extension");
}
$this->sourceObject = $sourceObject;
$this->keyField = $keyField;
$this->labelField = $labelField;
$this->showSearch = $showSearch;
$this->setSourceObject($sourceObject);
$this->setKeyField($keyField);
$this->setLabelField($labelField);
$this->setShowSearch($showSearch);
// Extra settings for Folders
if (strcasecmp($sourceObject, Folder::class) === 0) {
$this->childrenMethod = 'ChildFolders';
$this->numChildrenMethod = 'numChildFolders';
$this->setChildrenMethod('ChildFolders');
$this->setNumChildrenMethod('numChildFolders');
}
$this->addExtraClass('single');
@ -226,6 +233,17 @@ class TreeDropdownField extends FormField
parent::__construct($name, $title);
}
/**
* Set the ID of the root node of the tree. This defaults to 0 - i.e.
* displays the whole tree.
*
* @return int
*/
public function getTreeBaseID()
{
return $this->baseID;
}
/**
* Set the ID of the root node of the tree. This defaults to 0 - i.e.
* displays the whole tree.
@ -239,11 +257,22 @@ class TreeDropdownField extends FormField
return $this;
}
/**
* Get a callback used to filter the values of the tree before
* displaying to the user.
*
* @return callable
*/
public function getFilterFunction()
{
return $this->filterCallback;
}
/**
* Set a callback used to filter the values of the tree before
* displaying to the user.
*
* @param callback $callback
* @param callable $callback
* @return $this
*/
public function setFilterFunction($callback)
@ -256,10 +285,20 @@ class TreeDropdownField extends FormField
return $this;
}
/**
* Get the callback used to disable checkboxes for some items in the tree
*
* @return callable
*/
public function getDisableFunction()
{
return $this->disableCallback;
}
/**
* Set a callback used to disable checkboxes for some items in the tree
*
* @param callback $callback
* @param callable $callback
* @return $this
*/
public function setDisableFunction($callback)
@ -276,7 +315,18 @@ class TreeDropdownField extends FormField
* Set a callback used to search the hierarchy globally, even before
* applying the filter.
*
* @param callback $callback
* @return callable
*/
public function getSearchFunction()
{
return $this->searchCallback;
}
/**
* Set a callback used to search the hierarchy globally, even before
* applying the filter.
*
* @param callable $callback
* @return $this
*/
public function setSearchFunction($callback)
@ -309,6 +359,16 @@ class TreeDropdownField extends FormField
return $this;
}
/**
* Get method to invoke on each node to get the child collection
*
* @return string
*/
public function getChildrenMethod()
{
return $this->childrenMethod;
}
/**
* @param string $method The parameter to ChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
* {@link Hierarchy}. The method specified determines the structure of the returned list. Use "ChildFolders"
@ -324,6 +384,16 @@ class TreeDropdownField extends FormField
return $this;
}
/**
* Get method to invoke on nodes to count children
*
* @return string
*/
public function getNumChildrenMethod()
{
return $this->numChildrenMethod;
}
/**
* @param string $method The parameter to numChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
* {@link Hierarchy}. Should be used in conjunction with setChildrenMethod().
@ -344,9 +414,9 @@ class TreeDropdownField extends FormField
{
$record = $this->Value() ? $this->objectForKey($this->Value()) : null;
if ($record instanceof ViewableData) {
$title = $record->obj($this->labelField)->forTemplate();
$title = $record->obj($this->getLabelField())->forTemplate();
} elseif ($record) {
$title = Convert::raw2xml($record->{$this->labelField});
$title = Convert::raw2xml($record->{$this->getLabelField()});
} else {
$title = $this->getEmptyString();
}
@ -354,7 +424,7 @@ class TreeDropdownField extends FormField
// TODO Implement for TreeMultiSelectField
$metadata = array(
'id' => $record ? $record->ID : null,
'ClassName' => $record ? $record->ClassName : $this->sourceObject
'ClassName' => $record ? $record->ClassName : $this->getSourceObject()
);
$properties = array_merge(
@ -371,7 +441,7 @@ class TreeDropdownField extends FormField
public function extraClass()
{
return implode(' ', array(parent::extraClass(), ($this->showSearch ? "searchable" : null)));
return implode(' ', array(parent::extraClass(), ($this->getShowSearch() ? "searchable" : null)));
}
/**
@ -387,11 +457,10 @@ class TreeDropdownField extends FormField
$isSubTree = false;
$this->search = $request->requestVar('search');
$flatlist = $request->requestVar('flatList');
$id = (is_numeric($request->latestParam('ID')))
? (int)$request->latestParam('ID')
: (int)$request->requestVar('ID');
// pre-process the tree - search needs to operate globally, not locally as marking filter does
if ($this->search) {
$this->populateIDs();
@ -399,29 +468,30 @@ class TreeDropdownField extends FormField
/** @var DataObject|Hierarchy $obj */
$obj = null;
$sourceObject = $this->getSourceObject();
if ($id && !$request->requestVar('forceFullTree')) {
$obj = DataObject::get_by_id($this->sourceObject, $id);
$obj = DataObject::get_by_id($sourceObject, $id);
$isSubTree = true;
if (!$obj) {
throw new Exception(
"TreeDropdownField->tree(): the object #$id of type $this->sourceObject could not be found"
"TreeDropdownField->tree(): the object #$id of type $sourceObject could not be found"
);
}
} else {
if ($this->baseID) {
$obj = DataObject::get_by_id($this->sourceObject, $this->baseID);
if ($this->getTreeBaseID()) {
$obj = DataObject::get_by_id($sourceObject, $this->getTreeBaseID());
}
if (!$this->baseID || !$obj) {
$obj = DataObject::singleton($this->sourceObject);
if (!$this->getTreeBaseID() || !$obj) {
$obj = DataObject::singleton($sourceObject);
}
}
// Create marking set
$markingSet = MarkedSet::create($obj, $this->childrenMethod, $this->numChildrenMethod, 30);
$markingSet = MarkedSet::create($obj, $this->getChildrenMethod(), $this->getNumChildrenMethod(), 30);
// Set filter on searched nodes
if ($this->filterCallback || $this->search) {
if ($this->getFilterFunction() || $this->search) {
// Rely on filtering to limit tree
$markingSet->setMarkingFilterFunction(function ($node) {
return $this->filterMarking($node);
@ -452,9 +522,9 @@ class TreeDropdownField extends FormField
$customised = function (DataObject $child) use ($isSubTree) {
return [
'name' => $this->getName(),
'id' => $child->obj($this->keyField),
'title' => $child->getTitle(),
'treetitle' => $child->obj($this->labelField),
'id' => $child->obj($this->getKeyField()),
'title' => $child->obj($this->getTitleField()),
'treetitle' => $child->obj($this->getLabelField()),
'disabled' => $this->nodeIsDisabled($child),
'isSubTree' => $isSubTree
];
@ -465,8 +535,8 @@ class TreeDropdownField extends FormField
// Format JSON output
$json = $markingSet
->getChildrenAsArray($customised);
if ($flatlist) {
if ($request->requestVar('flatList')) {
// format and filter $json here
$json['children'] = $this->flattenChildrenArray($json['children']);
}
@ -495,7 +565,8 @@ class TreeDropdownField extends FormField
*/
public function filterMarking($node)
{
if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) {
$callback = $this->getFilterFunction();
if ($callback && !call_user_func($callback, $node)) {
return false;
}
@ -513,7 +584,8 @@ class TreeDropdownField extends FormField
*/
public function nodeIsDisabled($node)
{
return ($this->disableCallback && call_user_func($this->disableCallback, $node));
$callback = $this->getDisableFunction();
return $callback && call_user_func($callback, $node);
}
/**
@ -527,13 +599,35 @@ class TreeDropdownField extends FormField
}
/**
* @return String
* @return string
*/
public function getLabelField()
{
return $this->labelField;
}
/**
* Field to use for item titles
*
* @return string
*/
public function getTitleField()
{
return $this->titleField;
}
/**
* Set field to use for item title
*
* @param string $field
* @return $this
*/
public function setTitleField($field)
{
$this->titleField = $field;
return $this;
}
/**
* @param string $field
* @return $this
@ -563,13 +657,15 @@ class TreeDropdownField extends FormField
}
/**
* @return String
* Get class of source object
*
* @return string
*/
public function getSourceObject()
{
return $this->sourceObject;
}
/**
* Flattens a given list of children array items, so the data is no longer
* structured in a hierarchy
@ -583,24 +679,24 @@ class TreeDropdownField extends FormField
protected function flattenChildrenArray($children, $parentTitles = [])
{
$output = [];
foreach ($children as $child) {
$childTitles = array_merge($parentTitles, [$child['title']]);
$grandChildren = $child['children'];
$contextString = implode('/', $parentTitles);
$child['contextString'] = ($contextString !== '') ? $contextString .'/' : '';
$child['children'] = [];
unset($child['children']);
if (!$this->search || in_array($child['id'], $this->realSearchIds)) {
$output[] = $child;
}
$output = array_merge($output, $this->flattenChildrenArray($grandChildren, $childTitles));
}
return $output;
}
/**
* Populate $this->searchIds with the IDs of the pages matching the searched parameter and their parents.
* Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter, but modified
@ -610,11 +706,11 @@ class TreeDropdownField extends FormField
{
// get all the leaves to be displayed
$res = $this->getSearchResults();
if (!$res) {
return;
}
// iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
foreach ($res as $row) {
if ($row->ParentID) {
@ -624,7 +720,7 @@ class TreeDropdownField extends FormField
}
$this->realSearchIds = $res->column();
$sourceObject = $this->sourceObject;
$sourceObject = $this->getSourceObject();
while (!empty($parents)) {
$items = DataObject::get($sourceObject)
@ -640,7 +736,7 @@ class TreeDropdownField extends FormField
}
}
}
/**
* Get the DataObjects that matches the searched parameter.
*
@ -648,14 +744,15 @@ class TreeDropdownField extends FormField
*/
protected function getSearchResults()
{
if ($this->searchCallback) {
return call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
$callback = $this->getSearchFunction();
if ($callback) {
return call_user_func($callback, $this->getSourceObject(), $this->getLabelField(), $this->search);
}
$sourceObject = $this->sourceObject;
$sourceObject = $this->getSourceObject();
$filters = array();
if (singleton($sourceObject)->hasDatabaseField($this->labelField)) {
$filters["{$this->labelField}:PartialMatch"] = $this->search;
if (singleton($sourceObject)->hasDatabaseField($this->getLabelField())) {
$filters["{$this->getLabelField()}:PartialMatch"] = $this->search;
} else {
if (singleton($sourceObject)->hasDatabaseField('Title')) {
$filters["Title:PartialMatch"] = $this->search;
@ -669,10 +766,10 @@ class TreeDropdownField extends FormField
throw new InvalidArgumentException(sprintf(
'Cannot query by %s.%s, not a valid database column',
$sourceObject,
$this->labelField
$this->getLabelField()
));
}
return DataObject::get($this->sourceObject)->filterAny($filters);
return DataObject::get($this->getSourceObject())->filterAny($filters);
}
/**
@ -683,8 +780,8 @@ class TreeDropdownField extends FormField
*/
protected function objectForKey($key)
{
return DataObject::get($this->sourceObject)
->filter($this->keyField, $key)
return DataObject::get($this->getSourceObject())
->filter($this->getKeyField(), $key)
->first();
}
@ -695,9 +792,10 @@ class TreeDropdownField extends FormField
{
/** @var TreeDropdownField_Readonly $copy */
$copy = $this->castedCopy(TreeDropdownField_Readonly::class);
$copy->setKeyField($this->keyField);
$copy->setLabelField($this->labelField);
$copy->setSourceObject($this->sourceObject);
$copy->setKeyField($this->getKeyField());
$copy->setLabelField($this->getLabelField());
$this->setTitleField($this->getTitleField());
$copy->setSourceObject($this->getSourceObject());
return $copy;
}
@ -710,7 +808,7 @@ class TreeDropdownField extends FormField
$field = $classOrCopy;
if (!is_object($field)) {
$field = new $classOrCopy($this->name, $this->title, $this->sourceObject);
$field = new $classOrCopy($this->name, $this->title, $this->getSourceObject());
}
return parent::castedCopy($field);
@ -719,17 +817,15 @@ class TreeDropdownField extends FormField
public function getSchemaStateDefaults()
{
$data = parent::getSchemaStateDefaults();
// Check label for field
$record = $this->Value() ? $this->objectForKey($this->Value()) : null;
$selectedlabel = null;
// Ensure cache is keyed by last modified date of the underlying list
$data['data']['cacheKey'] = DataList::create($this->sourceObject)->max('LastEdited');
$data['data']['cacheKey'] = DataList::create($this->getSourceObject())->max('LastEdited');
if ($record) {
$data['data']['valueObject'] = [
'id' => $record->getField($this->keyField),
'title' => $record->getTitle(),
'treetitle' => $record->obj($this->labelField)->getSchemaValue(),
'id' => $record->obj($this->getKeyField())->getValue(),
'title' => $record->obj($this->getTitleField())->getValue(),
'treetitle' => $record->obj($this->getLabelField())->getSchemaValue(),
];
}
@ -741,9 +837,10 @@ class TreeDropdownField extends FormField
$data = parent::getSchemaDataDefaults();
$data['data'] = array_merge($data['data'], [
'urlTree' => $this->Link('tree'),
'showSearch' => $this->showSearch,
'showSearch' => $this->getShowSearch(),
'emptyString' => $this->getEmptyString(),
'hasEmptyDefault' => $this->getHasEmptyDefault(),
'multiple' => false,
]);
return $data;
@ -791,7 +888,7 @@ class TreeDropdownField extends FormField
return $this->emptyString;
}
$item = DataObject::singleton($this->sourceObject);
$item = DataObject::singleton($this->getSourceObject());
$emptyString = _t(
'SilverStripe\\Forms\\DropdownField.CHOOSE_MODEL',
'(Choose {name})',

View File

@ -8,7 +8,7 @@ class TreeDropdownField_Readonly extends TreeDropdownField
public function Field($properties = array())
{
$fieldName = $this->labelField;
$fieldName = $this->getLabelField();
if ($this->value) {
$keyObj = $this->objectForKey($this->value);
$obj = $keyObj ? $keyObj->$fieldName : '';
@ -23,7 +23,6 @@ class TreeDropdownField_Readonly extends TreeDropdownField
$field = new LookupField($this->name, $this->title, $source);
$field->setValue($this->value);
$field->setForm($this->form);
$field->dontEscape = true;
return $field->Field();
}
}

View File

@ -5,9 +5,11 @@ namespace SilverStripe\Forms;
use SilverStripe\Core\Convert;
use SilverStripe\Control\Controller;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\Group;
use SilverStripe\View\ViewableData;
use stdClass;
@ -54,52 +56,106 @@ use stdClass;
*/
class TreeMultiselectField extends TreeDropdownField
{
public function __construct($name, $title = null, $sourceObject = "SilverStripe\\Security\\Group", $keyField = "ID", $labelField = "Title")
{
public function __construct(
$name,
$title = null,
$sourceObject = Group::class,
$keyField = "ID",
$labelField = "Title"
) {
parent::__construct($name, $title, $sourceObject, $keyField, $labelField);
$this->removeExtraClass('single');
$this->addExtraClass('multiple');
$this->value = 'unchanged';
}
public function getSchemaDataDefaults()
{
$data = parent::getSchemaDataDefaults();
$data['data'] = array_merge($data['data'], [
'hasEmptyDefault' => false,
'multiple' => true,
]);
return $data;
}
public function getSchemaStateDefaults()
{
$data = parent::getSchemaStateDefaults();
unset($data['data']['valueObject']);
$items = $this->getItems();
$values = [];
foreach ($items as $item) {
if ($item instanceof DataObject) {
$values[] = [
'id' => $item->obj($this->getKeyField())->getValue(),
'title' => $item->obj($this->getTitleField())->getValue(),
'parentid' => $item->ParentID,
'treetitle' => $item->obj($this->getLabelField())->getSchemaValue(),
];
} else {
$values[] = $item;
}
}
$data['data']['valueObjects'] = $values;
// cannot rely on $this->value as this could be a many-many relationship
$value = array_column($values, 'id');
$data['value'] = ($value) ? $value : 'unchanged';
return $data;
}
/**
* Return this field's linked items
* @return ArrayList|DataList $items
*/
public function getItems()
{
// If the value has been set, use that
if ($this->value != 'unchanged' && is_array($this->sourceObject)) {
$items = array();
$values = is_array($this->value) ? $this->value : preg_split('/ *, */', trim($this->value));
foreach ($values as $value) {
$item = new stdClass;
$item->ID = $value;
$item->Title = $this->sourceObject[$value];
$items[] = $item;
}
return $items;
$items = new ArrayList();
// Otherwise, look data up from the linked relation
} if ($this->value != 'unchanged' && is_string($this->value)) {
$items = new ArrayList();
$ids = explode(',', $this->value);
foreach ($ids as $id) {
if (!is_numeric($id)) {
continue;
}
$item = DataObject::get_by_id($this->sourceObject, $id);
if ($item) {
// If the value has been set, use that
if ($this->value != 'unchanged') {
$sourceObject = $this->getSourceObject();
if (is_array($sourceObject)) {
$values = is_array($this->value) ? $this->value : preg_split('/ *, */', trim($this->value));
foreach ($values as $value) {
$item = new stdClass;
$item->ID = $value;
$item->Title = $sourceObject[$value];
$items->push($item);
}
return $items;
}
return $items;
} elseif ($this->form) {
// Otherwise, look data up from the linked relation
if (is_string($this->value)) {
$ids = explode(',', $this->value);
foreach ($ids as $id) {
if (!is_numeric($id)) {
continue;
}
$item = DataObject::get_by_id($sourceObject, $id);
if ($item) {
$items->push($item);
}
}
return $items;
}
}
if ($this->form) {
$fieldName = $this->name;
$record = $this->form->getRecord();
if (is_object($record) && $record->hasMethod($fieldName)) {
return $record->$fieldName();
}
}
return $items;
}
/**
@ -121,8 +177,8 @@ class TreeMultiselectField extends TreeDropdownField
foreach ($items as $item) {
$idArray[] = $item->ID;
$titleArray[] = ($item instanceof ViewableData)
? $item->obj($this->labelField)->forTemplate()
: Convert::raw2xml($item->{$this->labelField});
? $item->obj($this->getLabelField())->forTemplate()
: Convert::raw2xml($item->{$this->getLabelField()});
}
$title = implode(", ", $titleArray);
@ -161,7 +217,7 @@ class TreeMultiselectField extends TreeDropdownField
{
// Detect whether this field has actually been updated
if ($this->value !== 'unchanged') {
$items = array();
$items = [];
$fieldName = $this->name;
$saveDest = $record->$fieldName();
@ -174,7 +230,9 @@ class TreeMultiselectField extends TreeDropdownField
);
}
if ($this->value) {
if (is_array($this->value)) {
$items = $this->value;
} elseif ($this->value) {
$items = preg_split("/ *, */", trim($this->value));
}
@ -196,11 +254,12 @@ class TreeMultiselectField extends TreeDropdownField
*/
public function performReadonlyTransformation()
{
$copy = $this->castedCopy('SilverStripe\\Forms\\TreeMultiselectField_Readonly');
$copy->setKeyField($this->keyField);
$copy->setLabelField($this->labelField);
$copy->setSourceObject($this->sourceObject);
/** @var TreeMultiselectField_Readonly $copy */
$copy = $this->castedCopy(TreeMultiselectField_Readonly::class);
$copy->setKeyField($this->getKeyField());
$copy->setLabelField($this->getLabelField());
$copy->setSourceObject($this->getSourceObject());
$copy->setTitleField($this->getTitleField());
return $copy;
}
}