Skip to content

Commit

Permalink
API Refactor SiteTree filtering to work like GridField filters
Browse files Browse the repository at this point in the history
CMSMain uses an entirely separate code path for building the filter form
and for the actual filter functionality compared with the gridfield
filter used literally everywhere else in the CMS.
This aims to unify the two as much as possible, so that filtering pages
in CMSMain is the same as filtering everywhere else.
  • Loading branch information
GuySartorelli committed Jan 31, 2025
1 parent 7a7755c commit 7d991c5
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 254 deletions.
142 changes: 37 additions & 105 deletions code/Controllers/CMSMain.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use SilverStripe\CMS\Forms\CMSMainAddForm;
use SilverStripe\CMS\Model\CurrentRecordIdentifier;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Search\SearchForm;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
Expand Down Expand Up @@ -61,6 +60,7 @@
use SilverStripe\ORM\Hierarchy\MarkedSet;
use SilverStripe\Model\List\SS_List;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Security\InheritedPermissions;
use SilverStripe\Security\PermissionProvider;
use SilverStripe\Security\Security;
Expand Down Expand Up @@ -903,18 +903,30 @@ public function getSearchFieldSchema(): string

$singleton = DataObject::singleton($this->getModelClass());
$context = $singleton->getDefaultSearchContext();

// This logic poached directly from GridFieldFilterHeader
$searchField = $singleton::config()->get('general_search_field');
if (!$searchField) {
$searchField = $context->getSearchFields()->first();
$searchField = $searchField && property_exists($searchField, 'name') ? $searchField->name : null;
}

$params = $this->getRequest()->requestVar('q') ?: [];
$context->setSearchParams($params);

$placeholder = _t(SearchForm::class . '.FILTERLABELTEXT2', 'Search "{model}"', ['model' => $singleton->i18n_plural_name()]);
$placeholder = _t(__CLASS__ . '.FILTERLABELTEXT', 'Search "{model}"', ['model' => $singleton->i18n_plural_name()]);
$searchParams = $context->getSearchParams();
$searchParams = array_combine(array_map(function ($key) {
return 'Search__' . $key;
}, array_keys($searchParams ?? [])), $searchParams ?? []);

// Prefix "Search__" onto the search params to match the field names in the actual form
if (!empty($searchParams)) {
$searchParams = array_combine(array_map(function ($key) {
return 'Search__' . $key;
}, array_keys($searchParams ?? [])), $searchParams ?? []);
}

$schema = [
'formSchemaUrl' => $schemaUrl,
'name' => 'Term',
'name' => $searchField,
'placeholder' => $placeholder,
'filters' => $searchParams ?: new \stdClass // stdClass maps to empty json object '{}'
];
Expand All @@ -929,62 +941,13 @@ public function getSearchFieldSchema(): string
*/
public function getSearchForm(): Form
{
$modelClass = $this->getModelClass();
$singleton = DataObject::singleton($modelClass);
// Create the fields
$dateFrom = DateField::create(
'Search__LastEditedFrom',
_t(SearchForm::class . '.FILTERDATEFROM', 'From')
)->setLocale(Security::getCurrentUser()->Locale);
$dateTo = DateField::create(
'Search__LastEditedTo',
_t(SearchForm::class . '.FILTERDATETO', 'To')
)->setLocale(Security::getCurrentUser()->Locale);
$filters = CMSSiteTreeFilter::get_all_filters();
// Remove 'All records' as we set that to empty/default value
unset($filters[CMSSiteTreeFilter_Search::class]);
$recordFilter = DropdownField::create(
'Search__FilterClass',
_t(SearchForm::class . '.RECORD_STATUS', '{model} status', ['model' => $singleton->i18n_singular_name()]),
$filters
);
$recordFilter->setEmptyString(_t(
SearchForm::class . '.RECORDS_ALLOPT',
'All {model}',
['model' => mb_strtolower($singleton->i18n_plural_name())]
));
$classes = DropdownField::create(
'Search__ClassName',
_t(
SearchForm::class . '.RECORD_TYPEOPT',
'{model} type',
'Dropdown for limiting search to a record type',
['model' => $singleton->i18n_singular_name()]
),
$this->getRecordTypes()
);
$classes->setEmptyString(_t(SearchForm::class . '.RECORD_TYPEANYOPT', 'Any'));

// Group the Datefields
$dateGroup = FieldGroup::create(
_t(SearchForm::class . '.RECORD_FILTERDATEHEADING', 'Last edited'),
[$dateFrom, $dateTo]
)->setName('Search__LastEdited')
->addExtraClass('fieldgroup--fill-width');

// Create the Field list
$fields = new FieldList(
$recordFilter,
$classes,
$dateGroup
);

// Create the form
$fields = DataObject::singleton($this->getModelClass())->scaffoldSearchFields();
$this->addSearchPrefixToFields($fields);
$form = Form::create(
$this,
'SearchForm',
$fields,
new FieldList()
$fields
);
$form->addExtraClass('cms-search-form');
$form->setFormMethod('GET');
Expand All @@ -1001,6 +964,19 @@ public function getSearchForm(): Form
return $form;
}

/**
* Append a prefix to search field names to prevent conflicts with other fields in the search form
*/
private function addSearchPrefixToFields(FieldList $fields): void
{
foreach ($fields as $field) {
$field->setName('Search__' . $field->getName());
if ($field instanceof CompositeField) {
$this->addSearchPrefixToFields($field->getChildren());
}
}
}

/**
* Returns a sorted array suitable for a dropdown with classes and their localised name
*/
Expand Down Expand Up @@ -1658,58 +1634,15 @@ public function childfilter(HTTPRequest $request): HTTPResponse
->setBody(json_encode($disallowedChildren));
}

/**
* Safely reconstruct a selected filter from a given set of query parameters
*
* @param array $params Query parameters to use, or null if none present
* @return CMSSiteTreeFilter The filter class
* @throws InvalidArgumentException if invalid filter class is passed.
*/
protected function getQueryFilter($params)
{
if (empty($params['FilterClass'])) {
return null;
}
$filterClass = $params['FilterClass'];
if (!is_subclass_of($filterClass, CMSSiteTreeFilter::class)) {
throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
}
return $filterClass::create($params);
}

/**
* Returns the records meet a certain criteria as {@see CMSSiteTreeFilter} or the subrecords of a parent record
* defaulting to no filter and show all records in first level.
* Doubles as search results, if any search parameters are set through {@link SearchForm()}.
*
* @param array $params Search filter criteria
* @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
* @return SS_List
* @throws InvalidArgumentException if invalid filter class is passed.
*/
public function getList($params = [], $parentID = 0)
{
if ($filter = $this->getQueryFilter($params)) {
return $filter->getFilteredPages();
} else {
$list = DataObject::get($this->getModelClass());
$parentID = is_numeric($parentID) ? $parentID : 0;
return $list->filter("ParentID", $parentID);
}
}

