mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge branch '5' into 6
This commit is contained in:
commit
051dadde12
@ -387,6 +387,9 @@ class FieldList extends ArrayList
|
||||
$currentPointer = $this;
|
||||
|
||||
foreach ($parts as $k => $part) {
|
||||
if ($currentPointer === null) {
|
||||
return null;
|
||||
}
|
||||
$currentPointer = $currentPointer->fieldByName($part);
|
||||
}
|
||||
$this->checkIsTabOrTabSetOrNull($tabName, $currentPointer);
|
||||
|
@ -28,8 +28,15 @@ class FormScaffolder
|
||||
*/
|
||||
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
|
||||
* @deprecated 5.3.0 Will be removed without equivalent functionality.
|
||||
*/
|
||||
public $ajaxSafe = false;
|
||||
|
||||
@ -39,6 +46,11 @@ class FormScaffolder
|
||||
*/
|
||||
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}.
|
||||
* By default the scaffolder will determine the field instance by {@link DBField::scaffoldFormField()}.
|
||||
@ -46,10 +58,21 @@ class FormScaffolder
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@ -76,12 +99,20 @@ class FormScaffolder
|
||||
$mainTab->setTitle(_t(__CLASS__ . '.TABMAIN', 'Main'));
|
||||
}
|
||||
|
||||
if ($this->mainTabOnly) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
// Add logical fields directly specified in db config
|
||||
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 ?? [])) {
|
||||
continue;
|
||||
}
|
||||
// Skip ignored fields
|
||||
if (in_array($fieldName, $this->ignoreFields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->fieldClasses && isset($this->fieldClasses[$fieldName])) {
|
||||
$fieldClass = $this->fieldClasses[$fieldName];
|
||||
@ -110,6 +141,9 @@ class FormScaffolder
|
||||
if ($this->restrictFields && !in_array($relationship, $this->restrictFields ?? [])) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($relationship, $this->ignoreFields)) {
|
||||
continue;
|
||||
}
|
||||
$fieldName = $component === 'SilverStripe\\ORM\\DataObject'
|
||||
? $relationship // Polymorphic has_one field is composite, so don't refer to ID subfield
|
||||
: "{$relationship}ID";
|
||||
@ -138,6 +172,12 @@ class FormScaffolder
|
||||
&& ($this->includeRelations === true || isset($this->includeRelations['has_many']))
|
||||
) {
|
||||
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;
|
||||
$fieldLabel = $this->obj->fieldLabel($relationship);
|
||||
$fieldClass = (isset($this->fieldClasses[$relationship]))
|
||||
@ -177,6 +217,12 @@ class FormScaffolder
|
||||
&& ($this->includeRelations === true || isset($this->includeRelations['many_many']))
|
||||
) {
|
||||
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(
|
||||
$fields,
|
||||
$relationship,
|
||||
@ -252,8 +298,12 @@ class FormScaffolder
|
||||
{
|
||||
return [
|
||||
'tabbed' => $this->tabbed,
|
||||
'mainTabOnly' => $this->mainTabOnly,
|
||||
'includeRelations' => $this->includeRelations,
|
||||
'restrictRelations' => $this->restrictRelations,
|
||||
'ignoreRelations' => $this->ignoreRelations,
|
||||
'restrictFields' => $this->restrictFields,
|
||||
'ignoreFields' => $this->ignoreFields,
|
||||
'fieldClasses' => $this->fieldClasses,
|
||||
'ajaxSafe' => $this->ajaxSafe
|
||||
];
|
||||
|
@ -249,6 +249,39 @@ class TabSet extends CompositeField
|
||||
return parent::insertAfter($insertAfter, $field, $appendIfMissing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the order of tabs which are direct children of this TabSet by specifying an ordered list of
|
||||
* tab names.
|
||||
*
|
||||
* This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and
|
||||
* shuffle the tabs around to the order that you want.
|
||||
*
|
||||
* Tab names should exclude prefixes. For example if this TabSet is "Root", include "Main" not "Root.Main"
|
||||
*/
|
||||
public function changeTabOrder(array $tabNames): static
|
||||
{
|
||||
// Build a map of tabs indexed by their name. This will make the 2nd step much easier.
|
||||
$existingTabs = [];
|
||||
foreach ($this->children as $tab) {
|
||||
$existingTabs[$tab->getName()] = $tab;
|
||||
}
|
||||
|
||||
// Iterate through the ordered list of names, building a new array.
|
||||
// While we're doing this, empty out $existingTabs so that we can keep track of leftovers.
|
||||
// Unrecognised field names are okay; just ignore them.
|
||||
$orderedTabs = [];
|
||||
foreach ($tabNames as $tabName) {
|
||||
if (isset($existingTabs[$tabName])) {
|
||||
$orderedTabs[] = $existingTabs[$tabName];
|
||||
unset($existingTabs[$tabName]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the leftover fields to the end of the ordered list.
|
||||
$this->setTabs(FieldList::create([...$orderedTabs, ...$existingTabs]));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an additional default for $schemaData.
|
||||
* The existing keys are immutable. HideNav is added in this overriding method to ensure it is not ignored by
|
||||
|
@ -289,6 +289,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -2474,8 +2483,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$params = array_merge(
|
||||
[
|
||||
'tabbed' => false,
|
||||
'mainTabOnly' => false,
|
||||
'includeRelations' => false,
|
||||
'restrictRelations' => [],
|
||||
'ignoreRelations' => [],
|
||||
'restrictFields' => false,
|
||||
'ignoreFields' => [],
|
||||
'fieldClasses' => false,
|
||||
'ajaxSafe' => false
|
||||
],
|
||||
@ -2484,8 +2497,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
$fs = FormScaffolder::create($this);
|
||||
$fs->tabbed = $params['tabbed'];
|
||||
$fs->mainTabOnly = $params['mainTabOnly'];
|
||||
$fs->includeRelations = $params['includeRelations'];
|
||||
$fs->restrictRelations = $params['restrictRelations'];
|
||||
$fs->ignoreRelations = $params['ignoreRelations'];
|
||||
$fs->restrictFields = $params['restrictFields'];
|
||||
$fs->ignoreFields = $params['ignoreFields'];
|
||||
$fs->fieldClasses = $params['fieldClasses'];
|
||||
$fs->ajaxSafe = $params['ajaxSafe'];
|
||||
|
||||
@ -2605,12 +2622,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public function getCMSFields()
|
||||
{
|
||||
$tabbedFields = $this->scaffoldFormFields([
|
||||
// Don't allow has_many/many_many relationship editing before the record is first saved
|
||||
'includeRelations' => ($this->ID > 0),
|
||||
'tabbed' => true,
|
||||
'ajaxSafe' => true
|
||||
]);
|
||||
$scaffoldOptions = static::config()->get('scaffold_cms_fields_settings');
|
||||
// Don't allow has_many/many_many relationship editing before the record is first saved
|
||||
if (!$this->isInDB()) {
|
||||
$scaffoldOptions['includeRelations'] = false;
|
||||
}
|
||||
$tabbedFields = $this->scaffoldFormFields($scaffoldOptions);
|
||||
|
||||
$this->extend('updateCMSFields', $tabbedFields);
|
||||
|
||||
|
@ -193,6 +193,9 @@ class SSViewer_Scope
|
||||
public function getObj($name, $arguments = [], $cache = false, $cacheName = null)
|
||||
{
|
||||
$on = $this->getItem();
|
||||
if ($on === null) {
|
||||
return null;
|
||||
}
|
||||
return $on->obj($name, $arguments, $cache, $cacheName);
|
||||
}
|
||||
|
||||
|
@ -295,6 +295,7 @@ class FieldListTest extends SapphireTest
|
||||
$message = "Root.Tab1.Field1 is an instance of 'SilverStripe\Forms\TextField', not Tab or TabSet";
|
||||
$this->expectExceptionMessage($message);
|
||||
$fields->findOrMakeTab('Root.Tab1.Field1');
|
||||
$this->assertNull($fields->findTab('This.Doesnt.Exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,21 +173,40 @@ class FormScaffolderTest extends SapphireTest
|
||||
|
||||
public function provideScaffoldRelationFormFields()
|
||||
{
|
||||
return [
|
||||
[true],
|
||||
[false],
|
||||
$scenarios = [
|
||||
'ignore no relations' => [
|
||||
'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
|
||||
*/
|
||||
public function testScaffoldRelationFormFields(bool $includeInOwnTab)
|
||||
public function testScaffoldRelationFormFields(bool $includeInOwnTab, array $ignoreRelations)
|
||||
{
|
||||
$parent = $this->objFromFixture(ParentModel::class, 'parent1');
|
||||
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) {
|
||||
$scaffoldedFormField = $fields->dataFieldByName($hasOneName . 'ID');
|
||||
if ($hasOneName === 'ChildPolymorphic') {
|
||||
@ -196,20 +215,136 @@ class FormScaffolderTest extends SapphireTest
|
||||
$this->assertInstanceOf(DateField::class, $scaffoldedFormField, "$hasOneName should be a DateField");
|
||||
}
|
||||
}
|
||||
// has_many
|
||||
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"));
|
||||
if (in_array($hasManyName, $ignoreRelations)) {
|
||||
$this->assertNull($fields->dataFieldByName($hasManyName));
|
||||
} else {
|
||||
$this->assertNull($fields->findTab("Root.$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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// many_many
|
||||
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"));
|
||||
if (in_array($hasManyName, $ignoreRelations)) {
|
||||
$this->assertNull($fields->dataFieldByName($hasManyName));
|
||||
} else {
|
||||
$this->assertNull($fields->findTab("Root.$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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
tests/php/Forms/TabSetTest.php
Normal file
37
tests/php/Forms/TabSetTest.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Forms\Tests;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\TabSet;
|
||||
|
||||
class TabSetTest extends SapphireTest
|
||||
{
|
||||
protected $usesDatabase = false;
|
||||
|
||||
public function testChangeTabOrder(): void
|
||||
{
|
||||
$tabSet = new TabSet('Root');
|
||||
$fieldList = new FieldList([$tabSet]);
|
||||
$fieldList->findOrMakeTab('Root.Main');
|
||||
$fieldList->findOrMakeTab('Root.Next');
|
||||
$fieldList->findOrMakeTab('Root.More');
|
||||
$fieldList->findOrMakeTab('Root.Extra');
|
||||
$fieldList->addFieldToTab('Root', new TabSet('SubTabSet'));
|
||||
$fieldList->findOrMakeTab('Root.SubTabSet.Another');
|
||||
|
||||
// Reorder tabs - intentionally leaving some alone, which will be added to the end.
|
||||
$tabSet->changeTabOrder([
|
||||
'SubTabSet',
|
||||
'More',
|
||||
'Main',
|
||||
'Non-Existent', // will be ignored
|
||||
'Another', // will be ignored
|
||||
]);
|
||||
// Order is correct
|
||||
$this->assertSame(['SubTabSet', 'More', 'Main', 'Next', 'Extra'], $tabSet->getChildren()->column('Name'));
|
||||
// Sub-tab is still there
|
||||
$this->assertNotNull($fieldList->findTab('Root.SubTabSet.Another'));
|
||||
}
|
||||
}
|
@ -364,6 +364,12 @@ SS;
|
||||
);
|
||||
}
|
||||
|
||||
public function testGlobalVariablesReturnNull()
|
||||
{
|
||||
$this->assertEquals('<p></p>', $this->render('<p>$SSViewerTest_GlobalReturnsNull</p>'));
|
||||
$this->assertEquals('<p></p>', $this->render('<p>$SSViewerTest_GlobalReturnsNull.Chained.Properties</p>'));
|
||||
}
|
||||
|
||||
public function testCoreGlobalVariableCalls()
|
||||
{
|
||||
$this->assertEquals(
|
||||
|
@ -18,8 +18,8 @@ class TestGlobalProvider implements TemplateGlobalProvider, TestOnly
|
||||
'SSViewerTest_GlobalReferencedByString' => 'get_reference',
|
||||
'SSViewerTest_GlobalReferencedInArray' => ['method' => 'get_reference'],
|
||||
|
||||
'SSViewerTest_GlobalThatTakesArguments' => ['method' => 'get_argmix', 'casting' => 'HTMLFragment']
|
||||
|
||||
'SSViewerTest_GlobalThatTakesArguments' => ['method' => 'get_argmix', 'casting' => 'HTMLFragment'],
|
||||
'SSViewerTest_GlobalReturnsNull' => 'getNull',
|
||||
];
|
||||
}
|
||||
|
||||
@ -43,4 +43,9 @@ class TestGlobalProvider implements TemplateGlobalProvider, TestOnly
|
||||
$args = func_get_args();
|
||||
return 'z' . implode(':', $args) . 'z';
|
||||
}
|
||||
|
||||
public static function getNull()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user