Last updated Nov 21, 2024

Build an App to Customise Jira Issues

This tutorial will walk you through creating a more complex Forge app for Jira using UI Kit.

The tutorial will cover the following Forge concepts:

  • Modules: jira-issue-panel
  • UI Kit: Box, Button, DynamicTable, Inline, Text, Textfield, xcss
  • Storage: Key-Value Store
  • Forge Bridge: invoke

The Jira Expense Tracker app

The Jira expense tracker app is designed to show you how to create an app that allows a user to create, edit and delete items in a to do list, the to do list items are stored and retreived using the Forge Storage API, and displayed using a Dynamic Table.

The Jira To Do List App

About the tutorial

Approximate time needed: 30-40 minutes

Suggested Skills: Basic understanding of Javascript, React is recommended.

In part one you will create your Forge UI kit app using the Jira issue panel template. You will then customised the app to display a list of expenses, and their total using test data.

Then in part two, you will make changes to your app to store and retrieve data created by the user within Forge storage.

Finally, in part three, you will modify the app to update and display (in addition to storing and retrieving) data created by the user within Forge storage.

Along the way, you'll find extra tips, links and resources that will allow you to learn more about what was covered in each section, and become familiar with how resources are laid out in the Forge documentation.

Before you begin

We recommend completing Forge Quest in order as each section is designed to build on the previous one.

If you haven't completed the Forge Novices section, it is recommended you complete that before starting this tutorial.

Create your app

Create an app based on the Jira Issue Panel template.

  1. Create your app by running:

    1
    2
    forge create
    
  2. Enter a name for your app (up to 50 characters). For example jira-issue-expenses.

  3. Select the UI Kit category.

  4. Select Jira from the list of products.

  5. Select the jira-issue-panel template.

  6. Change to the app subdirectory to see the app files:

    1
    2
    cd jira-issue-expenses
    

About the jira-issue-panel template

The app we'll create will create a Jira Issue Panel, which can be added to the new Jira Issue View in Jira Work Management, Jira Software and Jira Service Management.

Deploy and install your app

  1. Navigate to the app's top-level directory and deploy your app by running:
    1
    2
    forge deploy
    
  2. Install your app by running:
    1
    2
    forge install
    
  3. Select jira using the arrow keys and press the enter key.
  4. Enter the URL for your Atlassian development site. For example example.atlassian.net. You can view a list of your active sites at Atlassian administration

Once the install complete message appears, your app is installed and ready to use on the site specified. You can delete your app from the site by running the forge uninstall command.

View your app

Now that your app is installed, it's time to see it in action in your Jira site.

  1. Open your jira site (example.atlassian.com) and open a Jira issue.
  2. Depending on what type of project you open, you'll see the Jira Issue Panel Apps in two possible places:
    • Under the Issue Summary, click on the Apps button, and select your app (jira-issue-expenses) from the list; or
    • Under the Issue Summary, click on your app (jira-issue-expenses), or click on the ... and then select your app (jira-issue-expenses) from the list.
  3. Your app will now appear under the Issue Description, it will display 'Hello World' twice.

Add a table with some test data

In this section, you'll make some changes to the src/frontend/index.jsx to display some test data.

This data will be displayed using a Dynamic table.

  1. Start your tunnel by running
    1
    2
    forge tunnel
    
  2. Navigate to the src/frontend directory.
  3. Create a new file called data.jsx with the following content (this will be our temporary display data until the storage is up and running):
    1
    2
    export const conferenceExpenses = [
      {
        id: 1,
        description: "Hotel",
        amount: 310,
      },
      {
        id: 2,
        description: "Lunch",
        amount: 19,
      },
      {
        id: 3,
        description: "Uber",
        amount: 15.90,
      },
      {
        id: 4,
        description: "Dinner",
        amount: 35.00,
      }
    ];
    
  4. Open the index.jsx file. The default content of the file is shown below:
    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>
    );
    
  5. Modify the import ForgeReconciler, { Text } from '@forge/react' line to include all of the components listed above: import ForgeReconciler, {Button, DynamicTable, Inline, Lozenge, Text, Textfield} from '@forge/react';
  6. Add another import statement below the others to make the temporary data accessible: import { conferenceExpenses } from './data';
  7. Modify the App return statement to display a dynamic table,
    1
    2
     return (
       <>
         <DynamicTable 
           caption="Expenses"
           rows={fillTable(conferenceExpenses)} />
       </>
     );     
    
  8. Above the return statement, create the fillTable function:
    1
    2
      const fillTable = ( expenses ) => {
        console.log(expenses)
        if (expenses.length > 0) {
          const rows = expenses.map((item) => ({
            cells: [
              {
                content: <Text>{item.description}</Text>,
              },
              {
                content: <Text>{item.amount}</Text>,
              },
            ],
          }))
        return rows;
        }
        else return null;
      }
    
  9. Save the changes you made to index.jsx.
  10. Once the tunnel has completed its reload, refresh your Jira issue to see the changes.
  11. Your app will display a table, with a list of tasks, and their corresponding statuses. Now you'll improve the appearance of the table with some of the other components mentioned earlier.

