diff --git a/ORM/Hierarchy/Hierarchy.php b/ORM/Hierarchy/Hierarchy.php index 841df76b0..d34a7ebb2 100644 --- a/ORM/Hierarchy/Hierarchy.php +++ b/ORM/Hierarchy/Hierarchy.php @@ -329,7 +329,8 @@ class Hierarchy extends DataExtension { foreach($children as $child) { $markingMatches = $this->markingFilterMatches($child); if($markingMatches) { - if($child->$numChildrenMethod()) { + // Mark a child node as unexpanded if it has children and has not already been expanded + if($child->$numChildrenMethod() && !$child->isExpanded()) { $child->markUnexpanded(); } else { $child->markExpanded(); diff --git a/docs/en/01_Tutorials/05_Dataobject_Relationship_Management.md b/docs/en/01_Tutorials/05_Dataobject_Relationship_Management.md index c52e6f576..918978f4f 100644 --- a/docs/en/01_Tutorials/05_Dataobject_Relationship_Management.md +++ b/docs/en/01_Tutorials/05_Dataobject_Relationship_Management.md @@ -172,7 +172,8 @@ We call `setDisplayFields()` directly on the component responsible for their ren Adding a `GridField` to a page type is a popular way to manage data, but not the only one. If your data requires a dedicated interface with more sophisticated search and management logic, consider - using the `[ModelAdmin](reference/modeladmin)` interface instead. + using the [ModelAdmin](/developer_guides/customising_the_admin_interface/modeladmin) + interface instead. ![tutorial:tutorial5_project_creation.jpg](../_images/tutorial5_project_creation.jpg) diff --git a/forms/Form.php b/forms/Form.php index c8fafec2d..06bde80c7 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -494,13 +494,9 @@ class Form extends RequestHandler { return true; } - // Always allow actions which map to buttons. See httpSubmission() for further access checks. - $fields = $this->fields->dataFields() ?: array(); - $actions = $this->actions->dataFields() ?: array(); - - $fieldsAndActions = array_merge($fields, $actions); - foreach ($fieldsAndActions as $fieldOrAction) { - if ($fieldOrAction instanceof FormAction && $fieldOrAction->actionName() === $action) { + $actions = $this->getAllActions(); + foreach ($actions as $formAction) { + if ($formAction->actionName() === $action) { return true; } } @@ -1734,21 +1730,31 @@ class Form extends RequestHandler { * @return FormAction */ public function buttonClicked() { - $fields = $this->fields->dataFields() ?: array(); - $actions = $this->actions->dataFields() ?: array(); - - if(!$actions && !$fields) { - return null; - } - - $fieldsAndActions = array_merge($fields, $actions); - foreach ($fieldsAndActions as $fieldOrAction) { - if ($fieldOrAction instanceof FormAction && $this->buttonClickedFunc === $fieldOrAction->actionName()) { - return $fieldOrAction; + $actions = $this->getAllActions(); + foreach ($actions as $action) { + if ($this->buttonClickedFunc === $action->actionName()) { + return $action; } } - return null; + return null; + } + + /** + * Get a list of all actions, including those in the main "fields" FieldList + * + * @return array + */ + protected function getAllActions() { + $fields = $this->fields->dataFields() ?: array(); + $actions = $this->actions->dataFields() ?: array(); + + $fieldsAndActions = array_merge($fields, $actions); + $actions = array_filter($fieldsAndActions, function($fieldOrAction) { + return $fieldOrAction instanceof FormAction; + }); + + return $actions; } /** diff --git a/forms/gridfield/GridFieldExportButton.php b/forms/gridfield/GridFieldExportButton.php index 687c63ead..d7f01f716 100644 --- a/forms/gridfield/GridFieldExportButton.php +++ b/forms/gridfield/GridFieldExportButton.php @@ -97,6 +97,25 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP } } + /** + * Return the columns to export + * + * @param GridField $gridField + * + * @return array + */ + protected function getExportColumnsForGridField(GridField $gridField) { + if($this->exportColumns) { + $exportColumns = $this->exportColumns; + } else if($dataCols = $gridField->getConfig()->getComponentByType('GridFieldDataColumns')) { + $exportColumns = $dataCols->getDisplayFields($gridField); + } else { + $exportColumns = singleton($gridField->getModelClass())->summaryFields(); + } + + return $exportColumns; + } + /** * Generate export fields for CSV. * @@ -104,9 +123,7 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP * @return array */ public function generateExportFileData($gridField) { - $csvColumns = ($this->exportColumns) - ? $this->exportColumns - : singleton($gridField->getModelClass())->summaryFields(); + $csvColumns = $this->getExportColumnsForGridField($gridField); $fileData = array(); if($this->csvHasHeader) { diff --git a/tests/model/HierarchyTest.php b/tests/model/HierarchyTest.php index ed756cf74..351ad9129 100644 --- a/tests/model/HierarchyTest.php +++ b/tests/model/HierarchyTest.php @@ -65,12 +65,12 @@ class HierarchyTest extends SapphireTest { // Obj 3 has been deleted; let's bring it back from the grave $obj3 = Versioned::get_including_deleted("HierarchyTest_Object", "\"Title\" = 'Obj 3'")->First(); - // Check that both obj 3 children are returned - $this->assertEquals(array("Obj 3a", "Obj 3b", "Obj 3c"), + // Check that all obj 3 children are returned + $this->assertEquals(array("Obj 3a", "Obj 3b", "Obj 3c", "Obj 3d"), $obj3->AllHistoricalChildren()->column('Title')); // Check numHistoricalChildren - $this->assertEquals(3, $obj3->numHistoricalChildren()); + $this->assertEquals(4, $obj3->numHistoricalChildren()); } @@ -100,11 +100,11 @@ class HierarchyTest extends SapphireTest { public function testNumChildren() { $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj1')->numChildren(), 0); $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj2')->numChildren(), 2); - $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj3')->numChildren(), 3); + $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj3')->numChildren(), 4); $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj2a')->numChildren(), 2); $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj2b')->numChildren(), 0); $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj3a')->numChildren(), 2); - $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj3b')->numChildren(), 0); + $this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj3d')->numChildren(), 0); $obj1 = $this->objFromFixture('HierarchyTest_Object', 'obj1'); $this->assertEquals($obj1->numChildren(), 0); @@ -186,6 +186,53 @@ class HierarchyTest extends SapphireTest { $this->assertEquals('Obj 2 » Obj 2a » Obj 2aa', $obj2aa->getBreadcrumbs()); } + /** + * @covers Hierarchy::markChildren() + */ + public function testMarkChildrenDoesntUnmarkPreviouslyMarked() { + $obj3 = $this->objFromFixture('HierarchyTest_Object', 'obj3'); + $obj3aa = $this->objFromFixture('HierarchyTest_Object', 'obj3aa'); + $obj3ba = $this->objFromFixture('HierarchyTest_Object', 'obj3ba'); + $obj3ca = $this->objFromFixture('HierarchyTest_Object', 'obj3ca'); + + $obj3->markPartialTree(); + $obj3->markToExpose($obj3aa); + $obj3->markToExpose($obj3ba); + $obj3->markToExpose($obj3ca); + + $expected = << +
  • Obj 3a + +
  • +
  • Obj 3b + +
  • +
  • Obj 3c + +
  • +
  • Obj 3d +
  • + + +EOT; + + $this->assertSame($expected, $obj3->getChildrenAsUL()); + } + public function testGetChildrenAsUL() { $obj1 = $this->objFromFixture('HierarchyTest_Object', 'obj1'); $obj2 = $this->objFromFixture('HierarchyTest_Object', 'obj2'); @@ -574,6 +621,8 @@ class HierarchyTest_Object extends DataObject implements TestOnly { 'SilverStripe\\ORM\\Versioning\\Versioned', ); + private static $default_sort = 'Title ASC'; + public function cmstreeclasses() { return $this->markingClasses(); } diff --git a/tests/model/HierarchyTest.yml b/tests/model/HierarchyTest.yml index 614a803dc..c396a966d 100644 --- a/tests/model/HierarchyTest.yml +++ b/tests/model/HierarchyTest.yml @@ -1,46 +1,55 @@ HierarchyTest_Object: - obj1: - Title: Obj 1 - obj2: - Title: Obj 2 - obj3: - Title: Obj 3 - obj2a: - Parent: =>HierarchyTest_Object.obj2 - Title: Obj 2a - obj2b: - Parent: =>HierarchyTest_Object.obj2 - Title: Obj 2b - obj3a: - Parent: =>HierarchyTest_Object.obj3 - Title: Obj 3a - obj3b: - Parent: =>HierarchyTest_Object.obj3 - Title: Obj 3b - obj3c: - Parent: =>HierarchyTest_Object.obj3 - Title: Obj 3c - obj2aa: - Parent: =>HierarchyTest_Object.obj2a - Title: Obj 2aa - obj2ab: - Parent: =>HierarchyTest_Object.obj2a - Title: Obj 2ab - obj3aa: - Parent: =>HierarchyTest_Object.obj3a - Title: Obj 3aa - obj3ab: - Parent: =>HierarchyTest_Object.obj3a - Title: Obj 3ab - + obj1: + Title: Obj 1 + obj2: + Title: Obj 2 + obj3: + Title: Obj 3 + obj2a: + Parent: =>HierarchyTest_Object.obj2 + Title: Obj 2a + obj2b: + Parent: =>HierarchyTest_Object.obj2 + Title: Obj 2b + obj3a: + Parent: =>HierarchyTest_Object.obj3 + Title: Obj 3a + obj3b: + Parent: =>HierarchyTest_Object.obj3 + Title: Obj 3b + obj3c: + Parent: =>HierarchyTest_Object.obj3 + Title: Obj 3c + obj3d: + Parent: =>HierarchyTest_Object.obj3 + Title: Obj 3d + obj2aa: + Parent: =>HierarchyTest_Object.obj2a + Title: Obj 2aa + obj2ab: + Parent: =>HierarchyTest_Object.obj2a + Title: Obj 2ab + obj3aa: + Parent: =>HierarchyTest_Object.obj3a + Title: Obj 3aa + obj3ab: + Parent: =>HierarchyTest_Object.obj3a + Title: Obj 3ab + obj3ba: + Parent: =>HierarchyTest_Object.obj3b + Title: Obj 3ba + obj3bb: + Parent: =>HierarchyTest_Object.obj3b + Title: Obj 3bb + obj3ca: + Parent: =>HierarchyTest_Object.obj3c + Title: Obj 3c HierarchyHideTest_Object: - obj4: - Title: Obj 4 - obj4a: - Parent: =>HierarchyHideTest_Object.obj4 - Title: Obj 4a - + obj4: + Title: Obj 4 + obj4a: + Parent: =>HierarchyHideTest_Object.obj4 + Title: Obj 4a HierarchyHideTest_SubObject: - obj4b: - Parent: =>HierarchyHideTest_Object.obj4 - Title: Obj 4b + obj4b: + Parent: =>HierarchyHideTest_Object.obj4 diff --git a/view/Requirements.php b/view/Requirements.php index f4c778e27..c7b072388 100644 --- a/view/Requirements.php +++ b/view/Requirements.php @@ -199,6 +199,23 @@ class Requirements implements Flushable { self::backend()->themedCSS($name, $module, $media); } + /** + * Registers the given themeable javascript as required. + * + * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for, + * and it that doesn't exist and the module parameter is set then a javascript file with that name in + * the module is used. + * + * @param string $name The name of the file - eg '/javascript/File.js' would have the name 'File' + * @param string $module The module to fall back to if the javascript file does not exist in the + * current theme. + * @param string $type Comma-separated list of types to use in the script tag + * (e.g. 'text/javascript,text/ecmascript') + */ + public static function themedJavascript($name, $module = null, $type = null) { + return self::backend()->themedJavascript($name, $module, $type); + } + /** * Clear either a single or all requirements * @@ -1817,10 +1834,52 @@ class Requirements_Backend return $this->css($path . $css, $media); } } + throw new \InvalidArgumentException( + "The css file doesn't exists. Please check if the file $name.css exists in any context or search for " + . "themedCSS references calling this file in your templates." + ); + } - if($module) { - return $this->css($module . $css, $media); - } + /** + * Registers the given themeable javascript as required. + * + * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for, + * and it that doesn't exist and the module parameter is set then a javascript file with that name in + * the module is used. + * + * @param string $name The name of the file - eg '/js/File.js' would have the name 'File' + * @param string $module The module to fall back to if the javascript file does not exist in the + * current theme. + * @param string $type Comma-separated list of types to use in the script tag + * (e.g. 'text/javascript,text/ecmascript') + */ + public function themedJavascript($name, $module = null, $type = null) { + $js = "/javascript/$name.js"; + + $opts = array( + 'type' => $type, + ); + + $project = project(); + $absbase = BASE_PATH . DIRECTORY_SEPARATOR; + $absproject = $absbase . $project; + + if(file_exists($absproject . $js)) { + return $this->javascript($project . $js, $opts); + } + + foreach(SSViewer::get_themes() as $theme) { + $path = TemplateLoader::instance()->getPath($theme); + $abspath = BASE_PATH . '/' . $path; + + if(file_exists($abspath . $js)) { + return $this->javascript($path . $js, $opts); + } + } + throw new \InvalidArgumentException( + "The javascript file doesn't exists. Please check if the file $name.js exists in any context or search for " + . "themedJavascript references calling this file in your templates." + ); } /**