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.1
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.0.0",
...
},
"devDependencies": {
...
"@tridion-docs/extensions-cli": "2.0.0",
...
}
BaseFolder, DocumentObjectType, FolderType, PublicationObjectType enums keys were changed from UPPERCASE to PascalCase.
// before
BaseFolder.EDITOR_TEMPLATE
// after
BaseFolder.EditorTemplate-
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.jsoninside 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(-Dindicates 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);