Issue with Forge Custom Field configuration in forge App

I have created a Forge app with a custom field that is read-only. Currently, it displays the issue creation date fetched from the Jira API. Now, I want to make this field configurable so that users can choose how they want to see the result—whether in days, hours, or minutes. How can I implement this configurability in my Forge app?

Here is my menifest file : modules:
jira:customField:
- key: custom-field-hello-world
name: custom-field-a
description: A hello world custom field.
type: string
validation:
expression: value == null || !!value.match(“[1]+$”)
errorMessage: The value must consist only of letters
edit:
resource: main
contextConfig:
function: timeSinceCreatedConfig

- key: editable-custom-field
  name: editable-custom-field
  description: A editable custom field custom field.
  type: string
  validation:
    expression: value == null || !!value.match("^[A-Za-z]+$")
    errorMessage: The value must consist only of letters

- key: read-only-custom-field
  name: New-Read-Only-Custom-Field
  description: A Read Only custom field.
  type: string
  # take note that we are using value.function here
  value: 
    function: getAbhi
  readOnly: true
  
- key: time-since-created
  name: time-since-created
  description: A time since created custom field.
  type: string
  value:
    function: getTime
  readOnly: true
  contextConfig:
    function: timeSinceCreatedConfig

function:
- key: getAbhi
handler: index.getAbhi
- key: getTime
handler: index.getTime
- key: timeSinceCreatedConfig
handler: index.timeSinceCreatedConfig

resources:

  • key: main
    path: static/hello-world/build # Ensure this path is correct

permissions:
content:
styles:
- unsafe-inline
scopes:
- read:jira-work
- write:jira-work

app:
runtime:
name: nodejs22.x
and here is my index.js file : import api, { route } from “@forge/api”;
import { timeSinceCreatedConfig } from “./config”; // Import the config function

export { timeSinceCreatedConfig }; // Export it so Forge can use it

// You need to export it so it can be referenced by the manifest
export const getAbhi = (args) => {
// console.log(“Function getAbhi was called”, args);

// Assign the value to the returned issue/s
return args.issues.map(issue => ‘Hello Abhi’);
}

// export async function getTime(event) {
// // const issueIdOrKey = event.context.issueKey;
// const issueIdOrKey = event.issues[0].id;

// console.log(‘issueIdOrKey’, issueIdOrKey)

// const response = await api.asApp().requestJira(route/rest/api/3/issue/${issueIdOrKey}, {
// headers: {
// ‘Accept’: ‘application/json’
// }
// });

// // console.log(‘response’, response)

// const data = await response.json();

// // console.log(‘Data’,data)
// const createdDate = data.fields.created; // Created date from Jira

// console.log(‘createdDate’, createdDate)

// if (!createdDate) {
// return { value: “Unknown” };
// }

// const createdTime = new Date(createdDate);
// console.log(‘createdTime’,createdTime)
// const now = new Date();
// const diffMs = now - createdTime;

// // Convert milliseconds to a human-readable format
// const minutes = Math.floor(diffMs / (1000 * 60));
// const hours = Math.floor(minutes / 60);
// const days = Math.floor(hours / 24);

// let timeSinceCreated;
// if (days > 0) {
// timeSinceCreated = ${days} day${days > 1 ? "s" : ""} ago;
// } else if (hours > 0) {
// timeSinceCreated = ${hours} hour${hours > 1 ? "s" : ""} ago;
// } else {
// timeSinceCreated = ${minutes} minute${minutes > 1 ? "s" : ""} ago;
// }

// console.log(‘New time’,timeSinceCreated)
// console.log(‘New time’,event.issues[0].value)

// // return { value: createdDate };
// return event.issues.map(issue => timeSinceCreated);
// //return event.issues[0].value;
// // return createdDate;
// }

// export async function getTime(event) {
// console.log(‘hehe’,event)
// const issueIdOrKey = event.issues[0].id;
// // const response = await api.asApp().requestJira(route/rest/api/3/issue/${issueIdOrKey}, {
// // headers: { ‘Accept’: ‘application/json’ }
// // });

// const response = await api.asApp().requestJira(route/rest/api/3/issue/${issueIdOrKey}, {
// headers: {
// ‘Accept’: ‘application/json’
// }
// });

// const data = await response.json();
// const createdDate = data.fields.created;
// console.log(‘createdDate’,createdDate)

// if (!createdDate) {
// return { value: “Unknown” };
// }

// const createdTime = new Date(createdDate);
// const now = new Date();
// const diffMs = now - createdTime;

// // Get user-selected format
// // const format = event.config?.format || “days”;
// const format = event.fieldConfig?.format || “days”;
// console.log(‘format’,format)

// let timeSinceCreated;
// if (format === “days”) {
// const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
// timeSinceCreated = ${days} day${days !== 1 ? "s" : ""} ago;
// } else if (format === “hours”) {
// const hours = Math.floor(diffMs / (1000 * 60 * 60));
// timeSinceCreated = ${hours} hour${hours !== 1 ? "s" : ""} ago;
// } else {
// const minutes = Math.floor(diffMs / (1000 * 60));
// timeSinceCreated = ${minutes} minute${minutes !== 1 ? "s" : ""} ago;
// }

// return event.issues.map(issue => timeSinceCreated);
// }

export async function getTime(event) {
try {
console.log(“Event received:”, event);

// Ensure issues exist
if (!event.issues || event.issues.length === 0) {
  console.error("No issues provided.");
  return [];
}

const issueIdOrKey = event.issues[0].id;
const response = await api.asApp().requestJira(route`/rest/api/3/issue/${issueIdOrKey}`, {
  headers: { 'Accept': 'application/json' }
});

if (!response.ok) {
  console.error("Jira API request failed:", response.status, await response.text());
  return event.issues.map(() => "Error fetching issue");
}

const data = await response.json();
const createdDate = data.fields.created;
if (!createdDate) {
  console.error("Issue has no created date.");
  return event.issues.map(() => "Unknown");
}

// Calculate time difference
const createdTime = new Date(createdDate);
const now = new Date();
const diffMs = now - createdTime;

// Fetch config
const format = event.fieldConfig?.format || "days";
console.log("Using format:", format);

let timeSinceCreated;
if (format === "days") {
  const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  timeSinceCreated = `${days} day${days !== 1 ? "s" : ""} ago`;
} else if (format === "hours") {
  const hours = Math.floor(diffMs / (1000 * 60 * 60));
  timeSinceCreated = `${hours} hour${hours !== 1 ? "s" : ""} ago`;
} else {
  const minutes = Math.floor(diffMs / (1000 * 60));
  timeSinceCreated = `${minutes} minute${minutes !== 1 ? "s" : ""} ago`;
}

console.log("Final time value:", timeSinceCreated);

// Return array for each issue
return event.issues.map(() => timeSinceCreated);

} catch (error) {
console.error(“Error in getTime function:”, error);
return event.issues.map(() => “Error”);
}
}


  1. A-Za-z ↩︎