daniel
Daniel Rotter
Core developer and support guru. Passionate traveler and soccer player.
@danrot90

Sulu 2.0.0-RC2 released

After releasing Sulu 2.0 RC1 we have continued to work on improving the product. We've added some smaller features that existed in the 1.x series and refactored others where we sadly could not avoid BC breaks compared to RC1.

RC2 also comes with updated dependencies and improved compatibility, e.g. we have updated all our JS dependencies to their latest version, made sure that we are compatible with MySQL 8 and updated the minimum version of PHP to 7.2, because the security support for PHP 7.1 is ending this december.

The most important new features include:

  • Locking of users
  • Disallow specific operations not applicable to the Homepage (moving, changing the URL, deleting, ...)
  • Hide UI elements if the currently logged in user has no permissions for them
  • An updated design for error messages
  • Allow filtering by type in media_selection field type

The changes concerning extension points important for developers will be explained in more detail in the following paragraphs.

Move tooling for admin build to assets folder

In this Pull Request we have made some adoptions that will allow users to move the package.json, webpack.config.js and other configurations files to the "assets/admin" folder. This will free up the root of the directory for your own package.json files, scripts and so on.

However, this means that you have to execute "npm install" and "npm run build" in the "assets/admin" folder instead of the root folder if you want to build the admin of Sulu. If you are updating an existing installation you should also move the files as done in this PR in sulu-minimal.

Refactor definition for administation routes and navigation items

Previously it was not possible to adjust existing routes and the items in the navigation created by another bundle. Due to our forms and lists, which allow quite some configuration, it would be preferrable to be able to add more configuration in your own application. For example, adding another button to an existing form, e.g. in the contact form, or adding another sub entry in the navigation, e.g. for the "Settings" navigation item. The latter one was already possible somehow, but felt a bit like a hack, and since we were refactoring the routes anyway, we decided that it should work the same for both cases.

So what we did is introducing two collection classes, the "RouteCollection" and the "NavigationItemCollection". These collection classes are passed to the "configureRoutes" and the "configureNavigationItems" functions, which are replacing the "getRoutes" and "getNavigationItems" functions. The biggest difference is that these functions do not return an array of routes or navigation items, but add them to the previously mentioned collections being passed as the first argument.

The collections also allow for retreival of already added routes or navigation items using their "get" functions, which means they can be edited as well. This way it is possible to e.g. add more navigation items to the settings section in the navigation. The RouteCollection actually holds instances of different RouteBuilder classes instead of the resulting Route.

// Before
public function getNavigation(): Navigation
{
    $rootNavigationItem = $this->getNavigationItemRoot();
    $settings = Admin::getNavigationItemSettings();

    $roles = new NavigationItem('sulu_tag.tags', $settings);
    $roles->setMainRoute(static::LIST_ROUTE);
    $rootNavigationItem->addChild($settings);

    return new Navigation($rootNavigationItem);
}

public function getRoutes(): array
{
    $routes = [];

    $routes[] = $this->routeBuilderFactory->createListRouteBuilder(static::LIST_ROUTE, '/tags')
        ->setResourceKey('tags')
        ->setListKey('tags')
        ->setTitle('sulu_tag.tags')
        ->addListAdapters(['table'])
        ->getRoute();
      
    return $routes;
}

// After
public function configureNavigationItems(NavigationItemCollection $navigationItemCollection): void
{
    $tags = new NavigationItem('sulu_tag.tags');
    $tags->setMainRoute(static::LIST_ROUTE);
    $navigationItemCollection->get(Admin::SETTINGS_NAVIGATION_ITEM)->addChild($tags);
}
    
public function configureRoutes(RouteCollection $routeCollection): void
{
    $routeCollection->add(
        $this->routeBuilderFactory->createListRouteBuilder(static::LIST_ROUTE, '/tags')
            ->setResourceKey('tags')
            ->setListKey('tags')
            ->setTitle('sulu_tag.tags')
            ->addListAdapters(['table'])
    );
}
Refactor definition for administation routes and navigation items

Easier retrieval of all webspace locales for routes

Also related to the above change is the one in this Pull Request. There were a number of forms, which should have offered their content in all localizations defined in a webspace. This involved a few lines of code, and was copied over multiple places. To avoid situations like this, we have defined another function in the "WebspaceManager", which makes it easier to retrieve the locales defined in all webspaces.

public function configureRoutes(RouteCollection $routeCollection): void
{
    // Before
    $snippetLocales = array_values(
        array_map(	
            function(Localization $localization) {	
                return $localization->getLocale();	
            },	
            $this->webspaceManager->getAllLocalizations()	
        )	
    );

    // After
    $snippetLocales = $this->webspaceManager->getAllLocales();

    $routeCollection->add(
        $this->routeBuilderFactory->createListRouteBuilder(static::LIST_ROUTE, '/snippets/:locale')
            ->setResourceKey('snippets')
            ->setListKey('snippets')
            ->setTitle('sulu_snippet.snippets')
            ->addListAdapters(['table'])
            ->addLocales($snippetLocales)
            ->setDefaultLocale($snippetLocales[0])
        )
}
Easier retrieval of all webspace locales for routes

Allow to configure ToolbarActions

ToolbarActions allow the user to define what buttons appear in the top toolbar of the Sulu Admin, and what happens when you click on them. These ToolbarActions are available for Forms and Lists, and they are registered using a plain string in JavaScript. This string can then be used to add them by using this string when the route will be defined.

Previously they were not very flexible, meaning that as soon as you want to have a different endpoint than in an existing ToolbarAction, you had to implement it completely on your own. Therefore we decided to make these ToolbarActions configurable as well. They can take options now, and these options can be used in the ToolbarAction itself, where they can be accessed by "this.options".

The following example shows how these options can be set. It is copied from our user form, which has a toggler that immediately sends a request to the server to lock or unlock the user. We used to have a separate "sulu_security.lock_user" ToolbarAction for that, just because we had to hardcode some parameters. Now we are using the options on the ToolbarAction instead.

$this->routeBuilderFactory
    ->createFormRouteBuilder('sulu_security.form.permissions', '/permissions')
    ->setResourceKey('users')
    ->setFormKey('user_details')
    ->setTabTitle('sulu_security.permissions')
    ->addToolbarActions([
        'sulu_admin.save',
        'sulu_security.enable_user',
        'sulu_admin.toggler' => [
            'label' => $this->translator->trans('sulu_security.user_locked', [], 'admin'),
            'property' => 'locked',
            'activate' => 'lock',
            'deactivate' => 'unlock',
        ],
    ])
    ->setIdQueryParameter('contactId')
    ->setTitleVisible(true)
    ->setTabOrder(3072)
    ->setParent(ContactAdmin::CONTACT_EDIT_FORM_ROUTE);
Allow to configure ToolbarActions

The "sulu_admin.save" and "sulu_security.enable_user" ToolbarActions don't take any options in this example. But the "sulu_admin.toggler" ToolbarAction has e.g. a property which will be shown as the label of the button. This allows us to reuse the toggler in different situations where a similar behavior is desired, and all that without writing a single line of JavaScript.

Extension Points for CKEditor

In Sulu 1.6 it was very popular to change the configuration of the CKEditor or add a few more plugins based on your needs. These features have again been introduced in this Pull Request. We have implemented a "ckeditorPluginRegistry" and a "ckeditorConfigRegistry". Both classes are singletons, and can be imported in JavaScript from "sulu-admin-bundle/containers" path.

In the "ckeditorPluginRegistry" class you can add any CKEditor5 Plugin you can find. It will then be added to every CKEditor5 instance being created. The "ckeditorConfigRegistry" class is also used by every CKEditor5 instance and takes a function that receives the current config, and returns an object that is shallow merged with the current configuration, and the end result is passed to the CKEditor5 configuration.

The below example shows how to do that with the Heading plugin, although that one is already integrated into our CKEditor5 by default.

import {ckeditorPluginRegistry, ckeditorConfigRegistry} from 'sulu-admin-bundle/containers';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

ckeditorPluginRegistry.add(Heading);
ckeditorConfigRegistry.add((config) => ({
    toolbar: [...config.toolbar, 'heading'],
}));
Extension Points for CKEditor