diff --git a/code/Controllers/CMSMain.php b/code/Controllers/CMSMain.php index 28513ecd4d..b4fa8bb80a 100644 --- a/code/Controllers/CMSMain.php +++ b/code/Controllers/CMSMain.php @@ -2,7 +2,6 @@ namespace SilverStripe\CMS\Controllers; -use InvalidArgumentException; use LogicException; use Psr\SimpleCache\CacheInterface; use SilverStripe\Admin\AdminRootController; @@ -13,11 +12,9 @@ use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive; use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish; use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish; -use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search; 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; @@ -33,9 +30,6 @@ use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Manifest\ModuleResource; use SilverStripe\Core\Manifest\ModuleResourceLoader; -use SilverStripe\Forms\DateField; -use SilverStripe\Forms\DropdownField; -use SilverStripe\Forms\FieldGroup; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; use SilverStripe\Forms\FormAction; @@ -66,11 +60,10 @@ use SilverStripe\Security\Security; use SilverStripe\Security\SecurityToken; use SilverStripe\SiteConfig\SiteConfig; -use SilverStripe\Versioned\ChangeSet; -use SilverStripe\Versioned\ChangeSetItem; use SilverStripe\Versioned\Versioned; use SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController; use SilverStripe\Model\ArrayData; +use SilverStripe\ORM\Search\SearchContextForm; use SilverStripe\Security\Permission; use SilverStripe\Security\PermissionCheckable; use SilverStripe\Versioned\RecursivePublishable; @@ -135,7 +128,7 @@ class CMSMain extends LeftAndMain implements CurrentRecordIdentifier, Permission 'submit', 'EditForm', 'schema', - 'SearchForm', + 'getSearchForm', 'TreeAsUL', 'getshowdeletedsubtree', 'savetreenode', @@ -150,6 +143,7 @@ class CMSMain extends LeftAndMain implements CurrentRecordIdentifier, Permission private static array $url_handlers = [ 'EditForm/$ID' => 'EditForm', + 'GET SearchForm' => 'getSearchForm', ]; private static array $casting = [ @@ -496,11 +490,10 @@ public function AddForm(): CMSMainAddForm public function TreeAsUL() { $modelClass = $this->getModelClass(); - $filter = $this->getSearchFilter(); DataObject::singleton($modelClass)->prepopulateTreeDataCache(null, [ - 'childrenMethod' => $filter ? $filter->getChildrenMethod() : 'AllChildrenIncludingDeleted', - 'numChildrenMethod' => $filter ? $filter->getNumChildrenMethod() : 'numChildren', + 'childrenMethod' => 'AllChildrenIncludingDeleted', + 'numChildrenMethod' => 'numChildren', ]); $html = $this->getTreeFor($modelClass); @@ -530,21 +523,6 @@ public function getTreeFor( $nodeCountThreshold = null ) { $nodeCountThreshold = is_null($nodeCountThreshold) ? Config::inst()->get($className, 'node_threshold_total') : $nodeCountThreshold; - // Provide better defaults from filter - $filter = $this->getSearchFilter(); - if ($filter) { - if (!$childrenMethod) { - $childrenMethod = $filter->getChildrenMethod(); - } - if (!$numChildrenMethod) { - $numChildrenMethod = $filter->getNumChildrenMethod(); - } - if (!$filterFunction) { - $filterFunction = function ($node) use ($filter) { - return $filter->isRecordIncluded($node); - }; - } - } // Build set from node and begin marking $record = ($rootID) ? $this->getRecord($rootID) : null; @@ -627,15 +605,6 @@ public function getTreeNodeClasses(DataObject $node): string } } - // Get additional filter classes - $filter = $this->getSearchFilter(); - if ($filter && ($filterClasses = $filter->getRecordClasses($node))) { - if (is_array($filterClasses)) { - $filterClasses = implode(' ', $filterClasses); - } - $classes .= ' ' . $filterClasses; - } - return trim($classes ?? ''); } @@ -895,109 +864,16 @@ public function ExtraTreeTools(): string } /** - * Returns the search form schema for the current model + * Returns a Form for record searching for use in templates. */ - public function getSearchFieldSchema(): string + public function getSearchForm(): SearchContextForm { - $schemaUrl = $this->Link('schema/SearchForm'); - - $singleton = DataObject::singleton($this->getModelClass()); - $context = $singleton->getDefaultSearchContext(); + $context = DataObject::singleton($this->getModelClass())->getDefaultSearchContext(); $params = $this->getRequest()->requestVar('q') ?: []; $context->setSearchParams($params); - - $placeholder = _t(SearchForm::class . '.FILTERLABELTEXT2', 'Search "{model}"', ['model' => $singleton->i18n_plural_name()]); - $searchParams = $context->getSearchParams(); - $searchParams = array_combine(array_map(function ($key) { - return 'Search__' . $key; - }, array_keys($searchParams ?? [])), $searchParams ?? []); - - $schema = [ - 'formSchemaUrl' => $schemaUrl, - 'name' => 'Term', - 'placeholder' => $placeholder, - 'filters' => $searchParams ?: new \stdClass // stdClass maps to empty json object '{}' - ]; - - return json_encode($schema); - } - - /** - * Returns a Form for record searching for use in templates. - * - * Can be modified from a decorator by a 'updateSearchForm' method - */ - 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 - $form = Form::create( - $this, - 'SearchForm', - $fields, - new FieldList() - ); - $form->addExtraClass('cms-search-form'); - $form->setFormMethod('GET'); - $form->setFormAction(CMSMain::singleton()->Link()); - $form->disableSecurityToken(); - $form->unsetValidator(); - - // Load the form with previously sent search data - $form->loadDataFrom($this->getRequest()->getVars()); - + $form = SearchContextForm::create($this, $context); // Allow decorators to modify the form $this->extend('updateSearchForm', $form); - return $form; } @@ -1658,58 +1534,21 @@ 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'); + $modelClass = $this->getModelClass(); $parentID = $this->getRequest()->requestVar('ParentID'); - // Set default filter if other params are set - if ($params && empty($params['FilterClass'])) { - $params['FilterClass'] = CMSSiteTreeFilter_Search::class; + $params = $this->getRequest()->requestVar('q') ?? []; + if ($params) { + $form = $this->getSearchForm(); + $params = $form->prepareValuesForSearchContext($params); + $list = DataObject::singleton($modelClass)->getDefaultSearchContext()->getQuery($params); + } else { + $list = DataObject::singleton($modelClass)::get()->filter('ParentID', is_numeric($parentID) ? $parentID : 0); } - $list = $this->getList($params, $parentID); $gridFieldConfig = GridFieldConfig::create()->addComponents( Injector::inst()->create(GridFieldSortableHeader::class), Injector::inst()->create(GridFieldDataColumns::class), @@ -1729,7 +1568,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'), diff --git a/code/Controllers/CMSSiteTreeFilter.php b/code/Controllers/CMSSiteTreeFilter.php index ca77dbcba4..ca65c4bb26 100644 --- a/code/Controllers/CMSSiteTreeFilter.php +++ b/code/Controllers/CMSSiteTreeFilter.php @@ -2,77 +2,23 @@ namespace SilverStripe\CMS\Controllers; -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; /** - * Base class for filtering the subtree for certain node statuses. - * - * The simplest way of building a CMSSiteTreeFilter is to create a pagesToBeShown() method that - * returns an Iterator of maps, each entry containing the 'ID' and 'ParentID' of the pages to be - * included in the tree. The result of a DB::query() can then be returned directly. - * - * If you wish to make a more complex tree, you can overload includeInTree($page) to return true/ - * false depending on whether the given page should be included. Note that you will need to include - * parent helper pages yourself. + * Base class for filtering againt SiteTree for certain statuses (e.g. archived or draft only). */ -abstract class CMSSiteTreeFilter implements LeftAndMain_SearchFilter +abstract class CMSSiteTreeFilter { use Injectable; - /** - * Search parameters, mostly properties on {@link SiteTree}. - * Caution: Unescaped data. - * - * @var array - */ - protected $params = []; - - /** - * List of filtered items and all their parents - * - * @var array - */ - protected $_cache_ids = null; - - - /** - * Subset of $_cache_ids which include only items that appear directly in search results. - * When highlighting these, item IDs in this subset should be visually distinguished from - * others in the complete set. - * - * @var array - */ - protected $_cache_highlight_ids = null; - - /** - * @var array - */ - protected $_cache_expanded = []; - - /** - * @var string - */ - protected $childrenMethod = null; - - /** - * @var string - */ - protected $numChildrenMethod = 'numChildren'; - /** * Returns a sorted array of all implementators of CMSSiteTreeFilter, suitable for use in a dropdown. * - * @return array + * @return array */ - public static function get_all_filters() + public static function get_all_filters(): array { // get all filter instances $filters = ClassInfo::subclassesFor(CMSSiteTreeFilter::class); @@ -96,169 +42,13 @@ public static function get_all_filters() return $filterMap; } - public function __construct($params = null) - { - if ($params) { - $this->params = $params; - } - } - - public function getChildrenMethod() - { - return $this->childrenMethod; - } - - public function getNumChildrenMethod() - { - return $this->numChildrenMethod; - } - - public function getRecordClasses($page) - { - if ($this->_cache_ids === null) { - $this->populateIDs(); - } - - // If directly selected via filter, apply highlighting - if (!empty($this->_cache_highlight_ids[$page->ID])) { - return 'filtered-item'; - } - - return null; - } - - /** - * Gets the list of filtered pages - * - * @see {@link ModelData::getStatusFlags()} - * @return SS_List - */ - abstract public function getFilteredPages(); - /** - * @return array Map of Page IDs to their respective ParentID values. + * Get a title for this filter to display to the user (e.g. in a dropdown field). */ - public function pagesIncluded() - { - return $this->mapIDs($this->getFilteredPages()); - } + abstract public static function title(): string; /** - * Populate the IDs of the pages returned by pagesIncluded(), also including - * the necessary parent helper pages. + * Gets the list of filtered pages based on an existing list. */ - protected function populateIDs() - { - $parents = []; - $this->_cache_ids = []; - $this->_cache_highlight_ids = []; - - if ($pages = $this->pagesIncluded()) { - // And keep a record of parents we don't need to get - // parents of themselves, as well as IDs to mark - foreach ($pages as $pageArr) { - $parents[$pageArr['ParentID']] = true; - $this->_cache_ids[$pageArr['ID']] = true; - $this->_cache_highlight_ids[$pageArr['ID']] = true; - } - - while (!empty($parents)) { - $q = Versioned::get_including_deleted(SiteTree::class) - ->byIDs(array_keys($parents ?? [])); - $list = $q->map('ID', 'ParentID'); - $parents = []; - foreach ($list as $id => $parentID) { - if ($parentID) { - $parents[$parentID] = true; - } - $this->_cache_ids[$id] = true; - $this->_cache_expanded[$id] = true; - } - } - } - } - - public function isRecordIncluded($page) - { - if ($this->_cache_ids === null) { - $this->populateIDs(); - } - - 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 - * - * @param SS_List $pages - * @return array - */ - protected function mapIDs($pages) - { - $ids = []; - if ($pages) { - foreach ($pages as $page) { - $ids[] = ['ID' => $page->ID, 'ParentID' => $page->ParentID]; - } - } - return $ids; - } + abstract public function getFilteredPages(DataList $list): DataList; } diff --git a/code/Controllers/CMSSiteTreeFilter_ChangedPages.php b/code/Controllers/CMSSiteTreeFilter_ChangedPages.php index 5d180eccc9..4eca081a30 100644 --- a/code/Controllers/CMSSiteTreeFilter_ChangedPages.php +++ b/code/Controllers/CMSSiteTreeFilter_ChangedPages.php @@ -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; /** @@ -11,20 +11,18 @@ */ class CMSSiteTreeFilter_ChangedPages extends CMSSiteTreeFilter { - - public static function title() + public static function title(): string { return _t(__CLASS__ . '.Title', "Modified pages"); } - public function getFilteredPages() + public function getFilteredPages(DataList $list): 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); + $list = Versioned::updateListToAlsoIncludeStage($list, Versioned::DRAFT); + $list = $list->leftJoin($liveTable, "\"$liveTable\".\"ID\" = \"$table\".\"ID\"") ->where("\"$table\".\"Version\" <> \"$liveTable\".\"Version\""); - return $pages; + return $list; } } diff --git a/code/Controllers/CMSSiteTreeFilter_DeletedPages.php b/code/Controllers/CMSSiteTreeFilter_DeletedPages.php index 926f2b749f..d04343b640 100644 --- a/code/Controllers/CMSSiteTreeFilter_DeletedPages.php +++ b/code/Controllers/CMSSiteTreeFilter_DeletedPages.php @@ -2,7 +2,7 @@ namespace SilverStripe\CMS\Controllers; -use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\ORM\DataList; use SilverStripe\Versioned\Versioned; /** @@ -12,26 +12,13 @@ */ class CMSSiteTreeFilter_DeletedPages extends CMSSiteTreeFilter { - - /** - * @var string - */ - protected $childrenMethod = "AllHistoricalChildren"; - - /** - * @var string - */ - protected $numChildrenMethod = 'numHistoricalChildren'; - - public static function title() + public static function title(): string { return _t(__CLASS__ . '.Title', "All pages, including archived"); } - public function getFilteredPages() + public function getFilteredPages(DataList $list): DataList { - $pages = Versioned::get_including_deleted(SiteTree::class); - $pages = $this->applyDefaultFilters($pages); - return $pages; + return Versioned::updateListToAlsoIncludeDeleted($list); } } diff --git a/code/Controllers/CMSSiteTreeFilter_PublishedPages.php b/code/Controllers/CMSSiteTreeFilter_PublishedPages.php index 5c120aeb01..6ddccb8a3c 100644 --- a/code/Controllers/CMSSiteTreeFilter_PublishedPages.php +++ b/code/Controllers/CMSSiteTreeFilter_PublishedPages.php @@ -3,7 +3,7 @@ namespace SilverStripe\CMS\Controllers; use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Model\List\SS_List; +use SilverStripe\ORM\DataList; use SilverStripe\Versioned\Versioned; /** @@ -14,41 +14,22 @@ */ class CMSSiteTreeFilter_PublishedPages extends CMSSiteTreeFilter { - - /** - * @return string - */ - public static function title() + public static function title(): string { return _t(__CLASS__ . '.Title', "Published pages"); } - /** - * @var string - */ - protected $childrenMethod = "AllHistoricalChildren"; - - /** - * @var string - */ - protected $numChildrenMethod = 'numHistoricalChildren'; - /** * Filters out all pages who's status who's status that doesn't exist on live - * - * @see {@link ModelData::getStatusFlags()} - * @return SS_List */ - public function getFilteredPages() + public function getFilteredPages(DataList $list): DataList { - $pages = Versioned::get_including_deleted(SiteTree::class) - ->innerJoin( - 'SiteTree_Live', - '"SiteTree_Versions"."RecordID" = "SiteTree_Live"."ID"' - ); - - $pages = $this->applyDefaultFilters($pages); - - return $pages; + $list = Versioned::updateListToAlsoIncludeDeleted($list); + $baseTable = SiteTree::singleton()->baseTable(); + $liveTable = SiteTree::singleton()->stageTable($baseTable, Versioned::LIVE); + return $list->innerJoin( + $liveTable, + "\"{$baseTable}_Versions\".\"RecordID\" = \"$liveTable\".\"ID\"" + ); } } diff --git a/code/Controllers/CMSSiteTreeFilter_Search.php b/code/Controllers/CMSSiteTreeFilter_Search.php index 64ba79587f..a889a5e568 100644 --- a/code/Controllers/CMSSiteTreeFilter_Search.php +++ b/code/Controllers/CMSSiteTreeFilter_Search.php @@ -2,14 +2,12 @@ namespace SilverStripe\CMS\Controllers; -use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Model\List\SS_List; +use SilverStripe\ORM\DataList; use SilverStripe\Versioned\Versioned; class CMSSiteTreeFilter_Search extends CMSSiteTreeFilter { - - public static function title() + public static function title(): string { return _t(__CLASS__ . '.Title', "All pages"); } @@ -17,14 +15,9 @@ public static function title() /** * Retun an array of maps containing the keys, 'ID' and 'ParentID' for each page to be displayed * in the search. - * - * @return SS_List */ - public function getFilteredPages() + public function getFilteredPages(DataList $list): DataList { - // Filter default records - $pages = Versioned::get_by_stage(SiteTree::class, Versioned::DRAFT); - $pages = $this->applyDefaultFilters($pages); - return $pages; + return Versioned::updateListToAlsoIncludeStage($list, Versioned::DRAFT); } } diff --git a/code/Controllers/CMSSiteTreeFilter_StatusDeletedPages.php b/code/Controllers/CMSSiteTreeFilter_StatusDeletedPages.php index 9d117f4584..ddd357d84a 100644 --- a/code/Controllers/CMSSiteTreeFilter_StatusDeletedPages.php +++ b/code/Controllers/CMSSiteTreeFilter_StatusDeletedPages.php @@ -2,8 +2,7 @@ namespace SilverStripe\CMS\Controllers; -use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Model\List\SS_List; +use SilverStripe\ORM\DataList; use SilverStripe\Versioned\Versioned; /** @@ -11,31 +10,16 @@ */ class CMSSiteTreeFilter_StatusDeletedPages extends CMSSiteTreeFilter { - - /** - * @var string - */ - protected $childrenMethod = "AllHistoricalChildren"; - - /** - * @var string - */ - protected $numChildrenMethod = 'numHistoricalChildren'; - - public static function title() + public static function title(): string { return _t(__CLASS__ . '.Title', 'Archived pages'); } /** * Filters out all pages who's status is set to "Deleted". - * - * @see {@link ModelData::getStatusFlags()} - * @return SS_List */ - public function getFilteredPages() + public function getFilteredPages(DataList $list): DataList { - $pages = Versioned::getArchivedOnly(SiteTree::class); - return $this->applyDefaultFilters($pages); + return Versioned::updateListToOnlyIncludeArchived($list); } } diff --git a/code/Controllers/CMSSiteTreeFilter_StatusDraftPages.php b/code/Controllers/CMSSiteTreeFilter_StatusDraftPages.php index 75a7e43818..4b3354c5ab 100644 --- a/code/Controllers/CMSSiteTreeFilter_StatusDraftPages.php +++ b/code/Controllers/CMSSiteTreeFilter_StatusDraftPages.php @@ -2,8 +2,7 @@ namespace SilverStripe\CMS\Controllers; -use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Model\List\SS_List; +use SilverStripe\ORM\DataList; use SilverStripe\Versioned\Versioned; /** @@ -11,28 +10,22 @@ */ class CMSSiteTreeFilter_StatusDraftPages extends CMSSiteTreeFilter { - - public static function title() + public static function title(): string { return _t(__CLASS__ . '.Title', 'Draft pages'); } /** * Filters out all pages who's status is set to "Draft". - * - * @see {@link ModelData::getStatusFlags()} - * @return SS_List */ - public function getFilteredPages() + public function getFilteredPages(DataList $list): DataList { - $pages = SiteTree::get(); // Get all pages existing in draft but not live // Don't just use withVersionedMode - that would just get the latest draft versions // including records which have since been published. - $pages = $pages->setDataQueryParam([ + return $list->setDataQueryParam([ 'Versioned.mode' => 'stage_unique', 'Versioned.stage' => Versioned::DRAFT, ]); - return $this->applyDefaultFilters($pages); } } diff --git a/code/Controllers/CMSSiteTreeFilter_StatusRemovedFromDraftPages.php b/code/Controllers/CMSSiteTreeFilter_StatusRemovedFromDraftPages.php index b3b407e536..cbb2298725 100644 --- a/code/Controllers/CMSSiteTreeFilter_StatusRemovedFromDraftPages.php +++ b/code/Controllers/CMSSiteTreeFilter_StatusRemovedFromDraftPages.php @@ -2,8 +2,7 @@ namespace SilverStripe\CMS\Controllers; -use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Model\List\SS_List; +use SilverStripe\ORM\DataList; use SilverStripe\Versioned\Versioned; /** @@ -11,27 +10,22 @@ */ class CMSSiteTreeFilter_StatusRemovedFromDraftPages extends CMSSiteTreeFilter { - - public static function title() + public static function title(): string { return _t(__CLASS__ . '.Title', 'Live but removed from draft'); } /** * Filters out all pages who's status is set to "Removed from draft". - * - * @return SS_List */ - public function getFilteredPages() + public function getFilteredPages(DataList $list): DataList { - $pages = SiteTree::get(); // Get all pages removed from stage but not live // Don't just use withVersionedMode - that would just get the latest live versions // including records which were not removed from draft. - $pages = $pages->setDataQueryParam([ + return $list->setDataQueryParam([ 'Versioned.mode' => 'stage_unique', 'Versioned.stage' => Versioned::LIVE, ]); - return $this->applyDefaultFilters($pages); } } diff --git a/code/Model/SiteTree.php b/code/Model/SiteTree.php index 7a4678dee8..15950fa8e1 100755 --- a/code/Model/SiteTree.php +++ b/code/Model/SiteTree.php @@ -7,10 +7,13 @@ use SilverStripe\Admin\CMSEditLinkExtension; use SilverStripe\Assets\Shortcodes\FileLinkTracking; use SilverStripe\CMS\Controllers\CMSMain; +use SilverStripe\CMS\Controllers\CMSSiteTreeFilter; +use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search; use SilverStripe\CMS\Controllers\ContentController; use SilverStripe\CMS\Controllers\ModelAsController; use SilverStripe\CMS\Controllers\RootURLController; use SilverStripe\CMS\Forms\SiteTreeURLSegmentField; +use SilverStripe\CMS\Search\SiteTreeSearchContext; use SilverStripe\Control\ContentNegotiator; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; @@ -50,6 +53,8 @@ use SilverStripe\ORM\HiddenClass; use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\Core\Validation\ValidationResult; +use SilverStripe\Forms\DateField; +use SilverStripe\Forms\EmailField; use SilverStripe\Forms\HiddenField; use SilverStripe\Security\Group; use SilverStripe\Security\InheritedPermissions; @@ -64,6 +69,7 @@ use SilverStripe\Versioned\RecursivePublishable; use SilverStripe\Versioned\Versioned; use SilverStripe\Model\ArrayData; +use SilverStripe\ORM\Filters\WithinRangeFilter; use SilverStripe\Security\PermissionCheckable; use SilverStripe\View\HTML; use SilverStripe\View\Parsers\HTMLValue; @@ -289,8 +295,29 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi ]; private static $searchable_fields = [ - 'Title', - 'Content', + // See also searchableFields() which adds `FilterClass` + 'ClassName' => [ + 'general' => false, + ], + 'LastEdited' => [ + 'general' => false, + 'filter' => WithinRangeFilter::class, + 'field' => DateField::class, + ], + // The below fields are excluded from the filter form but + // are included in the general search + 'Title' => [ + 'field' => HiddenField::class, + ], + 'URLSegment' => [ + 'field' => HiddenField::class, + ], + 'MenuTitle' => [ + 'field' => HiddenField::class, + ], + 'Content' => [ + 'field' => HiddenField::class, + ], ]; private static $field_labels = [ @@ -2844,6 +2871,52 @@ public function isHomePage(): bool return $this->URLSegment === RootURLController::get_homepage_link(); } + public function getDefaultSearchContext() + { + return SiteTreeSearchContext::create( + static::class, + $this->scaffoldSearchFields(), + $this->defaultSearchFilters() + ); + } + + public function searchableFields() + { + $fields = parent::searchableFields(); + // Push `FilterClass` to the top of the fields list + $fields = array_merge([ + 'FilterClass' => [ + 'filter' => '', + 'general' => false, + 'title' => _t(__CLASS__ . '.Filter_Status', 'Page status'), + 'field' => DropdownField::class, + ] + ], $fields); + return $fields; + } + + public function scaffoldSearchFields($_params = null) + { + $fields = parent::scaffoldSearchFields($_params); + + // Update "Status" sources + $filters = CMSSiteTreeFilter::get_all_filters(); + // Remove 'All records' as we set that to empty/default value + unset($filters[CMSSiteTreeFilter_Search::class]); + $fields->dataFieldByName('FilterClass')?->setSource($filters)->setEmptyString(CMSSiteTreeFilter_Search::singleton()->title()); + + // Update "Page type" sources + $classNames = []; + $validClasses = ClassInfo::getValidSubClasses(SiteTree::class); + $this->invokeWithExtensions('updateAllowedSubClasses', $validClasses); + foreach ($validClasses as $class) { + $classNames[$class] = SiteTree::singleton($class)->i18n_singular_name(); + } + $fields->dataFieldByName('ClassName')?->setSource($classNames)->setEmptyString(_t(__CLASS__ . '.Filter_ClassName_Any', 'Any')); + + return $fields; + } + /** * Updates the list of status updates sent in response to the CMSMain::savetreenode action */ diff --git a/code/Search/SiteTreeSearchContext.php b/code/Search/SiteTreeSearchContext.php new file mode 100644 index 0000000000..efca8a5ff4 --- /dev/null +++ b/code/Search/SiteTreeSearchContext.php @@ -0,0 +1,49 @@ +setSearchParams($origSearchParams); + return $query; + } + + protected function individualFieldSearch(DataList $query, array $searchableFields, string $searchField, $searchPhrase): DataList + { + if ($searchField === 'FilterClass') { + $filter = $this->getQueryFilter($searchPhrase); + return $filter->getFilteredPages($query); + } + + return parent::individualFieldSearch($query, $searchableFields, $searchField, $searchPhrase); + } + + private function getQueryFilter($filterClass): CMSSiteTreeFilter + { + if (!is_subclass_of($filterClass, CMSSiteTreeFilter::class)) { + throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}"); + } + return $filterClass::create(); + } +} diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_Filter.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_Filter.ss deleted file mode 100644 index 7564e527a6..0000000000 --- a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_Filter.ss +++ /dev/null @@ -1,6 +0,0 @@ -
- <% if not $TreeIsFiltered %> - - <% end_if %> -
- diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_LeftPanel.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_LeftPanel.ss index 12e1feba9b..e1e5c56b58 100644 --- a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_LeftPanel.ss +++ b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_LeftPanel.ss @@ -12,13 +12,11 @@ <%-- Full breadcrumbs (useful for tree view which isn't available when viewing an edit form) --%> <% include SilverStripe\\Admin\\CMSBreadcrumbs %> <% end_if %> - <% include SilverStripe\\CMS\\Controllers\\CMSMain_Filter %> + $SearchForm.FilterButton($TreeIsFiltered)
-
-
-
+ $SearchForm.Placeholder($TreeIsFiltered) $RecordList
diff --git a/tests/behat/features/manage-page-permissions.feature b/tests/behat/features/manage-page-permissions.feature index c18501fe60..c038699c96 100644 --- a/tests/behat/features/manage-page-permissions.feature +++ b/tests/behat/features/manage-page-permissions.feature @@ -14,13 +14,12 @@ Feature: Manage page permissions Then I should see an edit page form And I click the "Settings" CMS tab - # BUG: https://github.com/silverstripe/silverstripe-cms/issues/1897 - # Scenario: I can open view permissions to everyone - # Given I select "Anyone" from "Who can view this page?" input group - # And I press the "Save" button - # When I am not logged in - # And I go to the homepage - # Then I should see "Welcome" + Scenario: I can open view permissions to everyone + Given I select "Anyone" from "Who can view this page?" input group + And I press the "Save" button + When I am not logged in + And I go to the homepage + Then I should see "Welcome" Scenario: I can limit page view permissions to logged-in users Given I select "Logged-in users" from "Who can view this page?" input group And I press the "Publish" button diff --git a/tests/behat/features/search-for-a-page.feature b/tests/behat/features/search-for-a-page.feature index e94fc79f1e..00c58098d6 100644 --- a/tests/behat/features/search-for-a-page.feature +++ b/tests/behat/features/search-for-a-page.feature @@ -11,11 +11,11 @@ Feature: Search for a page And the "group" "EDITOR" has permissions "Access to 'Pages' section" And I am logged in as a member of "EDITOR" group And I go to "/admin/pages" - And I press the "Filter" button + And I press the "Open search and filter" button Scenario: I can search for a page by its title Given I fill in "Search" with "About Us" - And I press the "Enter" key in the "SearchBox__Term" field + And I press the "Enter" key in the "SearchBox__q" field Then I should see "About Us" in the cms list But I should not see "Contact Us" in the cms list @@ -23,35 +23,34 @@ Feature: Search for a page Given a "Virtual Page" "My Virtual Page" When I press the "Advanced" button And I select "Virtual Page" from "Page type" - And I press the "Enter" key in the "SearchBox__Term" field + And I press the "Enter" key in the "SearchBox__q" field Then I should see "My Virtual Page" in the cms list But I should not see "Contact Us" in the cms list - # BUG: https://github.com/silverstripe/silverstripe-admin/issues/631 - # Scenario: I can search for a page by its oldest last edited date - # Given a "page" "Recent Page" - # And a "page" "Old Page" was last edited "7 days ago" - # When I press the "Advanced" button - # And I fill in "From" with "the date of 5 days ago" - # And I press the "Search" button - # Then I should see "Recent Page" in the cms list - # But I should not see "Old Page" in the cms list + Scenario: I can search for a page by its oldest last edited date + Given a "page" "Recent Page" was last edited "08-08-2007" + And a "page" "Old Page" was last edited "03-03-2007" + And I take a screenshot after every step + When I press the "Advanced" button + And I fill in "Search__LastEdited_SearchFrom" with "06-06-2007" + And I press the "Search" button + Then I should see "Recent Page" in the cms list + But I should not see "Old Page" in the cms list - # BUG: https://github.com/silverstripe/silverstripe-admin/issues/631 - # Scenario: I can search for a page by its newest last edited date - # Given a "page" "Recent Page" - # And a "page" "Old Page" was last edited "7 days ago" - # When I press the "Advanced" button - # And I fill in "To" with "the date of 5 days ago" - # And I press the "Search" button - # Then I should not see "Recent Page" in the cms list - # But I should see "Old Page" in the cms list + Scenario: I can search for a page by its newest last edited date + Given a "page" "Recent Page" was last edited "08-08-2007" + And a "page" "Old Page" was last edited "03-03-2007" + When I press the "Advanced" button + And I fill in "Search__LastEdited_SearchTo" with "06-06-2007" + And I press the "Search" button + Then I should not see "Recent Page" in the cms list + But I should see "Old Page" in the cms list Scenario: I can include deleted pages in my search Given a "page" "Deleted Page" And the "page" "Deleted Page" is unpublished And the "page" "Deleted Page" is deleted - When I press the "Enter" key in the "SearchBox__Term" field + When I press the "Enter" key in the "SearchBox__q" field Then I should not see "Deleted Page" in the cms list When I press the "Advanced" button And I select "All pages, including archived" from "Page status" @@ -62,7 +61,7 @@ Feature: Search for a page Given a "page" "Deleted Page" And the "page" "Deleted Page" is unpublished And the "page" "Deleted Page" is deleted - When I press the "Enter" key in the "SearchBox__Term" field + When I press the "Enter" key in the "SearchBox__q" field Then I should not see "Deleted Page" in the cms list When I press the "Advanced" button And I select "Archived pages" from "Page status" @@ -73,7 +72,7 @@ Feature: Search for a page Scenario: I can include draft pages in my search Given a "page" "Draft Page" And the "page" "Draft Page" is not published - When I press the "Enter" key in the "SearchBox__Term" field + When I press the "Enter" key in the "SearchBox__q" field Then I should see "Draft Page" in the cms list When I press the "Advanced" button And I select "Draft pages" from "Page status" @@ -90,7 +89,7 @@ Feature: Search for a page Then I should see "Saved" in the "button#Form_EditForm_action_save" element When I go to "/admin/pages" - And I press the "Filter" button + And I press the "Open search and filter" button And I press the "Advanced" button And I select "Modified pages" from "Page status" And I press the "Search" button @@ -101,7 +100,7 @@ Feature: Search for a page Given a "page" "Live Page" And the "page" "Live Page" is published And the "page" "Live Page" is deleted - When I press the "Enter" key in the "SearchBox__Term" field + When I press the "Enter" key in the "SearchBox__q" field Then I should not see "Live Page" in the cms list When I press the "Advanced" button And I select "Live but removed from draft" from "Page status" diff --git a/tests/php/Controllers/CMSMainTest.php b/tests/php/Controllers/CMSMainTest.php index 0312f74e37..17289009c8 100644 --- a/tests/php/Controllers/CMSMainTest.php +++ b/tests/php/Controllers/CMSMainTest.php @@ -6,8 +6,13 @@ use PHPUnit\Framework\Attributes\DataProvider; use Psr\SimpleCache\CacheInterface; use ReflectionMethod; +use ReflectionProperty; use SilverStripe\Admin\CMSBatchActionHandler; use SilverStripe\CMS\Controllers\CMSMain; +use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_PublishedPages; +use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search; +use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_StatusDeletedPages; +use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_StatusRemovedFromDraftPages; use SilverStripe\CMS\Model\RedirectorPage; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Tests\Controllers\CMSMainTest\TestHierarchicalDataObject; @@ -24,6 +29,7 @@ use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\TestOnly; use SilverStripe\Forms\FieldList; +use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\Security\Member; @@ -541,24 +547,65 @@ public function testGetNewItem() } /** - * Tests filtering in {@see CMSMain::getList()} + * Note this is not intended to be exhaustive - we're just validating + * that the search filter stuff is generally applied to ListViewForm. + * More robust testing of the filtering functionality is done separately. */ - public function testGetList() + public static function provideListViewForm(): array { - $controller = CMSMain::create(); - $controller->setRequest(Controller::curr()->getRequest()); + return [ + 'include all pages' => [ + 'params' => [], + 'limit' => 5, + 'expectedPreLimitCount' => 26, // Note this explicitly excludes records with parents! + 'expectedTitles' => ['Home', 'Page 10', 'Page 11', 'Page 13', 'Page 14'], + ], + 'filter by terms' => [ + 'params' => ['q' => 'Page 4'], + 'limit' => null, + 'expectedPreLimitCount' => 3, + 'expectedTitles' => ['Page 14', 'Page 24', 'Page 4'], + ], + 'deleted pages only' => [ + 'params' => [ + 'FilterClass' => CMSSiteTreeFilter_StatusDeletedPages::class, + ], + 'limit' => null, + 'expectedPreLimitCount' => 1, + 'expectedTitles' => ['Page 1'], + ], + 'pages removed from draft only' => [ + 'params' => [ + 'FilterClass' => CMSSiteTreeFilter_StatusRemovedFromDraftPages::class, + ], + 'limit' => null, + 'expectedPreLimitCount' => 1, + 'expectedTitles' => ['Page 12'], + ], + 'published pages only' => [ + 'params' => [ + 'FilterClass' => CMSSiteTreeFilter_PublishedPages::class, + ], + 'limit' => null, + 'expectedPreLimitCount' => 2, + 'expectedTitles' => ['Page 11', 'Page 12'], + ], + ]; + } - // Test all pages (stage) - $pages = $controller->getList()->sort('Title'); - $this->assertEquals(28, $pages->count()); - $this->assertEquals( - ['Home', 'Page 1', 'Page 10', 'Page 11', 'Page 12'], - $pages->Limit(5)->column('Title') - ); + #[DataProvider('provideListViewForm')] + public function testListViewForm(array $params, ?int $limit, int $expectedPreLimitCount, array $expectedTitles): void + { + $request = Controller::curr()->getRequest(); + $requestVarsReflection = new ReflectionProperty($request, 'getVars'); + $requestVarsReflection->setValue($request, ['q' => $params]); + $controller = CMSMain::create()->setRequest($request); + + /** @var DataList $pages */ + $pages = $controller->ListViewForm()->Fields()->dataFieldByName('Record')->getList(); // Change state of tree $page1 = $this->objFromFixture(SiteTree::class, 'page1'); - $page3 = $this->objFromFixture(SiteTree::class, 'page3'); $page11 = $this->objFromFixture(SiteTree::class, 'page11'); $page12 = $this->objFromFixture(SiteTree::class, 'page12'); // Deleted @@ -570,62 +617,48 @@ public function testGetList() $page12->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); $page12->delete(); - // Re-test all pages (stage) - $pages = $controller->getList()->sort('Title'); - $this->assertEquals(26, $pages->count()); - $this->assertEquals( - ['Home', 'Page 10', 'Page 11', 'Page 13', 'Page 14'], - $pages->Limit(5)->column('Title') - ); + $this->assertSame($expectedTitles, $pages->sort('Title')->limit($limit)->column('Title')); + $this->assertCount($expectedPreLimitCount, $pages); + } - // Test deleted page filter - $params = [ - 'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_StatusDeletedPages', + public static function provideListViewFormParentID(): array + { + return [ + [ + 'includeFilter' => true, + ], + [ + 'includeFilter' => false, + ], ]; - $pages = $controller->getList($params); - $this->assertEquals(1, $pages->count()); - $this->assertEquals( - ['Page 1'], - $pages->column('Title') - ); + } - // Test live, but not on draft filter - $params = [ - 'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_StatusRemovedFromDraftPages', - ]; - $pages = $controller->getList($params); - $this->assertEquals(1, $pages->count()); - $this->assertEquals( - ['Page 12'], - $pages->column('Title') - ); + #[DataProvider('provideListViewFormParentID')] + public function testListViewFormParentID(bool $includeFilter): void + { + $page3 = $this->objFromFixture(SiteTree::class, 'page3'); + $page11 = $this->objFromFixture(SiteTree::class, 'page11'); + $page12 = $this->objFromFixture(SiteTree::class, 'page12'); + $page11->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); + $page12->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); - // Test live pages filter - $params = [ - 'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_PublishedPages', - ]; - $pages = $controller->getList($params); - $this->assertEquals(2, $pages->count()); - $this->assertEquals( - ['Page 11', 'Page 12'], - $pages->column('Title') - ); + $request = Controller::curr()->getRequest(); + $params = ['ParentID' => $page3->ID]; + if ($includeFilter) { + $params['q'] = ['FilterClass' => CMSSiteTreeFilter_PublishedPages::class]; + } + $requestVarsReflection = new ReflectionProperty($request, 'getVars'); + $requestVarsReflection->setValue($request, $params); + $controller = CMSMain::create()->setRequest($request); - // Test that parentID is ignored when filtering - $pages = $controller->getList($params, $page3->ID); - $this->assertEquals(2, $pages->count()); - $this->assertEquals( - ['Page 11', 'Page 12'], - $pages->column('Title') - ); + /** @var DataList $pages */ + $pages = $controller->ListViewForm()->Fields()->dataFieldByName('Record')->getList(); - // Test that parentID is respected when not filtering - $pages = $controller->getList([], $page3->ID); - $this->assertEquals(2, $pages->count()); - $this->assertEquals( - ['Page 3.1', 'Page 3.2'], - $pages->column('Title') - ); + if ($includeFilter) { + $this->assertSame(['Page 11', 'Page 12'], $pages->column('Title')); + } else { + $this->assertSame(['Page 3.1', 'Page 3.2'], $pages->column('Title')); + } } /** @@ -730,46 +763,6 @@ public function testTreeHintsCache() $this->assertNotNull($hints); } - public function testSearchField() - { - $cms = CMSMain::create(); - $searchSchema = $cms->getSearchFieldSchema(); - - $this->assertJsonStringEqualsJsonString( - json_encode([ - 'formSchemaUrl' => 'admin/pages/schema/SearchForm', - 'name' => 'Term', - 'placeholder' => 'Search "Pages"', - 'filters' => new \stdClass - ]), - $searchSchema - ); - - $request = new HTTPRequest( - 'GET', - 'admin/pages/schema/SearchForm', - ['q' => [ - 'Term' => 'test', - 'FilterClass' => 'SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search' - ]] - ); - $cms->setRequest($request); - $searchSchema = $cms->getSearchFieldSchema(); - - $this->assertJsonStringEqualsJsonString( - json_encode([ - 'formSchemaUrl' => 'admin/pages/schema/SearchForm', - 'name' => 'Term', - 'placeholder' => 'Search "Pages"', - 'filters' => [ - 'Search__Term' => 'test', - 'Search__FilterClass' => 'SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search' - ] - ]), - $searchSchema - ); - } - public function testCanOrganiseTree() { $cms = CMSMain::create(); diff --git a/tests/php/Controllers/CMSSiteTreeFilterTest.php b/tests/php/Controllers/CMSSiteTreeFilterTest.php index fad67818e2..5dce12bc3d 100644 --- a/tests/php/Controllers/CMSSiteTreeFilterTest.php +++ b/tests/php/Controllers/CMSSiteTreeFilterTest.php @@ -16,61 +16,21 @@ class CMSSiteTreeFilterTest extends SapphireTest { protected static $fixture_file = 'CMSSiteTreeFilterTest.yml'; - public function testSearchFilterEmpty() + public function testSearchFilterDefault() { - $page1 = $this->objFromFixture(SiteTree::class, 'page1'); + $page1ID = $this->idFromFixture(SiteTree::class, 'page1'); $page2 = $this->objFromFixture(SiteTree::class, 'page2'); + $page3ID = $this->idFromFixture(SiteTree::class, 'page3'); - $f = new CMSSiteTreeFilter_Search(); - $results = $f->pagesIncluded(); - - $this->assertTrue($f->isRecordIncluded($page1)); - $this->assertTrue($f->isRecordIncluded($page2)); - } - - public function testSearchFilterByTitle() - { - $page1 = $this->objFromFixture(SiteTree::class, 'page1'); - $page2 = $this->objFromFixture(SiteTree::class, 'page2'); + $page2->delete(); - $f = new CMSSiteTreeFilter_Search(['Title' => 'Page 1']); - $results = $f->pagesIncluded(); - - $this->assertTrue($f->isRecordIncluded($page1)); - $this->assertFalse($f->isRecordIncluded($page2)); - $this->assertEquals(1, count($results ?? [])); - $this->assertEquals( - ['ID' => $page1->ID, 'ParentID' => 0], - $results[0] - ); - } - - public function testUrlSegmentFilter() - { - $page = $this->objFromFixture(SiteTree::class, 'page8'); - - $filter = CMSSiteTreeFilter_Search::create(['Term' => 'lake-wanaka+adventure']); - $this->assertTrue($filter->isRecordIncluded($page)); - - $filter = CMSSiteTreeFilter_Search::create(['URLSegment' => 'lake-wanaka+adventure']); - $this->assertTrue($filter->isRecordIncluded($page)); - } + $f = new CMSSiteTreeFilter_Search(); + $results = $f->getFilteredPages(SiteTree::get()); - public function testIncludesParentsForNestedMatches() - { - $parent = $this->objFromFixture(SiteTree::class, 'page3'); - $child = $this->objFromFixture(SiteTree::class, 'page3b'); - - $f = new CMSSiteTreeFilter_Search(['Title' => 'Page 3b']); - $results = $f->pagesIncluded(); - - $this->assertTrue($f->isRecordIncluded($parent)); - $this->assertTrue($f->isRecordIncluded($child)); - $this->assertEquals(1, count($results ?? [])); - $this->assertEquals( - ['ID' => $child->ID, 'ParentID' => $parent->ID], - $results[0] - ); + $this->assertTrue(in_array($page1ID, $results->column('ID'))); + $this->assertTrue(in_array($page3ID, $results->column('ID'))); + // Deleted page is not included + $this->assertFalse(in_array($page2->ID, $results->column('ID'))); } public function testChangedPagesFilter() @@ -88,33 +48,23 @@ public function testChangedPagesFilter() $changedPageVersion = $changedPage->Version; // Check that only changed pages are returned - $f = new CMSSiteTreeFilter_ChangedPages(['Term' => 'Changed']); - $results = $f->pagesIncluded(); - - $this->assertTrue($f->isRecordIncluded($changedPage)); - $this->assertFalse($f->isRecordIncluded($unchangedPage)); - $this->assertEquals(1, count($results ?? [])); - $this->assertEquals( - ['ID' => $changedPage->ID, 'ParentID' => 0], - $results[0] - ); + $f = new CMSSiteTreeFilter_ChangedPages(); + $results = $f->getFilteredPages(SiteTree::get()); - // Check that only changed pages are returned - $f = new CMSSiteTreeFilter_ChangedPages(['Term' => 'No Matches']); - $results = $f->pagesIncluded(); - $this->assertEquals(0, count($results ?? [])); + $this->assertSame([$changedPage->ID], $results->column('ID')); + $this->assertSame('Changed', $results->first()->Title); - // If we roll back to an earlier version than what's on the published site, we should still show the changed + // If we roll back to an earlier version than what's on the published site, we should show the currently "modified" version $changedPage->Title = 'Changed 2'; $changedPage->write(); $changedPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); $changedPage->rollbackRecursive($changedPageVersion); - $f = new CMSSiteTreeFilter_ChangedPages(['Term' => 'Changed']); - $results = $f->pagesIncluded(); + $f = new CMSSiteTreeFilter_ChangedPages(); + $results = $f->getFilteredPages(SiteTree::get()); - $this->assertEquals(1, count($results ?? [])); - $this->assertEquals(['ID' => $changedPage->ID, 'ParentID' => 0], $results[0]); + $this->assertSame([$changedPage->ID], $results->column('ID')); + $this->assertSame('Changed', $results->first()->Title); } public function testDeletedPagesFilter() @@ -129,95 +79,63 @@ public function testDeletedPagesFilter() ['"SiteTree_Live"."ID"' => $deletedPageID] ); - $f = new CMSSiteTreeFilter_DeletedPages(['Term' => 'Page']); - $this->assertTrue($f->isRecordIncluded($deletedPage)); - - // Check that only changed pages are returned - $f = new CMSSiteTreeFilter_DeletedPages(['Term' => 'No Matches']); - $this->assertFalse($f->isRecordIncluded($deletedPage)); + $f = new CMSSiteTreeFilter_DeletedPages(); + $results = $f->getFilteredPages(SiteTree::get()); + // Check this page is included even though it was deleted + $this->assertTrue(in_array($deletedPageID, $results->column('ID'))); } public function testStatusDraftPagesFilter() { $draftPage = $this->objFromFixture(SiteTree::class, 'page4'); - $draftPage = Versioned::get_one_by_stage( - SiteTree::class, - 'Stage', - sprintf('"SiteTree"."ID" = %d', $draftPage->ID) - ); // Check draft page is shown - $f = new CMSSiteTreeFilter_StatusDraftPages(['Term' => 'Page']); - $this->assertTrue($f->isRecordIncluded($draftPage)); - - // Check filter respects parameters - $f = new CMSSiteTreeFilter_StatusDraftPages(['Term' => 'No Match']); - $this->assertFalse($f->isRecordIncluded($draftPage)); - - // Ensures empty array returned if no data to show $f = new CMSSiteTreeFilter_StatusDraftPages(); - $draftPage->delete(); - $this->assertFalse($f->isRecordIncluded($draftPage)); - } - - public function testDateFromToLastSameDate() - { - $draftPage = $this->objFromFixture(SiteTree::class, 'page4'); - // Grab the date - $date = substr($draftPage->LastEdited ?? '', 0, 10); - // Filter with that date - $filter = new CMSSiteTreeFilter_Search([ - 'LastEditedFrom' => $date, - 'LastEditedTo' => $date, - ]); - $this->assertTrue( - $filter->isRecordIncluded($draftPage), - 'Using the same date for from and to should show find that page' - ); + $results = $f->getFilteredPages(SiteTree::get()); + $this->assertTrue(in_array($draftPage->ID, $results->column('ID'))); + + // Published and modified pages not shown + $draftPage->publishSingle(); + $results = $f->getFilteredPages(SiteTree::get()); + $this->assertFalse(in_array($draftPage->ID, $results->column('ID'))); + $draftPage->Title = 'modified'; + $draftPage->write(); + $results = $f->getFilteredPages(SiteTree::get()); + $this->assertFalse(in_array($draftPage->ID, $results->column('ID'))); } public function testStatusRemovedFromDraftFilter() { - $removedDraftPage = $this->objFromFixture(SiteTree::class, 'page6'); - $removedDraftPage->publishRecursive(); - $removedDraftPage->deleteFromStage('Stage'); - $removedDraftPage = Versioned::get_one_by_stage( - SiteTree::class, - 'Live', - sprintf('"SiteTree"."ID" = %d', $removedDraftPage->ID) - ); - - // Check live-only page is included - $f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(['LastEditedFrom' => '2000-01-01 00:00']); - $this->assertTrue($f->isRecordIncluded($removedDraftPage)); + $publishedPage = $this->objFromFixture(SiteTree::class, 'page3'); + $publishedPage->publishSingle(); + $archivePage = $this->objFromFixture(SiteTree::class, 'page1'); + $archivePage->doArchive(); - // Check filter is respected - $f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(['LastEditedTo' => '1999-01-01 00:00']); - $this->assertFalse($f->isRecordIncluded($removedDraftPage)); - - // Ensures empty array returned if no data to show + // Draft, published, and archive pages not included $f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(); - $removedDraftPage->delete(); - $this->assertFalse($f->isRecordIncluded($removedDraftPage)); + $results = $f->getFilteredPages(SiteTree::get()); + $this->assertSame([], $results->column('ID')); + + // Page is included when draft gets deleted + $publishedPage->deleteFromStage('Stage'); + $results = $f->getFilteredPages(SiteTree::get()); + $this->assertSame([$publishedPage->ID], $results->column('ID')); } public function testStatusDeletedFilter() { - $deletedPage = $this->objFromFixture(SiteTree::class, 'page7'); - $deletedPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); - $deletedPageID = $deletedPage->ID; - - // Can't use straight $blah->delete() as that blows it away completely and test fails - $deletedPage->deleteFromStage(Versioned::LIVE); - $deletedPage->deleteFromStage(Versioned::DRAFT); - $checkParentExists = Versioned::get_latest_version(SiteTree::class, $deletedPageID); - - // Check deleted page is included + $publishedPage = $this->objFromFixture(SiteTree::class, 'page4'); + $publishedPage->publishSingle(); + $notInDraftPage = $this->objFromFixture(SiteTree::class, 'page5'); + $notInDraftPage->publishSingle(); + $notInDraftPage->deleteFromStage('Stage'); + $archivedPage = $this->objFromFixture(SiteTree::class, 'page6'); + $archivedPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); + $archivedPage->doArchive(); + + // Check ONLY the archived page is included $f = new CMSSiteTreeFilter_StatusDeletedPages(['Title' => 'Page']); - $this->assertTrue($f->isRecordIncluded($checkParentExists)); - - // Check filter is respected - $f = new CMSSiteTreeFilter_StatusDeletedPages(['Title' => 'Bobby']); - $this->assertFalse($f->isRecordIncluded($checkParentExists)); + $results = $f->getFilteredPages(SiteTree::get()); + $this->assertSame([$archivedPage->ID], $results->column('ID')); } } diff --git a/tests/php/Search/SiteTreeSearchContextTest.php b/tests/php/Search/SiteTreeSearchContextTest.php new file mode 100644 index 0000000000..68db447913 --- /dev/null +++ b/tests/php/Search/SiteTreeSearchContextTest.php @@ -0,0 +1,77 @@ +idFromFixture(SiteTree::class, 'page1'); + $page2 = $this->objFromFixture(SiteTree::class, 'page2'); + $page3ID = $this->idFromFixture(SiteTree::class, 'page3'); + + $page2->delete(); + + $searchContext = new SiteTreeSearchContext(SiteTree::class); + $results = $searchContext->getQuery([]); + + $this->assertTrue(in_array($page1ID, $results->column('ID'))); + $this->assertTrue(in_array($page3ID, $results->column('ID'))); + $this->assertFalse(in_array($page2->ID, $results->column('ID'))); + } + + public function testSearchFilterByTitle() + { + $page = $this->objFromFixture(SiteTree::class, 'page1'); + $searchContext = new SiteTreeSearchContext(SiteTree::class); + $results = $searchContext->getQuery(['q' => 'Home']); + $this->assertSame([$page->ID], $results->column('ID')); + } + + public function testUrlSegmentFilter() + { + $page = $this->objFromFixture(SiteTree::class, 'page8'); + $searchContext = new SiteTreeSearchContext(SiteTree::class); + $results = $searchContext->getQuery(['q' => 'lake wanaka']); + $this->assertSame([$page->ID], $results->column('ID')); + } + + public function testChangedPagesFilter() + { + /** @var Page $unchangedPage */ + $unchangedPage = $this->objFromFixture(SiteTree::class, 'page1'); + $unchangedPage->publishRecursive(); + + /** @var Page $changedPage */ + $changedPage = $this->objFromFixture(SiteTree::class, 'page2'); + $changedPage->Title = 'Original'; + $changedPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); + $changedPage->Title = 'Changed'; + $changedPage->write(); + + // Check that only changed pages are returned + $searchContext = new SiteTreeSearchContext(SiteTree::class); + $results = $searchContext->getQuery(['q' => 'Changed']); + + $this->assertSame([$changedPage->ID], $results->column('ID')); + $this->assertSame('Changed', $results->first()->Title); + + // Check that filter doesn't override other query portions + $results = $searchContext->getQuery(['q' => 'No Matches']); + $this->assertCount(0, $results); + } +} diff --git a/tests/php/Search/SiteTreeSearchContextTest.yml b/tests/php/Search/SiteTreeSearchContextTest.yml new file mode 100644 index 0000000000..bb68379e05 --- /dev/null +++ b/tests/php/Search/SiteTreeSearchContextTest.yml @@ -0,0 +1,34 @@ +SilverStripe\CMS\Model\SiteTree: + page1: + Title: Home + page2: + Title: Page 2 + page3: + Title: Page 3 + page4: + Title: Page 4 + page5: + Title: Page 5 + Content: 'Default text' + page6: + Title: Page 6 + page7: + Title: Page 7 + page7a: + Parent: =>SilverStripe\CMS\Model\SiteTree.page7 + Title: Page 7a + page2a: + Parent: =>SilverStripe\CMS\Model\SiteTree.page2 + Title: Page 2a + page2b: + Parent: =>SilverStripe\CMS\Model\SiteTree.page2 + Title: Page 2b + page3a: + Parent: =>SilverStripe\CMS\Model\SiteTree.page3 + Title: Page 3a + page3b: + Parent: =>SilverStripe\CMS\Model\SiteTree.page3 + Title: Page 3b + page8: + Title: EncodedUrlSegment + URLSegment: lake-wanaka+adventure