npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@atlassianlabs/jira-metaui-transformer

v1.0.11

Published

Transform jira meta information to a ui descriptor

Downloads

154

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 @atlassianlabs/jira-metaui-transformer

Simple Usage

import {
    CreateIssueScreenTransformer,
    FieldTransformerResult,
    JiraSiteInfo,
    UIType,
    FieldUI,
    InputFieldUI
} from '@atlassianlabs/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 | | Team | 'team', | | 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.