'getSearchFormSchema' ]; } /** * @param bool $useLegacy This will be removed in 5.0 * @param callable|null $updateSearchContext This will be removed in 5.0 * @param callable|null $updateSearchForm This will be removed in 5.0 */ public function __construct( $useLegacy = false, callable $updateSearchContext = null, callable $updateSearchForm = null ) { $forceLegacy = Deprecation::withNoReplacement(function () { return Config::inst()->get(self::class, 'force_legacy'); }); $this->useLegacyFilterHeader = $forceLegacy || $useLegacy; $this->updateSearchContextCallback = $updateSearchContext; $this->updateSearchFormCallback = $updateSearchForm; } /** * 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; } public function getSearchField(): ?string { return $this->searchField; } public function setSearchField(string $field): self { $this->searchField = $field; return $this; } /** * 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( static::class . " expects an SS_Filterable list to be passed to the GridField." ); } return false; } } /** * If the GridField has a filterable datalist, return an array of actions * * @param GridField $gridField * @return array */ public function getActions($gridField) { if (!$this->checkDataType($gridField->getList())) { return []; } return ['filter', 'reset']; } /** * If the GridField has a filterable datalist, return an array of actions * * @param GridField $gridField * @param string $actionName * @param array $data * @return void */ public function handleAction(GridField $gridField, $actionName, $arguments, $data) { if (!$this->checkDataType($gridField->getList())) { return; } $state = $this->getState($gridField); $state->Columns = []; if ($actionName === 'filter') { if (isset($data['filter'][$gridField->getName()])) { foreach ($data['filter'][$gridField->getName()] as $key => $filter) { $state->Columns->$key = $filter; } } } } /** * 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' => []]); } /** * @inheritDoc */ public function getManipulatedData(GridField $gridField, SS_List $dataList) { if (!$this->checkDataType($dataList)) { return $dataList; } /** @var Filterable $dataList */ /** @var array $filterArguments */ $filterArguments = $this->getState($gridField)->Columns->toArray(); if (empty($filterArguments)) { return $dataList; } $dataListClone = clone($dataList); $results = $this->getSearchContext($gridField) ->getQuery($filterArguments, false, false, $dataListClone); return $results; } /** * Returns whether this {@link GridField} has any columns to filter on at all * * @param GridField $gridField * @return boolean */ public function canFilterAnyColumns($gridField) { $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; } /** * 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) { if (!$this->searchContext) { $this->searchContext = singleton($gridField->getModelClass())->getDefaultSearchContext(); if ($this->updateSearchContextCallback) { call_user_func($this->updateSearchContextCallback, $this->searchContext); } } return $this->searchContext; } /** * 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'); $inst = singleton($gridField->getModelClass()); $context = $this->getSearchContext($gridField); $params = $gridField->getRequest()->postVar('filter') ?: []; if (array_key_exists($gridField->getName(), $params ?? [])) { $params = $params[$gridField->getName()]; } if ($context->getSearchParams()) { $params = array_merge($context->getSearchParams(), $params); } $context->setSearchParams($params); $searchField = $this->getSearchField() ?: $inst->config()->get('general_search_field'); if (!$searchField) { $searchField = $context->getSearchFields()->first(); $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 (!$this->useLegacyFilterHeader && !empty($filters)) { $filters = array_combine(array_map(function ($key) { return 'Search__' . $key; }, array_keys($filters ?? [])), $filters ?? []); } $searchAction = GridField_FormAction::create($gridField, 'filter', false, 'filter', null); $clearAction = GridField_FormAction::create($gridField, 'reset', false, 'reset', null); $schema = [ 'formSchemaUrl' => $schemaUrl, 'name' => $searchField, 'placeholder' => _t(__CLASS__ . '.Search', 'Search "{name}"', ['name' => $name]), 'filters' => $filters ?: new \stdClass, // stdClass maps to empty json object '{}' 'gridfield' => $gridField->getName(), 'searchAction' => $searchAction->getAttribute('name'), 'searchActionState' => $searchAction->getAttribute('data-action-state'), 'clearAction' => $clearAction->getAttribute('name'), 'clearActionState' => $clearAction->getAttribute('data-action-state'), ]; return json_encode($schema); } /** * Returns the search form for the component * * @param GridField $gridField * @return Form|null */ public function getSearchForm(GridField $gridField) { $searchContext = $this->getSearchContext($gridField); $searchFields = $searchContext->getSearchFields(); if ($searchFields->count() === 0) { return null; } if ($this->searchForm) { return $this->searchForm; } // 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()); } $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 $name = explode('.', $columnField ?? ''); $title = $metadata['title']; $field = $searchFields->fieldByName($name[0]); if ($field) { $field->setTitle($title); } } foreach ($searchFields->getIterator() as $field) { $field->addExtraClass('stacked no-change-track'); } $name = $gridField->Title ?: singleton($gridField->getModelClass())->i18n_plural_name(); $this->searchForm = $form = new Form( $gridField, $name . "SearchForm", $searchFields, new FieldList() ); $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 $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); } $parts = $gridField->getRequest()->getHeader(LeftAndMain::SCHEMA_HEADER); $schemaID = $gridField->getRequest()->getURL(); $data = FormSchema::singleton() ->getMultipartSchema($parts, $schemaID, $form); $response = new HTTPResponse(json_encode($data)); $response->addHeader('Content-Type', 'application/json'); return $response; } /** * Generate fields for the legacy filter header row * * @deprecated 4.12.0 Use search field instead * @param GridField $gridfield * @return ArrayList|null */ public function getLegacyFilterHeader(GridField $gridField) { Deprecation::notice('4.12.0', 'Use search field instead'); $list = $gridField->getList(); if (!$this->checkDataType($list)) { return null; } $columns = $gridField->getColumns(); $filterArguments = $this->getState($gridField)->Columns->toArray(); $currentColumn = 0; $canFilter = false; $fieldsList = new ArrayList(); foreach ($columns as $columnField) { $currentColumn++; $metadata = $gridField->getColumnMetadata($columnField); $title = $metadata['title']; $fields = new FieldGroup(); if ($title && $list->canFilterBy($columnField)) { $canFilter = true; $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', _t('SilverStripe\\Forms\\GridField\\GridField.FilterBy', "Filter by ") . _t('SilverStripe\\Forms\\GridField\\GridField.' . $metadata['title'], $metadata['title']) ); $fields->push($field); $fields->push( GridField_FormAction::create($gridField, 'reset', false, 'reset', null) ->addExtraClass('btn font-icon-cancel btn-secondary btn--no-text ss-gridfield-button-reset') ->setAttribute('title', _t('SilverStripe\\Forms\\GridField\\GridField.ResetFilter', "Reset")) ->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField) ); } if ($currentColumn == count($columns ?? [])) { $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') ->setAttribute('title', _t('SilverStripe\\Forms\\GridField\\GridField.Filter', 'Filter')) ->setAttribute('id', 'action_filter_' . $gridField->getModelClass() . '_' . $columnField) ); $fields->push( GridField_FormAction::create($gridField, 'reset', false, 'reset', null) ->addExtraClass('btn font-icon-cancel btn--no-text grid-field__filter-clear btn--icon-md ss-gridfield-button-close') ->setAttribute('title', _t('SilverStripe\\Forms\\GridField\\GridField.ResetFilter', "Reset")) ->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField) ); $fields->addExtraClass('grid-field__filter-buttons'); $fields->addExtraClass('no-change-track'); } $fieldsList->push($fields); } 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)) { return null; } if ($this->useLegacyFilterHeader) { $fieldsList = Deprecation::withNoReplacement(function () use ($gridField) { return $this->getLegacyFilterHeader($gridField); }); $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( '', _t('SilverStripe\\Forms\\GridField\\GridField.OpenFilter', "Open search and filter"), _t('SilverStripe\\Forms\\GridField\\GridField.OpenFilter', "Open search and filter") ) ]; } } }