Skip to content

Commit

Permalink
NEW Provide an easy way to filter arbitrary ViewableData in gridfields
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Nov 13, 2023
1 parent a29b079 commit 80048c3
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 8 deletions.
34 changes: 28 additions & 6 deletions src/Forms/GridField/GridFieldFilterHeader.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use SilverStripe\Forms\Form;
use SilverStripe\Forms\Schema\FormSchema;
use SilverStripe\ORM\Filterable;
use SilverStripe\ORM\Search\SearchContext;
use SilverStripe\ORM\SS_List;
use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer;
Expand All @@ -33,7 +34,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
protected $throwExceptionOnBadDataType = true;

/**
* @var \SilverStripe\ORM\Search\SearchContext
* @var SearchContext
*/
protected $searchContext = null;

Expand Down Expand Up @@ -250,7 +251,7 @@ public function canFilterAnyColumns($gridField)
* Generate a search context based on the model class of the of the GridField
*
* @param GridField $gridfield
* @return \SilverStripe\ORM\Search\SearchContext
* @return SearchContext
*/
public function getSearchContext(GridField $gridField)
{
Expand All @@ -261,6 +262,16 @@ public function getSearchContext(GridField $gridField)
return $this->searchContext;
}

/**
* Sets a specific SearchContext instance for this component to use, instead of the default
* context provided by the ModelClass.
*/
public function setSearchContext(SearchContext $context): static
{
$this->searchContext = $context;
return $this;
}

/**
* Returns the search field schema for the component
*
Expand All @@ -287,8 +298,6 @@ public function getSearchFieldSchema(GridField $gridField)
$searchField = $searchField && property_exists($searchField, 'name') ? $searchField->name : null;
}

$name = $gridField->Title ?: $inst->i18n_plural_name();

// Prefix "Search__" onto the filters for the React component
$filters = $context->getSearchParams();
if (!empty($filters)) {
Expand All @@ -302,7 +311,7 @@ public function getSearchFieldSchema(GridField $gridField)
$schema = [
'formSchemaUrl' => $schemaUrl,
'name' => $searchField,
'placeholder' => _t(__CLASS__ . '.Search', 'Search "{name}"', ['name' => $name]),
'placeholder' => _t(__CLASS__ . '.Search', 'Search "{name}"', ['name' => $this->getTitle($gridField, $inst)]),
'filters' => $filters ?: new \stdClass, // stdClass maps to empty json object '{}'
'gridfield' => $gridField->getName(),
'searchAction' => $searchAction->getAttribute('name'),
Expand All @@ -314,6 +323,19 @@ public function getSearchFieldSchema(GridField $gridField)
return json_encode($schema);
}

private function getTitle(GridField $gridField, object $inst): string
{
if ($gridField->Title) {
return $gridField->Title;
}

if (ClassInfo::hasMethod($inst, 'i18n_plural_name')) {
return $inst->i18n_plural_name();
}

return ClassInfo::shortName($inst);
}

/**
* Returns the search form for the component
*
Expand Down Expand Up @@ -357,7 +379,7 @@ public function getSearchForm(GridField $gridField)
$field->addExtraClass('stacked no-change-track');
}

$name = $gridField->Title ?: singleton($gridField->getModelClass())->i18n_plural_name();
$name = $this->getTitle($gridField, singleton($gridField->getModelClass()));

$this->searchForm = $form = new Form(
$gridField,
Expand Down
117 changes: 117 additions & 0 deletions src/ORM/Search/BasicSearchContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace SilverStripe\ORM\Search;

use BadMethodCallException;
use InvalidArgumentException;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\Filterable;
use SilverStripe\ORM\Limitable;
use SilverStripe\ORM\Sortable;

/**
* A SearchContext that can be used with non-ORM data.
*/
class BasicSearchContext extends SearchContext
{
use Configurable;

/**
* Name of the field which, if included in search forms passed to this object, will be used
* to search across all searchable fields.
*/
private static $general_search_field_name = 'q';

/**
* Returns a list which has been limited, sorted, and filtered by the given parameters.
*
* @param array $searchParams Map of search criteria, mostly taken from $_REQUEST.
* If a filter is applied to a relationship in dot notation,
* the parameter name should have the dots replaced with double underscores,
* for example "Comments__Name" instead of the filter name "Comments.Name".
* @param array|bool|string $sort Field to sort on.
* @param array|null|string $limit
* @param Filterable&Sortable&Limitable $existingQuery
*/
public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null): Filterable&Sortable&Limitable
{
if (!$existingQuery || !($existingQuery instanceof Filterable) || !($existingQuery instanceof Sortable) || !($existingQuery instanceof Limitable)) {
throw new InvalidArgumentException('getQuery requires a pre-existing filterable/sortable/limitable list (usually ArrayList) to be passed as $existingQuery.');
}

if ((count(func_get_args()) >= 3) && (!in_array(gettype($limit), ['array', 'NULL', 'string']))) {
Deprecation::notice(
'5.1.0',
'$limit should be type of array|string|null'
);
$limit = null;
}

$searchParams = $this->normaliseFieldNames($searchParams);
$result = $this->applyGeneralSearchField($searchParams, $existingQuery);

// Filter the list by the requested filters.
if (!empty($searchParams)) {
$result = $result->filter($searchParams);
}

// Only sort if a sort value is provided - sort by "false" just means use the existing sort.
if ($sort) {
$result = $result->sort($sort);
}

// Limit must be last so that ArrayList results don't have an applied limit before they can be filtered/sorted.
$result = $result->limit($limit);

return $result;
}

private function normaliseFieldNames(array $searchParams): array
{
$normalised = [];
foreach ($searchParams as $field => $searchTerm) {
$normalised[str_replace('__', '.', $field)] = $searchTerm;
}
return $normalised;
}

private function applyGeneralSearchField(array &$searchParams, Filterable $existingQuery): Filterable
{
$generalFieldName = static::config()->get('general_search_field_name');
if (array_key_exists($generalFieldName, $searchParams)) {
$searchTerm = $searchParams[$generalFieldName];
$generalFilter = [];
foreach ($this->getSearchFields()->dataFieldNames() as $fieldName) {
if ($fieldName === $generalFieldName) {
continue;
}
$generalFilter[$fieldName] = $searchTerm;
}
$result = $existingQuery->filterAny($generalFilter);
unset($searchParams[$generalFieldName]);
}

return $result ?? $existingQuery;
}

/**
* Throws an exception. SearchFilters aren't defined for this search context. Instead,
* the field names should have search filter syntax directly (e.g. "Name:PartialMatch")
* @throws BadMethodCallException
*/
public function setFilters($filters)
{
throw new BadMethodCallException('Not implemented');
}

/**
* Throws an exception. SearchFilters aren't defined for this search context. Instead,
* the field names should have search filter syntax directly (e.g. "Name:PartialMatch")
* @throws BadMethodCallException
*/
public function addFilter($filter)
{
throw new BadMethodCallException('Not implemented');
}
}
4 changes: 2 additions & 2 deletions src/ORM/Search/SearchContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class SearchContext
public function __construct($modelClass, $fields = null, $filters = null)
{
$this->modelClass = $modelClass;
$this->fields = ($fields) ? $fields : new FieldList();
$this->fields = ($fields) ? $fields : null;
$this->filters = ($filters) ? $filters : [];
}

Expand Down Expand Up @@ -411,7 +411,7 @@ public function removeFilterByName($name)
*/
public function getFields()
{
return $this->fields;
return $this->fields ?? FieldList::create();
}

/**
Expand Down

0 comments on commit 80048c3

Please sign in to comment.