Forums

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

How to restrict the creation of subtasks when the parent ticket is finished ?

Manar El Kefi July 17, 2024

The main idea is to restrict the creation of subtasks when the parent ticket is finished, so I created a creation form in a panel. The problem is that the issue type options are not displayed, and there is the following error: TypeError: Cannot read properties of undefined (reading 'prop') at Object.panel (webpack://jira-project-page-ui-kit/node_modules/@forge/ui/out/backend-runtime.js:37:1)   

Here is the code :

 

import ForgeUI, { useProductContext, IssuePanel, render, Fragment, Text, Button, useState, useEffect, Table, Head, Cell, Row, Link, TextField, Form, Select, Option } 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 || [];
  };
 
  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",
    });
  };
 
  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);
 
    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);
        console.log("Filtered Issue Types:", issueTypes); // Log filtered issue types
        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) => {
      await deleteIssue(issueKey);
      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));
      setCurrentPage(1);
    };
 
    const handlePageChange = (pageNumber, issues, setFilteredIssues) => {
      const startIndex = (pageNumber - 1) * PAGE_SIZE;
      const endIndex = startIndex + PAGE_SIZE;
      setFilteredIssues(issues.slice(startIndex, endIndex));
      setCurrentPage(pageNumber);
    };
 
    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);
    };
 
    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:**" />
                <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)} />
                  </Cell>
                </Row>
              ))}
            </Table>
            {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>
 
            <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>
 
         
          </Fragment>
        )}
      </Fragment>
    );
  };
 
  export const panel = render(
    <IssuePanel>
      <Panel />
    </IssuePanel>
  );

1 answer

1 vote
Rilwan Ahmed
Community Champion
July 17, 2024

Hi @Manar El Kefi ,

Welcome to the community !!

I have a simple workaround.

In the Subtask workflow, for the create transition, add 'Parent Status Validator' and give all the status that are not 'Finished' and publish the workflow.

This validator will allow users to create subtask only when the status of the parent ticket is not finished

image.png

Suggest an answer

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

Atlassian Community Events