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
The available configuration options are:
* '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
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
* @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
*/
private static $url_handlers = array(
'bulkEdit/update' => 'update',
'bulkEdit' => 'edit'
'bulkEdit/bulkEditForm' => 'bulkEditForm',
'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
* with their respective editable fields.
*
* @return Form Selected DataObjects editable fields
*/
public function editForm()
public function bulkEditForm()
{
$crumbs = $this->Breadcrumbs();
if($crumbs && $crumbs->count()>=2)
@ -42,13 +58,11 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$actions = new FieldList();
$actions->push(
FormAction::create('SaveAll', _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.SAVE_BTN_LABEL', 'Save all'))
->setAttribute('id', 'bulkEditingUpdateBtn')
->addExtraClass('ss-ui-action-constructive cms-panel-link')
FormAction::create('doSave', _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.SAVE_BTN_LABEL', 'Save all'))
->setAttribute('id', 'bulkEditingSaveBtn')
->addExtraClass('ss-ui-action-constructive')
->setAttribute('data-icon', 'accept')
->setAttribute('data-url', $this->gridField->Link('bulkaction/bulkEdit/update'))
->setUseButtonTag(true)
->setAttribute('src', '')//changes type to image so isn't hooked by default actions handlers
);
$actions->push(
@ -70,6 +84,7 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$singleton = singleton($modelClass);
$titleModelClass = (($editingCount > 1) ? $singleton->i18n_plural_name() : $singleton->i18n_singular_name());
//some cosmetics
$headerText = _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.HEADER_TEXT',
'Editing {count} {class}',
array(
@ -85,10 +100,12 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$toggle = LiteralField::create('bulkEditToggle', '<span id="bulkEditToggle">' . _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.TOGGLE_ALL_LINK', 'Show/Hide all') . '</span>');
$recordsFieldList->push($toggle);
//fetch fields for each record and push to fieldList
foreach ( $recordList as $id )
{
$record = DataObject::get_by_id($modelClass, $id);
{
$record = DataObject::get_by_id($modelClass, $id);
$recordEditingFields = $this->getRecordEditingFields($record);
$toggleField = ToggleCompositeField::create(
@ -103,18 +120,72 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$recordsFieldList->push($toggleField);
}
$form = new Form(
$bulkEditForm = Form::create(
$this,
'BulkEditingForm',
'recordEditForm', //recordEditForm name is here to trick SS to pass all subform request to recordEditForm()
$recordsFieldList,
$actions
);
);
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();
$editableFields = $config['editableFields'];
$fieldsNameBlacklist = $config['fieldsNameBlacklist'];
$readOnlyClasses = $config['readOnlyFieldClasses'];
// get all dataFields or just the ones allowed in config
if ( $editableFields )
@ -179,39 +248,62 @@ class GridFieldBulkActionEditHandler extends GridFieldBulkActionHandler
$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
foreach ( $dataFields as $name => $field )
{
$field->Name = 'record_' . $id . '_' . $name;
$field->Name = $this->escapeFieldName($id, $name);
$dataFields[$name] = $field;
}
return $dataFields;
}
/**
* 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 editing interface
* Creates and return the bulk editing interface
*
* @return string Form's HTML
*/
public function edit()
public function index()
{
$form = $this->editForm();
$form = $this->bulkEditForm();
$form->setTemplate('LeftAndMain_EditForm');
$form->addExtraClass('center cms-content');
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
@ -236,49 +328,72 @@ 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()
{
$data = $this->request->requestVars();
$return = array();
$className = $this->gridField->list->dataClass;
public function doSave($data, $form)
{
$className = $this->gridField->list->dataClass;
$singleton = singleton($className);
if ( isset($data['url']) ) unset($data['url']);
if ( isset($data['cacheBuster']) ) unset($data['cacheBuster']);
if ( isset($data['locale']) ) unset($data['locale']);
$formsData = array();
$ids = array();
$done = 0;
foreach ($data as $recordID => $recordDataSet)
//unescape and sort form data per record ID
foreach ($data as $fieldName => $value)
{
$record = DataObject::get_by_id($className, $recordID);
foreach($recordDataSet as $recordData)
if ( $fieldInfo = $this->unEscapeFieldName($fieldName) )
{
$field = preg_replace('/record_(\d+)_(\w+)/i', '$2', $recordData['name']);
$value = $recordData['value'];
if ( !isset($formsData[$fieldInfo['id']]) )
{
$formsData[$fieldInfo['id']] = array();
}
if ( $record->hasMethod($field) )
{
$list = $record->$field();
$list->setByIDList($value);
}
else{
$record->setCastedField($field, $value);
}
$formsData[$fieldInfo['id']][$fieldInfo['name']] = $value;
}
$done = $record->write();
array_push($return, array(
'id' => $done,
'title' => $record->getTitle()
));
}
return json_encode(array(
'done' => 1,
'records' => $return
), JSON_NUMERIC_CHECK);
//process each record's form data and save
foreach ($formsData as $recordID => $recordData)
{
$record = DataObject::get_by_id($className, $recordID);
$recordForm = Form::create(
$this, "RecordForm",
$record->getCMSFields(),
FieldList::create()
);
$recordForm->loadDataFrom($recordData);
$recordForm->saveInto($record);
$id = $record->write();
array_push($ids, $record->ID);
if ( $id )
{
$done++;
}
}
//compose form message
$messageModelClass = (($editingCount > 1) ? $singleton->i18n_plural_name() : $singleton->i18n_singular_name());
$message = _t('GRIDFIELD_BULKMANAGER_EDIT_HANDLER.SAVE_RESULT_TEXT',
'{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)
{
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
*
* '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
*
* @var array
*/
protected $config = array(
'editableFields' => null,
'fieldsNameBlacklist' => array(),
'readOnlyFieldClasses' => array(),
'actions' => array()
'editableFields' => null,
'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
*
@ -42,7 +31,6 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
public function __construct($editableFields = null, $defaultActions = true)
{
if ( $editableFields != null ) $this->setConfig ( 'editableFields', $editableFields );
$this->config['readOnlyFieldClasses'] = $this->readOnlyFieldClasses;
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);
}
if ( ($reference == 'readOnlyFieldClasses' || $reference == 'fieldsNameBlacklist' || $reference == 'editableFields') && !is_array($value) )
if ( ($reference == 'editableFields') && !is_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;
return $this;
@ -130,62 +112,6 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
if ( $reference ) return $this->config[$reference];
else return $this->config;
}
/**
* 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;
}
}
/**
@ -379,7 +305,7 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
'Menu' => $dropDownActionsList->FieldHolder(),
'Button' => array(
'Label' => _t('GRIDFIELD_BULK_MANAGER.ACTION_BTN_LABEL', 'Go'),
'DataURL' => $gridField->Link('bulkaction'),
'DataURL' => $gridField->Link('bulkAction'),
'Icon' => $this->config['actions'][$firstAction]['config']['icon'],
'DataConfig' => htmlspecialchars(json_encode($actionsConfig), ENT_QUOTES, 'UTF-8')
),
@ -410,7 +336,7 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
*/
public function getURLHandlers($gridField) {
return array(
'bulkaction' => 'handlebulkaction'
'bulkAction' => 'handleBulkAction'
);
}
@ -427,7 +353,7 @@ class GridFieldBulkManager implements GridField_HTMLProvider, GridField_ColumnPr
* @param SS_HTTPRequest $request
* @return mixed
*/
public function handlebulkaction($gridField, $request)
public function handleBulkAction($gridField, $request)
{
$controller = $gridField->getForm()->Controller();

View File

@ -10,7 +10,7 @@
onunmatch: function(){},
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')
;
@ -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));

View File

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