Is anyone has any idea about this problem? Also my code like this:
import React, { useEffect, useState } from 'react';
import ForgeReconciler, { Form, LoadingButton, Text, useProductContext } from '@forge/react';
import { view, invoke } from '@forge/bridge';
import { ModalFormForEditField } from './components/ModalFormForEditField';
const App = () => {
const context = useProductContext();
/* ---------- state ---------- */
const [loading, setLoading] = useState(true);
const [fieldId, setFieldId] = useState(null);
const [fieldValue, setFieldValue] = useState(null);
const [project, setProject] = useState(null);
const [issue, setIssue] = useState();
const [checkboxes, setCheckboxes] = useState({
limitToProject: false,
showKey: false,
showStatus: false,
multiSelect: false,
jqlAsApp: false,
showCount: false,
deleteLink: false,
});
const [name, setName] = useState('');
const [linkType, setLinkType] = useState(null);
const [jql, setJql] = useState('');
/* ---------- pick up context ---------- */
useEffect(() => {
if (!context) return;
console.log('Context:', context.extension);
setFieldId(context.extension?.fieldId ?? null);
setFieldValue(context.extension?.fieldValue ?? null);
setProject(context.extension?.project?.key ?? null);
setIssue(context.extension?.issue?.key ?? null);
}, [context]); // <‑ only re‑run when the reference genuinely changes
/* ---------- load saved config ---------- */
useEffect(() => {
if (!fieldId) { // nothing to look up yet
setLoading(false); // drop the spinner if we end up with no field
return;
}
const load = async () => {
setLoading(true);
const { status, res, message } = await invoke('getStorage', { key: 'issuePicker' });
if (status !== 200 || !Array.isArray(res)) {
console.error(message ?? 'Storage fetch failed');
setLoading(false);
return;
}
const cfg = res.find((item) => item.id === fieldId);
if (!cfg) {
console.warn(`No saved config for ${fieldId}`);
setLoading(false);
return;
}
setCheckboxes({
limitToProject: !!cfg.isCheckedLimitOptionsToCurrentProject,
showKey: !!cfg.isCheckedShowIssueKeyOfSelectedIssues,
showStatus: !!cfg.isCheckedShowStatusOfSelectedIssues,
multiSelect: !!cfg.isCheckedMultiSelect,
jqlAsApp: !!cfg.isCheckedSearchJQLAsApp,
showCount: !!cfg.isCheckedShowIssueCountDuringEdit,
deleteLink: !!cfg.isCheckedDeleteAllLinks
});
setName(cfg.name ?? '');
setLinkType(cfg.selectedLinkType ?? null);
setJql(cfg.jql ?? '');
setLoading(false);
};
load();
}, [fieldId]);
const closeModal = () => view.close();
if (loading) return <LoadingButton appearance='subtle' isLoading={loading} />
return (
<ModalFormForEditField
isMulti={checkboxes.multiSelect}
isJqlAsApp={checkboxes.jqlAsApp}
isCountVisible={checkboxes.showCount}
isShowStatusVisible={checkboxes.showStatus}
isShowKeyVisible={checkboxes.showKey}
isLimitOptionToCurrentProject={checkboxes.limitToProject}
name={name}
fieldValue={fieldValue}
linkType={linkType}
deleteLink={checkboxes.deleteLink}
preDefinedJQL={jql}
project={project}
onClose={closeModal}
issue={issue}
/>
);
};
ForgeReconciler.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import React, { useCallback, useEffect, useState } from "react"
import { Box, Button, Form, Inline, Label, LoadingButton, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle, ModalTransition, Select, Text, xcss } from "@forge/react"
import { invoke, view } from "@forge/bridge";
import { checkIssuesIsNull, defineJql, lozangeStatus, sleep } from "../helper";
import { FIELDS, MAX_RESULTS } from "../constants";
import { ViewField } from "../../components/ViewOfField";
export const ModalFormForEditField = ({
isMulti,
isJqlAsApp,
isCountVisible,
isShowStatusVisible,
isShowKeyVisible,
isLimitOptionToCurrentProject,
name,
fieldValue,
linkType,
deleteLink,
preDefinedJQL,
project,
issue,
onClose,
}) => {
const [searchText, setSearchText] = useState("");
const [options, setOptions] = useState([]);
const [fetchedOptions, setFetchedOptions] = useState([]);
const [issueCount, setIssueCount] = useState(0);
const [isloading, setIsLoading] = useState(false);
const [isCountLoading, setIsCountLoading] = useState(false);
const [lastJql, setLastJql] = useState("")
const [isFromPicker, setIsFromPicker] = useState(true);
const [selectedIssues, setSelectedIssues] = useState([]);
const [loading, setLoading] = useState(false);
const getIssues = useCallback(async () => {
setIsLoading(true);
setIsFromPicker(true);
await sleep(500);
console.log("LINKTYPE:", linkType);
console.log("DELETE LINK:", deleteLink);
console.log("PREDEFINED JQL:", preDefinedJQL)
console.log("IS LIMIT OPTION:", isLimitOptionToCurrentProject)
console.log("PROJECT:", project);
const lastDefinedJQL = defineJql({
payload: {
preDefinedJQL,
isLimitOptionToCurrentProject,
project
}
})
console.log("LAST DEFINED JQL:", lastDefinedJQL);
if (searchText === "") {
if (isCountVisible) {
setIsCountLoading(true);
const cnt = await invoke("issueCount", { jql: lastDefinedJQL, asApp: isJqlAsApp });
if (cnt.status === 200) {
setIssueCount(cnt.res.count);
}
}
setFetchedOptions([]);
setIsLoading(false);
setIsCountLoading(false);
return;
}
//Historysi olan issuelar geliyor ancak kullanıcı bir issue yarattıysa ve hiç girmediyse gelmiyor o yüzden getIssues çağrılıyor
let picker = await invoke("getIssuePicker", { query: searchText, currentJql: lastDefinedJQL, asApp: isJqlAsApp });
let allIssues = checkIssuesIsNull(picker, true);
if (allIssues.length === 0) {
let fallbackJql = isLimitOptionToCurrentProject
? `(issuekey = ${searchText} OR summary ~ ${searchText}\\u002a) AND project = ${project}`
: `(issuekey = ${searchText} OR summary ~ ${searchText}\\u002a)`;
console.log("FALLBACK JQL:", fallbackJql);
const search = await invoke("getIssues", { jql: fallbackJql, nextPageToken: null, maxResults: MAX_RESULTS, fields: FIELDS, asApp: isJqlAsApp });
allIssues = checkIssuesIsNull(search, false);
setIsFromPicker(false);
}
// key'e göre aynı keyden iki tane gözükmemesi için bir Map kullanıyrouz.
const map = new Map();
allIssues.forEach(i => map.set(i.key, i));
console.log("ARRAY FROM: ", Array.from(map.values()));
setFetchedOptions(Array.from(map.values()));
setIsLoading(false);
}, [searchText, preDefinedJQL, isLimitOptionToCurrentProject, project, isJqlAsApp, isCountVisible])
useEffect(() => {
getIssues();
}, []);
useEffect(() => {
if (searchText) {
getIssues();
}
}, [searchText, getIssues]);
useEffect(() => {
if (fetchedOptions.length === 0) setOptions([])
else {
console.log("FETCHED OPTIONS:", fetchedOptions);
const opts = fetchedOptions.map(issue => ({
label: isFromPicker ? `${issue?.key}: ${issue?.summaryText}`
: <><Text weight="bold" as="strong">{issue?.key}</Text>: {issue?.fields?.summary}</>,
value: issue.key
}));
setOptions(opts);
}
}, [fetchedOptions, isFromPicker])
const handleSelectChange = async (options) => {
if (!options || (Array.isArray(options) && options.length === 0)) {
setSelectedIssues([]);
return;
}
const optionArray = Array.isArray(options) ? options : [options];
const jqlClause = `issue in (${optionArray.map(item => `"${item.value}"`).join(", ")})`;
const previewIssues = await invoke('getIssues', {
jql: jqlClause,
nextPageToken: null,
maxResults: MAX_RESULTS,
fields: FIELDS,
asApp: isJqlAsApp
});
if (previewIssues.status === 200 && previewIssues.res?.issues) {
const normalized = previewIssues.res.issues.map(issue => ({
key: issue.key,
summary: issue.fields.summary,
status: issue.fields.status.name
}));
setSelectedIssues(normalized);
} else {
setSelectedIssues([]);
}
}
const handleInputChange = (input) => setSearchText(input)
const onSubmit = async () => {
setLoading(true);
const keys = selectedIssues.map(issue => issue.key);
if (linkType?.value?.linkType === "None") {
console.log("Link type is None, skipping link operations.");
setLoading(false);
await view.submit(keys);
return;
}
if (linkType?.value?.linkType !== "None") {
const linkTypeName = linkType.value.linkTypeName;
const isInward = linkType?.value?.isInwardOrOutward === "inward";
// Kaldırılan issue'ların linklerini sil (deleteLink true ve kaldırılan issue varsa)
if (deleteLink) {
const issueLinks = await invoke('getIssue', {
issueKey: issue,
fields: 'issuelinks'
});
if (issueLinks.status === 200 && issueLinks.res?.fields?.issuelinks) {
const links = issueLinks.res.fields.issuelinks;
// linkTypeName ve yön bilgisine göre eşleşen linkleri filtrele
const linksToDelete = links.filter(link => {
const isMatchingType = link.type.name === linkTypeName;
const isMatchingDirection = isInward
? link.inwardIssue?.key
: link.outwardIssue?.key
return isMatchingType && isMatchingDirection;
});
console.log("LINKS TO DELETE:", linksToDelete);
// Filtrelenen linkleri sil
for (const link of linksToDelete) {
console.log("Deleting link:", link.id);
await invoke('deleteIssueLink', { linkId: link.id });
await sleep(200);
}
} else {
console.error("Failed to fetch issue links for deletion.");
}
}
// Yeni seçilen issue'lar için link oluştur
if (keys.length > 0) {
for (const key of keys) {
if (issue === key) continue; // Kendisiyle link oluşturmayı atla
let payload;
if (isInward) {
payload = {
inwardIssueKey: key,
outwardIssueKey: issue,
linkType: linkTypeName
};
} else {
payload = {
inwardIssueKey: issue,
outwardIssueKey: key,
linkType: linkTypeName
};
}
await invoke("createIssueLink", {
inwardIssueId: payload.inwardIssueKey,
outwardIssueId: payload.outwardIssueKey,
linkType: payload.linkType
});
await sleep(200);
}
}
}
console.log("Submitting keys:", keys);
setLoading(false);
await view.submit(keys);
};
return (
<ModalTransition>
<Modal width={'large'} onClose={onClose}>
<Form onSubmit={onSubmit}>
<ModalHeader>
<ModalTitle>{name} Issue Picker</ModalTitle>
</ModalHeader>
<ModalBody>
<Box>
<Label labelFor="Issue Picker">{isMulti ? "Choose issue(s) for picker" : "Choose an issue for picker"}</Label>
<Select
placeholder="Type issue key or summary"
isMulti={isMulti}
isSearchable
onChange={handleSelectChange}
onInputChange={handleInputChange}
isClearable
isLoading={isloading}
options={options}
/>
{isCountVisible && (isCountLoading ? (
<LoadingButton
appearance="subtle"
isLoading={isCountLoading}
/>
) : (
<Text>
Searching {issueCount} issues that fit the underlying Filter.
</Text>
))}
</Box>
<Box xcss={xcss({
marginBlock: "space.100"
})}>
<Inline alignBlock="center" space="space.100">
<Label labelFor="preview">Preview: </Label>
<ViewField
isKeySelected={isShowKeyVisible}
isStatusSelected={isShowStatusVisible}
issues={selectedIssues}
/>
</Inline>
</Box>
</ModalBody>
<ModalFooter>
<Button appearance="subtle" onClick={onClose}>Close</Button>
<LoadingButton appearance="primary" type="submit" onClick={onSubmit} isLoading={loading}>Submit</LoadingButton>
</ModalFooter>
</Form>
</Modal>
</ModalTransition >
)
}