diff --git a/.upgrade.yml b/.upgrade.yml
index 7cfb4b03..9850edd5 100644
--- a/.upgrade.yml
+++ b/.upgrade.yml
@@ -62,3 +62,4 @@ mappings:
MigrateSiteTreeLinkingTask: SilverStripe\CMS\Tasks\MigrateSiteTreeLinkingTask
RemoveOrphanedPagesTask: SilverStripe\CMS\Tasks\RemoveOrphanedPagesTask
SiteTreeMaintenanceTask: SilverStripe\CMS\Tasks\SiteTreeMaintenanceTask
+ LeftAndMain_TreeNode: SilverStripe\CMS\Controllers\CMSTreeNode
diff --git a/code/Controllers/CMSMain.php b/code/Controllers/CMSMain.php
index be84a3b8..b9c2c8d8 100644
--- a/code/Controllers/CMSMain.php
+++ b/code/Controllers/CMSMain.php
@@ -4,6 +4,9 @@ namespace SilverStripe\CMS\Controllers;
use SilverStripe\Admin\AdminRootController;
use SilverStripe\Admin\CMSBatchActionHandler;
+use SilverStripe\Admin\LeftAndMainFormRequestHandler;
+use SilverStripe\CMS\Model\VirtualPage;
+use SilverStripe\Forms\Tab;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
@@ -38,7 +41,6 @@ use SilverStripe\Forms\GridField\GridFieldSortableHeader;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\LabelField;
use SilverStripe\Forms\LiteralField;
-use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\ResetFormAction;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextField;
@@ -50,6 +52,7 @@ use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\HiddenClass;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationResult;
+use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
@@ -94,7 +97,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
private static $subitem_class = Member::class;
- private static $session_namespace = 'SilverStripe\\CMS\\Controllers\\CMSMain';
+ private static $session_namespace = self::class;
private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
@@ -121,6 +124,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'SearchForm',
'SiteTreeAsUL',
'getshowdeletedsubtree',
+ 'savetreenode',
+ 'getsubtree',
+ 'updatetreenodes',
'batchactions',
'treeview',
'listview',
@@ -128,6 +134,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'childfilter',
);
+ private static $url_handlers = [
+ 'EditForm/$ID' => 'EditForm',
+ ];
+
private static $casting = array(
'TreeIsFiltered' => 'Boolean',
'AddForm' => 'HTMLFragment',
@@ -389,8 +399,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function SiteTreeAsUL()
{
// Pre-cache sitetree version numbers for querying efficiency
- Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Stage");
- Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Live");
+ Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::DRAFT);
+ Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::LIVE);
$html = $this->getSiteTreeFor($this->stat('tree_class'));
$this->extend('updateSiteTreeAsUL', $html);
@@ -398,6 +408,368 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $html;
}
+ /**
+ * Get a site tree HTML listing which displays the nodes under the given criteria.
+ *
+ * @param string $className The class of the root object
+ * @param string $rootID The ID of the root object. If this is null then a complete tree will be
+ * shown
+ * @param string $childrenMethod The method to call to get the children of the tree. For example,
+ * Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
+ * @param string $numChildrenMethod
+ * @param callable $filterFunction
+ * @param int $nodeCountThreshold
+ * @return string Nested unordered list with links to each page
+ */
+ public function getSiteTreeFor(
+ $className,
+ $rootID = null,
+ $childrenMethod = null,
+ $numChildrenMethod = null,
+ $filterFunction = null,
+ $nodeCountThreshold = 30
+ ) {
+
+ // Filter criteria
+ $filter = $this->getSearchFilter();
+
+ // Default childrenMethod and numChildrenMethod
+ if (!$childrenMethod) {
+ $childrenMethod = ($filter && $filter->getChildrenMethod())
+ ? $filter->getChildrenMethod()
+ : 'AllChildrenIncludingDeleted';
+ }
+
+ if (!$numChildrenMethod) {
+ $numChildrenMethod = 'numChildren';
+ if ($filter && $filter->getNumChildrenMethod()) {
+ $numChildrenMethod = $filter->getNumChildrenMethod();
+ }
+ }
+ if (!$filterFunction && $filter) {
+ $filterFunction = function ($node) use ($filter) {
+ return $filter->isPageIncluded($node);
+ };
+ }
+
+ // Get the tree root
+ $record = ($rootID) ? $this->getRecord($rootID) : null;
+ $obj = $record ? $record : singleton($className);
+
+ // Get the current page
+ // NOTE: This *must* be fetched before markPartialTree() is called, as this
+ // causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
+ // which means that deleted pages stored in the marked tree would be removed
+ $currentPage = $this->currentPage();
+
+ // Mark the nodes of the tree to return
+ if ($filterFunction) {
+ $obj->setMarkingFilterFunction($filterFunction);
+ }
+
+ $obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
+
+ // Ensure current page is exposed
+ if ($currentPage) {
+ $obj->markToExpose($currentPage);
+ }
+
+ SiteTree::prepopulate_permission_cache(
+ 'CanEditType',
+ $obj->markedNodeIDs(),
+ [ SiteTree::class, 'can_edit_multiple']
+ );
+
+ // getChildrenAsUL is a flexible and complex way of traversing the tree
+ $controller = $this;
+ $recordController = CMSPageEditController::singleton();
+ $titleFn = function (&$child, $numChildrenMethod) use (&$controller, &$recordController, $filter) {
+ $link = Controller::join_links($recordController->Link("show"), $child->ID);
+ $node = CMSTreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
+ return $node->forTemplate();
+ };
+
+ // Limit the amount of nodes shown for performance reasons.
+ // Skip the check if we're filtering the tree, since its not clear how many children will
+ // match the filter criteria until they're queried (and matched up with previously marked nodes).
+ $nodeThresholdLeaf = SiteTree::config()->get('node_threshold_leaf');
+ if ($nodeThresholdLeaf && !$filterFunction) {
+ $nodeCountCallback = function ($parent, $numChildren) use (&$controller, $nodeThresholdLeaf) {
+ if ( !$parent->ID || $numChildren <= $nodeThresholdLeaf) {
+ return null;
+ }
+ return sprintf(
+ '
',
+ _t('LeftAndMain.TooManyPages', 'Too many pages'),
+ Controller::join_links(
+ $controller->LinkWithSearch($controller->Link()),
+ '?view=listview&ParentID=' . $parent->ID
+ ),
+ _t(
+ 'LeftAndMain.ShowAsList',
+ 'show as list',
+ 'Show large amount of pages in list instead of tree view'
+ )
+ );
+ };
+ } else {
+ $nodeCountCallback = null;
+ }
+
+ // If the amount of pages exceeds the node thresholds set, use the callback
+ $html = null;
+ if ($obj->ParentID && $nodeCountCallback) {
+ $html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
+ }
+
+ // Otherwise return the actual tree (which might still filter leaf thresholds on children)
+ if (!$html) {
+ $html = $obj->getChildrenAsUL(
+ "",
+ $titleFn,
+ CMSPagesController::singleton(),
+ true,
+ $childrenMethod,
+ $numChildrenMethod,
+ $nodeCountThreshold,
+ $nodeCountCallback
+ );
+ }
+
+ // Wrap the root if needs be.
+ if (!$rootID) {
+ // This lets us override the tree title with an extension
+ if ($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
+ $treeTitle = $customTreeTitle;
+ } elseif (class_exists(SiteConfig::class)) {
+ $siteConfig = SiteConfig::current_site_config();
+ $treeTitle = Convert::raw2xml($siteConfig->Title);
+ } else {
+ $treeTitle = '...';
+ }
+
+ $html = "$treeTitle "
+ . $html . " ";
+ }
+
+ return $html;
+ }
+
+ /**
+ * Get a subtree underneath the request param 'ID'.
+ * If ID = 0, then get the whole tree.
+ *
+ * @param HTTPRequest $request
+ * @return string
+ */
+ public function getsubtree($request)
+ {
+ $html = $this->getSiteTreeFor(
+ $this->stat('tree_class'),
+ $request->getVar('ID'),
+ null,
+ null,
+ null,
+ $request->getVar('minNodeCount')
+ );
+
+ // Trim off the outer tag
+ $html = preg_replace('/^[\s\t\r\n]*]*>/', '', $html);
+ $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html);
+
+ return $html;
+ }
+
+ /**
+ * Allows requesting a view update on specific tree nodes.
+ * Similar to {@link getsubtree()}, but doesn't enforce loading
+ * all children with the node. Useful to refresh views after
+ * state modifications, e.g. saving a form.
+ *
+ * @param HTTPRequest $request
+ * @return HTTPResponse
+ */
+ public function updatetreenodes($request)
+ {
+ $data = array();
+ $ids = explode(',', $request->getVar('ids'));
+ foreach ($ids as $id) {
+ if ($id === "") {
+ continue; // $id may be a blank string, which is invalid and should be skipped over
+ }
+
+ $record = $this->getRecord($id);
+ if (!$record) {
+ continue; // In case a page is no longer available
+ }
+ $recordController = CMSPageEditController::singleton();
+
+ // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
+ // TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
+ $prev = null;
+
+ $className = $this->stat('tree_class');
+ $next = DataObject::get($className)
+ ->filter('ParentID', $record->ParentID)
+ ->filter('Sort:GreaterThan', $record->Sort)
+ ->first();
+
+ if (!$next) {
+ $prev = DataObject::get($className)
+ ->filter('ParentID', $record->ParentID)
+ ->filter('Sort:LessThan', $record->Sort)
+ ->reverse()
+ ->first();
+ }
+
+ $link = Controller::join_links($recordController->Link("show"), $record->ID);
+ $html = CMSTreeNode::create($record, $link, $this->isCurrentPage($record))->forTemplate(). '';
+
+ $data[$id] = array(
+ 'html' => $html,
+ 'ParentID' => $record->ParentID,
+ 'NextID' => $next ? $next->ID : null,
+ 'PrevID' => $prev ? $prev->ID : null
+ );
+ }
+ return $this
+ ->getResponse()
+ ->addHeader('Content-Type', 'application/json')
+ ->setBody(Convert::raw2json($data));
+ }
+
+ /**
+ * Update the position and parent of a tree node.
+ * Only saves the node if changes were made.
+ *
+ * Required data:
+ * - 'ID': The moved node
+ * - 'ParentID': New parent relation of the moved node (0 for root)
+ * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
+ * In case of a 'ParentID' change, relates to the new siblings under the new parent.
+ *
+ * @param HTTPRequest $request
+ * @return HTTPResponse JSON string with a
+ * @throws HTTPResponse_Exception
+ */
+ public function savetreenode($request)
+ {
+ if (!SecurityToken::inst()->checkRequest($request)) {
+ return $this->httpError(400);
+ }
+ if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
+ return $this->httpError(
+ 403,
+ _t(
+ 'LeftAndMain.CANT_REORGANISE',
+ "You do not have permission to rearange the site tree. Your change was not saved."
+ )
+ );
+ }
+
+ $className = $this->stat('tree_class');
+ $id = $request->requestVar('ID');
+ $parentID = $request->requestVar('ParentID');
+ if (!is_numeric($id) || !is_numeric($parentID)) {
+ return $this->httpError(400);
+ }
+
+ // Check record exists in the DB
+ /** @var SiteTree $node */
+ $node = DataObject::get_by_id($className, $id);
+ if (!$node) {
+ return $this->httpError(
+ 500,
+ _t(
+ 'LeftAndMain.PLEASESAVE',
+ "Please Save Page: This page could not be updated because it hasn't been saved yet."
+ )
+ );
+ }
+
+ // Check top level permissions
+ $root = $node->getParentType();
+ if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
+ return $this->httpError(
+ 403,
+ _t(
+ 'LeftAndMain.CANT_REORGANISE',
+ "You do not have permission to alter Top level pages. Your change was not saved."
+ )
+ );
+ }
+
+ $siblingIDs = $request->requestVar('SiblingIDs');
+ $statusUpdates = array('modified'=>array());
+
+ if (!$node->canEdit()) {
+ return Security::permissionFailure($this);
+ }
+
+ // Update hierarchy (only if ParentID changed)
+ if ($node->ParentID != $parentID) {
+ $node->ParentID = (int)$parentID;
+ $node->write();
+
+ $statusUpdates['modified'][$node->ID] = array(
+ 'TreeTitle' => $node->TreeTitle
+ );
+
+ // Update all dependent pages
+ $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
+ foreach ($virtualPages as $virtualPage) {
+ $statusUpdates['modified'][$virtualPage->ID] = array(
+ 'TreeTitle' => $virtualPage->TreeTitle()
+ );
+ }
+
+ $this->getResponse()->addHeader(
+ 'X-Status',
+ rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
+ );
+ }
+
+ // Update sorting
+ if (is_array($siblingIDs)) {
+ $counter = 0;
+ foreach ($siblingIDs as $id) {
+ if ($id == $node->ID) {
+ $node->Sort = ++$counter;
+ $node->write();
+ $statusUpdates['modified'][$node->ID] = array(
+ 'TreeTitle' => $node->TreeTitle
+ );
+ } elseif (is_numeric($id)) {
+ // Nodes that weren't "actually moved" shouldn't be registered as
+ // having been edited; do a direct SQL update instead
+ ++$counter;
+ $table = DataObject::getSchema()->baseDataTable($className);
+ DB::prepared_query(
+ "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
+ array($counter, $id)
+ );
+ }
+ }
+
+ $this->getResponse()->addHeader(
+ 'X-Status',
+ rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
+ );
+ }
+
+ return $this
+ ->getResponse()
+ ->addHeader('Content-Type', 'application/json')
+ ->setBody(Convert::raw2json($statusUpdates));
+ }
+
+ public function CanOrganiseSitetree()
+ {
+ return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
+ }
+
/**
* @return boolean
*/
@@ -662,55 +1034,72 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/
public function getRecord($id, $versionID = null)
{
+ if (!$id) {
+ return null;
+ }
$treeClass = $this->stat('tree_class');
-
if ($id instanceof $treeClass) {
return $id;
- } elseif ($id && is_numeric($id)) {
- $currentStage = Versioned::get_reading_mode();
-
- if ($this->getRequest()->getVar('Version')) {
- $versionID = (int) $this->getRequest()->getVar('Version');
- }
-
- if ($versionID) {
- $record = Versioned::get_version($treeClass, $id, $versionID);
- } else {
- $record = DataObject::get_by_id($treeClass, $id);
- }
-
- // Then, try getting a record from the live site
- if (!$record) {
- // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
- Versioned::set_stage(Versioned::LIVE);
- singleton($treeClass)->flushCache();
-
- $record = DataObject::get_by_id($treeClass, $id);
- }
-
- // Then, try getting a deleted record
- if (!$record) {
- $record = Versioned::get_latest_version($treeClass, $id);
- }
-
- // Don't open a page from a different locale
- /** The record's Locale is saved in database in 2.4, and not related with Session,
- * we should not check their locale matches the Translatable::get_current_locale,
- * here as long as we all the HTTPRequest is init with right locale.
- * This bit breaks the all FileIFrameField functions if the field is used in CMS
- * and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
- */
- /* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
- $record = null;
- }*/
-
- // Set the reading mode back to what it was.
- Versioned::set_reading_mode($currentStage);
-
- return $record;
- } elseif (substr($id, 0, 3) == 'new') {
+ }
+ if (substr($id, 0, 3) == 'new') {
return $this->getNewItem($id);
}
+ if (!is_numeric($id)) {
+ return null;
+ }
+
+ $currentStage = Versioned::get_reading_mode();
+
+ if ($this->getRequest()->getVar('Version')) {
+ $versionID = (int) $this->getRequest()->getVar('Version');
+ }
+
+ /** @var SiteTree $record */
+ if ($versionID) {
+ $record = Versioned::get_version($treeClass, $id, $versionID);
+ } else {
+ $record = DataObject::get_by_id($treeClass, $id);
+ }
+
+ // Then, try getting a record from the live site
+ if (!$record) {
+ // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
+ Versioned::set_stage(Versioned::LIVE);
+ singleton($treeClass)->flushCache();
+
+ $record = DataObject::get_by_id($treeClass, $id);
+ }
+
+ // Then, try getting a deleted record
+ if (!$record) {
+ $record = Versioned::get_latest_version($treeClass, $id);
+ }
+
+ // Set the reading mode back to what it was.
+ Versioned::set_reading_mode($currentStage);
+
+ return $record;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param HTTPRequest $request
+ * @return Form
+ */
+ public function EditForm($request = null)
+ {
+ // set page ID from request
+ if ($request) {
+ // Validate id is present
+ $id = $request->param('ID');
+ if (!isset($id)) {
+ $this->httpError(400);
+ return null;
+ }
+ $this->setCurrentPageID($id);
+ }
+ return $this->getEditForm();
}
/**
@@ -720,119 +1109,130 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/
public function getEditForm($id = null, $fields = null)
{
+ // Get record
if (!$id) {
$id = $this->currentPageID();
}
- $form = parent::getEditForm($id, $fields);
-
- // TODO Duplicate record fetching (see parent implementation)
+ /** @var SiteTree $record */
$record = $this->getRecord($id);
- if ($record && !$record->canView()) {
- return Security::permissionFailure($this);
+
+ // Check parent form can be generated
+ $form = parent::getEditForm($record, $fields);
+ if (!$form || !$record) {
+ return $form;
}
if (!$fields) {
$fields = $form->Fields();
}
- $actions = $form->Actions();
- if ($record) {
- $deletedFromStage = !$record->isOnDraft();
+ // Add extra fields
+ $deletedFromStage = !$record->isOnDraft();
+ $fields->push($idField = new HiddenField("ID", false, $id));
+ // Necessary for different subsites
+ $fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink()));
+ $fields->push($liveLinkField = new HiddenField("LiveLink"));
+ $fields->push($stageLinkField = new HiddenField("StageLink"));
+ $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
+ $fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle()));
- $fields->push($idField = new HiddenField("ID", false, $id));
- // Necessary for different subsites
- $fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink()));
- $fields->push($liveLinkField = new HiddenField("LiveLink"));
- $fields->push($stageLinkField = new HiddenField("StageLink"));
- $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
- $fields->push(new HiddenField("TreeTitle", false, $record->TreeTitle));
+ $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
- $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
-
- if ($record->ID && is_numeric($record->ID)) {
- $liveLink = $record->getAbsoluteLiveLink();
- if ($liveLink) {
- $liveLinkField->setValue($liveLink);
- }
- if (!$deletedFromStage) {
- $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
- if ($stageLink) {
- $stageLinkField->setValue($stageLink);
- }
- }
+ // Build preview / live links
+ $liveLink = $record->getAbsoluteLiveLink();
+ if ($liveLink) {
+ $liveLinkField->setValue($liveLink);
+ }
+ if (!$deletedFromStage) {
+ $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
+ if ($stageLink) {
+ $stageLinkField->setValue($stageLink);
}
+ }
- // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
- /** @skipUpgrade */
- if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
- $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
- $navField->setAllowHTML(true);
- $fields->push($navField);
- }
+ // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
+ /** @skipUpgrade */
+ if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
+ $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
+ $navField->setAllowHTML(true);
+ $fields->push($navField);
+ }
- // getAllCMSActions can be used to completely redefine the action list
- if ($record->hasMethod('getAllCMSActions')) {
- $actions = $record->getAllCMSActions();
- } else {
- $actions = $record->getCMSActions();
+ // getAllCMSActions can be used to completely redefine the action list
+ if ($record->hasMethod('getAllCMSActions')) {
+ $actions = $record->getAllCMSActions();
+ } else {
+ $actions = $record->getCMSActions();
- // Find and remove action menus that have no actions.
- if ($actions && $actions->count()) {
- /** @var TabSet $tabset */
- $tabset = $actions->fieldByName('ActionMenus');
- if ($tabset) {
- foreach ($tabset->getChildren() as $tab) {
- if (!$tab->getChildren()->count()) {
- $tabset->removeByName($tab->getName());
- }
+ // Find and remove action menus that have no actions.
+ if ($actions && $actions->count()) {
+ /** @var TabSet $tabset */
+ $tabset = $actions->fieldByName('ActionMenus');
+ if ($tabset) {
+ /** @var Tab $tab */
+ foreach ($tabset->getChildren() as $tab) {
+ if (!$tab->getChildren()->count()) {
+ $tabset->removeByName($tab->getName());
}
}
}
}
-
- // Use to allow full jQuery UI styling
- $actionsFlattened = $actions->dataFields();
- if ($actionsFlattened) {
- /** @var FormAction $action */
- foreach ($actionsFlattened as $action) {
- $action->setUseButtonTag(true);
- }
- }
-
- if ($record->hasMethod('getCMSValidator')) {
- $validator = $record->getCMSValidator();
- } else {
- $validator = new RequiredFields();
- }
-
- // TODO Can't merge $FormAttributes in template at the moment
- $form->addExtraClass('center ' . $this->BaseCSSClasses());
- // Set validation exemptions for specific actions
- $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
-
- // Announce the capability so the frontend can decide whether to allow preview or not.
- if ($record instanceof CMSPreviewable) {
- $form->addExtraClass('cms-previewable');
- }
- $form->addExtraClass('fill-height flexbox-area-grow');
-
- if (!$record->canEdit() || $deletedFromStage) {
- $readonlyFields = $form->Fields()->makeReadonly();
- $form->setFields($readonlyFields);
- }
-
- $form->Fields()->setForm($form);
-
- $this->extend('updateEditForm', $form);
- return $form;
- } elseif ($id) {
- $form = Form::create($this, "EditForm", new FieldList(
- new LabelField('PageDoesntExistLabel', _t('CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
- ), new FieldList())->setHTMLID('Form_EditForm');
- return $form;
}
+
+ // Use to allow full jQuery UI styling
+ $actionsFlattened = $actions->dataFields();
+ if ($actionsFlattened) {
+ /** @var FormAction $action */
+ foreach ($actionsFlattened as $action) {
+ $action->setUseButtonTag(true);
+ }
+ }
+
+ // TODO Can't merge $FormAttributes in template at the moment
+ $form->addExtraClass('center ' . $this->BaseCSSClasses());
+ // Set validation exemptions for specific actions
+ $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
+
+ // Announce the capability so the frontend can decide whether to allow preview or not.
+ if ($record instanceof CMSPreviewable) {
+ $form->addExtraClass('cms-previewable');
+ }
+ $form->addExtraClass('fill-height flexbox-area-grow');
+
+ if (!$record->canEdit() || $deletedFromStage) {
+ $readonlyFields = $form->Fields()->makeReadonly();
+ $form->setFields($readonlyFields);
+ }
+
+ $form->Fields()->setForm($form);
+
+ $this->extend('updateEditForm', $form);
+
+ // Use custom reqest handler for LeftAndMain requests;
+ // CMS Forms cannot be identified solely by name, but also need ID (and sometimes OtherID)
+ $form->setRequestHandler(
+ LeftAndMainFormRequestHandler::create($form, [$id])
+ );
+ return $form;
}
+ public function EmptyForm()
+ {
+ $fields = new FieldList(
+ new LabelField('PageDoesntExistLabel', _t('CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
+ );
+ $form = parent::EmptyForm();
+ $form->setFields($fields);
+ $fields->setForm($form);
+ return $form;
+ }
+
+ /**
+ * Build an archive warning message based on the page's children
+ *
+ * @param SiteTree $record
+ * @return string
+ */
protected function getArchiveWarningMessage($record)
{
// Get all page's descendants
@@ -955,7 +1355,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return null;
}
$filterClass = $params['FilterClass'];
- if (!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
+ if (!is_subclass_of($filterClass, CMSSiteTreeFilter::class)) {
throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
}
return $filterClass::create($params);
diff --git a/code/Controllers/CMSPageAddController.php b/code/Controllers/CMSPageAddController.php
index b9fc8991..0b87ef23 100644
--- a/code/Controllers/CMSPageAddController.php
+++ b/code/Controllers/CMSPageAddController.php
@@ -16,7 +16,6 @@ use SilverStripe\Forms\SelectionGroup_Item;
use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField;
-use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
diff --git a/code/Controllers/CMSPageHistoryController.php b/code/Controllers/CMSPageHistoryController.php
index f5620ab3..a0df7ba3 100644
--- a/code/Controllers/CMSPageHistoryController.php
+++ b/code/Controllers/CMSPageHistoryController.php
@@ -2,20 +2,20 @@
namespace SilverStripe\CMS\Controllers;
+use SilverStripe\Admin\LeftAndMainFormRequestHandler;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Forms\CheckboxField;
-use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\HTMLReadonlyField;
use SilverStripe\Forms\LiteralField;
+use SilverStripe\Forms\Tab;
use SilverStripe\ORM\FieldType\DBField;
-use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Security\Security;
use SilverStripe\View\ArrayData;
@@ -35,6 +35,7 @@ class CMSPageHistoryController extends CMSMain
private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
private static $allowed_actions = array(
+ 'EditForm',
'VersionsForm',
'CompareVersionsForm',
'show',
@@ -42,15 +43,23 @@ class CMSPageHistoryController extends CMSMain
);
private static $url_handlers = array(
- '$Action/$ID/$VersionID/$OtherVersionID' => 'handleAction'
+ '$Action/$ID/$VersionID/$OtherVersionID' => 'handleAction',
+ 'EditForm/$ID/$VersionID' => 'EditForm',
);
+ /**
+ * Current version ID for this request. Can be 0 for latest version
+ *
+ * @var int
+ */
+ protected $versionID = null;
+
public function getResponseNegotiator()
{
$negotiator = parent::getResponseNegotiator();
$controller = $this;
$negotiator->setCallback('CurrentForm', function () use (&$controller) {
- $form = $controller->ShowVersionForm($controller->getRequest()->param('VersionID'));
+ $form = $controller->getEditForm();
if ($form) {
return $form->forTemplate();
} else {
@@ -65,19 +74,30 @@ class CMSPageHistoryController extends CMSMain
/**
* @param HTTPRequest $request
- * @return array
+ * @return HTTPResponse
*/
public function show($request)
{
- $form = $this->ShowVersionForm($request->param('VersionID'));
+ // Record id and version for this request
+ $id = $request->param('ID');
+ $this->setCurrentPageID($id);
+ $versionID = $request->param('VersionID');
+ $this->setVersionID($versionID);
+
+ // Show id
+ $form = $this->getEditForm();
$negotiator = $this->getResponseNegotiator();
$controller = $this;
$negotiator->setCallback('CurrentForm', function () use (&$controller, &$form) {
- return $form ? $form->forTemplate() : $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
+ return $form
+ ? $form->forTemplate()
+ : $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
});
$negotiator->setCallback('default', function () use (&$controller, &$form) {
- return $controller->customise(array('EditForm' => $form))->renderWith($controller->getViewer('show'));
+ return $controller
+ ->customise(array('EditForm' => $form))
+ ->renderWith($controller->getViewer('show'));
});
return $negotiator->respond($request);
@@ -85,7 +105,7 @@ class CMSPageHistoryController extends CMSMain
/**
* @param HTTPRequest $request
- * @return array
+ * @return HTTPResponse
*/
public function compare($request)
{
@@ -117,6 +137,24 @@ class CMSPageHistoryController extends CMSMain
}
}
+ /**
+ * @param HTTPRequest $request
+ * @return Form
+ */
+ public function EditForm($request = null)
+ {
+ if ($request) {
+ // Validate VersionID is present
+ $versionID = $request->param('VersionID');
+ if (!isset($versionID)) {
+ $this->httpError(400);
+ return null;
+ }
+ $this->setVersionID($versionID);
+ }
+ return parent::EditForm($request);
+ }
+
/**
* Returns the read only version of the edit form. Detaches all {@link FormAction}
* instances attached since only action relates to revert.
@@ -135,11 +173,21 @@ class CMSPageHistoryController extends CMSMain
if (!$id) {
$id = $this->currentPageID();
}
+ if (!$versionID) {
+ $versionID = $this->getVersionID();
+ }
$record = $this->getRecord($id, $versionID);
- $versionID = ($record) ? $record->Version : $versionID;
+ if (!$record) {
+ return $this->EmptyForm();
+ }
- $form = parent::getEditForm($record, ($record) ? $record->getCMSFields() : null);
+ // Refresh version ID
+ $versionID = $record->Version;
+ $this->setVersionID($versionID);
+
+ // Get edit form
+ $form = parent::getEditForm($record, $record->getCMSFields());
// Respect permission failures from parent implementation
if (!($form instanceof Form)) {
return $form;
@@ -148,9 +196,14 @@ class CMSPageHistoryController extends CMSMain
// TODO: move to the SilverStripeNavigator structure so the new preview can pick it up.
//$nav = new SilverStripeNavigatorItem_ArchiveLink($record);
- $form->setActions(new FieldList(
- $revert = FormAction::create('doRollback', _t('CMSPageHistoryController.REVERTTOTHISVERSION', 'Revert to this version'))->setUseButtonTag(true)
- ));
+ $actions = new FieldList(
+ $revert = FormAction::create(
+ 'doRollback',
+ _t('CMSPageHistoryController.REVERTTOTHISVERSION', 'Revert to this version')
+ )->setUseButtonTag(true)
+ );
+ $actions->setForm($form);
+ $form->setActions($actions);
$fields = $form->Fields();
$fields->removeByName("Status");
@@ -189,7 +242,9 @@ class CMSPageHistoryController extends CMSMain
}
}
- $fields->fieldByName('Root.Main')->unshift(
+ /** @var Tab $mainTab */
+ $mainTab = $fields->fieldByName('Root.Main');
+ $mainTab->unshift(
new LiteralField('CurrentlyViewingMessage', ArrayData::create(array(
'Content' => DBField::create_field('HTMLFragment', $message),
'Classes' => 'notice'
@@ -202,12 +257,17 @@ class CMSPageHistoryController extends CMSMain
"Version" => $versionID,
));
- if (($record && $record->isLatestVersion())) {
+ if ($record->isLatestVersion()) {
$revert->setReadonly(true);
}
$form->removeExtraClass('cms-content');
+ // History form has both ID and VersionID as suffixes
+ $form->setRequestHandler(
+ LeftAndMainFormRequestHandler::create($form, [$id, $versionID])
+ );
+
return $form;
}
@@ -276,28 +336,11 @@ class CMSPageHistoryController extends CMSMain
$hiddenID = new HiddenField('ID', false, "")
);
- $actions = new FieldList(
- new FormAction(
- 'doCompare',
- _t('CMSPageHistoryController.COMPAREVERSIONS', 'Compare Versions')
- ),
- new FormAction(
- 'doShowVersion',
- _t('CMSPageHistoryController.SHOWVERSION', 'Show Version')
- )
- );
-
- // Use to allow full jQuery UI styling
- foreach ($actions->dataFields() as $action) {
- /** @var FormAction $action */
- $action->setUseButtonTag(true);
- }
-
$form = Form::create(
$this,
'VersionsForm',
$fields,
- $actions
+ new FieldList()
)->setHTMLID('Form_VersionsForm');
$form->loadDataFrom($this->getRequest()->requestVars());
$hiddenID->setValue($id);
@@ -311,97 +354,6 @@ class CMSPageHistoryController extends CMSMain
return $form;
}
- /**
- * Process the {@link VersionsForm} compare function between two pages.
- *
- * @param array $data
- * @param Form $form
- * @return HTTPResponse|DBHTMLText
- */
- public function doCompare($data, $form)
- {
- $versions = $data['Versions'];
- if (count($versions) < 2) {
- return null;
- }
-
- $version1 = array_shift($versions);
- $version2 = array_shift($versions);
-
- $form = $this->CompareVersionsForm($version1, $version2);
-
- // javascript solution, render into template
- if ($this->getRequest()->isAjax()) {
- return $this->customise(array(
- "EditForm" => $form
- ))->renderWith(array(
- static::class . '_EditForm',
- 'LeftAndMain_Content'
- ));
- }
-
- // non javascript, redirect the user to the page
- return $this->redirect(Controller::join_links(
- $this->Link('compare'),
- $version1,
- $version2
- ));
- }
-
- /**
- * Process the {@link VersionsForm} show version function. Only requires
- * one page to be selected.
- *
- * @param array
- * @param Form
- *
- * @return DBHTMLText|HTTPResponse
- */
- public function doShowVersion($data, $form)
- {
- $versionID = null;
-
- if (isset($data['Versions']) && is_array($data['Versions'])) {
- $versionID = array_shift($data['Versions']);
- }
-
- if (!$versionID) {
- return null;
- }
-
- $request = $this->getRequest();
- if ($request->isAjax()) {
- return $this->customise(array(
- "EditForm" => $this->ShowVersionForm($versionID)
- ))->renderWith(array(
- static::class . '_EditForm',
- 'LeftAndMain_Content'
- ));
- }
-
- // non javascript, redirect the user to the page
- return $this->redirect(Controller::join_links(
- $this->Link('version'),
- $versionID
- ));
- }
-
- /**
- * @param int|null $versionID
- * @return Form
- */
- public function ShowVersionForm($versionID = null)
- {
- if (!$versionID) {
- return null;
- }
-
- $id = $this->currentPageID();
- $form = $this->getEditForm($id, null, $versionID);
-
- return $form;
- }
-
/**
* @param int $versionID
* @param int $otherVersionID
@@ -434,8 +386,8 @@ class CMSPageHistoryController extends CMSMain
$record = $page->compareVersions($fromVersion, $toVersion);
}
- $fromVersionRecord = Versioned::get_version('SilverStripe\\CMS\\Model\\SiteTree', $id, $fromVersion);
- $toVersionRecord = Versioned::get_version('SilverStripe\\CMS\\Model\\SiteTree', $id, $toVersion);
+ $fromVersionRecord = Versioned::get_version(SiteTree::class, $id, $fromVersion);
+ $toVersionRecord = Versioned::get_version(SiteTree::class, $id, $toVersion);
if (!$fromVersionRecord) {
user_error("Can't find version $fromVersion of page $id", E_USER_ERROR);
@@ -490,4 +442,26 @@ class CMSPageHistoryController extends CMSMain
}
return $fields;
}
+
+ /**
+ * Set current version ID
+ *
+ * @param int $versionID
+ * @return $this
+ */
+ public function setVersionID($versionID)
+ {
+ $this->versionID = $versionID;
+ return $this;
+ }
+
+ /**
+ * Get current version ID
+ *
+ * @return int
+ */
+ public function getVersionID()
+ {
+ return $this->versionID;
+ }
}
diff --git a/code/Controllers/CMSSiteTreeFilter.php b/code/Controllers/CMSSiteTreeFilter.php
index 416fb6b8..3e43e78d 100644
--- a/code/Controllers/CMSSiteTreeFilter.php
+++ b/code/Controllers/CMSSiteTreeFilter.php
@@ -5,9 +5,9 @@ namespace SilverStripe\CMS\Controllers;
use SilverStripe\Admin\LeftAndMain_SearchFilter;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\ClassInfo;
+use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Forms\DateField;
use SilverStripe\ORM\DataList;
-use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\Versioned\Versioned;
@@ -24,8 +24,7 @@ use SilverStripe\Versioned\Versioned;
*/
abstract class CMSSiteTreeFilter implements LeftAndMain_SearchFilter
{
-
- use \SilverStripe\Core\Injector\Injectable;
+ use Injectable;
/**
* Search parameters, mostly properties on {@link SiteTree}.
diff --git a/code/Controllers/CMSTreeNode.php b/code/Controllers/CMSTreeNode.php
new file mode 100644
index 00000000..cb127d4a
--- /dev/null
+++ b/code/Controllers/CMSTreeNode.php
@@ -0,0 +1,200 @@
+obj = $obj;
+ $this->link = $link;
+ $this->isCurrent = $isCurrent;
+ $this->numChildrenMethod = $numChildrenMethod;
+ $this->filter = $filter;
+ }
+
+ /**
+ * Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
+ * Does not include closing tag to allow this method to inject its own children.
+ *
+ * @todo Remove hardcoded assumptions around returning an , by implementing recursive tree node rendering
+ *
+ * @return string
+ */
+ public function forTemplate()
+ {
+ $obj = $this->obj;
+
+ return (string)SSViewer::execute_template(
+ [ 'type' => 'Includes', self::class ],
+ $obj,
+ array(
+ 'Classes' => $this->getClasses(),
+ 'Link' => $this->getLink(),
+ 'Title' => sprintf(
+ '(%s: %s) %s',
+ trim(_t('LeftAndMain.PAGETYPE', 'Page type'), " :"),
+ $obj->i18n_singular_name(),
+ $obj->Title
+ ),
+ )
+ );
+ }
+
+ /**
+ * Determine the CSS classes to apply to this node
+ *
+ * @return string
+ */
+ public function getClasses()
+ {
+ // Get classes from object
+ $classes = $this->obj->CMSTreeClasses($this->numChildrenMethod);
+ if ($this->isCurrent) {
+ $classes .= ' current';
+ }
+ // Get status flag classes
+ $flags = $this->obj->hasMethod('getStatusFlags')
+ ? $this->obj->getStatusFlags()
+ : false;
+ if ($flags) {
+ $statuses = array_keys($flags);
+ foreach ($statuses as $s) {
+ $classes .= ' status-' . $s;
+ }
+ }
+ // Get additional filter classes
+ if ($this->filter && ($filterClasses = $this->filter->getPageClasses($this->obj))) {
+ if (is_array($filterClasses)) {
+ $filterClasses = implode(' ', $filterClasses);
+ }
+ $classes .= ' ' . $filterClasses;
+ }
+ return $classes ?: '';
+ }
+
+ /**
+ * Get page backing this node
+ *
+ * @return SiteTree
+ */
+ public function getObj()
+ {
+ return $this->obj;
+ }
+
+ /**
+ * Set object backing this node
+ *
+ * @param SiteTree $obj
+ * @return $this
+ */
+ public function setObj($obj)
+ {
+ $this->obj = $obj;
+ return $this;
+ }
+
+ /**
+ * Get link to this node
+ *
+ * @return string
+ */
+ public function getLink()
+ {
+ return $this->link;
+ }
+
+ /**
+ * Set link to this node
+ *
+ * @param string $link
+ * @return $this
+ */
+ public function setLink($link)
+ {
+ $this->link = $link;
+ return $this;
+ }
+
+ /**
+ * Check if this is the currently selected node
+ *
+ * @return bool
+ */
+ public function getIsCurrent()
+ {
+ return $this->isCurrent;
+ }
+
+ /**
+ * Set this node to current, or not current
+ *
+ * @param bool $bool
+ * @return $this
+ */
+ public function setIsCurrent($bool)
+ {
+ $this->isCurrent = $bool;
+ return $this;
+ }
+}
diff --git a/lang/de.yml b/lang/de.yml
index 33b66a5d..bea6f141 100644
--- a/lang/de.yml
+++ b/lang/de.yml
@@ -314,6 +314,8 @@ de:
SINGULARNAME: Weiterleitungsseite
SilverStripe\CMS\Model\SiteTree:
DESCRIPTION: 'Allgemeine Inhaltsseite'
+ SINGULARNAME: Seite
+ PLURALNAME: Seiten
SilverStripe\CMS\Model\VirtualPage:
DESCRIPTION: 'Zeigt den Inhalt einer anderen Seite an'
PLURALNAME: 'Virtuelle Seiten'
diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSTreeNode.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSTreeNode.ss
new file mode 100644
index 00000000..51b8b874
--- /dev/null
+++ b/templates/SilverStripe/CMS/Controllers/Includes/CMSTreeNode.ss
@@ -0,0 +1,4 @@
+
+
+ $TreeTitle
+
diff --git a/tests/controller/CMSTreeTest.php b/tests/controller/CMSTreeTest.php
new file mode 100644
index 00000000..3500a444
--- /dev/null
+++ b/tests/controller/CMSTreeTest.php
@@ -0,0 +1,130 @@
+logInWithPermission('ADMIN');
+
+ // forcing sorting for non-MySQL
+ $rootPages = SiteTree::get()
+ ->filter("ParentID", 0)
+ ->sort('"ID"');
+ $siblingIDs = $rootPages->column('ID');
+ $page1 = $rootPages->offsetGet(0);
+ $page2 = $rootPages->offsetGet(1);
+ $page3 = $rootPages->offsetGet(2);
+
+ // Move page2 before page1
+ $siblingIDs[0] = $page2->ID;
+ $siblingIDs[1] = $page1->ID;
+ $data = array(
+ 'SiblingIDs' => $siblingIDs,
+ 'ID' => $page2->ID,
+ 'ParentID' => 0
+ );
+
+ $response = $this->post('admin/pages/edit/savetreenode', $data);
+ $this->assertEquals(200, $response->getStatusCode());
+ /** @var SiteTree $page1 */
+ $page1 = SiteTree::get()->byID($page1->ID);
+ /** @var SiteTree $page2 */
+ $page2 = SiteTree::get()->byID($page2->ID);
+ /** @var SiteTree $page3 */
+ $page3 = SiteTree::get()->byID($page3->ID);
+
+ $this->assertEquals(2, $page1->Sort, 'Page1 is sorted after Page2');
+ $this->assertEquals(1, $page2->Sort, 'Page2 is sorted before Page1');
+ $this->assertEquals(3, $page3->Sort, 'Sort order for other pages is unaffected');
+ }
+
+ public function testSaveTreeNodeParentID()
+ {
+ $this->logInWithPermission('ADMIN');
+
+ $page2 = $this->objFromFixture(SiteTree::class, 'page2');
+ $page3 = $this->objFromFixture(SiteTree::class, 'page3');
+ $page31 = $this->objFromFixture(SiteTree::class, 'page31');
+ $page32 = $this->objFromFixture(SiteTree::class, 'page32');
+
+ // Move page2 into page3, between page3.1 and page 3.2
+ $siblingIDs = array(
+ $page31->ID,
+ $page2->ID,
+ $page32->ID
+ );
+ $data = array(
+ 'SiblingIDs' => $siblingIDs,
+ 'ID' => $page2->ID,
+ 'ParentID' => $page3->ID
+ );
+ $response = $this->post('admin/pages/edit/savetreenode', $data);
+ $this->assertEquals(200, $response->getStatusCode());
+ $page2 = DataObject::get_by_id(SiteTree::class, $page2->ID, false);
+ $page31 = DataObject::get_by_id(SiteTree::class, $page31->ID, false);
+ $page32 = DataObject::get_by_id(SiteTree::class, $page32->ID, false);
+
+ $this->assertEquals($page3->ID, $page2->ParentID, 'Moved page gets new parent');
+ $this->assertEquals(1, $page31->Sort, 'Children pages before insertaion are unaffected');
+ $this->assertEquals(2, $page2->Sort, 'Moved page is correctly sorted');
+ $this->assertEquals(3, $page32->Sort, 'Children pages after insertion are resorted');
+ }
+
+
+ /**
+ * Test {@see CMSMain::updatetreenodes}
+ */
+ public function testUpdateTreeNodes()
+ {
+ $page1 = $this->objFromFixture(SiteTree::class, 'page1');
+ $page2 = $this->objFromFixture(SiteTree::class, 'page2');
+ $page3 = $this->objFromFixture(SiteTree::class, 'page3');
+ $page31 = $this->objFromFixture(SiteTree::class, 'page31');
+ $page32 = $this->objFromFixture(SiteTree::class, 'page32');
+ $this->logInWithPermission('ADMIN');
+
+ // Check page
+ $result = $this->get('admin/pages/edit/updatetreenodes?ids='.$page1->ID);
+ $this->assertEquals(200, $result->getStatusCode());
+ $this->assertEquals('application/json', $result->getHeader('Content-Type'));
+ $data = json_decode($result->getBody(), true);
+ $pageData = $data[$page1->ID];
+ $this->assertEquals(0, $pageData['ParentID']);
+ $this->assertEquals($page2->ID, $pageData['NextID']);
+ $this->assertEmpty($pageData['PrevID']);
+
+ // check subpage
+ $result = $this->get('admin/pages/edit/updatetreenodes?ids='.$page31->ID);
+ $this->assertEquals(200, $result->getStatusCode());
+ $this->assertEquals('application/json', $result->getHeader('Content-Type'));
+ $data = json_decode($result->getBody(), true);
+ $pageData = $data[$page31->ID];
+ $this->assertEquals($page3->ID, $pageData['ParentID']);
+ $this->assertEquals($page32->ID, $pageData['NextID']);
+ $this->assertEmpty($pageData['PrevID']);
+
+ // Multiple pages
+ $result = $this->get('admin/pages/edit/updatetreenodes?ids='.$page1->ID.','.$page2->ID);
+ $this->assertEquals(200, $result->getStatusCode());
+ $this->assertEquals('application/json', $result->getHeader('Content-Type'));
+ $data = json_decode($result->getBody(), true);
+ $this->assertEquals(2, count($data));
+
+ // Invalid IDs
+ $result = $this->get('admin/pages/edit/updatetreenodes?ids=-3');
+ $this->assertEquals(200, $result->getStatusCode());
+ $this->assertEquals('application/json', $result->getHeader('Content-Type'));
+ $data = json_decode($result->getBody(), true);
+ $this->assertEquals(0, count($data));
+ }
+
+}
diff --git a/tests/controller/CMSTreeTest.yml b/tests/controller/CMSTreeTest.yml
new file mode 100644
index 00000000..ce70a189
--- /dev/null
+++ b/tests/controller/CMSTreeTest.yml
@@ -0,0 +1,91 @@
+SilverStripe\CMS\Model\SiteTree:
+ page1:
+ Title: Page 1
+ Sort: 1
+ page2:
+ Title: Page 2
+ Sort: 2
+ page3:
+ Title: Page 3
+ Sort: 3
+ page31:
+ Title: Page 3.1
+ Parent: =>SilverStripe\CMS\Model\SiteTree.page3
+ Sort: 1
+ page32:
+ Title: Page 3.2
+ Parent: =>SilverStripe\CMS\Model\SiteTree.page3
+ Sort: 2
+ page4:
+ Title: Page 4
+ Sort: 4
+ page5:
+ Title: Page 5
+ Sort: 5
+ page6:
+ Title: Page 6
+ Sort: 6
+ page7:
+ Title: Page 7
+ Sort: 7
+ page8:
+ Title: Page 8
+ Sort: 8
+ page9:
+ Title: Page 9
+ Sort: 9
+ page10:
+ Title: Page 10
+ Sort: 10
+ page11:
+ Title: Page 11
+ Sort: 11
+ page12:
+ Title: Page 12
+ Sort: 12
+ page13:
+ Title: Page 13
+ Sort: 13
+ page14:
+ Title: Page 14
+ Sort: 14
+ page15:
+ Title: Page 15
+ Sort: 15
+ page16:
+ Title: Page 16
+ Sort: 16
+ page17:
+ Title: Page 17
+ Sort: 17
+ page18:
+ Title: Page 18
+ Sort: 18
+ page19:
+ Title: Page 19
+ Sort: 19
+ page20:
+ Title: Page 20
+ Sort: 20
+ page21:
+ Title: Page 21
+ Sort: 21
+ page22:
+ Title: Page 22
+ Sort: 22
+ page23:
+ Title: Page 23
+ Sort: 23
+ page24:
+ Title: Page 24
+ Sort: 24
+ page25:
+ Title: Page 25
+ Sort: 25
+ page26:
+ Title: Page 26
+ Sort: 26
+ home:
+ Title: Home
+ URLSegment: home
+ Sort: 0