NEW Allow DataObject classes to define scaffolded relation formfields (#11269)

This commit is contained in:
Guy Sartorelli 2024-06-07 17:06:01 +12:00 committed by GitHub
parent 64ac096b46
commit be0eab2bae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 292 additions and 56 deletions

View File

@ -37,7 +37,7 @@
"psr/http-message": "^1", "psr/http-message": "^1",
"sebastian/diff": "^4.0", "sebastian/diff": "^4.0",
"silverstripe/config": "^2", "silverstripe/config": "^2",
"silverstripe/assets": "^2.2", "silverstripe/assets": "^2.3",
"silverstripe/vendor-plugin": "^2", "silverstripe/vendor-plugin": "^2",
"sminnee/callbacklist": "^0.1.1", "sminnee/callbacklist": "^0.1.1",
"symfony/cache": "^6.1", "symfony/cache": "^6.1",

View File

@ -138,27 +138,37 @@ class FormScaffolder
&& ($this->includeRelations === true || isset($this->includeRelations['has_many'])) && ($this->includeRelations === true || isset($this->includeRelations['has_many']))
) { ) {
foreach ($this->obj->hasMany() as $relationship => $component) { foreach ($this->obj->hasMany() as $relationship => $component) {
if ($this->tabbed) { $includeInOwnTab = true;
$fields->findOrMakeTab( $fieldLabel = $this->obj->fieldLabel($relationship);
"Root.$relationship",
$this->obj->fieldLabel($relationship)
);
}
$fieldClass = (isset($this->fieldClasses[$relationship])) $fieldClass = (isset($this->fieldClasses[$relationship]))
? $this->fieldClasses[$relationship] ? $this->fieldClasses[$relationship]
: 'SilverStripe\\Forms\\GridField\\GridField'; : null;
/** @var GridField $grid */ if ($fieldClass) {
$grid = Injector::inst()->create( /** @var GridField */
$fieldClass, $hasManyField = Injector::inst()->create(
$relationship, $fieldClass,
$this->obj->fieldLabel($relationship), $relationship,
$this->obj->$relationship(), $fieldLabel,
GridFieldConfig_RelationEditor::create() $this->obj->$relationship(),
); GridFieldConfig_RelationEditor::create()
if ($this->tabbed) { );
$fields->addFieldToTab("Root.$relationship", $grid);
} else { } else {
$fields->push($grid); /** @var DataObject */
$hasManySingleton = singleton($component);
$hasManyField = $hasManySingleton->scaffoldFormFieldForHasMany($relationship, $fieldLabel, $this->obj, $includeInOwnTab);
}
if ($this->tabbed) {
if ($includeInOwnTab) {
$fields->findOrMakeTab(
"Root.$relationship",
$fieldLabel
);
$fields->addFieldToTab("Root.$relationship", $hasManyField);
} else {
$fields->addFieldToTab('Root.Main', $hasManyField);
}
} else {
$fields->push($hasManyField);
} }
} }
} }
@ -187,7 +197,7 @@ class FormScaffolder
* *
* @param FieldList $fields Reference to the @FieldList to add fields to. * @param FieldList $fields Reference to the @FieldList to add fields to.
* @param string $relationship The relationship identifier. * @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 bool $tabbed Whether this relationship has it's own tab or not.
* @param DataObject $dataObject The @DataObject that has the relation. * @param DataObject $dataObject The @DataObject that has the relation.
*/ */
@ -198,28 +208,37 @@ class FormScaffolder
$tabbed, $tabbed,
DataObject $dataObject DataObject $dataObject
) { ) {
if ($tabbed) { $includeInOwnTab = true;
$fields->findOrMakeTab( $fieldLabel = $dataObject->fieldLabel($relationship);
"Root.$relationship",
$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) { 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 { } else {
$fields->push($grid); $fields->push($manyManyField);
} }
} }

View File

