-
Notifications
You must be signed in to change notification settings - Fork 96
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NEW Add a trait to dynamically generate edit URLs
- Loading branch information
1 parent
36fd3f4
commit abc3fe5
Showing
8 changed files
with
401 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Admin; | ||
|
||
use LogicException; | ||
use SilverStripe\CMS\Controllers\CMSMain; | ||
use SilverStripe\Control\Controller; | ||
use SilverStripe\Control\Director; | ||
use SilverStripe\Forms\FieldList; | ||
use SilverStripe\Forms\GridField\GridField; | ||
use SilverStripe\Forms\GridField\GridFieldDetailForm; | ||
use SilverStripe\ORM\DataObject; | ||
|
||
/** | ||
* A trait that automatically generates a CMS edit link for DataObjects even if | ||
* they are canonically edited in some nested {@link GridField}. | ||
* Designed to be used in conjunction with the {@link CMSPreviewable} interface. | ||
* | ||
* For nested relations (e.g. a DataObject managed in a GridField of another DataObject) | ||
* you can apply this trait to both the parent and the child object and the links | ||
* will chain down the nested `GridField`s to the root canonical edit owner. | ||
* | ||
* You must set a canonical_edit_owner config variable which defines the canonical | ||
* owner for this class. | ||
* e.g. set this to a {@link LeftAndMain} class: | ||
* private static string canonical_edit_owner = MyModelAdmin::class; | ||
* or to a has_one relation: | ||
* private static string canonical_edit_owner = 'Parent'; | ||
* | ||
* Note that the canonical edit owner must implement a getEditLinkForObject() method. | ||
* | ||
* If the canonical edit owner is a has_one relation, the class on the other end | ||
* of the relation must have a CMSEditLink() method. | ||
*/ | ||
trait HasCanonicalEditLink | ||
{ | ||
/** | ||
* Get the admin or DataObject which owns this object for CMS editing purposes. | ||
* | ||
* @return LeftAndMain|DataObject|null | ||
*/ | ||
public function getCanonicalEditOwner() | ||
{ | ||
$ownerType = static::config()->get('canonical_edit_owner'); | ||
if (is_subclass_of($ownerType, LeftAndMain::class)) { | ||
return $ownerType::singleton(); | ||
} | ||
return $this->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 getEditLinkForObject(DataObject $obj, string $reciprocalRelation): string | ||
{ | ||
$fields = $this->getCMSFields(); | ||
$link = $this->getLinkForRelation($this->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->getCanonicalEditOwner(); | ||
if (!$owner || !$owner->exists()) { | ||
return ''; | ||
} | ||
|
||
if (!$owner->hasMethod('getEditLinkForObject')) { | ||
throw new LogicException('The canonical owner must implement getEditLinkForObject()'); | ||
} | ||
|
||
if ($owner instanceof DataObject) { | ||
$relativeLink = $owner->getEditLinkForObject($this, static::config()->get('canonical_edit_owner')); | ||
} else { | ||
$relativeLink = $owner->getEditLinkForObject($this); | ||
} | ||
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 = static::config()->get('canonical_edit_owner'); | ||
$prefix = is_a($ownerType, CMSMain::class, true) ? 'field' : 'ItemEditForm/field'; | ||
return Controller::join_links( | ||
$this->CMSEditLink(), | ||
$prefix, | ||
$relation, | ||
'item', | ||
$id | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Admin\Tests; | ||
|
||
use LogicException; | ||
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BasicNestedObject; | ||
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BelongsToModelAdmin; | ||
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\CanonicalModelAdmin; | ||
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\NestedObject; | ||
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\PolymorphicNestedObject; | ||
use SilverStripe\Dev\SapphireTest; | ||
|
||
class HasCanonicalEditLinkTest extends SapphireTest | ||
{ | ||
protected static $fixture_file = 'HasCanonicalEditLinkTest.yml'; | ||
|
||
protected $usesDatabase = true; | ||
|
||
protected static $extra_dataobjects = [ | ||
BelongsToModelAdmin::class, | ||
BasicNestedObject::class, | ||
NestedObject::class, | ||
PolymorphicNestedObject::class, | ||
]; | ||
|
||
protected static $extra_controllers = [ | ||
CanonicalModelAdmin::class, | ||
]; | ||
|
||
public function testGetCanonicalEditOwner() | ||
{ | ||
$adminSingleton = CanonicalModelAdmin::singleton(); | ||
$root = $this->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 testGetEditLinkForObject() | ||
{ | ||
$root = $this->objFromFixture(BelongsToModelAdmin::class, 'root'); | ||
$basicNested = $this->objFromFixture(BasicNestedObject::class, 'one'); | ||
$nested = $this->objFromFixture(NestedObject::class, 'one'); | ||
$polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one'); | ||
|
||
$this->assertSame( | ||
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/BasicNested/item/$basicNested->ID", | ||
$root->getEditLinkForObject($basicNested, 'Parent') | ||
); | ||
$this->assertSame( | ||
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/Nested/item/$nested->ID", | ||
$root->getEditLinkForObject($nested, 'Parent') | ||
); | ||
$this->assertSame( | ||
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/Polymorphic/item/$polymorphic->ID", | ||
$root->getEditLinkForObject($polymorphic, 'Parent') | ||
); | ||
} | ||
|
||
public function testGetEditLinkForObjectException() | ||
{ | ||
$root = $this->objFromFixture(BelongsToModelAdmin::class, 'root'); | ||
$nested = $this->objFromFixture(NestedObject::class, 'redHerringOne'); | ||
|
||
$this->expectException(LogicException::class); | ||
$this->assertNull($root->getEditLinkForObject($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'); | ||
|
||
$this->assertSame( | ||
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID", | ||
$root->CMSEditLink() | ||
); | ||
$this->assertSame( | ||
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/BasicNested/item/$basicNested->ID", | ||
$basicNested->CMSEditLink() | ||
); | ||
$this->assertSame( | ||
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/Nested/item/$nested->ID", | ||
$nested->CMSEditLink() | ||
); | ||
$this->assertSame( | ||
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/Polymorphic/item/$polymorphic->ID", | ||
$polymorphic->CMSEditLink() | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BasicNestedObject: | ||
one: | ||
Name: 'some name' | ||
|
||
SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\PolymorphicNestedObject: | ||
one: | ||
Name: 'some name' | ||
|
||
SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\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\HasCanonicalEditLinkTest\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\HasCanonicalEditLinkTest\NestedObject.one' | ||
ArbitraryRelation: | ||
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\NestedObject.redHerringOne' | ||
AnotherArbitraryRelation: | ||
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\NestedObject.redHerringTwo' | ||
BasicNested: | ||
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BasicNestedObject.one' | ||
PolyMorphic: | ||
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\PolymorphicNestedObject.one' | ||
redHerringTwo: | ||
Name: 'This exists so we know it doesnt just grab the last record' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Admin\Tests\HasCanonicalEditLinkTest; | ||
|
||
use SilverStripe\Admin\HasCanonicalEditLink; | ||
use SilverStripe\Dev\TestOnly; | ||
use SilverStripe\ORM\DataObject; | ||
|
||
class BasicNestedObject extends DataObject implements TestOnly | ||
{ | ||
use HasCanonicalEditLink; | ||
|
||
private static $table_name = 'HasCanonicalEditLinkTest_BasicNestedObject'; | ||
|
||
private static $canonical_edit_owner = 'Parent'; | ||
|
||
private static $db = [ | ||
'Name' => 'Varchar(25)', | ||
]; | ||
|
||
private static $has_one = [ | ||
'Parent' => BelongsToModelAdmin::class, | ||
]; | ||
} |
35 changes: 35 additions & 0 deletions
35
tests/php/HasCanonicalEditLinkTest/BelongsToModelAdmin.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Admin\Tests\HasCanonicalEditLinkTest; | ||
|
||
use SilverStripe\Admin\HasCanonicalEditLink; | ||
use SilverStripe\Dev\TestOnly; | ||
use SilverStripe\ORM\DataObject; | ||
|
||
class BelongsToModelAdmin extends DataObject implements TestOnly | ||
{ | ||
use HasCanonicalEditLink; | ||
|
||
private static $table_name = 'HasCanonicalEditLinkTest_BelongsToModelAdmin'; | ||
|
||
private static $canonical_edit_owner = CanonicalModelAdmin::class; | ||
|
||
private static $db = [ | ||
'Name' => '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; | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
tests/php/HasCanonicalEditLinkTest/CanonicalModelAdmin.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Admin\Tests\HasCanonicalEditLinkTest; | ||
|
||
use SilverStripe\Admin\ModelAdmin; | ||
use SilverStripe\Dev\TestOnly; | ||
|
||
class CanonicalModelAdmin extends ModelAdmin implements TestOnly | ||
{ | ||
private static $url_segment = 'canonical-test'; | ||
|
||
private static $managed_models = [ | ||
'belongsHere' => BelongsToModelAdmin::class, | ||
]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Admin\Tests\HasCanonicalEditLinkTest; | ||
|
||
use SilverStripe\Admin\HasCanonicalEditLink; | ||
use SilverStripe\Dev\TestOnly; | ||
use SilverStripe\ORM\DataObject; | ||
|
||
class NestedObject extends DataObject implements TestOnly | ||
{ | ||
use HasCanonicalEditLink; | ||
|
||
private static $table_name = 'HasCanonicalEditLinkTest_NestedObject'; | ||
|
||
private static $canonical_edit_owner = 'Parent'; | ||
|
||
private static $db = [ | ||
'Name' => 'Varchar(25)', | ||
]; | ||
|
||
private static $has_one = [ | ||
'Parent' => BelongsToModelAdmin::class, | ||
'AnotherOfTheSameClass' => BelongsToModelAdmin::class, | ||
'ThirdOne' => BelongsToModelAdmin::class, | ||
]; | ||
} |
Oops, something went wrong.