Skip to content

Commit 2cab3e2

Browse files
avuyteambhorai-atl
andauthored
[AXON-370] Display full issues hierarchy in breadcrumbs (#477)
* display issue hierarchy * Add ... while loading top parent of the hierarchy * refactor: removed unused code * feat: added loading animation for breadcrumbs * refactor: remove AI generated comments * test: added unit tests for fetchIssueHierarchy * chore: updated changelog * refactor: removed AI generated comments * docs: updated changelog * refactor: removed old code * feat: updated version for loading hierarchy * refactor: updated layout --------- Co-authored-by: bhorai-atl <[email protected]>
1 parent 1cb969d commit 2cab3e2

File tree

4 files changed

+121
-60
lines changed

4 files changed

+121
-60
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
- Update logic of selecting the default project and site to the most relevant one for the user on the Creating a JIRA issue page
1818
- Added filtering for options and error message in projects field on Create Jira issue page.
19+
- Implemented full parent hierarchy display for Jira issues
1920

2021
### Bug Fixes
2122

src/webviews/components/App.css

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -663,14 +663,23 @@ pre {
663663
margin: 0px 8px;
664664
}
665665

666-
/* [class^=styled__StyledTitle-] p {
667-
margin: 0px;
668-
color: var(--vscode-editor-foreground);
666+
.ac-breadcrumb-loading {
667+
color: var(--vscode-breadcrumb-foreground);
669668
}
670669

671-
[class^=styled__StyledTitle-] {
672-
margin: 0px;
673-
} */
670+
.ac-breadcrumb-loading .animate-pulse {
671+
animation: blink 1.4s infinite both;
672+
opacity: 0.2;
673+
}
674+
675+
@keyframes blink {
676+
0%, 80%, 100% {
677+
opacity: 0.2;
678+
}
679+
40% {
680+
opacity: 1;
681+
}
682+
}
674683

675684
/*-------------------- END - page header --------------------*/
676685

src/webviews/components/issue/view-issue-screen/JiraIssuePage.tsx

Lines changed: 78 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { LoadingButton } from '@atlaskit/button';
22
import Page, { Grid, GridColumn } from '@atlaskit/page';
33
import Tooltip from '@atlaskit/tooltip';
44
import WidthDetector from '@atlaskit/width-detector';
5-
import { CommentVisibility, Transition } from '@atlassianlabs/jira-pi-common-models';
5+
import { CommentVisibility, MinimalIssue, Transition } from '@atlassianlabs/jira-pi-common-models';
66
import { FieldUI, InputFieldUI, SelectFieldUI, UIType, ValueType } from '@atlassianlabs/jira-pi-meta-models';
77
import { Box } from '@material-ui/core';
88
import { formatDistanceToNow, parseISO } from 'date-fns';
99
import * as React from 'react';
10+
import { DetailedSiteInfo } from 'src/atlclients/authInfo';
1011
import { v4 } from 'uuid';
1112

1213
import { AnalyticsView } from '../../../../analyticsTypes';
@@ -42,6 +43,8 @@ export interface ViewState extends CommonEditorViewState, EditIssueData {
4243
currentInlineDialog: string;
4344
commentText: string;
4445
isEditingComment: boolean;
46+
hierarchyLoading: boolean;
47+
hierarchy: MinimalIssue<DetailedSiteInfo>[];
4548
}
4649

4750
const emptyState: ViewState = {
@@ -51,6 +54,8 @@ const emptyState: ViewState = {
5154
currentInlineDialog: '',
5255
commentText: '',
5356
isEditingComment: false,
57+
hierarchyLoading: false,
58+
hierarchy: [],
5459
};
5560

5661
export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept, {}, ViewState> {
@@ -116,6 +121,14 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
116121
}
117122
break;
118123
}
124+
case 'hierarchyUpdate': {
125+
this.setState({ hierarchy: e.hierarchy, hierarchyLoading: false });
126+
break;
127+
}
128+
case 'hierarchyLoading': {
129+
this.setState({ hierarchy: e.hierarchy, hierarchyLoading: true });
130+
break;
131+
}
119132
}
120133
}
121134
return handled;
@@ -472,22 +485,8 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
472485
};
473486

474487
getMainPanelNavMarkup(): any {
475-
const epicLinkValue = this.state.fieldValues[this.state.epicFieldInfo.epicLink.id];
476-
let epicLinkKey: string = '';
477-
478-
if (epicLinkValue) {
479-
if (typeof epicLinkValue === 'object' && epicLinkValue.value) {
480-
epicLinkKey = epicLinkValue.value;
481-
} else if (typeof epicLinkValue === 'string') {
482-
epicLinkKey = epicLinkValue;
483-
}
484-
}
485-
486-
const parentIconUrl =
487-
this.state.fieldValues['parent'] && this.state.fieldValues['parent'].issuetype
488-
? this.state.fieldValues['parent'].issuetype.iconUrl
489-
: undefined;
490488
const itIconUrl = this.state.fieldValues['issuetype'] ? this.state.fieldValues['issuetype'].iconUrl : undefined;
489+
491490
return (
492491
<div>
493492
{this.state.showPMF && (
@@ -501,45 +500,69 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
501500
)}
502501
<div className="ac-page-header">
503502
<div className="ac-breadcrumbs">
504-
{epicLinkValue && epicLinkKey !== '' && (
505-
<React.Fragment>
506-
<NavItem
507-
text={epicLinkKey}
508-
onItemClick={() =>
509-
this.handleOpenIssue({ siteDetails: this.state.siteDetails, key: epicLinkKey })
510-
}
511-
/>
512-
<span className="ac-breadcrumb-divider">/</span>
513-
</React.Fragment>
514-
)}
515-
{this.state.fieldValues['parent'] && (
516-
<React.Fragment>
517-
<NavItem
518-
text={this.state.fieldValues['parent'].key}
519-
iconUrl={parentIconUrl}
520-
onItemClick={() =>
521-
this.handleOpenIssue({
522-
siteDetails: this.state.siteDetails,
523-
key: this.state.fieldValues['parent'].key,
524-
})
525-
}
526-
/>
527-
<span className="ac-breadcrumb-divider">/</span>
528-
</React.Fragment>
503+
{this.state.hierarchy && this.state.hierarchy.length > 0 && (
504+
<>
505+
{this.state.hierarchyLoading && this.state.hierarchy.length <= 1 && (
506+
<>
507+
<span className="ac-breadcrumb-loading">
508+
{[...Array(3)].map((_, idx) => (
509+
<span
510+
key={idx}
511+
className="animate-pulse"
512+
style={{ animationDelay: `${idx * 0.2}s` }}
513+
>
514+
.
515+
</span>
516+
))}
517+
</span>
518+
<span className="ac-breadcrumb-divider">/</span>
519+
</>
520+
)}
521+
{this.state.hierarchy.map((issue, index) => {
522+
const isLastItem = index === this.state.hierarchy.length - 1;
523+
const shouldOpenInJira = issue.key === this.state.key;
524+
const handleItemClick = !shouldOpenInJira
525+
? () =>
526+
this.handleOpenIssue({
527+
siteDetails: this.state.siteDetails,
528+
key: issue.key,
529+
})
530+
: undefined;
531+
532+
return (
533+
<React.Fragment key={issue.key}>
534+
<NavItem
535+
text={issue.key}
536+
iconUrl={issue.issuetype?.iconUrl}
537+
href={
538+
shouldOpenInJira
539+
? `${this.state.siteDetails.baseLinkUrl}/browse/${issue.key}`
540+
: undefined
541+
}
542+
onItemClick={handleItemClick}
543+
onCopy={isLastItem ? this.handleCopyIssueLink : undefined}
544+
/>
545+
{!isLastItem && <span className="ac-breadcrumb-divider">/</span>}
546+
</React.Fragment>
547+
);
548+
})}
549+
</>
529550
)}
530-
531-
<Tooltip
532-
content={`Created on ${
533-
this.state.fieldValues['created.rendered'] || this.state.fieldValues['created']
534-
}`}
535-
>
536-
<NavItem
537-
text={`${this.state.key}`}
538-
href={`${this.state.siteDetails.baseLinkUrl}/browse/${this.state.key}`}
539-
iconUrl={itIconUrl}
540-
onCopy={this.handleCopyIssueLink}
541-
/>
542-
</Tooltip>
551+
{!this.state.hierarchy ||
552+
(this.state.hierarchy.length === 0 && (
553+
<Tooltip
554+
content={`Created on ${
555+
this.state.fieldValues['created.rendered'] || this.state.fieldValues['created']
556+
}`}
557+
>
558+
<NavItem
559+
text={`${this.state.key}`}
560+
href={`${this.state.siteDetails.baseLinkUrl}/browse/${this.state.key}`}
561+
iconUrl={itIconUrl}
562+
onCopy={this.handleCopyIssueLink}
563+
/>
564+
</Tooltip>
565+
))}
543566
</div>
544567
</div>
545568
{this.state.isErrorBannerOpen && (
@@ -624,6 +647,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
624647
</div>
625648
);
626649
}
650+
627651
commonSidebar(): any {
628652
const commonItems: SidebarItem[] = ['assignee', 'reporter', 'labels', 'priority', 'components', 'fixVersions']
629653
.filter((field) => !!this.state.fields[field])

src/webviews/jiraIssueWebview.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export class JiraIssueWebview
167167
this.updateWatchers(),
168168
this.updateVoters(),
169169
this.updateRelatedPullRequests(),
170+
this.fetchFullHierarchy(),
170171
]);
171172

172173
const updatesDuration = timer.measureAndClear(updatePerfMarker);
@@ -180,6 +181,7 @@ export class JiraIssueWebview
180181
this.isRefeshing = false;
181182
}
182183
}
184+
183185
async updateEpicChildren() {
184186
if (this._issue.isEpic) {
185187
const site = this._issue.siteDetails;
@@ -1020,6 +1022,31 @@ export class JiraIssueWebview
10201022
return handled;
10211023
}
10221024

1025+
private async fetchFullHierarchy() {
1026+
if (!this._issue.parentKey) {
1027+
return;
1028+
}
1029+
1030+
const hierarchy = [this._issue];
1031+
1032+
this.postMessage({ type: 'hierarchyLoading', hierarchy });
1033+
1034+
try {
1035+
let currentParentKey: string | undefined = this._issue.parentKey;
1036+
while (currentParentKey) {
1037+
const parent = await fetchMinimalIssue(currentParentKey, this._issue.siteDetails);
1038+
hierarchy.unshift(parent);
1039+
currentParentKey = parent.parentKey;
1040+
}
1041+
} catch (e) {
1042+
Logger.error(e, 'Error fetching issue hierarchy');
1043+
} finally {
1044+
this.postMessage({ type: 'hierarchyUpdate', hierarchy });
1045+
}
1046+
1047+
return hierarchy;
1048+
}
1049+
10231050
private async recentPullRequests(): Promise<PullRequestData[]> {
10241051
if (!Container.bitbucketContext) {
10251052
return [];

0 commit comments

Comments
 (0)