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