diff --git a/code/LeftAndMain.php b/code/LeftAndMain.php index 90c41df2..a0247020 100644 --- a/code/LeftAndMain.php +++ b/code/LeftAndMain.php @@ -32,6 +32,8 @@ class LeftAndMain extends Controller { static $tree_class = null; + static $edit_timeout = 180; + static $ForceReload; static $allowed_actions = array( @@ -48,7 +50,7 @@ class LeftAndMain extends Controller { 'Member_ProfileForm', 'EditorToolbar', 'EditForm', - + 'pageStatus', ); /** @@ -343,7 +345,7 @@ class LeftAndMain extends Controller { } $form = $this->EditForm(); - if($form) return $form->formHtmlContent(); + if ($form) return $form->formHtmlContent(); else return ""; } public function getLastFormIn($html) { @@ -716,6 +718,9 @@ JS; FormResponse::add("\$('Form_EditForm_StageURLSegment').value = \"{$record->URLSegment}\";"); } + $newVersion = ((int)$record->Version) + 1; + FormResponse::add("\$('Form_EditForm_Version').value = {$newVersion};"); + // If the 'Save & Publish' button was clicked, also publish the page if (isset($urlParams['publish']) && $urlParams['publish'] == 1) { $record->doPublish(); @@ -971,6 +976,8 @@ JS; DataObject::delete_by_id($this->stat('tree_class'), $id); $script .= "node = st.getTreeNodeByIdx($id); if(node) node.parentTreeNode.removeTreeNode(node); $('Form_EditForm').closeIfSetTo($id); \n"; + + if ($id == $this->currentPageID()) FormResponse::add('CurrentPage.isDeleted = 1;'); } } FormResponse::add($script); @@ -1038,6 +1045,52 @@ JS; public function isCurrentPage(DataObject $page) { return $page->ID == Session::get("{$this->class}.currentPage"); } + + /** + * Get the staus of a certain page and version. + * + * This function is used for concurrent editing, and providing alerts + * when multiple users are editing a single page. It echoes a json + * encoded string to the UA. + */ + public function pageStatus() { + // If no ID is set, we're merely keeping the session alive + if (!isset($_REQUEST['ID'])) return 1; + + $page = $this->getRecord($_REQUEST['ID']); + if (!$page) { + // Page has not been found + $return = array('status' => 'not_found'); + } elseif ($page->getIsDeletedFromStage()) { + // Page has been deleted from stage + $return = array('status' => 'deleted'); + } else { + // Mark me as editing if I'm not already + $page->UsersCurrentlyEditing()->add(Member::currentUser()); + DB::query("UPDATE SiteTree_UsersCurrentlyEditing SET LastPing = '".date('Y-m-d H:i:s')."' + WHERE MemberID = ".Member::currentUserID()." AND SiteTreeID = {$page->ID}"); + + // Page exists, who else is editing it? + $names = array(); + foreach($page->UsersCurrentlyEditing() as $user) { + if ($user->ID == Member::currentUserId()) continue; + $names[] = trim($user->FirstName . ' ' . $user->Surname); + } + $return = array('status' => 'editing', 'names' => $names); + + // Has it been published since the CMS first loaded it? + $usersCurrentVersion = isset($_REQUEST['Version']) ? $_REQUEST['Version'] : $page->Version; + if ($usersCurrentVersion < $page->Version) { + $return = array('status' => 'not_current_version'); + } + } + + // Delete pings older than 3 minutes from the cache... + DB::query("DELETE FROM SiteTree_UsersCurrentlyEditing WHERE LastPing < '".date('Y-m-d H:i:s', time()-self::$edit_timeout)."'"); + + echo Convert::array2json($return); + return; + } /** * Return the CMS's HTML-editor toolbar diff --git a/javascript/LeftAndMain.js b/javascript/LeftAndMain.js index 76402928..dba239e7 100644 --- a/javascript/LeftAndMain.js +++ b/javascript/LeftAndMain.js @@ -1,4 +1,5 @@ var _AJAX_LOADING = false; +var pagePingInterval = 15; // Resize the tabs once the document is properly loaded // @todo most of this file needs to be tidied up using jQuery @@ -865,9 +866,66 @@ function hideIndicator(id) { Effect.Fade(id, {duration: 0.3}); } +var CurrentPage = { + id: function() { return $('Form_EditForm_ID').value; }, + version: function() { return $('Form_EditForm_Version').value; }, + isDeleted: function() { return $('SiteTree_Alert').getAttribute('deletedfromstage'); } +} + setInterval(function() { - new Ajax.Request("Security/ping"); -}, 5*60*1000); + if ($('Form_EditForm_ID')) { + new Ajax.Request("admin/pageStatus?ID="+CurrentPage.id()+'&Version='+CurrentPage.version(), { + onSuccess: function(t) { + var data = eval('('+t.responseText+')'); + var hasAlert = false; + + switch(data.status) { + case 'editing': + $('SiteTree_Alert').style.border = '2px solid #B5D4FE'; + $('SiteTree_Alert').style.backgroundColor = '#F8FAFC'; + if (data.names.length) { + hasAlert = true; + $('SiteTree_Alert').innerHTML = "This page is also being edited by: "+data.names.join(', '); + } + break; + case 'deleted': + // handle deletion by another user (but not us, or if we're already looking at a deleted version) + if (CurrentPage.isDeleted() == 0) { + $('SiteTree_Alert').style.border = '2px solid #ffd324'; + $('SiteTree_Alert').style.backgroundColor = '#fff6bf'; + $('SiteTree_Alert').innerHTML = "This page has been deleted since you opened it."; + hasAlert = true; + } + break; + case 'not_current_version': + // handle another user publishing + $('SiteTree_Alert').style.border = '2px solid #FFD324'; + $('SiteTree_Alert').style.backgroundColor = '#fff6bf'; + $('SiteTree_Alert').innerHTML = "This page has been saved since you opened it. You may want to reload it, or risk overwriting changes."; + hasAlert = true; + break; + case 'not_found': + break; + } + + if (hasAlert) { + $('SiteTree_Alert').style.padding = '5px'; + $('SiteTree_Alert').style.marginBottom = '5px'; + $('SiteTree_Alert').style.display = 'block'; + } else { + $('SiteTree_Alert').innerHTML = ''; + $('SiteTree_Alert').style.padding = '0px'; + $('SiteTree_Alert').style.marginBottom = '0px'; + if ($('SiteTree_Alert').style.display != 'none') $('SiteTree_Alert').style.display = 'none'; + } + } + }); + } else { + // We're not looking at a page, so at this stage, we're just + // pinging to keep the session alive. + new Ajax.Request("admin/pageStatus"); + } +}, pagePingInterval*1000); /** * Find and enable TinyMCE on all htmleditor fields