/**
* @return Form
*/
public function ListViewForm()
{
$params = $this->getRequest()->requestVar('q');
$params = $this->getRequest()->requestVar('q') ?? [];
$parentID = $this->getRequest()->requestVar('ParentID');
// Set default filter if other params are set
if ($params && empty($params['FilterClass'])) {
$params['FilterClass'] = CMSSiteTreeFilter_Search::class;
}
$list = $this->getList($params, $parentID);
$modelClass = $this->getModelClass();
$list = DataObject::singleton($modelClass)->getDefaultSearchContext()->getQuery($params);
$gridFieldConfig = GridFieldConfig::create()->addComponents(
Injector::inst()->create(GridFieldSortableHeader::class),
Injector::inst()->create(GridFieldDataColumns::class),
Expand All @@ -1729,7 +1662,6 @@ public function ListViewForm()
$columns = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);

// Set up columns and sorting for list view GridField
$modelClass = $this->getModelClass();
$fields = [
'getTreeTitle' => _t($modelClass . '.TREETITLE', 'Title'),
'i18n_singular_name' => _t($modelClass . '.TREETYPE', 'Record Type'),
Expand Down
78 changes: 1 addition & 77 deletions code/Controllers/CMSSiteTreeFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
use SilverStripe\Admin\LeftAndMain_SearchFilter;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Forms\DateField;
use SilverStripe\ORM\DataList;
use SilverStripe\Model\List\SS_List;
use SilverStripe\Versioned\Versioned;
Expand All @@ -27,14 +25,6 @@ abstract class CMSSiteTreeFilter implements LeftAndMain_SearchFilter
{
use Injectable;

/**
* Search parameters, mostly properties on {@link SiteTree}.
* Caution: Unescaped data.
*
* @var array
*/
protected $params = [];

/**
* List of filtered items and all their parents
*
Expand Down Expand Up @@ -96,13 +86,6 @@ public static function get_all_filters()
return $filterMap;
}

public function __construct($params = null)
{
if ($params) {
$this->params = $params;
}
}

public function getChildrenMethod()
{
return $this->childrenMethod;
Expand Down Expand Up @@ -131,9 +114,8 @@ public function getRecordClasses($page)
* Gets the list of filtered pages
*
* @see {@link ModelData::getStatusFlags()}
* @return SS_List
*/
abstract public function getFilteredPages();
abstract public function getFilteredPages(DataList $existingQuery = null): DataList;

/**
* @return array Map of Page IDs to their respective ParentID values.
Expand Down Expand Up @@ -187,64 +169,6 @@ public function isRecordIncluded($page)
return !empty($this->_cache_ids[$page->ID]);
}

/**
* Applies the default filters to a specified DataList of pages
*
* @param DataList $query Unfiltered query
* @return DataList Filtered query
*/
protected function applyDefaultFilters($query)
{
$sng = SiteTree::singleton();
foreach ($this->params as $name => $val) {
if (empty($val)) {
continue;
}

switch ($name) {
case 'Term':
$query = $query->filterAny([
'URLSegment:PartialMatch' => Convert::raw2url($val),
'Title:PartialMatch' => $val,
'MenuTitle:PartialMatch' => $val,
'Content:PartialMatch' => $val
]);
break;

case 'URLSegment':
$query = $query->filter([
'URLSegment:PartialMatch' => Convert::raw2url($val),
]);
break;

case 'LastEditedFrom':
$fromDate = new DateField(null, null, $val);
$query = $query->filter("LastEdited:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
break;

case 'LastEditedTo':
$toDate = new DateField(null, null, $val);
$query = $query->filter("LastEdited:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
break;

case 'ClassName':
if ($val != 'All') {
$query = $query->filter('ClassName', $val);
}
break;

default:
$field = $sng->dbObject($name);
if ($field) {
$filter = $field->defaultSearchFilter();
$filter->setValue($val);
$query = $query->alterDataQuery([$filter, 'apply']);
}
}
}
return $query;
}

/**
* Maps a list of pages to an array of associative arrays with ID and ParentID keys
*
Expand Down
19 changes: 11 additions & 8 deletions code/Controllers/CMSSiteTreeFilter_ChangedPages.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace SilverStripe\CMS\Controllers;

use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataList;
use SilverStripe\Versioned\Versioned;

/**
Expand All @@ -17,14 +17,17 @@ public static function title()
return _t(__CLASS__ . '.Title', "Modified pages");
}

public function getFilteredPages()
public function getFilteredPages(DataList $list = null): DataList
{
$table = DataObject::singleton(SiteTree::class)->baseTable();
$liveTable = DataObject::singleton(SiteTree::class)->stageTable($table, Versioned::LIVE);
$pages = Versioned::get_by_stage(SiteTree::class, Versioned::DRAFT);
$pages = $this->applyDefaultFilters($pages)
->leftJoin($liveTable, "\"$liveTable\".\"ID\" = \"$table\".\"ID\"")
$table = SiteTree::singleton()->baseTable();
$liveTable = SiteTree::singleton()->stageTable($table, Versioned::LIVE);
if ($list) {
$list = Versioned::updateListForStage($list, Versioned::DRAFT);
} else {
$list = Versioned::get_by_stage(SiteTree::class, Versioned::DRAFT);
}
$list = $list->leftJoin($liveTable, "\"$liveTable\".\"ID\" = \"$table\".\"ID\"")
->where("\"$table\".\"Version\" <> \"$liveTable\".\"Version\"");
return $pages;
return $list;
}
}
10 changes: 6 additions & 4 deletions code/Controllers/CMSSiteTreeFilter_DeletedPages.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace SilverStripe\CMS\Controllers;

use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\ORM\DataList;
use SilverStripe\Versioned\Versioned;

/**
Expand All @@ -28,10 +29,11 @@ public static function title()
return _t(__CLASS__ . '.Title', "All pages, including archived");
}

public function getFilteredPages()
public function getFilteredPages(DataList $list = null): DataList
{
$pages = Versioned::get_including_deleted(SiteTree::class);
$pages = $this->applyDefaultFilters($pages);
return $pages;
if ($list) {
return Versioned::updateListToIncludeDeleted($list);
}
return Versioned::get_including_deleted(SiteTree::class);
}
}
Loading

0 comments on commit 7d991c5

Please sign in to comment.