npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@valiantys/atlassian-app-frontend

v1.0.0

Published

This library provides an Atlassian Forge Custom UI wrapper component that handles all the setup necessary to support an app that can run deployed or in standalone mode

Downloads

72

Readme

@valiantys/atlassian-app-frontend

This library provides an Atlassian Forge Custom UI wrapper component that handles all the setup necessary to support an app that can run deployed or in standalone mode.

Using the library

The library can be installed into a React Custom UI Forge App that is built with Vite. It definitely does not work with react-scripts. It has not been tested with Webpack.

Table of Contents

Generate a new Forge App

See the README file in the @valiantys/atlassian-app-backend library for instructions on creating a Forge app that supports standalone and deployed modes.

Creating a Custom UI module with Vite

Delete the static/hello-world app that was generated by the forge cli, then follow the steps below to add a Custom UI app with Vite.

Generate the Custom UI app

Run Vite CLI command to generate a new react app.

cd static
npm create vite@latest my-module-name -- --template react-ts

Install the library

cd my-module-name
npm i @valiantys/atlassian-app-frontend

Update the generated code

You can delete the App.css file, then replace the contents of main.tsx, App.tsx, and index.css as shown below.

main.tsx
import '@atlaskit/css-reset';
import * as ReactDOM from 'react-dom/client';

import './index.css';
import App from './app';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(<App />);
app.tsx
import { AtlassianApp } from '@valiantys/atlassian-app-frontend';

function App() {
  return (
    <AtlassianApp appName="hello" standaloneConfig={{}} embeddedConfig={{}}>
      Hello World
    </AtlassianApp>
  );
}

export default App;
index.css
@import '../node_modules/@valiantys/atlassian-app-frontend/style.css';

Vite config

Add base: './' to the vite.config.ts

export default defineConfig({
  base: './',
  server: {
    port: 4200,
    host: 'localhost',
  },

  preview: {
    port: 4300,
    host: 'localhost',
  },
  plugins: [react()],
});

manifest.yml

Add/change resource path to point to the dist directory of your new Custom UI app. You also need to allow inline styles.

resources:
  - key: main
    path: static/my-module-name/dist

permissions:
  content:
    styles:
      - 'unsafe-inline'

Setup OAuth for invoking Atlassian APIs

Add OAuth config to a .env file

Create an OAuth 2.0 integration app at https://developer.atlassian.com/console/myapps/ with the read:me scope activated under User identity API and the authorization callback url set to http://localhost:4200/callback. Here, you will also need to add any additional scopes needed for Atlassian APIs that you use. 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. The secret will go in the backend .env file. You should only include the client ID in the frontend .env file as shown below.

Your .env file will contain secrets that should NOT be committed to git. 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.

static/my-module-name/.env

# Needed for standalone authentication with Atlassian OAuth
VITE_ATLASSIAN_OAUTH_CLIENT_ID=<client-id>

# Set to deployed url/port in non local environment
VITE_STANDALONE_BACKEND_URL='http://localhost'
VITE_STANDALONE_BACKEND_PORT=3000
Add OAuth config to AtlassianApp props:

The oAuthScopes listed here must match exactly with the OAuth scopes granted for the OAuth client id supplied in the .env file. The configuration below contains the OAuth scopes needed for all the provided code examples.

app.tsx

    const backendUrl = `${import.meta.env.VITE_STANDALONE_BACKEND_URL}:${import.meta.env.VITE_STANDALONE_BACKEND_PORT}/api/`;

    ...

    <AtlassianApp
      appName="hello"
      standaloneConfig={{
        oauthConfig: {
          appName: 'hello',
          codeTokenExchangeUrl: `${backendUrl}${CommonResolverPaths.getOauthToken}`,
          clientId: import.meta.env.VITE_ATLASSIAN_OAUTH_CLIENT_ID,
          oAuthScopes: [
            'read:jira-user',
            'read:me',
            'read:jira-work', // needed to query for Jira issues
            'read:servicedesk-request', // needed to query for assets workspace ID
            'read:cmdb-schema:jira', // needed to query for list of assets schemas
          ],
        },
      }}
      embeddedConfig={{}}
    >

Run the app locally (standalone mode)

cd static/my-module-name
npm run dev

Deploy the app

cd static/my-module-name
npm run build
cd ../../
forge deploy

Using provided abstractions for @forge/bridge APIs

Invoking backend resolver functions (invoke)

