Merge branch '4.3' into 4

This commit is contained in:
Robbie Averill 2018-11-06 11:05:22 +01:00
commit 64c2938c96
15 changed files with 171 additions and 20 deletions

View File

@ -959,6 +959,9 @@ warnings:
'Object': 'Object':
message: 'Replaced with traits' message: 'Replaced with traits'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#object-replace' url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#object-replace'
'SS_Object':
message: 'Replaced with traits'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#object-replace'
'SS_Log': 'SS_Log':
message: 'Replaced with a PSR-3 logger' message: 'Replaced with a PSR-3 logger'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#psr3-logging' url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#psr3-logging'

View File

@ -55,15 +55,35 @@ coding conventions.
A pattern library is a collection of user interface design elements, this helps developers and designers collaborate and to provide a quick preview of elements as they were intended without the need to build an entire interface to see it. A pattern library is a collection of user interface design elements, this helps developers and designers collaborate and to provide a quick preview of elements as they were intended without the need to build an entire interface to see it.
Components built in React and used by the CMS are actively being added to the pattern library. Components built in React and used by the CMS are actively being added to the pattern library.
The pattern library can be used to preview React components without including them in the SilverStripe CMS.
To access the pattern library, starting from your project root: ### Viewing the latest pattern library
``` The easiest way to access the pattern library is to view it online. The pattern library for the latest SilverStripe 4 development branch is automatically built and deployed. Note that this may include new components that are not yet available in a stable release.
cd vendor/silverstripe/admin && yarn pattern-lib
[Browse the SilverStripe pattern library online](https://silverstripe.github.io/silverstripe-admin).
### Running the pattern library
If you're developing a new React component, running the pattern library locally is a good way to interact with it.
The pattern library is built from the `silverstripe/admin` module, but it also requires `silverstripe/asset-admin`, `silversrtipe/cms` and `silverstripe/campaign-admin`.
To run the pattern library locally, you'll need a SilverStripe project based on `silverstripe/recipe-cms` and `yarn` installed locally. The pattern library requires the JS source files so you'll need to use the `--prefer-source` flag when installing your dependencies with Composer.
```bash
composer install --prefer-source
(cd vendor/silverstripe/asset-admin && yarn install)
(cd vendor/silverstripe/campaign-admin && yarn install)
(cd vendor/silverstripe/cms && yarn install)
cd vendor/silverstripe/admin && yarn install && yarn pattern-lib
``` ```
Then browse to `http://localhost:6006/` The pattern library will be available at [http://localhost:6006](http://localhost:6006). The JS source files will be watched, so every time you make a change to a JavaScript file, the pattern library will automatically update itself.
If you want to build a static version of the pattern library, you can replace `yarn pattern-lib` with `yarn build-storybook`. This will output the pattern library files to a `storybook-static` folder.
The SilverStripe pattern library is built using the [StoryBook JS library](https://storybook.js.org/). You can read the StoryBook documentation to learn about more advanced features and customisation options.
## The Admin URL ## The Admin URL

View File

@ -7,6 +7,7 @@
- Take care with `stageChildren()` overrides. `Hierarchy::numChildren() ` results will only make use of `stageChildren()` customisations that are applied to the base class and don't include record-specific behaviour. - Take care with `stageChildren()` overrides. `Hierarchy::numChildren() ` results will only make use of `stageChildren()` customisations that are applied to the base class and don't include record-specific behaviour.
- New React-based search UI for the CMS, Asset-Admin, GridFields and ModelAdmins. - New React-based search UI for the CMS, Asset-Admin, GridFields and ModelAdmins.
- A new `GridFieldLazyLoader` component can be added to `GridField`. This will delay the fetching of data until the user access the container Tab of the GridField. - A new `GridFieldLazyLoader` component can be added to `GridField`. This will delay the fetching of data until the user access the container Tab of the GridField.
- `SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController` is now the default CMS history controller and `SilverStripe\CMS\Controllers\CMSPageHistoryController` has been deprecated.
## Upgrading {#upgrading} ## Upgrading {#upgrading}
@ -25,6 +26,12 @@ To enable the legacy search API on a `GridFieldFilterHeader`, you can either:
* set the `useLegacyFilterHeader` property to `true`, * set the `useLegacyFilterHeader` property to `true`,
* or pass `true` to the first argument of its constructor. * or pass `true` to the first argument of its constructor.
To force the legacy search API on all instances of `GridFieldFilterHeader`, you can set it in your [configuration file](../../configuration):
```yml
SilverStripe\Forms\GridField\GridFieldFilterHeader:
force_legacy: true
```
```php ```php
public function getCMSFields() public function getCMSFields()
{ {
@ -41,3 +48,23 @@ public function getCMSFields()
} }
``` ```
### Keep using the legacy `CMSPageHistoryController`
To keep using the old CMS history controller for every page type, add the following entry to your YML config.
```yml
SilverStripe\Core\Injector\Injector:
SilverStripe\CMS\Controllers\CMSPageHistoryController:
class: SilverStripe\CMS\Controllers\CMSPageHistoryController
```
If you want to use both CMS history controllers in different contexts, you can implement your own _Factory_ class.
```yml
SilverStripe\Core\Injector\Injector:
SilverStripe\CMS\Controllers\CMSPageHistoryController:
factory:
App\MySite\MyCustomControllerFactory
```
[Implementing a _Factory_ with the Injector](/developer_guides/extending/injector/#factories)

View File

@ -564,6 +564,7 @@ class Convert
/** /**
* Turn a memory string, such as 512M into an actual number of bytes. * Turn a memory string, such as 512M into an actual number of bytes.
* Preserves integer values like "1024" or "-1"
* *
* @param string $memString A memory limit string, such as "64M" * @param string $memString A memory limit string, such as "64M"
* @return float * @return float
@ -573,7 +574,7 @@ class Convert
// Remove non-unit characters from the size // Remove non-unit characters from the size
$unit = preg_replace('/[^bkmgtpezy]/i', '', $memString); $unit = preg_replace('/[^bkmgtpezy]/i', '', $memString);
// Remove non-numeric characters from the size // Remove non-numeric characters from the size
$size = preg_replace('/[^0-9\.]/', '', $memString); $size = preg_replace('/[^0-9\.\-]/', '', $memString);
if ($unit) { if ($unit) {
// Find the position of the unit in the ordered string which is the power // Find the position of the unit in the ordered string which is the power

View File

@ -13,6 +13,7 @@ use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Environment; use SilverStripe\Core\Environment;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\DataObject;
/** /**
* A simple injection manager that manages creating objects and injecting * A simple injection manager that manages creating objects and injecting
@ -581,6 +582,14 @@ class Injector implements ContainerInterface
$constructorParams = $spec['constructor']; $constructorParams = $spec['constructor'];
} }
// If we're dealing with a DataObject singleton without specific constructor params, pass through Singleton
// flag as second argument
if ((!$type || $type !== self::PROTOTYPE)
&& empty($constructorParams)
&& is_subclass_of($class, DataObject::class)) {
$constructorParams = array(null, true);
}
$factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator(); $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
$object = $factory->create($class, $constructorParams); $object = $factory->create($class, $constructorParams);

View File

@ -113,11 +113,13 @@ class GridField extends FormField
protected $readonlyComponents = [ protected $readonlyComponents = [
GridField_ActionMenu::class, GridField_ActionMenu::class,
GridFieldConfig_RecordViewer::class, GridFieldConfig_RecordViewer::class,
GridFieldButtonRow::class,
GridFieldDataColumns::class, GridFieldDataColumns::class,
GridFieldDetailForm::class, GridFieldDetailForm::class,
GridFieldLazyLoader::class, GridFieldLazyLoader::class,
GridFieldPageCount::class, GridFieldPageCount::class,
GridFieldPaginator::class, GridFieldPaginator::class,
GridFieldFilterHeader::class,
GridFieldSortableHeader::class, GridFieldSortableHeader::class,
GridFieldToolbarHeader::class, GridFieldToolbarHeader::class,
GridFieldViewButton::class, GridFieldViewButton::class,
@ -241,16 +243,22 @@ class GridField extends FormField
{ {
$copy = clone $this; $copy = clone $this;
$copy->setReadonly(true); $copy->setReadonly(true);
$copyConfig = $copy->getConfig();
// get the whitelist for allowable readonly components // get the whitelist for allowable readonly components
$allowedComponents = $this->getReadonlyComponents(); $allowedComponents = $this->getReadonlyComponents();
foreach ($this->getConfig()->getComponents() as $component) { foreach ($this->getConfig()->getComponents() as $component) {
// if a component doesn't exist, remove it from the readonly version. // if a component doesn't exist, remove it from the readonly version.
if (!in_array(get_class($component), $allowedComponents)) { if (!in_array(get_class($component), $allowedComponents)) {
$copy->getConfig()->removeComponent($component); $copyConfig->removeComponent($component);
} }
} }
// As the edit button may have been removed, add a view button if it doesn't have one
if (!$copyConfig->getComponentByType(GridFieldViewButton::class)) {
$copyConfig->addComponent(new GridFieldViewButton);
}
return $copy; return $copy;
} }
@ -290,6 +298,18 @@ class GridField extends FormField
return $this; return $this;
} }
/**
* @param bool $readonly
*
* @return $this
*/
public function setReadonly($readonly)
{
parent::setReadonly($readonly);
$this->getState()->Readonly = $readonly;
return $this;
}
/** /**
* @return ArrayList * @return ArrayList
*/ */
@ -1009,6 +1029,9 @@ class GridField extends FormField
} }
if ($request->getHeader('X-Pjax') === 'CurrentField') { if ($request->getHeader('X-Pjax') === 'CurrentField') {
if ($this->getState()->Readonly === true) {
$this->performDisabledTransformation();
}
return $this->FieldHolder(); return $this->FieldHolder();
} }

View File

@ -6,6 +6,7 @@ use LogicException;
use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\FieldGroup; use SilverStripe\Forms\FieldGroup;
@ -43,6 +44,16 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
*/ */
public $useLegacyFilterHeader = false; public $useLegacyFilterHeader = false;
/**
* Forces all filter components to revert to displaying the legacy
* table header style rather than the react driven search box
*
* @deprecated 4.3.0:5.0.0 Will be removed in 5.0
* @config
* @var bool
*/
private static $force_legacy = false;
/** /**
* @var \SilverStripe\ORM\Search\SearchContext * @var \SilverStripe\ORM\Search\SearchContext
*/ */
@ -76,7 +87,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
} }
/** /**
* @param bool $useLegacy * @param bool $useLegacy This will be removed in 5.0
* @param callable|null $updateSearchContext This will be removed in 5.0 * @param callable|null $updateSearchContext This will be removed in 5.0
* @param callable|null $updateSearchForm This will be removed in 5.0 * @param callable|null $updateSearchForm This will be removed in 5.0
*/ */
@ -85,7 +96,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
callable $updateSearchContext = null, callable $updateSearchContext = null,
callable $updateSearchForm = null callable $updateSearchForm = null
) { ) {
$this->useLegacyFilterHeader = $useLegacy; $this->useLegacyFilterHeader = Config::inst()->get(self::class, 'force_legacy') || $useLegacy;
$this->updateSearchContextCallback = $updateSearchContext; $this->updateSearchContextCallback = $updateSearchContext;
$this->updateSearchFormCallback = $updateSearchForm; $this->updateSearchFormCallback = $updateSearchForm;
} }
@ -154,7 +165,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
* If the GridField has a filterable datalist, return an array of actions * If the GridField has a filterable datalist, return an array of actions
* *
* @param GridField $gridField * @param GridField $gridField
* @return array * @return void
*/ */
public function handleAction(GridField $gridField, $actionName, $arguments, $data) public function handleAction(GridField $gridField, $actionName, $arguments, $data)
{ {
@ -163,14 +174,13 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
} }
$state = $gridField->State->GridFieldFilterHeader; $state = $gridField->State->GridFieldFilterHeader;
$state->Columns = null;
if ($actionName === 'filter') { if ($actionName === 'filter') {
if (isset($data['filter'][$gridField->getName()])) { if (isset($data['filter'][$gridField->getName()])) {
foreach ($data['filter'][$gridField->getName()] as $key => $filter) { foreach ($data['filter'][$gridField->getName()] as $key => $filter) {
$state->Columns->$key = $filter; $state->Columns->$key = $filter;
} }
} }
} elseif ($actionName === 'reset') {
$state->Columns = null;
} }
} }
@ -193,12 +203,10 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
$filterArguments = $columns->toArray(); $filterArguments = $columns->toArray();
$dataListClone = clone($dataList); $dataListClone = clone($dataList);
foreach ($filterArguments as $columnName => $value) { $results = $this->getSearchContext($gridField)
if ($dataList->canFilterBy($columnName) && $value) { ->getQuery($filterArguments, false, false, $dataListClone);
$dataListClone = $dataListClone->filter($columnName . ':PartialMatch', $value);
} return $results;
}
return $dataListClone;
} }
/** /**
@ -337,9 +345,11 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
$field->addExtraClass('stacked'); $field->addExtraClass('stacked');
} }
$name = $gridField->Title ?: singleton($gridField->getModelClass())->i18n_plural_name();
$this->searchForm = $form = new Form( $this->searchForm = $form = new Form(
$gridField, $gridField,
"SearchForm", $name . "SearchForm",
$searchFields, $searchFields,
new FieldList() new FieldList()
); );

View File

@ -101,6 +101,7 @@ class GridField_FormAction extends FormAction
// will strip it from the requests // will strip it from the requests
'name' => 'action_gridFieldAlterAction' . '?' . http_build_query($actionData), 'name' => 'action_gridFieldAlterAction' . '?' . http_build_query($actionData),
'data-url' => $this->gridField->Link(), 'data-url' => $this->gridField->Link(),
'type' => "button",
) )
); );
} }

View File

@ -83,6 +83,7 @@ abstract class HTMLEditorConfig
// Create new instance if unconfigured // Create new instance if unconfigured
if (!isset(self::$configs[$identifier])) { if (!isset(self::$configs[$identifier])) {
self::$configs[$identifier] = static::create(); self::$configs[$identifier] = static::create();
self::$configs[$identifier]->setOption('editorIdentifier', $identifier);
} }
return self::$configs[$identifier]; return self::$configs[$identifier];
} }
@ -98,6 +99,7 @@ abstract class HTMLEditorConfig
{ {
if ($config) { if ($config) {
self::$configs[$identifier] = $config; self::$configs[$identifier] = $config;
self::$configs[$identifier]->setOption('editorIdentifier', $identifier);
} else { } else {
unset(self::$configs[$identifier]); unset(self::$configs[$identifier]);
} }

View File

@ -130,11 +130,13 @@ class TinyMCECombinedGenerator implements TinyMCEScriptGenerator, Flushable
// Register vars for config // Register vars for config
$baseDirJS = Convert::raw2js(Director::absoluteBaseURL()); $baseDirJS = Convert::raw2js(Director::absoluteBaseURL());
$name = Convert::raw2js($this->checkName($config));
$buffer = []; $buffer = [];
$buffer[] = <<<SCRIPT $buffer[] = <<<SCRIPT
(function() { (function() {
var baseTag = window.document.getElementsByTagName('base'); var baseTag = window.document.getElementsByTagName('base');
var baseURL = baseTag.length ? baseTag[0].baseURI : '$baseDirJS'; var baseURL = baseTag.length ? baseTag[0].baseURI : '$baseDirJS';
var editorIdentifier = '$name';
SCRIPT; SCRIPT;
$buffer[] = <<<SCRIPT $buffer[] = <<<SCRIPT
(function() { (function() {

View File

@ -94,7 +94,7 @@ class SingleLookupField extends SingleSelectField
return $label; return $label;
} }
return $value; return parent::Value();
} }
/** /**

View File

@ -576,6 +576,7 @@ XML
public function memString2BytesProvider() public function memString2BytesProvider()
{ {
return [ return [
['-1', (float)-1],
['2048', (float)(2 * 1024)], ['2048', (float)(2 * 1024)],
['2k', (float)(2 * 1024)], ['2k', (float)(2 * 1024)],
['512M', (float)(512 * 1024 * 1024)], ['512M', (float)(512 * 1024 * 1024)],

View File

@ -9,6 +9,13 @@ use SilverStripe\Forms\HTMLEditor\TinyMCEConfig;
class TinyMCEConfigTest extends SapphireTest class TinyMCEConfigTest extends SapphireTest
{ {
public function testEditorIdentifier()
{
$config = TinyMCEConfig::get('myconfig');
$this->assertEquals('myconfig', $config->getOption('editorIdentifier'));
}
/** /**
* Ensure that all TinyMCEConfig.tinymce_lang are valid * Ensure that all TinyMCEConfig.tinymce_lang are valid
*/ */

View File

@ -66,6 +66,51 @@ class DataObjectTest extends SapphireTest
); );
} }
/**
* @dataProvider provideSingletons
*/
public function testSingleton($inst, $defaultValue, $altDefaultValue)
{
$inst = $inst();
// Test that populateDefaults() isn't called on singletons
// which can lead to SQL errors during build, and endless loops
if ($defaultValue) {
$this->assertEquals($defaultValue, $inst->MyFieldWithDefault);
} else {
$this->assertEmpty($inst->MyFieldWithDefault);
}
if ($altDefaultValue) {
$this->assertEquals($altDefaultValue, $inst->MyFieldWithAltDefault);
} else {
$this->assertEmpty($inst->MyFieldWithAltDefault);
}
}
public function provideSingletons()
{
// because PHPUnit evalutes test providers *before* setUp methods
// any extensions added in the setUp methods won't be available
// we must return closures to generate the arguments at run time
return array(
'create() static method' => array(function () {
return DataObjectTest\Fixture::create();
}, 'Default Value', 'Default Value'),
'New object creation' => array(function () {
return new DataObjectTest\Fixture();
}, 'Default Value', 'Default Value'),
'singleton() function' => array(function () {
return singleton(DataObjectTest\Fixture::class);
}, null, null),
'singleton() static method' => array(function () {
return DataObjectTest\Fixture::singleton();
}, null, null),
'Manual constructor args' => array(function () {
return new DataObjectTest\Fixture(null, true);
}, null, null),
);
}
public function testDb() public function testDb()
{ {
$schema = DataObject::getSchema(); $schema = DataObject::getSchema();