Skip to content

Commit

Permalink
Merge pull request #15 from PortableSteve/merge-ss4-changes
Browse files Browse the repository at this point in the history
Merge ss4 changes
  • Loading branch information
stevie-mayhew authored Mar 14, 2019
2 parents da45ca9 + c0b19fd commit 36113cd
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 64 deletions.
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Has One Edit

This module allows you to directly edit the fields of a related has\_one object directly, without having to mess around with GridField or links to ModelAdmin. If the related has\_one doesn't exist yet, then this module also creates the object and sets up the relation for you on first write.
This module allows you to directly edit the fields of a related `has_one` object directly, without having to mess around with `GridField` or links to `ModelAdmin`. If the related `has_one` doesn't exist yet, then this module also creates the object and sets up the relation for you on first write.

This module has been tested editing a has\_one in both a GridFieldDetailForm and on a generic Page in CMSMain.
This module has been tested editing a `has_one` in both a `GridFieldDetailForm` and on a generic `Page` in `CMSMain`.

## Requirements

Expand All @@ -14,10 +14,25 @@ This module has been tested on both 3.0.x-dev and 3.1.x-dev. There is no separat

To use this module, simply add a field to the CMS fields for your object in your `getCMSFields()` method. The name of the field should be `HasOneName-_1_-FieldName`.

For example, say you have a has\_one called `Show` and that has\_one has a field called `Title` you want to edit. You'd add the field `TextField::create('Show-_1_-Title', 'Show Title')`.
For example, say you have a `has_one` called `Show` and that `has_one` has a field called `Title` you want to edit. You'd add the field `TextField::create('Show-_1_-Title', 'Show Title')`.

If you do not require that the outputted name of the field matches the value you supply, you can also use a colon as a separator instead of `-_1_-`.

### Generating fields with the `ProvidesHasOneInlineFields` trait

If you simply want to display all the CMS fields for a related object, you can add the `ProvidesHasOneInlineFields` trait to the object. This adds a method which calls `getCMSFields()`
on your `DataObject` and return the `FormField`s for that object. Those `FormField`s will be converted for use with this module by adding the relation name and separator to their name.

In the owning object, where you want to display the fields, call `HasOneEdit::getInlineFields($this, 'my_has_one_name')`. This will return the fields for adding to the CMS - e.g. you
can display the related object's fields in their own tab by calling `$fields->addFieldsToTab('Root.RelatedObject', HasOneEdit::getInlineFields($this, 'Relation'))`.

This has the advantage of running the entire `getCMSFields()` call tree (e.g. `updateCMSFields` for any functionality provided via extension) etc. without having to repeat logic
in a lot of places.

You can also implement a method `public function provideHasOneInlineFields($relationName)` returning `FieldList|FormField[]` to provide a custom interface different
to `getCMSFields()` (e.g. just a small subset of fields). In this case, all the field names should be in the form `$relationName . HasOneEdit::FIELD_SEPARATOR . $dataObjectFieldName`.
This method will be called by `HasOneEdit::getInlineFields` even if your class does not use the `ProvidesHasOneInlineFields` trait.

### Using with your own form

To add support to your own forms, you need to add the `sgn_hasoneedit_UpdateFormExtension` extension to your controller and call `$this->extend('updateEditForm', $form)` before returning the form to the template. Without this, the fields will not get populated with the values from the has\_one though saving will work.
To add support to your own forms, you need to add the `sgn_hasoneedit_UpdateFormExtension` extension to your controller and call `$this->extend('updateEditForm', $form)` before returning the form to the template. Without this, the fields will not get populated with the values from the `has_one` though saving will work.
48 changes: 23 additions & 25 deletions code/DataObjectExtension.php
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
<?php

