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.
You can also see a sample implementation of a rich-text bodied macro in the rich-text-custom-config-macro sample app.
Make sure you have the following:
npm install -g @forge/cli@latest
on the command line.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 2macro: - key: my-macro ... layout: bodied
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 2import 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> );
The ADF body can be rendered in two ways; using the renderer components, or by rendering the raw HTML.
The following content types are supported by the HTML export, but not supported by the ADF renderer component:
adfExport
functionThe following content types are not supported at all by the HTML export or the ADF renderer component:
adfExport
functionSee detailed documentation for the AdfRenderer.
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.
To render in UI Kit, you can use the AdfRenderer
component from @forge/react.
1 2import { AdfRenderer } from "@forge/react"; const App = () => { // ... return macroBody && <AdfRenderer document={macroBody} /> }
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.
Next, we can construct the async API call for the macro body in HTML form using the Asynchronously convert content body call.
If we want to support rendering embedded content, we must also provide the following query parameters:
contentIdContext
: The content ID for resolving embedded content in the content bodyexpand
: Return CSS and JavaScript tags in the response for rendering embedded contentNote that this embedded content includes core Confluence macros, but does not support other Connect or Forge macros.
Also note the ADF body (value
) is stringified again.
This is because the API accepts bodies in various formats, including XML,
so it cannot make any assumptions about the format of the body.
1 2import { requestConfluence } from "@forge/bridge"; async function convertMacroBody(to, macroBody, contentId) { const params = new URLSearchParams({ contentIdContext: contentId, expand: "webresource.tags.all,webresource.superbatch.tags.all", }).toString(); const response = await requestConfluence( `/wiki/rest/api/contentbody/convert/async/${to}?${params}`, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ value: JSON.stringify(macroBody), representation: "atlas_doc_format", }), } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return (await response.json()).asyncId; } const macroBody = context?.extension?.macro?.body; const contentId = context?.extension?.content?.id; if (macroBody) { 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.
Note that we inject the expanded CSS and JavaScript tags into the HTML body.
1 2async function fetchConvertedMacroBody(id) { const response = await requestConfluence( `/wiki/rest/api/contentbody/convert/async/${id}`, { headers: { Accept: "application/json", }, } ); const { status, error, value, webresource } = await response.json(); if (status === "FAILED") { throw new Error(`Conversion failed: ${error}`); } else if (status === "COMPLETED") { const scripts = webresource.superbatch.tags.all + webresource.tags.all; const html = value.replace("</head>", `${scripts}</head>`); return html; } // Keep polling until completed return fetchConvertedMacroBody(id); } const htmlBody = await fetchConvertedMacroBody(asyncId);
Finally, we can directly render the HTML.
For UI Kit, we can render the HTML in a Frame
component.
See Frame for more details.
First define the Frame
in the resources
section of the manifest.yml
file.
See an example manifest structure here.
1 2resources: ... - 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 2import { events } from "@forge/bridge"; events.on("PROPS", ({ html }) => { // Create a fragment, allowing any embedded content scripts to be executed when appended const documentFragment = document .createRange() .createContextualFragment(html); document.body.innerHTML = ""; document.body.appendChild(documentFragment); });
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 2import { events } from "@forge/bridge"; import { Frame } from "@forge/react" const App = () => { useEffect(() => { events.emit("PROPS", { html: htmlBody }); }, [htmlBody]); return <Frame resource="html-frame" /> }
Additionally, if we want to render embedded content, we must specify the following permissions in the manifest.yml
file:
1 2permissions: content: styles: - "unsafe-inline" # Required for styled_view inline styles in the converted HTML scripts: - "unsafe-inline" # Rendering embedded content when converted to HTML with expand external: fetch: client: - "*.atlassian.net" # Embedded content can call back to Atlassian sites images: - "*" # Required for images in the converted HTML styles: # Rendering embedded content when converted to HTML with expand - "*.atl-paas.net" - "*.cloudfront.net" scripts: # Rendering embedded content when converted to HTML with expand - "*.atl-paas.net" - "*.cloudfront.net"
adfExport
functionWhen the page is exported to PDF, Word, or viewed in the page history, you can specify how the macro 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:
See the full adfExport
tutorial here.
1 2import { 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 ) }
Issue | Solution |
---|---|
Macro does not have a body |
|
Body is not set when calling view.submit() |
|
Body not present in the app context |
|
Body ADF not rendered at all or not rendered correctly |
|
Cannot upgrade a Connect bodied macro |
|
Body is not exported as expected |
|
Rate this page: