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
.jsx
3: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.
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>
)
export
default
NewNoteForm
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:
Click Submit button and voila:
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.
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:
Choose our note and press Submit. It's gone forever.
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.
In next part of this tutorial we'll dig in deployment of our app with BitBucket Pipelines.
Part 3 added
Excellent @Anton Chemlev - Toolstrek - . Thank you.
const App = () => {
const { accountId } = useProductContext();
const [notes, setNotes] = useIssueProperty("issueNotes", []);
const [isShowCreateDialog, setShowCreate] = useState(false);
console.log("notes: ", notes);
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.
After deletion of a note I get an error:
ERROR 16:01:30.296 b32bcd86d71bd6a1 Cannot read property 'prop' of undefined
:(