Define custom layout per project with forge
Asked by Manar El Kefi:

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  
icon: <https://developer.atlassian.com/platform/forge/images/icons/issue-panel-icon.svg>  
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\>);


Answers:
author: L?gia Zanchet
body: Hello [Manar El Kefi](https://community.atlassian.com/t5/user/viewprofilepage/user-id/5543953)  

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


Tags: cloud

custom-layout

forge

jira-cloud