Components

Rate this page:

This page 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.

Comparison with UI Kit

UI Kit 2 offers enhanced features and capabilities compared to UI Kit. While much of the functionality remains similar, variations in the underlying architecture require you to make some modifications to adapt a UI kit app for compatibility with UI Kit 2.

Component APIs

Most UI Kit 2 components share the same API as their UI kit counterparts. The exceptions for some components are as follows:

FormCondition does not exist

The FormCondition component is no longer used in UI Kit 2. The same conditional rendering can be achieved with onChange handlers and state variables.

UI kit

1
2
return (
  <Form onSubmit={(data) => console.log(data)}>
    <CheckboxGroup label="More options" name="JQLOption">
      <Checkbox label="Run a JQL Query" value="true" />
    </CheckboxGroup>
    <FormCondition when="JQLOption" is={['true']}>
      <TextField label="JQL Query" name="query" />
    </FormCondition>
  </Form>
);

UI Kit 2

1
2
const [jqlOptionChecked, setJqlOptionChecked] = useState(true);

return (
  <Form onSubmit={(data) => console.log(data)}>
    <Checkbox
      label="Run a JQL Query"
      name="JQLOption"
      value={jqlOptionChecked}
      onChange={(value) => setJqlOptionChecked(value)}
    />
    {jqlOptionChecked && (
      <TextField label="JQL Query" name="query" />  
    )}
  </Form>
);

Text property no longer accepted by some components

Button, Code, StatusLozenge, and Tag components no longer accept a text property in UI Kit 2. The text content is placed in children instead. See the following examples for the Button and StatusLozenge components.

UI kit

1
2
<Button text="Click me!" onClick={() => console.log('Button clicked')} />
<StatusLozenge text="In Progress" appearance="inprogress" />

UI Kit 2

1
2
<Button onClick={() => console.log('Button clicked')}> 
 Click me!
</Button> 

<StatusLozenge appearance="inprogress">
  In Progress
</StatusLozenge> 



Hooks

The hooks in UI kit do not exist in the @forge/react package. The useState and useEffect hooks should be imported from the react package instead (see Hooks API Reference – React).

The useProductContext, useContentProperty and useIssueProperty hooks have no equivalent helper function. They must be retrieved with @forge/bridge instead. For more information on integrating and utilizing the custom UI bridge, see Custom UI bridge documentation.

UI kit

1
2
import ForgeUI, { useState, useEffect, Text } from '@forge/ui';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <Text>Count: {count}</Text>
  );
}

UI Kit 2

1
2
import React, { useState, useEffect } from 'react';
import { Text } from '@forge/react';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <Text>Count: {count}</Text>
  );
};

useProductContext

UI kit

1
2
import ForgeUI, { useProductContext, Text, Macro } from '@forge/ui';

const App = () => {
  const context = useProductContext();
  return <Text>All info about my context: {JSON.stringify(context)}</Text>;
};

UI Kit 2

1
2
import React, { useState, useEffect } from 'react';
import { Text } from '@forge/react';
import { view } from '@forge/bridge';

const App = () => {
  const [context, setContext] = useState(undefined);

  useEffect(() => {
    view.getContext().then(setContext)
  }, []);

  return (
    context ? (
      <Text>All info about my context: {JSON.stringify(context)}</Text>
    ) : (
      <Text>Fetching context...</Text>
    )
  );
};

useContentProperty

You must make a Confluence Cloud API request to get and set content properties in UI Kit 2. First, retrieve the content id from the product context. You can then use the requestConfluence method on @forge/bridge to make the request. Refer to Confluence Cloud REST API for the API documentation.

1
2
import React, { useState, useEffect } from 'react';
import { Text } from '@forge/react';
import { view, requestConfluence } from '@forge/bridge';

const App = () => {
  const [context, setContext] = useState(undefined);
  const [contentProperty, setContentProperty] = useState(undefined);

  useEffect(() => {
    view.getContext().then(setContext)
  }, []);
  
  const myPropertyKey = 'my-property';
  
  useEffect(() => {
    const contentId = context?.extension?.content?.id;
    if (contentId) {
      // Get content property
      requestConfluence(`/wiki/rest/api/content/${contentId}/${myPropertyKey}`, {
        headers: {
          'Accept': 'application/json'
        }
      })
      .then((response) => response.json())
      .then((response) => setContentProperty(response.value))
    }
  }, [context]);

  return (
    <Text>{JSON.stringify(contentProperty)}</Text>
  );
};

useIssueProperty

The same approach as above is needed for useIssueProperty. Get the issue id from the product context object, and then call the Jira Cloud API with the requestJira method on @forge/bridge. Refer to The Jira Cloud platform REST API for the Jira API documentation.

1
2
import React, { useState, useEffect } from 'react';
import { Text } from '@forge/react';
import { view, requestJira } from '@forge/bridge';

