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:
jira-issue-panelKey-Value StoreinvokeThe 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.
![]()
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.
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 an app based on the Jira Issue Panel template.
Create your app by running:
1 2forge create
Enter a name for your app (up to 50 characters). For example jira-issue-expenses.
Select the UI Kit category.
Select Jira from the list of products.
Select the jira-issue-panel template.
Change to the app subdirectory to see the app files:
1 2cd jira-issue-expenses
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.
1 2forge deploy
1 2forge install
jira using the arrow keys and press the enter key.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.
Now that your app is installed, it's time to see it in action in your Jira site.
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 2forge tunnel
src/frontend directory.data.jsx with the following content (this will be our temporary display data until the storage is up and running):
1 2export 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, } ];
index.jsx file. The default content of the file is shown below:
1 2import 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> );
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';import { conferenceExpenses } from './data';1 2return ( <> <DynamicTable caption="Expenses" rows={fillTable(conferenceExpenses)} /> </> );
fillTable function:
1 2const 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; }
index.jsx.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.
index.jsx if you closed itfillTable function:
content: <Text>{item.description}</Text>, with a Textfield so that later the user will be able to edit the to do items:
1 2content: <Textfield appearance="subtle" spacing="compact" id="expense-description" defaultValue={item.description}/>,
content: <Text>{item.amount}</Text>, with a Textfield so that later the user will be able to edit the to do items:
1 2content: <Textfield appearance="subtle" spacing="compact" id="expense-amount" defaultValue={item.amount}/>,
1 2{ content: <Button appearance="subtle" iconBefore="trash" spacing="compact"/> },
fillTable function, create a new function called getTotal to determine the total of the expenses in the table:
1 2const getTotal = (expenses) => { let total = 0; expenses.forEach(expense => { total += expense.amount; }); return total; }
1 2return ( <> <DynamicTable caption="Expenses" rows={fillTable(conferenceExpenses)} /> <Inline spread='space-between'> <Text>Total: ${getTotal(conferenceExpenses)}</Text> <Button>Delete All</Button> </Inline> </> );
1 2forge deploy
Your src/frontend/index.jsx should look like this:
1 2import 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> );
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: