jira-metaui-transformer
TypeScript icon, indicating that this package has built-in type declarations

0.0.9 • Public • Published

jira-metaui-transformer

*** Use At Your Own Risk! ***

This library is used internally at Atlassian and is unsupported.

What's this for?

This library takes Jira field level meta-data from multiple Jira endpoints and transforms them into a more useable UI descriptor.

It also deals with unknown non-renderable custom-field types as well as fixes a lot of inconsistencies within the Jira meta-data itself.

This only provides a UI descirptor which can then be used to generate a Jira-like create issue UI.

Installation

npm i jira-metaui-transformer

Simple Usage

import { CreateIssueScreenTransformer, FieldTransformerResult, JiraSiteInfo, UIType, FieldUI, InputFieldUI } from 'jira-metaui-transformer';

const dummyHttpClient = new SomeHttpClientOfYourChoice();

// NOTE: the library expects JSON Objects. If your http client doesn't auto-parse json, you may need to call something like response.body.json

// Get the Jira meta-data
const jiraMeta = dummyHttpClient.get('https://api.atlassian.com/ex/jira/ACLOUDID/rest/api/2/issue/createmeta?projectKeys=TESTPROJECT&expand=projects.issuetypes.fields');
const allFields = dummyHttpClient.get('https://api.atlassian.com/ex/jira/ACLOUDID/rest/api/2/field');
const issueLinkTypes = dummyHttpClient.get('https://api.atlassian.com/ex/jira/ACLOUDID/rest/api/2/issueLinkType');

// Create the transformer for the current site (see below for details about site info)
const siteInfo = {baseApiUrl: 'https://api.atlassian.com/ex/jira/ACLOUDID/rest', isCloud: true};

const createIssueTransformer: CreateIssueScreenTransformer<JiraSiteInfo> = new CreateIssueScreenTransformer(siteInfo, '2', allFields, issueLinkTypes);

// Transform the meta-data
// Note: the call to createmeta specified a single projectKey so there will only be a single project returned.
// This is considered the best practice and you should provide a project selector if needed and make a new call
// to createmeta and the transformer when the project changes.

const transformerResult = await createIssueTransformer.transformIssueScreens(meta.projects[0]);

// Loop over the fields and create the UI
Object.values(transformerResult.issueTypeUIs[transformerResult.selectedIssueType.id].fields).forEach((field:FieldUI) => {
  switch (field.uiType) {
    case UIType.Input: {
      if ((field as InputFieldUI).isMultiline) {
        return <textarea />;
      } else {
        return <input />;
      }
    }
  }
});

Running the Example

in the examples folder of this project there's a small example that uses json text files for the input data. When it's run it will dump the result to the console as well as to a file named result.json in the current directory.

To run the example, from the project root: npm run-script examples

Inputs

The transformer requires 3 separate bits of Jira data: createmeta, fields (allfields), and issueLinkTypes. The transformer itself doesn't make any http calls allowing the caller to use any http client they wish. These 3 bits of data should come from the endpoints in the above example and must be in JSON format (not a string).

Projects

As note in the example, it's best to call createmeta with a single projectKey as calling it without a projectKey or multiple projectKeys will result in a ton of data and poor performance. If your UI needs to handle multiple projects, we suggest adding a project selector dropdown and then re-running the transformation process when the user selects a different project.

The call to CreateIssueScreenTransformer.transformIssueScreens has a single required parameter which is the project node from the createmeta response. If you've followed the 'best practice' you should just be able to pass it metaresponse.projects[0]. (be sure to check that you actually got a project back and not an empty array).

SiteInfo

The transformer needs to generate Jira URLs for things like auto-completion and select item creation. To accommodate this, you need to provide the baseApiUrl, a cloud flag, and an API version to the transformer.

Apart from URL generation, the siteDetails are also added to each issueTypeUI in the result. This is handy for making any extra calls your code may need especially in a multi-site scenario. The siteDetails can be of any type you wish and can have any extra fields you wish so long as it contains baseApiUrl and isCloud fields.

To facilitate proper typing when adding the siteDetails to results, the transofrmer requires a generic type to tell it what kind type it should be returning. specifically, the type it needs is: <S extends JiraSiteInfo> where JiraSiteInfo includes the 2 required fields. If you don't need or want to use a custom siteDetails type, you can simply use JiraSiteInfo.

Common Fields

