NEW Many-many relation data editing in GridFieldDetailForm

Also adds GridFieldDetailForm->setFields() for customizing
the displayed form fields (required for adding relational fields).
This commit is contained in:
Ingo Schommer 2012-11-08 01:26:38 +01:00
parent 46dc8ae1e5
commit c8136f5d4c
4 changed files with 166 additions and 10 deletions

View File

@ -78,7 +78,7 @@ We will now move onto what the `GridFieldConfig`s are and how to use them.
----
## GridFieldConfig
## Configuration
A gridfields's behaviour and look all depends on what config we're giving it. In the above example
we did not specify one, so it picked a default config called `GridFieldConfig_Base`.
@ -98,7 +98,7 @@ A config object can be either injected as the fourth argument of the GridField c
The framework comes shipped with some base GridFieldConfigs:
### GridFieldConfig_Base
### Table listing with GridFieldConfig_Base
A simple read-only and paginated view of records with sortable and searchable headers.
@ -107,7 +107,7 @@ A simple read-only and paginated view of records with sortable and searchable he
The fields displayed are from `DataObject::getSummaryFields()`
### GridFieldConfig_RecordViewer
### Viewing records with GridFieldConfig_RecordViewer
Similar to `GridFieldConfig_Base` with the addition support of:
@ -118,7 +118,7 @@ The fields displayed in the read-only view is from `DataObject::getCMSFields()`
:::php
$gridField = new GridField('pages', 'All pages', SiteTree::get(), GridFieldConfig_RecordViewer::create());
### GridFieldConfig_RecordEditor
### Editing records with GridFieldConfig_RecordEditor
Similar to `GridFieldConfig_RecordViewer` with the addition support of:
@ -130,7 +130,7 @@ Similar to `GridFieldConfig_RecordViewer` with the addition support of:
The fields displayed in the edit form are from `DataObject::getCMSFields()`
### GridFieldConfig_RelationEditor
### Editing relations with GridFieldConfig_RelationEditor
Similar to `GridFieldConfig_RecordEditor`, but adds features to work on a record's has-many or
many-many relationships. As such, it expects the list used with the `GridField` to be a
@ -147,9 +147,61 @@ The relations can be:
The fields displayed in the edit form are from `DataObject::getCMSFields()`
## Customizing Detail Forms
The `GridFieldDetailForm` component drives the record editing form which is usually configured
through the configs `GridFieldConfig_RecordEditor` and `GridFieldConfig_RelationEditor`
described above. It takes its fields from `DataObject->getCMSFields()`,
but can be customized to accept different fields via its `[api:GridFieldDetailForm->setFields()](api:setFields())` method.
The component also has the ability to load and save data stored on join tables
when two records are related via a "many_many" relationship, as defined through
`[api:DataObject::$many_many_extraFields]`. While loading and saving works transparently,
you need to add the necessary fields manually, they're not included in the `getCMSFields()` scaffolding.
These extra fields act like usual form fields, but need to be "namespaced"
in order for the gridfield logic to detect them as fields for relation extradata,
and to avoid clashes with the other form fields.
The namespace notation is `ManyMany[<extradata-field-name>]`, so for example
`ManyMany[MyExtraField]`.
Example:
:::php
class Player extends DataObject {
public static $db = array('Name' => 'Text');
public static $many_many = array('Teams' => 'Team');
public static $many_many_extraFields = array(
'Teams' => array('Position' => 'Text')
);
public function getCMSFields() {
$fields = parent::getCMSFields();
if($this->ID) {
$teamFields = singleton('Team')->getCMSFields();
$teamFields->addFieldToTab(
'Root.Main',
// Please follow the "ManyMany[<extradata-name>]" convention
new TextField('ManyMany[Position]', 'Current Position')
);
$config = GridFieldConfig_RelationEditor::create();
$config->getComponentByType('GridFieldDetailForm')->setFields($teamFields);
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
$fields->findOrMakeTab('Root.Teams')->replaceField('Teams', $gridField);
}
return $fields;
}
}
class Team extends DataObject {
public static $db = array('Name' => 'Text');
public static $many_many = array('Players' => 'Player');
}
## GridFieldComponents
GridFieldComponents the actual workers in a gridfield. They can be responsible for:
The `GridFieldComponent` classes are the actual workers in a gridfield. They can be responsible for:
- Output some HTML to be rendered
- Manipulate data

View File

@ -152,6 +152,8 @@ Consider replacing it with a more powerful interface in case you have many recor
Has-many and many-many relationships are usually handled via the `[GridField](/reference/grid-field)` class,
more specifically the `[api:GridFieldAddExistingAutocompleter]` and `[api:GridFieldRelationDelete]` components.
They provide a list/detail interface within a single record edited in your ModelAdmin.
The `[GridField](/reference/grid-field)` docs also explain how to manage
extra relation fields on join tables through its detail forms.
## Permissions

View File

