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:
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