This project contains the helper function, components and hooks for working with Tridion Docs extensions.
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.
This version of the package is compatible with Tridion Docs 15.2
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",
...
}
-
Components - React components
-
Hooks - React hooks, helps you to show custom components, refresh content etc.
-
Extension Points - Allows to register custom components in one of the available areas.
-
Other documentation
The extension library provides components to assist with extension development.
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 |
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
|
The extension library provides hooks to assist with extension development.
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. |
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. |
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. |
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. |
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. |
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. |
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 |
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(() => ({
//...
}));
},
};
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
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 |
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 |
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. |
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 |
{
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 |
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
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',
});
//....
Import t
function returned by createExtensionGlobals
and pass it the translation key.
`Hello in, {t('key')}`
Name | Code |
---|---|
English | en |
Deutsch | de |
Spanish | es |
French | fr |
French (Canada) | fr-CA |
Italian | it |
Japanese | ja |
Chinese (Simplified) | zh |
- Put a swagger-generated file
spec.json
inside your project for examplesrc/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 commandnpm i nswag -D
(-D
indicates that it should be a dev dependency) - Add generate command into the
package.json
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
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);
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.