@ -18,7 +18,10 @@ use SilverStripe\Forms\FormField;
use SilverStripe\Forms\FormScaffolder; use SilverStripe\Forms\FormScaffolder;
use SilverStripe\Forms\CompositeValidator; use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\FieldsValidator; use SilverStripe\Forms\FieldsValidator;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\SearchableDropdownField;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\i18n\i18nEntityProvider; use SilverStripe\i18n\i18nEntityProvider;
use SilverStripe\ORM\Connect\MySQLSchemaManager; use SilverStripe\ORM\Connect\MySQLSchemaManager;
@ -26,6 +29,7 @@ use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBForeignKey;
use SilverStripe\ORM\Filters\PartialMatchFilter; use SilverStripe\ORM\Filters\PartialMatchFilter;
use SilverStripe\ORM\Filters\SearchFilter; use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\Queries\SQLDelete; use SilverStripe\ORM\Queries\SQLDelete;
@ -2485,6 +2489,70 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $fs->getFieldList(); 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 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
* being called on extensions * being called on extensions

View File

@ -28,7 +28,8 @@ class DBForeignKey extends DBInt
protected $object; 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 * This will also be used as the lazy load limit
* *
* @config * @config
@ -65,23 +66,7 @@ class DBForeignKey extends DBInt
return null; return null;
} }
$hasOneSingleton = singleton($hasOneClass); $hasOneSingleton = singleton($hasOneClass);
if ($hasOneSingleton instanceof File) { $field = $hasOneSingleton->scaffoldFormFieldForHasOne($this->name, $title, $relationName, $this->object);
$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);
return $field; return $field;
} }

View File

@ -5,12 +5,18 @@ namespace SilverStripe\Forms\Tests;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField; use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Forms\CurrencyField;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\Tests\FormScaffolderTest\Article; use SilverStripe\Forms\Tests\FormScaffolderTest\Article;
use SilverStripe\Forms\Tests\FormScaffolderTest\ArticleExtension; use SilverStripe\Forms\Tests\FormScaffolderTest\ArticleExtension;
use SilverStripe\Forms\Tests\FormScaffolderTest\Author; 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\Tests\FormScaffolderTest\Tag;
use SilverStripe\Forms\TimeField;
/** /**
* Tests for DataObject FormField scaffolding * Tests for DataObject FormField scaffolding
@ -30,9 +36,11 @@ class FormScaffolderTest extends SapphireTest
Article::class, Article::class,
Tag::class, Tag::class,
Author::class, Author::class,
ParentModel::class,
Child::class,
ParentChildJoin::class,
]; ];
public function testGetCMSFieldsSingleton() public function testGetCMSFieldsSingleton()
{ {
$article = new Article; $article = new Article;
@ -162,4 +170,47 @@ class FormScaffolderTest extends SapphireTest
$this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default'); $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"));
}
}
}
} }

View File

@ -10,3 +10,6 @@ SilverStripe\Forms\Tests\FormScaffolderTest\Author:
author1: author1:
FirstName: Author 1 FirstName: Author 1
Tags: =>SilverStripe\Forms\Tests\FormScaffolderTest\Article.article1 Tags: =>SilverStripe\Forms\Tests\FormScaffolderTest\Article.article1
SilverStripe\Forms\Tests\FormScaffolderTest\ParentModel:
parent1:
Title: Parent 1

View File

@ -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);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace SilverStripe\Forms\Tests\FormScaffolderTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class ParentChildJoin extends DataObject implements TestOnly
{
private static $table_name = 'FormScaffolderTest_ParentChildJoin';
private static $db = [
'Title' => 'Varchar',
];
private static $has_one = [
'Parent' => ParentModel::class,
'Child' => Child::class,
];
}

View File

@ -0,0 +1,33 @@
<?php
namespace SilverStripe\Forms\Tests\FormScaffolderTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class ParentModel extends DataObject implements TestOnly
{
private static $table_name = 'FormScaffolderTest_ParentModel';
private static $db = [
'Title' => 'Varchar',
];
private static $has_one = [
'Child' => Child::class,
'ChildPolymorphic' => DataObject::class,
];
private static $has_many = [
'ChildrenHasMany' => Child::class . '.Parent',
];
private static $many_many = [
'ChildrenManyMany' => Child::class,
'ChildrenManyManyThrough' => [
'through' => ParentChildJoin::class,
'from' => 'Parent',
'to' => 'Child',
]
];
}