diff --git a/composer.json b/composer.json index 39d94cbbacc..b634895127d 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/src/Forms/FormScaffolder.php b/src/Forms/FormScaffolder.php index d654e73c79b..c151c2aa2d7 100644 --- a/src/Forms/FormScaffolder.php +++ b/src/Forms/FormScaffolder.php @@ -138,27 +138,39 @@ public function getFieldList() && ($this->includeRelations === true || isset($this->includeRelations['has_many'])) ) { foreach ($this->obj->hasMany() as $relationship => $component) { + $includeInOwnTab = true; + $fieldLabel = $this->obj->fieldLabel($relationship); if ($this->tabbed) { $fields->findOrMakeTab( "Root.$relationship", - $this->obj->fieldLabel($relationship) + $fieldLabel ); } $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->addFieldToTab("Root.$relationship", $hasManyField); + } else { + $fields->addFieldToTab('Root.Main', $hasManyField); + } } else { - $fields->push($grid); + $fields->push($hasManyField); } } } @@ -187,7 +199,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. */ @@ -198,28 +210,40 @@ public static function addManyManyRelationshipFields( $tabbed, DataObject $dataObject ) { + $includeInOwnTab = true; + $fieldLabel = $dataObject->fieldLabel($relationship); + if ($tabbed) { $fields->findOrMakeTab( "Root.$relationship", - $dataObject->fieldLabel($relationship) + $fieldLabel ); } - $fieldClass = $overrideFieldClass ?: GridField::class; - - /** @var GridField $grid */ - $grid = Injector::inst()->create( - $fieldClass, - $relationship, - $dataObject->fieldLabel($relationship), - $dataObject->$relationship(), - GridFieldConfig_RelationEditor::create() - ); + 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); + } if ($tabbed) { - $fields->addFieldToTab("Root.$relationship", $grid); + if ($includeInOwnTab) { + $fields->addFieldToTab("Root.$relationship", $manyManyField); + } else { + $fields->addFieldToTab('Root.Main', $manyManyField); + } } else { - $fields->push($grid); + $fields->push($manyManyField); } } diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index 42d0166458a..c456e1083a3 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -18,7 +18,10 @@ 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; @@ -26,6 +29,7 @@ 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; @@ -2485,6 +2489,65 @@ public function scaffoldFormFields($_params = null) return $fs->getFieldList(); } + /** + * Scaffold a form field for selecting records of this model type in a has_one relation. + */ + 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 diff --git a/src/ORM/FieldType/DBForeignKey.php b/src/ORM/FieldType/DBForeignKey.php index ab23db7489b..4265491ae18 100644 --- a/src/ORM/FieldType/DBForeignKey.php +++ b/src/ORM/FieldType/DBForeignKey.php @@ -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 @@ -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; } diff --git a/tests/php/Forms/FormScaffolderTest.php b/tests/php/Forms/FormScaffolderTest.php index 8e0716ee21c..f178c5cd0d8 100644 --- a/tests/php/Forms/FormScaffolderTest.php +++ b/tests/php/Forms/FormScaffolderTest.php @@ -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 @@ -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; @@ -162,4 +170,25 @@ public function testGetFormFields() $this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default'); } + + public function testScaffoldRelationFormFields() + { + $parent = $this->objFromFixture(ParentModel::class, 'parent1'); + $fields = $parent->scaffoldFormFields(['includeRelations' => 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"); + } + foreach (array_keys(ParentModel::config()->uninherited('many_many')) as $manyManyName) { + $this->assertInstanceOf(TimeField::class, $fields->dataFieldByName($manyManyName), "$manyManyName should be a TimeField"); + } + } } diff --git a/tests/php/Forms/FormScaffolderTest.yml b/tests/php/Forms/FormScaffolderTest.yml index 70847acc3fb..14d3c598515 100644 --- a/tests/php/Forms/FormScaffolderTest.yml +++ b/tests/php/Forms/FormScaffolderTest.yml @@ -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 diff --git a/tests/php/Forms/FormScaffolderTest/Child.php b/tests/php/Forms/FormScaffolderTest/Child.php new file mode 100644 index 00000000000..29227a827ce --- /dev/null +++ b/tests/php/Forms/FormScaffolderTest/Child.php @@ -0,0 +1,53 @@ + 'Varchar', + ]; + + private static $has_one = [ + 'Parent' => ParentModel::class, + ]; + + 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 { + // 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 { + // Intentionally return a field that is unlikely to be used by default in the future. + return TimeField::create($relationName, $fieldTitle); + } +} diff --git a/tests/php/Forms/FormScaffolderTest/ParentChildJoin.php b/tests/php/Forms/FormScaffolderTest/ParentChildJoin.php new file mode 100644 index 00000000000..6fbb55c8786 --- /dev/null +++ b/tests/php/Forms/FormScaffolderTest/ParentChildJoin.php @@ -0,0 +1,20 @@ + 'Varchar', + ]; + + private static $has_one = [ + 'Parent' => ParentModel::class, + 'Child' => Child::class, + ]; +} diff --git a/tests/php/Forms/FormScaffolderTest/ParentModel.php b/tests/php/Forms/FormScaffolderTest/ParentModel.php new file mode 100644 index 00000000000..8a7b7b16b81 --- /dev/null +++ b/tests/php/Forms/FormScaffolderTest/ParentModel.php @@ -0,0 +1,33 @@ + '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', + ] + ]; +}