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( '', - $htmlFragment['header'] + $htmlFragment['header'], + 'We expect a rendered City filter' ); } - public function testRenderFilterHeaderWithFullConfig() + public function testRenderFilterHeaderWithFullConfig(): void { $gridField = $this->gridField; $config = $gridField->getConfig(); @@ -151,22 +141,24 @@ public function testRenderFilterHeaderWithFullConfig() $htmlFragment = $component->getHTMLFragments($gridField); - $this->assertContains( + $this->assertStringContainsString( '', - $htmlFragment['header'] + $htmlFragment['header'], + 'We expect a rendered City filter' ); } - public function testRenderFilterHeaderBasicFilter() + public function testRenderFilterHeaderBasicFilter(): void { $gridField = $this->gridField; $config = $gridField->getConfig(); @@ -213,11 +205,16 @@ public function testRenderFilterHeaderBasicFilter() $gridField->gridFieldAlterAction(['StateID' => $stateID], $this->form, $request); $list = $component->getManipulatedData($gridField, $gridField->getList()); - $this->assertEquals(1, (int) $list->count()); - $this->assertEquals($city, $list->first()->City); + $this->assertSame( + [ + $city, + ], + $list->column('City'), + 'We expect a single item after filtering by City' + ); } - public function testRenderFilterHeaderAdvancedFilterAllKeywords() + public function testRenderFilterHeaderAdvancedFilterAllKeywords(): void { $gridField = $this->gridField; $config = $gridField->getConfig(); @@ -265,11 +262,16 @@ public function testRenderFilterHeaderAdvancedFilterAllKeywords() $gridField->gridFieldAlterAction(['StateID' => $stateID], $this->form, $request); $list = $component->getManipulatedData($gridField, $gridField->getList()); - $this->assertEquals(1, (int) $list->count()); - $this->assertEquals($keywords, $list->first()->Name); + $this->assertSame( + [ + $keywords, + ], + $list->column('Name'), + 'We expect a single item after filtering by Name' + ); } - public function testRenderFilterHeaderAdvancedFilterManyManyRelation() + public function testRenderFilterHeaderAdvancedFilterManyManyRelation(): void { $gridField = $this->gridField; $gridField->setList(DataList::create(Cheerleader::class)); @@ -320,11 +322,11 @@ public function testRenderFilterHeaderAdvancedFilterManyManyRelation() $gridField->gridFieldAlterAction(['StateID' => $stateID], $this->form, $request); $list = $component->getManipulatedData($gridField, $gridField->getList()); - $this->assertEquals(1, (int) $list->count()); - $this->assertEquals($hat->ID, $list->first()->Hats()->first()->ID); + $this->assertEquals(1, (int) $list->count(), 'We expect a single item after filtering'); + $this->assertEquals($hat->ID, $list->first()->Hats()->first()->ID, 'We expect a specific result item'); } - public function testRenderFilterHeaderAdvancedFilterCustomCallback() + public function testRenderFilterHeaderAdvancedFilterCustomCallback(): void { $gridField = $this->gridField; $config = $gridField->getConfig(); @@ -377,8 +379,16 @@ public function testRenderFilterHeaderAdvancedFilterCustomCallback() /** @var DataList $list */ $list = $component->getManipulatedData($gridField, $gridField->getList()); - $this->assertEquals(2, (int) $list->count()); - $cities = $list->sort('City', 'ASC')->column('City'); - $this->assertEquals(['newton', 'Wellington'], $cities); + $cities = $list + ->sort('City', 'ASC') + ->column('City'); + $this->assertEquals( + [ + 'newton', + 'Wellington' + ], + $cities, + 'We expect specific results after filtering' + ); } } diff --git a/tests/php/Form/GridField/RichFilterHeaderTest.yml b/tests/php/Form/GridField/RichFilterHeaderTest.yml index ec39074..7cacf06 100644 --- a/tests/php/Form/GridField/RichFilterHeaderTest.yml +++ b/tests/php/Form/GridField/RichFilterHeaderTest.yml @@ -1,44 +1,44 @@ Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\CheerleaderHat: hat1: - Colour: Blue + Colour: 'Blue' hat2: - Colour: Red + Colour: 'Red' hat3: - Colour: Green + Colour: 'Green' hat4: - Colour: Pink + Colour: 'Pink' Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\Cheerleader: cheerleader1: - Name: Heather + Name: 'Heather' Hats: - =>Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\CheerleaderHat.hat2 cheerleader2: - Name: Bob + Name: 'Bob' Hats: - =>Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\CheerleaderHat.hat4 cheerleader3: - Name: Jenny + Name: 'Jenny' Hats: - =>Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\CheerleaderHat.hat1 cheerleader4: - Name: Sam + Name: 'Sam' Hats: - =>Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\CheerleaderHat.hat3 Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\Team: team1: - Name: Team 1 - City: newton + Name: 'Team 1' + City: 'newton' Cheerleader: =>Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\Cheerleader.cheerleader3 team2: - Name: Team 2 - City: Wellington + Name: 'Team 2' + City: 'Wellington' Cheerleader: =>Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\Cheerleader.cheerleader2 team3: - Name: Team 3 - City: Auckland + Name: 'Team 3' + City: 'Auckland' Cheerleader: =>Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\Cheerleader.cheerleader4 team4: - Name: Team 4 - City: Melbourne + Name: 'Team 4' + City: 'Melbourne' Cheerleader: =>Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest\Cheerleader.cheerleader1 diff --git a/tests/php/Form/GridField/RichFilterHeaderTest/Cheerleader.php b/tests/php/Form/GridField/RichFilterHeaderTest/Cheerleader.php index 0c22f7a..47618ba 100644 --- a/tests/php/Form/GridField/RichFilterHeaderTest/Cheerleader.php +++ b/tests/php/Form/GridField/RichFilterHeaderTest/Cheerleader.php @@ -4,10 +4,12 @@ use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\HasManyList; /** - * Class Cheerleader - * @package Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest + * @property string $Name + * @method Team Team() + * @method HasManyList|CheerleaderHat[] Hats() */ class Cheerleader extends DataObject implements TestOnly { diff --git a/tests/php/Form/GridField/RichFilterHeaderTest/CheerleaderHat.php b/tests/php/Form/GridField/RichFilterHeaderTest/CheerleaderHat.php index 3f1751c..3aa5a3f 100644 --- a/tests/php/Form/GridField/RichFilterHeaderTest/CheerleaderHat.php +++ b/tests/php/Form/GridField/RichFilterHeaderTest/CheerleaderHat.php @@ -4,10 +4,11 @@ use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\ManyManyList; /** - * Class CheerleaderHat - * @package Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest + * @property string $Colour + * @method ManyManyList|Cheerleader[] Cheerleaders() */ class CheerleaderHat extends DataObject implements TestOnly { diff --git a/tests/php/Form/GridField/RichFilterHeaderTest/Team.php b/tests/php/Form/GridField/RichFilterHeaderTest/Team.php index 165562c..3e589b1 100644 --- a/tests/php/Form/GridField/RichFilterHeaderTest/Team.php +++ b/tests/php/Form/GridField/RichFilterHeaderTest/Team.php @@ -6,8 +6,9 @@ use SilverStripe\ORM\DataObject; /** - * Class Team - * @package Terraformers\RichFilterHeader\Tests\Form\GridField\RichFilterHeaderTest + * @property string $Name + * @property string $City + * @method Cheerleader Cheerleader() */ class Team extends DataObject implements TestOnly {