Rate this page:
This tutorial describes how to build an app for sharing definitions for terminology and acronyms across an entire Confluence site. The app uses the App storage API to store definitions, which allows them to be shared between several macros and accessed from the site administration.
In this tutorial you will learn how to persist and retrieve data from the App Storage service and display the results in a table.
This tutorial has an accompanying Bitbucket repository. You'll find a link to a git tag at the end of each step which you can use to compare with your code or to skip ahead.
To complete this tutorial, you need the following:
npm install -g @forge/cli@latest
on the command line.See Set up Forge for step-by-step instructions. It is recommended you complete all the steps in Getting started so that you're familiar with the Forge development process.
Create an app based on the Hello world template. Using your terminal complete the following:
Navigate to the directory where you want to create the app.
Create your app by running:
1 2forge create
Enter a name for the app. For example, definitions-macro.
Select the UI kit category.
Select the confluence-macro template.
Your app has been created in a directory with the same name as your app, for example definitions-macro. Open the app directory to see the files associated with your app.
Install the latest version of the Forge API package
1 2npm install --save @forge/api@latest
Enable the storage API by adding the storage:app
scope to the mainifest.yml
file. [Learn more about adding scopes to call an Atlassian REST API]
(/platform/forge/add-scopes-to-call-an-atlassian-rest-api/).
1 2permissions: scopes: - storage:app
You can check your app against step 1 in the tutorial repository.
Navigate to the app's top-level directory and deploy your app by running:
1 2forge deploy
Install your app by running:
1 2forge install
Select your Atlassian product using the arrow keys and press the enter key.
Enter the URL for your development site. For example, example.atlassian.net. View a list of your active sites at Atlassian administration.
Once the successful installation message appears, your app is installed and ready
to use on the specified site.
You can always delete your app from the site by running the forge uninstall
command.
Running the forge install
command only installs your app onto the selected product.
To install onto multiple products, repeat these steps again, selecting another product each time.
Note that the Atlassian Marketplace
does not support cross-product apps yet.
You must run forge deploy
before running forge install
in any of the Forge environments.
Start tunneling your app
1 2forge tunnel
Add your new Confluence macro to a page and verify you can see the invocation in the logs.
You can check your app against step 2 in the tutorial repository.
Add a new component named Config
containing a ConfigForm
and use the component in the config
property of the macro. This configuration form allows a user of the macro to specify a set of definitions to show in a table.
1 2const Config = () => ( <ConfigForm> <TextArea label="Terms to include (one per line)" name="terms" isRequired /> </ConfigForm> ); export const run = render(<Macro app={<App />} config={<Config />} />);
Add the useConfig
hook to the App
component. This hook allows the app
to access configuration set by the user in the macro configuration dialog.
If there is no list of terms set the macro renders a blank state.
1 2const App = () => { const config = useConfig(); const terms = config?.terms?.split("\n"); const definitions = []; if (!config || !config.terms) { return ( <Fragment> <Text>No terms</Text> </Fragment> ); } return ( <Fragment> <Text>{terms.join(",")}</Text> </Fragment> ); };
You can check your app against step 3 in the tutorial repository.
Your app makes use of the JavaScript API provided in the @forge/api
package to
interact with the App Storage service. This package provides methods for reading,
writing, and querying data within the App Storage service.
This tutorial starts with reading a list of definitions from the storage service. Initially the results are empty, adding definitions is covered in a later stage.
The app stores key entities based on the term, with the following format:
1 2interface Term { definition: string; term: string; }
Create a new file storage.js
for storage-related code.
Create a function getDefinition
to load a definition given a term.
1 2import { storage } from "@forge/api"; import { useConfig, useState } from "@forge/ui"; // Create a key composed from the term function termKey(term) { return `term-${term}`; } export async function getDefinition(term) { // Fetch a definition from the Storage API const value = await storage.get(termKey(term)); // If a value was found, return the definition field if (value) { return value.definition; } }
Create a function getDefinitions
to turn a list of terms into a list of definitions.
1 2export async function getDefinitions(terms) { const pendingDefinitions = terms.map((term) => getDefinition(term)); return await Promise.all(pendingDefinitions); }
This function makes use of the Promise.all
operation to turn a list
of pending promises into a single promise. The resulting promise resolves to an
array of definitions when awaited.
You can compose UI kit hooks together to create custom hooks with a
purpose-built API. The definition management logic is a good candidate for consolidating
into a hook as it will greatly simplify interacting with storage from the UI kit code.
Add a new hook to the storage.js
file named useDefinitions
. This custom hook will compose the
useConfig
and useState
hooks to integrate the storage API with a UI kit macro.
1 2export function useDefinitions() { const config = useConfig() || {}; const terms = config.terms ? config.terms.split("\n") : []; const [definitions] = useState(() => getDefinitions(terms)); return { terms, definitions, }; }
Use this hook API within the App
component in the index.jsx
file to load the
definitions for the provided terms list.
1 2import { useDefinitions } from "./storage"; const App = () => { const { terms, definitions } = useDefinitions(); if (terms.length === 0) { return ( <Fragment> <Text>No terms</Text> </Fragment> ); } return ( <Fragment> <Text>{terms.join(",")}</Text> <Text>{definitions.join(",")}</Text> </Fragment> ); };
You can check your app against step 4 in the tutorial repository.
In this step, you'll add a table to the definitions macro to show the list of terms side by side with a list of definitions.
It can be useful when building a UI kit app to group elements together into reusable components.
For this app, we will add DefinitionTable
, DefinitionHeader
, and DefinitionRow
components.
Create a new file named definition-table.js
.
In this file, add a new component named DefinitionRow
- this component will be used
to render each row in the definitions table.
1 2import ForgeUI, { Button, Cell, Head, Row, Table, Text } from "@forge/ui"; // Definition row renders a single row in the definitions table for a specific term const DefinitionRow = ({ term, definition }) => { // Render a row with the term and definition return ( <Row> <Cell> <Text>{term}</Text> </Cell> <Cell> <Text>{definition}</Text> </Cell> </Row> ); };
Create a component named DefinitionTable
in the same file. This component renders
a list of definitions in a table.
1 2// Render a definitions table given a list of terms and definitions export const DefinitionTable = ({ terms, definitions }) => { // Render a definition row for each term const definitionRows = terms.map((term, i) => ( <DefinitionRow term={term} definition={definitions[i]} /> )); // Return a table with the list of definitions return ( <Table> <Head> <Cell> <Text>Acronym</Text> </Cell> <Cell> <Text>Definition</Text> </Cell> </Head> {definitionRows} </Table> ); };
Add this table to your existing macro in index.js
and add an import for the DefinitionTable
component.
1 2import { DefinitionTable } from "./definition-table"; const App = () => { const { terms, definitions } = useDefinitions(); if (terms.length === 0) { return ( <Fragment> <Text>No terms</Text> </Fragment> ); } return ( <Fragment> <DefinitionTable terms={terms} definitions={definitions} /> </Fragment> ); };
You can check your app against step 5 in the tutorial repository.
At this stage, there's still no data stored for the app. In this step, you'll add the ability to store a definition for a term.
Add a saveDefinition
method to the storage.js
file.
1 2async function saveDefinition(term, definition) { return await storage.set(termKey(term), { term, definition }); }
Add the updateDefinition
method to the useDefinitions
hook. This allows the
macro to save an update to a definition and refresh the UI kit state.
1 2export function useDefinitions() { const config = useConfig() || {}; const terms = config.terms ? config.terms.split("\n") : []; const [definitions, setDefinitions] = useState(() => getDefinitions(terms) ); return { terms, definitions, async updateDefinition(term, definition) { await saveDefinition(term, definition); const i = terms.indexOf(term); if (i >= 0) { definitions[i] = definition; setDefinitions(definitions); } }, }; }
Create a file add-definition.jsx
.
Create a dialog in the add-definition.jsx
file that enables a user to provide
a definition for a term. When the user clicks submit for the dialog, the term
onSaveDefinition
callback is invoked and then the dialog closed.
1 2import ForgeUI, { Form, ModalDialog, TextArea } from "@forge/ui"; export const AddDefinition = ({ term, onClose, onSaveDefinition }) => ( <ModalDialog header={`Add definition for ${term}`} onClose={onClose}> <Form onSubmit={async ({ definition }) => { await onSaveDefinition(term, definition); onClose(); }} > <TextArea name="definition" label={`Definition for ${term}`} /> </Form> </ModalDialog> );
Add a button with the text "Add definition" to the DefinitionRow
component.
This button triggers the AddDefinition
dialog.
1 2const DefinitionRow = ({ term, definition, showAddDefinitionDialog }) => { let definitionContent = null; if (definition) { definitionContent = <Text>{definition}</Text>; } else { definitionContent = ( <Button text="Add definition" onClick={showAddDefinitionDialog} /> ); } return ( <Row> <Cell> <Text>{term}</Text> </Cell> <Cell>{definitionContent}</Cell> </Row> ); };
Add a callback showAddDefinitionDialog
to the DefinitionTable
. This callback
triggers displaying the AddDefinition
dialog for a specific term.
1 2export const DefinitionTable = ({ terms, definitions, showAddDefinitionDialog, }) => { // Render a definition row for each term const definitionRows = terms.map((term, i) => ( <DefinitionRow term={term} definition={definitions[i]} showAddDefinitionDialog={() => showAddDefinitionDialog(term)} /> )); // Return a table with the list of definitions return ( <Table> <Head> <Cell> <Text>Term</Text> </Cell> <Cell> <Text>Definition</Text> </Cell> </Head> {definitionRows} </Table> ); };
Add both the AddDefinition
dialog and useDefinitions
hook to the App
component. The app uses a useState
hook to track the visibility of the edit dialog.
When the dialog invokes the onSaveDefinition
property the associated definition is
stored in the app storage API.
1 2import { AddDefinition } from "./add-definition"; const App = () => { const { terms, definitions, updateDefinition } = useDefinitions(); const [addingDefinitionToTerm, showAddDefinitionDialog] = useState( undefined ); if (terms.length === 0) { return ( <Fragment> <Text>No terms</Text> </Fragment> ); } return ( <Fragment> {!!addingDefinitionToTerm && ( <AddDefinition onClose={() => showAddDefinitionDialog(undefined)} term={addingDefinitionToTerm} onSaveDefinition={updateDefinition} /> )} <DefinitionTable terms={terms} definitions={definitions} showAddDefinitionDialog={showAddDefinitionDialog} /> </Fragment> ); };
Stop tunneling your app and deploy it by running:
1 2forge deploy
You can check your app against step 6 in the tutorial repository.
At this point your app is able display a list of terms and their associated definitions. The app stores definitions in the storage service, and shares these across all the instances of the macro in the site.
Explore the app storage API in further detail over the following pages:
Rate this page: