Forge Developer

Forge Developer

Last updatedJul 27, 2020

Rate this page:

Use the App storage API in a Confluence macro

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.

A definition is added for the term GraphQL

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.

Before you begin

To complete this tutorial, you need the following:

  • The latest version of Forge CLI. To update your CLI version, run npm install -g @forge/cli@latest on the command line.
  • An Atlassian site with Jira and Confluence Cloud where you can install your app.

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.

Step 1: Create your app

Create an app based on the Hello world template. Using your terminal complete the following:

  1. Navigate to the directory where you want to create the app.
  2. Create your app by running:

    1
    forge create
    1. Enter a name for the app. For example, definitions-macro.
    2. Select the UI kit category.
    3. Select the confluence-macro template.
  3. 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.

  4. Install the latest version of the Forge API package

    1
    npm install --save @forge/api@latest
  5. 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.

    1
    2
    3
    permissions:
      scopes:
        - storage:app

You can check your app against step 1 in the tutorial repository.

Step 2: Deploy and install your app

  1. Navigate to the app's top-level directory and deploy your app by running:

    1
    forge deploy
  2. Install your app by running:

    1
    forge install
    1. Select your Atlassian product using the arrow keys and press the enter key.

      Note: 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.

    2. 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.

  1. Start tunnelling your app

    1
    forge tunnel
  2. 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.

Step 3: Add a configuration dialog to the macro

The configuration dialog for the macro

  1. 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
    2
    3
    4
    5
    6
    7
    const Config = () => (
      <ConfigForm>
        <TextArea label="Terms to include (one per line)" name="terms" isRequired />
      </ConfigForm>
    );
    
    export const run = render(<Macro app={<App />} config={<Config />} />);
  2. 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const 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.

Step 4: Fetch a list of definitions from storage

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
2
3
4
interface Term {
  definition: string;
  term: string;
}
  1. Create a new file storage.js for storage-related code.
  2. Create a function getDefinition to load a definition given a term.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import { 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;
      }
    }
  3. Create a function getDefinitions to turn a list of terms into a list of definitions.

    1
    2
    3
    4
    5
    export async function getDefinitions(terms) {
      const pendingDefinitions = terms.map((term) => getDefinition(term));
    
      return await Promise.all(pendingDefinitions);
    }

    Note: 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.

  4. 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    export function useDefinitions() {
      const config = useConfig() || {};
      const terms = config.terms ? config.terms.split("\n") : [];
      const [definitions] = useState(() => getDefinitions(terms));
    
      return {
        terms,
        definitions,
      };
    }
  5. Use this hook API within the App component in the index.jsx file to load the definitions for the provided terms list.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import { 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.

Step 5: Create the definitions table

The app showing configured terms with definitions

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.

  1. Create a new file named definition-table.js.
  2. In this file, add a new component named DefinitionRow - this component will be used to render each row in the definitions table.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import 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>
      );
    };
  3. Create a component named DefinitionTable in the same file. This component renders a list of definitions in a table.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 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>
      );
    };
  4. Add this table to your existing macro in index.js and add an import for the DefinitionTable component.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import { 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.

Step 6: Allow a user to add a definition

The app showing configured terms with a button to add a definition

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.

  1. Add a saveDefinition method to the storage.js file.

    1
    2
    3
    async function saveDefinition(term, definition) {
      return await storage.set(termKey(term), { term, definition });
    }
  2. 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    export 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);
          }
        },
      };
    }
  3. Create a file add-definition.jsx.

  4. 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import 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>
    );
  5. Add a button with the text "Add definition" to the DefinitionRow component. This button triggers the AddDefinition dialog.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const 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>
      );
    };
  6. Add a callback showAddDefinitionDialog to the DefinitionTable. This callback triggers displaying the AddDefinition dialog for a specific term.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    export 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>
      );
    };
  7. 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import { 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>
      );
    };
    1. Stop tunneling your app and deploy it by running:
    1
    forge deploy

You can check your app against step 6 in the tutorial repository.

Next steps

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:

  • Read about the Storage API which details the JavaScript API.
  • Learn more about how Queries can be run against data stored in the app storage API.
  • View the Limits that apply to apps using the app storage API.

Rate this page: