Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW Add an extension to dynamically generate edit URLs #1361

Merged
merged 2 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions code/CMSEditLinkExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace SilverStripe\Admin;

use LogicException;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Core\Extension;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\ORM\DataObject;

/**
* An extension 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 extension to both the parent and the child object and the links
* will chain down the nested `GridField`s to the root cms_edit_owner.
*
* You must set a cms_edit_owner config variable which defines the cms edit
* owner for this class.
* e.g. set this to a {@link LeftAndMain} class:
* private static string cms_edit_owner = MyModelAdmin::class;
* or to a has_one relation:
* private static string cms_edit_owner = 'Parent';
*
* Note that the cms_edit_owner must implement a getCMSEditLinkForManagedDataObject() method.
*
* If the cms_edit_owner is a has_one relation, the class on the other end
* of the relation must have a CMSEditLink() method.
*/
class CMSEditLinkExtension extends Extension
{
private static string $cms_edit_owner = '';

/**
* Get the ModelAdmin, LeftAndMain, or DataObject which owns this object for CMS editing purposes.
*
* @return LeftAndMain|DataObject|null
*/
public function getCMSEditOwner()
{
$ownerType = $this->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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private function constructLink(string $relation, int $id): string
private function constructCMSEditLink(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
);
}
}
9 changes: 7 additions & 2 deletions code/ModelAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -450,7 +450,7 @@ public function SearchForm()
* }
* </code>
*
* 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
*/
Expand Down Expand Up @@ -538,11 +538,16 @@ public function getManagedModels()

// Normalize models to have their model class in array key
foreach ($models as $k => $v) {
// No custom tab url segment
if (is_numeric($k)) {
$models[$v] = ['dataClass' => $v, 'title' => singleton($v)->i18n_plural_name()];
unset($models[$k]);
// Custom title but no custom tab url segment
} elseif (is_array($v) && !isset($v['dataClass'])) {
$models[$k]['dataClass'] = $k;
// Custom tab url segment but no custom title
} elseif (is_a($v, DataObject::class, true)) {
$models[$k] = ['dataClass' => $v, 'title' => singleton($v)->i18n_plural_name()];
}
}

Expand Down
97 changes: 97 additions & 0 deletions tests/php/CMSEditLinkExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace SilverStripe\Admin\Tests;

use LogicException;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\BasicNestedObject;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\ManagedDataObject;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\CMSEditModelAdmin;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\PolymorphicNestedObject;
use SilverStripe\Dev\SapphireTest;

class CMSEditLinkExtensionTest extends SapphireTest
{
protected static $fixture_file = 'CMSEditLinkExtensionTest.yml';

protected $usesDatabase = true;

protected static $extra_dataobjects = [
ManagedDataObject::class,
BasicNestedObject::class,
NestedObject::class,
PolymorphicNestedObject::class,
];

protected static $extra_controllers = [
CMSEditModelAdmin::class,
];

public function testGetCMSEditOwner()
{
$adminSingleton = CMSEditModelAdmin::singleton();
$root = $this->objFromFixture(ManagedDataObject::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(ManagedDataObject::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(ManagedDataObject::class, 'root');
$nested = $this->objFromFixture(NestedObject::class, 'redHerringOne');

$this->expectException(LogicException::class);
$this->assertNull($root->getCMSEditLinkForManagedDataObject($nested, 'AnotherOfTheSameClass'));
}

public function testCMSEditLink()
{
$root = $this->objFromFixture(ManagedDataObject::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()
);
}
}
33 changes: 33 additions & 0 deletions tests/php/CMSEditLinkExtensionTest.yml
Original file line number Diff line number Diff line change
@@ -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\ManagedDataObject:
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'
26 changes: 26 additions & 0 deletions tests/php/CMSEditLinkExtensionTest/BasicNestedObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace SilverStripe\Admin\Tests\CMSEditLinkExtensionTest;

use SilverStripe\Admin\CMSEditLinkExtension;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;

class BasicNestedObject extends DataObject implements TestOnly
{
private static $table_name = 'CMSEditLinkTest_BasicNestedObject';

private static $cms_edit_owner = 'Parent';

private static $db = [
'Name' => 'Varchar(255)',
];

private static $has_one = [
'Parent' => ManagedDataObject::class,
];

private static $extensions = [
CMSEditLinkExtension::class,
];
}
15 changes: 15 additions & 0 deletions tests/php/CMSEditLinkExtensionTest/CMSEditModelAdmin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace SilverStripe\Admin\Tests\CMSEditLinkExtensionTest;

use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Dev\TestOnly;

class CMSEditModelAdmin extends ModelAdmin implements TestOnly
{
private static $url_segment = 'cms-edit-test';

private static $managed_models = [
'belongsHere' => ManagedDataObject::class,
];
}
37 changes: 37 additions & 0 deletions tests/php/CMSEditLinkExtensionTest/ManagedDataObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace SilverStripe\Admin\Tests\CMSEditLinkExtensionTest;

use SilverStripe\Admin\CMSEditLinkExtension;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;

class ManagedDataObject extends DataObject implements TestOnly
{
private static $table_name = 'CMSEditLinkTest_ManagedDataObject';

private static $cms_edit_owner = CMSEditModelAdmin::class;

private static $db = [
'Name' => '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,
];
}
Loading