diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php
index 92f2eea6c..7d775bb43 100644
--- a/admin/code/LeftAndMain.php
+++ b/admin/code/LeftAndMain.php
@@ -72,6 +72,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
'save',
'savetreenode',
'getsubtree',
+ 'updatetreenodes',
'printable',
'show',
'ping',
@@ -678,16 +679,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
$controller = $this;
$recordController = ($this->stat('tree_class') == 'SiteTree') ? singleton('CMSPageEditController') : $this;
$titleFn = function(&$child) use(&$controller, &$recordController) {
- $classes = $child->CMSTreeClasses();
- if($controller->isCurrentPage($child)) $classes .= " current";
- $flags = $child->hasMethod('getStatusFlags') ? $child->getStatusFlags() : false;
- if($flags) $classes .= ' ' . implode(' ', array_keys($flags));
- return "
ID\" data-id=\"$child->ID\" data-pagetype=\"$child->ClassName\" class=\"" . $classes . "\">" .
- " " .
- "Link("show"), $child->ID) . "\" title=\"" .
- _t('LeftAndMain.PAGETYPE','Page type: ') .
- "$child->class\" > " . ($child->TreeTitle).
- "";
+ $link = Controller::join_links($recordController->Link("show"), $child->ID);
+ return LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child))->forTemplate();
};
$html = $obj->getChildrenAsUL(
"",
@@ -740,6 +733,45 @@ class LeftAndMain extends Controller implements PermissionProvider {
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.
+ *
+ * @return String JSON
+ */
+ public function updatetreenodes($request) {
+ $data = array();
+ $ids = explode(',', $request->getVar('ids'));
+ foreach($ids as $id) {
+ $record = $this->getRecord($id);
+ $recordController = ($this->stat('tree_class') == 'SiteTree') ? singleton('CMSPageEditController') : $this;
+
+ // 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
+ $next = $prev = null;
+
+ $className = $this->stat('tree_class');
+ $next = DataObject::get($className, 'ParentID = '.$record->ParentID.' AND Sort > '.$record->Sort)->first();
+ if (!$next) {
+ $prev = DataObject::get($className, 'ParentID = '.$record->ParentID.' AND Sort < '.$record->Sort)->reverse()->first();
+ }
+
+ $link = Controller::join_links($recordController->Link("show"), $record->ID);
+ $html = LeftAndMain_TreeNode::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
+ );
+ }
+ $this->response->addHeader('Content-Type', 'text/json');
+ return Convert::raw2json($data);
+ }
/**
* Save handler
@@ -1499,3 +1531,87 @@ class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
}
}
+
+/**
+ * Wrapper around objects being displayed in a tree.
+ * Caution: Volatile API.
+ *
+ * @todo Implement recursive tree node rendering
+ */
+class LeftAndMain_TreeNode extends ViewableData {
+
+ /**
+ * @var obj
+ */
+ protected $obj;
+
+ /**
+ * @var String Edit link to the current record in the CMS
+ */
+ protected $link;
+
+ /**
+ * @var Bool
+ */
+ protected $isCurrent;
+
+ function __construct($obj, $link = null, $isCurrent = false) {
+ $this->obj = $obj;
+ $this->link = $link;
+ $this->isCurrent = $isCurrent;
+ }
+
+ /**
+ * 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
+ */
+ function forTemplate() {
+ $obj = $this->obj;
+ return "ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\"" . $this->getClasses() . "\">" .
+ " " .
+ "getLink() . "\" title=\"" .
+ _t('LeftAndMain.PAGETYPE','Page type: ') .
+ "$obj->class\" > " . ($obj->TreeTitle).
+ "";
+ }
+
+ function getClasses() {
+ $classes = $this->obj->CMSTreeClasses();
+ if($this->isCurrent) $classes .= " current";
+ $flags = $this->obj->hasMethod('getStatusFlags') ? $this->obj->getStatusFlags() : false;
+ if($flags) $classes .= ' ' . implode(' ', array_keys($flags));
+ return $classes;
+ }
+
+ function getObj() {
+ return $this->obj;
+ }
+
+ function setObj($obj) {
+ $this->obj = $obj;
+ return $this;
+ }
+
+ function getLink() {
+ return $this->link;
+ }
+
+ function setLink($link) {
+ $this->link = $link;
+ return $this;
+ }
+
+ function getIsCurrent() {
+ return $this->isCurrent;
+ }
+
+ function setIsCurrent($bool) {
+ $this->isCurrent = $bool;
+ return $this;
+ }
+
+}
\ No newline at end of file
diff --git a/admin/css/screen.css b/admin/css/screen.css
index 990bfe290..0098385c5 100644
--- a/admin/css/screen.css
+++ b/admin/css/screen.css
@@ -339,11 +339,11 @@ body.cms { overflow: hidden; }
.cms-content-actions { margin: 0; padding: 12px 16px; z-index: 0; border-top: 1px solid rgba(201, 205, 206, 0.8); border-top: 1px solid #FAFAFA; -webkit-box-shadow: #cccccc 0 -1px 1px; -moz-box-shadow: #cccccc 0 -1px 1px; box-shadow: #cccccc 0 -1px 1px; }
/** -------------------------------------------- Messages -------------------------------------------- */
-.message { margin: 0 0 8px 0; padding: 7px 7px; font-weight: bold; border: 1px black solid; }
+.message { display: block; clear: both; margin: 0 0 8px 0; padding: 7px 7px; font-weight: bold; border: 1px black solid; }
.message.notice { background-color: #ffbe66; border-color: #ff9300; }
.message.notice a { color: #999; }
.message.warning { background-color: #ffbe66; border-color: #ff9300; }
-.message.error, .message.bad, .message.required { background-color: #ffbe66; border-color: #ff9300; }
+.message.error, .message.bad, .message.required, .message.validation { background-color: #ffbe66; border-color: #ff9300; }
.message.good { background-color: #65a839; background-color: rgba(101, 168, 57, 0.7); border-color: #65a839; color: #fff; text-shadow: 1px -1px 0 #1f9433; -webkit-border-radius: 3px 3px 3px 3px; -moz-border-radius: 3px 3px 3px 3px; -ms-border-radius: 3px 3px 3px 3px; -o-border-radius: 3px 3px 3px 3px; border-radius: 3px 3px 3px 3px; }
.message.good a { text-shadow: none; }
.message p { margin: 0; }
diff --git a/admin/javascript/LeftAndMain.EditForm.js b/admin/javascript/LeftAndMain.EditForm.js
index 94dc89146..643de0c54 100644
--- a/admin/javascript/LeftAndMain.EditForm.js
+++ b/admin/javascript/LeftAndMain.EditForm.js
@@ -88,6 +88,11 @@
if(this.hasClass('validationerror')) {
// TODO validation shouldnt need a special case
statusMessage(ss.i18n._t('ModelAdmin.VALIDATIONERROR', 'Validation Error'), 'bad');
+
+ // Ensure the first validation error is visible
+ var firstTabWithErrors = this.find('.message.validation:first').closest('.tab');
+ $('.cms-container').clearCurrentTabState(); // clear state to avoid override later on
+ firstTabWithErrors.closest('.tabset').tabs('select', firstTabWithErrors.attr('id'));
}
// Move navigator to preview if one is available.
diff --git a/admin/javascript/LeftAndMain.Tree.js b/admin/javascript/LeftAndMain.Tree.js
index f9ffdde84..41da0a3c2 100644
--- a/admin/javascript/LeftAndMain.Tree.js
+++ b/admin/javascript/LeftAndMain.Tree.js
@@ -10,6 +10,10 @@
Hints: null,
+ IsUpdatingTree: false,
+
+ IsLoaded: false,
+
onadd: function(){
this._super();
@@ -22,7 +26,6 @@
/**
* @todo Icon and page type hover support
* @todo Sorting of sub nodes (originally placed in context menu)
- * @todo Refresh after language