Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Can we move subtask from parent A to parent B using forge ?

Manar El Kefi July 19, 2024

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>
  );

2 answers

0 votes
Nathan Phillips
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 22, 2024

Hello @Manar El Kefi,

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, a direct link to the Forge developer community can be found here

0 votes
Tuncay Senturk
Community Champion
July 22, 2024

Hi @Manar El Kefi 

Welcome to the Community!

I believe you'll reach out to more Forge developers in the Atlassian Developer Community.

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
CLOUD
PRODUCT PLAN
STANDARD
TAGS
AUG Leaders

Atlassian Community Events