The result of the transform marks each field as either 'common' or 'advanced' by way of an advanced:boolean flag on each field. When false, the field is considered common, and when true the field is considered advanced.

This feature is to enable the UI code to split the common field inputs from the advanced field inputs and/or only show the very minimal set of fields required to create an issue.

The default set of fields considered as 'common' are:

  • project
  • issuetype
  • summary
  • description
  • fixVersions
  • components
  • labels

This list is exposed as the const defaultCommonFields. The set of common field keys can be overridden by passing commonFields:string[] to the transformIssueScreens call.

Also when calling CreateIssueScreenTransformer.transformIssueScreens you can pass an optional boolean flag requiredAsCommon which will mark any required field as common even if it's not in the list of common field keys. These 2 parameters can be used in tandem to easily get a list of the minimal set of fields you required to create an issue. requiredAsCommon defaults to true;

Filtering Fields

Jira returns some fields that shouldn't be sent as part of the create issue call and they need to be filtered out of the UI. On top of that, there may be some fields you simply never want to render and want to exclude them from the transformer results.

Much like the commonFields parameter, there's also an optional filterFieldKeys?: string[] parameter that allows you to pass field keys you want filtered from the results.

The default set of filtered keys is:

  • parent
  • reporter
  • statuscategorychangedate
  • lastViewed

This list is exposed as the const defaultFieldFilters

Understanding the results

The result of the transformation is a CreateMetaTransformerResult object. The top-level fields are:

Name Description
issueTypes An array of IssueType objects that can be rendered. IssueTypes with non-renderable required fields are excluded. The IssueType objects are augmented with an epic boolean flag to easily tell if the issuetype is an epic type.
selectedIssueType The first renderable IssueType. This can be used to pre-select the issue type on the first render. This IssueType is guaranteed to be renderable
issueTypeUIs An object containing the UI descriptors for all renderable issuetypes where the key is the issuetype ID and the value is an object containing the UI details
problems An object containing a problem report for each/any issuetype. The keys are the issuetype ID and the value is the problem reporter

Once you receive a result, you need to choose which issuetype you want to render a screen for. For the first render you're probably going to want to render the first issuetype that's renderable. You can get the UI details like this: result.issueTypeUIs[result.selectedIssueType.id]

This will return the IssueTypeUI object you can use for rendering.

When building your UI, you can provider a dropdown containing the renderable issuetypes so user's can switch the type of issue to create. When the user selects a new issuetype, you can get the new screen to render by simply doing: result.issueTypeUIs[userSelectedIssueType.id]

IssueTypeUI Objects

IssueTypeUI objects are the main entry point in rendering a UI. The top-level fields are as such:

Name Description
fields And object containing FieldUI descriptors whose keys are the field's key and the value is a FieldUI descriptor. These describe what kind of UI to render
fieldValues An object containing the current value of any given field. The keys are the field's key and the value is the current value. It's encouraged to mutate this object to keep state as user's fill in the create issue form.
selectFieldOptions And object containing the current options for a select field. The keys are the field's key and the value is an array of the options. It's encouraged to mutate this object and use it as state for select boxes. e.g. when a user creates a new version/component/label you can add it to the proper list in this object and re-render
nonRenderableFields An array of fields that cannot be rendered with the known UI types.
siteDetails The site details object passed into the transformer
epicFieldInfo An object containing the IDs and names of the epicName and epicLink fields as well as a flag to determine if epics are enabled in Jira.

Why all of these objects with field key -> value? Why not just use 'allowedValues' on a field for select boxes like Jira does? After lots of iterations, we've determined that more often than not when rendering a UI, especially with user input handlers and async calls it's easier to manage state with these separate objects and the only sensible way to deal with the dynamic nature of Jira fields is to use dictionaries keyed by the field keys. This makes it possible to changes various portions of state without having to keep references of the fields all over the place.

FieldUI Objects

Once you've picked an issue type and your ready to render individual fields, you'll loop through the fields of the IssueTypeUI object. Each field is a FieldUI object that gives you a descriptor of what to render in the UI.

There are too many variations of UITypes to detail all of them here so let's just pick a simple 'input' and a 'select'... For starters all FieldUI objects contain a set of common fields:

Name Type Description
required boolean A flag to tell if this is a required field
name string The display name of the field
key string The field key. This can be used in all other state object lookups
uiType string The type of UI element to render
displayOrder number The display order of the field used for sorting the fields
valueType string The type of value the field holds
advanced boolean A flag to tell if this is a common or advanced field

