Noxious Plant Miasma

    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.

    Keywords

    none

    Install

    npm i jira-metaui-transformer

    DownloadsWeekly Downloads

    0

    Version

    0.0.9

    License

    MIT

    Unpacked Size

    1.09 MB

    Total Files

    87

    Last publish

    Collaborators

    • doklovic