Forge app development. Part 2. Creating and deleting notes

Anton Chemlev - Toolstrek -
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
September 9, 2020

Creating new note

We must store our notes somewhere, so lets enchance our index.jsx file. There are 2 main possibilities to store data in Jira Forge app: Storage API and useIssuePropertyHook, we'll use second. First of all, install new dependency from your app directory: npm i @forge/ui-jira

And then update file in this way:

import ForgeUI, {render, IssueActivity} from '@forge/ui'
import NotesList from './components/NotesList'
import {useIssueProperty} from '@forge/ui-jira'

const App = () => {    
    
const [notes, setNotes] = useIssueProperty('issueNotes', []])     
    
return (<NotesList notes={notes}/>)
}

export const run = render(
    <IssueActivity>        
  <App/>    
  </IssueActivity>
);

 

Our app will be built successfully (if you run forge tunnel previously), but warning appeared in console:

/app/src/index.jsx3:8     warning  Jira UI hook: "useIssueProperty" requires the "read:jira-work" scope  permission-scope-required 3:8     warning  Jira UI hook: "useIssueProperty" requires the "write:jira-work" scope  permission-scope-required ⚠ 2 problems (0 errors, 2 warnings)  Run forge lint --fix to automatically fix 0 errors and 2 warnings.

 Let's run what Forge suggests. After this check your manifest changes. Read more about scopes here. If you run your app now, you'll set that no notes present for now. Okay, we need a form for creating new notes.

NewNoteForm

Go to our components folder and create new file NewNoteForm.js:

import ForgeUI, {ModalDialog, Form, TextArea} from'@forge/ui'

const NewNoteForm = ({show, submit}) => (
  <ModalDialog header='New Note'onClose={() => show(false)}>
  <Form onSubmit={submit}>
  <TextArea name='text'label=''placeholder='Type your note...'/>
  </Form>
  </ModalDialog>
)

exportdefaultNewNoteForm

 

Pay attention to two functions we must provide to form. Next, create them in index.jsx and add storage facilities with control button:

import ForgeUI, {render, IssueActivity, useState, useProductContext, Fragment, Button, ButtonSet} from '@forge/ui' 
import {useIssueProperty} from '@forge/ui-jira'
import NotesList from './components/NotesList'
import NewNoteForm from "./components/NewNoteForm"

const App = () => {
const {accountId} = useProductContext()
const [notes, setNotes] = useIssueProperty('issueNotes', [])
const [showCreate, setShowCreate] = useState(false)
const createNote = async formData => {
const text = formData.text
const updatedNotes = [...notes, {accountId, text}] s
setShowCreate(false)
await setNotes(updatedNotes)
}

return (
<Fragment>
<NotesList notes={notes}/>
<ButtonSet>
<Button text="Create" onClick={() => setShowCreate(true)}/>
</ButtonSet> {showCreate && <NewNoteForm show={setShowCreate} submit={createNote}/>}
</Fragment>
)
}

export const run = render(
<IssueActivity>
<App/>
</IssueActivity>
);

 

Our createNote function is async and we use await before setNotes function. This guarantees that we can see out new note right after its creation.

Time to test!

Go to your development instance, open issue and click Notes tab. You'll see Create button! Click it, and enter value in form:

4.pngClick Submit button and voila:

5.png

Deleting notes

Great, we can add notes. Now let's delete them.Unfortunately, we have no ability to pick events on Forge components as we use to do in browser environment, because Forge is fully rendered in server. Solution is to create a form where we can choose note to delete.

DeleteNoteForm

Go to our components folder and create new file DeleteNoteForm.js:

import ForgeUI, {Form, Fragment, ModalDialog, Select, Option} from '@forge/ui'

const DeleteNoteForm = ({show, submit, notes}) => {
const options = notes.map(note => <Option label={note.text} value={note.text}/>)

return (
<Fragment>
<ModalDialog header='Delete Note' onClose={() => show(false)}> <Form onSubmit={submit}>
<Select label='' name='text'> {options} </Select>
</Form>
</ModalDialog>
</Fragment>
)
}

export default DeleteNoteForm

 

This form is similar to creation form with little difference - we use Select. 

Let's edit our index.jsx in order to use this shiny form:

import ForgeUI, {render, IssueActivity, useState, useProductContext, Fragment, Button, ButtonSet} from '@forge/ui' 
import {useIssueProperty} from '@forge/ui-jira'
import NotesList from './components/NotesList'
import NewNoteForm from "./components/NewNoteForm"
import DeleteNoteForm from "./components/DeleteNoteForm"

const App = () => {
const {accountId} = useProductContext()
const [notes, setNotes] = useIssueProperty('issueNotes', [])
const [showCreate, setShowCreate] = useState(false)
const [showDelete, setShowDelete] = useState(false)

const createNote = async formData => {
const text = formData.text
const updatedNotes = [...notes, {accountId, text}]
setShowCreate(false)
await setNotes(updatedNotes)
}

const deleteNote = async formData => {
const text = formData.text
const updatedNotes = notes.filter(note => note.text !== text)
setShowDelete(false)
await setNotes(updatedNotes)
}

return (
<Fragment>
<NotesList notes={notes}/>
<ButtonSet>
<Button text="Create" onClick={() => setShowCreate(true)}/>
<Button text="Delete" onClick={() => setShowDelete(true)}/>
</ButtonSet>
{showCreate && <NewNoteForm show={setShowCreate} submit= {createNote}/>}
{showDelete && <DeleteNoteForm show={setShowDelete} submit={deleteNote} notes={notes}/>}
</Fragment>
)
}

export const run = render(
<IssueActivity>
<App/>
</IssueActivity>
);

Ready for testing! 

We'got new button - Delete. Click it:

6.png

Choose our note and press Submit. It's gone forever.

Improving app (it's up to you)

App implementation is rather simplistic. The main drawback is that we can't uniquely identify our notes. And if you have notes with identical text - all of them will be gone on deletion of one.

Also, we store data as entity properties and they are available to all apps. What we can do is to use Storage API.

The last but not the least is code duplication. Compare deleteNote and createNote functions, do you see they are similar? It is possible to abstract with updateNote function with additional function as a param.

There is a room for improvement, i leave it to you as an exercise.

Next

In next part of this tutorial we'll dig in deployment of our app with BitBucket Pipelines.

5 comments

Comment

Log in or Sign up to comment
Anton Chemlev - Toolstrek -
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
September 9, 2020

Part 1  is here

Anton Chemlev - Toolstrek -
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
September 14, 2020

Part 3  added

M Amine
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
March 11, 2021

Excellent @Anton Chemlev - Toolstrek - . Thank you.

Arthur
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
March 17, 2023
I get an error that notes is undefined.
But it should be at least an empty array no?..
const App = () => {

const { accountId } = useProductContext();

const [notes, setNotes] = useIssueProperty("issueNotes", []);

const [isShowCreateDialog, setShowCreate] = useState(false);

console.log("notes: ", notes);
-> generates the output: notes: undefined
I have added the needed scopes and also accepted the permission in jira, I ran install --upgrade, etc. Also installed latest forge cli.
Arthur
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
March 17, 2023

I could solve this issue by putting notes={notes || []} in the props for the component, so when notes isnt initialised yet I pass an empty array, which should be the default behaviour of the useState anyway.. so again the forge framework fails with basic features.

Arthur
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
March 17, 2023

After deletion of a note I get an error:

ERROR 16:01:30.296 b32bcd86d71bd6a1 Cannot read property 'prop' of undefined

:(

TAGS
AUG Leaders

Atlassian Community Events