Can we use the same resource for multi Forge modules?

Idea

I tried to use the same static resource (React/Custom UI) for multi modules in my Forge app.

The idea is to avoid using multi react apps. Instead, I will use only one react app, and render the content based on the context where this resource is loaded (use view from Forge bridge).

Another discussion for that idea here.

A part of my manifest:

modules:
  jira:adminPage:
    - key: admin-page
      resource: custom-ui-resource
      resolver:
        function: custom-ui-resolver
      title: Admin Page Jira Cloud
  jira:issuePanel:
    - key: issue-panel
      resource: custom-ui-resource
      resolver:
        function: custom-ui-resolver
      title: Issue Panel

The top App.js component in my React code looks like this:

import React, { useEffect, useState } from 'react';
import { view } from '@forge/bridge';

function App() {
  const [context, setContext] = useState({});
  const [isDetectingContext, setDetectingContext] = useState(true);

  useEffect(() => {
    setDetectingContext(true);

    view
      .getContext()
      .then(setContext)
      .finally(() => {
        setDetectingContext(false);
      });
  }, []);

  if (isDetectingContext) {
    return <div>Loading...</div>;
  }

  switch (context.moduleKey) {
    case 'admin-page':
      return <AdminPage />;
    case 'issue-panel':
      return <IssuePanel />;
    default:
      return <div>Cannot Detect Context</div>;
  }
}

export default App;

Problem

The app static resource cannot be loaded, it failed with the 403 error.

But, the weirdest thing here is that it works in the tunnel mode.

So any idea, discussion here?

1 Like

I have solved this by using different resolvers that return a different “entrypoint” to the frontend. I use invoke at the top of my React App to create a ReactContext and and my resolver sends either “entrypoint=FOO” or “entrypoint=BAR” for the different modules. Then the ReactApp decides on a top level which views to show.
Both resolvers share common code.

modules:
  jira:adminPage:
    - key: admin-page
      resource: custom-ui-resource
      resolver:
        function: index.handlerAdminPage
      title: Admin Page Jira Cloud
  jira:issuePanel:
    - key: issue-panel
      resource: custom-ui-resource
      resolver:
        function: index.handlerIssuePannel
      title: Issue Panel

in the index.js

const resolverIssuePannel = new Resolver();
new CustomBackendResolver(resolverIssuePannel, "ISSUE_PANEL");
export const handlerIssuePannel = resolverIssuePannel.getDefinitions();


const resolverAdminPage = new Resolver();
new CustomBackendResolver(resolverAdminPage, "ADMIN_PAGE");
export const handlerAdminPage = resolverAdminPage.getDefinitions();

You can then pass the “entrypoint” with every invoke of such resolver like so:

export class CustomBackendResolver {
  constructor(private resolver: any,  private entrypoint: string) {
    this.resolver.define("GET_ALL_FOO", this.invoke_getAllFoo());
  }

  public invoke_getAllFoo() { 
      return async (ctxpayload: any): Promise<SomeThing> => {
      const response = await this.api.asUser().requestJira(`/rest/api/3/myself`, DEFAULT_JSON_HEADERS);
       return {
          entrypoint: this.entrypoint,
          myself: response
       } as SomeThing
    }

}

Might not be the best option but it works for me :slight_smile:

3 Likes

Hi @clouless, the problem is not fixed even when I use 2 different resolvers for them.

modules:
  jira:adminPage:
    - key: admin-page
      resource: custom-ui-resource
      resolver:
        function: index.handlerAdminPage
      title: Admin Page Jira Cloud
  jira:issuePanel:
    - key: issue-panel
      resource: custom-ui-resource
      resolver:
        function: index.handlerIssuePannel
      title: Issue Panel

And index.js:

const issuePanelResolver = new Resolver();
export const handlerIssuePannel = issuePanelResolver.getDefinitions();

const adminPageResolver = new Resolver();
export const handlerAdminPage = adminPageResolver.getDefinitions();

Yes. Your two resolvers have nothing to distinguish between them.
Read my code above again and you will see the difference. It is about the “entrypoint” what makes it possible.

If you call the same GET_ALL_FOO from your React Frontend on adminPage and on issuePanel:


  useEffect(() => {
    invoke("GET_ALL_FOO", { myParam: "foo" }).then((result: any) => {
      console.log(result);
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

The console.log will show on adminPage

{
  entrypoint: "ADMIN_PAGE",
  myself: { ... }
} 

And with that you can do an if/else which component to render in React :slight_smile:

And the console.log on issuePanel will show

{
  entrypoint: "ISSUE_PANEL",
  myself: { ... }
} 

I am using this for my adminPage and projectPage and it works perfectly for me :slight_smile:

2 Likes

Should I understand it this way: “Not only the Resolver need to be different, but the returning data from them need to be different also”?

1 Like

yes :slight_smile: The resolvers can share common code as in my case with the class CustomBackendResolver BUT it gets a different entrypoint string in the constructor. And I return with every invoke this entrypoint. That way the frontend can always tell if it is in adminPage or issuePanel.

3 Likes

Thanks, @clouless. I will try that solution :smiley:

1 Like

ok cool :slight_smile: if it works, you can mark my response as accepted resolution :smiley: Then I can earn them precious internet points :laughing:

2 Likes

Hi @clouless, thank you very very much for your kindly help. I really appreciate it.

After a long time debugging, I realize that I miss the line "homepage": "." in my React package.json file, as Atlassian already documented in the Custom UI page.

I think we misunderstood each other because of my confusion title, I’m really sorry for that.

My original problem was I got 403 error when loading static resources to Custom UI page (please read again my post till the end), and the title should be that sentence.

And because I use the same resource and resolver for two modules, so I wondered if Forge allows us to do that this is the reason for this title.

Anyway, the problem is solved, and my original code was a good solution for this use case.

I think you can upgrade the @forge/bridge to the newest version, there we can use the view.getContext() to get the context where this React app is rendering, instead of return and entrypoint for each invokes.
With my solution, you can use the same resource and resolver for multi modules, without doing the CustomBackendResolver and other complex stuff.
You can take a look at my original code in the question, they are working very well.

Thanks and best regards.

3 Likes