Configuration

static/my-module-name/.env

# Set to deployed url/port in non local environment
VITE_STANDALONE_BACKEND_URL='http://localhost'
VITE_STANDALONE_BACKEND_PORT=3000

app.tsx

const backendUrl = `${import.meta.env.VITE_STANDALONE_BACKEND_URL}:${import.meta.env.VITE_STANDALONE_BACKEND_PORT}/api/`;

function App() {
  return (
    <AtlassianApp
      appName="hello"
      standaloneConfig={{
        backendUrl,
Example Component
import { useEffect, useState } from 'react';
import { useBackendAdapter } from '@valiantys/atlassian-app-frontend';

export function Hello() {
  const [text, setText] = useState<string>('');
  const { invoke } = useBackendAdapter();

  useEffect(() => {
    invoke<{ text: string }>('getText').then((t) => setText(t.text));
  }, [invoke]);

  return <div>{text}</div>;
}

requestJira, requestConfluence, requestBitbucket

The library provides hooks to access abstractions for each one of these. Using these abstractions is what allows the frontend app to run in both standalone and deployed modes. When deployed in Forge, these abstractions are just simple wrappers around the @forge/bridge methods. In standalone mode, however, they make requests using the OAuth token and the fetch API. See Setup OAuth for invoking Atlassian APIs.

Example component with useRequestJira

This component uses the Jira REST API to retrieve the list of Issue Types that the user has permissions to view.

import { useEffect, useState } from 'react';
import { useRequestJira } from '@valiantys/atlassian-app-frontend';

export function IssueTypesExample() {
  const [issueTypes, setIssueTypes] = useState<IssueTypeDetails[]>([]);
  const requestJiraSvc = useRequestJira();

  useEffect(() => {
    requestJiraSvc
      .fetch<IssueTypeDetails[]>({
        url: requestJiraSvc.route`/rest/api/2/issuetype`,
        method: 'GET',
      })
      .then((types) => setIssueTypes(types));
  }, [requestJiraSvc]);

  return (
    <div>
      {issueTypes.map((it) => (
        <div key={it.id}>
          {it.id}: {it.description}
        </div>
      ))}
    </div>
  );
}

interface IssueTypeDetails {
  avatarId: number;
  description: string;
  hierarchyLevel: number;
  iconUrl: string;
  id: string;
  name: string;
  self: string;
  subtask: boolean;
}

Accessing JSM Assets APIs that require a workspace ID

The JSM Assets APIs require a workspace ID in addition to the cloud ID of the site. The library can handle the details of getting the workspace ID for you and will then provide that to your components. You can access the workspaceId using the useWorkspaceId hook. OAuth configuration is required for standalone mode, see Setup OAuth for invoking Atlassian APIs.

Turn on workspace checking
    <AtlassianApp
      appName="hello"
      doCheckWorkspace={true} // Needed for querying JSM Assets
Example component with useWorkspaceId

This example also demonstrates use of the useLoadDataEffect hook and the PageLoadingView component. These help implement the common pattern of loading data on first component render.

import { useCallback } from 'react';
import { PageLoadingView, useLoadDataEffect, useRequestJira, useWorkspaceId } from '@valiantys/atlassian-app-frontend';

interface AssetSchema {
  name: string;
  id: string;
}

export function ListAssets() {
  const requestJiraSvc = useRequestJira();
  const workspaceId = useWorkspaceId();

  const listAssetsSchemas = useCallback(async () => {
    if (requestJiraSvc && workspaceId) {
      const paginatedResult = await requestJiraSvc.fetch<{
        values: AssetSchema[];
      }>({
        url: requestJiraSvc.route`/jsm/assets/workspace/${workspaceId}/v1/objectschema/list`,
        method: 'GET',
      });
      return paginatedResult.values;
    }
    return [];
  }, [requestJiraSvc, workspaceId]);

  const { data, loading, error } = useLoadDataEffect<AssetSchema[]>(listAssetsSchemas);
  return loading || error ? <PageLoadingView label="" loadingError={error} /> : <div>{data?.map((schema) => <div key={schema.id}>{schema.name}</div>)}</div>;
}

Accessing the View Context with useViewContext hook

The useViewContext hook gives access to the Atlassian view context, which provides information about the module itself and the Atlassian page in which it is loaded. When running in standalone mode, you may configure the context with hard-coded values for testing.

Example

In this example, the standaloneConfig contains issue and project data for a module that would normally be loaded in the context of a Jira issue.

App.tsx
<AtlassianApp
  standaloneConfig={{
    initialMockViewContext: {
      extension: {
        issue: {
          key: 'BST-18',
          id: 'BST-18',
          type: 'Submit a request or incident',
          typeId: '10013',
        },
        project: {
          id: '10002',
          key: 'FMSR',
          type: 'service_desk',
        },
        type: 'jira:issueContext',
      },
    },
Accessing the View Context from a component
import { useViewContext } from '@valiantys/atlassian-app-frontend';
import { useEffect, useState } from 'react';

export function ViewContextExample() {
  const viewContext = useViewContext();
  const [contextData, setContextData] = useState<string | undefined>();

  useEffect(() => {
    if (viewContext) {
      viewContext.getContext().then((fullContext) => setContextData(JSON.stringify(fullContext)));
    }
  }, [viewContext]);

  return <div>{contextData}</div>;
}

Host Router (@forge/bridge router)

The router provides methods for opening a new window, navigating the host application window to a new location, and for reloading the host window (https://developer.atlassian.com/platform/forge/custom-ui-bridge/router/). In standalone mode, the host window is your app window. When deployed, the host window would be whatever Atlassian product window the module is running in.

import { useHostRouter } from '@valiantys/atlassian-app-frontend';
import Button from '@atlaskit/button/new';
export function HostRouterExample() {
  const hostRouter = useHostRouter();

  return (
    <div>
      <Button onClick={() => void hostRouter.open('http://example.com/')}>Navigate</Button>
    </div>
  );
}

Modals (@forge/bridge Modal)

The forge bride API provides the ability to open modal windows within your module (https://developer.atlassian.com/platform/forge/custom-ui-bridge/modal/). In standalone mode, your modal will simply be opened in a new browser window. A current limitation of standalone mode is that the modal will not receive data from the opening module, this must be instead hard-coded into the standalone config on the modal application.

Example Modal Opener

The App.tsx config must contain the URL of the modal app that is also running locally. If the app opens multiple modal resources, you may provide a mapping of resource name to URL.

    <AtlassianApp
      appName="hello"
      standaloneConfig={{
        modalOpenerConfig: { defaultUrl: 'http://localhost:4201/' },
      }}

Component that opens a modal resource.

import Button from '@atlaskit/button/new';
import Textfield from '@atlaskit/textfield';

import { useModalService } from '@valiantys/atlassian-app-frontend';
import { useCallback, useState } from 'react';

export function OpenModalExample() {
  const [isOpen, setIsOpen] = useState<boolean>(false);
  const [text, setText] = useState<string>('Hello');

  const modalService = useModalService();

  const openModal = useCallback(() => {
    if (!isOpen) {
      setIsOpen(true);
      void modalService.open({
        resource: 'hello-world-modal', // must be the key of a resource defined in manifest.yml
        onClose: () => {
          setIsOpen(false);
        },
        /*
          small - w: 400px h: 20vh minHeight: 320px
          medium - w: 600px h: 40vh minHeight: 520px
          large - w: 800px h: 70vh minHeight: 720px
          xlarge - w: 968px h: 90vh
          max - w: 100% h: 100%
        */
        size: 'large',
        context: {
          text,
        },
        closeOnEscape: true,
        closeOnOverlayClick: true,
      });
    }
  }, [modalService, isOpen, text]);

  return (
    <div>
      <Textfield value={text} onChange={(e) => setText((e.target as HTMLInputElement).value)}></Textfield>
      <Button onClick={openModal}>Open Modal</Button>
    </div>
  );
}
Example Modal Content Application
App.tsx
import { AtlassianApp, ModalContent } from '@valiantys/atlassian-app-frontend';
import { Hello } from './hello';

function App() {
  return (
    <AtlassianApp
      appName="hello-modal"
      standaloneConfig={{
        modalContextConfig: { openerOrigin: 'http://localhost:4200' },
        initialMockViewContext: {
          extension: {
            // This is a hard-coded version of the data that will be passed
            // from the opening app when running in an Atlassian environment.
            modal: {
              text: 'Hello Bob',
            },
          },
        },
      }}
      embeddedConfig={{}}
    >
      {/* ModalContent component provides modal-related services, see Hello component for example usage. */}
      <ModalContent>
        <Hello></Hello>
      </ModalContent>
    </AtlassianApp>
  );
}

export default App;
Content Component
import Button from '@atlaskit/button/new';
import { useModalContentService } from '@valiantys/atlassian-app-frontend';

export function Hello() {
  const { modalContextData, close } = useModalContentService<{
    text: string;
  }>();

  return (
    <div>
      <div>{modalContextData?.text}</div>
      <Button onClick={close}>Close Modal</Button>
    </div>
  );
}

Invoking remote calls from the client (@forge/bridge invokeRemote)

Instead of invoking call on the Forge backend, an app may invoke calls on a remote backend that is configured in the app's manifest file.

App config

    <AtlassianApp
      appName="hello-remote"
      standaloneConfig={{
        remoteUrl: 'https://jsonplaceholder.typicode.com/', // For invokeRemote from client (useRemoteAdapter)
      }}

Component

/**
 * manifest.yml must contain this configuration to enable the remote invocation:
 *
 * remotes:
 *   - key: example-remote
 *     baseUrl: https://jsonplaceholder.typicode.com
 * permissions:
 *   external:
 *     fetch:
 *       client:
 *         - remote: example-remote
 */

import { useRemoteAdapter } from '@valiantys/atlassian-app-frontend';
import { useEffect, useState } from 'react';

interface Todo {
  completed: boolean;
  id: number;
  title: string;
  userId: number;
}

export function InvokeRemoteExample() {
  const remoteSvc = useRemoteAdapter();
  const [todo, setTodo] = useState<Todo | undefined>(undefined);

  useEffect(() => {
    remoteSvc
      .invokeRemote<Todo, void>({
        path: '/todos/1',
        method: 'GET',
        headers: { accept: 'application/json' },
      })
      .then((response) => setTodo(response.body))
      .catch((error) => console.error('remote error', error));
  }, [remoteSvc]);

  return <div>Todo: {todo?.title}</div>;
}

Unit testing

Add react and jest testing support

cd static/my-module-name
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
npx ts-jest config:init

Configure Jest

Make changes to static/my-module-name/jest.config.js:

  • Change testEnvironment to "jest-fixed-jsdom"
  • Add tsConfig path to transform
  • Add moduleNameMapper to prevent errors with Jest trying to interpret compiled css files
export default {
  testEnvironment: 'jest-fixed-jsdom', // https://github.com/mswjs/jest-fixed-jsdom
  transform: {
    '^.+.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.app.json' }],
  },
  moduleNameMapper: {
    '\\.(css|scss)$': 'identity-obj-proxy',
  },
};

Example

Component
import { useEffect, useState } from 'react';
import { useBackendAdapter } from '@valiantys/atlassian-app-frontend';

export function Hello() {
  const [text, setText] = useState<string>('');
  const { invoke } = useBackendAdapter();

  useEffect(() => {
    invoke<{ text: string }>('getText').then((t) => setText(t.text));
  }, [invoke]);

  return <div>{text}</div>;
}
Test
import { act, render, screen } from '@testing-library/react';

import { AtlassianAppTest, AtlassianAppTestProps, defaultProps } from '@valiantys/atlassian-app-frontend';

import { Hello } from './hello';

describe('HelloWorld', () => {
  it('should render successfully', async () => {
    await act(async () => {
      // Setup mocks
      const mockInvoke = jest.fn();
      mockInvoke.mockReturnValue(Promise.resolve({ text: 'Hello World' }));
      const appProps: AtlassianAppTestProps = {
        ...defaultProps(jest.fn),
        invoke: mockInvoke,
      };

      // Render the component (which calls invoke on first render)
      render(
        <AtlassianAppTest {...appProps}>
          <Hello></Hello>
        </AtlassianAppTest>
      );
    });

    // Throws error if text is not found within timeout period
    await screen.findByText('Hello World');
  });
});

References

  • https://testing-library.com/docs/react-testing-library/example-intro
  • https://testing-library.com/docs/queries/about/

Example Application

You will find an examples directory under node_modules/@valiantys/atlassian-app-frontend/examples, containing all the examples shown in this README along with more supporting files. The repo for the full example application is also available at https://bitbucket.org/oasisdigital/atlassian-app-examples.

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.

Support

Our issue-tracking board is viewable at https://trello.com/b/aRmPXQjq/atlassian-app-frontend-backend-npm-package-issue-tracker. To file an issue you may send it in an email to [email protected].

You may also contact us with questions at [email protected].