@tridion-docs/extensions
TypeScript icon, indicating that this package has built-in type declarations

2.1.0 • Public • Published

Docs Extensions

This project contains the helper function, components and hooks for working with Tridion Docs extensions.

License

License: https://www.rws.com/legal/terms-and-conditions/rws-technology/

Copyright © 2024 RWS Holdings plc including its subsidiaries and affiliated companies. All rights reserved.

Compatibility

This version of the package is compatible with Tridion Docs 15.2

Upgrade instruction

Packages

To use latest version of extension and extension-cli you need to update versions of package.json in your extension application.

You need to update versions of these two packages @tridion-docs/extensions and @tridion-docs/extensions-cli. We also recommend you to keep all other dependencies up to date. You can find examples of packages and their versions in package.json of extension-example application available on CD.

"dependencies": {
    ...
    "@tridion-docs/extensions": "2.1.0",
    ...
    },
    "devDependencies": {
        ...
        "@tridion-docs/extensions-cli": "2.1.0",
        ...
    }

Documentation

Components

The extension library provides components to assist with extension development.

React component: Iframe

The Iframe component allows you to embed an HTML page inside the extension area. The width and height can be defined as seen in the example below, these can also be percentages, e.g.: "50%", "100%", etc.

The security sandbox is pretty relaxed: allow-forms, allow-popups, allow-same-origin, allow-scripts, however the iframe content is not allowed to access data from the parent frame (the Docs Organize Space application).

Example

import { Iframe } from '@tridion-docs/extensions';

export const CustomIframePage = () => {
    return <Iframe src="https://www.wikipedia.org/" width="600" height="400" />;
};

Props

Name Type Description
src string Url to load inside the iframe
width number / string Width of the iframe ("100%" by default)
height number / string Height of the iframe ("100%" by default)
className string Optional CSS classname

React component: DataIndicator

The DataIndicator component handles loading, error and empty content views.

Example

import { DataIndicator } from '@tridion-docs/extensions';

export const CustomPage = () => {
    return  (
        <DataIndicator isLoading={false} noDataMessage="There is no information to show.">
            <div>This content will be visible if isLoading set to false</div>
        </DataIndicator>
    );
};

Props

Name Type Description
children ReactNode Content that should be rendered when hasData is true AND isLoading is false AND isLoadingFailed is false
errorMessage string Message that rendered when isLoadingFailed is true
hasData boolean Indicates that content is ready to be rendered (children). When true children is rendered
isLoading boolean Controls loading indicator. When true loading indicator is shown
isLoadingFailed boolean Controls error message. When true error message is rendered
noDataMessage string Rendered when hasData is false

Hooks

The extension library provides hooks to assist with extension development.

useItemSelector

The useItemSelector allows you to show dialog with a structured folder tree on the left side and the content of the folder on the right side. This dialog supposes to help you to select one of the multiple objects based on your configuration.

Example

import { useItemSelector, ItemSelectorObjectType } from '@tridion-docs/extensions';

export const useCustomHook = () => {

    const itemSelectorProps = {
        objectTypesToSelect: [
            DocumentObjectType.Illustration,
            DocumentObjectType.Library,
            DocumentObjectType.Map,
            DocumentObjectType.Other,
            DocumentObjectType.Topic,
        ],
        isMultiSelect: false,
        onOk: async (values: Content[]) => {
            console.log('selected', values);
        },
        okLabel: 'Ok',
        cancelLabel: 'Cancel',
    };
    const { execute } = useItemSelector(itemSelectorProps);

    return {
        showItemSelector: execute,
    };
}

Props

Name Type Description
isMultiSelect boolean Whether multiple items can be selected
objectTypesToSelect ItemSelectorObjectType List of object types that can be selected. Default available types: undefined, illustration, library, map, other, topic, publication
onOk function Function that will be called when a user clicks the OK button
okLabel string Label of the 'Ok' button
cancelLabel string Label of the 'Cancel' button
modalZindex number Property to set z-index property on the item selector modal

Return

Name Type Description
execute function By calling this function you will show item selector on the screen.