const App = () => {
  const [context, setContext] = useState(undefined);
  const [issueProperty, setIssueProperty] = useState(undefined);

  useEffect(() => {
    view.getContext().then(setContext)
  }, []);
  
  const myPropertyKey = 'my-property';
  
  useEffect(() => {
    const issueId = context?.extension?.issue?.id;
    if (issueId) {
      // Get issue property
      requestJira(`/rest/api/3/issue/${issueId}/properties/${myPropertyKey}`, {
        headers: {
          'Accept': 'application/json'
        }
      })
      .then((response) => response.json())
      .then((response) => setIssueProperty(response.value))
    }
  }, [context]);

  return (
    <Text>{JSON.stringify(issueProperty)}</Text>
  );
};



API calls

The @forge/api package is designed to run in a Forge function (for example, UI kit app, Custom UI resolver), and so will not work in the app frontend in UI Kit 2.

Product APIs

To make requests to product APIs, use the requestJira and requestConfluence methods exported from @forge/bridge instead.

UI kit

1
2
await api.asUser()
  .requestJira(route`/rest/api/3/issue/${issueKey}/watchers`, {
      method: "POST",
      headers: {
          "Content-Type": "application/json",
          "Accept": "application/json"
      },
      body: JSON.stringify(`${accountId}`)
  });

UI Kit 2

1
2
await requestJira(route`/rest/api/3/issue/${issueKey}/watchers`, {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Accept": "application/json"
    },
    body: JSON.stringify(`${accountId}`)
});

Storage API

There is no helper method exported from @forge/bridge to call the Storage API directly. You must instead set up a Custom UI resolver that can call the Storage API, and invoke the resolver from your app frontend.

UI kit

1
2
import { storage } from '@forge/api';

const App = () => {
  const [storageValue, setStorageValue] = useState(undefined);

  return (
    <>
      <Button text="Get value" onClick={async () => {
        const value = await storage.get('example-key');
        setStorageValue(value);
      }}
      <Text>Value: {storageValue}</Text>
    </>
  );
};

UI Kit 2

frontend/hello-world/src/App.js

1
2
import React, { useState, useEffect } from 'react';
import { Text } from '@forge/react';
import { invoke } from '@forge/bridge';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <Button text="Get value" onClick={async () => {
        const value = await invoke('getStorageValue');
        setStorageValue(value);
      }}
      <Text>Value: {storageValue}</Text>
    </>
  );
};

src/index.js

1
2
import Resolver from "@forge/resolver";
import { storage } from '@forge/api';

const resolver = new Resolver();

resolver.define("getStorageValue", async ({ payload, context }) => {
  const value = await storage.get('example-key');
  return value;
});

export const handler = resolver.getDefinitions();



MacroConfig

Define configuration options for a macro in UI Kit 2 the same way as in UI kit. Add configuration to a macro). It still requires specifying the config entry point in the manifest, but now the macro module replaces the function property with resource and adds the render: native property, indicating that it is a UI Kit 2 module.

UI Kit 2

1
2
modules:
  macro:
    - key: pet-facts
      resource: main # <---- replaces function: main
      render: native # <---- defines this module as a UI Kit 2 module
      title: Pet
      description: Inserts facts about a pet
      config:
        function: config-function-key
  function:
    - key: config-function-key
      handler: index.config
resources:
  - key: main
    path: frontend/hello-world/src/index.js
app:
  id: "<your app id>"
  name: pet-facts-macro

The config form is still defined with the MacroConfig component exported from @forge/ui.

Identical in UI kit and Custom UI

1
2
// UI Kit 2 still requires the @forge/ui package to define macro config
import ForgeUI, { render, MacroConfig, TextField } from "@forge/ui";

const defaultConfig = {
  name: "Unnamed Pet",
  age: "0"
};

const Config = () => {
  return (
    <MacroConfig>
      <TextField name="name" label="Pet name" defaultValue={defaultConfig.name} />
      <TextField name="age" label="Pet age" defaultValue={defaultConfig.age} />
    </MacroConfig>
  );
};

export const config = render(<Config />);

The key difference is how to read your app's config values. You can get the config object directly in UI kit by calling the useConfig hook.

A UI Kit 2 must read the app's config values asynchronously from the product context. The product context can be retrieved by calling view.getContext(). Note that the shape of the context object returned by the view.getContext differs slightly from the shape returned by UI kit’s useProductContext hook. See Custom UI view for the function signature.

UI kit

1
2
import ForgeUI, { useConfig, Text } from '@forge/ui';

function App() {
  const config = useConfig();

  return (
    <Text>
      Config: {config.age}
    </Text>
  );
}

UI Kit 2

1
2
import React, { useEffect, useState } from 'react';
import { view } from '@forge/bridge';
import { Text } from '@forge/react';

function App() {
  const [productContext, setProductContext] = useState({});
  
  useEffect(() => {
    view.getContext().then(setProductContext);
  }, []);

  return (
    <Text>
      Config: {productContext?.extension?.config?.age}
    </Text>
  );
}

Logging and debugging

Due to architectural differences, the approach for logging and debugging in UI Kit 2 is different.

As UI kit runs on the server-side, any console.log statement in your app will be stored and available for viewing through the forge logs CLI command.

However, in UI Kit 2, because the app runs directly on the browser, console.log statements won't be stored and will only be available for visualization directly in your browser's developer console. If you need to log and store information, you can invoke a Resolver function and utilize console.log statements.

Rate this page: