diff --git a/docs/en/examples/elementalarea_with_dataobject.md b/docs/en/examples/elementalarea_with_dataobject.md new file mode 100644 index 000000000..3ceab5686 --- /dev/null +++ b/docs/en/examples/elementalarea_with_dataobject.md @@ -0,0 +1,111 @@ +# Using ElementalArea with DataObject + +## Creating Model and adding ElementalArea + +In this example the class `BlogPost` will be created, it will extend the `DataObject` class. Also a new `BlogPost` Admin section will be created in the Admin panel where CMS users can manage the `BlogPost` objects. + +Let's look at an example: + +**app/src/Admins/BlogPostsAdmin.php** + +```php + ElementalArea::class, + ]; + + private static $owns = ['ElementalArea']; + + // ... +} + ``` + +If you are using `ElementalArea` together with `DataObject`, it is important to define the `CMSEditLink()` method in the class.\ +`BaseElement::CMSEditLink()` relies on a valid CMS link being available in the parent `DataObject` - in this case, `BlogPost`. It is used to navigate directly to the editing section of the particular element.\ +***Note:*** For nested `GridField`s this method can get more complicated. Similarly, if your class is used in multiple admins, you will have to choose one to be the canonical admin section for the purposes of this method. This example only shows the simplest case, where the `BlogPost` class is used directly in `BlogPostsAdmin`. + +```php +// ... + +class BlogPost extends DataObject +{ + // ... + + public function CMSEditLink() + { + // In this example we use BlogPostsAdmin class as Controller + $admin = BlogPostsAdmin::singleton(); + + // Makes link more readable. Instead App\Models\ BlogPost, we get App-Models-BlogPost + $sanitisedClassname = str_replace('\\', '-', $this->ClassName); + + // Returns link to editing section with elements + return Controller::join_links( + $admin->Link($sanitisedClassname), + 'EditForm/field/', + $sanitisedClassname, + 'item', + $this->ID, + ); + } + + // ... +} + +``` + +And finally, add `ElementalAreasExtension` to the `DataObject` + +**app/_config/elemental.yml** + +```yml +App\Models\BlogPost: + extensions: + - DNADesign\Elemental\Extensions\ElementalAreasExtension +``` + +## Related Documentation + +* [Preview](https://docs.silverstripe.org/en/4/developer_guides/customising_the_admin_interface/preview/) +(note that this requires at least `silverstripe/admin` 4.11.0 and `dnadesign/silverstripe-elemental` 4.9.0) \ No newline at end of file diff --git a/src/Models/BaseElement.php b/src/Models/BaseElement.php index 3cb20a215..70d81f744 100644 --- a/src/Models/BaseElement.php +++ b/src/Models/BaseElement.php @@ -243,13 +243,18 @@ public function canEdit($member = null) public function canDelete($member = null) { $extended = $this->extendedCan(__FUNCTION__, $member); + if ($extended !== null) { return $extended; } if ($this->hasMethod('getPage')) { if ($page = $this->getPage()) { - return $page->canArchive($member); + if ($page->hasExtension(Versioned::class)) { + return $page->canArchive($member); + } else { + return $page->canDelete($member); + } } } @@ -568,7 +573,8 @@ public function getSimpleClassName() } /** - * @return null|SiteTree + * Despite the name of the method, getPage can return any type of DataObject + * @return null|DataObject * @throws \Psr\Container\NotFoundExceptionInterface * @throws \SilverStripe\ORM\ValidationException */ @@ -579,7 +585,6 @@ public function getPage() return $this->cacheData['page']; } - $class = DataObject::getSchema()->hasOneComponent($this, 'Parent'); $area = ($this->ParentID) ? DataObject::get_by_id($class, $this->ParentID) : null; if ($area instanceof ElementalArea && $area->exists()) { @@ -638,9 +643,10 @@ public function getAnchor() */ public function AbsoluteLink($action = null) { - if ($page = $this->getPage()) { + $page = $this->getPage(); + + if ($page && ClassInfo::hasMethod($page, 'AbsoluteLink')) { $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor(); - $this->extend('updateAbsoluteLink', $link); return $link; @@ -657,9 +663,10 @@ public function AbsoluteLink($action = null) */ public function Link($action = null) { - if ($page = $this->getPage()) { - $link = $page->Link($action) . '#' . $this->getAnchor(); + $page = $this->getPage(); + if ($page && ClassInfo::hasMethod($page, 'Link')) { + $link = $page->Link($action) . '#' . $this->getAnchor(); $this->extend('updateLink', $link); return $link; @@ -712,35 +719,62 @@ public function CMSEditLink($directLink = false) return $this->cacheData['cms_edit_link']; } + $link = $this->getElementCMSLink($directLink); + $this->extend('updateCMSEditLink', $link); + + if ($link) { + $this->cacheData['cms_edit_link'] = $link; + } + + return $link; + } + + /** + * @param bool $directLink + * @return null|string + */ + private function getElementCMSLink(bool $directLink) + { $relationName = $this->getAreaRelationName(); $page = $this->getPage(); + $link = null; + if (!$page) { - $link = null; - $this->extend('updateCMSEditLink', $link); return $link; } - if (!$page instanceof SiteTree && method_exists($page, 'CMSEditLink')) { - $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm'); - } else { + if ($page instanceof SiteTree) { $link = $page->CMSEditLink(); + } elseif (ClassInfo::hasMethod($page, 'CMSEditLink')) { + $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm'); } - - // In-line editable blocks should just take you to the page. Editable ones should add the suffix for detail form + // In-line editable blocks should just take you to the page. + // Editable ones should add the suffix for detail form. if (!$this->inlineEditable() || $directLink) { - $link = Controller::join_links( - singleton(CMSPageEditController::class)->Link('EditForm'), - $page->ID, - 'field/' . $relationName . '/item/', - $this->ID, - 'edit' - ); + if ($page instanceof SiteTree) { + return Controller::join_links( + singleton(CMSPageEditController::class)->Link('EditForm'), + $page->ID, + 'field', + $relationName, + 'item', + $this->ID, + 'edit' + ); + } else { + // If $page is not a Page, then generate $link base on $page->CMSEditLink() + return Controller::join_links( + $link, + 'field', + $relationName, + 'item', + $this->ID, + 'edit' + ); + } } - $this->extend('updateCMSEditLink', $link); - - $this->cacheData['cms_edit_link'] = $link; return $link; } diff --git a/tests/BaseElementTest.php b/tests/BaseElementTest.php index dee091143..3f8bad3b6 100644 --- a/tests/BaseElementTest.php +++ b/tests/BaseElementTest.php @@ -7,9 +7,13 @@ use DNADesign\Elemental\Models\BaseElement; use DNADesign\Elemental\Models\ElementalArea; use DNADesign\Elemental\Models\ElementContent; +use DNADesign\Elemental\Tests\Src\TestDataObject; use DNADesign\Elemental\Tests\Src\TestElement; +use DNADesign\Elemental\Tests\Src\TestElementDataObject; +use DNADesign\Elemental\Tests\Src\TestDataObjectWithCMSEditLink; use DNADesign\Elemental\Tests\Src\TestPage; use Page; +use ReflectionClass; use SilverStripe\Control\Director; use SilverStripe\Core\Config\Config; use SilverStripe\Dev\FunctionalTest; @@ -18,17 +22,29 @@ class BaseElementTest extends FunctionalTest { - protected static $fixture_file = 'ElementalPageExtensionTest.yml'; + protected static $fixture_file = [ + 'ElementalPageExtensionTest.yml', + 'ElementalAreaDataObjectTest.yml' + ]; protected static $required_extensions = [ Page::class => [ ElementalPageExtension::class, ], + TestDataObject::class => [ + ElementalPageExtension::class, + ], + TestDataObjectWithCMSEditLink::class => [ + ElementalPageExtension::class, + ], ]; protected static $extra_dataobjects = [ TestPage::class, TestElement::class, + TestDataObject::class, + TestDataObjectWithCMSEditLink::class, + TestElementDataObject::class, ]; public function testSimpleClassName() @@ -236,4 +252,141 @@ public function testOnBeforeWriteNoParent() $this->assertEquals(0, (int) $element1->Sort); } + + public function getElementCMSLinkDataProvider() + { + return [ + // Element in DataObject with $directLink === true + 'element1' => [ + TestElement::class, + 'elementDataObject1', + 'http://localhost/test/1/ItemEditForm/field/ElementalArea/item/', + true + ], + // Element in DataObject with $inline_editable = false + 'element2' => [ + TestElementDataObject::class, + 'testElementDataObject1', + 'http://localhost/test/1/ItemEditForm/field/ElementalArea/item/', + ], + // Element in DataObject with $inline_editable = true + 'element3' => [ + ElementContent::class, + 'contentDataObject1', + 'http://localhost/test/1/ItemEditForm', + ], + // Element in Page with $inline_editable = true + 'element4' => [ + ElementContent::class, + 'content1', + 'http://localhost/admin/pages/edit/show/1', + ], + // Element in DataObject with $directLink === true + 'element5' => [ + ElementContent::class, + 'content1', + 'admin/pages/edit/EditForm/1/field/ElementalArea/item/1/edit', + true + ], + // Element without any Page or DataObject + 'element6' => [ + ElementContent::class, + 'contentDataObject2', + null + ], + // DataObject without CMSEditLink method + 'element7' => [ + TestElement::class, + 'elementDataObject2', + null + ], + ]; + } + + /** + * @dataProvider getElementCMSLinkDataProvider + */ + public function testCMSEditLink(string $class, string $element, ?string $link, bool $directLink = false) + { + $object = $this->objFromFixture($class, $element); + $editLink = $object->CMSEditLink($directLink); + + if ($link) { + $this->assertStringContainsString($link, $editLink); + } else { + $this->assertNull($link); + } + } + + public function canDeleteProvider() + { + return [ + // Element on Page + 'element1' => [ + ElementContent::class, + 'content1', + 'ADMIN', + ], + // Element in DataObject + 'element2' => [ + TestElement::class, + 'elementDataObject1', + 'CMS_ACCESS_CMSMain', + ], + ]; + } + + /** + * @dataProvider canDeleteProvider + */ + public function testCanDelete( + string $class, + string $element, + string $permission + ) { + $this->logOut(); + $this->logInWithPermission($permission); + $object = $this->objFromFixture($class, $element); + $canDelete = $object->canDelete(); + $this->assertNotNull($canDelete); + } + + public function linksProvider() + { + return [ + // Element on Page + 'element1' => [ + ElementContent::class, + 'content1', + '/test-elemental/#e1', + ], + // Element in DataObject + 'element2' => [ + TestElement::class, + 'elementDataObject1', + null, + ], + ]; + } + + /** + * @dataProvider linksProvider + */ + public function testLinkWithDataObject(string $class, string $element, ?string $link) + { + $object = $this->objFromFixture($class, $element); + $canDelete = $object->Link(); + $this->assertEquals($link, $canDelete); + } + + /** + * @dataProvider linksProvider + */ + public function testAbsoluteLink(string $class, string $element, ?string $link) + { + $link = $link ? Director::absoluteURL($link) : $link; + $object = $this->objFromFixture($class, $element); + $absoluteLink = $object->AbsoluteLink(); + $this->assertEquals($link, $absoluteLink); + } } diff --git a/tests/ElementalAreaDataObjectTest.yml b/tests/ElementalAreaDataObjectTest.yml new file mode 100644 index 000000000..3763e40cd --- /dev/null +++ b/tests/ElementalAreaDataObjectTest.yml @@ -0,0 +1,44 @@ +DNADesign\Elemental\Models\ElementalArea: + areaDataObject1: + Title: Area 1 + OwnerClassName: DNADesign\Elemental\Tests\Src\TestDataObjectWithCMSEditLink + areaDataObject2: + Title: Area 2 + OwnerClassName: DNADesign\Elemental\Tests\Src\TestDataObject + areaWithoutPage: + Title: Area without page + +DNADesign\Elemental\Tests\Src\TestDataObjectWithCMSEditLink: + dataObject1: + Title: DataObject with CMSEditLink method + ElementalAreaID: =>DNADesign\Elemental\Models\ElementalArea.areaDataObject1 + +DNADesign\Elemental\Tests\Src\TestDataObject: + dataObject2: + Title: DataObject without CMSEditLink method + ElementalAreaID: =>DNADesign\Elemental\Models\ElementalArea.areaDataObject2 + +DNADesign\Elemental\Tests\Src\TestElement: + elementDataObject1: + Title: Element 1 + TestValue: 'Hello Test' + ParentID: =>DNADesign\Elemental\Models\ElementalArea.areaDataObject1 + elementDataObject2: + Title: Element 2 + TestValue: 'Hello Test' + ParentID: =>DNADesign\Elemental\Models\ElementalArea.areaDataObject2 + +DNADesign\Elemental\Tests\Src\TestElementDataObject: + testElementDataObject1: + Title: Test Element inline-editable + TestValue: 'Hello Test' + ParentID: =>DNADesign\Elemental\Models\ElementalArea.areaDataObject1 + +DNADesign\Elemental\Models\ElementContent: + contentDataObject1: + HTML: Some content + ParentID: =>DNADesign\Elemental\Models\ElementalArea.areaDataObject1 + contentDataObject2: + HTML: Some content + ParentID: =>DNADesign\Elemental\Models\ElementalArea.areaWithoutPage + \ No newline at end of file diff --git a/tests/Src/TestDataObject.php b/tests/Src/TestDataObject.php new file mode 100644 index 000000000..524299bfa --- /dev/null +++ b/tests/Src/TestDataObject.php @@ -0,0 +1,28 @@ + 'Varchar(255)', + 'Content' => 'HTMLText', + ]; + + private static $has_one = [ + 'ElementalArea' => ElementalArea::class, + ]; + + private static $owns = [ + 'ElementalArea', + ]; +} diff --git a/tests/Src/TestDataObjectWithCMSEditLink.php b/tests/Src/TestDataObjectWithCMSEditLink.php new file mode 100644 index 000000000..af691ff60 --- /dev/null +++ b/tests/Src/TestDataObjectWithCMSEditLink.php @@ -0,0 +1,37 @@ + 'Varchar(255)', + 'Content' => 'HTMLText', + ]; + + private static $has_one = [ + 'ElementalArea' => ElementalArea::class, + ]; + + private static $owns = [ + 'ElementalArea', + ]; + + public function CMSEditLink() + { + $link = Controller::join_links( + 'test/', + $this->ID, + ); + return Director::absoluteURL($link); + } +} diff --git a/tests/Src/TestElementDataObject.php b/tests/Src/TestElementDataObject.php new file mode 100644 index 000000000..c3a527485 --- /dev/null +++ b/tests/Src/TestElementDataObject.php @@ -0,0 +1,22 @@ + 'Text', + ]; + + private static bool $inline_editable = false; + + public function getType() + { + return 'A test element in DataObject'; + } +}