From c8136f5d4c8a37a4da274cd1c93907c0a2af86a7 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 8 Nov 2012 01:26:38 +0100 Subject: [PATCH] NEW Many-many relation data editing in GridFieldDetailForm Also adds GridFieldDetailForm->setFields() for customizing the displayed form fields (required for adding relational fields). --- docs/en/reference/grid-field.md | 64 ++++++++++++++++-- docs/en/reference/modeladmin.md | 2 + forms/gridfield/GridFieldDetailForm.php | 45 ++++++++++++- .../gridfield/GridFieldDetailFormTest.php | 65 ++++++++++++++++++- 4 files changed, 166 insertions(+), 10 deletions(-) diff --git a/docs/en/reference/grid-field.md b/docs/en/reference/grid-field.md index 465786a1e..dfe52bf06 100644 --- a/docs/en/reference/grid-field.md +++ b/docs/en/reference/grid-field.md @@ -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[]`, 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[]" 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 diff --git a/docs/en/reference/modeladmin.md b/docs/en/reference/modeladmin.md index c584c15f5..7a01fe4f5 100644 --- a/docs/en/reference/modeladmin.md +++ b/docs/en/reference/modeladmin.md @@ -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 diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php index 0f1c37fe5..23818c217 100644 --- a/forms/gridfield/GridFieldDetailForm.php +++ b/forms/gridfield/GridFieldDetailForm.php @@ -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( diff --git a/tests/forms/gridfield/GridFieldDetailFormTest.php b/tests/forms/gridfield/GridFieldDetailFormTest.php index fc7a66529..c883aefd3 100644 --- a/tests/forms/gridfield/GridFieldDetailFormTest.php +++ b/tests/forms/gridfield/GridFieldDetailFormTest.php @@ -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 { +} \ No newline at end of file