Adding UI Actions

November 14, 2023

You can extend Jahia UI by integrating actions from your own modules. This allows you to make Jahia 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 headerPrimaryActions and a contentActions 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 properties

The application can pass contextual properties to an action. For example, jContent will pass the current path of the selected node to all actions through the "path" property. If multiple nodes are selected, a "paths" property will be sent. The context will depend on the target - actions that require a path property can only be used in target that will provide this context. 

Actions structure

Action properties

Each action is described by a simple JS object that is added into the common registry. As with most of the items in the registry, it must have a unique key and a list of targets. This list of targets declare where the action appears. Action descriptor can contain the following properties:

  • 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 contentActions:4.
    • Starting with Jahia 8.1.6, when adding an action in the registry, you can easily identify the type and the target by looking at the html source of a similar entry: the <li> tag contains the data-registry-key (containing the type)  and data-registry-target (containing the target and position) attributes. See example in Identifying the type and target.
  • buttonLabel 
    The label key in the button or menu entry. The key must be prefixed by a namespace (module name):buttonLabel:, for example jexperience:label.title.
  • buttonLabelParams
    Parameters passed to the label key for placeholder replacements.
  • buttonIcon
    An optional icon that you can display. It should be a React icon component. You can use the helper function toIconComponent() (from @jahia/moonstone, or window.jahia.moonstone) to transform a path or an inline SVG to an icon component.
  • enabled
    If set to false, the action will be "disabled".

  • isVisible
    If set to false, the action won't be displayed.

There are 2 ways to declare actions: either simple or component actions. 

Simple actions

Simple actions can be written in plain JS, with 2 simple callbacks :

  • 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 displays or when it receives a new context. The function receives the context as a parameter and can enhance the context, for example adding buttonLabelParams or enabled.

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

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.

In the example below, we add an action that will only be enabled on the mySite website and that will simply log a message in the console when clicked.

    registry.add('action', 'simple-action', {
        targets: ['contentActions:10'],
        buttonIcon: <DefaultEntry/>,
        buttonLabel: 'Simple action',
        init: context => {
            context.enabled = context.path.startsWith('/sites/mySite');
            console.debug('simple-action: initialized enabled=' + context.enabled)
        },
        onClick: context => {
            console.debug('simple-action: clicked ' + context.path);
        }
    });

Component actions

You can create a component action by writing a React component. In this case, you just need to declare the component to use:

  • component 
    The component to use for the action.

Component actions are much more powerful than simple actions as they can rely on all React features. For example, you can make asynchronous call, check a permission, perform a graphql query, create modal when being called, and more.

The component receives the context as properties, along with the render and loading props. The render is actually the component to use to render the action. It can be a button or menu item, depending on the way the action was called. The component should never render a button by itself, but only contain the logic and rely on the component to do the rendering. The render component needs to receive all original props, enhanced with an onClick callback - the function that needs to be called to trigger the action. 

The loading component should be returned if the component is waiting for an asynchronous result [in future versions, the loading prop will be removed and the system will rely on React suspense].

Here is the same action as before, written as a component:

    const MyComponent = ({path, render: Render, ...others}) => (
        <Render
            {...others}
            enabled={path.startsWith('/sites/mySite')}
            onClick={() => {
                console.debug('clicked ' + path);
            }}/>
    );

    registry.add('action', 'component-action', {
        targets: ['contentActions:10'],
        buttonIcon: <DefaultEntry/>,
        buttonLabel: 'Component action',
        component: MyComponent
    });

Registering actions

You register actions in the registry by calling the registry.add function (from @jahia/ui-extender or window.jahia.uiExtender) with "action" as the first parameter and a unique key as the second parameter, followed by the action descriptor. An action can be found by its key by using registry.get('action',key). Note that the action key is added in the action descriptor (under the "key" property).
If the action already exists with the same key, an error will be thrown. If you want to avoid this error and force the action to be installed you can instead use the registry.addOrReplace function.

You can extend an existing action by adding its descriptor in the registry.add function: 

    registry.add('action', 'base-action', {
        targets: ['contentActions:10'],
        buttonIcon: <DefaultEntry/>,
        buttonLabel: 'Param action',
        onClick: context => {
            console.debug('param', context.param);
        }
    });

    registry.add('action', 'action-1', registry.get('action', 'base-action'), {
        param: 'one'
    });

    registry.add('action', 'action-2', registry.get('action', 'base-action'), {
        param: 'two'
    });

Actually, the registry.add can receive any number of descriptors after the key, and will compose them to create a new descriptor.

Actions cookbook

Menu actions

The @jahia/ui-extender library provides a generic drop-down menu action. This action will open a drop-down menu when clicked, showing other actions. The menu needs to define a target where these actions will be added. A base descriptor is provided as menuAction  and can be found with registry.get('action', 'menuAction')

Creating a menu can be done by just declaring a few descriptors, one for the menu and one for each action in the menu: 

    registry.add('action', 'myMenu', registry.get('action', 'menuAction'), {
        targets: ['contentActions:10'],
        buttonIcon: <DefaultEntry/>,
        buttonLabel: 'My menu...',
        menuTarget: 'myMenuTarget'
    });

    registry.add('action', 'myMenu-action-1', {
        targets: ['myMenuTarget:1'],
        buttonLabel: 'Action 1',
        onClick: () => console.debug('myMenu action 1')
    });

    registry.add('action', 'action-2', {
        targets: ['myMenuTarget:2'],
        buttonLabel: 'Action 2',
        onClick: () => console.debug('myMenu action 2')
    });

Documentation on the menuAction itself can also be found on GitHub.

Adding node requirements

The @jahia/data-helper library provides useful functions to check requirements on a node. The useNodeChecks hook will take a path as parameter, and a list of options to check on the node. These options can include: 

  • requiredPermission
    A single permission required for the action to be enabled, or a list (array) of permissions
  • requiredSitePermission
    A single site permission required for the action to be enabled, or a list (array) of permissions
  • 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

The hook will test these predicates on the given node and will return a simple boolean value in checksResult. Here is a simple example of a component using the useNodeChecks hook (from @jahia/data-helper) to test a permission:

    const MyComponent = ({path, render: Render, loading: Loading, ...others}) => {
        const {checksResult, loading} = useNodeChecks(
            {path},
            {requiredPermission: 'permission'}
        );

        if (Loading && loading) {
            return <Loading {...others}/>;
        }

        console.debug('Permission check results', checksResult);

        return (
            <Render
                {...others}
                enabled={checksResult}
                onClick={() => {
                    console.debug('clicked ' +path);
                }}/>
        );
    };

    registry.add('action', 'component-action', {
        targets: ['contentActions:10'],
        buttonIcon: <DefaultEntry/>,
        buttonLabel: 'Permission action',
        component: MyComponent
    });