diff --git a/code/CMSBatchAction.php b/code/CMSBatchAction.php index 6d420b71..23f45099 100644 --- a/code/CMSBatchAction.php +++ b/code/CMSBatchAction.php @@ -1,7 +1,9 @@ * CMSMain::register_batch_action('publishitems', new CMSBatchAction('doPublish', @@ -17,12 +19,6 @@ abstract class CMSBatchAction extends Object { */ abstract function getActionTitle(); - /** - * Get text to be shown while the action is being processed, of the form - * "publishing pages". - */ - abstract function getDoingText(); - /** * Run this action for the given set of pages. * Return a set of status-updated JavaScript to return to the CMS. @@ -36,33 +32,45 @@ abstract class CMSBatchAction extends Object { * @param $pages The DataObjectSet of SiteTree objects to perform this batch action * on. * @param $helperMethod The method to call on each of those objects. - * @para + * @return JSON encoded map in the following format: + * { + * 'modified': { + * 3: {'TreeTitle': 'Page3'}, + * 5: {'TreeTitle': 'Page5'} + * }, + * 'deleted': { + * // all deleted pages + * } + * } */ - public function batchaction(DataObjectSet $pages, $helperMethod, $successMessage, $arguments = array()) { + public function batchaction(DataObjectSet $pages, $helperMethod, $successMessage) { $failures = 0; + $status = array('modified' => array(), 'error' => array()); foreach($pages as $page) { // Perform the action if (!call_user_func_array(array($page, $helperMethod), $arguments)) { - $failures++; + $status['error'][$page->ID] = ''; } // Now make sure the tree title is appropriately updated $publishedRecord = DataObject::get_by_id('SiteTree', $page->ID); if ($publishedRecord) { - $JS_title = Convert::raw2js($publishedRecord->TreeTitle); - FormResponse::add("\$('sitetree').setNodeTitle($page->ID, '$JS_title');"); + $status['modified'][$publishedRecord->ID] = array( + 'TreeTitle' => $publishedRecord->TreeTitle, + ); } $page->destroy(); unset($page); } - $message = sprintf($successMessage, $pages->Count()-$failures, $failures); + Controller::curr()->getResponse()->setStatusCode( + 200, + sprintf($successMessage, $pages->Count()) + ); - FormResponse::add('statusMessage("'.$message.'","good");'); - - return FormResponse::respond(); + return Convert::raw2json($status); } // if your batchaction has parameters, return a fieldset here @@ -81,9 +89,6 @@ class CMSBatchAction_Publish extends CMSBatchAction { function getActionTitle() { return _t('CMSBatchActions.PUBLISH_PAGES', 'Publish'); } - function getDoingText() { - return _t('CMSBatchActions.PUBLISHING_PAGES', 'Publishing pages'); - } function run(DataObjectSet $pages) { return $this->batchaction($pages, 'doPublish', @@ -92,6 +97,24 @@ class CMSBatchAction_Publish extends CMSBatchAction { } } +/** + * Un-publish items batch action. + * + * @package cms + * @subpackage batchaction + */ +class CMSBatchAction_Unpublish extends CMSBatchAction { + function getActionTitle() { + return _t('CMSBatchActions.UNPUBLISH_PAGES', 'Un-publish'); + } + + function run(DataObjectSet $pages) { + return $this->batchaction($pages, 'doUnpublish', + _t('CMSBatchActions.UNPUBLISHED_PAGES', 'Un-published %d pages') + ); + } +} + /** * Delete items batch action. * @@ -102,36 +125,34 @@ class CMSBatchAction_Delete extends CMSBatchAction { function getActionTitle() { return _t('CMSBatchActions.DELETE_DRAFT_PAGES', 'Delete from draft'); } - function getDoingText() { - return _t('CMSBatchActions.DELETING_DRAFT_PAGES', 'Deleting selected pages from draft'); - } function run(DataObjectSet $pages) { + $status = array( + 'modified'=>array(), + 'deleted'=>array() + ); + foreach($pages as $page) { $id = $page->ID; // Perform the action if($page->canDelete()) $page->delete(); - // check to see if the record exists on the live site, if it doesn't remove the tree node + // check to see if the record exists on the live site, + // if it doesn't remove the tree node $liveRecord = Versioned::get_one_by_stage( 'SiteTree', 'Live', "\"SiteTree\".\"ID\"=$id"); if($liveRecord) { $liveRecord->IsDeletedFromStage = true; - $title = Convert::raw2js($liveRecord->TreeTitle); - FormResponse::add("$('sitetree').setNodeTitle($id, '$title');"); - FormResponse::add("$('Form_EditForm').reloadIfSetTo($id);"); + $status['modified'][$liveRecord->ID] = array( + 'TreeTitle' => $liveRecord->TreeTitle, + ); } else { - FormResponse::add("var node = $('sitetree').getTreeNodeByIdx('$id');"); - FormResponse::add("if(node && node.parentTreeNode) node.parentTreeNode.removeTreeNode(node);"); - FormResponse::add("$('Form_EditForm').reloadIfSetTo($id);"); + $status['deleted'][$id] = array(); } } - $message = sprintf(_t('CMSBatchActions.DELETED_DRAFT_PAGES', 'Deleted %d pages from the draft site'), $pages->Count()); - FormResponse::add('statusMessage("'.$message.'","good");'); - - return FormResponse::respond(); + return Convert::raw2json($status); } } @@ -145,38 +166,33 @@ class CMSBatchAction_DeleteFromLive extends CMSBatchAction { function getActionTitle() { return _t('CMSBatchActions.DELETE_PAGES', 'Delete from published site'); } - function getDoingText() { - return _t('CMSBatchActions.DELETING_PAGES', 'Deleting selected pages from the published site'); - } function run(DataObjectSet $pages) { - $ids = $pages->column('ID'); - $this->batchaction($pages, 'doUnpublish', - _t('CMSBatchActions.DELETED_PAGES', 'Deleted %d pages from the published site, %d failures') + $status = array( + 'modified'=>array(), + 'deleted'=>array() ); - foreach($ids as $pageID) { - $id = $pageID; + foreach($pages as $page) { + $id = $page->ID; + + // Perform the action + if($page->canDelete()) $page->doDeleteFromLive(); // check to see if the record exists on the live site, if it doesn't remove the tree node $stageRecord = Versioned::get_one_by_stage( 'SiteTree', 'Stage', "\"SiteTree\".\"ID\"=$id"); if($stageRecord) { $stageRecord->IsAddedToStage = true; - $title = Convert::raw2js($stageRecord->TreeTitle); - FormResponse::add("$('sitetree').setNodeTitle($id, '$title');"); - FormResponse::add("$('Form_EditForm').reloadIfSetTo($id);"); + $status['modified'][$stageRecord->ID] = array( + 'TreeTitle' => $stageRecord->TreeTitle, + ); } else { - FormResponse::add("var node = $('sitetree').getTreeNodeByIdx('$id');"); - FormResponse::add("if(node && node.parentTreeNode) node.parentTreeNode.removeTreeNode(node);"); - FormResponse::add("$('Form_EditForm').reloadIfSetTo($id);"); + $status['deleted'][$stageRecord->ID] = array(); } } - $message = sprintf(_t('CMSBatchActions.DELETED_PAGES', 'Deleted %d pages from the published site'), $pages->Count()); - FormResponse::add('statusMessage("'.$message.'","good");'); - - return FormResponse::respond(); + return Convert::raw2json($status); } } diff --git a/code/CMSBatchActionHandler.php b/code/CMSBatchActionHandler.php index 640b78b4..1fb933df 100644 --- a/code/CMSBatchActionHandler.php +++ b/code/CMSBatchActionHandler.php @@ -96,8 +96,7 @@ class CMSBatchActionHandler extends RequestHandler { $actionObj = new $actionClass(); $actionDef = new ArrayData(array( "Link" => Controller::join_links($this->Link(), $urlSegment), - "Title" => $actionObj->getActionTitle(), - "DoingText" => $actionObj->getDoingText(), + "Title" => $actionObj->getActionTitle() )); $actionList->push($actionDef); } diff --git a/code/CMSMain.php b/code/CMSMain.php index 9b3d1d99..dda0cfc0 100755 --- a/code/CMSMain.php +++ b/code/CMSMain.php @@ -83,6 +83,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr Requirements::javascript(CMS_DIR . '/javascript/CMSMain.js'); Requirements::javascript(CMS_DIR . '/javascript/CMSMain_left.js'); + Requirements::javascript(CMS_DIR . '/javascript/CMSMain.BatchActions.js'); Requirements::javascript(CMS_DIR . '/javascript/CMSMain.Translatable.js'); Requirements::css(CMS_DIR . '/css/CMSMain.css'); @@ -137,11 +138,16 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr * * @return string */ - public function getfilteredsubtree($data, $form) { + public function getfilteredsubtree($request) { $params = $form->getData(); // Get the tree - $tree = $this->getSiteTreeFor($this->stat('tree_class'), $_REQUEST['ID'], null, array(new CMSMainMarkingFilter($params), 'mark')); + $tree = $this->getSiteTreeFor( + $this->stat('tree_class'), + $request->requestVar('ID'), + null, + array(new CMSMainMarkingFilter($params), 'mark') + ); // Trim off the outer tag $tree = ereg_replace('^[ \t\r\n]*]*>','', $tree); @@ -1097,6 +1103,45 @@ JS; return $form; } + + /** + * @return Form + */ + function BatchActionsForm() { + $actions = $this->batchactions()->batchActionList(); + $actionsMap = array(); + foreach($actions as $action) $actionsMap[$action->Link] = $action->Title; + + $form = new Form( + $this, + 'BatchActionsForm', + new FieldSet( + new LiteralField( + 'Intro', + sprintf('

%s

', + _t( + 'CMSMain_left.ss.SELECTPAGESACTIONS', + 'Select the pages that you want to change & then click an action:' + ) + ) + ), + new HiddenField('csvIDs'), + new DropdownField( + 'Action', + false, + $actionsMap + ) + ), + new FieldSet( + // TODO i18n + new FormAction('submit', "Go") + ) + ); + $form->addExtraClass('actionparams'); + $form->unsetValidator(); + + return $form; + } /** * Helper function to get page count diff --git a/javascript/CMSMain.BatchActions.js b/javascript/CMSMain.BatchActions.js new file mode 100644 index 00000000..67a877fa --- /dev/null +++ b/javascript/CMSMain.BatchActions.js @@ -0,0 +1,278 @@ +(function($) { + + /** + * @class Batch actions which take a bunch of selected pages, + * usually from the CMS tree implementation, and perform serverside + * callbacks on the whole set. We make the tree selectable when the jQuery.UI tab + * enclosing this form is opened. + * @name ss.Form_BatchActionsForm + * + * Events: + * - register: Called before an action is added. + * - unregister: Called before an action is removed. + */ + $('#Form_BatchActionsForm').concrete('ss', function($){ + return/** @lends ss.Form_BatchActionsForm */{ + + /** + * @type {DOMElement} + */ + Tree: null, + + /** + * @type {Array} Stores all actions that can be performed on the collected IDs as + * function closures. This might trigger filtering of the selected IDs, + * a confirmation message, etc. + */ + Actions: [], + + onmatch: function() { + var self = this; + + this.setTree($('#sitetree')[0]); + + $(this.Tree()).bind('selectionchanged', function(e, data) { + self._treeSelectionChanged(data.node); + }); + + // if tab which contains this form is shown, make the tree selectable + $('#TreeActions').bind('tabsselect', function(e, ui) { + if($(ui.panel).attr('id') != 'TreeActions-batchactions') return; + + // if the panel is visible (meaning about to be closed), + // disable tree selection and reset any values. Otherwise enable it. + if($(ui.panel).is(':visible')) { + $(self.Tree()).removeClass('multiselect'); + } else { + self._multiselectTransform(); + } + + }); + + this.bind('submit', function(e) {return self._submit(e);}); + }, + + /** + * @param {String} type + * @param {Function} callback + */ + register: function(type, callback) { + this.trigger('register', {type: type, callback: callback}); + + var actions = this.Actions(); + actions[type] = callback; + this.setActions(actions); + }, + + /** + * Remove an existing action. + * + * @param {String} type + */ + unregister: function(type) { + this.trigger('unregister', {type: type}); + + var actions = this.Actions(); + if(actions[type]) delete actions[type]; + this.setActions(actions); + }, + + /** + * Determines if we should allow and track tree selections. + * + * @todo Too much coupling with tabset + * @return boolean + */ + _isActive: function() { + return $('#TreeActions-batchactions').is(':visible'); + }, + + _submit: function(e) { + var ids = []; + var tree = this.Tree(); + // find all explicitly selected IDs + $(tree).find('li.selected').each(function() { + ids.push(tree.getIdxOf(this)); + // find implicitly selected children + $(this).find('li').each(function() { + ids.push(tree.getIdxOf(this)); + }); + }); + + // if no nodes are selected, return with an error + if(!ids || !ids.length) { + alert(ss.i18n._t('CMSMAIN.SELECTONEPAGE')); + return false; + } + + // apply callback, which might modify the IDs + var type = this.find(':input[name=Action]').val(); + if(this.Actions()[type]) ids = this.Actions()[type].apply(this, [ids]); + + // if no IDs are selected, stop here. This is an implict way for the + // callback to cancel the actions + if(!ids || !ids.length) { + return false; + } + + // write IDs to the hidden field + this.find(':input[name=csvIDs]').val(ids.join(',')); + + var button = this.find(':submit:first'); + button.addClass('loading'); + + jQuery.ajax({ + // don't use original form url + url: type, + type: 'POST', + data: this.serializeArray(), + complete: function(xmlhttp, status) { + button.removeClass('loading'); + + // status message + var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.statusText; + statusMessage(msg, (status == 'success') ? 'good' : 'bad'); + }, + success: function(data, status) { + // TODO This should use a more common serialization in a new tree library + if(data.modified) { + for(var id in data.modified) { + tree.setNodeTitle(id, data.modified[id]['TreeTitle']); + } + } + if(data.deleted) { + for(var id in data.deleted) { + var node = tree.getTreeNodeByIdx(id); + if(node && node.parentTreeNode) node.parentTreeNode.removeTreeNode(node); + } + } + + // reset selection state + // TODO Should unselect all selected nodes as well + jQuery(tree).removeClass('multiselect'); + + // Check if current page still exists, and refresh it. + // Otherwise remove the current form + var selectedNode = tree.firstSelected(); + if(selectedNode) { + var selectedNodeId = tree.getIdxOf(selectedNode); + if(data.modified[selectedNode.getIdx()]) { + // only if the current page was modified + selectedNode.selectTreeNode(); + } else if(data.deleted[selectedNode.getIdx()]) { + $('#Form_EditForm').concrete('ss').removeForm(); + } + } else { + $('#Form_EditForm').concrete('ss').removeForm(); + } + + // close panel + // TODO Coupling with tabs + $('#TreeActions').tabs('select', -1); + }, + dataType: 'json' + }); + + return false; + }, + + /** + * @todo This is simulating MultiselectTree functionality, and shouldn't be necessary. + */ + _multiselectTransform : function() { + // make tree selectable + jQuery(this.Tree()).addClass('multiselect'); + + // auto-select the current node + var node = this.Tree().firstSelected(); + if(node){ + node.removeNodeClass('current'); + node.addNodeClass('selected'); + node.open(); + + // Open all existing children, which might trigger further + // ajaxExansion calls to ensure all nodes are selectable + var children = $(node).find('li').each(function() { + this.open(); + }); + } + }, + + /** + * Only triggers if the field is considered 'active'. + * @todo Most of this is basically simulating broken behaviour of the MultiselectTree mixin, + * and should be removed. + */ + _treeSelectionChanged: function(node) { + if(!this._isActive()) return; + + if(node.selected) { + node.removeNodeClass('selected'); + node.selected = false; + } else { + // Select node + node.addNodeClass('selected'); + node.selected = true; + + // Open node in order to allow proper selection of children + if($(node).hasClass('unexpanded')) { + node.open(); + } + + // Open all existing children, which might trigger further + // ajaxExansion calls to ensure all nodes are selectable + var children = $(node).find('li').each(function() { + this.open(); + }); + } + } + }; + }); + + $(document).ready(function() { + /** + * Publish selected pages action + */ + $('#Form_BatchActionsForm').concrete('ss').register('admin/batchactions/publish', function(ids) { + var confirmed = confirm( + "You have " + ids.length + " pages selected.\n\n" + + "Do your really want to publish?" + ); + return (confirmed) ? ids : false; + }); + + /** + * Unpublish selected pages action + */ + $('#Form_BatchActionsForm').concrete('ss').register('admin/batchactions/unpublish', function(ids) { + var confirmed = confirm( + "You have " + ids.length + " pages selected.\n\n" + + "Do your really want to unpublish?" + ); + return (confirmed) ? ids : false; + }); + + /** + * Delete selected pages action + */ + $('#Form_BatchActionsForm').concrete('ss').register('admin/batchactions/delete', function(ids) { + var confirmed = confirm( + "You have " + ids.length + " pages selected.\n\n" + + "Do your really want to delete?" + ); + return (confirmed) ? ids : false; + }); + + /** + * Delete selected pages from live action + */ + $('#Form_BatchActionsForm').concrete('ss').register('admin/batchactions/deletefromlive', function(ids) { + var confirmed = confirm( + "You have " + ids.length + " pages selected.\n\n" + + "Do your really want to delete these pages from live?" + ); + return (confirmed) ? ids : false; + }); + }); + +})(jQuery); \ No newline at end of file diff --git a/javascript/CMSMain_left.js b/javascript/CMSMain_left.js index 3b7df37d..77be9e4b 100755 --- a/javascript/CMSMain_left.js +++ b/javascript/CMSMain_left.js @@ -40,279 +40,6 @@ SiteTreeFilter.prototype = { } } - -/** - * Batch Actions button click action - */ -batchactionsclass = Class.create(); -batchactionsclass.applyTo('#batchactions'); -batchactionsclass.prototype = { - - initialize : function() { - Observable.applyTo($(_HANDLER_FORMS.batchactions)); - }, - onclick : function() { - if(treeactions.toggleSelection(this)) { - this.multiselectTransform(); - } - return false; - }, - - actionChanged: function() { - var urlSegment = $('choose_batch_action').value.split('/').pop() - if ($('BatchActionParameters_'+urlSegment)) { - jQuery('#actionParams').empty(); - jQuery('#BatchActionParameters_'+urlSegment).appendTo('#actionParams'); - $('actionParams').style.display = 'block'; - $('actionParams').style.padding = '4px'; - } else { - $('actionParams').innerHTML = ''; - $('actionParams').style.display = 'none'; - } - }, - - multiselectTransform : function() { - batchActionGlobals.o1 = $('sitetree').observeMethod('SelectionChanged', batchActionGlobals.treeSelectionChanged); - batchActionGlobals.o2 = $(_HANDLER_FORMS.batchactions).observeMethod('Close', batchActionGlobals.popupClosed); - - jQuery('#sitetree').addClass('multiselect'); - - batchActionGlobals.selectedNodes = { }; - - var selectedNode = $('sitetree').firstSelected(); - if(selectedNode && selectedNode.className.indexOf('nodelete') == -1) { - var selIdx = $('sitetree').getIdxOf(selectedNode); - batchActionGlobals.selectedNodes[selIdx] = true; - selectedNode.removeNodeClass('current'); - selectedNode.addNodeClass('selected'); - selectedNode.open(); - - // Open all existing children, which might trigger further - // ajaxExansion calls to ensure all nodes are selectable - var children = selectedNode.getElementsByTagName('li'); - for(var i=0; i 0) { - batchActionGlobals.count += batchActionGlobals.newNodes.length; - - if(confirm(ss.i18n.sprintf( - ss.i18n._t('CMSMAIN.REALLYDELETEPAGES'), - batchActionGlobals.count - ))) { - this.elements.csvIDs.value = csvIDs; - - statusMessage(ss.i18n._t('CMSMAIN.DELETINGPAGES')); - // TODO: Remove 'new-' code http://open.silverstripe.com/ticket/875 - for( var idx = 0; idx < batchActionGlobals.newNodes.length; idx++ ) { - var newNode = $('sitetree').getTreeNodeByIdx( batchActionGlobals.newNodes[idx] ); - - if( newNode.parentTreeNode ) - newNode.parentTreeNode.removeTreeNode( newNode ); - else - alert( newNode.id + ' has no parent node'); - - $('Form_EditForm').reloadIfSetTo(idx); - } - - batchActionGlobals.newNodes = new Array(); - // Put an AJAXY loading icon on the button - $('Form_DeleteItemsForm_action_deleteitems').className = 'loading'; - Ajax.SubmitForm(this, null, { - onSuccess : function(response) { - Ajax.Evaluator(response); - $('Form_DeleteItemsForm_action_deleteitems').className = ''; - treeactions.closeSelection($('batchactions')); - }, - onFailure : function(response) { - errorMessage(ss.i18n._t('CMSMAIN.ERRORDELETINGPAGES'), response); - } - }); - } - - } else { - alert(ss.i18n._t('CMSMAIN.SELECTONEPAGE')); - } - - return false; - } -} - /** * Tree context menu */ diff --git a/templates/Includes/CMSMain_TreeTools.ss b/templates/Includes/CMSMain_TreeTools.ss index 8db018a4..84c4a0e6 100644 --- a/templates/Includes/CMSMain_TreeTools.ss +++ b/templates/Includes/CMSMain_TreeTools.ss @@ -27,20 +27,7 @@
-
-

<% _t('SELECTPAGESACTIONS','Select the pages that you want to change & then click an action:') %>

- - - -
- - -
-
+ $BatchActionsForm