The 'input' UIType adds a single field:

Name Type Description
isMultiline boolean A flag to tell if this is a single line 'input' or a multiline 'textarea'

The 'select' UIType does not contain isMultiline but adds the following fields:

Name Type Description
isMulti boolean A flag to tell if this the user should be allowed to select multiple values
isCreateable boolean A flag to tell if this the user should be able to create new select options
autoCompleteUrl string The full URL to be used for searching for options. This will be blank if search is not supported
createUrl string The full URL to be used for creating new options. This will be blank if creation is not supported

UITypes

Below is a list of all supported UIType values. These are exposed as an enum called UIType to make switch statements easier

enum value
Select 'select'
Checkbox 'checkbox'
Radio 'radio'
Input 'input'
Date 'date'
DateTime 'datetime'
IssueLinks 'issuelinks'
IssueLink 'issuelink'
Subtasks 'subtasks'
Timetracking 'timetracking'
Worklog 'worklog'
Comments 'comments'
Watches 'watches'
Votes 'votes'
Attachment 'attachment'
NonEditable 'noneditable'
Participants 'participants'

ValueTypes

Each field has a value type. Below is a list of all supported ValueTypes. These are exposed as an enum called ValueType.

enum value
String 'string'
Number 'number'
Url 'url'
DateTime 'datetime'
Option 'option', // as type: single select or radio, as array items: multi-select or checkboxes (also check schema), {id, value}
Resolution 'resolution', // single select, {id, name}
Priority 'priority', // single select, {id, name, iconUrl}
User 'user', // single select, {key, accountId, accountType, name, emailAddress, avatarUrls{'48x48'...}, displayName, active, timeZone, locale}
Status 'status', // {description, iconUrl, name, id, statusCategory{id, key, colorName, name}}
Transition 'transition', // array of transitions
Progress 'progress', //part of time tracking
Date 'date',
Votes 'votes', // for display: {votes:number, hasVoted:boolean}
IssueType 'issuetype', // single select, {id, description, iconUrl, name, subtask:boolean, avatarId}
Project 'project', //single select, { id, key, name, projectTypeKey, simplified:boolean, avatarUrls{ '48x48'... }}
Watches 'watches', // mutli-user picker for edit, for display: {watchCount:number, isWatching:boolean, self:url } self contains url to get the user details for watchers
Timetracking 'timetracking', //timetracking UI
CommentsPage 'comments-page', // textarea, system schema will be 'comment'
Version 'version', // multi-select, {id, name, archived:boolean, released:boolean}
IssueLinks 'issuelinks',
IssueLink 'issuelink', // used for subtask parent link
Component 'component', // mutli-select, {id, name}
Worklog 'worklog',
Attachment 'attachment',
Group 'group',

Tips and Tricks

Sorting

By default, Jira returns fields generally in the order they should be displayed. Unfortunately Jira does not include the sort order as a datapoint anywhere in the payload and so the order can get lost when sending json objects between backend and UI code. To correct this the transformer adds displayOrder to each field so they can be re-sorted if needed.

If you'd like to sort fields, here's an example of how to do it:

function sortFieldValues(fields: FieldUIs): FieldUI[] {
    return Object.values(fields).sort((left: FieldUI, right: FieldUI) => {
        if (left.displayOrder < right.displayOrder) { return -1; }
        if (left.displayOrder > right.displayOrder) { return 1; }
        return 0;
    });
}

Separating Common from Advanced Fields

As discussed above, all fields are either 'common' or 'advanced' and are marked as such with the advanced boolean on each field.

If you need to separate the common fields from advanced fields for rendering (and you do), here's an example of how to do it:

const orderedValues: FieldUI[] = sortFieldValues(data.fields);

const advancedFields = [];
const commonFields = [];

orderedValues.forEach(field => {
    if (field.advanced) {
        advancedFields.push(field);
    } else {
        commonFields.push(field);
    }
});

Full complicated UI example

If you like pain and want to see this stuff in action, take a look at the createIssueWebview.ts (controller) and the CreateIssuePage.tsx (ui) files in the atlascode project.

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 0.0.9
    0
    • latest

Version History

Package Sidebar

Install

npm i jira-metaui-transformer

Weekly Downloads

0

Version

0.0.9

License

MIT

Unpacked Size

1.09 MB

Total Files

87

Last publish

Collaborators

  • doklovic