diff --git a/.upgrade.yml b/.upgrade.yml index 3b9616a95..8e06b802c 100644 --- a/.upgrade.yml +++ b/.upgrade.yml @@ -959,6 +959,9 @@ warnings: 'Object': message: 'Replaced with traits' 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': message: 'Replaced with a PSR-3 logger' url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#psr3-logging' diff --git a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md index 9bb80dda9..15d3f594f 100644 --- a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md +++ b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md @@ -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. 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 -``` -cd vendor/silverstripe/admin && yarn pattern-lib +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. + +[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 diff --git a/docs/en/04_Changelogs/4.3.0.md b/docs/en/04_Changelogs/4.3.0.md index 58f9a5b9d..3965ad378 100644 --- a/docs/en/04_Changelogs/4.3.0.md +++ b/docs/en/04_Changelogs/4.3.0.md @@ -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. - 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. + - `SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController` is now the default CMS history controller and `SilverStripe\CMS\Controllers\CMSPageHistoryController` has been deprecated. ## Upgrading {#upgrading} @@ -25,6 +26,12 @@ To enable the legacy search API on a `GridFieldFilterHeader`, you can either: * set the `useLegacyFilterHeader` property to `true`, * 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 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) diff --git a/src/Core/Convert.php b/src/Core/Convert.php index 8490a9016..313d64aae 100644 --- a/src/Core/Convert.php +++ b/src/Core/Convert.php @@ -564,6 +564,7 @@ class Convert /** * 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" * @return float @@ -573,7 +574,7 @@ class Convert // Remove non-unit characters from the size $unit = preg_replace('/[^bkmgtpezy]/i', '', $memString); // Remove non-numeric characters from the size - $size = preg_replace('/[^0-9\.]/', '', $memString); + $size = preg_replace('/[^0-9\.\-]/', '', $memString); if ($unit) { // Find the position of the unit in the ordered string which is the power diff --git a/src/Core/Injector/Injector.php b/src/Core/Injector/Injector.php index 6b0885132..6cf980cbf 100644 --- a/src/Core/Injector/Injector.php +++ b/src/Core/Injector/Injector.php @@ -13,6 +13,7 @@ use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Environment; use SilverStripe\Dev\Deprecation; +use SilverStripe\ORM\DataObject; /** * A simple injection manager that manages creating objects and injecting @@ -581,6 +582,14 @@ class Injector implements ContainerInterface $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(); $object = $factory->create($class, $constructorParams); diff --git a/src/Forms/GridField/GridField.php b/src/Forms/GridField/GridField.php index 0585e3703..977d346dc 100644 --- a/src/Forms/GridField/GridField.php +++ b/src/Forms/GridField/GridField.php @@ -113,11 +113,13 @@ class GridField extends FormField protected $readonlyComponents = [ GridField_ActionMenu::class, GridFieldConfig_RecordViewer::class, + GridFieldButtonRow::class, GridFieldDataColumns::class, GridFieldDetailForm::class, GridFieldLazyLoader::class, GridFieldPageCount::class, GridFieldPaginator::class, + GridFieldFilterHeader::class, GridFieldSortableHeader::class, GridFieldToolbarHeader::class, GridFieldViewButton::class, @@ -241,16 +243,22 @@ class GridField extends FormField { $copy = clone $this; $copy->setReadonly(true); + $copyConfig = $copy->getConfig(); // get the whitelist for allowable readonly components $allowedComponents = $this->getReadonlyComponents(); foreach ($this->getConfig()->getComponents() as $component) { // if a component doesn't exist, remove it from the readonly version. 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; } @@ -290,6 +298,18 @@ class GridField extends FormField return $this; } + /** + * @param bool $readonly + * + * @return $this + */ + public function setReadonly($readonly) + { + parent::setReadonly($readonly); + $this->getState()->Readonly = $readonly; + return $this; + } + /** * @return ArrayList */ @@ -1009,6 +1029,9 @@ class GridField extends FormField } if ($request->getHeader('X-Pjax') === 'CurrentField') { + if ($this->getState()->Readonly === true) { + $this->performDisabledTransformation(); + } return $this->FieldHolder(); } diff --git a/src/Forms/GridField/GridFieldFilterHeader.php b/src/Forms/GridField/GridFieldFilterHeader.php index 802340d78..e489643f8 100755 --- a/src/Forms/GridField/GridFieldFilterHeader.php +++ b/src/Forms/GridField/GridFieldFilterHeader.php @@ -6,6 +6,7 @@ use LogicException; use SilverStripe\Admin\LeftAndMain; use SilverStripe\Control\Controller; use SilverStripe\Control\HTTPResponse; +use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; use SilverStripe\Dev\Deprecation; use SilverStripe\Forms\FieldGroup; @@ -43,6 +44,16 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi */ 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 */ @@ -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 $updateSearchForm This will be removed in 5.0 */ @@ -85,7 +96,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi callable $updateSearchContext = null, callable $updateSearchForm = null ) { - $this->useLegacyFilterHeader = $useLegacy; + $this->useLegacyFilterHeader = Config::inst()->get(self::class, 'force_legacy') || $useLegacy; $this->updateSearchContextCallback = $updateSearchContext; $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 * * @param GridField $gridField - * @return array + * @return void */ public function handleAction(GridField $gridField, $actionName, $arguments, $data) { @@ -163,14 +174,13 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi } $state = $gridField->State->GridFieldFilterHeader; + $state->Columns = null; if ($actionName === 'filter') { if (isset($data['filter'][$gridField->getName()])) { foreach ($data['filter'][$gridField->getName()] as $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(); $dataListClone = clone($dataList); - foreach ($filterArguments as $columnName => $value) { - if ($dataList->canFilterBy($columnName) && $value) { - $dataListClone = $dataListClone->filter($columnName . ':PartialMatch', $value); - } - } - return $dataListClone; + $results = $this->getSearchContext($gridField) + ->getQuery($filterArguments, false, false, $dataListClone); + + return $results; } /** @@ -337,9 +345,11 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi $field->addExtraClass('stacked'); } + $name = $gridField->Title ?: singleton($gridField->getModelClass())->i18n_plural_name(); + $this->searchForm = $form = new Form( $gridField, - "SearchForm", + $name . "SearchForm", $searchFields, new FieldList() ); diff --git a/src/Forms/GridField/GridField_FormAction.php b/src/Forms/GridField/GridField_FormAction.php index 627ef8b87..10dc772bc 100644 --- a/src/Forms/GridField/GridField_FormAction.php +++ b/src/Forms/GridField/GridField_FormAction.php @@ -101,6 +101,7 @@ class GridField_FormAction extends FormAction // will strip it from the requests 'name' => 'action_gridFieldAlterAction' . '?' . http_build_query($actionData), 'data-url' => $this->gridField->Link(), + 'type' => "button", ) ); } diff --git a/src/Forms/HTMLEditor/HTMLEditorConfig.php b/src/Forms/HTMLEditor/HTMLEditorConfig.php index 27a1527aa..6672971d8 100644 --- a/src/Forms/HTMLEditor/HTMLEditorConfig.php +++ b/src/Forms/HTMLEditor/HTMLEditorConfig.php @@ -83,6 +83,7 @@ abstract class HTMLEditorConfig // Create new instance if unconfigured if (!isset(self::$configs[$identifier])) { self::$configs[$identifier] = static::create(); + self::$configs[$identifier]->setOption('editorIdentifier', $identifier); } return self::$configs[$identifier]; } @@ -98,6 +99,7 @@ abstract class HTMLEditorConfig { if ($config) { self::$configs[$identifier] = $config; + self::$configs[$identifier]->setOption('editorIdentifier', $identifier); } else { unset(self::$configs[$identifier]); } diff --git a/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php b/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php index f2f3a3b09..a037cb355 100644 --- a/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php +++ b/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php @@ -130,11 +130,13 @@ class TinyMCECombinedGenerator implements TinyMCEScriptGenerator, Flushable // Register vars for config $baseDirJS = Convert::raw2js(Director::absoluteBaseURL()); + $name = Convert::raw2js($this->checkName($config)); $buffer = []; $buffer[] = <<