class sgn_hasoneedit_DataObjectExtension extends DataExtension {
const separator = '-_1_-';

public function onBeforeWrite() {
/**
* @see DataObject::onBeforeWrite}
*/
public function onBeforeWrite()
{
$changed = $this->owner->getChangedFields();
$toWrite = array();
foreach($changed as $name => $value) {
if(!strpos($name, self::separator)) {
// Also skip $name that starts with a separator
continue;
}
$value = (string)$value['after'];
list($hasone, $key) = explode(self::separator, $name, 2);
if($this->owner->has_one($hasone) || $this->owner->belongs_to($hasone)) {
$rel = $this->owner->getComponent($hasone);

// Get original:
$original = (string)$rel->__get($key);
if($original !== $value) {
$rel->setCastedField($key, $value);
$toWrite[$hasone] = $rel;
}
}
$toWrite = [];

foreach ($changed as $name => $value) {
if (!HasOneEdit::isHasOneEditField($name)) continue;

list($relationName, $fieldOnRelation) = HasOneEdit::getRelationNameAndField($name);
$relatedObject = HasOneEdit::getRelationRecord($this->owner, $relationName);
if ($relatedObject === null) continue;

$relatedObject->setCastedField($fieldOnRelation, $value['after']);
if ($relatedObject->isChanged(null, DataObject::CHANGE_VALUE)) {
$toWrite[$relationName] = $relatedObject;
}

}
foreach($toWrite as $rel => $obj) {

foreach ($toWrite as $relationName => $obj) {
$obj->write();
$key = $rel . 'ID';
if(!$this->owner->$key) {
$this->owner->$key = $obj->ID;
}
$this->owner->setField("{$relationName}ID", $obj->ID);
}
}

}
80 changes: 80 additions & 0 deletions code/HasOneEdit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

/**
* Class HasOneEdit
*/
class HasOneEdit
{
/**
*
*/
const FIELD_SEPARATOR = '-_1_-';

/**
*
*/
const SUPPORTED_SEPARATORS = [
self::FIELD_SEPARATOR,
':',
'/',
];

/**
* @param FormField|string $field
* @return string[] Array of [relation name, field on relation]
*/
public static function getRelationNameAndField($field)
{
if (!is_string($field)) {
$field = $field->getName();
}

return explode(static::FIELD_SEPARATOR, $field, 2);
}

/**
* @param DataObject $parent
* @param string $relationName
* @return DataObject|null
*/
public static function getRelationRecord(DataObject $parent, $relationName)
{
return ($parent->hasOneComponent($relationName) || $parent->belongsToComponent($relationName, false))
? $parent->getComponent($relationName)
: null;
}

/**
* @param FormField|string $field
* @return bool
*/
public static function isHasOneEditField($field)
{
if (!is_string($field)) {
$field = $field->getName();
}

return boolval(strpos($field, static::FIELD_SEPARATOR));
}

/**
* @param string $fieldName
* @return string
*/
public static function normaliseSeparator($fieldName)
{
return str_replace(static::SUPPORTED_SEPARATORS, static::FIELD_SEPARATOR, $fieldName);
}

/**
* @param DataObject $parent
* @param string $relation
* @return FieldList|FormField[]
*/
public static function getInlineFields(DataObject $parent, $relation)
{
/** @var DataObject|ProvidesHasOneInlineFields $relatedObject */
$relatedObject = static::getRelationRecord($parent, $relation);
return $relatedObject->provideHasOneInlineFields($relation);
}
}
63 changes: 63 additions & 0 deletions code/HasOneUploadField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/**
* Class HasOneUploadField
*/
class HasOneUploadField extends UploadField
{

/**
* HasOneUploadField constructor.
* @param UploadField $original
*/
public function __construct(UploadField $original)
{
if (!HasOneEdit::isHasOneEditField($original)) {
throw new InvalidArgumentException('Original upload field passed to HasOneUploadField must have the has_one separator "' .
HasOneEdit::FIELD_SEPARATOR . '" in its name.');
}

parent::__construct($original->getName(), $original->title, $original->getItems());

// Copy state from original upload field
foreach (get_object_vars($original) as $prop => $value) {
$this->{$prop} = $value;
}
}

/**
* @see UploadField::saveInto()
* @inheritDoc
*/
public function saveInto(DataObjectInterface $record)
{
list($relationName, $fieldOnRelation) = HasOneEdit::getRelationNameAndField($this);
$record = HasOneEdit::getRelationRecord($this->getRecord(), $relationName);

// Check type of relation
$relation = $record->hasMethod($fieldOnRelation) ? $record->$fieldOnRelation() : null;
if ($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
// has_many or many_many
$relation->setByIDList($this->getItemIDs());
} else if ($class = $record->hasOneComponent($fieldOnRelation)) {
// Get details to save
$idList = $this->getItemIDs();

// Assign has_one ID
$id = !empty($idList) ? reset($idList) : 0;
$record->setField("{$fieldOnRelation}ID", $id);

// Polymorphic asignment
if ($class === DataObject::class) {
$file = $id ? File::get()->byID($id) : null;
$fileClass = $file ? get_class($file) : File::class;
$record->{"{$fieldOnRelation}Class"} = $id ? $fileClass : null;
}

// Write has one record
$record->write();
}

return $this;
}
}
23 changes: 23 additions & 0 deletions code/ProvidesHasOneInlineFields.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/**
* Trait ProvidesHasOneInlineFields
* @mixin DataObject
*/
trait ProvidesHasOneInlineFields
{
/**
* @param string $relationName
* @return FieldList|FormField[]
*/
public function provideHasOneInlineFields($relationName)
{
$fields = $this->getCMSFields()->dataFields();

foreach ($fields as $name => $field) {
$field->setName($relationName . HasOneEdit::FIELD_SEPARATOR . $field->getName());
}

return $fields;
}
}
80 changes: 45 additions & 35 deletions code/UpdateFormExtension.php
Original file line number Diff line number Diff line change
@@ -1,43 +1,53 @@
<?php

class sgn_hasoneedit_UpdateFormExtension extends \Extension {
public function updateEditForm(\Form $form) {
class sgn_hasoneedit_UpdateFormExtension extends Extension {

/**
* @param Form $form
*/
public function updateItemEditForm(Form $form)
{
$this->updateEditForm($form);
}

/**
* @param Form $form
*/
public function updateEditForm(Form $form)
{
$record = $form->getRecord();
$fields = $form->Fields()->dataFields();

foreach($fields as $name => $field) {
$name = str_replace(array(':', '/'), sgn_hasoneedit_DataObjectExtension::separator, $name);
if(!strpos($name, sgn_hasoneedit_DataObjectExtension::separator)) {
// Also skip $name that starts with a separator
continue;
}
$fieldList = $form->Fields();

foreach ($fieldList->dataFields() as $name => $field) {
$name = HasOneEdit::normaliseSeparator($name);
if (!HasOneEdit::isHasOneEditField($name)) continue;

$field->setName($name);
if(!$record) {
continue;
}
if($field->Value()) {
// Skip fields that already have a value
continue;
}
list($hasone, $key) = explode(sgn_hasoneedit_DataObjectExtension::separator, $name, 2);
if($record->has_one($hasone)) {
$rel = $record->getComponent($hasone);
// Copied from loadDataFrom()
$exists = (
isset($rel->$key) ||
$rel->hasMethod($key) ||
($rel->hasMethod('hasField') && $rel->hasField($key))
);

if($exists) {
$value = $rel->__get($key);
$field->setValue($value);
}
}

if ($field instanceof UploadField) {
$field = HasOneUploadField::create($field);
$fieldList->replaceField($name, $field);
}

// Skip populating value if record doesn't exist yet, or field already has value
if (!$record || $field->Value()) continue;

list($relationName, $fieldOnRelation) = HasOneEdit::getRelationNameAndField($name);
$relatedObject = HasOneEdit::getRelationRecord($record, $relationName);
if ($relatedObject === null) continue;

if ($field instanceof HasOneUploadField) {
if ($relatedObject->hasField("{$fieldOnRelation}ID")) {
$field->setValue([ 'Files' => [ $relatedObject->getField("{$fieldOnRelation}ID") ] ]);
}

} else {
if ($relatedObject->hasField($fieldOnRelation)) {
$field->setValue($relatedObject->getField($fieldOnRelation));
}
}

}
}

public function updateItemEditForm(\Form $form) {
$this->updateEditForm($form);
}
}

0 comments on commit 36113cd

Please sign in to comment.