mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW Provide an easy way to filter arbitrary ViewableData in gridfields
This commit is contained in:
parent
bc47d65cc5
commit
b1295af281
@ -12,6 +12,7 @@ use SilverStripe\Forms\FieldList;
|
||||
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;
|
||||
@ -33,7 +34,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
protected $throwExceptionOnBadDataType = true;
|
||||
|
||||
/**
|
||||
* @var \SilverStripe\ORM\Search\SearchContext
|
||||
* @var SearchContext
|
||||
*/
|
||||
protected $searchContext = null;
|
||||
|
||||
@ -250,7 +251,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
* 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)
|
||||
{
|
||||
@ -261,6 +262,16 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
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
|
||||
*
|
||||
@ -287,8 +298,6 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
$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)) {
|
||||
@ -302,7 +311,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
$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'),
|
||||
@ -314,6 +323,19 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
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
|
||||
*
|
||||
@ -357,7 +379,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
$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,
|
||||
|
188
src/ORM/Search/BasicSearchContext.php
Normal file
188
src/ORM/Search/BasicSearchContext.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Search;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\ORM\Filterable;
|
||||
use SilverStripe\ORM\Filters\PartialMatchFilter;
|
||||
use SilverStripe\ORM\Filters\SearchFilter;
|
||||
use SilverStripe\ORM\Limitable;
|
||||
use SilverStripe\ORM\Sortable;
|
||||
|
||||
/**
|
||||
* A SearchContext that can be used with non-ORM data.
|
||||
* This class isn't guaranteed to respect the full searchable fields spec defined on DataObject classes.
|
||||
*/
|
||||
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 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->applySearchFilters($this->normaliseSearchParams($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 normaliseSearchParams(array $searchParams): array
|
||||
{
|
||||
$normalised = [];
|
||||
foreach ($searchParams as $field => $searchTerm) {
|
||||
if ($this->clearEmptySearchFields($searchTerm)) {
|
||||
$normalised[str_replace('__', '.', $field)] = $searchTerm;
|
||||
}
|
||||
}
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
private function applySearchFilters(array $searchParams): array
|
||||
{
|
||||
$applied = [];
|
||||
foreach ($searchParams as $fieldName => $searchTerm) {
|
||||
// Ignore the general search field - we'll deal with that in a special way.
|
||||
if ($fieldName === static::config()->get('general_search_field_name')) {
|
||||
$applied[$fieldName] = $searchTerm;
|
||||
continue;
|
||||
}
|
||||
$filterTerm = $this->getFilterTerm($fieldName);
|
||||
$applied["{$fieldName}:{$filterTerm}"] = $searchTerm;
|
||||
}
|
||||
return $applied;
|
||||
}
|
||||
|
||||
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];
|
||||
if (Config::inst()->get($this->modelClass, 'general_search_split_terms') !== false) {
|
||||
$searchTerm = explode(' ', $searchTerm);
|
||||
}
|
||||
$generalFilter = [];
|
||||
foreach ($this->getSearchFields()->dataFieldNames() as $fieldName) {
|
||||
if ($fieldName === $generalFieldName) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->getCanGeneralSearch($fieldName)) {
|
||||
continue;
|
||||
}
|
||||
$filterTerm = $this->getGeneralSearchFilterTerm($fieldName);
|
||||
$generalFilter["{$fieldName}:{$filterTerm}"] = $searchTerm;
|
||||
}
|
||||
$result = $existingQuery->filterAny($generalFilter);
|
||||
unset($searchParams[$generalFieldName]);
|
||||
}
|
||||
|
||||
return $result ?? $existingQuery;
|
||||
}
|
||||
|
||||
private function getCanGeneralSearch(string $fieldName): bool
|
||||
{
|
||||
$singleton = singleton($this->modelClass);
|
||||
|
||||
// Allowed if we're dealing with arbitrary data.
|
||||
if (!ClassInfo::hasMethod($singleton, 'searchableFields')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fields = $singleton->searchableFields();
|
||||
|
||||
// Not allowed if the field isn't searchable.
|
||||
if (!isset($fields[$fieldName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allowed if 'general' isn't part of the spec, or is explicitly truthy.
|
||||
return !isset($fields[$fieldName]['general']) || $fields[$fieldName]['general'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the search filter for the given fieldname when searched from the general search field.
|
||||
*/
|
||||
private function getGeneralSearchFilterTerm(string $fieldName): string
|
||||
{
|
||||
$filterClass = Config::inst()->get($this->modelClass, 'general_search_field_filter');
|
||||
if ($filterClass) {
|
||||
return $this->getTermFromFilter(Injector::inst()->create($filterClass, $fieldName));
|
||||
}
|
||||
|
||||
if ($filterClass === '') {
|
||||
return $this->getFilterTerm($fieldName);
|
||||
}
|
||||
|
||||
return 'PartialMatch:nocase';
|
||||
}
|
||||
|
||||
private function getFilterTerm(string $fieldName): string
|
||||
{
|
||||
$filter = $this->getFilter($fieldName) ?? PartialMatchFilter::create($fieldName);
|
||||
return $this->getTermFromFilter($filter);
|
||||
}
|
||||
|
||||
private function getTermFromFilter(SearchFilter $filter): string
|
||||
{
|
||||
$modifiers = $filter->getModifiers() ?? [];
|
||||
|
||||
// Get the string used to refer to the filter, e.g. "PartialMatch"
|
||||
// Ask the injector for it first - but for any not defined there, fall back to string manipulation.
|
||||
$filterTerm = Injector::inst()->getServiceName(get_class($filter));
|
||||
if (!$filterTerm) {
|
||||
$filterTerm = preg_replace('/Filter$/', '', ClassInfo::shortName($filter));
|
||||
}
|
||||
|
||||
// Add modifiers to filter
|
||||
foreach ($modifiers as $modifier) {
|
||||
$filterTerm .= ":{$modifier}";
|
||||
}
|
||||
|
||||
return $filterTerm;
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ use SilverStripe\Forms\SelectField;
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\ORM\DataQuery;
|
||||
@ -104,7 +105,18 @@ class SearchContext
|
||||
*/
|
||||
public function getSearchFields()
|
||||
{
|
||||
return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields();
|
||||
if ($this->fields->exists()) {
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
$singleton = singleton($this->modelClass);
|
||||
if (!$singleton->hasMethod('scaffoldSearchFields')) {
|
||||
throw new LogicException(
|
||||
'Cannot dynamically determine search fields. Pass the fields to setFields()'
|
||||
. " or implement a scaffoldSearchFields() method on {$this->modelClass}"
|
||||
);
|
||||
}
|
||||
return $singleton->scaffoldSearchFields();
|
||||
}
|
||||
|
||||
protected function applyBaseTableFields()
|
||||
|
Loading…
Reference in New Issue
Block a user