@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.
Files changed (66) hide show
  1. package/README.md +726 -0
  2. package/atlassian-app-forge-CTaVjJLt.js +1 -0
  3. package/atlassian-app-forge-DdtDadi2.mjs +117 -0
  4. package/atlassian-app-frontend.api.json +8829 -0
  5. package/atlassian-app-frontend.api.md +740 -0
  6. package/atlassian-app-standalone-DJOVbfp6.js +1 -0
  7. package/atlassian-app-standalone-DyyH6WPO.mjs +111 -0
  8. package/examples/backend/index.ts +44 -0
  9. package/examples/backend/lib/forge-functions.d.ts +3 -0
  10. package/examples/backend/lib/forge-functions.ts +23 -0
  11. package/examples/backend/lib/handler-functions.d.ts +58 -0
  12. package/examples/backend/lib/handler-functions.ts +72 -0
  13. package/examples/backend/lib/standalone-functions.d.ts +3 -0
  14. package/examples/backend/lib/standalone-functions.ts +17 -0
  15. package/examples/client-sample-.env +5 -0
  16. package/examples/hello-world/app.d.ts +2 -0
  17. package/examples/hello-world/app.tsx +77 -0
  18. package/examples/hello-world/example-box.d.ts +8 -0
  19. package/examples/hello-world/example-box.tsx +22 -0
  20. package/examples/hello-world/example-components/forge-storage-example.d.ts +1 -0
  21. package/examples/hello-world/example-components/forge-storage-example.tsx +66 -0
  22. package/examples/hello-world/example-components/hello-with-loading-spinner.d.ts +1 -0
  23. package/examples/hello-world/example-components/hello-with-loading-spinner.tsx +24 -0
  24. package/examples/hello-world/example-components/hello.d.ts +1 -0
  25. package/examples/hello-world/example-components/hello.spec.tsx +39 -0
  26. package/examples/hello-world/example-components/hello.tsx +13 -0
  27. package/examples/hello-world/example-components/host-router-example.d.ts +1 -0
  28. package/examples/hello-world/example-components/host-router-example.tsx +13 -0
  29. package/examples/hello-world/example-components/issue-types-example-backend.d.ts +4 -0
  30. package/examples/hello-world/example-components/issue-types-example-backend.tsx +37 -0
  31. package/examples/hello-world/example-components/issue-types-example.d.ts +4 -0
  32. package/examples/hello-world/example-components/issue-types-example.tsx +40 -0
  33. package/examples/hello-world/example-components/list-assets.d.ts +1 -0
  34. package/examples/hello-world/example-components/list-assets.tsx +38 -0
  35. package/examples/hello-world/example-components/open-modal-example.d.ts +1 -0
  36. package/examples/hello-world/example-components/open-modal-example.tsx +47 -0
  37. package/examples/hello-world/example-components/view-context-example.d.ts +1 -0
  38. package/examples/hello-world/example-components/view-context-example.tsx +17 -0
  39. package/examples/hello-world/example-components/who-am-i.d.ts +1 -0
  40. package/examples/hello-world/example-components/who-am-i.tsx +13 -0
  41. package/examples/hello-world/main.d.ts +0 -0
  42. package/examples/hello-world/main.tsx +11 -0
  43. package/examples/hello-world/styles.css +1 -0
  44. package/examples/hello-world-modal/app.d.ts +2 -0
  45. package/examples/hello-world-modal/app.tsx +30 -0
  46. package/examples/hello-world-modal/hello.d.ts +1 -0
  47. package/examples/hello-world-modal/hello.tsx +15 -0
  48. package/examples/hello-world-modal/main.d.ts +0 -0
  49. package/examples/hello-world-modal/main.tsx +11 -0
  50. package/examples/hello-world-modal/styles.css +1 -0
  51. package/examples/hello-world-remote/app.d.ts +2 -0
  52. package/examples/hello-world-remote/app.tsx +23 -0
  53. package/examples/hello-world-remote/invoke-remote-example.d.ts +13 -0
  54. package/examples/hello-world-remote/invoke-remote-example.tsx +40 -0
  55. package/examples/hello-world-remote/main.d.ts +0 -0
  56. package/examples/hello-world-remote/main.tsx +11 -0
  57. package/examples/hello-world-remote/styles.css +1 -0
  58. package/examples/manifest.yml.example +49 -0
  59. package/index-CBKhl1FP.mjs +22 -0
  60. package/index-CP8emE0q.js +1 -0
  61. package/index.d.ts +654 -0
  62. package/index.js +2 -0
  63. package/index.mjs +1145 -0
  64. package/package.json +54 -0
  65. package/style.css +1 -0
  66. 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).