Studio Developer Manual / Version 2307
Table Of Contents
In Section 9.1, “General Remarks On Customizing (Multiple) Studio Apps”, two ways of bootstrapping custom code were introduced, Studio plugins
and auto-loaded scripts. While auto-loaded scripts are a more light-weight
and easy to use approach, Studio plugins come with more utility and pre-fabrication
for customizing Ext JS components. In addition, many of CoreMedia's pre-defined
Studio customizations are only available as Studio plugin configurations.
The extension
modules in the
CoreMedia Blueprint workspace demonstrate the usage of the Studio plugin mechanism,
and define several plugins for Studio.
Caution
Note that a Studio plugin is not to be confused with an Ext JS component plugin. The former is an application-level construct; Studio plugins are designed to aggregate various extensions (custom UI elements and their functional code, together with the required UI elements to trigger the respective functionality). The latter means a per-component plugin and is purely an Ext JS mechanism. This section deals with Studio plugins; Ext JS plugins are described in Section 5.1.2, “Component Plugins”. In this manual, the terms Studio plugin and component plugin are used, respectively, to avoid ambiguity.
Examples for CoreMedia Studio extension points that plugins may hook into are:
Localization of content types and properties
Custom forms for content types
Custom collection thumbnail view, and custom columns in collection list view
Custom tab types (example in Blueprint: Taxonomy Manager tab)
Custom library search filters
Allowed image types and respective blob properties for drag and drop into rich text fields
Additional extensions to extension menu
Document types without a valid preview
A plugin for CoreMedia Studio usually has the following structure:
The example above depicts the layout of a typical Studio module in the
CoreMedia Blueprint workspace.
All plugins contain a package.json
file that defines the dependencies of the plugin. The actual source code
goes into the subdirectories src
and sencha
. The former contains TypeScript code,
the latter Sass files in the sass
subfolder and additional static resources such as images or CSS files
in the resources
subfolder not shown in the example. The jangaroo.config.js
file registers the
Studio plugin, see further below.
For example, the module es-studio
holds a resource bundle ElasticSocialStudioPlugin_properties
and
the mein plugin file ElasticSocialStudioPlugin.ts
(declaring the plugin and its applicable rules and configuration) under
src
. In addition, further Typescript source code files are held
in several sub-folders under src
.
Each plugin is described in a TypeScript file like ElasticSocialStudioPlugin.ts
. This file declares
the plugin's rule definitions (that is the various Studio extension points that this
plugin hooks into) and configuration options. For many defining these rules and configuration TypeScript file is sufficient for a plugin declaration.
However, you can of course also run arbitrary further Typescript code as part of your plugin's initialization.
The Main Class
The main class of a plugin shall be defined as TypeScript code.
In the example in Figure 9.2, “Plugin structure” the main class is
ElasticSocialStudioPlugin
. For your own
plugins, it is recommended to use a name schema like <your plugin
name>StudioPlugin.
The main class for a plugin must implement the interface EditorPlugin
.
The interface defines only one init()
method that receives a context object implementing
IEditorContext
as its only parameter, which is supposed to be used to configure
CoreMedia Studio.
You can simply implement the interface in your source code.
However, Studio also provides a base TypeScript
class to inherit from, namely StudioPlugin
which not only implements the
EditorPlugin
interface, it also delegates the init()
call to all Studio plugins
specified in its configurations
config option.
The IEditorContext
instance handed in to the init()
method
can be used for the following purposes:
Configure which content types can be instantiated by the CoreMedia Studio user. This basically restricts the list of content types offered after clicking on the Create Document Icon in the Collection View (see Section 9.5.6, “Excluding Document Types from the Library” for details). Note that only those content items are offered in the create content menu that the current user has the appropriate rights for in the selected folder - excluded content types will be placed on top of that rule (that is, you can exclude content type X from the menu even when the user has technically the rights to create content items of type X).
Configure image properties for display in the thumbnail view and for drag and drop
Register hooks that fill certain properties after initial content creation (see Section 9.5.7, “Client-side initialization of new content items” for details)
Add properties to the localization property bundles, or override existing properties (see Section 9.4, “Localizing Labels” for details)
Get access to the central bean factory and the application context bean
Get access to the REST session and indirectly to the associated repositories
Register content types for which Studio should not attempt to render an embedded preview
Register a transformer function to post-process the preview URL generated for an existing content item for use in the embedded preview
Get access to persistent per-user application settings, such as the tabs opened by the user or custom search folders
Register symbol mappings for pasting external text from the system clipboard into a RichText property field, which can be useful when you have to paste content from Microsoft Word with special non-standard characters
Note that a Studio plugin's init()
method is allowed to perform asynchronous calls,
which is essential if it needs server-side information (access user, groups, Content, and so on)
during initialization. CoreMedia Studio waits for the plugin to
handle all callbacks, only then the next plugin (if any) is initialized and eventually,
CoreMedia Studio is started. However, you cannot use
setTimeout()
or setInterval()
in Studio plugin
initialization code!
Plugin Rules
The other essential part of a CoreMedia Studio plugin is the plugin
rules it declares in its rules: []
element. Plugin rules are applied to
components whenever they are created, which allows you to modify behavior of standard
CoreMedia Studio components with component plugins. The
ElasticSocialStudioPlugin
plugin, for example, declares rules that add content forms
for elastic social.
The studio plugin file consists of one "rules" element that contains component elements. The components can be either identified by their global id or by namespace and xtype. For the latter case, you need to declare the required namespace(s) in the root tag of the plugin file. You can read a Studio plugin rule like this: "Whenever a component of the given xtype is built, add the following component plugin(s)."
You can use predefined Ext JS component plugins to modify framework components.
The ElasticSocialStudioPlugin
plugin, for example, uses the
AddItemsPlugin
to add content forms to the CommentExtensionTabPanel
.
In the ElasticSocialStudioPlugin
, custom forms for the elastic social
content types are added by using the AddTabbedDocumentFormsPlugin
(which is a
component plugin).
Caution
While in simple cases, the items to add can be specified directly inline in the Studio plugin TypeScript file, this is generally not recommended.
The reason is that the Studio plugin class is instantiated as a singleton, and all TypeScript objects that are not components or plugins, most prominently Actions, are instantiated immediately, too. This means that Actions are instantiated (too) early, and that a plugin rule may be applied several times with the same Action instance, leading to unexpected results.
The best practice is to move the whole component plugin to a separate TypeScript file and reference this new plugin subclass from the Studio plugin rule. Since the new plugin is referenced by its ptype, a new plugin instance and thus a new Action instance is created for each application of the plugin rule as expected.
The Ext JS plugins of any component are executed in a defined order:
Plugins provided directly in the component definition are initialized
Plugins defined in Studio plugin rules, starting with the plugins for the most generic applicable
xtype
, then those with successively more specificxtypes
.Plugins configured for the component's ID
If that specification does not unambiguously decide the order of two plugins, plugins registered earlier are executed earlier. To make sure that a certain module's Studio plugins are registered after another module's Studio plugin, the former module must declare a package.json dependency on the latter module. This way, the Studio plugins run and register in a defined order.
For your own Studio plugin, you might want to use the file from the CoreMedia
Project workspace as a starting point. The name of the Studio plugin file should
reflect the functionality of the plugin, for example
<My-plugin-Name>StudioPlugin.ts
for better readability.
The following example shows how a button can be added to the actions toolbar on the right side of the work area:
import Config from "@jangaroo/runtime/Config"; import ConfigUtils from "@jangaroo/runtime/ConfigUtils"; import StudioPlugin from "@coremedia/studio-client.main.editor-components/configuration/StudioPlugin"; import ActionsToolbar from "@coremedia/studio-client.main.editor-components/sdk/desktop/ActionsToolbar"; import AddActionsToolbarItemsPlugin from "./AddActionsToolbarItemsPlugin"; class AddButtonToActionsToolbarPlugin extends StudioPlugin { constructor(config: Config<AddButtonToActionsToolbarPlugin>) { super(ConfigUtils.apply(Config(AddButtonToActionsToolbarPlugin, { //... rules: [ Config(ActionsToolbar, { plugins: [ Config(AddActionsToolbarItemsPlugin, {}), ], }), ], }), config)); //... } } export default AddButtonToActionsToolbarPlugin;
Example 9.6. Adding a plugin rule to customize the actions toolbar
Because it is embedded in the element ActionsToolbar
in the above
declaration, your custom plugin AddActionsToolbarItemsPlugin
will be
added to all instances of the ActionsToolbar
class.
Your custom plugin is defined in a separate TypeScript file AddActionsToolbarItemsPlugin.ts
that configures an addItemsPlugin
to add a separator and a button with a
custom action to the ActionsToolbar
at index 5:
import Config from "@jangaroo/runtime/Config"; import AddItemsPlugin from "@coremedia/studio-client.ext.ui-components/plugins/AddItemsPlugin"; import Separator from "@jangaroo/ext-ts/toolbar/Separator"; import Button from "@jangaroo/ext-ts/button/Button"; import MyAction from "./MyAction"; //... Config(AddItemsPlugin, { index: 5, items: [ Config(Separator), Config(Button, { baseAction: new MyAction({text: "hello"}) }) ] })
Example 9.7. Adding a separator and a button with a custom action to a toolbar
While you can insert a component at a fixed position as shown above, it might also make sense to
add the component after or before another component with a certain (global) ID, itemId
, or
xtype
. To that end, the AddItemsPlugin
allows you to specify pattern objects so that
new items are added before or after the represented objects. If the component you want to use as
an "anchor component" is not a direct child of the component you plug into, you can set the
recursive
attribute in your rules declaration to true
.
When the component you want to modify is located inside a container that is also
a public API extension point, you might have to access that container's API to provide
context for your customizations.
A typical use case for this is that you want to add a button to a toolbar that is nested below a
container, but you need to apply your plugin rule to the container (and not the toolbar),
because you need to access some API of that Container to configure the items to add
(for example, access to the current selection managed by that container), or
because the toolbar is reused by other containers, and you want your button to only appear in
one specific context. Some Studio components define public API
interfaces for accessing the runtime component instance, for example
CollectionView
creates a component that is documented to implement
the public API interface ICollectionView
.
To express such nested extension point plugin rules, there is the plugin
NestedRulesPlugin
. Its usage is similar to CoreMedia Studio plugin rules,
namely is must contain an element rules
that again contains nested plugin
rules. A nested plugin rule consists of the element of the subcomponent to locate with an
optional itemId
, which in turn contains a plugins
element with
the plugins to add to that component. Typical plugins to use here are
AddItemsPlugin
, RemoveItemsPlugin
, and ReplaceItemsPlugin
,
all located in namespace exml:com.coremedia.ui.config
.
For example, assume that to every LinkList property field, you want to add a custom toolbar action that needs
access to the current selection of items in the LinkList given via
LinkListPropertyField#getSelectedValuesExpression()
of type ValueExpression
.
Like in the example above, you have to add a custom plugin to a CoreMedia Studio extension point in
your CoreMedia Studio plugin TypeScript file:
import Config from "@jangaroo/runtime/Config"; import ConfigUtils from "@jangaroo/runtime/ConfigUtils"; import StudioPlugin from "@coremedia/studio-client.main.editor-components/configuration/StudioPlugin"; import LinkListPropertyField from "@coremedia/studio-client.main.editor-components/sdk/premular/fields/LinkListPropertyField"; import CustomizeLinkListPropertyFieldPlugin from "./CustomizeLinkListPropertyFieldPlugin"; class MyPlugin extends StudioPlugin { constructor(config:Config<MyPlugin>){ super(ConfigUtils.apply(Config(MyPlugin, { //... rules: [ Config(LinkListPropertyField, { plugins: [ Config(CustomizeLinkListPropertyFieldPlugin), ], }), ], }), config)); //... } } export default MyPlugin;
Example 9.8. Adding a plugin rule to customize all LinkList property field toolbars
Now, in your plugin CustomizeLinkListPropertyFieldPlugin.ts
, instead of using
AddItemsPlugin
directly, you apply NestedRulesPlugin
to locate the toolbar you want to customize.
Still, the component you plug into is a LinkList property field, and when your custom plugin
is instantiated, that component is instantiated, too, and handed in as the config option
config.cmp
.
It is good practice to assign the LinkList property field component as well as its initial
configuration (when needed) to typed local TypeScript variables to avoid repeating longish expressions
and type casts in inline code.
import Config from "@jangaroo/runtime/Config"; import ConfigUtils from "@jangaroo/runtime/ConfigUtils"; import Component from "@jangaroo/ext-ts/Component"; import Separator from "@jangaroo/ext-ts/toolbar/Separator"; import AddItemsPlugin from "@coremedia/studio-client.ext.ui-components/plugins/AddItemsPlugin"; import IconButton from "@coremedia/studio-client.ext.ui-components/components/IconButton"; import NestedRulesPlugin from "@coremedia/studio-client.ext.ui-components/plugins/NestedRulesPlugin"; import LinkListPropertyField from "@coremedia/studio-client.main.editor-components/sdk/premular/fields/LinkListPropertyField"; import LinkListPropertyFieldToolbar from "@coremedia/studio-client.main.editor-components/sdk/premular/fields/LinkListPropertyFieldToolbar"; import MyAction from "./MyAction"; class CustomizeLinkListPropertyFieldPlugin extends NestedRulesPlugin { static override readonly xtype: string = "com.coremedia.blueprint.studio.template.config.CustomizeLinkListPropertyFieldPlugin"; constructor(config: Config<NestedRulesPlugin>) { const linkListPropField = as(config.cmp, LinkListPropertyField); super(ConfigUtils.apply(Config(CustomizeLinkListPropertyFieldPlugin, { ...ConfigUtils.append({ rules: [ Config(LinkListPropertyFieldToolbar, { plugins: [ Config(AddItemsPlugin, { items: [ Config(Separator), Config(IconButton, { baseAction: new MyAction({ contentValueExpression: linkListPropField.getSelectedValuesExpression(), }), contentValueExpression: linkListPropField.getSelectedValuesExpression(), forceReadOnlyValueExpression: linkListPropField.forceReadOnlyValueExpression, }), ], before: Config(Component, { itemId: LinkListPropertyFieldToolbar.LINK_LIST_SEP_FIRST_ITEM_ID, }), }), ], }) ], }) }), config)); } } export default CustomizeLinkListPropertyFieldPlugin;
Example 9.9. Using NestedRulesPlugin to customize a subcomponent using its container's API
Note how the above code makes use of the TypeScript element
LinkListPropertyFieldToolbar
to locate the toolbar inside the
LinkListPropertyField
, as well as to use an ..._ITEM_ID
constant from that config class to specify the new items' location.
As another example, assume you want to create your own component inheriting from
LinkListPropertyField
. You want to reuse the default toolbar that the standard
link list component defines, but you want to add one additional button to that toolbar.
In a very similar fashion to the example above concerning CoreMedia Studio plugins, you can
then write your custom component's TypeScript file like this:
// ----------------- // TODO: find another example: LinkListPropertyField already got a config additionalToolbarItems, this clashes! // ----------------- import Config from "@jangaroo/runtime/Config"; import ConfigUtils from "@jangaroo/runtime/ConfigUtils"; import Component from "@jangaroo/ext-ts/Component"; import AddItemsPlugin from "@coremedia/studio-client.ext.ui-components/plugins/AddItemsPlugin"; import NestedRulesPlugin from "@coremedia/studio-client.ext.ui-components/plugins/NestedRulesPlugin"; import LinkListPropertyField from "@coremedia/studio-client.main.editor-components/sdk/premular/fields/LinkListPropertyField"; import LinkListPropertyFieldToolbar from "@coremedia/studio-client.main.editor-components/sdk/premular/fields/LinkListPropertyFieldToolbar"; interface UsingNestedRulesPluginConfig extends Config<LinkListPropertyField> { additionalToolbarItems?: Component; } class UsingNestedRulesPlugin extends LinkListPropertyField { static override readonly xtype: string = "com.coremedia.blueprint.studio.template.config.UsingNestedRulesPlugin"; declare Config: UsingNestedRulesPluginConfig; constructor(config: Config<UsingNestedRulesPlugin>) { super(ConfigUtils.apply(Config(UsingNestedRulesPlugin, { ...ConfigUtils.append({ plugins: [ Config(NestedRulesPlugin, { rules: [ Config(LinkListPropertyFieldToolbar, { plugins: [ Config(AddItemsPlugin, { items: config.additionalToolbarItems }), ], }), ], }), ], }) }), config)); } } export default UsingNestedRulesPlugin;
Example 9.10. Using NestedRulesPlugin to customize a subcomponent
Note that when you inherit from a component and
use the plugins
element to declare the plugins you want to apply to
this component, you overwrite the plugins
definition of the component you inherit
from. That means that all the plugins that the super component defines would not be used in
your custom component. To avoid that, you have to wrap your additional plugins
definition
into a ...ConfigUtils.append()
or ...ConfigUtils.prepend()
call. This will
then add your custom plugin definitions to the end of the super component's declarations, or
insert them at the beginning, respectively.
You might also want to remove certain components from their containers. In that case, you can
add the RemoveItemsPlugin
to the container component and remove items, again
identifying them by pattern objects that can specify id, item id, or xtype.
In order the replace an existing component, you can use the ReplaceItemsPlugin
. For
this plugin, you specify one or more replacement components in the items
property.
Each item must specify an id or an item id and replaces the existing component with exactly
that id or item id.
Finally, a custom CoreMedia Studio plugin needs to be registered with
the Studio application. This is done in the jangaroo.config.js
file in the
module root folder. The purpose of this file is to add the fully qualified main plugin class to the list of
Studio plugins as shown in the following example:
module.exports = jangarooConfig({ type: "code", ... sencha: { studioPlugins: [ { mainClass: "com.acme.AcmeStudioPlugin", name: "Ac me!", }, ], }, ... });
Example 9.11. Registering a plugin
The object created in the jangaroo.config.js
file
may use the attributes defined by the class EditorPluginDescriptor
,
especially name
and mainClass
as shown above.
In addition, the attributes requiredGroup
and requiredLicenseFeature
may be used.
You can also implement group specific and own conditions using the OnlyIf
plugin.
To recapitulate, this is a brief overview of the configuration chain:
NPM dependencies introduce Studio plugin modules to CoreMedia Studio.
Studio plugin modules register Studio plugins in the
jangaroo.config.js
file.Studio plugin rules definitions denote components by ID or xtype and add Ext JS plugins to those components.
The Ext JS plugins shown here change the list of items of the components. Any other Ext JS plugins can be used in the same way.
Load external resources
If you want to load external style sheets or JavaScript files into
Studio, you have to place them below the folder src/main/sencha/resources
in your module and add the file paths to the sencha
entry of your module's jangaroo.config.js file with the
configuration options css
and js
as follows:
/** @type { import('@jangaroo/core').IJangarooConfig } */ module.exports = { type: "code", sencha: { css: [ { path: "resources/path/to/myStylesheet1.css", }, { path: "resources/path/to/myStylesheet2.css", }, ], js: [ { path: "resources/path/to/myJavascript1.js", }, { path: "resources/path/to/myJavascript2.js", }, ], }, };
Example 9.12. Loading external resources