2011-12-06 01:56:24 +01:00
< ? php
2016-06-15 06:03:16 +02:00
2016-08-19 00:51:35 +02:00
namespace SilverStripe\Forms\GridField ;
2018-09-04 01:35:17 +02:00
use LogicException ;
use SilverStripe\Admin\LeftAndMain ;
use SilverStripe\Control\Controller ;
use SilverStripe\Control\HTTPResponse ;
2018-10-26 03:28:15 +02:00
use SilverStripe\Core\Config\Config ;
2018-09-04 01:35:17 +02:00
use SilverStripe\Core\Convert ;
use SilverStripe\Dev\Deprecation ;
2016-08-19 00:51:35 +02:00
use SilverStripe\Forms\FieldGroup ;
2018-09-04 01:35:17 +02:00
use SilverStripe\Forms\FieldList ;
use SilverStripe\Forms\Form ;
use SilverStripe\Forms\Schema\FormSchema ;
2016-08-19 00:51:35 +02:00
use SilverStripe\Forms\TextField ;
2018-09-04 01:35:17 +02:00
use SilverStripe\ORM\ArrayList ;
2016-09-09 08:43:05 +02:00
use SilverStripe\ORM\Filterable ;
2016-06-15 06:03:16 +02:00
use SilverStripe\ORM\SS_List ;
2016-08-19 00:51:35 +02:00
use SilverStripe\View\ArrayData ;
use SilverStripe\View\SSViewer ;
2011-12-06 01:56:24 +01:00
/**
2014-08-15 08:53:05 +02:00
* GridFieldFilterHeader alters the { @ link GridField } with some filtering
2013-05-20 12:18:07 +02:00
* fields in the header of each column .
2014-08-15 08:53:05 +02:00
*
2011-12-06 01:56:24 +01:00
* @ see GridField
*/
2022-02-01 23:14:33 +01:00
class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridField_URLHandler , GridField_HTMLProvider , GridField_DataManipulator , GridField_ActionProvider , GridField_StateProvider
2016-11-29 00:31:16 +01:00
{
/**
* See { @ link setThrowExceptionOnBadDataType ()}
*
* @ var bool
*/
protected $throwExceptionOnBadDataType = true ;
2018-09-04 01:35:17 +02:00
/**
* Indicates that this component should revert to displaying it ' s legacy
* table header style rather than the react driven search box
*
2018-09-28 10:38:56 +02:00
* @ deprecated 4.3 . 0 : 5.0 . 0 Will be removed in 5.0
2018-09-04 01:35:17 +02:00
* @ var bool
*/
public $useLegacyFilterHeader = false ;
2018-10-26 03:28:15 +02:00
/**
* Forces all filter components to revert to displaying the legacy
* table header style rather than the react driven search box
*
* @ deprecated 4.3 . 0 : 5.0 . 0 Will be removed in 5.0
* @ config
* @ var bool
*/
private static $force_legacy = false ;
2018-09-28 02:31:23 +02:00
/**
* @ var \SilverStripe\ORM\Search\SearchContext
*/
protected $searchContext = null ;
/**
* @ var Form
*/
protected $searchForm = null ;
/**
* @ var callable
2018-09-28 10:38:56 +02:00
* @ deprecated 4.3 . 0 : 5.0 . 0 Will be removed in 5.0
2018-09-28 02:31:23 +02:00
*/
2018-09-28 10:38:56 +02:00
protected $updateSearchContextCallback = null ;
2018-09-28 02:31:23 +02:00
/**
* @ var callable
2018-09-28 10:38:56 +02:00
* @ deprecated 4.3 . 0 : 5.0 . 0 Will be removed in 5.0
2018-09-28 02:31:23 +02:00
*/
2018-09-28 10:38:56 +02:00
protected $updateSearchFormCallback = null ;
2018-09-28 02:31:23 +02:00
2019-11-26 23:38:38 +01:00
/**
* The name of the default search field
* @ var string | null
*/
protected ? string $searchField = null ;
2018-09-04 01:35:17 +02:00
/**
* @ inheritDoc
*/
public function getURLHandlers ( $gridField )
{
return [
'GET schema/SearchForm' => 'getSearchFormSchema'
];
}
/**
2018-10-26 03:28:15 +02:00
* @ param bool $useLegacy This will be removed in 5.0
2018-09-28 10:38:56 +02:00
* @ param callable | null $updateSearchContext This will be removed in 5.0
2018-09-28 02:31:23 +02:00
* @ param callable | null $updateSearchForm This will be removed in 5.0
2018-09-04 01:35:17 +02:00
*/
2018-09-28 02:31:23 +02:00
public function __construct (
$useLegacy = false ,
callable $updateSearchContext = null ,
callable $updateSearchForm = null
) {
2022-11-15 06:20:54 +01:00
$forceLegacy = Deprecation :: withNoReplacement ( function () {
return Config :: inst () -> get ( self :: class , 'force_legacy' );
});
$this -> useLegacyFilterHeader = $forceLegacy || $useLegacy ;
2018-09-28 02:31:23 +02:00
$this -> updateSearchContextCallback = $updateSearchContext ;
$this -> updateSearchFormCallback = $updateSearchForm ;
2018-09-04 01:35:17 +02:00
}
2016-11-29 00:31:16 +01:00
/**
* Determine what happens when this component is used with a list that isn ' t { @ link SS_Filterable } .
*
* - true : An exception is thrown
* - false : This component will be ignored - it won ' t make any changes to the GridField .
*
* By default , this is set to true so that it 's clearer what' s happening , but the predefined
* { @ link GridFieldConfig } subclasses set this to false for flexibility .
*
* @ param bool $throwExceptionOnBadDataType
*/
public function setThrowExceptionOnBadDataType ( $throwExceptionOnBadDataType )
{
$this -> throwExceptionOnBadDataType = $throwExceptionOnBadDataType ;
}
/**
* See { @ link setThrowExceptionOnBadDataType ()}
*/
public function getThrowExceptionOnBadDataType ()
{
return $this -> throwExceptionOnBadDataType ;
}
2019-11-26 23:38:38 +01:00
public function getSearchField () : ? string
{
return $this -> searchField ;
}
public function setSearchField ( string $field ) : self
{
$this -> searchField = $field ;
return $this ;
}
2016-11-29 00:31:16 +01:00
/**
* Check that this dataList is of the right data type .
* Returns false if it ' s a bad data type , and if appropriate , throws an exception .
*
* @ param SS_List $dataList
* @ return bool
*/
protected function checkDataType ( $dataList )
{
if ( $dataList instanceof Filterable ) {
return true ;
} else {
if ( $this -> throwExceptionOnBadDataType ) {
throw new LogicException (
2017-05-17 07:40:13 +02:00
static :: class . " expects an SS_Filterable list to be passed to the GridField. "
2016-11-29 00:31:16 +01:00
);
}
return false ;
}
}
/**
2018-09-04 01:35:17 +02:00
* If the GridField has a filterable datalist , return an array of actions
2016-11-29 00:31:16 +01:00
*
* @ param GridField $gridField
* @ return array
*/
public function getActions ( $gridField )
{
if ( ! $this -> checkDataType ( $gridField -> getList ())) {
return [];
}
2018-09-04 01:35:17 +02:00
return [ 'filter' , 'reset' ];
2016-11-29 00:31:16 +01:00
}
2018-09-04 01:35:17 +02:00
/**
* If the GridField has a filterable datalist , return an array of actions
*
* @ param GridField $gridField
2020-12-21 22:23:23 +01:00
* @ param string $actionName
* @ param array $data
2018-10-26 03:43:56 +02:00
* @ return void
2018-09-04 01:35:17 +02:00
*/
2016-11-29 00:31:16 +01:00
public function handleAction ( GridField $gridField , $actionName , $arguments , $data )
{
if ( ! $this -> checkDataType ( $gridField -> getList ())) {
return ;
}
2019-08-23 07:15:29 +02:00
$state = $this -> getState ( $gridField );
2021-01-21 01:47:40 +01:00
$state -> Columns = [];
2019-08-23 07:15:29 +02:00
2016-11-29 00:31:16 +01:00
if ( $actionName === 'filter' ) {
if ( isset ( $data [ 'filter' ][ $gridField -> getName ()])) {
foreach ( $data [ 'filter' ][ $gridField -> getName ()] as $key => $filter ) {
$state -> Columns -> $key = $filter ;
}
}
}
}
2019-08-23 07:15:29 +02:00
/**
* Extract state data from the parent gridfield
* @ param GridField $gridField
* @ return GridState_Data
*/
private function getState ( GridField $gridField ) : GridState_Data
{
return $gridField -> State -> GridFieldFilterHeader ;
}
public function initDefaultState ( GridState_Data $data ) : void
{
$data -> GridFieldFilterHeader -> initDefaults ([ 'Columns' => []]);
}
2016-11-29 00:31:16 +01:00
/**
2018-09-04 01:35:17 +02:00
* @ inheritDoc
2016-11-29 00:31:16 +01:00
*/
public function getManipulatedData ( GridField $gridField , SS_List $dataList )
{
if ( ! $this -> checkDataType ( $dataList )) {
return $dataList ;
}
/** @var Filterable $dataList */
2019-08-23 07:15:29 +02:00
/** @var array $filterArguments */
$filterArguments = $this -> getState ( $gridField ) -> Columns -> toArray ();
if ( empty ( $filterArguments )) {
2016-11-29 00:31:16 +01:00
return $dataList ;
}
$dataListClone = clone ( $dataList );
2018-10-26 03:43:56 +02:00
$results = $this -> getSearchContext ( $gridField )
-> getQuery ( $filterArguments , false , false , $dataListClone );
return $results ;
2016-11-29 00:31:16 +01:00
}
2016-12-13 02:18:10 +01:00
/**
2018-09-04 01:35:17 +02:00
* Returns whether this { @ link GridField } has any columns to filter on at all
2016-12-13 02:18:10 +01:00
*
2017-01-11 00:00:01 +01:00
* @ param GridField $gridField
2016-12-13 02:18:10 +01:00
* @ return boolean
*/
2018-09-24 13:39:33 +02:00
public function canFilterAnyColumns ( $gridField )
2016-12-13 02:18:10 +01:00
{
$list = $gridField -> getList ();
if ( ! $this -> checkDataType ( $list )) {
return false ;
}
$columns = $gridField -> getColumns ();
foreach ( $columns as $columnField ) {
$metadata = $gridField -> getColumnMetadata ( $columnField );
$title = $metadata [ 'title' ];
if ( $title && $list -> canFilterBy ( $columnField )) {
return true ;
}
}
return false ;
}
2018-09-04 01:35:17 +02:00
/**
* Generate a search context based on the model class of the of the GridField
*
* @ param GridField $gridfield
* @ return \SilverStripe\ORM\Search\SearchContext
*/
public function getSearchContext ( GridField $gridField )
2016-11-29 00:31:16 +01:00
{
2018-09-28 02:31:23 +02:00
if ( ! $this -> searchContext ) {
$this -> searchContext = singleton ( $gridField -> getModelClass ()) -> getDefaultSearchContext ();
if ( $this -> updateSearchContextCallback ) {
call_user_func ( $this -> updateSearchContextCallback , $this -> searchContext );
}
}
2018-09-04 01:35:17 +02:00
2018-09-28 02:31:23 +02:00
return $this -> searchContext ;
2018-09-04 01:35:17 +02:00
}
/**
* Returns the search field schema for the component
*
* @ param GridField $gridfield
* @ return string
*/
public function getSearchFieldSchema ( GridField $gridField )
{
$schemaUrl = Controller :: join_links ( $gridField -> Link (), 'schema/SearchForm' );
2019-11-26 23:38:38 +01:00
$inst = singleton ( $gridField -> getModelClass ());
2018-09-04 01:35:17 +02:00
$context = $this -> getSearchContext ( $gridField );
$params = $gridField -> getRequest () -> postVar ( 'filter' ) ? : [];
2022-04-14 03:12:59 +02:00
if ( array_key_exists ( $gridField -> getName (), $params ? ? [])) {
2018-09-04 01:35:17 +02:00
$params = $params [ $gridField -> getName ()];
}
2018-09-28 02:31:23 +02:00
if ( $context -> getSearchParams ()) {
$params = array_merge ( $context -> getSearchParams (), $params );
}
2018-09-04 01:35:17 +02:00
$context -> setSearchParams ( $params );
2019-11-26 23:38:38 +01:00
$searchField = $this -> getSearchField () ? : $inst -> config () -> get ( 'general_search_field' );
if ( ! $searchField ) {
$searchField = $context -> getSearchFields () -> first ();
$searchField = $searchField && property_exists ( $searchField , 'name' ) ? $searchField -> name : null ;
}
2018-09-04 01:35:17 +02:00
2019-11-26 23:38:38 +01:00
$name = $gridField -> Title ? : $inst -> i18n_plural_name ();
2018-09-04 01:35:17 +02:00
2018-10-01 02:38:32 +02:00
// Prefix "Search__" onto the filters for the React component
$filters = $context -> getSearchParams ();
if ( ! $this -> useLegacyFilterHeader && ! empty ( $filters )) {
$filters = array_combine ( array_map ( function ( $key ) {
return 'Search__' . $key ;
2022-04-14 03:12:59 +02:00
}, array_keys ( $filters ? ? [])), $filters ? ? []);
2018-10-01 02:38:32 +02:00
}
2018-11-22 01:05:43 +01:00
$searchAction = GridField_FormAction :: create ( $gridField , 'filter' , false , 'filter' , null );
$clearAction = GridField_FormAction :: create ( $gridField , 'reset' , false , 'reset' , null );
2018-09-04 01:35:17 +02:00
$schema = [
'formSchemaUrl' => $schemaUrl ,
'name' => $searchField ,
'placeholder' => _t ( __CLASS__ . '.Search' , 'Search "{name}"' , [ 'name' => $name ]),
2018-10-01 02:38:32 +02:00
'filters' => $filters ? : new \stdClass , // stdClass maps to empty json object '{}'
2018-09-04 01:35:17 +02:00
'gridfield' => $gridField -> getName (),
2018-11-22 01:05:43 +01:00
'searchAction' => $searchAction -> getAttribute ( 'name' ),
'searchActionState' => $searchAction -> getAttribute ( 'data-action-state' ),
'clearAction' => $clearAction -> getAttribute ( 'name' ),
'clearActionState' => $clearAction -> getAttribute ( 'data-action-state' ),
2018-09-04 01:35:17 +02:00
];
2018-10-28 22:06:04 +01:00
return json_encode ( $schema );
2018-09-04 01:35:17 +02:00
}
/**
2018-09-28 02:31:23 +02:00
* Returns the search form for the component
2018-09-04 01:35:17 +02:00
*
2018-09-28 02:31:23 +02:00
* @ param GridField $gridField
* @ return Form | null
2018-09-04 01:35:17 +02:00
*/
2018-09-28 02:31:23 +02:00
public function getSearchForm ( GridField $gridField )
2018-09-04 01:35:17 +02:00
{
$searchContext = $this -> getSearchContext ( $gridField );
$searchFields = $searchContext -> getSearchFields ();
if ( $searchFields -> count () === 0 ) {
2018-09-28 02:31:23 +02:00
return null ;
}
if ( $this -> searchForm ) {
return $this -> searchForm ;
2018-09-04 01:35:17 +02:00
}
2018-09-27 07:39:50 +02:00
// Append a prefix to search field names to prevent conflicts with other fields in the search form
foreach ( $searchFields as $field ) {
$field -> setName ( 'Search__' . $field -> getName ());
}
2018-09-04 01:35:17 +02:00
$columns = $gridField -> getColumns ();
// Update field titles to match column titles
foreach ( $columns as $columnField ) {
$metadata = $gridField -> getColumnMetadata ( $columnField );
// Get the field name, without any modifications
2022-04-14 03:12:59 +02:00
$name = explode ( '.' , $columnField ? ? '' );
2018-09-04 01:35:17 +02:00
$title = $metadata [ 'title' ];
$field = $searchFields -> fieldByName ( $name [ 0 ]);
if ( $field ) {
$field -> setTitle ( $title );
}
}
foreach ( $searchFields -> getIterator () as $field ) {
2022-07-04 01:45:30 +02:00
$field -> addExtraClass ( 'stacked no-change-track' );
2018-09-04 01:35:17 +02:00
}
2018-10-26 03:43:56 +02:00
$name = $gridField -> Title ? : singleton ( $gridField -> getModelClass ()) -> i18n_plural_name ();
2018-09-28 02:31:23 +02:00
$this -> searchForm = $form = new Form (
2018-09-04 01:35:17 +02:00
$gridField ,
2018-10-26 03:43:56 +02:00
$name . " SearchForm " ,
2018-09-04 01:35:17 +02:00
$searchFields ,
new FieldList ()
);
2018-10-01 02:38:32 +02:00
2018-09-04 01:35:17 +02:00
$form -> setFormMethod ( 'get' );
$form -> setFormAction ( $gridField -> Link ());
$form -> addExtraClass ( 'cms-search-form form--no-dividers' );
$form -> disableSecurityToken (); // This form is not tied to session so we disable this
2018-09-28 02:31:23 +02:00
$form -> loadDataFrom ( $searchContext -> getSearchParams ());
if ( $this -> updateSearchFormCallback ) {
call_user_func ( $this -> updateSearchFormCallback , $form );
}
return $this -> searchForm ;
}
/**
* Returns the search form schema for the component
*
* @ param GridField $gridfield
* @ return HTTPResponse
*/
public function getSearchFormSchema ( GridField $gridField )
{
$form = $this -> getSearchForm ( $gridField );
// If there are no filterable fields, return a 400 response
if ( ! $form ) {
return new HTTPResponse ( _t ( __CLASS__ . '.SearchFormFaliure' , 'No search form could be generated' ), 400 );
}
2018-09-04 01:35:17 +02:00
$parts = $gridField -> getRequest () -> getHeader ( LeftAndMain :: SCHEMA_HEADER );
$schemaID = $gridField -> getRequest () -> getURL ();
$data = FormSchema :: singleton ()
-> getMultipartSchema ( $parts , $schemaID , $form );
2018-10-28 22:06:04 +01:00
$response = new HTTPResponse ( json_encode ( $data ));
2018-09-04 01:35:17 +02:00
$response -> addHeader ( 'Content-Type' , 'application/json' );
return $response ;
}
/**
* Generate fields for the legacy filter header row
*
2022-10-13 03:49:15 +02:00
* @ deprecated 4.12 . 0 Use search field instead
2018-09-04 01:35:17 +02:00
* @ param GridField $gridfield
* @ return ArrayList | null
*/
public function getLegacyFilterHeader ( GridField $gridField )
{
2022-10-13 03:49:15 +02:00
Deprecation :: notice ( '4.12.0' , 'Use search field instead' );
2018-09-04 01:35:17 +02:00
2016-11-29 00:31:16 +01:00
$list = $gridField -> getList ();
if ( ! $this -> checkDataType ( $list )) {
return null ;
}
$columns = $gridField -> getColumns ();
2019-08-23 07:15:29 +02:00
$filterArguments = $this -> getState ( $gridField ) -> Columns -> toArray ();
2016-11-29 00:31:16 +01:00
$currentColumn = 0 ;
2016-12-13 02:18:10 +01:00
$canFilter = false ;
2018-09-04 01:35:17 +02:00
$fieldsList = new ArrayList ();
2016-12-13 02:18:10 +01:00
2016-11-29 00:31:16 +01:00
foreach ( $columns as $columnField ) {
$currentColumn ++ ;
$metadata = $gridField -> getColumnMetadata ( $columnField );
$title = $metadata [ 'title' ];
$fields = new FieldGroup ();
if ( $title && $list -> canFilterBy ( $columnField )) {
2016-12-13 02:18:10 +01:00
$canFilter = true ;
2016-11-29 00:31:16 +01:00
$value = '' ;
if ( isset ( $filterArguments [ $columnField ])) {
$value = $filterArguments [ $columnField ];
}
$field = new TextField ( 'filter[' . $gridField -> getName () . '][' . $columnField . ']' , '' , $value );
$field -> addExtraClass ( 'grid-field__sort-field' );
$field -> addExtraClass ( 'no-change-track' );
$field -> setAttribute (
'placeholder' ,
2018-01-16 19:39:30 +01:00
_t ( 'SilverStripe\\Forms\\GridField\\GridField.FilterBy' , " Filter by " ) . _t ( 'SilverStripe\\Forms\\GridField\\GridField.' . $metadata [ 'title' ], $metadata [ 'title' ])
2016-11-29 00:31:16 +01:00
);
$fields -> push ( $field );
$fields -> push (
GridField_FormAction :: create ( $gridField , 'reset' , false , 'reset' , null )
2016-12-13 02:18:10 +01:00
-> addExtraClass ( 'btn font-icon-cancel btn-secondary btn--no-text ss-gridfield-button-reset' )
2017-04-20 03:15:24 +02:00
-> setAttribute ( 'title' , _t ( 'SilverStripe\\Forms\\GridField\\GridField.ResetFilter' , " Reset " ))
2016-11-29 00:31:16 +01:00
-> setAttribute ( 'id' , 'action_reset_' . $gridField -> getModelClass () . '_' . $columnField )
);
}
2022-04-14 03:12:59 +02:00
if ( $currentColumn == count ( $columns ? ? [])) {
2016-11-29 00:31:16 +01:00
$fields -> push (
GridField_FormAction :: create ( $gridField , 'filter' , false , 'filter' , null )
-> addExtraClass ( 'btn font-icon-search btn--no-text btn--icon-large grid-field__filter-submit ss-gridfield-button-filter' )
2017-04-20 03:15:24 +02:00
-> setAttribute ( 'title' , _t ( 'SilverStripe\\Forms\\GridField\\GridField.Filter' , 'Filter' ))
2016-11-29 00:31:16 +01:00
-> setAttribute ( 'id' , 'action_filter_' . $gridField -> getModelClass () . '_' . $columnField )
);
$fields -> push (
GridField_FormAction :: create ( $gridField , 'reset' , false , 'reset' , null )
2016-12-20 22:43:33 +01:00
-> addExtraClass ( 'btn font-icon-cancel btn--no-text grid-field__filter-clear btn--icon-md ss-gridfield-button-close' )
2017-04-20 03:15:24 +02:00
-> setAttribute ( 'title' , _t ( 'SilverStripe\\Forms\\GridField\\GridField.ResetFilter' , " Reset " ))
2016-11-29 00:31:16 +01:00
-> setAttribute ( 'id' , 'action_reset_' . $gridField -> getModelClass () . '_' . $columnField )
);
2017-04-21 05:30:09 +02:00
$fields -> addExtraClass ( 'grid-field__filter-buttons' );
2016-11-29 00:31:16 +01:00
$fields -> addExtraClass ( 'no-change-track' );
}
2018-09-04 01:35:17 +02:00
$fieldsList -> push ( $fields );
2016-11-29 00:31:16 +01:00
}
2018-09-04 01:35:17 +02:00
return $canFilter ? $fieldsList : null ;
}
/**
* Either returns the legacy filter header or the search button and field
*
* @ param GridField $gridField
* @ return array | null
*/
public function getHTMLFragments ( $gridField )
{
$forTemplate = new ArrayData ([]);
if ( ! $this -> canFilterAnyColumns ( $gridField )) {
2016-12-13 02:18:10 +01:00
return null ;
}
2018-09-04 01:35:17 +02:00
if ( $this -> useLegacyFilterHeader ) {
2022-11-15 06:20:54 +01:00
$fieldsList = Deprecation :: withNoReplacement ( function () use ( $gridField ) {
return $this -> getLegacyFilterHeader ( $gridField );
});
2018-09-04 01:35:17 +02:00
$forTemplate -> Fields = $fieldsList ;
$filterTemplates = SSViewer :: get_templates_by_class ( $this , '_Row' , __CLASS__ );
return [ 'header' => $forTemplate -> renderWith ( $filterTemplates )];
} else {
$fieldSchema = $this -> getSearchFieldSchema ( $gridField );
$forTemplate -> SearchFieldSchema = $fieldSchema ;
$searchTemplates = SSViewer :: get_templates_by_class ( $this , '_Search' , __CLASS__ );
return [
'before' => $forTemplate -> renderWith ( $searchTemplates ),
'buttons-before-right' => sprintf (
'<button type="button" name="showFilter" aria-label="%s" title="%s"' .
' class="btn btn-secondary font-icon-search btn--no-text btn--icon-large grid-field__filter-open"></button>' ,
_t ( 'SilverStripe\\Forms\\GridField\\GridField.OpenFilter' , " Open search and filter " ),
_t ( 'SilverStripe\\Forms\\GridField\\GridField.OpenFilter' , " Open search and filter " )
)
];
}
2016-11-29 00:31:16 +01:00
}
2012-02-11 03:26:26 +01:00
}