@valiantys/atlassian-app-frontend 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +726 -0
- package/atlassian-app-forge-CTaVjJLt.js +1 -0
- package/atlassian-app-forge-DdtDadi2.mjs +117 -0
- package/atlassian-app-frontend.api.json +8829 -0
- package/atlassian-app-frontend.api.md +740 -0
- package/atlassian-app-standalone-DJOVbfp6.js +1 -0
- package/atlassian-app-standalone-DyyH6WPO.mjs +111 -0
- package/examples/backend/index.ts +44 -0
- package/examples/backend/lib/forge-functions.d.ts +3 -0
- package/examples/backend/lib/forge-functions.ts +23 -0
- package/examples/backend/lib/handler-functions.d.ts +58 -0
- package/examples/backend/lib/handler-functions.ts +72 -0
- package/examples/backend/lib/standalone-functions.d.ts +3 -0
- package/examples/backend/lib/standalone-functions.ts +17 -0
- package/examples/client-sample-.env +5 -0
- package/examples/hello-world/app.d.ts +2 -0
- package/examples/hello-world/app.tsx +77 -0
- package/examples/hello-world/example-box.d.ts +8 -0
- package/examples/hello-world/example-box.tsx +22 -0
- package/examples/hello-world/example-components/forge-storage-example.d.ts +1 -0
- package/examples/hello-world/example-components/forge-storage-example.tsx +66 -0
- package/examples/hello-world/example-components/hello-with-loading-spinner.d.ts +1 -0
- package/examples/hello-world/example-components/hello-with-loading-spinner.tsx +24 -0
- package/examples/hello-world/example-components/hello.d.ts +1 -0
- package/examples/hello-world/example-components/hello.spec.tsx +39 -0
- package/examples/hello-world/example-components/hello.tsx +13 -0
- package/examples/hello-world/example-components/host-router-example.d.ts +1 -0
- package/examples/hello-world/example-components/host-router-example.tsx +13 -0
- package/examples/hello-world/example-components/issue-types-example-backend.d.ts +4 -0
- package/examples/hello-world/example-components/issue-types-example-backend.tsx +37 -0
- package/examples/hello-world/example-components/issue-types-example.d.ts +4 -0
- package/examples/hello-world/example-components/issue-types-example.tsx +40 -0
- package/examples/hello-world/example-components/list-assets.d.ts +1 -0
- package/examples/hello-world/example-components/list-assets.tsx +38 -0
- package/examples/hello-world/example-components/open-modal-example.d.ts +1 -0
- package/examples/hello-world/example-components/open-modal-example.tsx +47 -0
- package/examples/hello-world/example-components/view-context-example.d.ts +1 -0
- package/examples/hello-world/example-components/view-context-example.tsx +17 -0
- package/examples/hello-world/example-components/who-am-i.d.ts +1 -0
- package/examples/hello-world/example-components/who-am-i.tsx +13 -0
- package/examples/hello-world/main.d.ts +0 -0
- package/examples/hello-world/main.tsx +11 -0
- package/examples/hello-world/styles.css +1 -0
- package/examples/hello-world-modal/app.d.ts +2 -0
- package/examples/hello-world-modal/app.tsx +30 -0
- package/examples/hello-world-modal/hello.d.ts +1 -0
- package/examples/hello-world-modal/hello.tsx +15 -0
- package/examples/hello-world-modal/main.d.ts +0 -0
- package/examples/hello-world-modal/main.tsx +11 -0
- package/examples/hello-world-modal/styles.css +1 -0
- package/examples/hello-world-remote/app.d.ts +2 -0
- package/examples/hello-world-remote/app.tsx +23 -0
- package/examples/hello-world-remote/invoke-remote-example.d.ts +13 -0
- package/examples/hello-world-remote/invoke-remote-example.tsx +40 -0
- package/examples/hello-world-remote/main.d.ts +0 -0
- package/examples/hello-world-remote/main.tsx +11 -0
- package/examples/hello-world-remote/styles.css +1 -0
- package/examples/manifest.yml.example +49 -0
- package/index-CBKhl1FP.mjs +22 -0
- package/index-CP8emE0q.js +1 -0
- package/index.d.ts +654 -0
- package/index.js +2 -0
- package/index.mjs +1145 -0
- package/package.json +54 -0
- package/style.css +1 -0
- package/tsdoc-metadata.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
# @valiantys/atlassian-app-frontend
|
|
2
|
+
|
|
3
|
+
This library provides an Atlassian Forge Custom UI wrapper component that handles all the setup necessary to
|
|
4
|
+
support an app that can run deployed or in standalone mode.
|
|
5
|
+
|
|
6
|
+
## Using the library
|
|
7
|
+
|
|
8
|
+
The library can be installed into a React Custom UI Forge App that is built with Vite.
|
|
9
|
+
It definitely does not work with react-scripts.
|
|
10
|
+
It has not been tested with Webpack.
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [Generate a new Forge App](#generate-a-new-forge-app)
|
|
15
|
+
- [Creating a Custom UI module with Vite](#creating-a-custom-ui-module-with-vite)
|
|
16
|
+
- [Generate the Custom UI app](#generate-the-custom-ui-app)
|
|
17
|
+
- [Install the library](#install-the-library)
|
|
18
|
+
- [Update the generated code](#update-the-generated-code)
|
|
19
|
+
- [Vite config](#vite-config)
|
|
20
|
+
- [manifest.yml](#manifest-yml)
|
|
21
|
+
- [Setup OAuth for invoking Atlassian APIs](#setup-oauth-for-invoking-atlassian-apis)
|
|
22
|
+
- [Run the app locally (standalone mode)](#run-the-app-locally)
|
|
23
|
+
- [Deploy the app](#deploy-the-app)
|
|
24
|
+
- [Using provided abstractions for @forge/bridge APIs](#using-provided-abstractions)
|
|
25
|
+
- [Invoking backend resolver functions (invoke)](#invoke)
|
|
26
|
+
- [requestJira, requestConfluence, requestBitbucket](#request-jira)
|
|
27
|
+
- [Accessing JSM Assets APIs that require a workspace ID](#workspace-id)
|
|
28
|
+
- [Accessing the View Context with useViewContext hook](#accessing-the-view-context-with-useviewcontext-hook)
|
|
29
|
+
- [Host Router (@forge/bridge router)](#host-router)
|
|
30
|
+
- [Modals (@forge/bridge Modal)](#modals)
|
|
31
|
+
- [Invoking remote calls from the client (@forge/bridge invokeRemote)](#invoke-remote)
|
|
32
|
+
- [Unit testing](#unit-testing)
|
|
33
|
+
- [Add react and jest testing support](#add-react-and-jest-testing-support)
|
|
34
|
+
- [Configure Jest](#configure-jest)
|
|
35
|
+
- [Example](#testing-example)
|
|
36
|
+
- [References](#references)
|
|
37
|
+
- [Example Application](#example-application)
|
|
38
|
+
- [Support](#support)
|
|
39
|
+
|
|
40
|
+
### Generate a new Forge App
|
|
41
|
+
|
|
42
|
+
See the [README](../atlassian-app-backend/README.md) file in the @valiantys/atlassian-app-backend library for
|
|
43
|
+
instructions on creating a Forge app that supports standalone and deployed modes.
|
|
44
|
+
|
|
45
|
+
### Creating a Custom UI module with Vite
|
|
46
|
+
|
|
47
|
+
Delete the static/hello-world app that was generated by the forge cli,
|
|
48
|
+
then follow the steps below to add a Custom UI app with Vite.
|
|
49
|
+
|
|
50
|
+
#### Generate the Custom UI app
|
|
51
|
+
|
|
52
|
+
Run Vite CLI command to generate a new react app.
|
|
53
|
+
|
|
54
|
+
```shell
|
|
55
|
+
cd static
|
|
56
|
+
npm create vite@latest my-module-name -- --template react-ts
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Install the library
|
|
60
|
+
|
|
61
|
+
```shell
|
|
62
|
+
cd my-module-name
|
|
63
|
+
npm i @valiantys/atlassian-app-frontend
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Update the generated code
|
|
67
|
+
|
|
68
|
+
You can delete the App.css file, then replace the contents of main.tsx, App.tsx, and index.css as shown below.
|
|
69
|
+
|
|
70
|
+
##### <span id="update-the-generated-code-main"></span>main.tsx
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import '@atlaskit/css-reset';
|
|
74
|
+
import * as ReactDOM from 'react-dom/client';
|
|
75
|
+
|
|
76
|
+
import './index.css';
|
|
77
|
+
import App from './app';
|
|
78
|
+
|
|
79
|
+
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
|
80
|
+
|
|
81
|
+
root.render(<App />);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
##### <span id="update-the-generated-code-app"></span>app.tsx
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { AtlassianApp } from '@valiantys/atlassian-app-frontend';
|
|
88
|
+
|
|
89
|
+
function App() {
|
|
90
|
+
return (
|
|
91
|
+
<AtlassianApp appName="hello" standaloneConfig={{}} embeddedConfig={{}}>
|
|
92
|
+
Hello World
|
|
93
|
+
</AtlassianApp>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default App;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
##### <span id="index-css"></span>index.css
|
|
101
|
+
|
|
102
|
+
```css
|
|
103
|
+
@import '../node_modules/@valiantys/atlassian-app-frontend/style.css';
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Vite config
|
|
107
|
+
|
|
108
|
+
Add base: './' to the vite.config.ts
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
export default defineConfig({
|
|
112
|
+
base: './',
|
|
113
|
+
server: {
|
|
114
|
+
port: 4200,
|
|
115
|
+
host: 'localhost',
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
preview: {
|
|
119
|
+
port: 4300,
|
|
120
|
+
host: 'localhost',
|
|
121
|
+
},
|
|
122
|
+
plugins: [react()],
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### <span id="manifest-yml"></span>manifest.yml
|
|
127
|
+
|
|
128
|
+
Add/change resource path to point to the dist directory of your new Custom UI app.
|
|
129
|
+
You also need to allow inline styles.
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
resources:
|
|
133
|
+
- key: main
|
|
134
|
+
path: static/my-module-name/dist
|
|
135
|
+
|
|
136
|
+
permissions:
|
|
137
|
+
content:
|
|
138
|
+
styles:
|
|
139
|
+
- 'unsafe-inline'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Setup OAuth for invoking Atlassian APIs
|
|
143
|
+
|
|
144
|
+
##### Add OAuth config to a .env file
|
|
145
|
+
|
|
146
|
+
Create an OAuth 2.0 integration app at <https://developer.atlassian.com/console/myapps/> with the `read:me`
|
|
147
|
+
scope activated under `User identity API` and the authorization callback url set to `http://localhost:4200/callback`.
|
|
148
|
+
Here, you will also need to add any additional scopes needed for Atlassian APIs that you use.
|
|
149
|
+
From the developer console, you will need to copy the client ID and client secret values that are found on the Settings page of the OAuth app you create.
|
|
150
|
+
The secret will go in the backend .env file. You should only include the client ID in the frontend .env file as shown below.
|
|
151
|
+
|
|
152
|
+
Your .env file will contain secrets that should NOT be committed to git.
|
|
153
|
+
You should also add a sample.env file to your project that does NOT contain any secrets but makes it easier for other developers to set up their own .env file.
|
|
154
|
+
|
|
155
|
+
static/my-module-name/.env
|
|
156
|
+
|
|
157
|
+
```shell
|
|
158
|
+
# Needed for standalone authentication with Atlassian OAuth
|
|
159
|
+
VITE_ATLASSIAN_OAUTH_CLIENT_ID=<client-id>
|
|
160
|
+
|
|
161
|
+
# Set to deployed url/port in non local environment
|
|
162
|
+
VITE_STANDALONE_BACKEND_URL='http://localhost'
|
|
163
|
+
VITE_STANDALONE_BACKEND_PORT=3000
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
##### Add OAuth config to AtlassianApp props:
|
|
167
|
+
|
|
168
|
+
The oAuthScopes listed here must match exactly with the OAuth scopes granted for the
|
|
169
|
+
OAuth client id supplied in the .env file. The configuration below contains the
|
|
170
|
+
OAuth scopes needed for all the provided code examples.
|
|
171
|
+
|
|
172
|
+
app.tsx
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
const backendUrl = `${import.meta.env.VITE_STANDALONE_BACKEND_URL}:${import.meta.env.VITE_STANDALONE_BACKEND_PORT}/api/`;
|
|
176
|
+
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
<AtlassianApp
|
|
180
|
+
appName="hello"
|
|
181
|
+
standaloneConfig={{
|
|
182
|
+
oauthConfig: {
|
|
183
|
+
appName: 'hello',
|
|
184
|
+
codeTokenExchangeUrl: `${backendUrl}${CommonResolverPaths.getOauthToken}`,
|
|
185
|
+
clientId: import.meta.env.VITE_ATLASSIAN_OAUTH_CLIENT_ID,
|
|
186
|
+
oAuthScopes: [
|
|
187
|
+
'read:jira-user',
|
|
188
|
+
'read:me',
|
|
189
|
+
'read:jira-work', // needed to query for Jira issues
|
|
190
|
+
'read:servicedesk-request', // needed to query for assets workspace ID
|
|
191
|
+
'read:cmdb-schema:jira', // needed to query for list of assets schemas
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
}}
|
|
195
|
+
embeddedConfig={{}}
|
|
196
|
+
>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### <span id="run-the-app-locally"></span>Run the app locally (standalone mode)
|
|
200
|
+
|
|
201
|
+
```shell
|
|
202
|
+
cd static/my-module-name
|
|
203
|
+
npm run dev
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### Deploy the app
|
|
207
|
+
|
|
208
|
+
```shell
|
|
209
|
+
cd static/my-module-name
|
|
210
|
+
npm run build
|
|
211
|
+
cd ../../
|
|
212
|
+
forge deploy
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### <span id="using-provided-abstractions"></span>Using provided abstractions for @forge/bridge APIs
|
|
216
|
+
|
|
217
|
+
#### <span id="invoke"></span>Invoking backend resolver functions (invoke)
|
|
218
|
+
|
|
219
|
+
##### Configuration
|
|
220
|
+
|
|
221
|
+
static/my-module-name/.env
|
|
222
|
+
|
|
223
|
+
```shell
|
|
224
|
+
# Set to deployed url/port in non local environment
|
|
225
|
+
VITE_STANDALONE_BACKEND_URL='http://localhost'
|
|
226
|
+
VITE_STANDALONE_BACKEND_PORT=3000
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
app.tsx
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
const backendUrl = `${import.meta.env.VITE_STANDALONE_BACKEND_URL}:${import.meta.env.VITE_STANDALONE_BACKEND_PORT}/api/`;
|
|
233
|
+
|
|
234
|
+
function App() {
|
|
235
|
+
return (
|
|
236
|
+
<AtlassianApp
|
|
237
|
+
appName="hello"
|
|
238
|
+
standaloneConfig={{
|
|
239
|
+
backendUrl,
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
##### Example Component
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { useEffect, useState } from 'react';
|
|
246
|
+
import { useBackendAdapter } from '@valiantys/atlassian-app-frontend';
|
|
247
|
+
|
|
248
|
+
export function Hello() {
|
|
249
|
+
const [text, setText] = useState<string>('');
|
|
250
|
+
const { invoke } = useBackendAdapter();
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
invoke<{ text: string }>('getText').then((t) => setText(t.text));
|
|
254
|
+
}, [invoke]);
|
|
255
|
+
|
|
256
|
+
return <div>{text}</div>;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### <span id="request-jira"></span>requestJira, requestConfluence, requestBitbucket
|
|
261
|
+
|
|
262
|
+
The library provides hooks to access abstractions for each one of these.
|
|
263
|
+
Using these abstractions is what allows the frontend app to run in both standalone and deployed modes.
|
|
264
|
+
When deployed in Forge, these abstractions are just simple wrappers around the @forge/bridge methods.
|
|
265
|
+
In standalone mode, however, they make requests using the OAuth token and the fetch API.
|
|
266
|
+
See [Setup OAuth for invoking Atlassian APIs](#setup-oauth-for-invoking-atlassian-apis).
|
|
267
|
+
|
|
268
|
+
##### Example component with useRequestJira
|
|
269
|
+
|
|
270
|
+
This component uses the Jira REST API to retrieve the list of Issue Types that the user has permissions to view.
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import { useEffect, useState } from 'react';
|
|
274
|
+
import { useRequestJira } from '@valiantys/atlassian-app-frontend';
|
|
275
|
+
|
|
276
|
+
export function IssueTypesExample() {
|
|
277
|
+
const [issueTypes, setIssueTypes] = useState<IssueTypeDetails[]>([]);
|
|
278
|
+
const requestJiraSvc = useRequestJira();
|
|
279
|
+
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
requestJiraSvc
|
|
282
|
+
.fetch<IssueTypeDetails[]>({
|
|
283
|
+
url: requestJiraSvc.route`/rest/api/2/issuetype`,
|
|
284
|
+
method: 'GET',
|
|
285
|
+
})
|
|
286
|
+
.then((types) => setIssueTypes(types));
|
|
287
|
+
}, [requestJiraSvc]);
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div>
|
|
291
|
+
{issueTypes.map((it) => (
|
|
292
|
+
<div key={it.id}>
|
|
293
|
+
{it.id}: {it.description}
|
|
294
|
+
</div>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
interface IssueTypeDetails {
|
|
301
|
+
avatarId: number;
|
|
302
|
+
description: string;
|
|
303
|
+
hierarchyLevel: number;
|
|
304
|
+
iconUrl: string;
|
|
305
|
+
id: string;
|
|
306
|
+
name: string;
|
|
307
|
+
self: string;
|
|
308
|
+
subtask: boolean;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### <span id="workspace-id"></span>Accessing JSM Assets APIs that require a workspace ID
|
|
313
|
+
|
|
314
|
+
The JSM Assets APIs require a workspace ID in addition to the cloud ID of the site.
|
|
315
|
+
The library can handle the details of getting the workspace ID for you and will then provide
|
|
316
|
+
that to your components. You can access the workspaceId using the useWorkspaceId hook.
|
|
317
|
+
OAuth configuration is required for standalone mode, see [Setup OAuth for invoking Atlassian APIs](#setup-oauth-for-invoking-atlassian-apis).
|
|
318
|
+
|
|
319
|
+
##### Turn on workspace checking
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
<AtlassianApp
|
|
323
|
+
appName="hello"
|
|
324
|
+
doCheckWorkspace={true} // Needed for querying JSM Assets
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
##### Example component with useWorkspaceId
|
|
328
|
+
|
|
329
|
+
This example also demonstrates use of the useLoadDataEffect hook and the PageLoadingView component.
|
|
330
|
+
These help implement the common pattern of loading data on first component render.
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
import { useCallback } from 'react';
|
|
334
|
+
import { PageLoadingView, useLoadDataEffect, useRequestJira, useWorkspaceId } from '@valiantys/atlassian-app-frontend';
|
|
335
|
+
|
|
336
|
+
interface AssetSchema {
|
|
337
|
+
name: string;
|
|
338
|
+
id: string;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function ListAssets() {
|
|
342
|
+
const requestJiraSvc = useRequestJira();
|
|
343
|
+
const workspaceId = useWorkspaceId();
|
|
344
|
+
|
|
345
|
+
const listAssetsSchemas = useCallback(async () => {
|
|
346
|
+
if (requestJiraSvc && workspaceId) {
|
|
347
|
+
const paginatedResult = await requestJiraSvc.fetch<{
|
|
348
|
+
values: AssetSchema[];
|
|
349
|
+
}>({
|
|
350
|
+
url: requestJiraSvc.route`/jsm/assets/workspace/${workspaceId}/v1/objectschema/list`,
|
|
351
|
+
method: 'GET',
|
|
352
|
+
});
|
|
353
|
+
return paginatedResult.values;
|
|
354
|
+
}
|
|
355
|
+
return [];
|
|
356
|
+
}, [requestJiraSvc, workspaceId]);
|
|
357
|
+
|
|
358
|
+
const { data, loading, error } = useLoadDataEffect<AssetSchema[]>(listAssetsSchemas);
|
|
359
|
+
return loading || error ? <PageLoadingView label="" loadingError={error} /> : <div>{data?.map((schema) => <div key={schema.id}>{schema.name}</div>)}</div>;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### Accessing the View Context with useViewContext hook
|
|
364
|
+
|
|
365
|
+
The useViewContext hook gives access to the Atlassian view context, which provides information
|
|
366
|
+
about the module itself and the Atlassian page in which it is loaded. When running in standalone mode,
|
|
367
|
+
you may configure the context with hard-coded values for testing.
|
|
368
|
+
|
|
369
|
+
##### Example
|
|
370
|
+
|
|
371
|
+
In this example, the standaloneConfig contains issue and project data for a module that
|
|
372
|
+
would normally be loaded in the context of a Jira issue.
|
|
373
|
+
|
|
374
|
+
###### App.tsx
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
<AtlassianApp
|
|
378
|
+
standaloneConfig={{
|
|
379
|
+
initialMockViewContext: {
|
|
380
|
+
extension: {
|
|
381
|
+
issue: {
|
|
382
|
+
key: 'BST-18',
|
|
383
|
+
id: 'BST-18',
|
|
384
|
+
type: 'Submit a request or incident',
|
|
385
|
+
typeId: '10013',
|
|
386
|
+
},
|
|
387
|
+
project: {
|
|
388
|
+
id: '10002',
|
|
389
|
+
key: 'FMSR',
|
|
390
|
+
type: 'service_desk',
|
|
391
|
+
},
|
|
392
|
+
type: 'jira:issueContext',
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
###### Accessing the View Context from a component
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
import { useViewContext } from '@valiantys/atlassian-app-frontend';
|
|
401
|
+
import { useEffect, useState } from 'react';
|
|
402
|
+
|
|
403
|
+
export function ViewContextExample() {
|
|
404
|
+
const viewContext = useViewContext();
|
|
405
|
+
const [contextData, setContextData] = useState<string | undefined>();
|
|
406
|
+
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
if (viewContext) {
|
|
409
|
+
viewContext.getContext().then((fullContext) => setContextData(JSON.stringify(fullContext)));
|
|
410
|
+
}
|
|
411
|
+
}, [viewContext]);
|
|
412
|
+
|
|
413
|
+
return <div>{contextData}</div>;
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### <span id="host-router"></span>Host Router (@forge/bridge router)
|
|
418
|
+
|
|
419
|
+
The router provides methods for opening a new window, navigating the host application window to a new location,
|
|
420
|
+
and for reloading the host window (https://developer.atlassian.com/platform/forge/custom-ui-bridge/router/).
|
|
421
|
+
In standalone mode, the host window is your app window. When deployed, the host window would be whatever Atlassian
|
|
422
|
+
product window the module is running in.
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
import { useHostRouter } from '@valiantys/atlassian-app-frontend';
|
|
426
|
+
import Button from '@atlaskit/button/new';
|
|
427
|
+
export function HostRouterExample() {
|
|
428
|
+
const hostRouter = useHostRouter();
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<div>
|
|
432
|
+
<Button onClick={() => void hostRouter.open('http://example.com/')}>Navigate</Button>
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
#### <span id="modals"></span>Modals (@forge/bridge Modal)
|
|
439
|
+
|
|
440
|
+
The forge bride API provides the ability to open modal windows within your module (https://developer.atlassian.com/platform/forge/custom-ui-bridge/modal/).
|
|
441
|
+
In standalone mode, your modal will simply be opened in a new browser window.
|
|
442
|
+
A current limitation of standalone mode is that the modal will not receive data from the opening module, this
|
|
443
|
+
must be instead hard-coded into the standalone config on the modal application.
|
|
444
|
+
|
|
445
|
+
##### Example Modal Opener
|
|
446
|
+
|
|
447
|
+
The App.tsx config must contain the URL of the modal app that is also running locally.
|
|
448
|
+
If the app opens multiple modal resources, you may provide a mapping of resource name to URL.
|
|
449
|
+
|
|
450
|
+
```tsx
|
|
451
|
+
<AtlassianApp
|
|
452
|
+
appName="hello"
|
|
453
|
+
standaloneConfig={{
|
|
454
|
+
modalOpenerConfig: { defaultUrl: 'http://localhost:4201/' },
|
|
455
|
+
}}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Component that opens a modal resource.
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
import Button from '@atlaskit/button/new';
|
|
462
|
+
import Textfield from '@atlaskit/textfield';
|
|
463
|
+
|
|
464
|
+
import { useModalService } from '@valiantys/atlassian-app-frontend';
|
|
465
|
+
import { useCallback, useState } from 'react';
|
|
466
|
+
|
|
467
|
+
export function OpenModalExample() {
|
|
468
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
469
|
+
const [text, setText] = useState<string>('Hello');
|
|
470
|
+
|
|
471
|
+
const modalService = useModalService();
|
|
472
|
+
|
|
473
|
+
const openModal = useCallback(() => {
|
|
474
|
+
if (!isOpen) {
|
|
475
|
+
setIsOpen(true);
|
|
476
|
+
void modalService.open({
|
|
477
|
+
resource: 'hello-world-modal', // must be the key of a resource defined in manifest.yml
|
|
478
|
+
onClose: () => {
|
|
479
|
+
setIsOpen(false);
|
|
480
|
+
},
|
|
481
|
+
/*
|
|
482
|
+
small - w: 400px h: 20vh minHeight: 320px
|
|
483
|
+
medium - w: 600px h: 40vh minHeight: 520px
|
|
484
|
+
large - w: 800px h: 70vh minHeight: 720px
|
|
485
|
+
xlarge - w: 968px h: 90vh
|
|
486
|
+
max - w: 100% h: 100%
|
|
487
|
+
*/
|
|
488
|
+
size: 'large',
|
|
489
|
+
context: {
|
|
490
|
+
text,
|
|
491
|
+
},
|
|
492
|
+
closeOnEscape: true,
|
|
493
|
+
closeOnOverlayClick: true,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}, [modalService, isOpen, text]);
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<div>
|
|
500
|
+
<Textfield value={text} onChange={(e) => setText((e.target as HTMLInputElement).value)}></Textfield>
|
|
501
|
+
<Button onClick={openModal}>Open Modal</Button>
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
##### Example Modal Content Application
|
|
508
|
+
|
|
509
|
+
###### App.tsx
|
|
510
|
+
|
|
511
|
+
```tsx
|
|
512
|
+
import { AtlassianApp, ModalContent } from '@valiantys/atlassian-app-frontend';
|
|
513
|
+
import { Hello } from './hello';
|
|
514
|
+
|
|
515
|
+
function App() {
|
|
516
|
+
return (
|
|
517
|
+
<AtlassianApp
|
|
518
|
+
appName="hello-modal"
|
|
519
|
+
standaloneConfig={{
|
|
520
|
+
modalContextConfig: { openerOrigin: 'http://localhost:4200' },
|
|
521
|
+
initialMockViewContext: {
|
|
522
|
+
extension: {
|
|
523
|
+
// This is a hard-coded version of the data that will be passed
|
|
524
|
+
// from the opening app when running in an Atlassian environment.
|
|
525
|
+
modal: {
|
|
526
|
+
text: 'Hello Bob',
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
}}
|
|
531
|
+
embeddedConfig={{}}
|
|
532
|
+
>
|
|
533
|
+
{/* ModalContent component provides modal-related services, see Hello component for example usage. */}
|
|
534
|
+
<ModalContent>
|
|
535
|
+
<Hello></Hello>
|
|
536
|
+
</ModalContent>
|
|
537
|
+
</AtlassianApp>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export default App;
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
###### Content Component
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
import Button from '@atlaskit/button/new';
|
|
548
|
+
import { useModalContentService } from '@valiantys/atlassian-app-frontend';
|
|
549
|
+
|
|
550
|
+
export function Hello() {
|
|
551
|
+
const { modalContextData, close } = useModalContentService<{
|
|
552
|
+
text: string;
|
|
553
|
+
}>();
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<div>
|
|
557
|
+
<div>{modalContextData?.text}</div>
|
|
558
|
+
<Button onClick={close}>Close Modal</Button>
|
|
559
|
+
</div>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
#### <span id="invoke-remote"></span>Invoking remote calls from the client (@forge/bridge invokeRemote)
|
|
565
|
+
|
|
566
|
+
Instead of invoking call on the Forge backend, an app may invoke calls on a remote backend that is
|
|
567
|
+
configured in the app's manifest file.
|
|
568
|
+
|
|
569
|
+
App config
|
|
570
|
+
|
|
571
|
+
```tsx
|
|
572
|
+
<AtlassianApp
|
|
573
|
+
appName="hello-remote"
|
|
574
|
+
standaloneConfig={{
|
|
575
|
+
remoteUrl: 'https://jsonplaceholder.typicode.com/', // For invokeRemote from client (useRemoteAdapter)
|
|
576
|
+
}}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Component
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
582
|
+
/**
|
|
583
|
+
* manifest.yml must contain this configuration to enable the remote invocation:
|
|
584
|
+
*
|
|
585
|
+
* remotes:
|
|
586
|
+
* - key: example-remote
|
|
587
|
+
* baseUrl: https://jsonplaceholder.typicode.com
|
|
588
|
+
* permissions:
|
|
589
|
+
* external:
|
|
590
|
+
* fetch:
|
|
591
|
+
* client:
|
|
592
|
+
* - remote: example-remote
|
|
593
|
+
*/
|
|
594
|
+
|
|
595
|
+
import { useRemoteAdapter } from '@valiantys/atlassian-app-frontend';
|
|
596
|
+
import { useEffect, useState } from 'react';
|
|
597
|
+
|
|
598
|
+
interface Todo {
|
|
599
|
+
completed: boolean;
|
|
600
|
+
id: number;
|
|
601
|
+
title: string;
|
|
602
|
+
userId: number;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function InvokeRemoteExample() {
|
|
606
|
+
const remoteSvc = useRemoteAdapter();
|
|
607
|
+
const [todo, setTodo] = useState<Todo | undefined>(undefined);
|
|
608
|
+
|
|
609
|
+
useEffect(() => {
|
|
610
|
+
remoteSvc
|
|
611
|
+
.invokeRemote<Todo, void>({
|
|
612
|
+
path: '/todos/1',
|
|
613
|
+
method: 'GET',
|
|
614
|
+
headers: { accept: 'application/json' },
|
|
615
|
+
})
|
|
616
|
+
.then((response) => setTodo(response.body))
|
|
617
|
+
.catch((error) => console.error('remote error', error));
|
|
618
|
+
}, [remoteSvc]);
|
|
619
|
+
|
|
620
|
+
return <div>Todo: {todo?.title}</div>;
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Unit testing
|
|
625
|
+
|
|
626
|
+
#### Add react and jest testing support
|
|
627
|
+
|
|
628
|
+
```shell
|
|
629
|
+
cd static/my-module-name
|
|
630
|
+
npm i -D jest ts-jest @types/jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-fixed-jsdom identity-obj-proxy
|
|
631
|
+
npx ts-jest config:init
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
#### Configure Jest
|
|
635
|
+
|
|
636
|
+
Make changes to static/my-module-name/jest.config.js:
|
|
637
|
+
|
|
638
|
+
- Change testEnvironment to "jest-fixed-jsdom"
|
|
639
|
+
- Add tsConfig path to transform
|
|
640
|
+
- Add moduleNameMapper to prevent errors with Jest trying to interpret compiled css files
|
|
641
|
+
|
|
642
|
+
```js
|
|
643
|
+
export default {
|
|
644
|
+
testEnvironment: 'jest-fixed-jsdom', // https://github.com/mswjs/jest-fixed-jsdom
|
|
645
|
+
transform: {
|
|
646
|
+
'^.+.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.app.json' }],
|
|
647
|
+
},
|
|
648
|
+
moduleNameMapper: {
|
|
649
|
+
'\\.(css|scss)$': 'identity-obj-proxy',
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
#### <span id="testing-example"></span>Example
|
|
655
|
+
|
|
656
|
+
##### Component
|
|
657
|
+
|
|
658
|
+
```tsx
|
|
659
|
+
import { useEffect, useState } from 'react';
|
|
660
|
+
import { useBackendAdapter } from '@valiantys/atlassian-app-frontend';
|
|
661
|
+
|
|
662
|
+
export function Hello() {
|
|
663
|
+
const [text, setText] = useState<string>('');
|
|
664
|
+
const { invoke } = useBackendAdapter();
|
|
665
|
+
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
invoke<{ text: string }>('getText').then((t) => setText(t.text));
|
|
668
|
+
}, [invoke]);
|
|
669
|
+
|
|
670
|
+
return <div>{text}</div>;
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
##### Test
|
|
675
|
+
|
|
676
|
+
```tsx
|
|
677
|
+
import { act, render, screen } from '@testing-library/react';
|
|
678
|
+
|
|
679
|
+
import { AtlassianAppTest, AtlassianAppTestProps, defaultProps } from '@valiantys/atlassian-app-frontend';
|
|
680
|
+
|
|
681
|
+
import { Hello } from './hello';
|
|
682
|
+
|
|
683
|
+
describe('HelloWorld', () => {
|
|
684
|
+
it('should render successfully', async () => {
|
|
685
|
+
await act(async () => {
|
|
686
|
+
// Setup mocks
|
|
687
|
+
const mockInvoke = jest.fn();
|
|
688
|
+
mockInvoke.mockReturnValue(Promise.resolve({ text: 'Hello World' }));
|
|
689
|
+
const appProps: AtlassianAppTestProps = {
|
|
690
|
+
...defaultProps(jest.fn),
|
|
691
|
+
invoke: mockInvoke,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// Render the component (which calls invoke on first render)
|
|
695
|
+
render(
|
|
696
|
+
<AtlassianAppTest {...appProps}>
|
|
697
|
+
<Hello></Hello>
|
|
698
|
+
</AtlassianAppTest>
|
|
699
|
+
);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Throws error if text is not found within timeout period
|
|
703
|
+
await screen.findByText('Hello World');
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
#### References
|
|
709
|
+
|
|
710
|
+
- https://testing-library.com/docs/react-testing-library/example-intro
|
|
711
|
+
- https://testing-library.com/docs/queries/about/
|
|
712
|
+
|
|
713
|
+
### Example Application
|
|
714
|
+
|
|
715
|
+
You will find an examples directory under node_modules/@valiantys/atlassian-app-frontend/examples,
|
|
716
|
+
containing all the examples shown in this README along with more supporting files. The repo for the full
|
|
717
|
+
example application is also available at https://bitbucket.org/oasisdigital/atlassian-app-examples.
|
|
718
|
+
|
|
719
|
+
If you would like to clone a template repo to get started more quickly, you can find one at https://bitbucket.org/oasisdigital/atlassian-app-template.
|
|
720
|
+
|
|
721
|
+
### Support
|
|
722
|
+
|
|
723
|
+
Our issue-tracking board is viewable at https://trello.com/b/aRmPXQjq/atlassian-app-frontend-backend-npm-package-issue-tracker.
|
|
724
|
+
To file an issue you may send it in an email to [zacharykipping+wzolmxklz7ysfqky4alz@boards.trello.com](mailto:zacharykipping+wzolmxklz7ysfqky4alz@boards.trello.com).
|
|
725
|
+
|
|
726
|
+
You may also contact us with questions at [forge@valiantys.com](mailto:forge@valiantys.com).
|