Issue with Forge Custom Field in Jira App

Hello All,

I am facing an issue in the Forge App for a Custom Field I have created from it. The work of Custom Field is for the Issue Picker. The issue is that I am able to use that Custom Field & filter out things in that & add values, but other users are not able to do & are getting an error like -

An unexpected error occurred when fetching an auth token.

I did try to resolve this by resolver.js but didn’t work for me.

Please help over this.

Thanks & Regards,

Yash

Hi @YashBhandarkar and welcome to our community,

Can you share more about the app itself? Is the request done from the front end or the back end? Is the Custom Field used in Jira or on a Jira Service Management portal?
Was the app maybe based on this page? The code is linked at the top of the page.

In general, when other users are having issues but the app is working for me, I would check if the app is deployed to prod or if the version installed is the one from the DEV Forge environment.

Let us know,
Caterina

Hello Caterina,

The App is created using Custom UI Kit. Its basically used as Issue(Work Item) Picker based on JQL mentioned which we have declared/mentioned in the Code - edit.jsx

The request is done from Frontend.

Custom Field is used in Jira.

Also one more thing to add, There are 8 Custom Fields we have created using this App at once of same type Issue(Work Item) Picker. The JQL is declared already for each inside the edit.jsx code.

Please note this App is for Cloud.

Thanks & Regards,

Yash

Hello Caterina,

Please find the edit.jsx & manifest.yml code which we have for 8 Custom Fields we have created using the Forge App for Work Item Picker with different JQL’s.

The issue I am facing is that I am able to see the filtered issues in the Custom Field which are in JQL but other users arent able to see those & are getting error which I have already shared. The Custom Field does have the JQL defined in hardcoded manner inside the edit.jsx.

Can you please check the Code shared & let me know whats missing in that & what we can add in that to resolve this issue?

edit.jsx -

import React, { useCallback, useEffect, useRef, useState } from “react”;
import ForgeReconciler, { Select, Button, Stack, Text } from “@forge/react”;
import { CustomFieldEdit } from “@forge/react/jira”;
import { view, requestJira } from “@forge/bridge”;

const PAGE_SIZE = 50;
const DEBOUNCE_MS = 350;

/* ---------------------------------------------------------------- /
/
JQL mapping for multiple custom fields /
/
---------------------------------------------------------------- */

const JQL_MAP = {
“customfield_10940”: project = "ATB Test" AND issuetype = Task,
“customfield_10939”: project is not EMPTY,
“customfield_10974”: project = "Procurement" AND issuetype = "Order",
“customfield_10973”: project = "ATB Test" AND type = Bug,
“customfield_10976”: project = "ATB Test" AND type = Bug,
“customfield_10975”: project = "ATB Test" AND type = Bug,
“customfield_10978”: project = "ATB Test" AND type = Bug,
“customfield_10977”: project = "ATB Test" AND type = Bug
};

/* ---------------------------------------------------------------- */

let BROWSE_BOUND_JQL = “”;

function toOption(issue) {
const key = issue?.key;
const summary = issue?.fields?.summary || “”;
return { value: key, label: summary ? ${key} ${summary} : key };
}

const Edit = () => {
const [selected, setSelected] = useState(null);
const [options, setOptions] = useState();
const [inputValue, setInputValue] = useState(“”);

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const [nextPageToken, setNextPageToken] = useState(null);
const lastJqlRef = useRef(“”);

const debounceTimer = useRef(null);
const abortRef = useRef(null);
const initDoneRef = useRef(false);

const cacheRef = useRef(new Map());

const jiraSearchJql = useCallback(async ({ jql, token }) => {

if (abortRef.current) abortRef.current.abort();
abortRef.current = new AbortController();

const params = new URLSearchParams();
params.set(“jql”, jql);
params.set(“maxResults”, String(PAGE_SIZE));
params.set(“fields”, “summary”);

if (token) params.set(“nextPageToken”, token);

const res = await requestJira(/rest/api/3/search/jql?${params.toString()}, {
method: “GET”,
headers: { Accept: “application/json” },
signal: abortRef.current.signal,
});

const text = await res.text();

if (!res.ok) {
throw new Error(HTTP ${res.status}: ${text.slice(0, 220)});
}

return JSON.parse(text);

}, );

const loadPage = useCallback(
async ({ jql, token = null, reset = false, useCache = true }) => {

setError(null);
setIsLoading(true);

const cacheKey = ${jql}::${token || ""};

try {

if (useCache && cacheRef.current.has(cacheKey)) {

const cached = cacheRef.current.get(cacheKey);

if (reset) {
setOptions(cached.options);
} else {

setOptions((prev) => {

const seen = new Set(prev.map((o) => o.value));
const merged = prev.slice();

for (const opt of cached.options) {
if (opt?.value && !seen.has(opt.value)) {
merged.push(opt);
}
}

return merged;

});
}

setNextPageToken(cached.nextPageToken || null);
lastJqlRef.current = jql;

return;
}

const data = await jiraSearchJql({ jql, token });

const issues = data?.issues || ;
const newOptions = issues.map(toOption);

setOptions((prev) => {

const base = reset ? : prev.slice();
const seen = new Set(base.map((o) => o.value));

if (selected?.value && !seen.has(selected.value)) {
base.unshift(selected);
seen.add(selected.value);
}

for (const opt of newOptions) {
if (opt?.value && !seen.has(opt.value)) {
base.push(opt);
seen.add(opt.value);
}
}

return base;

});

const nextTok = data?.nextPageToken || null;

setNextPageToken(nextTok);
lastJqlRef.current = jql;

cacheRef.current.set(cacheKey, {
options: newOptions,
nextPageToken: nextTok,
});

} catch (e) {

if (String(e?.name) === “AbortError”) return;

console.error(e);

const msg = e?.message || String(e);

setError(
msg.includes(“HTTP 429”)
? “Rate limited (429). Slow down requests.”
: msg
);

} finally {
setIsLoading(false);
}

},
[jiraSearchJql, selected]
);

useEffect(() => {

if (initDoneRef.current) return;
initDoneRef.current = true;

(async () => {

try {

const ctx = await view.getContext();
const key = ctx?.extension?.fieldValue || null;

/* ------------------------------------------------------ /
/
FIX: Support both fieldId and fieldKey from Forge /
/
------------------------------------------------------ */

const fieldKey =
ctx?.extension?.fieldId ||
ctx?.extension?.fieldKey;

BROWSE_BOUND_JQL = JQL_MAP[fieldKey];

if (!BROWSE_BOUND_JQL) {
console.log(“No JQL mapping found for field:”, fieldKey);
BROWSE_BOUND_JQL = project is not EMPTY;
}

/* ------------------------------------------------------ */

if (key) {

const pre = { value: key, label: key };

setSelected(pre);
setOptions([pre]);

}

const jql = ${BROWSE_BOUND_JQL} ORDER BY updated DESC;

await loadPage({
jql,
reset: true,
useCache: true
});

} catch (e) {

console.error(e);
setError(e?.message || String(e));

}

})();

return () => {

if (debounceTimer.current) clearTimeout(debounceTimer.current);
if (abortRef.current) abortRef.current.abort();

};

}, [loadPage]);

const searchByText = useCallback(
async (text) => {

const q = (text || “”).trim();

if (q.length < 2) {

const jql = ${BROWSE_BOUND_JQL} ORDER BY updated DESC;

await loadPage({
jql,
reset: true,
useCache: true
});

return;
}

const jql =
${BROWSE_BOUND_JQL} AND summary ~ "${q}*" ORDER BY updated DESC;

await loadPage({
jql,
reset: true,
useCache: true
});

},
[loadPage]
);

const onInputChange = useCallback(

(newValue) => {

setInputValue(newValue);

if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}

debounceTimer.current = setTimeout(() => {
searchByText(newValue).catch(console.error);
}, DEBOUNCE_MS);

},
[searchByText]
);

const onChange = useCallback((opt) => {
setSelected(opt || null);
}, );

const onLoadMore = useCallback(async () => {

if (!nextPageToken) return;

await loadPage({
jql: lastJqlRef.current,
token: nextPageToken,
reset: false,
useCache: true,
});

}, [loadPage, nextPageToken]);

const onSubmit = useCallback(async () => {
await view.submit(selected?.value || null);
}, [selected]);

return (

{nextPageToken && (

        Load more
      
    )}

{error && (

        {error}
      
    )}

);
};

ForgeReconciler.render(
<React.StrictMode>

</React.StrictMode>
);

Manifest.yml -

modules:
jira:customField:

  • key: bug-picker
    name: Bug Picker
    description: Issue Picker
    type: string
    view:
    render: native
    resource: main
    experience:
  • issue-view
  • portal-view
    edit:
    resource: edit
    render: native
    isInline: true
    experience:
  • issue-create
  • issue-transition
  • issue-view
  • portal-request
- key: procurement-task-picker
  name: Procurement Tasks
  description: Issue Picker Field
  type: string
  view:
    render: native
    resource: main
    experience:
      - issue-view
      - portal-view
  edit:
    resource: edit 
    render: native
    isInline: true
    experience:
      - issue-create
      - issue-transition
      - issue-view
      - portal-request

- key: Picker-1
  name: Tasks-1
  description: Issue Picker Field
  type: string
  view:
    render: native
    resource: main
    experience:
      - issue-view
      - portal-view
  edit:
    resource: edit 
    render: native
    isInline: true
    experience:
      - issue-create
      - issue-transition
      - issue-view
      - portal-request

- key: Picker-2
  name: Tasks-2
  description: Issue Picker Field
  type: string
  view:
    render: native
    resource: main
    experience:
      - issue-view
      - portal-view
  edit:
    resource: edit 
    render: native
    isInline: true
    experience:
      - issue-create
      - issue-transition
      - issue-view
      - portal-request

- key: Picker-3
  name: Tasks-3
  description: Issue Picker Field
  type: string
  view:
    render: native
    resource: main
    experience:
      - issue-view
      - portal-view
  edit:
    resource: edit 
    render: native
    isInline: true
    experience:
      - issue-create
      - issue-transition
      - issue-view
      - portal-request

