About Jira modules
Customer portal
Project settings UI locations

Issue tab panel

Available:

Jira 3.0 and later.

Changed:

Jira 5.0 – added the <supports-ajax-load> configuration element.

 

Jira 5.0 – added the IssueTabPanel2 API.

 

Jira 6.0 – added the IssueTabPanel3 API.

 

Jira 9.0 – added the PaginatedIssueTabPanel API.

The issue tab panel plugin module allows you to add new tab panels to the View Issue screen.

You can add a new tab with a plugin displaying information about a single issue (most likely pulled from an external source).

Here is an example descriptor:

1
2
<issue-tabpanel key="custom-issue-tabpanel" name="Custom Tab Panel" class="com.atlassian.plugins.tutorial.IssueTabCustom">
    <description>Show a custom panel.</description>
    <label>Custom panel</label>
    <supports-ajax-load>true</supports-ajax-load>
</issue-tabpanel>

For more information about the atlassian-plugin.xml file, see the Configuring the app descriptor page.

Here is a simple Java implementation:

1
2
public class IssueTabCustom extends AbstractIssueTabPanel3 {
    @Override
    public boolean showPanel(ShowPanelRequest showPanelRequest) {
        return true;
    }

    @Override
    public List<IssueAction> getActions(GetActionsRequest getActionsRequest) {
        return Lists.newArrayList(new GenericMessageAction("first"), new GenericMessageAction(
                    this.descriptor.getI18nBean().getText("com.atlassian.plugins.tutorial.custom.issue.tab.panel.example")));
    }
}

The module class specified in the class="..." attribute must implement the IssueTabPanel3 interface.

To customize the look of your items, implement your own IssueAction.

For more details, see Loading Issue Tab Panels with AJAX.

Jira 9.0 and later

If you are working on Jira 9.0 and later versions, you can implement progressive pagination within your activity tab using PaginatedIssueTabPanel API. The interface requires that all actions in your activity tab are returning dates from IssueAction#getTimePerformed.

Only tabs that support pagination (PaginatedIssueTabPanel#paginationSupported returns true) will be displayed on the "All" Tab. Therefore, actions from tabs implementing IssueTabPanel, IssueTabPanel2 and IssueTabPanel3 will not be shown on "All" tab panel.

Similarly to how it's done in previous versions, you can add such tab with a plugin:

1
2
<issue-tabpanel key="custom-issue-tabpanel" name="Custom Tab Panel" 
                class="com.atlassian.plugins.tutorial.PaginatedIssueTabCustom">

    <description>Show a custom panel.</description>
    <label>Custom panel</label>
    <supports-ajax-load>true</supports-ajax-load>
 
    <show-newer-expander-label>Load newer custom events</show-newer-expander-label>
    <show-older-expander-label>Load older custom events</show-older-expander-label>
    <show-all-newer-expander-label>load all newer custom events</show-all-newer-expander-label>
    <show-all-older-expander-label>load all older custom events</show-all-older-expander-label>
</issue-tabpanel>

The <...-label> tags contain labels for buttons in different variants.

As for the class itself, here is an example Java implementation:

1
2
public class PaginatedIssueTabCustom implements PaginatedIssueTabPanel {
    IssueTabPanelModuleDescriptor descriptor;

    private static final Instant START_TIME = LocalDate.of(2022, 1, 1).atStartOfDay().toInstant(ZoneOffset.UTC);
    private static final List<IssueAction> ALL_ACTIONS = generateActions(START_TIME, 100);

    @Override
    public void init(IssueTabPanelModuleDescriptor descriptor) {
        this.descriptor = descriptor;
    }

    @Override
    public boolean showPanel(ShowPanelRequest request) {
        return true;
    }

    @Override
    public Page<IssueAction> getActions(GetActionsRequest request) {
        final Window<IssueAction> issueActions = searchActions(request);
        return new Page<IssueAction>() {
            @Override
            public boolean isFirstPage() {
                return !issueActions.hasElementsBefore();
            }

            @Override
            public boolean isLastPage() {
                return !issueActions.hasElementsAfter();
            }

            @Override
            public List<IssueAction> getPageContents() {
                return issueActions.get();
            }
        };
    }

    private Window<IssueAction> searchActions(GetActionsRequest request) {
        Optional<Integer> limit = request.isShowAll() ? Optional.empty() : Optional.of(request.getBatch().getShowMax());

        if (request.getBatch().getFetchMode() == FROM_OLDEST) {
            return searchFromOldest(limit, ALL_ACTIONS);
        } else if (request.getBatch().getFetchMode() == FROM_NEWEST) {
            return searchFromNewest(limit, ALL_ACTIONS);
        } else if (request.getBatch().getFetchMode() == NEWER_THAN_DATE) {
            return searchNewerThanDate(limit, request.getBatch().getFromDate(), ALL_ACTIONS);
        } else if (request.getBatch().getFetchMode() == OLDER_THAN_DATE) {
            return searchOlderThanDate(limit, request.getBatch().getFromDate(), ALL_ACTIONS);
        } else {
            throw new IllegalArgumentException();
        }
    }

    private static <T> Window<T> searchFromOldest(Optional<Integer> limit, List<T> fullList) {
        if (limit.isPresent()) {
            return Window.of(fullList).shrinkFromEnd(limit.get());
        } else {
            return Window.of(fullList);
        }
    }

    private static <T> Window<T> searchFromNewest(Optional<Integer> limit, List<T> fullList) {
        if (limit.isPresent()) {
            return Window.of(fullList).shrinkFromStart(limit.get());
        } else {
            return Window.of(fullList);
        }
    }

    private static <T extends IssueAction> Window<T> searchNewerThanDate(Optional<Integer> limit, Date from, List<T> fullList) {
        final Window<T> filtered = Window.of(fullList).dropUntil(elem -> elem.getTimePerformed().after(from));
        return limit.map(filtered::shrinkFromEnd).orElse(filtered);
    }

    private static <T extends IssueAction> Window<T> searchOlderThanDate(Optional<Integer> limit, Date from, List<T> fullList) {
        final Window<T> filtered = Window.of(fullList).keepUntil(elem -> elem.getTimePerformed().before(from));
        return limit.map(filtered::shrinkFromStart).orElse(filtered);
    }

    private static List<IssueAction> generateActions(Instant from, int howMany) {
        return IntStream.range(0, howMany)
                .mapToObj(i -> from.plus(i, ChronoUnit.MINUTES))
                .map(Action::new)
                .collect(Collectors.toList());
    }

    private static class Action implements IssueAction {

        private final Instant time;

        public Action(Instant time) {
            this.time = time;
        }

        @Override
        public String getHtml() {
            return String.format("<div class=\"issue-data-block\">Issue action at %s</div>", time.toString());
        }

        @Override
        public Date getTimePerformed() {
            return Date.from(time);
        }

        @Override
        public boolean isDisplayActionAllTab() {
            return false;
        }
    }
}

As for the front-end, you need to register your tab using jira/activity-tabs/items-lazy-loader dependency.

1
2
const WRMRequire = require("wrm/require");

WRMRequire('wr!com.atlassian.jira.jira-frontend-plugin:entrypoint-activityTabs').then(function() {
    require('jira/activity-tabs/items-lazy-loader').then(function(itemsLazyLoader) {
        itemsLazyLoader.registerTab('paginatedreference-tabpanel', {
            version: 1
        });
    });
});

The version argument here is to prevent unexpected behavior from happening once we add more features to the interface. For this interface to work, you also must include .issue-data-block class in all your action items HTML.

Rate this page: