From 44e16b9620472124a25720b0d020e23fe980144c Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Sat, 21 Nov 2009 03:15:40 +0000 Subject: [PATCH] API CHANGE Removed CMSBatchAction->getDoingText(), is now using button loading indicators in the UI API CHANGE Removed LeftAndMain->BatchActionList(), no longer necessary as we're creating the batch actions form in the same class ENHANCEMENT Changed CMSBatchAction logic to return JSON status changes rather than eval'ed JavaScript via FormResponse ENHANCEMENT Ported batchactions in CMSMain_left.js to concrete javascript, and moved to new CMSMain.BatchActions.js file ENHANCEMENT Using native CMSMain->BatchActionsForm() to render form instead of custom template markup in CMSMain_TreeTools.ss git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/cms/trunk@92722 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- code/CMSBatchAction.php | 118 +++++----- code/CMSBatchActionHandler.php | 3 +- code/CMSMain.php | 49 ++++- javascript/CMSMain.BatchActions.js | 278 ++++++++++++++++++++++++ javascript/CMSMain_left.js | 273 ----------------------- templates/Includes/CMSMain_TreeTools.ss | 15 +- 6 files changed, 394 insertions(+), 342 deletions(-) create mode 100644 javascript/CMSMain.BatchActions.js 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