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
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
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
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 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
1 Like
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