diff --git a/README.md b/README.md
index cc84e4e..1c2d7c0 100644
--- a/README.md
+++ b/README.md
@@ -38,13 +38,13 @@ Overall this module allows you to fully customise your `GridField` filters inclu
## Installation
-`composer require silverstripe-terraformers/gridfield-rich-filter-header dev-master`
+`composer require silverstripe-terraformers/gridfield-rich-filter-header`
## Basic configuration
Full filter configuration format looks like this:
-```
+```php
'GridField_column_name' => [
'title' => 'DB_column_name',
'filter' => 'search_filter_type',
@@ -53,7 +53,7 @@ Full filter configuration format looks like this:
Concrete example:
-```
+```php
'Expires.Nice' => [
'title' => 'Expires',
'filter' => 'ExactMatchFilter',
@@ -69,22 +69,26 @@ Shorter configuration formats are available as well:
Field mapping version doesn't include filter specification and will use `PartialMatchFilter`.
This should be used if you are happy with using `PartialMatchFilter`
-```
+```php
'GridField_column_name' => 'DB_column_name',
```
Whitelist version doesn't include filter specification nor field mapping.
This configuration will use `PartialMatchFilter` and will assume that both `GridField_column_name` and `DB_column_name` are the same.
-```
+```php
'GridField_column_name',
```
Multiple filters configuration example:
-```
-$gridFieldConfig->removeComponentsByType(GridFieldFilterHeader::class);
+```php
+$gridFieldConfig->removeComponentsByType(
+ GridFieldSortableHeader::class,
+ GridFieldFilterHeader::class,
+);
+$sort = new RichSortableHeader();
$filter = new RichFilterHeader();
$filter
->setFilterConfig([
@@ -96,6 +100,7 @@ $filter
]);
+$gridFieldConfig->addComponent($sort, GridFieldPaginator::class);
$gridFieldConfig->addComponent($filter, GridFieldPaginator::class);
```
@@ -103,13 +108,14 @@ If no configuration is provided via `setFilterConfig` method, filter configurati
If `searchable_fields` configuration is not available, `summary_fields` will be used instead.
Make sure you add the `RichFilterHeader` component BEFORE the `GridFieldPaginator`
-otherwise your paginaton will be broken since you always want to filter before paginating.
+otherwise your pagination will be broken since you always want to filter before paginating.
+Sort header component also needs to be replaced to allow the filter expand button to be shown.
## Field configuration
Any `FormField` can be used for filtering. You just need to add it to filter configuration like this:
-```
+```php
->setFilterFields([
'Expires' => DateField::create('', ''),
])
@@ -125,7 +131,7 @@ If filter method is specified for a field, it will override the standard filter.
Filter method is a callback which will be applied to the `DataList` and you are free to add any functionality you need
inside the callback. Make sure that your callback returns a `DataList` with the same `DataClass` as the original.
-```
+```php
->setFilterMethods([
'Title' => function (DataList $list, $name, $value) {
// my custom filter logic is implemented here
@@ -143,7 +149,7 @@ For your convenience there are couple of filter methods available to cover some
Both of these filters can be used in `setFilterMethods` like this:
-```
+```php
->setFilterMethods([
'Title' => RichFilterHeader::FILTER_ALL_KEYWORDS,
])
@@ -156,9 +162,13 @@ Both of these filters can be used in `setFilterMethods` like this:
* filter with `AutoCompleteField` and filtering by `AllKeywordsFilter` filter method
* filter with `DateField` and filtering by `StartsWithFilter`
-```
-$gridFieldConfig->removeComponentsByType(GridFieldFilterHeader::class);
+```php
+$gridFieldConfig->removeComponentsByType(
+ GridFieldSortableHeader::class,
+ GridFieldFilterHeader::class,
+);
+$sort = new RichSortableHeader();
$filter = new RichFilterHeader();
$filter
->setFilterConfig([
@@ -185,6 +195,7 @@ $dealsLookup
->setSourceSort('Label ASC')
->setRequireSelection(false);
+$gridFieldConfig->addComponent($sort, GridFieldPaginator::class);
$gridFieldConfig->addComponent($filter, GridFieldPaginator::class);
```
@@ -197,9 +208,13 @@ $gridFieldConfig->addComponent($filter, GridFieldPaginator::class);
Note that the items that are listed in the `GridField` have a `many_many` relation with `TaxonomyTerm` called `TaxonomyTerms`
-```
-$gridFieldConfig->removeComponentsByType(GridFieldFilterHeader::class);
+```php
+$gridFieldConfig->removeComponentsByType(
+ GridFieldSortableHeader::class,
+ GridFieldFilterHeader::class,
+);
+$sort = new RichSortableHeader();
$filter = new RichFilterHeader();
$filter
->setFilterConfig([
@@ -216,6 +231,7 @@ $filter
'TaxonomyTerms' => RichFilterHeader::FILTER_MANY_MANY_RELATION,
]);
+$gridFieldConfig->addComponent($sort, GridFieldPaginator::class);
$gridFieldConfig->addComponent($filter, GridFieldPaginator::class);
```
@@ -227,9 +243,13 @@ $gridFieldConfig->addComponent($filter, GridFieldPaginator::class);
Our custom filter method filters records by three different `DB` columns via `PartialMatch` filter.
-```
-$gridFieldConfig->removeComponentsByType(GridFieldFilterHeader::class);
+```php
+$gridFieldConfig->removeComponentsByType(
+ GridFieldSortableHeader::class,
+ GridFieldFilterHeader::class,
+);
+$sort = new RichSortableHeader();
$filter = new RichFilterHeader();
$filter
->setFilterConfig([
@@ -249,6 +269,7 @@ $filter
]);
$label->setAttribute('placeholder', 'Filter by three different columns');
+$gridFieldConfig->addComponent($sort, GridFieldPaginator::class);
$gridFieldConfig->addComponent($filter, GridFieldPaginator::class);
```
@@ -261,7 +282,7 @@ This example covers
* `Team` (has many `Player`)
* `PlayersAdmin`
-```
+```php
getConfig();
// custom filters
- $config->removeComponentsByType(GridFieldFilterHeader::class);
-
+ $config->removeComponentsByType(
+ GridFieldSortableHeader::class,
+ GridFieldFilterHeader::class,
+ );
+
+ $sort = new RichSortableHeader();
$filter = new RichFilterHeader();
$filter
->setFilterConfig([
@@ -423,6 +437,7 @@ class PlayersAdmin extends ModelAdmin
$team->setEmptyString('-- select --');
+ $config->addComponent($sort, GridFieldPaginator::class);
$config->addComponent($filter, GridFieldPaginator::class);
}
diff --git a/_config/config.yml b/_config/config.yml
index e7aeb73..bc8c9d0 100644
--- a/_config/config.yml
+++ b/_config/config.yml
@@ -6,10 +6,3 @@ SilverStripe\Forms\FormRequestHandler:
- Terraformers\RichFilterHeader\Extension\GridFieldRichFilterHeaderRequestExtension
url_handlers:
'field/$FieldName!': 'handleFieldComposite'
-
-# legacy option for filter header component
-# we need to use this as new rect version of this component uses a different way to scaffold search fields
-# this needs to be removed before SS 5 upgrade and the effort contained means checking every GridField that
-# uses this component and validating if the desired fields display works fine
-SilverStripe\Forms\GridField\GridFieldFilterHeader:
- force_legacy: true
diff --git a/composer.json b/composer.json
index d00fd7e..095dc2c 100644
--- a/composer.json
+++ b/composer.json
@@ -32,6 +32,10 @@
"dev-2": "2.x-dev"
}
},
+ "scripts": {
+ "lint": "phpcs src/ tests/php/",
+ "lint-clean": "phpcbf src/ tests/php/"
+ },
"autoload": {
"psr-4": {
"Terraformers\\RichFilterHeader\\": "src/",
@@ -39,5 +43,12 @@
}
},
"prefer-stable": true,
- "minimum-stability": "dev"
+ "minimum-stability": "dev",
+ "config": {
+ "allow-plugins": {
+ "composer/installers": true,
+ "silverstripe/recipe-plugin": true,
+ "silverstripe/vendor-plugin": true
+ }
+ }
}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..980215a
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,32 @@
+
+
+ CodeSniffer ruleset for SilverStripe coding conventions.
+
+ src
+ tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ */SSTemplateParser.php$
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 965f16a..6d65030 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,13 +1,17 @@
-
+
+
+
+
+ src/
+
+
+ tests/
+
+
+tests/php/
-
-
- src/
-
- tests/
-
-
-
+
diff --git a/src/Form/GridField/RichFilterHeader.php b/src/Form/GridField/RichFilterHeader.php
index 0160c26..d225dc3 100755
--- a/src/Form/GridField/RichFilterHeader.php
+++ b/src/Form/GridField/RichFilterHeader.php
@@ -22,8 +22,6 @@
use SilverStripe\View\SSViewer;
/**
- * Class RichFilterHeader
- *
* filter header with customisable filter fields and filters
* fields that use XHR are supported
*
@@ -139,8 +137,6 @@
* ])
*
* this is a great way to cover edge cases as the implementation of the filter is completely up to the developer
- *
- * @package Terraformers\RichFilterHeader\Form\GridField
*/
class RichFilterHeader extends GridFieldFilterHeader
{
@@ -598,7 +594,7 @@ protected function hasFilterField($field)
* @param GridField $gridField
* @return array|null
*/
- public function getHTMLFragments($gridField)
+ public function getHTMLFragments(mixed $gridField): mixed
{
$list = $gridField->getList();
if (!$this->checkDataType($list)) {
diff --git a/src/Form/GridField/RichSortableHeader.php b/src/Form/GridField/RichSortableHeader.php
new file mode 100644
index 0000000..04576c5
--- /dev/null
+++ b/src/Form/GridField/RichSortableHeader.php
@@ -0,0 +1,154 @@
+ section
+ *
+ * @param mixed $gridField
+ * @return mixed
+ */
+ public function getHTMLFragments(mixed $gridField): mixed
+ {
+ /** @var RichFilterHeader $filter */
+ $filter = $gridField
+ ->getConfig()
+ ->getComponentByType(RichFilterHeader::class);
+
+ if (!$filter) {
+ // We don't have a matching rich filter header component set up,
+ // so we will fall back to the default behaviour
+ return parent::getHTMLFragments($gridField);
+ }
+
+ $list = $gridField->getList();
+
+ if (!$this->checkDataType($list)) {
+ return null;
+ }
+
+ /** @var Sortable $list */
+ $forTemplate = new ArrayData([]);
+ $forTemplate->Fields = new ArrayList;
+
+ $state = $this->getState($gridField);
+ $columns = $gridField->getColumns();
+ $currentColumn = 0;
+
+ $schema = DataObject::getSchema();
+
+ foreach ($columns as $columnField) {
+ $currentColumn++;
+ $metadata = $gridField->getColumnMetadata($columnField);
+ $fieldName = str_replace('.', '-', $columnField ?? '');
+ $title = $metadata['title'];
+
+ if (isset($this->fieldSorting[$columnField]) && $this->fieldSorting[$columnField]) {
+ $columnField = $this->fieldSorting[$columnField];
+ }
+
+ $allowSort = ($title && $list->canSortBy($columnField));
+
+ if (!$allowSort && strpos($columnField ?? '', '.') !== false) {
+ // we have a relation column with dot notation
+ // @see DataObject::relField for approximation
+ $parts = explode('.', $columnField ?? '');
+ $tmpItem = singleton($list->dataClass());
+
+ for ($idx = 0; $idx < sizeof($parts ?? []); $idx++) {
+ $methodName = $parts[$idx];
+ if ($tmpItem instanceof SS_List) {
+ // It's impossible to sort on a HasManyList/ManyManyList
+ break;
+ } elseif ($tmpItem && method_exists($tmpItem, 'hasMethod') && $tmpItem->hasMethod($methodName)) {
+ // The part is a relation name, so get the object/list from it
+ $tmpItem = $tmpItem->$methodName();
+ } elseif ($tmpItem instanceof DataObject
+ && $schema->fieldSpec($tmpItem, $methodName, DataObjectSchema::DB_ONLY)
+ ) {
+ // Else, if we've found a database field at the end of the chain, we can sort on it.
+ // If a method is applied further to this field (E.g. 'Cost.Currency') then don't try to sort.
+ $allowSort = $idx === sizeof($parts ?? []) - 1;
+ break;
+ } else {
+ // If neither method nor field, then unable to sort
+ break;
+ }
+ }
+ }
+
+ if ($allowSort) {
+ $dir = 'asc';
+ if ($state->SortColumn(null) == $columnField && $state->SortDirection('asc') == 'asc') {
+ $dir = 'desc';
+ }
+
+ $field = GridField_FormAction::create(
+ $gridField,
+ 'SetOrder' . $fieldName,
+ $title,
+ "sort$dir",
+ ['SortColumn' => $columnField]
+ )->addExtraClass('grid-field__sort');
+
+ if ($state->SortColumn(null) == $columnField) {
+ $field->addExtraClass('ss-gridfield-sorted');
+
+ if ($state->SortDirection('asc') == 'asc') {
+ $field->addExtraClass('ss-gridfield-sorted-asc');
+ } else {
+ $field->addExtraClass('ss-gridfield-sorted-desc');
+ }
+ }
+ } else {
+ // start
+ $sortActionFieldContents = $currentColumn == count($columns ?? []) && $filter->canFilterAnyColumns($gridField)
+ ? sprintf(
+ '',
+ _t('SilverStripe\\Forms\\GridField\\GridField.OpenFilter', "Open search and filter")
+ )
+ : '' . $title . '';
+ $field = LiteralField::create($fieldName, $sortActionFieldContents);
+ // end
+ }
+
+ $forTemplate->Fields->push($field);
+ }
+
+ $template = SSViewer::get_templates_by_class($this, '_Row', GridFieldSortableHeader::class);
+
+ return [
+ 'header' => $forTemplate->renderWith($template),
+ ];
+ }
+
+ /**
+ * Copied from parent without any change due to the method being private
+ *
+ * @param GridField $gridField
+ * @return GridState_Data
+ */
+ private function getState(GridField $gridField): GridState_Data
+ {
+ return $gridField->State->GridFieldSortableHeader;
+ }
+}
diff --git a/tests/php/Form/GridField/RichFilterHeaderTest.php b/tests/php/Form/GridField/RichFilterHeaderTest.php
index 24d7086..1aad5b9 100644
--- a/tests/php/Form/GridField/RichFilterHeaderTest.php
+++ b/tests/php/Form/GridField/RichFilterHeaderTest.php
@@ -19,37 +19,24 @@
use SilverStripe\ORM\DataList;
use SilverStripe\Security\SecurityToken;
-/**
- * Class RichFilterHeaderTest
- * @package Terraformers\RichFilterHeader\Tests\Form\GridField
- */
class RichFilterHeaderTest extends SapphireTest
{
- /**
- * @var GridField
- */
- protected $gridField;
+ protected ?GridField $gridField = null;
- /**
- * @var Form
- */
- protected $form;
+ protected ?Form $form = null;
/**
* @var string
*/
protected static $fixture_file = 'RichFilterHeaderTest.yml';
- /**
- * @var array
- */
- protected static $extra_dataobjects = array(
+ protected static $extra_dataobjects = [
Team::class,
Cheerleader::class,
CheerleaderHat::class,
- );
+ ];
- protected function setUp()
+ protected function setUp(): void
{
parent::setUp();
@@ -68,7 +55,7 @@ protected function setUp()
);
}
- public function testCompositeFieldName()
+ public function testCompositeFieldName(): void
{
$gridFieldName = 'test-grid-field1';
$childFieldName = 'test-child-field1';
@@ -76,11 +63,11 @@ public function testCompositeFieldName()
$data = RichFilterHeader::parseCompositeFieldName($compositeFieldName);
$this->assertNotEmpty($data);
- $this->assertEquals($gridFieldName, $data['grid_field']);
- $this->assertEquals($childFieldName, $data['child_field']);
+ $this->assertEquals($gridFieldName, $data['grid_field'], 'We expect a specific GridField name');
+ $this->assertEquals($childFieldName, $data['child_field'], 'We expect a specific child field name');
}
- public function testRenderFilteredHeaderStandard()
+ public function testRenderFilteredHeaderStandard(): void
{
$gridField = $this->gridField;
$config = $gridField->getConfig();
@@ -89,15 +76,16 @@ public function testRenderFilteredHeaderStandard()
$component = $config->getComponentByType(RichFilterHeader::class);
$htmlFragment = $component->getHTMLFragments($gridField);
- $this->assertContains(
+ $this->assertStringContainsString(
'',
- $htmlFragment['header']
+ $htmlFragment['header'],
+ 'We expect a rendered filter'
);
}
- public function testRenderFilterHeaderWithCustomFields()
+ public function testRenderFilterHeaderWithCustomFields(): void
{
$gridField = $this->gridField;
$config = $gridField->getConfig();
@@ -111,22 +99,24 @@ public function testRenderFilterHeaderWithCustomFields()
$htmlFragment = $component->getHTMLFragments($gridField);
- $this->assertContains(
+ $this->assertStringContainsString(
'