README
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.