@ -28,6 +28,11 @@ class GridFieldDetailForm implements GridField_URLHandler {
*/
protected $validator;
/**
* @var FieldList Falls back to {@link DataObject->getCMSFields()} if not defined.
*/
protected $fields;
/**
* @var String
*/
@ -127,6 +132,21 @@ class GridFieldDetailForm implements GridField_URLHandler {
return $this->validator;
}
/**
* @param FieldList $fields
*/
public function setFields(FieldList $fields) {
$this->fields = $fields;
return $this;
}
/**
* @return FieldList
*/
public function getFields() {
return $this->fields;
}
/**
* @param String
*/
@ -281,6 +301,8 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
* @return Form
*/
public function ItemEditForm() {
$list = $this->gridField->getList();
if (empty($this->record)) {
$controller = Controller::curr();
$noActionURL = $controller->removeAction($_REQUEST['url']);
@ -319,16 +341,25 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
$actions->push(new LiteralField('cancelbutton', $text));
}
}
$fields = $this->component->getFields();
if(!$fields) $fields = $this->record->getCMSFields();
$form = new Form(
$this,
'ItemEditForm',
$this->record->getCMSFields(),
$fields,
$actions,
$this->component->getValidator()
);
$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
// Load many_many extraData for record.
// Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
if($list instanceof ManyManyList) {
$extraData = $list->getExtraData('', $this->record->ID);
$form->loadDataFrom(array('ManyMany' => $extraData));
}
// TODO Coupling with CMS
$toplevelController = $this->getToplevelController();
if($toplevelController && $toplevelController instanceof LeftAndMain) {
@ -389,11 +420,19 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
public function doSave($data, $form) {
$new_record = $this->record->ID == 0;
$controller = Controller::curr();
$list = $this->gridField->getList();
if($list instanceof ManyManyList) {
// Data is escaped in ManyManyList->add()
$extraData = (isset($data['ManyMany'])) ? $data['ManyMany'] : null;
} else {
$extraData = null;
}
try {
$form->saveInto($this->record);
$this->record->write();
$this->gridField->getList()->add($this->record);
$list->add($this->record, $extraData);
} catch(ValidationException $e) {
$form->sessionMessage($e->getResult()->message(), 'bad');
$responseNegotiator = new PjaxResponseNegotiator(array(

View File

@ -106,6 +106,45 @@ class GridFieldDetailFormTest extends FunctionalTest {
$this->assertDOSContains(array(array('Surname' => 'Baggins')), $group->People());
}
public function testEditFormWithManyManyExtraData() {
$this->logInWithPermission('ADMIN');
// Lists all categories for a person
$response = $this->get('GridFieldDetailFormTest_CategoryController');
$this->assertFalse($response->isError());
$parser = new CSSContentParser($response->getBody());
$editlinkitem = $parser->getBySelector('.ss-gridfield-items .first .edit-link');
$editlink = (string) $editlinkitem[0]['href'];
// Edit a single category, incl. manymany extrafields added manually
// through GridFieldDetailFormTest_CategoryController
$response = $this->get($editlink);
$this->assertFalse($response->isError());
$parser = new CSSContentParser($response->getBody());
$editform = $parser->getBySelector('#Form_ItemEditForm');
$editformurl = (string) $editform[0]['action'];
$manyManyField = $parser->getByXpath('//*[@id="Form_ItemEditForm"]//input[@name="ManyMany[IsPublished]"]');
$this->assertTrue((bool)$manyManyField);
$response = $this->post(
$editformurl,
array(
'Name' => 'Updated Category',
'ManyMany' => array('IsPublished' => 1),
'action_doSave' => 1
)
);
$this->assertFalse($response->isError());
$person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First();
$category = $person->Categories()->filter(array('Name' => 'Updated Category'))->First();
$this->assertEquals(
array('IsPublished' => 1),
$person->Categories()->getExtraData('', $category->ID)
);
}
public function testNestedEditForm() {
$this->logInWithPermission('ADMIN');
@ -193,6 +232,12 @@ class GridFieldDetailFormTest_Person extends DataObject implements TestOnly {
'Categories' => 'GridFieldDetailFormTest_Category'
);
static $many_many_extraFields = array(
'Categories' => array(
'IsPublished' => 'Boolean'
)
);
static $default_sort = 'FirstName';
public function getCMSFields() {
@ -289,5 +334,23 @@ class GridFieldDetailFormTest_GroupController extends Controller implements Test
}
}
class GridFieldDetailFormTest_ItemRequest extends GridFieldDetailForm_ItemRequest implements TestOnly {
class GridFieldDetailFormTest_CategoryController extends Controller implements TestOnly {
protected $template = 'BlankPage';
public function Form() {
// GridField lists categories for a specific person
$person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First();
$detailFields = singleton('GridFieldDetailFormTest_Category')->getCMSFields();
$detailFields->addFieldToTab('Root.Main', new CheckboxField('ManyMany[IsPublished]'));
$field = new GridField('testfield', 'testfield', $person->Categories());
$field->getConfig()->addComponent($gridFieldForm = new GridFieldDetailForm($this, 'Form'));
$gridFieldForm->setFields($detailFields);
$field->getConfig()->addComponent(new GridFieldToolbarHeader());
$field->getConfig()->addComponent(new GridFieldAddNewButton('toolbar-header-right'));
$field->getConfig()->addComponent(new GridFieldEditButton());
return new Form($this, 'Form', new FieldList($field), new FieldList());
}
}
class GridFieldDetailFormTest_ItemRequest extends GridFieldDetailForm_ItemRequest implements TestOnly {
}