Improve the appearance of the table

In this section you will improve the table, making it look more like a to do list, using the following elements:

Under the table Text and a Button will be displayed.

You will also use the Inline component to control the way the elements are displayed.

These elements will not be wired up to change any data yet, but it will show how the final app will look.

  1. Open the index.jsx if you closed it
  2. In the fillTable function:
    1. Replace content: <Text>{item.description}</Text>, with a Textfield so that later the user will be able to edit the to do items:
      1
      2
        content: <Textfield appearance="subtle"  spacing="compact" id="expense-description" defaultValue={item.description}/>,
      
    2. Replace content: <Text>{item.amount}</Text>, with a Textfield so that later the user will be able to edit the to do items:
      1
      2
        content: <Textfield appearance="subtle"  spacing="compact" id="expense-amount" defaultValue={item.amount}/>,
      
    3. Below the existing two cells, add a new third cell:
      1
      2
          { 
            content: <Button appearance="subtle" iconBefore="trash" spacing="compact"/>
          },
      
  3. Now, under the fillTable function, create a new function called getTotal to determine the total of the expenses in the table:
    1
    2
      const getTotal = (expenses) => {
        let total = 0;
        expenses.forEach(expense => {
          total += expense.amount;
        });
        return total;
      }
    
  4. Finally, add the total, and a 'Delete All' button in the return statement for the App. The button should appear below the table, use the inline component so that it appears inline with the status lozenge at the bottom right of the table:
    1
    2
      return (
        <>
          <DynamicTable 
            caption="Expenses"
            rows={fillTable(conferenceExpenses)} />
          <Inline spread='space-between'>
            <Text>Total:  ${getTotal(conferenceExpenses)}</Text>
            <Button>Delete All</Button>
          </Inline>
        </>
    );
    
  5. Once you're happy with your changes, close the tunnel using Control + c and deploy your changes,
    1
    2
      forge deploy
    

Your src/frontend/index.jsx should look like this:

1
2
import React, { useEffect, useState } from 'react';
import ForgeReconciler, {Button, DynamicTable, Inline, Text, Textfield} from '@forge/react';
import { invoke } from '@forge/bridge';
import { conferenceExpenses } from './data';

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

  const fillTable = ( expenses ) => {
    console.log(expenses)
    if (expenses.length > 0) {
      const rows = expenses.map((item) => ({
        cells: [
          {
            content: <Textfield appearance="subtle"  spacing="compact" id="expense-description" defaultValue={item.description}/>,
          },
          {
            content:  <Textfield appearance="subtle"  spacing="compact" id="expense-amount" defaultValue={item.amount}/>,
          },
          { 
            content: <Button appearance="subtle" iconBefore="trash" spacing="compact"/>
          },
        ],
      }))
    return rows;
    }
    else return null;
  }

  const getTotal = (expenses) => {
    let total = 0;
    expenses.forEach(expense => {
      total += expense.amount;
    });
    return total;
  }

  return (
    <>
      <DynamicTable 
        caption="Expenses"
        rows={fillTable(conferenceExpenses)} />
      <Inline spread='space-between'>
        <Text>Total:  ${getTotal(conferenceExpenses)}</Text>
        <Button>Delete All</Button>
      </Inline>
    </>
  );
};

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

Next step

In the next step you will modify the app to store and retrieve data using the Forge Storage API.

If you found this part of the tutorial helpful, please consider providing feedback using rate this page below!

Rate this page: