diff --git a/admin/javascript/LeftAndMain.EditForm.js b/admin/javascript/LeftAndMain.EditForm.js index e440801d8..dbdb84139 100644 --- a/admin/javascript/LeftAndMain.EditForm.js +++ b/admin/javascript/LeftAndMain.EditForm.js @@ -193,6 +193,17 @@ } }); + /** + * Hide tabs when only one is available + */ + $('.cms-edit-form .ss-tabset').entwine({ + onmatch: function() { + var tabs = this.find("ul:first").children('li'); + if(tabs.length == 1) this.find('ul:first').hide(); + this._super(); + } + }); + }); }(jQuery)); \ No newline at end of file diff --git a/docs/en/changelogs/3.0.0.md b/docs/en/changelogs/3.0.0.md index f1d949f5d..9c248d013 100644 --- a/docs/en/changelogs/3.0.0.md +++ b/docs/en/changelogs/3.0.0.md @@ -326,7 +326,7 @@ The page tree moved from a bespoke tree library to [JSTree](http://jstree.com), which required changes to markup of the tree and its JavaScript architecture. This includes changes to `TreeDropdownField` and `TreeMultiSelectField`. -### Settings-related fields move from `SiteTree->getCMSFields()` to new `SiteTree->getSettingsFields()` [getcmsfields]### +### Settings-related fields move from SiteTree->getCMSFields() to new SiteTree->getSettingsFields() [getcmsfields]### The fields and tabs are now split into two separate forms, which required a structural change to the underlying class logic. In case you have added or removed fields @@ -338,6 +338,41 @@ We've also removed the `$params` attribute on `DataObject->getCMSFields()` which could be used as a shortcut for customizations to `FormScaffolder`, in order to achieve E_STRICT compliance. Please use `FormScaffolder` directly. +### Changed tab paths in SiteTree->getCMSFields() {#tab-paths} + +In order to simplify the interface, the `SiteTree->getCMSFields` +method now only has one rather than two levels of tabs. +This changes the tab paths, affecting any fields you might have added. +We have also moved all fields from the "Metadata" tab into the "Main Content" tab. + + :::php + // 2.4 + $fields->addFieldToTab('Root.Content.Main', $myField); + $fields->addFieldToTab('Root.Content.Metadata', $myOtherField); + // 3.0 + $fields->addFieldToTab('Root.Main', $myField); + $fields->addFieldToTab('Root.Main', $myOtherField); + +![Tab paths in 2.4](_images/tab-paths-before.png) +![Tab paths in 3.0](_images/tab-paths-after.png) + +The old paths are rewritten automatically, but will be deprecated in the next point release. +If you are working with tab objects directly in your `FieldSet`, you'll need to update +the tab names manually: + + :::php + // 2.4 + $fields->fieldByName('Root')->fieldByName('Content')->fieldByName('Main')->push($myField); + // 3.0 + $fields->fieldByName('Root')->fieldByName('Main')->push($myField); + +If only a single tab is found in any CMS tabset, it is hidden by default +to reduce UI clutter. You still need to address it through the usual tabset methods, +as the underlying object structure doesn't change. Once you add more tabs, +e.g. to the "Root.Main" tab in `SiteTree`, the tab bar automatically shows. + +![Tab paths in 3.0 with a custom tab](_images/tab-paths-customtab.png) + ### New `SiteTree::$description` field to describe purpose of a page type [sitetree-description]### Please use this static property to describe the purpose of your page types, diff --git a/docs/en/changelogs/_images/tab-paths-after.png b/docs/en/changelogs/_images/tab-paths-after.png new file mode 100644 index 000000000..3ee2ad39a Binary files /dev/null and b/docs/en/changelogs/_images/tab-paths-after.png differ diff --git a/docs/en/changelogs/_images/tab-paths-before.png b/docs/en/changelogs/_images/tab-paths-before.png new file mode 100644 index 000000000..c227aaf5a Binary files /dev/null and b/docs/en/changelogs/_images/tab-paths-before.png differ diff --git a/docs/en/changelogs/_images/tab-paths-customtab.png b/docs/en/changelogs/_images/tab-paths-customtab.png new file mode 100644 index 000000000..6673c233e Binary files /dev/null and b/docs/en/changelogs/_images/tab-paths-customtab.png differ diff --git a/forms/FieldList.php b/forms/FieldList.php index b08fe0186..7261585e4 100644 --- a/forms/FieldList.php +++ b/forms/FieldList.php @@ -25,6 +25,12 @@ class FieldList extends ArrayList { * @todo Documentation */ protected $containerField; + + /** + * @var array Ordered list of regular expressions, + * see {@link setTabPathRewrites()}. + */ + protected $tabPathRewrites = array(); public function __construct($items = array()) { if (!is_array($items) || func_num_args() > 1) { @@ -255,6 +261,9 @@ class FieldList extends ArrayList { * @return Tab The found or newly created Tab instance */ public function findOrMakeTab($tabName, $title = null) { + // Backwards compatibility measure: Allow rewriting of outdated tab paths + $tabName = $this->rewriteTabPath($tabName); + $parts = explode('.',$tabName); $last_idx = count($parts) - 1; // We could have made this recursive, but I've chosen to keep all the logic code within FieldList rather than add it to TabSet and Tab too. @@ -291,6 +300,7 @@ class FieldList extends ArrayList { * @todo Implement similiarly to dataFieldByName() to support nested sets - or merge with dataFields() */ public function fieldByName($name) { + $name = $this->rewriteTabPath($name); if(strpos($name,'.') !== false) list($name, $remainder) = explode('.',$name,2); else $remainder = null; @@ -555,6 +565,59 @@ class FieldList extends ArrayList { return false; } + /** + * Ordered list of regular expressions + * matching a tab path, to their rewrite rule (through preg_replace()). + * Mainly used for backwards compatibility. + * Ensure that more specific rules are placed earlier in the list, + * and that tabs with children are accounted for in the rule sets. + * + * Example: + * $fields->setTabPathRewriting(array( + * // Rewrite specific innermost tab + * '/^Root\.Content\.Main$/' => 'Root.Content', + * // Rewrite all innermost tabs + * '/^Root\.Content\.([^.]+)$/' => 'Root.\\1', + * )); + * + * @param array $rewrites + */ + public function setTabPathRewrites($rewrites) { + $this->tabPathRewrites = $rewrites; + } + + /** + * @return array + */ + public function getTabPathRewrites() { + return $this->tabPathRewrites; + } + + /** + * Support function for backwards compatibility purposes. + * Caution: Volatile API, might be removed in 3.1 or later. + * + * @param String $tabname Path to a tab, e.g. "Root.Content.Main" + * @return String Rewritten path, based on {@link tabPathRewrites} + */ + protected function rewriteTabPath($name) { + $isRunningTest = (class_exists('SapphireTest', false) && SapphireTest::is_running_test()); + foreach($this->getTabPathRewrites() as $regex => $replace) { + if(preg_match($regex, $name)) { + $newName = preg_replace($regex, $replace, $name); + Deprecation::notice('3.0.0', sprintf( + 'Using outdated tab path "%s", please use the new location "%s" instead', + $name, + $newName + )); + return $newName; + } + } + + // No match found, return original path + return $name; + } + /** * Default template rendering of a FieldList will concatenate all FieldHolder values. */ diff --git a/tests/forms/FieldListTest.php b/tests/forms/FieldListTest.php index c236b7052..26b5a62d5 100644 --- a/tests/forms/FieldListTest.php +++ b/tests/forms/FieldListTest.php @@ -789,5 +789,66 @@ class FieldListTest extends SapphireTest { $this->assertNotNull($visible->dataFieldByName('D2')); } + function testRewriteTabPath() { + $fields = new FieldList( + new Tabset("Root", + $tab1Level1 = new Tab("Tab1Level1", + $tab1Level2 = new Tab("Tab1Level2"), + $tab2Level2 = new Tab("Tab2Level2") + ), + $tab2Level1 = new Tab("Tab2Level1") + ) + ); + $fields->setTabPathRewrites(array( + '/Root\.Tab1Level1\.([^.]+)$/' => 'Root.Tab1Level1Renamed.\\1', + '/Root\.Tab1Level1$/' => 'Root.Tab1Level1Renamed', + )); + $method = new ReflectionMethod($fields, 'rewriteTabPath'); + $method->setAccessible(true); + $this->assertEquals( + 'Root.Tab1Level1Renamed', + $method->invoke($fields, 'Root.Tab1Level1Renamed'), + "Doesn't rewrite new name" + ); + $this->assertEquals( + 'Root.Tab1Level1Renamed', + $method->invoke($fields, 'Root.Tab1Level1'), + 'Direct aliasing on toplevel' + ); + $this->assertEquals( + 'Root.Tab1Level1Renamed.Tab1Level2', + $method->invoke($fields, 'Root.Tab1Level1.Tab1Level2'), + 'Indirect aliasing on toplevel' + ); + } + + function testRewriteTabPathFindOrMakeTab() { + $fields = new FieldList( + new Tabset("Root", + $tab1Level1 = new Tab("Tab1Level1Renamed", + $tab1Level2 = new Tab("Tab1Level2"), + $tab2Level2 = new Tab("Tab2Level2") + ), + $tab2Level1 = new Tab("Tab2Level1") + ) + ); + $fields->setTabPathRewrites(array( + '/Root\.Tab1Level1\.([^.]+)$/' => 'Root.Tab1Level1Renamed.\\1', + '/Root\.Tab1Level1$/' => 'Root.Tab1Level1Renamed', + )); + + $this->assertEquals($tab1Level1, $fields->findOrMakeTab('Root.Tab1Level1'), + 'findOrMakeTab() with toplevel tab under old name' + ); + $this->assertEquals($tab1Level1, $fields->findOrMakeTab('Root.Tab1Level1Renamed'), + 'findOrMakeTab() with toplevel tab under new name' + ); + $this->assertEquals($tab1Level2, $fields->findOrMakeTab('Root.Tab1Level1.Tab1Level2'), + 'findOrMakeTab() with nested tab under old parent tab name' + ); + $this->assertEquals($tab1Level2, $fields->findOrMakeTab('Root.Tab1Level1Renamed.Tab1Level2'), + 'findOrMakeTab() with nested tab under new parent tab name' + ); + } }