- key: Picker-4
  name: Tasks-4
  description: Issue Picker Field
  type: string
  view:
    render: native
    resource: main
    experience:
      - issue-view
      - portal-view
  edit:
    resource: edit 
    render: native
    isInline: true
    experience:
      - issue-create
      - issue-transition
      - issue-view
      - portal-request

- key: Picker-5
  name: Tasks-5
  description: Issue Picker Field
  type: string
  view:
    render: native
    resource: main
    experience:
      - issue-view
      - portal-view
  edit:
    resource: edit 
    render: native
    isInline: true
    experience:
      - issue-create
      - issue-transition
      - issue-view
      - portal-request

- key: Picker-6
  name: Tasks-6
  description: Issue Picker Field
  type: string
  view:
    render: native
    resource: main
    experience:
      - issue-view
      - portal-view
  edit:
    resource: edit 
    render: native
    isInline: true
    experience:
      - issue-create
      - issue-transition
      - issue-view
      - portal-request

resources:

  • key: main
    path: src/frontend/index.jsx

  • key: edit
    path: src/frontend/edit.jsx

app:
runtime:
name: nodejs24.x
memoryMB: 256
architecture: arm64
id: ari:cloud:ecosystem::app/b47cf64a-7bb5-44f4-a652-761f006361ff

permissions:
scopes:

  • read:jira-work
  • read:jira-user

Can you please help me over this & resolve this issue ?

Thanks & Regards,

Yash

Hi Caterina,

I have tested the Code which is there in the Work Item picker custom field in Jira & it’s working for me. But when other users are trying to access that field, they are facing an issue such that it asking them to allow access. And after allowing access, it shows like below -

Thanks & Regards,

Yash

Hi @CaterinaCurti ,

After clicking on Allow Access, it shows like this to the other users -

Please help me to fix this & whats blocking other users to access this.

Thanks & Regards,

Yash

Hi @CaterinaCurti ,

We were able to resolve the above issues which we added within the screenshots.

We are now not able to use the Custom Field within the JSM Project & Portal. Please confirm & help over this.

Regards,

Yash

Based on my understanding and recent tests, the “For this app to display, you need to grant it access to Atlassian apps on your behalf.” is shown when the app is deployed in the Forge DEV environment and the user accessing it is not one of its contributors.

Depending on who is accessing the app, the way to address it is:

I think this might be how this was solved.

Great to hear @YashBhandarkar about the issue above being resolved.

What is the issue in the JSM Project/Space and Portal? Is the field not available or is there an error?

Please also note that Forge Custom Fields are only available as fields directly but they can’t be used in forms.

I just did a test and I can see the custom field in my portal.

Cheers,
Caterina

Hi @CaterinaCurti ,

All issues have been resolved but we have come up with a use case over which we wanted your help/inputs. Please find it below -

So the Work Item Picker Custom Field in Jira solution which you provided worked wonderfully for us. Now the thing over which we wanted help is that when we are searching/viewing the field details in the JQL, when seeing in the columns for the details of the tickets, the tickets associated with the Work Item Picker shows “Work Item ID” & not “Work Item Key” which affects when user tries to search the same in the JQL as shown below in the attached screenshots. Please help over this & how we can achieve the JQL results for the Work Item Picker with Key & also in the Column details of the work items, it should show Key.

Regards,

Yash

Hello @YashBhandarkar ,
To display and be able to search by Work Item Key instead of Work Item Id, these are the following two changes to be applied.

Validation expression
The last part of the validation expression is currently set to value.match('^\d+$') != null to check for a number. When saving the value as an Work Item Key, that needs to be changed to match the key format so value.match('^[A-Z][A-Z0-9]+-\d+$') != null.

Here is how the validation needs to be set in the manifest.yml:

validation:
  expression: value == null || value == "" || value.match('^[A-Z][A-Z0-9]+-\d+$') != null

Saving the Key value instead of the Id
In the useCallback function (line 50 of the edit.jsx) file, the value being set needs to be changed from value: issue.id to value: issue.key:

if (data.issues) {
  const issueOptions = data.issues.map(issue => ({
    label: `${issue.key} - ${issue.fields?.summary}`,
    value: issue.key
  }));

By applying these two changes, the list view will show the Work Item Key:

And the users will be able to use the Work Item Key in the JQL search:

Let me know how this goes,
Caterina

Hi @CaterinaCurti ,

I did tried with the changes you mentioned but it wasnt working for me. It was showing as JQL is missing or something like that error.

But I found some way to make some changes in the code & it worked for me.

Now I am able to search the Jira Work Items based on Key.

I have one more question - I can use this Work Item Picker field on JSM Customer Portal but also wanted to check that can this be used on JSM Forms? Can you confirm over this once?

Please let us know.

Thanks & Regards,

Yash

Great job @YashBhandarkar in getting it to work.

I can confirm that Forge Custom Fields are not available to be used in JSM Forms.
I raised this request some time ago:

Caterina