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}`);
}
};
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>
);
};
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>
<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>
);
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.