Skip to content

Commit

Permalink
NEW Allow DataObject classes to define scaffolded relation formfields
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Jun 6, 2024
1 parent e35f12c commit cd72258
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 55 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"psr/http-message": "^1",
"sebastian/diff": "^4.0",
"silverstripe/config": "^2",
"silverstripe/assets": "^2.2",
"silverstripe/assets": "^2.3",
"silverstripe/vendor-plugin": "^2",
"sminnee/callbacklist": "^0.1.1",
"symfony/cache": "^6.1",
Expand Down
89 changes: 54 additions & 35 deletions src/Forms/FormScaffolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,27 +138,37 @@ public function getFieldList()
&& ($this->includeRelations === true || isset($this->includeRelations['has_many']))
) {
foreach ($this->obj->hasMany() as $relationship => $component) {
if ($this->tabbed) {
$fields->findOrMakeTab(
"Root.$relationship",
$this->obj->fieldLabel($relationship)
);
}
$includeInOwnTab = true;
$fieldLabel = $this->obj->fieldLabel($relationship);
$fieldClass = (isset($this->fieldClasses[$relationship]))
? $this->fieldClasses[$relationship]
: 'SilverStripe\\Forms\\GridField\\GridField';
/** @var GridField $grid */
$grid = Injector::inst()->create(
$fieldClass,
$relationship,
$this->obj->fieldLabel($relationship),
$this->obj->$relationship(),
GridFieldConfig_RelationEditor::create()
);
: null;
if ($fieldClass) {
/** @var GridField */
$hasManyField = Injector::inst()->create(
$fieldClass,
$relationship,
$fieldLabel,
$this->obj->$relationship(),
GridFieldConfig_RelationEditor::create()
);
} else {
/** @var DataObject */
$hasManySingleton = singleton($component);
$hasManyField = $hasManySingleton->scaffoldFormFieldForHasMany($relationship, $fieldLabel, $this->obj, $includeInOwnTab);
}
if ($this->tabbed) {
$fields->addFieldToTab("Root.$relationship", $grid);
if ($includeInOwnTab) {
$fields->findOrMakeTab(
"Root.$relationship",
$fieldLabel
);
$fields->addFieldToTab("Root.$relationship", $hasManyField);
} else {
$fields->addFieldToTab('Root.Main', $hasManyField);
}
} else {
$fields->push($grid);
$fields->push($hasManyField);
}
}
}
Expand Down Expand Up @@ -187,7 +197,7 @@ public function getFieldList()
*
* @param FieldList $fields Reference to the @FieldList to add fields to.
* @param string $relationship The relationship identifier.
* @param mixed $overrideFieldClass Specify the field class to use here or leave as null to use default.
* @param string|null $overrideFieldClass Specify the field class to use here or leave as null to use default.
* @param bool $tabbed Whether this relationship has it's own tab or not.
* @param DataObject $dataObject The @DataObject that has the relation.
*/
Expand All @@ -198,28 +208,37 @@ public static function addManyManyRelationshipFields(
$tabbed,
DataObject $dataObject
) {
if ($tabbed) {
$fields->findOrMakeTab(
"Root.$relationship",
$dataObject->fieldLabel($relationship)
$includeInOwnTab = true;
$fieldLabel = $dataObject->fieldLabel($relationship);

if ($overrideFieldClass) {
/** @var GridField */
$manyManyField = Injector::inst()->create(
$overrideFieldClass,
$relationship,
$fieldLabel,
$dataObject->$relationship(),
GridFieldConfig_RelationEditor::create()
);
} else {
$manyManyComponent = DataObject::getSchema()->manyManyComponent(get_class($dataObject), $relationship);
/** @var DataObject */
$manyManySingleton = singleton($manyManyComponent['childClass']);
$manyManyField = $manyManySingleton->scaffoldFormFieldForManyMany($relationship, $fieldLabel, $dataObject, $includeInOwnTab);
}

$fieldClass = $overrideFieldClass ?: GridField::class;

/** @var GridField $grid */
$grid = Injector::inst()->create(
$fieldClass,
$relationship,
$dataObject->fieldLabel($relationship),
$dataObject->$relationship(),
GridFieldConfig_RelationEditor::create()
);

if ($tabbed) {
$fields->addFieldToTab("Root.$relationship", $grid);
if ($includeInOwnTab) {
$fields->findOrMakeTab(
"Root.$relationship",
$fieldLabel
);
$fields->addFieldToTab("Root.$relationship", $manyManyField);
} else {
$fields->addFieldToTab('Root.Main', $manyManyField);
}
} else {
$fields->push($grid);
$fields->push($manyManyField);
}
}

Expand Down
68 changes: 68 additions & 0 deletions src/ORM/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@
use SilverStripe\Forms\FormScaffolder;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\FieldsValidator;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\SearchableDropdownField;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\i18nEntityProvider;
use SilverStripe\ORM\Connect\MySQLSchemaManager;
use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBForeignKey;
use SilverStripe\ORM\Filters\PartialMatchFilter;
use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\Queries\SQLDelete;
Expand Down Expand Up @@ -2485,6 +2489,70 @@ public function scaffoldFormFields($_params = null)
return $fs->getFieldList();
}

/**
* Scaffold a form field for selecting records of this model type in a has_one relation.
*
* @param string $fieldName The name we usually expect the field to have. This is often the has_one relation
* name with "ID" suffixed to it.
* @param string $relationName The name of the actual has_one relation, without "ID" suffixed to it.
* Some form fields such as UploadField use this instead of the usual field name.
*/
public function scaffoldFormFieldForHasOne(
string $fieldName,
?string $fieldTitle,
string $relationName,
DataObject $ownerRecord
): FormField {
$labelField = $this->hasField('Title') ? 'Title' : 'Name';
$list = DataList::create(static::class);
$threshold = DBForeignKey::config()->get('dropdown_field_threshold');
$overThreshold = $list->count() > $threshold;
$field = SearchableDropdownField::create($fieldName, $fieldTitle, $list, $labelField)
->setIsLazyLoaded($overThreshold)
->setLazyLoadLimit($threshold);
return $field;
}

/**
* Scaffold a form field for selecting records of this model type in a has_many relation.
*
* @param bool &$includeInTab Set this to true if the field should be in its own tab. False otherwise.
*/
public function scaffoldFormFieldForHasMany(
string $relationName,
?string $fieldTitle,
DataObject $ownerRecord,
bool &$includeInOwnTab
): FormField {
$includeInOwnTab = true;
return GridField::create(
$relationName,
$fieldTitle,
$ownerRecord->$relationName(),
GridFieldConfig_RelationEditor::create()
);
}

/**
* Scaffold a form field for selecting records of this model type in a many_many relation.
*
* @param bool &$includeInTab Set this to true if the field should be in its own tab. False otherwise.
*/
public function scaffoldFormFieldForManyMany(
string $relationName,
?string $fieldTitle,
DataObject $ownerRecord,
bool &$includeInOwnTab
): FormField {
$includeInOwnTab = true;
return GridField::create(
$relationName,
$fieldTitle,
$ownerRecord->$relationName(),
GridFieldConfig_RelationEditor::create()
);
}

/**
* Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
* being called on extensions
Expand Down
21 changes: 3 additions & 18 deletions src/ORM/FieldType/DBForeignKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class DBForeignKey extends DBInt
protected $object;

/**
* Number of related objects to show in a dropdown before it switches to using lazyloading
* Number of related objects to show in a scaffolded searchable dropdown field before it
* switches to using lazyloading.
* This will also be used as the lazy load limit
*
* @config
Expand Down Expand Up @@ -65,23 +66,7 @@ public function scaffoldFormField($title = null, $params = null)
return null;
}
$hasOneSingleton = singleton($hasOneClass);
if ($hasOneSingleton instanceof File) {
$field = Injector::inst()->create(FileHandleField::class, $relationName, $title);
if ($hasOneSingleton instanceof Image) {
$field->setAllowedFileCategories('image/supported');
}
if ($field->hasMethod('setAllowedMaxFileNumber')) {
$field->setAllowedMaxFileNumber(1);
}
return $field;
}
$labelField = $hasOneSingleton->hasField('Title') ? 'Title' : 'Name';
$list = DataList::create($hasOneClass);
$threshold = self::config()->get('dropdown_field_threshold');
$overThreshold = $list->count() > $threshold;
$field = SearchableDropdownField::create($this->name, $title, $list, $labelField)
->setIsLazyLoaded($overThreshold)
->setLazyLoadLimit($threshold);
$field = $hasOneSingleton->scaffoldFormFieldForHasOne($this->name, $title, $relationName, $this->object);
return $field;
}

Expand Down
53 changes: 52 additions & 1 deletion tests/php/Forms/FormScaffolderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\CurrencyField;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\Tests\FormScaffolderTest\Article;
use SilverStripe\Forms\Tests\FormScaffolderTest\ArticleExtension;
use SilverStripe\Forms\Tests\FormScaffolderTest\Author;
use SilverStripe\Forms\Tests\FormScaffolderTest\Child;
use SilverStripe\Forms\Tests\FormScaffolderTest\ParentModel;
use SilverStripe\Forms\Tests\FormScaffolderTest\ParentChildJoin;
use SilverStripe\Forms\Tests\FormScaffolderTest\Tag;
use SilverStripe\Forms\TimeField;

/**
* Tests for DataObject FormField scaffolding
Expand All @@ -30,9 +36,11 @@ class FormScaffolderTest extends SapphireTest
Article::class,
Tag::class,
Author::class,
ParentModel::class,
Child::class,
ParentChildJoin::class,
];


public function testGetCMSFieldsSingleton()
{
$article = new Article;
Expand Down Expand Up @@ -162,4 +170,47 @@ public function testGetFormFields()

$this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default');
}

public function provideScaffoldRelationFormFields()
{
return [
[true],
[false],
];
}

/**
* @dataProvider provideScaffoldRelationFormFields
*/
public function testScaffoldRelationFormFields(bool $includeInOwnTab)
{
$parent = $this->objFromFixture(ParentModel::class, 'parent1');
Child::$includeInOwnTab = $includeInOwnTab;
$fields = $parent->scaffoldFormFields(['includeRelations' => true, 'tabbed' => true]);

foreach (array_keys(ParentModel::config()->uninherited('has_one')) as $hasOneName) {
$scaffoldedFormField = $fields->dataFieldByName($hasOneName . 'ID');
if ($hasOneName === 'ChildPolymorphic') {
$this->assertNull($scaffoldedFormField, "$hasOneName should be null");
} else {
$this->assertInstanceOf(DateField::class, $scaffoldedFormField, "$hasOneName should be a DateField");
}
}
foreach (array_keys(ParentModel::config()->uninherited('has_many')) as $hasManyName) {
$this->assertInstanceOf(CurrencyField::class, $fields->dataFieldByName($hasManyName), "$hasManyName should be a CurrencyField");
if ($includeInOwnTab) {
$this->assertNotNull($fields->findTab("Root.$hasManyName"));
} else {
$this->assertNull($fields->findTab("Root.$hasManyName"));
}
}
foreach (array_keys(ParentModel::config()->uninherited('many_many')) as $manyManyName) {
$this->assertInstanceOf(TimeField::class, $fields->dataFieldByName($manyManyName), "$manyManyName should be a TimeField");
if ($includeInOwnTab) {
$this->assertNotNull($fields->findTab("Root.$manyManyName"));
} else {
$this->assertNull($fields->findTab("Root.$manyManyName"));
}
}
}
}
3 changes: 3 additions & 0 deletions tests/php/Forms/FormScaffolderTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ SilverStripe\Forms\Tests\FormScaffolderTest\Author:
author1:
FirstName: Author 1
Tags: =>SilverStripe\Forms\Tests\FormScaffolderTest\Article.article1
SilverStripe\Forms\Tests\FormScaffolderTest\ParentModel:
parent1:
Title: Parent 1
57 changes: 57 additions & 0 deletions tests/php/Forms/FormScaffolderTest/Child.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace SilverStripe\Forms\Tests\FormScaffolderTest;

