From 052ed99fe79ee41b202560a9cc17855f02e5e529 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Tue, 13 Sep 2022 16:08:18 +1200 Subject: [PATCH] NEW Add an extension to dynamically generate edit URLs --- code/CMSEditLinkExtension.php | 149 ++++++++++++++++++ code/ModelAdmin.php | 4 +- tests/php/CMSEditLinkExtensionTest.php | 97 ++++++++++++ tests/php/CMSEditLinkExtensionTest.yml | 33 ++++ .../BasicNestedObject.php | 26 +++ .../BelongsToModelAdmin.php | 37 +++++ .../CMSEditModelAdmin.php | 15 ++ .../CMSEditLinkExtensionTest/NestedObject.php | 28 ++++ .../PolymorphicNestedObject.php | 26 +++ tests/php/ModelAdminTest.php | 8 +- 10 files changed, 417 insertions(+), 6 deletions(-) create mode 100644 code/CMSEditLinkExtension.php create mode 100644 tests/php/CMSEditLinkExtensionTest.php create mode 100644 tests/php/CMSEditLinkExtensionTest.yml create mode 100644 tests/php/CMSEditLinkExtensionTest/BasicNestedObject.php create mode 100644 tests/php/CMSEditLinkExtensionTest/BelongsToModelAdmin.php create mode 100644 tests/php/CMSEditLinkExtensionTest/CMSEditModelAdmin.php create mode 100644 tests/php/CMSEditLinkExtensionTest/NestedObject.php create mode 100644 tests/php/CMSEditLinkExtensionTest/PolymorphicNestedObject.php diff --git a/code/CMSEditLinkExtension.php b/code/CMSEditLinkExtension.php new file mode 100644 index 000000000..f952bee2a --- /dev/null +++ b/code/CMSEditLinkExtension.php @@ -0,0 +1,149 @@ +owner->config()->get('cms_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 getCMSEditLinkForManagedDataObject(DataObject $obj, string $reciprocalRelation): string + { + $fields = $this->owner->getCMSFields(); + $link = $this->getCMSEditLinkForRelation($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 DataObject in the CMS. + */ + public function CMSEditLink(): string + { + $owner = $this->owner->getCMSEditOwner(); + if (!$owner || !$owner->exists()) { + return ''; + } + + if (!$owner->hasMethod('getCMSEditLinkForManagedDataObject')) { + throw new LogicException('The cms edit owner must implement getCMSEditLinkForManagedDataObject()'); + } + + if ($owner instanceof DataObject) { + $relativeLink = $owner->getCMSEditLinkForManagedDataObject($this->owner, $this->owner->config()->get('cms_edit_owner')); + } else { + $relativeLink = $owner->getCMSEditLinkForManagedDataObject($this->owner); + } + return Director::absoluteURL($relativeLink); + } + + private function getCMSEditLinkForRelation(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('cms_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/code/ModelAdmin.php b/code/ModelAdmin.php index b510376a4..c0a33681c 100644 --- a/code/ModelAdmin.php +++ b/code/ModelAdmin.php @@ -227,7 +227,7 @@ public function getLinkForModelTab(string $modelTab): string * * @throws InvalidArgumentException if $obj is not managed by this ModelAdmin. */ - public function getEditLinkForManagedDataObject(DataObject $obj): string + public function getCMSEditLinkForManagedDataObject(DataObject $obj): string { $modelTab = $this->getModelTabForModelClass($obj->ClassName); if ($modelTab === null) { @@ -450,7 +450,7 @@ public function SearchForm() * } * * - * Note: If you override this method you may also need to override getEditLinkForManagedDataObject() + * Note: If you override this method you may also need to override getCMSEditLinkForManagedDataObject() * * @return \SilverStripe\ORM\DataList */ diff --git a/tests/php/CMSEditLinkExtensionTest.php b/tests/php/CMSEditLinkExtensionTest.php new file mode 100644 index 000000000..140c5857f --- /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->getCMSEditOwner()); + $this->assertSame($root->ID, $basicNested->getCMSEditOwner()->ID); + $this->assertSame($root->ID, $nested->getCMSEditOwner()->ID); + $this->assertSame($root->ID, $polymorphic->getCMSEditOwner()->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/cms-edit-test/belongsHere/EditForm/field/belongsHere/item/$root->ID"; + $this->assertSame( + "$rootUrl/ItemEditForm/field/BasicNested/item/$basicNested->ID", + $root->getCMSEditLinkForManagedDataObject($basicNested, 'Parent') + ); + $this->assertSame( + "$rootUrl/ItemEditForm/field/Nested/item/$nested->ID", + $root->getCMSEditLinkForManagedDataObject($nested, 'Parent') + ); + $this->assertSame( + "$rootUrl/ItemEditForm/field/PolyMorphic/item/$polymorphic->ID", + $root->getCMSEditLinkForManagedDataObject($polymorphic, 'Parent') + ); + } + + public function testGetEditLinkForDataObjectException() + { + $root = $this->objFromFixture(BelongsToModelAdmin::class, 'root'); + $nested = $this->objFromFixture(NestedObject::class, 'redHerringOne'); + + $this->expectException(LogicException::class); + $this->assertNull($root->getCMSEditLinkForManagedDataObject($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/cms-edit-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..310a13b7e --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/BasicNestedObject.php @@ -0,0 +1,26 @@ + 'Varchar(255)', + ]; + + 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..e8d6880bc --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/BelongsToModelAdmin.php @@ -0,0 +1,37 @@ + 'Varchar(255)', + ]; + + 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/CMSEditModelAdmin.php b/tests/php/CMSEditLinkExtensionTest/CMSEditModelAdmin.php new file mode 100644 index 000000000..f19362cd6 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/CMSEditModelAdmin.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..223fa6f30 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/NestedObject.php @@ -0,0 +1,28 @@ + 'Varchar(255)', + ]; + + 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..a49917e73 --- /dev/null +++ b/tests/php/CMSEditLinkExtensionTest/PolymorphicNestedObject.php @@ -0,0 +1,26 @@ + 'Varchar(255)', + ]; + + private static $has_one = [ + 'Parent' => DataObject::class, + ]; + + private static $extensions = [ + CMSEditLinkExtension::class, + ]; +} diff --git a/tests/php/ModelAdminTest.php b/tests/php/ModelAdminTest.php index 6fa577f6e..9fe6db7c4 100644 --- a/tests/php/ModelAdminTest.php +++ b/tests/php/ModelAdminTest.php @@ -323,20 +323,20 @@ public function testLinkForInvalidModelTab() $admin->getLinkForModelTab(ContactSubclass::class); } - public function testGetEditLinkForManagedDataObject() + public function testGetCMSEditLinkForManagedDataObject() { $admin = new ModelAdminTest\MultiModelAdmin(); $contact = $this->objFromFixture(Contact::class, 'sam'); $sanitisedContact = $this->sanitiseClassName(Contact::class); $this->assertEquals( "admin/multi/$sanitisedContact/EditForm/field/$sanitisedContact/item/$contact->ID", - $admin->getEditLinkForManagedDataObject($contact) + $admin->getCMSEditLinkForManagedDataObject($contact) ); $contact2 = $this->objFromFixture(ContactSubclass::class, 'danie'); $this->assertEquals( "admin/multi/$sanitisedContact/EditForm/field/$sanitisedContact/item/$contact2->ID", - $admin->getEditLinkForManagedDataObject($contact2) + $admin->getCMSEditLinkForManagedDataObject($contact2) ); // Note: It uses the first tab that has this class - we're using @@ -344,7 +344,7 @@ public function testGetEditLinkForManagedDataObject() $player = $this->objFromFixture(Player::class, 'amy'); $this->assertEquals( "admin/multi/Player/EditForm/field/Player/item/$player->ID", - $admin->getEditLinkForManagedDataObject($player) + $admin->getCMSEditLinkForManagedDataObject($player) ); }