diff --git a/docs/en/reference/modeladmin.md b/docs/en/reference/modeladmin.md index 7a01fe4f5..c708ab6f5 100644 --- a/docs/en/reference/modeladmin.md +++ b/docs/en/reference/modeladmin.md @@ -154,6 +154,8 @@ more specifically the `[api:GridFieldAddExistingAutocompleter]` and `[api:GridFi They provide a list/detail interface within a single record edited in your ModelAdmin. The `[GridField](/reference/grid-field)` docs also explain how to manage extra relation fields on join tables through its detail forms. +The autocompleter can also search attributes on relations, +based on the search fields defined through `[api:DataObject::searchableFields()]`. ## Permissions diff --git a/forms/gridfield/GridFieldAddExistingAutocompleter.php b/forms/gridfield/GridFieldAddExistingAutocompleter.php index 9956febf8..5db26cfa9 100644 --- a/forms/gridfield/GridFieldAddExistingAutocompleter.php +++ b/forms/gridfield/GridFieldAddExistingAutocompleter.php @@ -28,10 +28,25 @@ class GridFieldAddExistingAutocompleter protected $searchList; /** - * Which columns that should be used for doing a "StartsWith" search. + * Define column names which should be included in the search. + * By default, they're searched with a {@link StartsWithFilter}. + * To define custom filters, use the same notation as {@link DataList->filter()}, + * e.g. "Name:EndsWith". + * * If multiple fields are provided, the filtering is performed non-exclusive. - * If no fields are provided, tries to auto-detect a "Title" or "Name" field, - * and falls back to the first textual field defined on the object. + * If no fields are provided, tries to auto-detect fields from + * {@link DataObject->searchableFields()}. + * + * The fields support "dot-notation" for relationships, e.g. + * a entry called "Team.Name" will search through the names of + * a "Team" relationship. + * + * @example + * array( + * 'Name', + * 'Email:StartsWith', + * 'Team.Name' + * ) * * @var Array */ @@ -192,15 +207,16 @@ class GridFieldAddExistingAutocompleter $dataClass)); } - // TODO Replace with DataList->filterAny() once it correctly supports OR connectives - $stmts = array(); + $params = array(); foreach($searchFields as $searchField) { - $stmts[] .= sprintf('"%s" LIKE \'%s%%\'', $searchField, - Convert::raw2sql($request->getVar('gridfield_relationsearch'))); + $name = (strpos($searchField, ':') !== FALSE) ? $searchField : "$searchField:StartsWith"; + $params[$name] = $request->getVar('gridfield_relationsearch'); } - $results = $allList->where(implode(' OR ', $stmts))->subtract($gridField->getList()); - $results = $results->sort($searchFields[0], 'ASC'); - $results = $results->limit($this->getResultsLimit()); + $results = $allList + ->subtract($gridField->getList()) + ->filterAny($params) + ->sort(strtok($searchFields[0], ':'), 'ASC') + ->limit($this->getResultsLimit()); $json = array(); foreach($results as $result) { @@ -250,20 +266,44 @@ class GridFieldAddExistingAutocompleter } /** - * Detect searchable + * Detect searchable fields and searchable relations. + * Falls back to {@link DataObject->summaryFields()} if + * no custom search fields are defined. * - * @param String - * @return Array + * @param String the class name + * @return Array|null names of the searchable fields */ - protected function scaffoldSearchFields($dataClass) { + public function scaffoldSearchFields($dataClass) { $obj = singleton($dataClass); - if($obj->hasDatabaseField('Title')) { - return array('Title'); - } else if($obj->hasDatabaseField('Name')) { - return array('Name'); - } else { - return null; + $fields = null; + if($fieldSpecs = $obj->searchableFields()) { + $customSearchableFields = $obj->stat('searchable_fields'); + foreach($fieldSpecs as $name => $spec) { + if(is_array($spec) && array_key_exists('filter', $spec)) { + // The searchableFields() spec defaults to PartialMatch, + // so we need to check the original setting. + // If the field is defined $searchable_fields = array('MyField'), + // then default to StartsWith filter, which makes more sense in this context. + if(!$customSearchableFields || array_search($name, $customSearchableFields)) { + $filter = 'StartsWith'; + } else { + $filter = preg_replace('/Filter$/', '', $spec['filter']); + } + $fields[] = "{$name}:{$filter}"; + } else { + $fields[] = $name; + } + } } + if (is_null($fields)) { + if ($obj->hasDatabaseField('Title')) { + $fields = array('Title'); + } elseif ($obj->hasDatabaseField('Name')) { + $fields = array('Name'); + } + } + + return $fields; } /** diff --git a/tests/forms/GridFieldTest.php b/tests/forms/GridFieldTest.php index 4d0676afc..8b8028133 100644 --- a/tests/forms/GridFieldTest.php +++ b/tests/forms/GridFieldTest.php @@ -472,6 +472,14 @@ class GridFieldTest_Team extends DataObject implements TestOnly { ); static $many_many = array('Players' => 'GridFieldTest_Player'); + + static $has_many = array('Cheerleaders' => 'GridFieldTest_Cheerleader'); + + static $searchable_fields = array( + 'Name', + 'City', + 'Cheerleaders.Name' + ); } class GridFieldTest_Player extends DataObject implements TestOnly { @@ -483,6 +491,14 @@ class GridFieldTest_Player extends DataObject implements TestOnly { static $belongs_many_many = array('Teams' => 'GridFieldTest_Team'); } +class GridFieldTest_Cheerleader extends DataObject implements TestOnly { + static $db = array( + 'Name' => 'Varchar' + ); + + static $has_one = array('Team' => 'GridFieldTest_Team'); +} + class GridFieldTest_HTMLFragments implements GridField_HTMLProvider, TestOnly{ public function __construct($fragments) { $this->fragments = $fragments; diff --git a/tests/forms/gridfield/GridFieldAddExistingAutocompleterTest.php b/tests/forms/gridfield/GridFieldAddExistingAutocompleterTest.php index d98e3a49c..ae2d59ae8 100644 --- a/tests/forms/gridfield/GridFieldAddExistingAutocompleterTest.php +++ b/tests/forms/gridfield/GridFieldAddExistingAutocompleterTest.php @@ -3,9 +3,28 @@ class GridFieldAddExistingAutocompleterTest extends FunctionalTest { static $fixture_file = 'GridFieldTest.yml'; - protected $extraDataObjects = array('GridFieldTest_Team', 'GridFieldTest_Player'); + protected $extraDataObjects = array('GridFieldTest_Team', 'GridFieldTest_Player', 'GridFieldTest_Cheerleader'); - public function testSearch() { + function testScaffoldSearchFields() { + $autoCompleter = new GridFieldAddExistingAutocompleter($targetFragment = 'before', array('Test')); + $gridFieldTest_Team = singleton('GridFieldTest_Team'); + $this->assertEquals( + $autoCompleter->scaffoldSearchFields('GridFieldTest_Team'), + array( + 'Name:PartialMatch', + 'City:StartsWith', + 'Cheerleaders.Name:StartsWith' + ) + ); + $this->assertEquals( + $autoCompleter->scaffoldSearchFields('GridFieldTest_Cheerleader'), + array( + 'Name:StartsWith' + ) + ); + } + + function testSearch() { $team1 = $this->objFromFixture('GridFieldTest_Team', 'team1'); $team2 = $this->objFromFixture('GridFieldTest_Team', 'team2'); @@ -17,21 +36,26 @@ class GridFieldAddExistingAutocompleterTest extends FunctionalTest { $response = $this->post( 'GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/search' . '/?gridfield_relationsearch=Team 2', - array( - (string)$btns[0]['name'] => 1 - ) + array((string)$btns[0]['name'] => 1) ); $this->assertFalse($response->isError()); $result = Convert::json2array($response->getBody()); $this->assertEquals(1, count($result)); $this->assertEquals(array($team2->ID => 'Team 2'), $result); + + $response = $this->post( + 'GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/' + . 'search/?gridfield_relationsearch=Heather', + array((string)$btns[0]['name'] => 1) + ); + $this->assertFalse($response->isError()); + $result = Convert::json2array($response->getBody()); + $this->assertEquals(1, count($result), "The relational filter did not work"); $response = $this->post( 'GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/search' . '/?gridfield_relationsearch=Unknown', - array( - (string)$btns[0]['name'] => 1 - ) + array((string)$btns[0]['name'] => 1) ); $this->assertFalse($response->isError()); $result = Convert::json2array($response->getBody()); @@ -78,7 +102,7 @@ class GridFieldAddExistingAutocompleterTest_Controller extends Controller implem public function Form() { $player = DataObject::get('GridFieldTest_Player')->find('Email', 'player1@test.com'); $config = GridFieldConfig::create()->addComponents( - $relationComponent = new GridFieldAddExistingAutocompleter('before', 'Name'), + $relationComponent = new GridFieldAddExistingAutocompleter('before'), new GridFieldDataColumns() ); $field = new GridField('testfield', 'testfield', $player->Teams(), $config); diff --git a/tests/forms/gridfield/GridFieldTest.yml b/tests/forms/gridfield/GridFieldTest.yml index 101557aec..1b3a6c100 100644 --- a/tests/forms/gridfield/GridFieldTest.yml +++ b/tests/forms/gridfield/GridFieldTest.yml @@ -15,4 +15,8 @@ GridFieldTest_Player: player1_team1: Name: Player 1 Email: player1@test.com - Teams: =>GridFieldTest_Team.team1 \ No newline at end of file + Teams: =>GridFieldTest_Team.team1 +GridFieldTest_Cheerleader: + cheerleader1_team1: + Name: Heather + Team: =>GridFieldTest_Team.team4 \ No newline at end of file