diff --git a/code/CMSEditLinkExtension.php b/code/CMSEditLinkExtension.php new file mode 100644 index 000000000..61c070ff2 --- /dev/null +++ b/code/CMSEditLinkExtension.php @@ -0,0 +1,149 @@ +owner->config()->get('canonical_edit_owner'); + if (is_subclass_of($ownerType, LeftAndMain::class)) { + return $ownerType::singleton(); + } + return $this->owner->getComponent($ownerType); + } + + /** + * Get the link for editing an object from the CMS edit form of this object. + * @throws LogicException if a link cannot be established + * e.g. if the object is not in a has_many relation or not edited inside a GridField. + */ + public function getEditLinkForManagedDataObject(DataObject $obj, string $reciprocalRelation): string + { + $fields = $this->owner->getCMSFields(); + $link = $this->getLinkForRelation($this->owner->hasMany(false), $obj, $reciprocalRelation, $fields); + if (!$link) { + throw new LogicException('Could not produce an edit link for the passed object.'); + } + return $link; + } + + /** + * Get a link to edit this page in the CMS. + */ + public function CMSEditLink(): string + { + $owner = $this->owner->getCanonicalEditOwner(); + if (!$owner || !$owner->exists()) { + return ''; + } + + if (!$owner->hasMethod('getEditLinkForManagedDataObject')) { + throw new LogicException('The canonical owner must implement getEditLinkForManagedDataObject()'); + } + + if ($owner instanceof DataObject) { + $relativeLink = $owner->getEditLinkForManagedDataObject($this->owner, $this->owner->config()->get('canonical_edit_owner')); + } else { + $relativeLink = $owner->getEditLinkForManagedDataObject($this->owner); + } + return Director::absoluteURL($relativeLink); + } + + private function getLinkForRelation(array $componentConfig, DataObject $obj, string $reciprocalRelation, FieldList $fields): string + { + $candidate = null; + foreach ($componentConfig as $relation => $class) { + // Check for dot notation being used to explicitly mark the reciprocal relation. + $remoteField = null; + if (strpos($class ?? '', '.') !== false) { + list($class, $remoteField) = explode('.', $class ?? ''); + } + + // We're only interested in relations to the $obj class. + if (!is_a($obj, $class)) { + continue; + } + + if ($remoteField) { + if ($remoteField === $reciprocalRelation) { + // We've found a direct reciprocal relation, so this is definitely correct. + if ($this->relationIsEditable($relation, $fields)) { + return $this->constructLink($relation, $obj->ID); + } + // If the relation isn't in a gridfield, we have no link for it. + return ''; + } + // We're not interested in unrelated relations. + continue; + } + + // Check for relations that have gridfields we can build a link from. + if ($this->relationIsEditable($relation, $fields)) { + $candidate = $relation; + } + } + + // Only do this if we didn't find a direct reciprocal relation. + return $candidate ? $this->constructLink($candidate, $obj->ID) : ''; + } + + private function relationIsEditable(string $relation, FieldList $fields): bool + { + $field = $fields->dataFieldByName($relation); + return $field + && $field instanceof GridField + && $field->getConfig()->getComponentByType(GridFieldDetailForm::class); + } + + private function constructLink(string $relation, int $id): string + { + $ownerType = $this->owner->config()->get('canonical_edit_owner'); + $prefix = is_a($ownerType, CMSMain::class, true) ? 'field' : 'ItemEditForm/field'; + return Controller::join_links( + $this->owner->CMSEditLink(), + $prefix, + $relation, + 'item', + $id + ); + } +} diff --git a/tests/php/CMSEditLinkExtensionTest.php b/tests/php/CMSEditLinkExtensionTest.php new file mode 100644 index 000000000..362ff3851 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest.php @@ -0,0 +1,97 @@ +objFromFixture(BelongsToModelAdmin::class, 'root'); + $basicNested = $this->objFromFixture(BasicNestedObject::class, 'one'); + $nested = $this->objFromFixture(NestedObject::class, 'one'); + $polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one'); + + $this->assertSame($adminSingleton, $root->getCanonicalEditOwner()); + $this->assertSame($root->ID, $basicNested->getCanonicalEditOwner()->ID); + $this->assertSame($root->ID, $nested->getCanonicalEditOwner()->ID); + $this->assertSame($root->ID, $polymorphic->getCanonicalEditOwner()->ID); + } + + public function testGetEditLinkForDataObject() + { + $root = $this->objFromFixture(BelongsToModelAdmin::class, 'root'); + $basicNested = $this->objFromFixture(BasicNestedObject::class, 'one'); + $nested = $this->objFromFixture(NestedObject::class, 'one'); + $polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one'); + + $rootUrl = "http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID"; + $this->assertSame( + "$rootUrl/ItemEditForm/field/BasicNested/item/$basicNested->ID", + $root->getEditLinkForManagedDataObject($basicNested, 'Parent') + ); + $this->assertSame( + "$rootUrl/ItemEditForm/field/Nested/item/$nested->ID", + $root->getEditLinkForManagedDataObject($nested, 'Parent') + ); + $this->assertSame( + "$rootUrl/ItemEditForm/field/PolyMorphic/item/$polymorphic->ID", + $root->getEditLinkForManagedDataObject($polymorphic, 'Parent') + ); + } + + public function testGetEditLinkForDataObjectException() + { + $root = $this->objFromFixture(BelongsToModelAdmin::class, 'root'); + $nested = $this->objFromFixture(NestedObject::class, 'redHerringOne'); + + $this->expectException(LogicException::class); + $this->assertNull($root->getEditLinkForManagedDataObject($nested, 'AnotherOfTheSameClass')); + } + + public function testCMSEditLink() + { + $root = $this->objFromFixture(BelongsToModelAdmin::class, 'root'); + $basicNested = $this->objFromFixture(BasicNestedObject::class, 'one'); + $nested = $this->objFromFixture(NestedObject::class, 'one'); + $polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one'); + + $rootUrl = "http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID"; + $this->assertSame($rootUrl, $root->CMSEditLink()); + $this->assertSame( + "$rootUrl/ItemEditForm/field/BasicNested/item/$basicNested->ID", + $basicNested->CMSEditLink() + ); + $this->assertSame( + "$rootUrl/ItemEditForm/field/Nested/item/$nested->ID", + $nested->CMSEditLink() + ); + $this->assertSame( + "$rootUrl/ItemEditForm/field/PolyMorphic/item/$polymorphic->ID", + $polymorphic->CMSEditLink() + ); + } +} diff --git a/tests/php/CMSEditLinkExtensionTest.yml b/tests/php/CMSEditLinkExtensionTest.yml new file mode 100644 index 000000000..69a9a0ca3 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest.yml @@ -0,0 +1,33 @@ +SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\BasicNestedObject: + one: + Name: 'some name' + +SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\PolymorphicNestedObject: + one: + Name: 'some name' + +SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject: + one: + Name: 'some name' + redHerringOne: + Name: 'This exists so there is a record in an edge-case relation' + redHerringTwo: + Name: 'This exists so there is a record in an edge-case relation' + +SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\BelongsToModelAdmin: + redHerringOne: + Name: 'This exists so we know it doesnt just grab the first record' + root: + Name: 'this is the record we care about' + Nested: + - '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject.one' + ArbitraryRelation: + - '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject.redHerringOne' + AnotherArbitraryRelation: + - '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject.redHerringTwo' + BasicNested: + - '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\BasicNestedObject.one' + PolyMorphic: + - '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\PolymorphicNestedObject.one' + redHerringTwo: + Name: 'This exists so we know it doesnt just grab the last record' diff --git a/tests/php/CMSEditLinkExtensionTest/BasicNestedObject.php b/tests/php/CMSEditLinkExtensionTest/BasicNestedObject.php new file mode 100644 index 000000000..16c646110 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/BasicNestedObject.php @@ -0,0 +1,26 @@ + 'Varchar(25)', + ]; + + private static $has_one = [ + 'Parent' => BelongsToModelAdmin::class, + ]; + + private static $extensions = [ + CMSEditLinkExtension::class, + ]; +} diff --git a/tests/php/CMSEditLinkExtensionTest/BelongsToModelAdmin.php b/tests/php/CMSEditLinkExtensionTest/BelongsToModelAdmin.php new file mode 100644 index 000000000..88e779be4 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/BelongsToModelAdmin.php @@ -0,0 +1,37 @@ + 'Varchar(25)', + ]; + + private static $has_many = [ + 'ArbitraryRelation' => NestedObject::class, + 'Nested' => NestedObject::class . '.Parent', + 'AnotherArbitraryRelation' => NestedObject::class . '.AnotherOfTheSameClass', + 'BasicNested' => BasicNestedObject::class, + 'PolyMorphic' => PolymorphicNestedObject::class, + ]; + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + $fields->removeByName(['ArbitraryRelation', 'AnotherArbitraryRelation']); + return $fields; + } + + private static $extensions = [ + CMSEditLinkExtension::class, + ]; +} diff --git a/tests/php/CMSEditLinkExtensionTest/CanonicalModelAdmin.php b/tests/php/CMSEditLinkExtensionTest/CanonicalModelAdmin.php new file mode 100644 index 000000000..cbb868fe0 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/CanonicalModelAdmin.php @@ -0,0 +1,15 @@ + BelongsToModelAdmin::class, + ]; +} diff --git a/tests/php/CMSEditLinkExtensionTest/NestedObject.php b/tests/php/CMSEditLinkExtensionTest/NestedObject.php new file mode 100644 index 000000000..2cd562407 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/NestedObject.php @@ -0,0 +1,28 @@ + 'Varchar(25)', + ]; + + private static $has_one = [ + 'Parent' => BelongsToModelAdmin::class, + 'AnotherOfTheSameClass' => BelongsToModelAdmin::class, + 'ThirdOne' => BelongsToModelAdmin::class, + ]; + + private static $extensions = [ + CMSEditLinkExtension::class, + ]; +} diff --git a/tests/php/CMSEditLinkExtensionTest/PolymorphicNestedObject.php b/tests/php/CMSEditLinkExtensionTest/PolymorphicNestedObject.php new file mode 100644 index 000000000..6c6474c36 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/PolymorphicNestedObject.php @@ -0,0 +1,26 @@ + 'Varchar(25)', + ]; + + private static $has_one = [ + 'Parent' => DataObject::class, + ]; + + private static $extensions = [ + CMSEditLinkExtension::class, + ]; +}