Skip to content

Commit 9c2acb1

Browse files
authored
Merge pull request #11895 from creative-commoners/pulls/6/coupla-things
Add functionality needed by version-admin PR
2 parents fbdb434 + 17190ba commit 9c2acb1

File tree

8 files changed

+195
-32
lines changed

8 files changed

+195
-32
lines changed

src/Forms/GridField/GridFieldFilterHeader.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public function canFilterAnyColumns($gridField)
176176
&& ClassInfo::hasMethod($singleton, 'searchableFields')
177177
) {
178178
// note: searchableFields() will return summary_fields if there are no searchable_fields on the model
179-
$searchableFields = array_keys($singleton->searchableFields());
179+
$searchableFields = array_keys($this->getSearchableFields($gridField, $singleton));
180180
$summaryFields = array_keys($singleton->summaryFields());
181181
sort($searchableFields);
182182
sort($summaryFields);
@@ -192,7 +192,10 @@ public function canFilterAnyColumns($gridField)
192192
}
193193
} else {
194194
// Allows non-DataObject classes to be used with this component
195-
$columns = $gridField->getColumns();
195+
$columns = array_merge(
196+
$gridField->getColumns(),
197+
array_keys($this->getSearchableFields($gridField, $singleton))
198+
);
196199
foreach ($columns as $columnField) {
197200
$metadata = $gridField->getColumnMetadata($columnField);
198201
$title = $metadata['title'];
@@ -205,6 +208,19 @@ public function canFilterAnyColumns($gridField)
205208
return false;
206209
}
207210

211+
private function getSearchableFields(GridField $gridField, object $singleton): array
212+
{
213+
try {
214+
return $this->getSearchContext($gridField)->getSearchFieldsSpec($singleton);
215+
} catch (LogicException) {
216+
// Fall back to just searchable fields on the record itself.
217+
if (ClassInfo::hasMethod($singleton, 'searchableFields')) {
218+
return $singleton->searchableFields();
219+
}
220+
return [];
221+
}
222+
}
223+
208224
/**
209225
* Get the text to be used as a placeholder in the search field.
210226
* If blank, the placeholder will be generated based on the class held in the GridField.

src/ORM/DataObject.php

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use SilverStripe\Core\Validation\ValidationResult;
1717
use SilverStripe\Dev\Debug;
1818
use SilverStripe\Dev\Deprecation;
19-
use SilverStripe\Forms\FieldGroup;
2019
use SilverStripe\Forms\FieldList;
2120
use SilverStripe\Forms\FormField;
2221
use SilverStripe\Forms\FormScaffolder;
@@ -2496,20 +2495,7 @@ public function scaffoldSearchFields($_params = null)
24962495

24972496
// If we're using a WithinRangeFilter, split the field into two separate fields (from and to)
24982497
if (is_a($spec['filter'] ?? '', WithinRangeFilter::class, true)) {
2499-
$fieldFrom = $field;
2500-
$fieldTo = clone $field;
2501-
$originalTitle = $field->Title();
2502-
$originalName = $field->getName();
2503-
2504-
$fieldFrom->setName($originalName . '_SearchFrom');
2505-
$fieldFrom->setTitle(_t(__CLASS__ . '.FILTER_WITHINRANGE_FROM', 'From'));
2506-
$fieldTo->setName($originalName . '_SearchTo');
2507-
$fieldTo->setTitle(_t(__CLASS__ . '.FILTER_WITHINRANGE_TO', 'To'));
2508-
2509-
$field = FieldGroup::create(
2510-
$originalTitle,
2511-
[$fieldFrom, $fieldTo]
2512-
)->setName($originalName)->addExtraClass('fieldgroup--fill-width');
2498+
$field = WithinRangeFilter::convertToRangeField($field);
25132499
}
25142500

25152501
$fields->push($field);

src/ORM/Filters/WithinRangeFilter.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace SilverStripe\ORM\Filters;
44

5+
use SilverStripe\Forms\FieldGroup;
6+
use SilverStripe\Forms\FormField;
7+
use SilverStripe\ORM\DataObject;
58
use SilverStripe\ORM\DataQuery;
69

710
class WithinRangeFilter extends SearchFilter
@@ -52,4 +55,25 @@ protected function excludeOne(DataQuery $query)
5255
]
5356
]);
5457
}
58+
59+
/**
60+
* Take a single form field and turn it into seaprate "from" and "to" fields in a group.
61+
*/
62+
public static function convertToRangeField(FormField $originalField): FormField
63+
{
64+
$fieldFrom = $originalField;
65+
$fieldTo = clone $originalField;
66+
$originalTitle = $originalField->Title();
67+
$originalName = $originalField->getName();
68+
69+
$fieldFrom->setName($originalName . '_SearchFrom');
70+
$fieldFrom->setTitle(_t(DataObject::class . '.FILTER_WITHINRANGE_FROM', 'From'));
71+
$fieldTo->setName($originalName . '_SearchTo');
72+
$fieldTo->setTitle(_t(DataObject::class . '.FILTER_WITHINRANGE_TO', 'To'));
73+
74+
return FieldGroup::create(
75+
$originalTitle,
76+
[$fieldFrom, $fieldTo]
77+
)->setName($originalName)->addExtraClass('fieldgroup--fill-width');
78+
}
5579
}

