From 1e0b9056f5309363ee0a31d7619a85f030e415dc Mon Sep 17 00:00:00 2001 From: Guy Sartorelli <36352093+GuySartorelli@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:59:59 +1200 Subject: [PATCH] FIX Don't load data up front for lazy-loaded searchable dropdown (#11278) --- src/Forms/SearchableDropdownField.php | 2 +- src/Forms/SearchableDropdownTrait.php | 28 ++++++++++-- .../php/Forms/SearchableDropdownTraitTest.php | 44 +++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/Forms/SearchableDropdownField.php b/src/Forms/SearchableDropdownField.php index 901a88739..98548810d 100644 --- a/src/Forms/SearchableDropdownField.php +++ b/src/Forms/SearchableDropdownField.php @@ -9,7 +9,7 @@ use SilverStripe\ORM\DataList; class SearchableDropdownField extends DropdownField { use SearchableDropdownTrait; - + // This needs to be defined on the class, not the trait, otherwise there is a PHP error protected $schemaComponent = 'SearchableDropdownField'; diff --git a/src/Forms/SearchableDropdownTrait.php b/src/Forms/SearchableDropdownTrait.php index 5931912f9..7d93f04c1 100644 --- a/src/Forms/SearchableDropdownTrait.php +++ b/src/Forms/SearchableDropdownTrait.php @@ -265,6 +265,13 @@ trait SearchableDropdownTrait return $this->getListMap($this->sourceList); } + public function Field($properties = []) + { + $context = $this; + $this->extend('onBeforeRender', $context, $properties); + return $context->customise($properties)->renderWith($context->getTemplates()); + } + /* * @param mixed $source */ @@ -450,9 +457,16 @@ trait SearchableDropdownTrait public function getSchemaStateDefaults(): array { - $data = parent::getSchemaStateDefaults(); - $data = $this->updateDataForSchema($data); - return $data; + $state = [ + 'name' => $this->getName(), + 'id' => $this->ID(), + 'value' => $this->getDefaultSchemaValue(), + 'message' => $this->getSchemaMessage(), + 'data' => [], + ]; + + $state = $this->updateDataForSchema($state); + return $state; } /** @@ -467,6 +481,14 @@ trait SearchableDropdownTrait return $this; } + private function getDefaultSchemaValue() + { + if (!$this->getIsLazyLoaded() && $this->hasMethod('getDefaultValue')) { + return $this->getDefaultValue(); + } + return $this->Value(); + } + private function getOptionsForSearchRequest(string $term): array { if (!$this->sourceList) { diff --git a/tests/php/Forms/SearchableDropdownTraitTest.php b/tests/php/Forms/SearchableDropdownTraitTest.php index d6f3aae4c..7f4858c81 100644 --- a/tests/php/Forms/SearchableDropdownTraitTest.php +++ b/tests/php/Forms/SearchableDropdownTraitTest.php @@ -3,6 +3,7 @@ namespace SilverStripe\Forms\Tests; use SilverStripe\Control\HTTPRequest; +use SilverStripe\Core\ClassInfo; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\SearchableDropdownField; @@ -217,4 +218,47 @@ class SearchableDropdownTraitTest extends SapphireTest $this->assertSame('My placeholder', $schema['placeholder']); $this->assertFalse($schema['searchable']); } + + public function provideLazyLoadedDoesntCallGetSource() + { + $methodsToCall = [ + 'Field', + 'getSchemaStateDefaults', + 'getSchemaState', + 'getSchemaDataDefaults', + 'getSchemaData', + ]; + $classes = [ + SearchableMultiDropdownField::class, + SearchableDropdownField::class, + ]; + $scenarios = []; + foreach ($classes as $class) { + foreach ($methodsToCall as $method) { + $scenarios[] = [ + 'fieldClass' => $class, + 'methodToCall' => $method, + ]; + } + } + return $scenarios; + } + + /** + * @dataProvider provideLazyLoadedDoesntCallGetSource + */ + public function testLazyLoadedDoesntCallGetSource(string $fieldClass, string $methodToCall) + { + // Some methods aren't shared between the two form fields. + if (!ClassInfo::hasMethod($fieldClass, $methodToCall)) { + $this->markTestSkipped("$fieldClass doesn't have method $methodToCall - skipping"); + } + // We have to disable the constructor because it ends up calling a static method, and we can't call static methods on mocks. + $mockField = $this->getMockBuilder($fieldClass)->onlyMethods(['getSource'])->disableOriginalConstructor()->getMock(); + $mockField->expects($this->never())->method('getSource'); + $mockField->setIsLazyLoaded(true); + $mockField->setSource(Team::get()); + $mockField->setForm(new Form()); + $mockField->$methodToCall(); + } }