use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\CurrencyField;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\TimeField;
use SilverStripe\ORM\DataObject;

class Child extends DataObject implements TestOnly
{
private static $table_name = 'FormScaffolderTest_Child';

private static $db = [
'Title' => 'Varchar',
];

private static $has_one = [
'Parent' => ParentModel::class,
];

public static bool $includeInOwnTab = true;

public function scaffoldFormFieldForHasOne(
string $fieldName,
?string $fieldTitle,
string $relationName,
DataObject $ownerRecord
): FormField {
// Intentionally return a field that is unlikely to be used by default in the future.
return DateField::create($fieldName, $fieldTitle);
}

public function scaffoldFormFieldForHasMany(
string $relationName,
?string $fieldTitle,
DataObject $ownerRecord,
bool &$includeInOwnTab
): FormField {
$includeInOwnTab = static::$includeInOwnTab;
// Intentionally return a field that is unlikely to be used by default in the future.
return CurrencyField::create($relationName, $fieldTitle);
}

public function scaffoldFormFieldForManyMany(
string $relationName,
?string $fieldTitle,
DataObject $ownerRecord,
bool &$includeInOwnTab
): FormField {
$includeInOwnTab = static::$includeInOwnTab;
// Intentionally return a field that is unlikely to be used by default in the future.
return TimeField::create($relationName, $fieldTitle);
}
}
Loading

0 comments on commit cd72258

Please sign in to comment.