Last updated Nov 11, 2024

Using rich-text bodied macros

This section describes a Forge preview feature. Preview features are deemed stable; however, they remain under active development and may be subject to shorter deprecation windows. Preview features are suitable for early adopters in production environments.

We release preview features so partners and developers can study, test, and integrate them prior to General Availability (GA). For more information, see Forge release phases: EAP, Preview, and GA.

This tutorial demonstrates how you can use rich-text bodied macros in Forge. It shows how to configure your manifest and render rich body content, using the ADF Renderer React component, or the Confluence convert content body APIs.

You can add simple configuration to a macro using UI Kit components, as described here.

You can add custom configuration to a macro, as described here.

Before you begin

This tutorial assumes you're already familiar with the basics of Forge development. If this is your first time using Forge, see Getting started first.

To complete this tutorial, you need the latest version of Forge CLI. To update your CLI version, run npm install -g @forge/cli@latest on the command line.

Set up a cloud developer site

An Atlassian cloud developer site lets you install and test your app on Confluence and Jira products set up for you. If you don't have one yet, set it up now:

  1. Go to http://go.atlassian.com/cloud-dev and create a site using the email address associated with your Atlassian account.
  2. Once your site is ready, log in and complete the setup wizard.

You can install your app to multiple Atlassian sites. However, app data won't be shared between separate Atlassian sites, products, or Forge environments.

The limits on the numbers of users you can create are as follows:

  • Confluence: 5 users
  • Jira Service Management: 1 agent
  • Jira Software and Jira Work Management: 5 users

The Atlassian Marketplace doesn't currently support cross-product apps. If your app supports multiple products, you can publish two separate listings on the Marketplace, but your app won't be able to make API calls across different products and instances/installations.

Step 1: Configure the manifest

To set your macro as a bodied macro, navigate to the app's manifest.yml file and add the line layout: bodied in the macro module properties.

1
2
macro:
  - key: my-macro
    ...
    layout: bodied

Step 2: Extract the macro body

The macro body is the content that the user enters in the editor. This can be retrieved from the context provided by useProductContext() or view.getContext().

In a UI Kit app, we can use useProductContext() to extract the macro body.

1
2
import React from 'react';
import ForgeReconciler, { useProductContext } from '@forge/react';

const App = () => {
  const context = useProductContext();
  const macroBody = context?.extension?.macro?.body;
  // ...
}

ForgeReconciler.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Step 3: Render rich body content

The ADF body can be rendered in two ways; using the renderer components, or by rendering the raw HTML.

Comparison between using renderer components or HTML export

The following content types are supported by the HTML export, but not supported by the ADF renderer component:

The following content types are not supported at all by the HTML export or the ADF renderer component:

See detailed documentation for the AdfRenderer.

Using renderer components

For UI Kit, you can use the AdfRenderer component, while for custom UI, you can use the ReactRenderer component. Both the AdfRenderer and ReactRenderer component require the prop document, which is an ADF document with the following structure:

1
2
"body": {
  "type": "doc",
  "version": 1,
  "content": [
    // ADF content
  ]
}

We can pass the macro body that we extracted previously into the document prop of AdfRenderer and ReactRenderer components for UI Kit and custom UI respectively.

Using the Confluence API to export to HTML

Alternatively, you can use requestConfluence() to make a request to the Confluence Cloud platform REST API to convert the macro body into HTML. We can render this HTML directly in our Forge app.

The following code imports requestConfluence, and extracts contentId and macroBody from the context obtained in step 2.

1
2
import { requestConfluence } from "@forge/bridge";

const App = () => {
  const context = useProductContext();
  
  const macroBody = context?.extension?.macro?.body;
  const contentId = context?.extension?.content?.id;
  ...
}

Next, we can construct the async API call for the macro body in HTML form using the Asynchronously convert content body call.

1
2
async function convertMacroBody(to, macroBody, contentId) {
  const response = await requestConfluence(
    `/wiki/rest/api/contentbody/convert/${to}?contentIdContext=${contentId}`,
    {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        value: macroBody,
        representation: "atlas_doc_format",
      }),
    }
  );

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return (await response.json()).asyncId;
};

const asyncId = await convertMacroBody(
  "styled_view",
  macroBody,
  contentId
);

Use "atlas_doc_format" for the representation parameter in body. This indicates that the macro body is in ADF format.

Use "styled_view" for the to parameter. This will convert the macro body to an HTML document with embedded styles.

We can obtain the HTML body from the id that is returned from convertMacroBody() using the Get asynchronously converted content body from the id or the current status of the task call.

1
2
async function fetchConvertedMacroBody(id) {
  const response = await requestConfluence(
    `/wiki/rest/api/contentbody/convert/async/${id}`, 
    {
      headers: {
        'Accept': 'application/json'
      }
    }
  );

  const { status, value, error } = response.json();
  if (status === "FAILED") {
    throw new Error(`Conversion failed: ${error}`)
  } else if (status === "COMPLETED") {
    return value;
  }
  // Keep polling until completed
  return fetchConvertedMacroBody(id);
};

const htmlBody = await fetchConvertedMacroBody(asyncId);

Rendering the export HTML

Finally, we can directly render the HTML.

For UI Kit, we can render the HTML in a Frame component. Please see Frame for more details.

First define the Frame in the resources section of the manifest.yml file. Please see an example manifest structure here.

1
2
resources:
  ...
  - key: html-frame
    path: resources/html-frame/build

Next, create an index.js and index.html file in the path specified in the manifest resources. See here for more information on setting up resources.

The onPropsUpdate function will be called each time the parent component provides HTML, which we will then render directly inside our Frame.

1
2
import { view } from "@forge/bridge";

events.on("PROPS", ({ html }) => {
  document.documentElement.innerHTML = html;
});

This frame can now be used in your UI Kit app to render the HTML body. This is your src/frontend/index.jsx file. The following code uses createFrame.

1
2
import { Frame } from "@forge/react"

const App = () => {
  useEffect(() => {
    events.emit("PROPS", { html: htmlBody });
  }, [htmlBody]);

  return <Frame resource="html-frame" />
}

Use the body in an adfExport function

When the page is exported to PDF, Word, or viewed in the page history, you can specify how the app should be displayed. This is done by specifying an adfExport function, and referencing it in your app's manifest.yml file.

When there is no export function defined, by default, the macro body will be returned.

Accessing the rich text body is done via the extensionPayload.macro.body property.

If you would like to customise the export:

  1. Create a new ADF document
  2. Insert any extra ADF nodes for customisation (such as paragraphs, shown in the sample code)
  3. Insert the rich-text body content from the macro into the document

Please see the full adfExport tutorial here.

1
2
import { doc, p } from '@atlaskit/adf-utils/builders';

export function adfExport(payload) {
  const macroBody = payload.extensionPayload.macro.body;

  return doc(
    p("This is my export function. Here's the macro content:"),
    ...macroBody.content
  )
}

Rate this page: