mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
API added flatList argument for generating the json tree list with a context string property
This commit is contained in:
parent
2b266276c2
commit
ccda816f90
@ -154,11 +154,20 @@ class TreeDropdownField extends FormField
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List of ids in current search result (keys are ids, values are true)
|
* List of ids in current search result (keys are ids, values are true)
|
||||||
|
* This includes parents of search result children which may not be an actual result
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $searchIds = [];
|
protected $searchIds = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of ids which matches the search result
|
||||||
|
* This excludes parents of search result children
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $realSearchIds = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if search should be shown
|
* Determine if search should be shown
|
||||||
*
|
*
|
||||||
@ -378,10 +387,16 @@ class TreeDropdownField extends FormField
|
|||||||
$isSubTree = false;
|
$isSubTree = false;
|
||||||
|
|
||||||
$this->search = $request->requestVar('search');
|
$this->search = $request->requestVar('search');
|
||||||
|
$flatlist = $request->requestVar('flatList');
|
||||||
$id = (is_numeric($request->latestParam('ID')))
|
$id = (is_numeric($request->latestParam('ID')))
|
||||||
? (int)$request->latestParam('ID')
|
? (int)$request->latestParam('ID')
|
||||||
: (int)$request->requestVar('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();
|
||||||
|
}
|
||||||
|
|
||||||
/** @var DataObject|Hierarchy $obj */
|
/** @var DataObject|Hierarchy $obj */
|
||||||
$obj = null;
|
$obj = null;
|
||||||
if ($id && !$request->requestVar('forceFullTree')) {
|
if ($id && !$request->requestVar('forceFullTree')) {
|
||||||
@ -402,11 +417,6 @@ class TreeDropdownField extends FormField
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pre-process the tree - search needs to operate globally, not locally as marking filter does
|
|
||||||
if ($this->search) {
|
|
||||||
$this->populateIDs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create marking set
|
// Create marking set
|
||||||
$markingSet = MarkedSet::create($obj, $this->childrenMethod, $this->numChildrenMethod, 30);
|
$markingSet = MarkedSet::create($obj, $this->childrenMethod, $this->numChildrenMethod, 30);
|
||||||
|
|
||||||
@ -455,6 +465,11 @@ class TreeDropdownField extends FormField
|
|||||||
// Format JSON output
|
// Format JSON output
|
||||||
$json = $markingSet
|
$json = $markingSet
|
||||||
->getChildrenAsArray($customised);
|
->getChildrenAsArray($customised);
|
||||||
|
|
||||||
|
if ($flatlist) {
|
||||||
|
// format and filter $json here
|
||||||
|
$json['children'] = $this->flattenChildrenArray($json['children']);
|
||||||
|
}
|
||||||
return HTTPResponse::create()
|
return HTTPResponse::create()
|
||||||
->addHeader('Content-Type', 'application/json')
|
->addHeader('Content-Type', 'application/json')
|
||||||
->setBody(json_encode($json));
|
->setBody(json_encode($json));
|
||||||
@ -555,6 +570,37 @@ class TreeDropdownField extends FormField
|
|||||||
return $this->sourceObject;
|
return $this->sourceObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens a given list of children array items, so the data is no longer
|
||||||
|
* structured in a hierarchy
|
||||||
|
*
|
||||||
|
* NOTE: uses {@link TreeDropdownField::$realSearchIds} to filter items by if there is a search
|
||||||
|
*
|
||||||
|
* @param array $children - the list of children, which could contain their own children
|
||||||
|
* @param array $parentTitles - a list of parent titles, which we use to construct the contextString
|
||||||
|
* @return array - flattened list of children
|
||||||
|
*/
|
||||||
|
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'] = [];
|
||||||
|
|
||||||
|
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.
|
* 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
|
* Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter, but modified
|
||||||
@ -563,9 +609,49 @@ class TreeDropdownField extends FormField
|
|||||||
protected function populateIDs()
|
protected function populateIDs()
|
||||||
{
|
{
|
||||||
// get all the leaves to be displayed
|
// 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) {
|
||||||
|
$parents[$row->ParentID] = true;
|
||||||
|
}
|
||||||
|
$this->searchIds[$row->ID] = true;
|
||||||
|
}
|
||||||
|
$this->realSearchIds = $res->column();
|
||||||
|
|
||||||
|
$sourceObject = $this->sourceObject;
|
||||||
|
|
||||||
|
while (!empty($parents)) {
|
||||||
|
$items = DataObject::get($sourceObject)
|
||||||
|
->filter("ID", array_keys($parents));
|
||||||
|
$parents = array();
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item->ParentID) {
|
||||||
|
$parents[$item->ParentID] = true;
|
||||||
|
}
|
||||||
|
$this->searchIds[$item->ID] = true;
|
||||||
|
$this->searchExpanded[$item->ID] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the DataObjects that matches the searched parameter.
|
||||||
|
*
|
||||||
|
* @return DataList
|
||||||
|
*/
|
||||||
|
protected function getSearchResults()
|
||||||
|
{
|
||||||
if ($this->searchCallback) {
|
if ($this->searchCallback) {
|
||||||
$res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
|
return call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
$sourceObject = $this->sourceObject;
|
$sourceObject = $this->sourceObject;
|
||||||
$filters = array();
|
$filters = array();
|
||||||
if (singleton($sourceObject)->hasDatabaseField($this->labelField)) {
|
if (singleton($sourceObject)->hasDatabaseField($this->labelField)) {
|
||||||
@ -586,34 +672,7 @@ class TreeDropdownField extends FormField
|
|||||||
$this->labelField
|
$this->labelField
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
$res = DataObject::get($this->sourceObject)->filterAny($filters);
|
return DataObject::get($this->sourceObject)->filterAny($filters);
|
||||||
}
|
|
||||||
|
|
||||||
if ($res) {
|
|
||||||
// iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
|
|
||||||
foreach ($res as $row) {
|
|
||||||
if ($row->ParentID) {
|
|
||||||
$parents[$row->ParentID] = true;
|
|
||||||
}
|
|
||||||
$this->searchIds[$row->ID] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceObject = $this->sourceObject;
|
|
||||||
|
|
||||||
while (!empty($parents)) {
|
|
||||||
$items = DataObject::get($sourceObject)
|
|
||||||
->filter("ID", array_keys($parents));
|
|
||||||
$parents = array();
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
if ($item->ParentID) {
|
|
||||||
$parents[$item->ParentID] = true;
|
|
||||||
}
|
|
||||||
$this->searchIds[$item->ID] = true;
|
|
||||||
$this->searchExpanded[$item->ID] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -680,9 +739,12 @@ class TreeDropdownField extends FormField
|
|||||||
public function getSchemaDataDefaults()
|
public function getSchemaDataDefaults()
|
||||||
{
|
{
|
||||||
$data = parent::getSchemaDataDefaults();
|
$data = parent::getSchemaDataDefaults();
|
||||||
$data['data']['urlTree'] = $this->Link('tree');
|
$data['data'] = array_merge($data['data'], [
|
||||||
$data['data']['emptyString'] = $this->getEmptyString();
|
'urlTree' => $this->Link('tree'),
|
||||||
$data['data']['hasEmptyDefault'] = $this->getHasEmptyDefault();
|
'showSearch' => $this->showSearch,
|
||||||
|
'emptyString' => $this->getEmptyString(),
|
||||||
|
'hasEmptyDefault' => $this->getHasEmptyDefault(),
|
||||||
|
]);
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,65 @@ class TreeDropdownFieldTest extends SapphireTest
|
|||||||
|
|
||||||
protected static $fixture_file = 'TreeDropdownFieldTest.yml';
|
protected static $fixture_file = 'TreeDropdownFieldTest.yml';
|
||||||
|
|
||||||
|
public function testTreeSearchJson()
|
||||||
|
{
|
||||||
|
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
|
||||||
|
|
||||||
|
// case insensitive search against keyword 'sub' for folders
|
||||||
|
$request = new HTTPRequest('GET', 'url', array('search'=>'sub', 'format' => 'json'));
|
||||||
|
$request->setSession(new Session([]));
|
||||||
|
$response = $field->tree($request);
|
||||||
|
$tree = json_decode($response->getBody(), true);
|
||||||
|
|
||||||
|
$folder1 = $this->objFromFixture(Folder::class, 'folder1');
|
||||||
|
$folder1Subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1');
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
$folder1->Name,
|
||||||
|
array_column($tree['children'], 'title'),
|
||||||
|
$folder1->Name.' is found in the json'
|
||||||
|
);
|
||||||
|
|
||||||
|
$filtered = array_filter($tree['children'], function ($entry) use ($folder1) {
|
||||||
|
return $folder1->Name === $entry['title'];
|
||||||
|
});
|
||||||
|
$folder1Tree = array_pop($filtered);
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
$folder1Subfolder1->Name,
|
||||||
|
array_column($folder1Tree['children'], 'title'),
|
||||||
|
$folder1Subfolder1->Name.' is found in the folder1 entry in the json'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTreeSearchJsonFlatlist()
|
||||||
|
{
|
||||||
|
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
|
||||||
|
|
||||||
|
// case insensitive search against keyword 'sub' for folders
|
||||||
|
$request = new HTTPRequest('GET', 'url', array('search'=>'sub', 'format' => 'json', 'flatList' => '1'));
|
||||||
|
$request->setSession(new Session([]));
|
||||||
|
$response = $field->tree($request);
|
||||||
|
$tree = json_decode($response->getBody(), true);
|
||||||
|
|
||||||
|
$folder1 = $this->objFromFixture(Folder::class, 'folder1');
|
||||||
|
$folder1Subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1');
|
||||||
|
|
||||||
|
$this->assertNotContains(
|
||||||
|
$folder1->Name,
|
||||||
|
array_column($tree['children'], 'title'),
|
||||||
|
$folder1->Name.' is not found in the json'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
$folder1Subfolder1->Name,
|
||||||
|
array_column($tree['children'], 'title'),
|
||||||
|
$folder1Subfolder1->Name.' is found in the json'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testTreeSearch()
|
public function testTreeSearch()
|
||||||
{
|
{
|
||||||
|
|
||||||
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
|
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
|
||||||
|
|
||||||
// case insensitive search against keyword 'sub' for folders
|
// case insensitive search against keyword 'sub' for folders
|
||||||
@ -30,7 +86,8 @@ class TreeDropdownFieldTest extends SapphireTest
|
|||||||
$folder1Subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1');
|
$folder1Subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1');
|
||||||
|
|
||||||
$parser = new CSSContentParser($tree);
|
$parser = new CSSContentParser($tree);
|
||||||
$cssPath = 'ul.tree li#selector-TestTree-'.$folder1->ID.' li#selector-TestTree-'.$folder1Subfolder1->ID.' a span.item';
|
$cssPath = 'ul.tree li#selector-TestTree-'.$folder1->ID.' li#selector-TestTree-'.
|
||||||
|
$folder1Subfolder1->ID.' a span.item';
|
||||||
$firstResult = $parser->getBySelector($cssPath);
|
$firstResult = $parser->getBySelector($cssPath);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$folder1Subfolder1->Name,
|
$folder1Subfolder1->Name,
|
||||||
@ -67,7 +124,8 @@ class TreeDropdownFieldTest extends SapphireTest
|
|||||||
$parser = new CSSContentParser($tree);
|
$parser = new CSSContentParser($tree);
|
||||||
|
|
||||||
// Even if we used File as the source object, folders are still returned because Folder is a File
|
// Even if we used File as the source object, folders are still returned because Folder is a File
|
||||||
$cssPath = 'ul.tree li#selector-TestTree-'.$folder1->ID.' li#selector-TestTree-'.$folder1Subfolder1->ID.' a span.item';
|
$cssPath = 'ul.tree li#selector-TestTree-'.$folder1->ID.' li#selector-TestTree-'.
|
||||||
|
$folder1Subfolder1->ID.' a span.item';
|
||||||
$firstResult = $parser->getBySelector($cssPath);
|
$firstResult = $parser->getBySelector($cssPath);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$folder1Subfolder1->Name,
|
$folder1Subfolder1->Name,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user