NEW Rewrite of bulk editing save FIX #76

Bulk editing now use the normal Form submission system and handles all
field type, including ajax dependent
This commit is contained in:
Thierry François 2014-09-07 20:18:32 +03:00
parent 4a349987a4
commit 1ceee59360
6 changed files with 195 additions and 221 deletions

View File

@ -14,8 +14,6 @@ The component's options can be configurated individually or in bulk through the
### $config overview ### $config overview
The available configuration options are: The available configuration options are:
* 'editableFields' : array of string referencing specific CMS fields available for editing * 'editableFields' : array of string referencing specific CMS fields available for editing
* 'fieldsNameBlacklist' : array of string referencing the names of fields that wont be available for editing
* 'readOnlyFieldClasses' : array of string referencing types (ClassName) of fields that will be transformed to read only (always includes `GridField` and `UploadField`)
## Custom actions ## Custom actions
You can remove or add individual action or replace them all via `addBulkAction()` and `removeBulkAction()` You can remove or add individual action or replace them all via `addBulkAction()` and `removeBulkAction()`

View File

@ -12,7 +12,11 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
* RequestHandler allowed actions * RequestHandler allowed actions
* @var array * @var array
*/ */
private static $allowed_actions = array('edit', 'update'); private static $allowed_actions = array(
'index',
'bulkEditForm',
'recordEditForm'
);
/** /**
@ -20,18 +24,30 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
* @var array * @var array
*/ */
private static $url_handlers = array( private static $url_handlers = array(
'bulkEdit/update' => 'update', 'bulkEdit/bulkEditForm' => 'bulkEditForm',
'bulkEdit' => 'edit' 'bulkEdit/recordEditForm' => 'recordEditForm',
'bulkEdit' => 'index'
); );
/**
* Return URL to this RequestHandler
* @param string $action Action to append to URL
* @return string URL
*/
public function Link($action = null)
{
return Controller::join_links(parent::Link(), 'bulkEdit', $action);
}
/** /**
* Return a form for all the selected DataObjects * Return a form for all the selected DataObjects
* with their respective editable fields. * with their respective editable fields.
* *
* @return Form Selected DataObjects editable fields * @return Form Selected DataObjects editable fields
*/ */
public function editForm() public function bulkEditForm()
{ {
$crumbs = $this->Breadcrumbs(); $crumbs = $this->Breadcrumbs();
if($crumbs && $crumbs->count()>=2) if($crumbs && $crumbs->count()>=2)
@ -42,13 +58,11 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$actions = new FieldList(); $actions = new FieldList();
$actions->push( $actions->push(
FormAction::create('SaveAll', _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.SAVE_BTN_LABEL', 'Save all')) FormAction::create('doSave', _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.SAVE_BTN_LABEL', 'Save all'))
->setAttribute('id', 'bulkEditingUpdateBtn') ->setAttribute('id', 'bulkEditingSaveBtn')
->addExtraClass('ss-ui-action-constructive cms-panel-link') ->addExtraClass('ss-ui-action-constructive')
->setAttribute('data-icon', 'accept') ->setAttribute('data-icon', 'accept')
->setAttribute('data-url', $this->gridField->Link('bulkaction/bulkEdit/update'))
->setUseButtonTag(true) ->setUseButtonTag(true)
->setAttribute('src', '')//changes type to image so isn't hooked by default actions handlers
); );
$actions->push( $actions->push(
@ -70,6 +84,7 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$singleton = singleton($modelClass); $singleton = singleton($modelClass);
$titleModelClass = (($editingCount > 1) ? $singleton->i18n_plural_name() : $singleton->i18n_singular_name()); $titleModelClass = (($editingCount > 1) ? $singleton->i18n_plural_name() : $singleton->i18n_singular_name());
//some cosmetics
$headerText = _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.HEADER_TEXT', $headerText = _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.HEADER_TEXT',
'Editing {count} {class}', 'Editing {count} {class}',
array( array(
@ -86,6 +101,8 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$toggle = LiteralField::create('bulkEditToggle', '<span id="bulkEditToggle">' . _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.TOGGLE_ALL_LINK', 'Show/Hide all') . '</span>'); $toggle = LiteralField::create('bulkEditToggle', '<span id="bulkEditToggle">' . _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.TOGGLE_ALL_LINK', 'Show/Hide all') . '</span>');
$recordsFieldList->push($toggle); $recordsFieldList->push($toggle);
//fetch fields for each record and push to fieldList
foreach ( $recordList as $id ) foreach ( $recordList as $id )
{ {
$record = DataObject::get_by_id($modelClass, $id); $record = DataObject::get_by_id($modelClass, $id);
@ -103,18 +120,72 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$recordsFieldList->push($toggleField); $recordsFieldList->push($toggleField);
} }
$form = new Form( $bulkEditForm = Form::create(
$this, $this,
'BulkEditingForm', 'recordEditForm', //recordEditForm name is here to trick SS to pass all subform request to recordEditForm()
$recordsFieldList, $recordsFieldList,
$actions $actions
); );
if($crumbs && $crumbs->count()>=2){ if($crumbs && $crumbs->count()>=2){
$form->Backlink = $one_level_up->Link; $bulkEditForm->Backlink = $one_level_up->Link;
} }
return $form; //override form action URL back to bulkEditForm
//and add record ids GET var
$bulkEditForm->setAttribute(
'action',
$this->Link('bulkEditForm?records[]='.implode('&', $recordList))
);
return $bulkEditForm;
}
/**
* Return's a form with only one record's fields
* Used for bulkEditForm subForm requests via ajax
*
* @return Form Currently being edited form
*/
public function recordEditForm()
{
//clone current request : used to figure out what record we are asking
$request = clone $this->request;
$recordInfo = $request->shift();
//shift request till we find the requested field
while ($recordInfo)
{
if ( $unescapedRecordInfo = $this->unEscapeFieldName($recordInfo) )
{
$id = $unescapedRecordInfo['id'];
$fieldName = $unescapedRecordInfo['name'];
$action = $request->shift();
break;
}
else{
$recordInfo = $request->shift();
}
}
//generate a form with only that requested record's fields
if ( $id )
{
$modelClass = $this->gridField->getModelClass();
$record = DataObject::get_by_id($modelClass, $id);
$cmsFields = $record->getCMSFields();
$recordEditingFields = $this->getRecordEditingFields($record);
return Form::create(
$this->gridField,
'recordEditForm',
FieldList::create($recordEditingFields),
FieldList::create()
);
}
} }
@ -159,8 +230,6 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
{ {
$config = $this->component->getConfig(); $config = $this->component->getConfig();
$editableFields = $config['editableFields']; $editableFields = $config['editableFields'];
$fieldsNameBlacklist = $config['fieldsNameBlacklist'];
$readOnlyClasses = $config['readOnlyFieldClasses'];
// get all dataFields or just the ones allowed in config // get all dataFields or just the ones allowed in config
if ( $editableFields ) if ( $editableFields )
@ -179,24 +248,10 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$dataFields = $fields->dataFields(); $dataFields = $fields->dataFields();
} }
// remove and/or set readonly fields in blacklists
foreach ($dataFields as $name => $field)
{
if ( in_array($name, $fieldsNameBlacklist) )
{
unset( $dataFields[$name] );
}
else if ( in_array(get_class($field), $readOnlyClasses) )
{
$newField = $field->performReadonlyTransformation();
$dataFields[$name] = $newField;
}
}
// escape field names with unique prefix // escape field names with unique prefix
foreach ( $dataFields as $name => $field ) foreach ( $dataFields as $name => $field )
{ {
$field->Name = 'record_' . $id . '_' . $name; $field->Name = $this->escapeFieldName($id, $name);
$dataFields[$name] = $field; $dataFields[$name] = $field;
} }
@ -205,13 +260,50 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
/** /**
* Creates and return the editing interface * Escape a fieldName with a unique prefix
*
* @param integer $recordID Record id from who the field belongs
* @param string $name Field name
* @return string Escaped field name
*/
protected function escapeFieldName($recordID, $name)
{
return 'record_' . $recordID . '_' . $name;
}
/**
* Un-escape a previously escaped field name
*
* @param string $fieldName Escaped field name
* @return array|false Fasle if the fieldName was not escaped. Or Array map with record 'id' and field 'name'
*/
protected function unEscapeFieldName($fieldName)
{
$parts = array();
$match = preg_match('/record_(\d+)_(\w+)/i', $fieldName, $parts);
if ( !$match )
{
return false;
}
else{
return array(
'id' => $parts[1],
'name' => $parts[2],
);
}
}
/**
* Creates and return the bulk editing interface
* *
* @return string Form's HTML * @return string Form's HTML
*/ */
public function edit() public function index()
{ {
$form = $this->editForm(); $form = $this->bulkEditForm();
$form->setTemplate('LeftAndMain_EditForm'); $form->setTemplate('LeftAndMain_EditForm');
$form->addExtraClass('center cms-content'); $form->addExtraClass('center cms-content');
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content'); $form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
@ -238,47 +330,70 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
/** /**
* Saves the changes made in the bulk edit into the dataObject * Handles bulkEditForm submission
* and parses and saves each records data
* *
* @return JSON * @param array $data Sumitted form data
* @param Form $form Form
*/ */
public function update() public function doSave($data, $form)
{ {
$data = $this->request->requestVars();
$return = array();
$className = $this->gridField->list->dataClass; $className = $this->gridField->list->dataClass;
$singleton = singleton($className);
if ( isset($data['url']) ) unset($data['url']); $formsData = array();
if ( isset($data['cacheBuster']) ) unset($data['cacheBuster']); $ids = array();
if ( isset($data['locale']) ) unset($data['locale']); $done = 0;
foreach ($data as $recordID => $recordDataSet) //unescape and sort form data per record ID
foreach ($data as $fieldName => $value)
{
if ( $fieldInfo = $this->unEscapeFieldName($fieldName) )
{
if ( !isset($formsData[$fieldInfo['id']]) )
{
$formsData[$fieldInfo['id']] = array();
}
$formsData[$fieldInfo['id']][$fieldInfo['name']] = $value;
}
}
//process each record's form data and save
foreach ($formsData as $recordID => $recordData)
{ {
$record = DataObject::get_by_id($className, $recordID); $record = DataObject::get_by_id($className, $recordID);
foreach($recordDataSet as $recordData) $recordForm = Form::create(
{ $this, "RecordForm",
$field = preg_replace('/record_(\d+)_(\w+)/i', '$2', $recordData['name']); $record->getCMSFields(),
$value = $recordData['value']; FieldList::create()
);
if ( $record->hasMethod($field) ) $recordForm->loadDataFrom($recordData);
$recordForm->saveInto($record);
$id = $record->write();
array_push($ids, $record->ID);
if ( $id )
{ {
$list = $record->$field(); $done++;
$list->setByIDList($value);
} }
else{
$record->setCastedField($field, $value);
}
}
$done = $record->write();
array_push($return, array(
'id' => $done,
'title' => $record->getTitle()
));
} }
return json_encode(array( //compose form message
'done' => 1, $messageModelClass = (($editingCount > 1) ? $singleton->i18n_plural_name() : $singleton->i18n_singular_name());
'records' => $return $message = _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.SAVE_RESULT_TEXT',
), JSON_NUMERIC_CHECK); '{count} {class} saved successfully.',
array(
'count' => $done,
'class' => $messageModelClass
)
);
$form->sessionMessage($message, 'good');
//return back to form
return Controller::curr()->redirect($this->Link('?records[]='.implode('&records[]=', $ids)));
//return Controller::curr()->redirect($form->Backlink); //returns to gridField
} }
} }

View File

@ -56,7 +56,7 @@ class GridFieldBulkActionHandler extends RequestHandler
*/ */
public function Link($action = null) public function Link($action = null)
{ {
return Controller::join_links($this->gridField->Link(), 'bulkediting', $action); return Controller::join_links($this->gridField->Link(), 'bulkAction', $action);
} }

View File

@ -12,27 +12,16 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
* component configuration * component configuration
* *
* 'editableFields' => fields editable on the Model * 'editableFields' => fields editable on the Model
* 'readOnlyFieldClasses' => field types that will be converted to readonly
* 'fieldsNameBlacklist' => fields that will be removed from the automatic form generation
* 'actions' => maps of action name and configuration * 'actions' => maps of action name and configuration
* *
* @var array * @var array
*/ */
protected $config = array( protected $config = array(
'editableFields' => null, 'editableFields' => null,
'fieldsNameBlacklist' => array(),
'readOnlyFieldClasses' => array(),
'actions' => array() 'actions' => array()
); );
/**
* Holds any class that should not be used as they break the component
* These cannot be removed from the blacklist
*/
protected $readOnlyFieldClasses = array('GridField', 'UploadField');
/** /**
* GridFieldBulkManager component constructor * GridFieldBulkManager component constructor
* *
@ -42,7 +31,6 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
public function __construct($editableFields = null, $defaultActions = true) public function __construct($editableFields = null, $defaultActions = true)
{ {
if ( $editableFields != null ) $this->setConfig ( 'editableFields', $editableFields ); if ( $editableFields != null ) $this->setConfig ( 'editableFields', $editableFields );
$this->config['readOnlyFieldClasses'] = $this->readOnlyFieldClasses;
if ( $defaultActions ) if ( $defaultActions )
{ {
@ -102,17 +90,11 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
user_error("Bulk actions must be edited via addBulkAction() and removeBulkAction()", E_USER_ERROR); user_error("Bulk actions must be edited via addBulkAction() and removeBulkAction()", E_USER_ERROR);
} }
if ( ($reference == 'readOnlyFieldClasses' || $reference == 'fieldsNameBlacklist' || $reference == 'editableFields') && !is_array($value) ) if ( ($reference == 'editableFields') && !is_array($value) )
{ {
$value = array($value); $value = array($value);
} }
//makes sure $readOnlyFieldClasses are in no matter what
if ( $reference == 'readOnlyFieldClasses' )
{
$value = array_unique( array_merge($value, $this->readOnlyFieldClasses) );
}
$this->config[$reference] = $value; $this->config[$reference] = $value;
return $this; return $this;
@ -132,62 +114,6 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
} }
/**
* Add a field to the editable fields blacklist
*
* @param string $fieldName
* @return boolean
*/
function addFieldNameToBlacklist ( $fieldName )
{
return array_push( $this->config['fieldsNameBlacklist'], $fieldName);
}
/**
* Add a class to the readonly list
*
* @param string $className
* @return boolean
*/
function addClassToReadOnlyList ( $className )
{
return array_push( $this->config['readOnlyFieldClasses'], $className);
}
/**
* Remove a field to the editable fields blacklist
*
* @param string $fieldName
* @return boolean
*/
function removeFieldNameFromBlacklist ( $fieldName )
{
if (key_exists($fieldName, $this->config['fieldsNameBlacklist'])) {
return delete( $this->config['fieldsNameBlacklist'][$fieldName] );
}else{
return false;
}
}
/**
* Remove a class to the readonly list
*
* @param string $className
* @return boolean
*/
function removeClassFromReadOnlyList ( $className )
{
if (key_exists($className, $this->config['readOnlyFieldClasses']) && !in_array($className, $this->forbiddenFieldsClasses)) {
return delete( $this->config['readOnlyFieldClasses'][$className] );
}else{
return false;
}
}
/** /**
* Lets you add custom bulk actions to the bulk manager interface * Lets you add custom bulk actions to the bulk manager interface
* *
@ -379,7 +305,7 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
'Menu' => $dropDownActionsList->FieldHolder(), 'Menu' => $dropDownActionsList->FieldHolder(),
'Button' => array( 'Button' => array(
'Label' => _t('GRIDFIELD_BULK_MANAGER.ACTION_BTN_LABEL', 'Go'), 'Label' => _t('GRIDFIELD_BULK_MANAGER.ACTION_BTN_LABEL', 'Go'),
'DataURL' => $gridField->Link('bulkaction'), 'DataURL' => $gridField->Link('bulkAction'),
'Icon' => $this->config['actions'][$firstAction]['config']['icon'], 'Icon' => $this->config['actions'][$firstAction]['config']['icon'],
'DataConfig' => htmlspecialchars(json_encode($actionsConfig), ENT_QUOTES, 'UTF-8') 'DataConfig' => htmlspecialchars(json_encode($actionsConfig), ENT_QUOTES, 'UTF-8')
), ),
@ -410,7 +336,7 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
*/ */
public function getURLHandlers($gridField) { public function getURLHandlers($gridField) {
return array( return array(
'bulkaction' => 'handlebulkaction' 'bulkAction' => 'handleBulkAction'
); );
} }
@ -427,7 +353,7 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
* @param SS_HTTPRequest $request * @param SS_HTTPRequest $request
* @return mixed * @return mixed
*/ */
public function handlebulkaction($gridField, $request) public function handleBulkAction($gridField, $request)
{ {
$controller = $gridField->getForm()->Controller(); $controller = $gridField->getForm()->Controller();

View File

@ -10,7 +10,7 @@
onunmatch: function(){}, onunmatch: function(){},
onclick: function(e) onclick: function(e)
{ {
var toggleFields = this.parents('#Form_BulkEditingForm').find('.ss-toggle .ui-accordion-header'), var toggleFields = this.parents('form').find('.ss-toggle .ui-accordion-header'),
state = this.data('state') state = this.data('state')
; ;
@ -58,71 +58,5 @@
} }
}); });
/**
* Save all button
* process all field holders with updates
*/
$('#bulkEditingUpdateBtn').entwine({
onmatch: function(){},
onunmatch: function(){},
onclick: function(e){
e.stopImmediatePropagation();
var $fieldHolders = $('div.bulkEditingFieldHolder.hasUpdate'),
url = this.data('url'),
data = {},
cacheBuster = new Date().getTime() + '_' + this.attr('name')
;
if ( $fieldHolders.length > 0 )
{
this.addClass('loading');
}
else{
return;
}
if ( url.indexOf('?') !== -1 )
{
cacheBuster = '&cacheBuster=' + cacheBuster;
}
else{
cacheBuster = '?cacheBuster=' + cacheBuster;
}
$fieldHolders.each(function(){
var $this = $(this);
data[$this.data('id')] = $this.find(':input').serializeArray();
});
$.ajax({
url: url + cacheBuster,
data: data,
type: "POST",
context: this
}).success(function(data, textStatus, jqXHR){
try{
data = $.parseJSON(data);
}catch(er){}
$.each(data.records, function(index, record){
var $fieldHolder = $('#Form_BulkEditingForm_RecordFields_'+record.id),
$header = $fieldHolder.find('.ui-accordion-header')
;
$fieldHolder.removeClass('hasUpdate').addClass('updated');
$header.find('a').html(record.title);
if ( $header.hasClass('ui-state-active') )
{
$header.click();
}
});
this.removeClass('loading');
});
}
});
}); });
}(jQuery)); }(jQuery));

View File

@ -20,3 +20,4 @@ en:
TOGGLE_ALL_LINK: Show/Hide all TOGGLE_ALL_LINK: Show/Hide all
SAVE_BTN_LABEL: Save all SAVE_BTN_LABEL: Save all
CANCEL_BTN_LABEL: Cancel CANCEL_BTN_LABEL: Cancel
SAVE_RESULT_TEXT: {count} {class} saved successfully.