NEW Make CMSFields scaffolding configurable, plus new options (#11328)

Note that includeRelations was intentionally changed to not include has_one in 524d7a9011
This commit is contained in:
Guy Sartorelli 2024-08-12 12:52:57 +12:00 committed by GitHub
parent eee7a84a48
commit dca62c7380
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 228 additions and 21 deletions

View File

@ -407,6 +407,9 @@ class FieldList extends ArrayList
$currentPointer = $this; $currentPointer = $this;
foreach ($parts as $k => $part) { foreach ($parts as $k => $part) {
if ($currentPointer === null) {
return null;
}
$currentPointer = $currentPointer->fieldByName($part); $currentPointer = $currentPointer->fieldByName($part);
} }

View File

@ -28,8 +28,15 @@ class FormScaffolder
*/ */
public $tabbed = false; public $tabbed = false;
/**
* Only set up the "Root.Main" tab, but skip scaffolding actual FormFields.
* If $tabbed is false, an empty FieldList will be returned.
*/
public bool $mainTabOnly = false;
/** /**
* @var boolean $ajaxSafe * @var boolean $ajaxSafe
* @deprecated 5.3.0 Will be removed without equivalent functionality.
*/ */
public $ajaxSafe = false; public $ajaxSafe = false;
@ -39,6 +46,11 @@ class FormScaffolder
*/ */
public $restrictFields; public $restrictFields;
/**
* Numeric array of field names and has_one relations to explicitly not scaffold.
*/
public array $ignoreFields = [];
/** /**
* @var array $fieldClasses Optional mapping of fieldnames to subclasses of {@link FormField}. * @var array $fieldClasses Optional mapping of fieldnames to subclasses of {@link FormField}.
* By default the scaffolder will determine the field instance by {@link DBField::scaffoldFormField()}. * By default the scaffolder will determine the field instance by {@link DBField::scaffoldFormField()}.
@ -46,10 +58,21 @@ class FormScaffolder
public $fieldClasses; public $fieldClasses;
/** /**
* @var boolean $includeRelations Include has_one, has_many and many_many relations * @var boolean $includeRelations Include has_many and many_many relations
*/ */
public $includeRelations = false; public $includeRelations = false;
/**
* Array of relation names to use as an allow list.
* If left blank, all has_many and many_many relations will be scaffolded unless explicitly ignored.
*/
public array $restrictRelations = [];
/**
* Numeric array of has_many and many_many relations to explicitly not scaffold.
*/
public array $ignoreRelations = [];
/** /**
* @param DataObject $obj * @param DataObject $obj
*/ */
@ -76,12 +99,20 @@ class FormScaffolder
$mainTab->setTitle(_t(__CLASS__ . '.TABMAIN', 'Main')); $mainTab->setTitle(_t(__CLASS__ . '.TABMAIN', 'Main'));
} }
if ($this->mainTabOnly) {
return $fields;
}
// Add logical fields directly specified in db config // Add logical fields directly specified in db config
foreach ($this->obj->config()->get('db') as $fieldName => $fieldType) { foreach ($this->obj->config()->get('db') as $fieldName => $fieldType) {
// Skip restricted fields // Skip fields that aren't in the allow list
if ($this->restrictFields && !in_array($fieldName, $this->restrictFields ?? [])) { if ($this->restrictFields && !in_array($fieldName, $this->restrictFields ?? [])) {
continue; continue;
} }
// Skip ignored fields
if (in_array($fieldName, $this->ignoreFields)) {
continue;
}
if ($this->fieldClasses && isset($this->fieldClasses[$fieldName])) { if ($this->fieldClasses && isset($this->fieldClasses[$fieldName])) {
$fieldClass = $this->fieldClasses[$fieldName]; $fieldClass = $this->fieldClasses[$fieldName];
@ -110,6 +141,9 @@ class FormScaffolder
if ($this->restrictFields && !in_array($relationship, $this->restrictFields ?? [])) { if ($this->restrictFields && !in_array($relationship, $this->restrictFields ?? [])) {
continue; continue;
} }
if (in_array($relationship, $this->ignoreFields)) {
continue;
}
$fieldName = $component === 'SilverStripe\\ORM\\DataObject' $fieldName = $component === 'SilverStripe\\ORM\\DataObject'
? $relationship // Polymorphic has_one field is composite, so don't refer to ID subfield ? $relationship // Polymorphic has_one field is composite, so don't refer to ID subfield
: "{$relationship}ID"; : "{$relationship}ID";
@ -138,6 +172,12 @@ 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 (!empty($this->restrictRelations) && !in_array($relationship, $this->restrictRelations)) {
continue;
}
if (in_array($relationship, $this->ignoreRelations)) {
continue;
}
$includeInOwnTab = true; $includeInOwnTab = true;
$fieldLabel = $this->obj->fieldLabel($relationship); $fieldLabel = $this->obj->fieldLabel($relationship);
$fieldClass = (isset($this->fieldClasses[$relationship])) $fieldClass = (isset($this->fieldClasses[$relationship]))
@ -177,6 +217,12 @@ class FormScaffolder
&& ($this->includeRelations === true || isset($this->includeRelations['many_many'])) && ($this->includeRelations === true || isset($this->includeRelations['many_many']))
) { ) {
foreach ($this->obj->manyMany() as $relationship => $component) { foreach ($this->obj->manyMany() as $relationship => $component) {
if (!empty($this->restrictRelations) && !in_array($relationship, $this->restrictRelations)) {
continue;
}
if (in_array($relationship, $this->ignoreRelations)) {
continue;
}
static::addManyManyRelationshipFields( static::addManyManyRelationshipFields(
$fields, $fields,
$relationship, $relationship,
@ -252,8 +298,12 @@ class FormScaffolder
{ {
return [ return [
'tabbed' => $this->tabbed, 'tabbed' => $this->tabbed,
'mainTabOnly' => $this->mainTabOnly,
'includeRelations' => $this->includeRelations, 'includeRelations' => $this->includeRelations,
'restrictRelations' => $this->restrictRelations,
'ignoreRelations' => $this->ignoreRelations,
'restrictFields' => $this->restrictFields, 'restrictFields' => $this->restrictFields,
'ignoreFields' => $this->ignoreFields,
'fieldClasses' => $this->fieldClasses, 'fieldClasses' => $this->fieldClasses,
'ajaxSafe' => $this->ajaxSafe 'ajaxSafe' => $this->ajaxSafe
]; ];

View File

@ -289,6 +289,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
private static $table_name = null; private static $table_name = null;
/**
* Settings used by the FormScaffolder that scaffolds fields for getCMSFields()
*/
private static array $scaffold_cms_fields_settings = [
'includeRelations' => true,
'tabbed' => true,
'ajaxSafe' => true,
];
/** /**
* Non-static relationship cache, indexed by component name. * Non-static relationship cache, indexed by component name.
* *
@ -2469,8 +2478,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$params = array_merge( $params = array_merge(
[ [
'tabbed' => false, 'tabbed' => false,
'mainTabOnly' => false,
'includeRelations' => false, 'includeRelations' => false,
'restrictRelations' => [],
'ignoreRelations' => [],
'restrictFields' => false, 'restrictFields' => false,
'ignoreFields' => [],
'fieldClasses' => false, 'fieldClasses' => false,
'ajaxSafe' => false 'ajaxSafe' => false
], ],
@ -2479,8 +2492,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$fs = FormScaffolder::create($this); $fs = FormScaffolder::create($this);
$fs->tabbed = $params['tabbed']; $fs->tabbed = $params['tabbed'];
$fs->mainTabOnly = $params['mainTabOnly'];
$fs->includeRelations = $params['includeRelations']; $fs->includeRelations = $params['includeRelations'];
$fs->restrictRelations = $params['restrictRelations'];
$fs->ignoreRelations = $params['ignoreRelations'];
$fs->restrictFields = $params['restrictFields']; $fs->restrictFields = $params['restrictFields'];
$fs->ignoreFields = $params['ignoreFields'];
$fs->fieldClasses = $params['fieldClasses']; $fs->fieldClasses = $params['fieldClasses'];
$fs->ajaxSafe = $params['ajaxSafe']; $fs->ajaxSafe = $params['ajaxSafe'];
@ -2600,12 +2617,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
public function getCMSFields() public function getCMSFields()
{ {
$tabbedFields = $this->scaffoldFormFields([ $scaffoldOptions = static::config()->get('scaffold_cms_fields_settings');
// Don't allow has_many/many_many relationship editing before the record is first saved // Don't allow has_many/many_many relationship editing before the record is first saved
'includeRelations' => ($this->ID > 0), if (!$this->isInDB()) {
'tabbed' => true, $scaffoldOptions['includeRelations'] = false;
'ajaxSafe' => true }
]); $tabbedFields = $this->scaffoldFormFields($scaffoldOptions);
$this->extend('updateCMSFields', $tabbedFields); $this->extend('updateCMSFields', $tabbedFields);

View File

@ -259,6 +259,8 @@ class FieldListTest extends SapphireTest
$this->assertNull($fields->findTab('More')); $this->assertNull($fields->findTab('More'));
$this->assertEquals($fields->findTab('Root.More'), $more); $this->assertEquals($fields->findTab('Root.More'), $more);
$this->assertEquals($fields->findTab('Root.More.Tab4'), $tab4); $this->assertEquals($fields->findTab('Root.More.Tab4'), $tab4);
$this->assertNull($fields->findTab('This.Doesnt.Exist'));
} }
/** /**

View File

@ -173,21 +173,40 @@ class FormScaffolderTest extends SapphireTest
public function provideScaffoldRelationFormFields() public function provideScaffoldRelationFormFields()
{ {
return [ $scenarios = [
[true], 'ignore no relations' => [
[false], 'includeInOwnTab' => true,
'ignoreRelations' => [],
],
'ignore some relations' => [
'includeInOwnTab' => true,
'ignoreRelations' => [
'ChildrenHasMany',
'ChildrenManyManyThrough',
],
],
]; ];
foreach ($scenarios as $name => $scenario) {
$scenario['includeInOwnTab'] = false;
$scenarios[$name . ' - not in own tab'] = $scenario;
}
return $scenarios;
} }
/** /**
* @dataProvider provideScaffoldRelationFormFields * @dataProvider provideScaffoldRelationFormFields
*/ */
public function testScaffoldRelationFormFields(bool $includeInOwnTab) public function testScaffoldRelationFormFields(bool $includeInOwnTab, array $ignoreRelations)
{ {
$parent = $this->objFromFixture(ParentModel::class, 'parent1'); $parent = $this->objFromFixture(ParentModel::class, 'parent1');
Child::$includeInOwnTab = $includeInOwnTab; Child::$includeInOwnTab = $includeInOwnTab;
$fields = $parent->scaffoldFormFields(['includeRelations' => true, 'tabbed' => true]); $fields = $parent->scaffoldFormFields([
'includeRelations' => true,
'tabbed' => true,
'ignoreRelations' => $ignoreRelations,
]);
// has_one
foreach (array_keys(ParentModel::config()->uninherited('has_one')) as $hasOneName) { foreach (array_keys(ParentModel::config()->uninherited('has_one')) as $hasOneName) {
$scaffoldedFormField = $fields->dataFieldByName($hasOneName . 'ID'); $scaffoldedFormField = $fields->dataFieldByName($hasOneName . 'ID');
if ($hasOneName === 'ChildPolymorphic') { if ($hasOneName === 'ChildPolymorphic') {
@ -196,7 +215,11 @@ class FormScaffolderTest extends SapphireTest
$this->assertInstanceOf(DateField::class, $scaffoldedFormField, "$hasOneName should be a DateField"); $this->assertInstanceOf(DateField::class, $scaffoldedFormField, "$hasOneName should be a DateField");
} }
} }
// has_many
foreach (array_keys(ParentModel::config()->uninherited('has_many')) as $hasManyName) { foreach (array_keys(ParentModel::config()->uninherited('has_many')) as $hasManyName) {
if (in_array($hasManyName, $ignoreRelations)) {
$this->assertNull($fields->dataFieldByName($hasManyName));
} else {
$this->assertInstanceOf(CurrencyField::class, $fields->dataFieldByName($hasManyName), "$hasManyName should be a CurrencyField"); $this->assertInstanceOf(CurrencyField::class, $fields->dataFieldByName($hasManyName), "$hasManyName should be a CurrencyField");
if ($includeInOwnTab) { if ($includeInOwnTab) {
$this->assertNotNull($fields->findTab("Root.$hasManyName")); $this->assertNotNull($fields->findTab("Root.$hasManyName"));
@ -204,7 +227,12 @@ class FormScaffolderTest extends SapphireTest
$this->assertNull($fields->findTab("Root.$hasManyName")); $this->assertNull($fields->findTab("Root.$hasManyName"));
} }
} }
}
// many_many
foreach (array_keys(ParentModel::config()->uninherited('many_many')) as $manyManyName) { foreach (array_keys(ParentModel::config()->uninherited('many_many')) as $manyManyName) {
if (in_array($hasManyName, $ignoreRelations)) {
$this->assertNull($fields->dataFieldByName($hasManyName));
} else {
$this->assertInstanceOf(TimeField::class, $fields->dataFieldByName($manyManyName), "$manyManyName should be a TimeField"); $this->assertInstanceOf(TimeField::class, $fields->dataFieldByName($manyManyName), "$manyManyName should be a TimeField");
if ($includeInOwnTab) { if ($includeInOwnTab) {
$this->assertNotNull($fields->findTab("Root.$manyManyName")); $this->assertNotNull($fields->findTab("Root.$manyManyName"));
@ -214,3 +242,110 @@ class FormScaffolderTest extends SapphireTest
} }
} }
} }
public function testScaffoldIgnoreFields(): void
{
$article1 = $this->objFromFixture(Article::class, 'article1');
$fields = $article1->scaffoldFormFields([
'ignoreFields' => [
'Content',
'Author',
],
]);
$this->assertSame(['ExtendedField', 'Title'], $fields->column('Name'));
}
public function testScaffoldRestrictRelations(): void
{
$article1 = $this->objFromFixture(Article::class, 'article1');
$fields = $article1->scaffoldFormFields([
'includeRelations' => true,
'restrictRelations' => [
'Tags',
],
// Ensure no db or has_one fields get scaffolded
'restrictFields' => [
'non-existent',
],
]);
$this->assertSame(['Tags'], $fields->column('Name'));
}
public function provideTabs(): array
{
return [
'only main tab' => [
'tabs' => true,
'mainTabOnly' => true,
],
'all tabs, all fields' => [
'tabs' => true,
'mainTabOnly' => false,
],
'no tabs, no fields' => [
'tabs' => false,
'mainTabOnly' => true,
],
'no tabs, all fields' => [
'tabs' => false,
'mainTabOnly' => false,
],
];
}
/**
* @dataProvider provideTabs
*/
public function testTabs(bool $tabbed, bool $mainTabOnly): void
{
$parent = $this->objFromFixture(ParentModel::class, 'parent1');
Child::$includeInOwnTab = true;
$fields = $parent->scaffoldFormFields([
'tabbed' => $tabbed,
'mainTabOnly' => $mainTabOnly,
'includeRelations' => true,
]);
$fieldsToExpect = [
['Name' => 'Title'],
['Name' => 'ChildID'],
['Name' => 'ChildrenHasMany'],
['Name' => 'ChildrenManyMany'],
['Name' => 'ChildrenManyManyThrough'],
];
$relationTabs = [
'Root.ChildrenHasMany',
'Root.ChildrenManyMany',
'Root.ChildrenManyManyThrough',
];
if ($tabbed) {
$this->assertNotNull($fields->findTab('Root.Main'));
if ($mainTabOnly) {
// Only Root.Main with no fields
$this->assertListNotContains($fieldsToExpect, $fields->flattenFields());
foreach ($relationTabs as $tabName) {
$this->assertNull($fields->findTab($tabName));
}
} else {
// All fields in all tabs
$this->assertListContains($fieldsToExpect, $fields->flattenFields());
foreach ($relationTabs as $tabName) {
$this->assertNotNull($fields->findTab($tabName));
}
}
} else {
if ($mainTabOnly) {
// Empty list
$this->assertEmpty($fields);
} else {
// All fields, no tabs
$this->assertNull($fields->findTab('Root.Main'));
foreach ($relationTabs as $tabName) {
$this->assertNull($fields->findTab($tabName));
}
$this->assertListContains($fieldsToExpect, $fields->flattenFields());
}
}
}
}