useFolderSelector

The useFolderSelector allows you to show dialog with a structured folder tree. This dialog supposes to help you to select one folder.

Example

import { useFolderSelector, ContentState, Folder } from '@tridion-docs/extensions';

export const useCustomHook = () => {

    const folderSelectorProps = {
        onOk: async (folder: Folder) => {
            console.log('selected', folder);
        },
        getDisabledFolderIds: (_hierarchy: Hierarchy<ContentState>, _sourceFolderId: string) => {
            return [];
        },
        showConstraints: false,
        constraintsComponent: () => <></>,
        modalIcon: <MyIcon />,
        modalTitle: 'The Modal Title',
        okButtonLabel: 'Select',
        cancelButtonLabel: 'Cancel',
    };
    const { execute } = useFolderSelector(folderSelectorProps);

    return {
        showFolderSelector: execute,
    };
}

Props

Name Type Description
getDisabledFolderIds function Disable the ability to select a list of Folders
showConstraints boolean Show or Hide the Show Constraints button
constraintsComponent boolean Component to show when user clicks on Show Constraints button
onOk function Function that will be called when a user clicks the OK button
okButtonLabel string Label of the 'Ok' button
cancelButtonLabel string Label of the 'Cancel' button
modalTitle string Title of the Modal
modalIcon string Icon of Modal Title
cancelButtonLabel string Label of the 'Cancel' button

Return

Name Type Description
execute function By calling this function you will show folder selector on the screen.

Notifications

Hook for displaying a notification message.

Example

export const useCustomHook = (props: any) => {
    const { execute: executeNotification, messageSeverity } = useNotification();

    executeNotification({
        message: 'Notification title',
        description: 'Notification message',
        severity: messageSeverity.SUCCESS,
    });

}

Props

Name Type Description
none

Return

Name Type Description
execute function(message, description, severity, actionArea) By calling this function you will show a notification message. message: Title of the notification, description: Body or text of the notification, severity: notification type, actionArea: Area to add action button (optional)
messageSeverity MessageSeverity Message type, based on the type design and behavior of notification may vary.

useRepositoryFolderRefresh

Hook to refresh content of the folder

Example

export const useCustomHook = (props: any) => {
    const { execute: executeRepositoryFolderRefresh } = useRepositoryFolderRefresh({ item: contextFolder as Content });

    executeRepositoryFolderRefresh().then(() => {
        console.log('refresh complete');
    })
}

Props

Name Type Description
item Content Folder to be refreshed

Return

Name Type Description
execute () => Promise<Content> By calling this function you will refresh folder content, promise will be resolved when the data fetching is completed.

useRepositoryObjectDetailsRefresh

Hook to refresh current details view

Example

export const useCustomHook = (props: any) => {
    const { execute } = useRepositoryObjectDetailsRefresh();

    execute().then(() => { console.log('refresh complete') })
}

Props

Name Type Description
none

Return

Name Type Description
execute () => Promise<Content> By calling this function you will refresh details view, promise will be resolved when the data fetching is completed.

useRepositoryObjectRefresh

Hook to refresh content of the provided item

Example

export const useCustomHook = (props: any) => {
    const { execute } = useRepositoryObjectRefresh();

    execute().then(() => { console.log('refresh complete') })
}

Props

Name Type Description
item Content Content item to be refreshed

Return

Name Type Description
execute () => Promise<Content> By calling this function you will refresh content, promise will be resolved when the data fetching is completed.

useUserProfile

Hook to get current logged on user information

Example

import { useUserProfile } from '@tridion-docs/extensions';


export const CustomComponents = (props: any) => {
    const { execute: getUserProfile } = useUserProfile();
    const { displayName } = getUserProfile();

    return (
        <div>
            Hi {displayName}!
        </div>
    )
}

Props

Name Type Description
none

Return

Name Type Description
execute () => UserInfo By calling this function you will get current logged user information. userName: User name, displayName: Display name, uiLanguage: Ui language of the user, workingLanguage: Working language, privileges: list of available privileges

Extension points

Organize Space of Tridion Docs supports several areas that can be extended and custom logic can be integrated into. Every extension needs to be initialized before it can be used. The initial registration of the extension areas can be done with the help of ExtensionModule interface. If you're using @tridion-docs/extensions-cli for generating your app then ExtensionModule interface will be already set in your index.ts file.

To register your extension you need to call a builder function inside initialize method.

const extensionModule: ExtensionModule = {
    runtimeInfo: packageJson,
    initializeGlobals,
    initialize: builder => {
        builder.objectInsight.addObjectInsightItem(() => ({
//...
        }));

        builder.action.addExplorerAction(() => ({
//...
        }));

        builder.header.addMainNavigationLink(() => ({
//...
        }));

        builder.header.addMainNavigationItem(() => ({
//...
        }));
    },
};

Header

A header extension point is providing the ability to extend the main navigation and add a link button to the right side of the header

mainNavigationItem

Registration of new navigation items is required to call the builder function builder.header.addMainNavigationItem

        builder.header.addMainNavigationItem(() => ({
            id: 'id',
            title: 'title',
            path: 'what-ever',
            position: {
                item: 'settings',
                positioningType: 'after',
            },
            isVisible: () => { return true}
            component: CustomPage,
        }));

Props

Name Type Description
id string Unique Id for menu item
title string Menu item title
path string Menu item path (should be unique)
position Position Menu item relative position.
isVisible ({userProfile: UserInfo}) => boolean Function to determine if the menu item should be visible, it receives the current user and should return a boolean value (show/hide)
component ComponentType Component that will be rendered

mainNavigationLink

Adds extra navigation link in the main application header. Registration of new navigation items is required to call the builder function builder.header.addMainNavigationLink

builder.header.addMainNavigationLink(() => ({
    id: 'id',
    title: 'title',
    tooltip: '',
    popupProperties: {
        width: '600px',
        height: '600px'
    },

    isVisible: () => {
        return true;
    },
    icon: CustomIcon,
    component: CustomPage,
}));

Props

Name Type Description
id string Unique Id for navigation link used by system to identify it
title string Title of navigation link that will be visible on UI
tooltip string Tooltip of navigation link that will be visible on mouse hover
popupProperties { width: string, height: string } Configuration for popup
isVisible ({userProfile: UserInfo}) => boolean Function to determine if the menu item should be visible, it receives the current user and should return a boolean value (show/hide)
icon ComponentType Component that will be rendered in place of the icon
component ComponentType Component that will be rendered after mouse click

Action

Custom action button that might be used for executing custom logic for selected items. Registration of new navigation items is required to call the builder function builder.header.builder.action.addExplorerAction.

addExplorerAction supports generic type definition, so we specify expected selected object type more about generics.

builder.action.addExplorerAction(() => ({
    id: 'id',
    title: 'title',
    tooltip: '',
    isVisible: (props) => {
        return true;
    },
    icon: CustomIcon,
    hook: useCustomHook,
}));

/* if we expect type other than `Content` we can define it here */
import { Project, ViewType } from '@tridion-docs/extensions';
builder.action.addExplorerAction<Project>(() => ({
    id: 'id',
    title: 'title',
    tooltip: '',
    isVisible: ({ activeView }) => {
        return activeView === ViewType.project;
    },
    icon: CustomIcon,
    hook: useCustomHook,
}));

Props

Name Type Description
id string Unique Id for action button used by system to identify it
title string Title of navigation action button will be visible on UI
tooltip string Tooltip of action button that will be visible on mouse hover
isVisible ({activeView: ViewType, userProfile: UserInfo}) => boolean Function to determine if the action button should be visible, it receives the current user and should return a boolean value (show/hide)
icon ComponentType Component that will be rendered in place of the icon
hook function Hook that will be used for this action.

Object Insight

The object right side panel is representing a custom data view for one or multiple selected items. Registration of new object insight is required to call the builder function builder.objectInsight.addObjectInsightItem

addObjectInsightItem supports generic type definition, so we specify expected selected object type more about generics.

builder.objectInsight.addObjectInsightItem(() => ({
    id: 'id',
    tooltip: 'tooltip',
    isVisible: (props) => {
        return false;
    },
    icon: CustomIcon,
    component: CustomComponent,
}));

/* if we expect type other than `Content` we can define it here */
builder.objectInsight.addObjectInsightItem<Project>(() => ({
    id: 'id',
    tooltip: 'tooltip',
    isVisible: (props) => {
        return false;
    },
    icon: CustomIcon,
    component: CustomComponent,
}));

Props

Name Type Description
id string Unique Id for insight panel used by system to identify it
tooltip string Tooltip for insight panel that will be visible on mouse hover
isVisible ({activeView: ViewType, userProfile: UserInfo}) => boolean Function to determine if the panel should be visible, it receives the current user and should return a boolean value (show/hide)
icon ComponentType Component that will be rendered in place of the icon
component ComponentType Component that will be rendered after mouse click

Types Explanations

PositionType

{
    item: 'settings',
    positioningType: PositioningType.addAfter,
}
Name Type Description
item string Id of the navigation item that will be used for positioning. Default navigation items ('content', 'events', 'settings'), also you can use a previously added custom navigation items id
PositioningType PositioningType string

Generic types

As far as we're supporting extensions in different areas like Publication Hub and Content Explorer hooks or components will operate with different types of input parameters. It doesn't not affect the API of the function but it will affect the types of arguments. For example, if we define action in Project or Project assignees in "Publication hub" we will get a Project type object instead of Content type. To support this case we introduced a generic type for some of the functions like addExplorerAction where you may specify the expected type of the active objects.

The default type is Content. That means addExplorerAction<Content>() and addExplorerAction() are equal code blocks.

More about typescript generics

Translations

Translation records can be registered per language by using the function builder.translations.addTranslation

    builder.translations.addTranslation('en', {
        key: 'EN Value',
    });

    builder.translations.addTranslation('es', {
        key: 'ES Value',
    });

    //....

Usage

Import t function returned by createExtensionGlobals and pass it the translation key.

    `Hello in, {t('key')}`

Supported language

Name Code
English en
Deutsch de
Spanish es
French fr
French (Canada) fr-CA
Italian it
Japanese ja
Chinese (Simplified) zh

