Extending Content and Media Manager

October 8, 2024

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.

Concepts

Actions

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.

Context objects

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.

Actions structure

Action properties

Each action is described by a simple JS object that can define the following properties: 

  • onClick 
    The function that is called when a user triggers the action. The function receives the context as a parameter.
  • init
    An optional function that is called when the action is displayed or when it receives a new context. The function receives the context as a parameter and can enhance the context.
  • target 
    The list of locations where the action is added. Each value is a simple unique string provided by the application, optionally followed by a number (float) indicating the relative position of the action in the list of actions, for example contextualMenu:4.
  • buttonLabel 
    The label key in the button or menu entry. The key must be prefixed by a namespace (module name):buttonLabel:, for example marketing-factory-core:label.title.
  • buttonLabelParams
    Parameters passed to the label key for placeholder replacements.
  • buttonIcon
    An optional icon that you can display. It can be a react icon component, path to file (passed directly to an img src attribute, which should include the root context), or an SVG as a string.
  • enabled
    A boolean value that specifies whether to enable the action. The action is usually initialized in the init function depending on the context.

All properties of the action are merged with the context and passed to the init() and onClick() functions.

Registering actions

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).

Initializing actions

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'
});

Actions composition

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:

  • For strings, the last value will override all others
  • For lists, values will be merged
  • For functions, all functions will be called in order, from first to last

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.

Abstract actions

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 can get these actions through the registry, and then provide additional parameters to extend them.

  • routerAction( actionsRegistry.get('router') )
    Sends an event to the react router
  • sideMenuAction( actionsRegistry.get('sideMenu') )
    Opens a left side menu, similar to the CMM>Manage menu.
  • sideMenuListAction ( actionsRegistry.get('sideMenuList') )
    Provides a simple item in the sideMenu

Starting from Content Media Manager 1.5.1, you can also access the following :

  • componentRendererAction( actionsRegistry.get('componentRenderer') )
    Enhances the context with the renderComponent function, which can be used to render a React component in DOM root (for dialogs)
  • menuAction( actionsRegistry.get('menu') )
    Displays a drop-down menu with other actions
  • requirementsAction( actionsRegistry.get('requirements') )
    Retrieves a node based on context path, by checking permissions, node types and other requirements for enabling an action
  • withApolloAction( actionsRegistry.get('withApollo') )
    Enhances the context with an Apollo Client
  • withI18nAction  ( actionsRegistry.get('withI18n') )
    Enhances the context with the t (translate) i18next function

Extending actions in modules

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.

Actions cookbook

Simple action

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');
   }
});

Adding node requirements ( available from v1.5.1 )

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
    A single permission required for the action to be enabled
  • showOnNodeTypes
    A list of node types allowed for this action
  • hideOnNodeTypes 
    A list of node types hidden for this action
  • requireModuleInstalledOnSite 
    The name of a module that must be installed on the site 
  • showForPaths
    A list of regexp paths on which the action is enabled
  • hideForPaths
    A list of regexp path on which the action is disabled
  • contentType 
    An action that is enabled only if contentType is allowed as a sub-node
  • enabled 
    A function that adds additional conditions for the enabled flag 
actionsRegistry.add('create', actionsRegistry.get('requirements'), {
   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',
      });
   }
});

Menu action ( available from v1.5.1 )

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', actionsRegistry.get('menu'), {
   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']
});

Side menu action

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.

Site settings/iframe action

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