Can we move subtask from parent A to parent B using forge ?
Asked by Manar El Kefi:

Hello , I want to move subtasks from parent to another using forge , but the problem is when i click on the moving icon , there's no change . I don't know why , here is the code of my forge app :

import ForgeUI, { useProductContext, IssuePanel, render, Fragment, Text, Button, useState, useEffect, Table, Head, Cell, Row, Link, TextField, Form, Select, Option, ModalDialog } from '@forge/ui';  
import api, { route } from '@forge/api';

const PAGE_SIZE = 3;

const fetchChildIssues = async (issueKey) =\> {  
const response = await api.asApp().requestJira(route\`/rest/api/3/search?jql=parent=${issueKey}\`);  
const data = await response.json();  
return data.issues \|\| \[\]; // Ensure data.issues is handled properly  
};

const fetchLinkedIssues = async (issueKey) =\> {  
const response = await api.asApp().requestJira(route\`/rest/api/3/search?jql=issue in linkedIssues(${issueKey})\`);  
const data = await response.json();  
return data.issues \|\| \[\];  
};  
const fetchIssueTypes = async (projectId) =\> {  
const response = await api.asApp().requestJira(route\`/rest/api/3/issuetype\`);  
const data = await response.json();  
console.log("Issue Types Response:", data);  
return data.issueTypes ? data.issueTypes.filter((issueType) =\> issueType.subtask) : \[\];  
};  
const createSubtask = async (parentKey, summary, assignee, priority, issueTypeId) =\> {  
const projectResponse = await api.asApp().requestJira(route\`/rest/api/3/issue/${parentKey}\`);  
const projectData = await projectResponse.json();  
const projectId = projectData.fields.project.id;  
const response = await api.asApp().requestJira(route\`/rest/api/3/issue\`, {  
method: "POST",  
body: JSON.stringify({  
fields: {  
project: { id: projectId },  
parent: { key: parentKey },  
summary: summary,  
assignee: { id: assignee },  
priority: { id: priority },  
issuetype: { id: issueTypeId },  
},  
}),  
});  
return response.json();  
};  
const fetchUsers = async () =\> {  
const response = await api.asApp().requestJira(route\`/rest/api/3/user/search?query=\`);  
const data = await response.json();  
return data.map((user) =\> ({ id: user.accountId, displayName: user.displayName }));  
};  
const fetchPriorities = async () =\> {  
const response = await api.asApp().requestJira(route\`/rest/api/3/priority\`);  
const data = await response.json();  
return data;  
};  
const deleteIssue = async (issueKey) =\> {  
await api.asApp().requestJira(route\`/rest/api/3/issue/${issueKey}\`, {  
method: "DELETE",  
});  
};  
// Fonction pour r?cup?rer les d?tails d'une issue par sa cl?  
const fetchIssueByKey = async (issueKey) =\> {  
const response = await api.asApp().requestJira(route\`/rest/api/3/issue/${issueKey}\`);  
if (!response.ok) {  
throw new Error(\`Failed to fetch issue ${issueKey}: ${response.statusText}\`);  
}  
return await response.json();  
};

// Fonction pour mettre ? jour l'issue avec un nouveau parent  
const updateParentIssue = async (issueKey, newParentKey) =\> {  
const issue = await fetchIssueByKey(issueKey);  
const updatePayload = {  
fields: {  
parent: { key: newParentKey }  
}  
};  
const response = await api.asApp().requestJira(route\`/rest/api/3/issue/${issueKey}\`, {  
method: 'PUT',  
body: JSON.stringify(updatePayload),  
headers: {  
'Content-Type': 'application/json'  
}  
});

if (!response.ok) {  
throw new Error(\`Failed to update issue ${issueKey}: ${response.statusText}\`);  
}  
};

<br />

<br />

const MoveSubtaskModal = ({ issueKey, onClose, handleMoveSubtask }) =\> {  
const \[newParentKey, setNewParentKey\] = useState('');  
const \[error, setError\] = useState(null);

const handleSubmit = async () =\> {  
try {  
await handleMoveSubtask(issueKey, newParentKey);  
onClose();  
} catch (error) {  
setError(error.message);  
}  
};

return (  
\<ModalDialog header="D?placer la sous-t?che vers un nouveau parent" onClose={onClose}\>  
\<Form onSubmit={handleSubmit}\>  
\<Text content="Entrez la cl? du nouveau parent pour d?placer cette sous-t?che :" /\>  
\<TextField name="newParentKey" label="Cl? du nouveau parent" value={newParentKey} onChange={setNewParentKey} /\>  
{error \&\& \<Text content={\`Erreur : ${error}\`} /\>}  
\<Button text="D?placer" type="submit" /\>  
\</Form\>  
\</ModalDialog\>  
);  
};

<br />

<br />

<br />

<br />

const Panel = () =\> {  
const { platformContext: { issueKey } } = useProductContext();  
const \[childIssues, setChildIssues\] = useState(\[\]);  
const \[linkedIssues, setLinkedIssues\] = useState(\[\]);  
const \[filteredChildIssues, setFilteredChildIssues\] = useState(\[\]);  
const \[filteredLinkedIssues, setFilteredLinkedIssues\] = useState(\[\]);  
const \[isPanelVisible, setPanelVisible\] = useState(true);  
const \[currentPage, setCurrentPage\] = useState(1);  
const \[parentIssueStatus, setParentIssueStatus\] = useState('');  
const \[issueTypes, setIssueTypes\] = useState(\[\]);  
const \[users, setUsers\] = useState(\[\]);  
const \[priorities, setPriorities\] = useState(\[\]);  
const \[isCreating, setIsCreating\] = useState(false);  
const \[isFormVisible, setFormVisible\] = useState(false);  
const \[showConfirmationDialog, setShowConfirmationDialog\] = useState(false);  
const \[issueToDelete, setIssueToDelete\] = useState(null);  
const \[selectedIssue, setSelectedIssue\] = useState(null);  
const \[isModalOpen, setModalOpen\] = useState(false);

const handleMoveSubtask = async (issueKey, newParentKey) =\> {  
try {  
await updateParentIssue(issueKey, newParentKey);  
const updatedChildIssues = await fetchChildIssues(issueKey);  
setChildIssues(updatedChildIssues);  
setFilteredChildIssues(updatedChildIssues.slice(0, PAGE_SIZE));  
setModalOpen(false);  
} catch (error) {  
console.error('Error handling move subtask:', error);  
}  
};

useEffect(async () =\> {  
try {  
const childIssues = await fetchChildIssues(issueKey);  
const linkedIssues = await fetchLinkedIssues(issueKey);  
setChildIssues(childIssues);  
setLinkedIssues(linkedIssues);  
setFilteredChildIssues(childIssues.slice(0, PAGE_SIZE));  
setFilteredLinkedIssues(linkedIssues.slice(0, PAGE_SIZE));

const response = await api.asApp().requestJira(route\`/rest/api/3/issue/${issueKey}\`);  
const data = await response.json();  
const parentStatus = data.fields.status.name;  
setParentIssueStatus(parentStatus);

const issueTypes = await fetchIssueTypes(data.fields.project.id);  
setIssueTypes(issueTypes);

const users = await fetchUsers();  
setUsers(users);

const priorities = await fetchPriorities();  
setPriorities(priorities);  
} catch (error) {  
console.error('Error during useEffect:', error);  
}  
}, \[issueKey\]);  
const handleFilter = (formData, issues, setFilteredIssues) =\> {  
const query = formData.query.toLowerCase();  
const filtered = issues.filter(  
(issue) =\>  
issue.fields.summary.toLowerCase().includes(query) \|\|  
(issue.fields.assignee \&\& issue.fields.assignee.displayName.toLowerCase().includes(query)) \|\|  
issue.fields.status.name.toLowerCase().includes(query) \|\|  
issue.key.toLowerCase().includes(query)  
);  
setFilteredIssues(filtered.slice(0, PAGE_SIZE));  
setCurrentPage(1);  
};  
const handleDeleteIssue = async (issueKey) =\> {  
setIssueToDelete(issueKey);  
setShowConfirmationDialog(true);  
};  
const confirmDeleteIssue = async () =\> {  
try {  
await deleteIssue(issueToDelete);  
setShowConfirmationDialog(false);  
const updatedChildIssues = await fetchChildIssues(issueKey);  
const updatedLinkedIssues = await fetchLinkedIssues(issueKey);  
setChildIssues(updatedChildIssues);  
setLinkedIssues(updatedLinkedIssues);  
setFilteredChildIssues(updatedChildIssues.slice(0, PAGE_SIZE));  
setFilteredLinkedIssues(updatedLinkedIssues.slice(0, PAGE_SIZE));  
setCurrentPage(1);  
} catch (error) {  
console.error("Error confirming issue deletion:", error);  
}  
};  
const cancelDeleteIssue = () =\> {  
setShowConfirmationDialog(false);  
setIssueToDelete(null);  
};  
const handleIssueClick = (issue) =\> {  
setSelectedIssue(issue);  
};

const closeIssueDetail = () =\> {  
setSelectedIssue(null);  
};  
const handlePageChange = (pageNumber, issues, setFilteredIssues) =\> {  
const startIndex = (pageNumber - 1) \* PAGE_SIZE;  
const endIndex = startIndex + PAGE_SIZE;  
setFilteredIssues(issues.slice(startIndex, endIndex));  
setCurrentPage(pageNumber);  
};  
const handleCreateButtonClick = () =\> {  
setFormVisible(true); // D?finit l'?tat pour afficher le formulaire lorsqu'on clique sur "Cr?er"  
};  
const handleCreateSubtask = async (formData) =\> {  
const { summary, assignee, priority, issueTypeId } = formData;  
setIsCreating(true);  
await createSubtask(issueKey, summary, assignee, priority, issueTypeId);  
const childIssues = await fetchChildIssues(issueKey);  
setChildIssues(childIssues);  
setFilteredChildIssues(childIssues.slice(0, PAGE_SIZE));  
setIsCreating(false);  
setFormVisible(false); // Apr?s cr?ation, masque le formulaire  
};  
const canCreateSubtask = parentIssueStatus !== "Termin?";  
return (  
\<Fragment\>  
\<Button text={isPanelVisible ? "Masquer les tickets enfants et li?s" : "Afficher les tickets enfants et li?s"} onClick={() =\> setPanelVisible(!isPanelVisible)} /\>  
{isPanelVisible \&\& (  
\<Fragment\>  
{canCreateSubtask \&\& (  
\<Fragment\>  
\<Text content="\*\*Cr?er une sous-t?che:\*\*" /\>  
{!isFormVisible \&\& ( // Affiche le bouton de cr?ation uniquement si le formulaire n'est pas d?j? visible  
\<Button text="Cr?er" onClick={handleCreateButtonClick} /\>  
)}  
{isFormVisible \&\& ( // Affiche le formulaire si isFormVisible est true  
\<Form onSubmit={handleCreateSubtask}\>  
\<TextField name="summary" label="R?sum?" isRequired /\>  
\<Select label="Type de sous-t?che" name="issueTypeId" isRequired\>  
{issueTypes.map((issueType) =\> (  
\<Option key={issueType.id} value={issueType.id} label={issueType.name} /\>  
))}  
\</Select\>  
\<Select label="Assign? ?" name="assignee" isRequired\>  
{users.map((user) =\> (  
\<Option key={user.id} value={user.id} label={user.displayName} /\>  
))}  
\</Select\>  
\<Select label="Priorit?" name="priority" isRequired\>  
{priorities.map((priority) =\> (  
\<Option key={priority.id} value={priority.id} label={priority.name} /\>  
))}  
\</Select\>  
\<Button text="Cr?er" type="submit" isDisabled={isCreating} /\>  
\</Form\>  
)}  
\</Fragment\>  
)}  
\<Text content="\*\*Tickets Enfants:\*\*" /\>  
{filteredChildIssues.length === 0 \&\& \<Text\>Aucun ticket enfant trouv?.\</Text\>}  
\<Table\>  
\<Head\>  
\<Cell\>  
\<Text\>  
\<strong\>Key\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Summary\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Status\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Assignee\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Priority\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Created\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Actions\</strong\>  
\</Text\>  
\</Cell\>  
\</Head\>  
{filteredChildIssues.map((issue) =\> (  
\<Row key={issue.id}\>  
\<Cell\>  
\<Text\>  
\<Link href={\`/browse/${issue.key}\`}\>{issue.key}\</Link\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>{issue.fields.summary}\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>{issue.fields.status.name}\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>{issue.fields.assignee ? issue.fields.assignee.displayName : "Non assign?"}\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>{issue.fields.priority ? issue.fields.priority.name : "N/A"}\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>{new Date(issue.fields.created).toLocaleString()}\</Text\>  
\</Cell\>  
\<Cell\>  
\<Button text="??" onClick={() =\> handleDeleteIssue(issue.key)} /\>  
\<Button text="Details" onClick={() =\> handleIssueClick(issue)} /\>  
\<Button text="?" onClick={() =\> setModalOpen(true)} /\>  
\</Cell\>  
\</Row\>  
))}  
\</Table\>  
{showConfirmationDialog \&\& (  
\<ModalDialog header="Confirm" onClose={cancelDeleteIssue}\>  
\<Text content={\`Would you like to delete ticket ${issueToDelete} ?\`} /\>  
\<Button text="Yes" onClick={confirmDeleteIssue} /\>  
\<Button text="No" onClick={cancelDeleteIssue} /\>  
\</ModalDialog\>  
)}  
{selectedIssue \&\& (  
\<ModalDialog header="D?tails du ticket" onClose={closeIssueDetail}\>  
\<Text content={\`Key: ${selectedIssue.key}\`} /\>  
\<Text content={\`Summary: ${selectedIssue.fields.summary}\`} /\>  
\<Text content={\`Assignee: ${selectedIssue.fields.assignee ? selectedIssue.fields.assignee.displayName : "Unassigned"}\`} /\>  
\<Text content={\`Status: ${selectedIssue.fields.status.name}\`} /\>  
\<Text content={\`Priority: ${selectedIssue.fields.priority.name}\`} /\>  
\<Text content={\`Description: ${selectedIssue.fields.description ? selectedIssue.fields.description : "No description"}\`} /\>  
\<Button text="Close" onClick={closeIssueDetail} /\>  
\</ModalDialog\>  
)}  
{isModalOpen \&\& (  
\<MoveSubtaskModal  
issueKey={issueKey}  
onClose={() =\> setModalOpen(false)}  
handleMoveSubtask={handleMoveSubtask}  
/\>  
)}  
{childIssues.length \> PAGE_SIZE \&\& (  
\<Fragment\>  
{Array.from({ length: Math.ceil(childIssues.length / PAGE_SIZE) }, (_, index) =\> (  
\<Button key={index + 1} text={index + 1} onClick={() =\> handlePageChange(index + 1, childIssues, setFilteredChildIssues)} style={{ marginRight: "5px" }} /\>  
))}  
\</Fragment\>  
)}  
\<Form onSubmit={(formData) =\> handleFilter(formData, childIssues, setFilteredChildIssues)}\>  
\<TextField name="query" label="Rechercher par Key, Summary, Status, Assignee" /\>  
\</Form\>  
\<Fragment\>  
{/\* \<Button text="D?placer le sous-t?che" onClick={handleOpenModal} /\> \*/}  
{/\* {isModalOpen \&\& \<MoveSubtaskModal issueKey={issueKey} onClose={handleCloseModal} /\>} \*/}  
\</Fragment\>

<br />

\<Text content="\*\*Tickets Li?s:\*\*" /\>  
{filteredLinkedIssues.length === 0 \&\& \<Text\>Aucun ticket li? trouv?.\</Text\>}  
\<Table\>  
\<Head\>  
\<Cell\>  
\<Text\>  
\<strong\>Key\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Summary\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Status\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Assignee\</strong\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>  
\<strong\>Actions\</strong\>  
\</Text\>  
\</Cell\>  
\</Head\>  
{filteredLinkedIssues.map((issue) =\> (  
\<Row key={issue.id}\>  
\<Cell\>  
\<Text\>  
\<Link href={\`/browse/${issue.key}\`}\>{issue.key}\</Link\>  
\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>{issue.fields.summary}\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>{issue.fields.status.name}\</Text\>  
\</Cell\>  
\<Cell\>  
\<Text\>{issue.fields.assignee ? issue.fields.assignee.displayName : "Non assign?"}\</Text\>  
\</Cell\>  
\<Cell\>  
\<Button text="??" onClick={() =\> handleDeleteIssue(issue.key)} /\>  
\</Cell\>  
\</Row\>  
))}  
\</Table\>  
{linkedIssues.length \> PAGE_SIZE \&\& (  
\<Fragment\>  
{Array.from({ length: Math.ceil(linkedIssues.length / PAGE_SIZE) }, (_, index) =\> (  
\<Button key={index + 1} text={index + 1} onClick={() =\> handlePageChange(index + 1, linkedIssues, setFilteredLinkedIssues)} style={{ marginRight: "5px" }} /\>  
))}  
\</Fragment\>  
)}  
\<Form onSubmit={(formData) =\> handleFilter(formData, linkedIssues, setFilteredLinkedIssues)}\>  
\<TextField name="query" label="Rechercher par Key, Summary, Status, Assignee" /\>  
\</Form\>  
{/\* Links to JXL views \*/}  
\<Text content="\*\*View in JXL:\*\*" /\>  
\</Fragment\>  
)}  
\</Fragment\>  
);  
};  
export const panel = render(  
\<IssuePanel\>  
\<Panel /\>  
\</IssuePanel\>  
);


Answers:
author: Nathan Phillips
body: Hello [@Manar El Kefi](/t5/user/viewprofilepage/user-id/5543953),

Thank you for reaching out on the Atlassian Community.

As suggested by Tuncay, I also agree your question would be petter posed on the Atlassian Developer Community.

As linked above by [@Tuncay Senturk](/t5/user/viewprofilepage/user-id/853260), a direct link to the Forge developer community can be found [here](https://community.developer.atlassian.com/c/forge/45).


author: Tuncay Senturk
body: Hi [@Manar El Kefi](/t5/user/viewprofilepage/user-id/5543953)

Welcome to the Community!

I believe you'll reach out to more Forge developers in the [Atlassian Developer Community](https://community.developer.atlassian.com/c/forge/45).


Tags: cloud

forge

jira-cloud

subtasks