Generate open api typescript client from the swagger

  • Put a swagger-generated file spec.json inside your project for example src/oapi/spec. You can skip this step if you want to use direct link to the swagger spec.
  • As an example, for the generation we are going to use nswag(https://www.npmjs.com/package/nswag). You can install it by running the command npm i nswag -D (-D indicates that it should be a dev dependency)
  • Add generate command into the package.json

Add build client command into package.json file

You can put client build command in the package.json file in the scripts section.

example for a local swagger spec file

  "scripts": {
        // ...
        "generate-client": "nswag openapi2tsclient /input:./src/oApi/spec/spec.json /output:src/oApi/client/api-client.ts"
    },

example for a remote swagger spec file

  "scripts": {
        // ...
        "generate-client": "nswag openapi2tsclient /input:https:/...../api-docs/v3/spec.json /output:src/oApi/client/api-client.ts"
    },
  • openapi2tsclient - Generates TypeScript client code from a Swagger/OpenAPI
  • /input: - path to the spec.json file. (direct url to the spec.json can be specified)
  • /output: - where to put generate file

Generating client

npm run "generate-client will generate a typescript client in the folder specified in output parameter.

Usage of generated client

import { Client } from 'oApi/client/api-client';

const client = new Client();
client
    .getApplicationVersion()
    .then(data => {
        // logic is here
    })
    .catch(error => {
        // error handling is here
    });

How to get baseUrl

All endpoints added via backend extensions will be accessible by the following URL https://[domain]/[instance]/OrganizeSpace/Extensions. To pass correct baseUrl while instantiating nswag client you can use following example.

import { Client } from 'oApi/client/api-client';

const getExtensionsBaseUrl = () => {
    const re = new RegExp('/.*?(OrganizeSpace)', 'i');
    const regExpResult = window.location.pathname.match(re);
    const base = regExpResult ? `${regExpResult[0]}/Extensions` : '';
    return base;
};

const baseUrl = getExtensionsBaseUrl();
const client = new Client(baseUrl);

FAQ

I want to use useItemSelector inside Antd Modal component but I can't see popups and filters in item selector.

This is a known issue caused by mixing two different UI frameworks. To resolve this issue, there are two approaches:

Preferable way

Reset the zIndex property for the Antd Modal and specify a custom container that positions it correctly relative to the ItemSelector.

    const popupContainer = useMemo(() => {
        const container = document.createElement('div');
        document.body.append(container);
        return container;
    }, []);

    <Modal
        zIndex={0}
        getContainer={popupContainer}

Complete page example

import { Content, DocumentObjectType, useItemSelector } from '@tridion-docs/extensions';
import { useMemo, useState } from 'react';
import { Button, Modal } from 'antd';

export const CustomPage = () => {
    const [isModalOpen, setIsModalOpen] = useState(false);

    const showModal = () => setIsModalOpen(true);
    const handleOk = () => setIsModalOpen(false);
    const handleCancel = () => setIsModalOpen(false);

    const itemSelectorProps = {
        objectTypesToSelect: [DocumentObjectType.Library],
        isMultiSelect: false,
        onOk: (selectedItems: Content[]) => console.log(selectedItems),
        okLabel: 'ok',
        cancelLabel: 'cancel',
    };

    const { execute: showItemSelector } = useItemSelector(itemSelectorProps);

    const popupContainer = useMemo(() => {
        const container = document.createElement('div');
        document.body.append(container);
        return container;
    }, []);

    return (
        <>
            <Button type="primary" onClick={showModal}>Open Modal </Button>
            <Modal
                title="Basic Modal"
                open={isModalOpen}
                onOk={handleOk}
                onCancel={handleCancel}
                zIndex={0}
                getContainer={popupContainer}
            >
                <Button onClick={showItemSelector}>show</Button>
            </Modal>
        </>
    );
};

Alternative way

If you prefer a simpler fix, you can use CSS to globally adjust the z-index for popups. However, this may cause issues with other components and is less reliable across different library versions.

import { Content, DocumentObjectType, useItemSelector } from '@tridion-docs/extensions';
import { useState } from 'react';

import { Button, Modal } from 'antd';

import './CustomPage.module.css';

export const CustomPage = () => {
    const [isModalOpen, setIsModalOpen] = useState(false);

    const showModal = () => setIsModalOpen(true);
    const handleOk = () => setIsModalOpen(false);
    const handleCancel = () => setIsModalOpen(false);

    const itemSelectorProps = {
        objectTypesToSelect: [DocumentObjectType.Library],
        isMultiSelect: false,
        onOk: (selectedItems: Content[]) => console.log(selectedItems),
        okLabel: 'ok',
        cancelLabel: 'cancel',
        modalZindex: 1002
    };

    const { execute: showItemSelector } = useItemSelector(itemSelectorProps);

    return (
        <>
            <Button type="primary" onClick={showModal}>Open Modal </Button>
            <Modal
                title="Basic Modal"
                open={isModalOpen}
                onOk={handleOk}
                onCancel={handleCancel}
            >
                <Button onClick={showItemSelector}>show</Button>
            </Modal>
        </>
    );
};
/* CustomPage.module.css */

div[data-test~='popup'] {
    z-index: 9999;
}

body > div > div[class*='ant-select-dropdown'] {
    z-index: 10000;
}

Note: This solution applies globally, affecting all popups on the page. It is not guaranteed to work consistently across future releases, as the CSS selector may change.

Readme

Keywords

none

Package Sidebar

Install

npm i @tridion-docs/extensions

Homepage

www.rws.com

Weekly Downloads

13

Version

2.1.0

License

SEE LICENSE IN LICENSE.md

Unpacked Size

89.3 kB

Total Files

95

Last publish

Collaborators

  • ssynevych
  • erwinlef
  • yaydos
  • echikunov
  • osolodovnyk