Last updated Apr 16, 2024

Migrate an app from UI Kit 1 to UI Kit

In this tutorial, we’re going through the steps of migrating a Jira issue translator app built with UI Kit 1 to the latest version of UI Kit. We’ll be creating a new UI Kit app and migrate the code from the existing UI Kit 1 app to the new version. We'll also be using this sample UI Kit 1 app.

Before you begin

Make sure to do the following before you start migrating your app:

  1. Update the Forge CLI to the latest version by running the following command:

    npm install -g @forge/cli@latest

  2. Create a new UI Kit app by running the following command:

    forge create

    For our app, the following options are used:

    1
    2
     Name: forge-ui-kit-translate
    
     Category: UI Kit
    
     Template: jira-issue-panel
    

    This is the latest version of UI Kit. The UI Kit 1 option has been removed from the latest version of the CLI.

  3. Update the manifest to include the same permissions as the existing UI Kit 1 app by running the following command:

    1
    2
    permissions:
      scopes:
        - "read:jira-work"
      external:
        fetch:
          backend:
            - "https://api.cognitive.microsofttranslator.com"
    

Migrate the UI

Go into src/frontend/index.jsx. The initial template looks like this:

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

const App = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    invoke('getText', { example: 'my-invoke-variable' }).then(setData);
  }, []);

  return (
    <>
      <Text>Hello world!</Text>
      <Text>{data ? data : 'Loading...'}</Text>
    </>
  );
};

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

This file holds all of the app’s frontend logic. We’ll be adding our UI Kit components here.

Copy over the existing app’s UI code and update UI Kit 1 components to the latest UI Kit components. The migrated code should now look like this:

1
2
import React, { Fragment, useState } from "react";
import ForgeReconciler, { ButtonGroup, Button, Text } from "@forge/react";
import { invoke } from "@forge/bridge";

const LANGUAGES = [
  ["🇯🇵 日本語", "ja"],
  ["🇰🇷 한국어", "ko"],
  ["🇬🇧 English", "en"],
];

const App = () => {
  const [translation, setTranslation] = useState(null);

  const setLanguage = async (countryCode) => {
    // will add in the next steps
  };

  return (
    <Fragment>
      <ButtonGroup>
        {LANGUAGES.map(([label, code]) => (
          <Button
            key={code}
            onClick={async () => {
              await setLanguage(code);
            }}
          >
            {label}
          </Button>
        ))}
      </ButtonGroup>
      {translation && (
        <Fragment>
          <Text>**{translation.summary}**</Text>
          <Text>{translation.description}</Text>
        </Fragment>
      )}
    </Fragment>
  );
};

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

Note the differences from the original UI Kit 1 code:

  • @forge/react is the new library holding all UI Kit components.
  • Usage of the Fragment component should be from the react library. The shorthand syntax <></> can also be used instead and doesn’t require an additional import.
  • Several updates to component APIs have been made between UI Kit 1 and the latest version of UI Kit. The full list of changes can be found here. In this example, we’ve had to:
    • Replace ButtonSet with ButtonGroup
    • Pass the text content in Button and Text as children of the component.

Use Forge resolvers

You’ll notice the setLanguage logic is missing in this file. This is due to the usage of the @forge/api package. @forge/api can only be used in a Forge resolver function, as it is a Node package and is incompatible with the frontend. Forge resolvers are a series of backend functions for your app that can use backend packages, such as @forge/api. These resolvers can then be invoked from the frontend.

To add this, go into src/resolvers/index.js, and copy over the setLanguage function into the resolver.

1
2
import Resolver from "@forge/resolver";
import api, { route } from "@forge/api";

async function checkResponse(apiName, response) {
  if (!response.ok) {
    const message = `Error from ${apiName}: ${
      response.status
    } ${await response.text()}`;
    console.error(message);
    throw new Error(message);
  } else if (process.env.DEBUG_LOGGING) {
    console.debug(`Response from ${apiName}: ${await response.text()}`);
  }
}

const TRANSLATE_API =
  "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0";

const resolver = new Resolver();

resolver.define("setLanguage", async ({ context, payload }) => {
  const countryCode = payload.countryCode;
  const issueKey = context.extension.issue.key;

  // Fetch issue fields to translate from Jira
  const issueResponse = await api
    .asApp()
    .requestJira(
      route`/rest/api/2/issue/${issueKey}?fields=summary,description`
    );
  await checkResponse("Jira API", issueResponse);
  const { summary, description } = (await issueResponse.json()).fields;

  // Translate the fields using the Azure Cognitive Services Translatioon API
  const translateResponse = await api.fetch(
    `${TRANSLATE_API}&to=${countryCode}`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=UTF-8",
        // See README.md for details on generating a Translation API key
        "Ocp-Apim-Subscription-Key": process.env.TRANSLATE_API_KEY,
        "Ocp-Apim-Subscription-Region": process.env.TRANSLATE_API_LOCATION,
      },
      body: JSON.stringify([
        { Text: summary },
        { Text: description || "No description" },
      ]),
    }
  );
  await checkResponse("Translate API", translateResponse);
  const [summaryTranslation, descriptionTranslation] =
    await translateResponse.json();

  // Update the UI with the translations
  return {
    to: countryCode,
    summary: summaryTranslation.translations[0].text,
    description: descriptionTranslation.translations[0].text,
  };
});

export const handler = resolver.getDefinitions();

Important things to note:

  • The first parameter of resolver.define is a string that we will use to invoke this resolver.
  • The second parameter holds an object that containing the context object of an extension point and payload of the function invocation.

Back in src/frontend/index.jsx, add the following to invoke your new resolver:

1
2
const setLanguage = async (countryCode) => {
  const resp = await invoke("setLanguage", { countryCode });
  setTranslation(resp);
};

That's it! To test, set up Forge variables, then deploy and install the app in to your instance.

You can find the migrated sample app here.

Rate this page: