Custom Field from Forge App with JQL

I recently created my first Forge app. It is a simple text field that counts the number of Helpscout links added to a Jira issue.

My hope is to use this as a way of prioritizing feature requests and bug reports in Jira.

I have successfully

created the field
attached it to an issue
calculated the correct number of links
had the Scout Count field show in JQL
The last problem that I just can’t seem to get is that the JQL cannot search or sort the values of the field. i.e. “Scout Count” > 0 returns nothing, even though there are issues with a correctly calculated number above zero.

I have also determined that the field is set to be searchable and sortable by fetching it.

I know that the fieldID is correct as the JQL search field suggests it

I feel like it has to do with the idea that it’s an unknown field type, but I have not been able to find how to adjust that. The manifest says it’s a number and I’ve tried to apply the type and searcher key.

This is my index.jsx

import api from '@forge/api';
import ForgeUI, { CustomField, CustomFieldEdit, render, Text, TextField, useProductContext, useState, Badge} from "@forge/ui";


//get the remote web links for the current issue
const fetchLinksForIssue = async (issueId) => {
  const response = await api.asApp().requestJira(`/rest/api/3/issue/${issueId}/remotelink`);

  let scouts = 0,
      urlText = "";

  const data = await response.json();

  //count the HelpScout links 
  for(let i = 0; i < data.length; i++) {
    urlText = data[i]["object"]["url"];

    if(urlText.includes("helpscout")) {
      scouts += 1;
    }
  }

  return scouts;
};


function setFieldOptions (fieldId) {
  var bodyData = `{
    "searcherKey": "com.atlassian.jira.plugin.system.customfieldtypes:exactnumber",
    "type": "com.atlassian.jira.plugin.system.customfieldtypes:textfield"
  }`;

  let response = api.asApp().requestJira('/rest/api/3/field/{fieldId}', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: bodyData
  });

  //console.log(bodyData);
  console.log('Are my prints even working?' + response);
};


const View = () => {
  const context = useProductContext();
  const [fieldValue] = useState(async () => await fetchLinksForIssue(context.platformContext.issueKey));
  setFieldOptions(10035);

  return (
    <CustomField>
      <Text>
        <Badge appearance = 'primary' text ={fieldValue} />
      </Text>
    </CustomField>
  );
};


const Edit = () => {
  const onSubmit = values => {
    return values.text
  };

  return (
    <CustomFieldEdit onSubmit={onSubmit}>
      <TextField name="text" label="Say hello to:"></TextField>
    </ CustomFieldEdit>
  );
}


export const runView = render(
  <View/>
);

export const runEdit = render(<Edit/>)

This is my manifest.yml, excluding app:id

modules:
  jira:customField:
    - key: scoutcount
      name: Scout Count
      description: A count of HelpScout links for an issue.
      type: number
      validation:
        expression: value == null || (value >= 0)
        errorMessage: The value must consist only of numbers
      readOnly: true
      function: main
      edit:
        function: edit
  function:
    - key: main
      handler: index.runView
    - key: edit
      handler: index.runEdit
app:
  name: scoutCount
permissions:
  scopes:
    - manage:jira-configuration
    - read:jira-work
    - write:jira-work
    - storage:app
1 Like

Hello @ChrisMcKay ,

For the field to be searchable, you need to set its value with the Update custom field value REST API. It seems your setFieldOptions method tries to create a new field, whereas it should set the value of your field instead, using the API I mentioned.

I hope this helps :slight_smile:

2 Likes

Thanks for the tip, I updated the index.jsx as shown here. I still can’t get the results to be found through JQL. I noticed that the logs only print after opening an issue. I suspect the problem may have to do with the app only being called to calculate the field value when an issue is open. I would guess that JQL is seeing it as a null field because the app is not being invoked by searching.

Does that make sense? How could I run the app on the initial Jira page load, to make sure its values are up-to-date, then store the values of the field, then update when a new external link is added to an issue? Unless of course, my idea is totally off.

import api from '@forge/api';
import ForgeUI, { CustomField, CustomFieldEdit, render, Text, TextField, useProductContext, useState, Badge} from "@forge/ui";


//get the remote web links for the current issue
const fetchLinksForIssue = async (issueId) => {
  const response = await api.asApp().requestJira(`/rest/api/3/issue/${issueId}/remotelink`);
  
  let scouts = 0,
      urlText = "";
  
  const data = await response.json();
  
  //count the HelpScout links  
  for(let i = 0; i < data.length; i++) {
	  urlText = data[i]["object"]["url"];
	  
	  if(urlText.includes("helpscout")) {
		  scouts += 1;
	  }
  }
    
  return scouts;
};


function setFieldOptions (fieldID, issueID)  {
	var bodyData = `{
		
		"updates": [
			{
			  "issueIds": [
				${issueID}
			  ],
			  "searcherKey": "com.atlassian.jira.plugin.system.customfieldtypes:exactnumber",
			  "type": "com.atlassian.jira.plugin.system.customfieldtypes:textfield"
			}
		]
	}`;

	const response = api.asApp().requestJira(`/rest/api/2/app/field/${fieldID}/value`, {
	  method: 'PUT',
	  headers: {
		'Accept': 'application/json',
		'Content-Type': 'application/json'
	  },
	  body: bodyData
	});
	
	console.log('Field ID: ' + fieldID);
	console.log('Issue ID: ' + issueID);
	console.log('Body Data: ' + bodyData);
	
	
};


const View = () => {
  const context = useProductContext();
  const [fieldValue] = useState(async () => await fetchLinksForIssue(context.platformContext.issueKey));
  const issueID = useState(async () => await context.platformContext.issueId);
  setFieldOptions('10035', issueID[0]);
    
  return (
    <CustomField>
      <Text>
		<Badge appearance = 'primary' text ={fieldValue} />
	  </Text>
    </CustomField>
  );
};


const Edit = () => {
  const onSubmit = values => {
      return values.text
  };

  return (
    <CustomFieldEdit onSubmit={onSubmit}>
      <TextField name="text" label="Say hello to:"></TextField>
    </ CustomFieldEdit>
  );
}


export const runView = render(
  <View/>
);

export const runEdit = render(<Edit/>)



Beside the fact that your REST call to the update custom field value REST API doesn’t seem to be correct (you don’t specify the value anywhere in the body), with the app design you have, you will only be able to search for issues that you have visited at least once. All others won’t have their value set.

You can listen to product events to set the value for every issue that has its links changed, regardless of the issue being displayed.

To set up ALL issues when the app is installed, you could create a web trigger that you would then invoke manually or maybe a scheduled trigger that would do it automatically.

I hope this makes sense, let me know if you have any questions!

1 Like

Hi, I’m also currently following along with Chris’ app example as I’m new to Jira development. I would also like to see the value for every issue be set properly - but haven’t been having any luck.

You had mentioned the product events as one possible solution to this problem. Can you please give an explicit example of how this may done to achieve the desired result within index.jsx.? What would be needed in the manifest ( currently there’s a trigger module that already has a reference to fetchLinksForIssue ).

Would the update custom field value API still be needed? If so, would it look similar to what Chris has for setFieldOptions() ?- of course with “value” mentioned within the bodyData? How would it be invoked?