You can extend Content and Media Manager by integrating actions and pages from your own modules into Content and Media Manager. This allows you to make Content and Media Manager better fit your needs and provide features specific to your organization.
An action describes a function that can be called by an end-user. An action is rendered by a button, icon, menu entry, or anything that a user can click, depending on the context where it is rendered. All actions are added in a central registry.
Applications display actions in predefined areas called targets. For example, an application can define a topBarActions and a contextualMenuActions target. The application looks in the registry for all actions to display in a specific target. The same actions can be available in different targets.
The application can pass a context object to an action. The context should have the same shape everywhere in the application where the action is used. In places which are contextual to a node, the context specifies the path to the node in the path property.
The context can be enhanced by the actions when being initialized.
Each action is described by a simple JS object that can define the following properties:
onClick
init
target
contextualMenu:4
.buttonLabel
buttonLabelParams
buttonIcon
enabled
All properties of the action are merged with the context and passed to the init()
and onClick()
functions.
You register actions in the registry by calling the actionsRegistry.add function with a unique key as the first parameter, followed by the action definition. An action can be found by its key by using actionsRegistry.get(key)
. Note that the action key is added in the action definition.
actionsRegistry.getAll()
returns all registered actions.
If the action already exists with the same key, the new action automatically extends and replaces the existing one. This allows splitting the declaration of actions in multiple places (first defining the onClick
, then defining a target and a label, or changing the label or icon of an existing action).
When properties cannot be statically initialized, you can use the init()
function to dynamically resolve values based on the context. For example, the enabled value is not directly provided by an action, but rather set by the init()
function based on the context. The buttonLabelParams
can also be dynamic, based on the context. Even the onClick
function can be set based on the context. Only the key and target values cannot be set by init()
function.
If the init()
function needs to perform asynchronous calls to set a value in the context, you can set an observable in the context instead of the final value. The context passed in onClick
uses the last value received by the observable. For example, context.enabled can be set with a boolean observable, based on a GraphQL query. Until the first results of the query are returned, context.enabled will have no value.
let buttonWithRequiredPermission = composeActions(withApolloAction, {
init: context => {
let watchQuery = from(context.client.watchQuery({query}));
// Map the result to a boolean value
context.enabled = watchQuery.pipe(
filter(res => (res.data && res.data.jcr)),
map(res => res.data.jcr.result.hasPermission)
);
},
});
The observable set in the context can send multiple values if needed, and can eventually be based on a GraphQL subscription, such as in workflowDashboardAction.js, which listens for available tasks to display a small count. The button is rerendered every time the observable emits a new value.
const labels = {
"label": {
"tasks": "You have {{numberOfTasks}} tasks"
}
}
const workflowDashboardAction = composeActions(withApolloAction, {
init: context => {
let subscription = from(context.client.subscribe({subscription}));
// Map the result to an object with label params
context.buttonLabelParams = subscription.pipe(
filter(res => (res.data && res.data.jcr)),
map(res => ( { numberOfTasks: res.data.jcr.result } )
);
},
buttonLabel: 'label.tasks'
});
An action can reuse the feature of another action by using a composition pattern. This enables you to reuse common features for different actions, or simply split the code in different parts.
The actionsRegistry.add
function can take a list of actions after the first key attribute, composing the different actions. You can also use the composeActions()
function when defining an action without registering it.
All properties will be merged depending on their type with the following pattern:
Actions that can be composed usually enhance the context by adding fields in the context object. These fields can then be used in your init()
function.
The framework provides reusable actions for common functionality, like menu or router actions. These actions do not have any target by default, but can be extended by composition. You provide additional parameters to extend the actions.
menuAction
routerAction
sideMenuAction
sideMenuListAction
reduxAction
requirementsAction
withApolloAction
withI18nAction
componentRendererAction
You can extend a Jahia module and fill the actions registry by simply adding a JS file that will be loaded before starting the application.
You first must add a line in your pom.xml file that will point to the JS file:
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Jahia-ActionList-Resources>/META-INF/actionlists/marketingFactoryLeftMenu.js::0</Jahia-ActionList-Resources>
</instructions>
</configuration>
</plugin>
This file contains JS code that is called before the application loads and that registers actions. The actionsRegistry
and dxContext
variables are available in the execution context of the script.
The most basic action is a JavaScript call defined in an onClick()
method. It should define a buttonLabel
, buttonIcon
, and a target
.
actionsRegistry.add('help', {
buttonLabel: 'my-module:label.actions.reviewHelp',
buttonIcon: <Help/>,
target: ['toolbarIcons:3'],
onClick: () => {
window.open('https://academy.jahia.com', '_blank');
}
});
An action can be composed with the requirementsAction action to set up the enabled property based on node permissions, types, and the path. It also enables you to retrieve specific fields that you may need for the onClick()
function, or for manually setting the enabled property. The initRequirements()
function, which should be called in your init()
, will fill the context.node and context.enabled properties (as observables). It accepts the following properties, either taking them from the context or as a parameter of initRequirements()
:
requiredPermission
showOnNodeTypes
hideOnNodeTypes
requireModuleInstalledOnSite
showForPaths
hideForPaths
contentType
enabled
actionsRegistry.add('create', requirementsAction, {
init: context => {
context.initRequirements();
}
buttonIcon: <Add/>,
buttonLabel: 'label.contentManager.create.create',
hideOnNodeTypes: ['jnt:page'],
requiredPermission: 'jcr:addChildNodes',
onClick: context => { }
});
actionsRegistry.add('menuWithRequirements', requirementsAction, menuAction, {
init: context => {
context.initRequirements({
requiredPermission: 'jcr:removeNode',
});
}
});
The menu action groups different actions in a pop-up menu. onClick()
and init()
are already implemented to handle the menu. You only need to pass the menu property that defines a new target for the actions to add in the menu. All actions with this value as “target” will be added as menu entries.
actionsRegistry.add('synchronizeMenu', menuAction, {
buttonLabel: 'label.actions.synchronize',
buttonIcon: <Sync style={{fontSize: '20px'}}/>,
target: ['sectionDetailsRow:1'],
menu: 'synchronizeMenu'
});
actionsRegistry.add('synchronize', synchronizeAction, {
buttonLabel: 'label.actions.synchronize',
buttonIcon: <Sync/>,
target: ['reviewChangesRow:1', 'preview:1']
});
The side menu action opens a left drawer containing a tree of actions. It behaves in the same way as menuAction
.
actionsRegistry.add('mfLeft', actionsRegistry.get("sideMenu"), {
buttonLabel: 'marketing-factory-core:label.contentManager.leftMenu.marketingFactory.title',
menu: "leftMenuMFActions",
target: ["leftMenuActions"],
requiredPermission: "canAccessMarketingFactory",
requireModuleInstalledOnSite: 'marketing-factory-core',
buttonIcon: '<svg viewBox="0 0 24 24"...</svg>'
});
As in menuAction
, the menu property must be provided to define which actions display in the menu.
The side menu can contain any action, but can also have sideMenuList
actions which are items in the tree that contain other sub actions.
actionsRegistry.add('mfSiteMetrics', actionsRegistry.get("sideMenuList"), {
buttonLabel: 'marketing-factory-core:label.contentManager.leftMenu.siteMetrics,
target: ["leftMenuMFActions:2"],
menu: "leftMenuMFSiteMetricsActions",
hasChildren: true,
buttonIcon: dxContext.contextPath + "/files/default/modules/marketing-factory-core/" + moduleVersion + "/templates/files/site-metrics.png"
});
Note that the target points to the parent side menu, and “target” will be used for sub actions.
Any action can be added in the side menu, based on the target, provided that is has a label and an icon.
A common action that can be added by modules is an entry pointing to a Site Settings page, either in the Manage menu (the target will be leftMenuManageActions
) or in another target. The action need to be composed from routerAction
, and provides the mode and iframeurl properties:
actionsRegistry.add('mfOptiPerso', actionsRegistry.get("router"), {
buttonLabel: 'marketing-factory-core:label.contentManager.leftMenu.marketingFactory.optiPerso.title',
target: ["leftMenuMFActions:1"],
mode: "apps",
iframeUrl: ":context/cms/editframe/:workspace/:lang/sites/:site.marketing-10-opti-perso.html",
buttonIcon: dxContext.contextPath + "/files/default/modules/marketing-factory-core/" + moduleVersion + "/templates/files/optimize.png"
});
The mode must be set to apps to tell the router to go into apps mode, where an iframe is displayed. The iframeurl is simply the URL to point to. It can contain different placeholders:
:context
:workspace
:lang
:site
:frame