Define custom layout per project with forge

Manar El Kefi August 7, 2024

Hello , I want to customize layout per project , so I created  a module : jira project setting where there is a form to select the columns that the user want to belayed in subtask panel . The problem is that the subtasks are not displayed and there is the error : 

Error fetching issue: {"code":401,"message":"Unauthorized; scope does not match"} Error loading panel data: Error: Failed to fetch issue

manifest.yml : 

 

modules:
  jira:projectSettingsPage:
    - key: subtask-column-config
      function: configFunction
      title: "Subtask Column Configuration"
      layout: basic
  jira:issuePanel:
    - key: subtask-panel
      title: My Issue Panel
      function: panelFunction
  function:
    - key: configFunction
      handler: index.configFunction
    - key: panelFunction
      handler: index.panelFunction
permissions:
  scopes:
    - read:jira-user
    - write:jira-work
    - read:jira-work
    - manage:jira-project
app:
  runtime:
    name: nodejs18.x
  id: ari:cloud:ecosystem::app/00ba7460-efec-42a5-ac54-247377f9ff23
  licensing:
    enabled: true
index.js : import ForgeUI, { render, ProjectSettingsPage, Fragment, Text, Table, Head, Cell, Row, Link, Button, Select, Option, Form, useState, useEffect, useProductContext, IssuePanel } from '@forge/ui';
import api, { route } from '@forge/api';

// Fonction pour récupérer les détails d'une issue par sa clé
const fetchIssueByKey = async (issueKey) => {
  const url = route(`/rest/api/3/issue/${issueKey}`);
  const response = await api.asApp().requestJira(url);

  if (!response.ok) {
    const errorMessage = await response.text();
    console.error('Error fetching issue:', errorMessage);
    throw new Error('Failed to fetch issue');
  }

  const data = await response.json();
  return data;
};

// Fonction de test de l'API
const testApiCall = async () => {
  try {
    const url = route('/rest/api/3/myself');
    const response = await api.asApp().requestJira(url);

    if (!response.ok) {
      const errorMessage = await response.text();
      console.error('Test API Call Response Error:', errorMessage);
      throw new Error('Failed to test API call');
    }

    const data = await response.json();
    console.log('Test API Call Response:', data);
  } catch (error) {
    console.error('Error in Test API Call:', error);
  }
};

// Configuration Panel Function
const ConfigPanel = () => {
  const [columns, setColumns] = useState([]);

  const handleSubmit = async (formData) => {
    const projectKey = formData.projectKey;
    const selectedColumns = formData.columns;
    const url = route(`/rest/api/3/project/${projectKey}/properties/subtask-columns`);
   
    try {
      const response = await api.asApp().requestJira(url, {
        method: 'PUT',
        body: JSON.stringify({ columns: selectedColumns }),
      });

      if (!response.ok) {
        const errorMessage = await response.text();
        console.error('Error saving configuration:', errorMessage);
        throw new Error('Failed to save configuration');
      }
    } catch (error) {
      console.error('Error in handleSubmit:', error);
    }

    // Réinitialisation du formulaire
    setColumns([]);
  };

  return (
    <ProjectSettingsPage>
      <Fragment>
        <Text content="Configure Subtask Columns" />
        <Form onSubmit={handleSubmit}>
          <Select
            label="Select Columns"
            isMulti
            name="columns"
            value={columns}
            onChange={(values) => setColumns(values)}
          >
            <Option value="Key" label="Key" />
            <Option value="Summary" label="Summary" />
            <Option value="Status" label="Status" />
            <Option value="Assignee" label="Assignee" />
            <Option value="Priority" label="Priority" />
            <Option value="Created" label="Created" />
          </Select>
          <Button text="Save" type="submit" />
        </Form>
      </Fragment>
    </ProjectSettingsPage>
  );
};

// Récupération de la configuration du layout
const fetchLayoutConfig = async (projectKey) => {
  const url = route(`/rest/api/3/project/${projectKey}/properties/subtask-columns`);
  const response = await api.asApp().requestJira(url);

  if (!response.ok) {
    const errorMessage = await response.text();
    console.error('Error fetching layout config:', errorMessage);
    throw new Error('Failed to fetch layout config');
  }

  const data = await response.json();
  return data.columns || ['Key', 'Summary', 'Status', 'Assignee', 'Priority', 'Created'];
};

// Récupération des sous-tâches
const fetchSubtasks = async (issueKey) => {
  const response = await api.asApp().requestJira(route(`/rest/api/3/search?jql=parent=${issueKey}&fields=summary,status,assignee,priority,created`));

  if (!response.ok) {
    const errorMessage = await response.text();
    console.error('Error fetching subtasks:', errorMessage);
    throw new Error('Failed to fetch subtasks');
  }

  const data = await response.json();
  return data.issues || [];
};

// Fonction pour le panneau de sous-tâches
const SubtaskPanel = () => {
  const { platformContext: { issueKey } } = useProductContext();
  const [subtasks, setSubtasks] = useState([]);
  const [layout, setLayout] = useState([]);

  useEffect(async () => {
    if (!issueKey) {
      console.error('No issueKey found in context.');
      return;
    }

    try {
      const issue = await fetchIssueByKey(issueKey);
      if (!issue.fields || !issue.fields.project) {
        console.error('Issue or project data is missing.');
        return;
      }
     
      const projectKey = issue.fields.project.key;
      const layoutConfig = await fetchLayoutConfig(projectKey);

      setLayout(layoutConfig);
      const subtasks = await fetchSubtasks(issueKey);
      setSubtasks(subtasks);
    } catch (error) {
      console.error('Error loading panel data:', error);
    }
  }, [issueKey]);

  return (
    <Fragment>
      <Text content="Subtasks" />
      <Table>
        <Head>
          {layout.includes('Key') && <Cell><Text content="Key" /></Cell>}
          {layout.includes('Summary') && <Cell><Text content="Summary" /></Cell>}
          {layout.includes('Status') && <Cell><Text content="Status" /></Cell>}
          {layout.includes('Assignee') && <Cell><Text content="Assignee" /></Cell>}
          {layout.includes('Priority') && <Cell><Text content="Priority" /></Cell>}
          {layout.includes('Created') && <Cell><Text content="Created" /></Cell>}
        </Head>
        {subtasks.map(subtask => (
          <Row key={subtask.id}>
            {layout.includes('Key') && (
              <Cell>
                <Text>
                  <Link href={`/browse/${subtask.key}`}>{subtask.key}</Link>
                </Text>
              </Cell>
            )}
            {layout.includes('Summary') && <Cell><Text content={subtask.fields.summary} /></Cell>}
            {layout.includes('Status') && <Cell><Text content={subtask.fields.status.name} /></Cell>}
            {layout.includes('Assignee') && <Cell><Text content={subtask.fields.assignee ? subtask.fields.assignee.displayName : 'Unassigned'} /></Cell>}
            {layout.includes('Priority') && <Cell><Text content={subtask.fields.priority ? subtask.fields.priority.name : 'N/A'} /></Cell>}
            {layout.includes('Created') && <Cell><Text content={new Date(subtask.fields.created).toLocaleString()} /></Cell>}
          </Row>
        ))}
      </Table>
    </Fragment>
  );
};

// Export Functions
export const configFunction = render(<ConfigPanel />);
export const panelFunction = render(<IssuePanel><SubtaskPanel /></IssuePanel>);

1 answer

0 votes
Lígia Zanchet
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 12, 2024

Hello Manar El Kefi

For module questions, you can redirect to our Developer community here: 
https://developer.atlassian.com/platform/forge/get-help/#ask-the-community

They should be able to assist you properly.

Thanks 

Suggest an answer

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

Atlassian Community Events