diff --git a/.upgrade.yml b/.upgrade.yml index 3b9616a95fb..8e06b802c05 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 9bb80dda978..15d3f594f6a 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 +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 ``` -cd vendor/silverstripe/admin && 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 58f9a5b9d24..3965ad3789a 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 8490a9016cc..313d64aaec5 100644 --- a/src/Core/Convert.php +++ b/src/Core/Convert.php @@ -564,6 +564,7 @@ public static function upperCamelToLowerCamel($str) /** * 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 @@ public static function memstring2bytes($memString) // 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 6b0885132f5..6cf980cbff2 100644 --- a/src/Core/Injector/Injector.php +++ b/src/Core/Injector/Injector.php @@ -13,6 +13,7 @@ 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 @@ protected function instantiate($spec, $id = null, $type = null) $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 0585e3703b0..977d346dcd2 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 @@ public function performReadonlyTransformation() { $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 @@ public function setConfig(GridFieldConfig $config) 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 @@ public function gridFieldAlterAction($data, $form, HTTPRequest $request) } 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 802340d7896..e489643f800 100755 --- a/src/Forms/GridField/GridFieldFilterHeader.php +++ b/src/Forms/GridField/GridFieldFilterHeader.php @@ -6,6 +6,7 @@ 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 @@ public function getURLHandlers($gridField) } /** - * @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 @@ public function __construct( 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 @@ public function getActions($gridField) * 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 @@ public function handleAction(GridField $gridField, $actionName, $arguments, $dat } $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 @@ public function getManipulatedData(GridField $gridField, SS_List $dataList) $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 @@ public function getSearchForm(GridField $gridField) $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 627ef8b87d5..10dc772bc72 100644 --- a/src/Forms/GridField/GridField_FormAction.php +++ b/src/Forms/GridField/GridField_FormAction.php @@ -101,6 +101,7 @@ public function getAttributes() // 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 27a1527aa2c..6672971d831 100644 --- a/src/Forms/HTMLEditor/HTMLEditorConfig.php +++ b/src/Forms/HTMLEditor/HTMLEditorConfig.php @@ -83,6 +83,7 @@ public static function get($identifier = null) // 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 @@ public static function set_config($identifier, HTMLEditorConfig $config = null) { 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 f2f3a3b09dd..a037cb35565 100644 --- a/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php +++ b/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php @@ -130,11 +130,13 @@ public function generateContent(TinyMCEConfig $config) // Register vars for config $baseDirJS = Convert::raw2js(Director::absoluteBaseURL()); + $name = Convert::raw2js($this->checkName($config)); $buffer = []; $buffer[] = <<