src/ORM/Search/BasicSearchContext.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,13 @@ private function applyGeneralSearchField(array &$searchParams, SS_List $existing
124124
private function getCanGeneralSearch(string $fieldName): bool
125125
{
126126
$singleton = singleton($this->modelClass);
127+
$fields = $this->getSearchFieldsSpec($singleton);
127128

128129
// Allowed if we're dealing with arbitrary data.
129-
if (!ClassInfo::hasMethod($singleton, 'searchableFields')) {
130+
if (empty($fields) && !ClassInfo::hasMethod($singleton, 'searchableFields')) {
130131
return true;
131132
}
132133

133-
$fields = $singleton->searchableFields();
134-
135134
// Not allowed if the field isn't searchable.
136135
if (!isset($fields[$fieldName])) {
137136
return false;

src/ORM/Search/SearchContext.php

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ class SearchContext
8989
*/
9090
protected $withinRangeFieldsChecked = [];
9191

92+
/**
93+
* Specifications for searchable fields to add to the specifications returned from
94+
* searchableFields() on the model.
95+
*/
96+
private array $additionalFieldSpecs = [];
97+
9298
/**
9399
* A key value pair of values that should be searched for.
94100
* The keys should match the field names specified in {@link SearchContext::$fields}.
@@ -117,18 +123,17 @@ public function __construct($modelClass, $fields = null, $filters = null)
117123
*/
118124
public function getSearchFields()
119125
{
120-
if ($this->fields?->exists()) {
121-
return $this->fields;
122-
}
123-
124-
$singleton = singleton($this->modelClass);
125-
if (!$singleton->hasMethod('scaffoldSearchFields')) {
126-
throw new LogicException(
127-
'Cannot dynamically determine search fields. Pass the fields to setFields()'
128-
. " or implement a scaffoldSearchFields() method on {$this->modelClass}"
129-
);
126+
if (!$this->fields->exists()) {
127+
$singleton = singleton($this->modelClass);
128+
if (!$singleton->hasMethod('scaffoldSearchFields')) {
129+
throw new LogicException(
130+
'Cannot dynamically determine search fields. Pass the fields to setFields()'
131+
. " or implement a scaffoldSearchFields() method on {$this->modelClass}"
132+
);
133+
}
134+
$this->fields = $singleton->scaffoldSearchFields();
130135
}
131-
return $singleton->scaffoldSearchFields();
136+
return $this->fields;
132137
}
133138

134139
protected function applyBaseTableFields()
@@ -175,7 +180,7 @@ private function search(DataList $query): DataList
175180
$this->withinRangeFieldsChecked = [];
176181
/** @var DataObject $modelObj */
177182
$modelObj = Injector::inst()->create($this->modelClass);
178-
$searchableFields = $modelObj->searchableFields();
183+
$searchableFields = $this->getSearchFieldsSpec($modelObj);
179184
foreach ($this->searchParams as $searchField => $searchPhrase) {
180185
$searchField = str_replace('__', '.', $searchField ?? '');
181186
if ($searchField !== '' && $searchField === $modelObj->getGeneralSearchFieldName()) {
@@ -514,6 +519,47 @@ public function getSearchParams()
514519
return $this->searchParams;
515520
}
516521

522+
/**
523+
* Get the specification for searchable fields, e.g. which fields can be used in general search.
524+
* This is a combination of searchableFields() (where applicable) and getAdditionalFieldSpecs().
525+
*/
526+
public function getSearchFieldsSpec(?object $modelObject): array
527+
{
528+
if (!$modelObject) {
529+
$modelObject = Injector::inst()->create($this->modelClass);
530+
}
531+
if (!ClassInfo::hasMethod($modelObject, 'searchableFields')) {
532+
return $this->getAdditionalFieldSpecs();
533+
}
534+
return array_merge($modelObject->searchableFields(), $this->getAdditionalFieldSpecs());
535+
}
536+
537+
/**
538+
* Get the additional field specifications that are applied on top of any searchableFields().
539+
*/
540+
public function getAdditionalFieldSpecs(): array
541+
{
542+
return $this->additionalFieldSpecs;
543+
}
544+
545+
/**
546+
* Add field specifications to be applied on top of any searchableFields().
547+
*/
548+
public function addAdditionalFieldSpecs(array $specs): static
549+
{
550+
$this->additionalFieldSpecs = array_merge($this->additionalFieldSpecs, $specs);
551+
return $this;
552+
}
553+
554+
/**
555+
* Set field specifications to be applied on top of any searchableFields().
556+
*/
557+
public function setAdditionalFieldSpecs(array $specs): static
558+
{
559+
$this->additionalFieldSpecs = $specs;
560+
return $this;
561+
}
562+
517563
public function getModelClass(): string
518564
{
519565
return $this->modelClass;

tests/php/Forms/GridField/GridFieldFilterHeaderTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace SilverStripe\Forms\Tests\GridField;
44

55
use LogicException;
6+
use PHPUnit\Framework\Attributes\DataProvider;
67
use ReflectionMethod;
78
use ReflectionProperty;
89
use SilverStripe\Control\HTTPRequest;
@@ -14,6 +15,7 @@
1415
use SilverStripe\Forms\GridField\GridField;
1516
use SilverStripe\Forms\GridField\GridFieldConfig;
1617
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
18+
use SilverStripe\Forms\GridField\GridFieldDataColumns;
1719
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
1820
use SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest\Cheerleader;
1921
use SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest\CheerleaderHat;
@@ -199,6 +201,44 @@ public function testCanFilterAnyColumnsNonDataObject()
199201
$this->assertFalse($component->canFilterAnyColumns($gridField));
200202
}
201203

204+
public static function provideCanFilterAnyColumnsOnlyAdditional(): array
205+
{
206+
return [
207+
[
208+
'dataClass' => ArrayData::class,
209+
],
210+
[
211+
'dataClass' => Team::class,
212+
],
213+
];
214+
}
215+
216+
#[DataProvider('provideCanFilterAnyColumnsOnlyAdditional')]
217+
public function testCanFilterAnyColumnsOnlyAdditional(string $dataClass): void
218+
{
219+
$config = GridFieldConfig::create()->addComponents([
220+
$filter = new GridFieldFilterHeader(),
221+
$columnData = new GridFieldDataColumns(),
222+
]);
223+
224+
if ($dataClass === ArrayData::class) {
225+
$list = new ArrayList([
226+
new ArrayData(['Title' => 'Boogie']),
227+
]);
228+
$searchContext = new BasicSearchContext(ArrayData::class);
229+
$columnData->setDisplayFields(['Title' => 'Title']);
230+
} else {
231+
$list = new DataList($dataClass);
232+
$searchContext = singleton($dataClass)->getDefaultSearchContext();
233+
}
234+
235+
$filter->setSearchContext($searchContext);
236+
$searchContext->addAdditionalFieldSpecs(['Title' => []]);
237+
$gridField = new GridField('testfield', 'testfield', $list, $config);
238+
239+
$this->assertTrue($filter->canFilterAnyColumns($gridField));
240+
}
241+
202242
public function testRenderHeadersNonDataObject()
203243
{
204244
$list = new ArrayList([

tests/php/ORM/Search/BasicSearchContextTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,23 @@ public function testSpecificFieldsCanBeSkipped()
342342
$this->assertCount(0, $results);
343343
}
344344

345+
public function testAdditionalFieldSpecs(): void
346+
{
347+
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
348+
$list = new ArrayList();
349+
$list->merge(SearchContextTest\GeneralSearch::get());
350+
$generalField = BasicSearchContext::config()->get('general_search_field_name');
351+
$context = new BasicSearchContext(SearchContextTest\GeneralSearch::class);
352+
$context->addAdditionalFieldSpecs([
353+
'ExcludeThisField' => [
354+
'general' => true,
355+
],
356+
]);
357+
// Overriding the field spec above should let us find results using this field
358+
$results = $context->getQuery([$generalField => $general1->ExcludeThisField], existingQuery: $list);
359+
$this->assertNotEmpty($general1->ExcludeThisField);
360+
$this->assertCount(2, $results);
361+
}
345362

346363
#[DataProviderExternal(SearchContextTest::class, 'provideQueryWithinRangeFilter')]
347364
public function testQueryWithinRangeFilter(array $params, array $expectedFixtureNames): void

tests/php/ORM/Search/SearchContextTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,41 @@ public function testGeneralSearch()
589589
$results = $context->getResults([$generalField => 'arbitrary']);
590590
$this->assertCount(1, $results);
591591
$this->assertEquals('General One', $results->first()->Name);
592+
593+
// Matches nothing, because that field is excluded from general search
594+
$results = $context->getResults([$generalField => 'excluded']);
595+
$this->assertCount(0, $results);
596+
597+
// Matches against MatchAny1 or MatchAny2 field via MatchAny
598+
$results = $context->getResults([$generalField => 'first']);
599+
$this->assertCount(1, $results);
600+
$results = $context->getResults([$generalField => 'second']);
601+
$this->assertCount(1, $results);
602+
}
603+
604+
public function testAdditionalFieldSpecs(): void
605+
{
606+
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
607+
$generalField = $general1->getGeneralSearchFieldName();
608+
$context = $general1->getDefaultSearchContext();
609+
$context->addAdditionalFieldSpecs([
610+
'ExcludeThisField' => [
611+
'general' => true,
612+
],
613+
'MatchAny' => [
614+
'match_any' => [
615+
'MatchAny1',
616+
],
617+
],
618+
]);
619+
620+
// Compare with testGeneralSearch
621+
$results = $context->getResults([$generalField => 'excluded']);
622+
$this->assertCount(2, $results);
623+
$results = $context->getResults([$generalField => 'first']);
624+
$this->assertCount(1, $results);
625+
$results = $context->getResults([$generalField => 'second']);
626+
$this->assertCount(0, $results);
592627
}
593628

594629
public function testSpecificSearchFields()

0 commit comments

Comments
 (0)