Skip to content

Commit cec34e3

Browse files
authored
[AXON-265] New notification logic to notify for every unseen jira ticket assigned to the user (#268)
1 parent ef334c5 commit cec34e3

File tree

8 files changed

+348
-33
lines changed

8 files changed

+348
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ coverage
1919
.generated/
2020
e2e/.resources/
2121
e2e/.test-extensions/
22+
*.orig

CHANGELOG.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
## What's new in 3.4.24
2-
3-
### Improvements
4-
- In PR details page, made UI Changes to make the comment section and Add Reviewer option cleaner and user friendly
5-
61
## What's new in 3.4.23
72

83
### Improvements
4+
95
- Improved the pull request details page with idea to make UI more intuitive and close to Butbucket UI.
6+
- In PR details page, made UI Changes to make the comment section and Add Reviewer option cleaner and user friendly
7+
- Improved the notification for new Jira issues, which now includes issues created long time ago but just assigned to you.
108

119
## What's new in 3.4.22
1210

13-
### Improvements
11+
### Improvements
12+
1413
- Changed text in the onboarding flow to "Sign in to ____." Used to be "What version of {product} do you use?"
1514

1615
## What's new in 3.4.21

src/views/jira/treeViews/jiraAssignedWorkItemsViewProvider.test.ts

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { forceCastTo } from '../../../../testsutil';
1+
import { expansionCastTo, forceCastTo } from '../../../../testsutil';
22
import { AssignedWorkItemsViewProvider } from './jiraAssignedWorkItemsViewProvider';
33
import { Container } from '../../../container';
44
import { JQLManager } from '../../../jira/jqlManager';
@@ -8,6 +8,9 @@ import { JQLEntry } from '../../../config/model';
88
import { MinimalIssue } from '@atlassianlabs/jira-pi-common-models';
99
import { DetailedSiteInfo } from '../../../atlclients/authInfo';
1010
import * as vscode from 'vscode';
11+
import { PromiseRacer } from 'src/util/promises';
12+
import { JiraNotifier } from './jiraNotifier';
13+
import { RefreshTimer } from 'src/views/RefreshTimer';
1114

1215
const mockedJqlEntry = forceCastTo<JQLEntry>({
1316
id: 'jqlId',
@@ -52,6 +55,7 @@ const mockedIssue3 = forceCastTo<MinimalIssue<DetailedSiteInfo>>({
5255
children: [],
5356
});
5457

58+
jest.mock('./jiraNotifier');
5559
jest.mock('../searchJiraHelper');
5660
jest.mock('../../../container', () => ({
5761
Container: {
@@ -78,7 +82,9 @@ jest.mock('../../../container', () => ({
7882
},
7983
}));
8084

81-
class PromiseRacerMockClass {
85+
type ExtractPublic<T> = { [P in keyof T]: T[P] };
86+
87+
class PromiseRacerMockClass implements ExtractPublic<PromiseRacer<any>> {
8288
public static LastInstance: PromiseRacerMockClass | undefined = undefined;
8389
private count: number;
8490
private mockedData: any[] = [];
@@ -108,6 +114,44 @@ jest.mock('../../../util/promises', () => ({
108114
PromiseRacer: jest.fn().mockImplementation((promises) => new PromiseRacerMockClass(promises)),
109115
}));
110116

117+
class JiraNotifierMockClass implements ExtractPublic<JiraNotifier> {
118+
public static LastInstance: JiraNotifierMockClass | undefined = undefined;
119+
constructor() {
120+
JiraNotifierMockClass.LastInstance = this;
121+
}
122+
public ignoreAssignedIssues(issues: MinimalIssue<DetailedSiteInfo>[]): void {}
123+
public notifyForNewAssignedIssues(issues: MinimalIssue<DetailedSiteInfo>[]): void {}
124+
}
125+
126+
jest.mock('./jiraNotifier', () => ({
127+
JiraNotifier: jest.fn().mockImplementation(() => new JiraNotifierMockClass()),
128+
}));
129+
130+
class RefreshTimerMockClass implements ExtractPublic<RefreshTimer> {
131+
public static LastInstance: RefreshTimerMockClass | undefined = undefined;
132+
constructor(
133+
_enabledConfigPath: string | undefined,
134+
_intervalConfigPath: string,
135+
public refreshFunc: () => void,
136+
) {
137+
RefreshTimerMockClass.LastInstance = this;
138+
}
139+
dispose(): void {}
140+
isEnabled(): boolean {
141+
return false;
142+
}
143+
setActive(active: boolean): void {}
144+
}
145+
146+
jest.mock('../../RefreshTimer', () => ({
147+
RefreshTimer: jest
148+
.fn()
149+
.mockImplementation(
150+
(enabledConfigPath, intervalConfigPath, refreshFunc) =>
151+
new RefreshTimerMockClass(enabledConfigPath, intervalConfigPath, refreshFunc),
152+
),
153+
}));
154+
111155
const mockedTreeView = {
112156
onDidChangeVisibility: () => {},
113157
};
@@ -118,13 +162,44 @@ describe('AssignedWorkItemsViewProvider', () => {
118162
beforeEach(() => {
119163
jest.spyOn(vscode.window, 'createTreeView').mockReturnValue(mockedTreeView as any);
120164
provider = undefined;
165+
166+
PromiseRacerMockClass.LastInstance = undefined;
167+
JiraNotifierMockClass.LastInstance = undefined;
168+
RefreshTimerMockClass.LastInstance = undefined;
121169
});
122170

123171
afterEach(() => {
124172
provider?.dispose();
125173
jest.restoreAllMocks();
126174
});
127175

176+
describe('initialization', () => {
177+
it('onDidSitesAvailableChange is registered during construction', async () => {
178+
let onDidSitesAvailableChangeCallback = undefined;
179+
jest.spyOn(Container.siteManager, 'onDidSitesAvailableChange').mockImplementation(
180+
(func: any, parent: any): any => {
181+
onDidSitesAvailableChangeCallback = (...args: any[]) => func.apply(parent, args);
182+
},
183+
);
184+
185+
provider = new AssignedWorkItemsViewProvider();
186+
187+
expect(onDidSitesAvailableChangeCallback).toBeDefined();
188+
});
189+
190+
it('RefreshTimer is registered during construction and triggers refresh', async () => {
191+
let dataChanged = false;
192+
193+
provider = new AssignedWorkItemsViewProvider();
194+
provider.onDidChangeTreeData(() => (dataChanged = true), undefined);
195+
196+
expect(RefreshTimerMockClass.LastInstance).toBeDefined();
197+
198+
RefreshTimerMockClass.LastInstance?.refreshFunc();
199+
expect(dataChanged).toBeTruthy();
200+
});
201+
});
202+
128203
describe('getAllDefaultJQLEntries', () => {
129204
it('should initialize with configure Jira message if no JQL entries', async () => {
130205
jest.spyOn(Container.jqlManager, 'getAllDefaultJQLEntries').mockReturnValue([]);
@@ -139,7 +214,7 @@ describe('AssignedWorkItemsViewProvider', () => {
139214
});
140215

141216
it('should initialize with JQL promises if JQL entries exist, returns empty', async () => {
142-
const jqlEntries = [forceCastTo<JQLEntry>({ siteId: 'site1', query: 'query1' })];
217+
const jqlEntries = [expansionCastTo<JQLEntry>({ siteId: 'site1', query: 'query1' })];
143218
jest.spyOn(Container.jqlManager, 'getAllDefaultJQLEntries').mockReturnValue(jqlEntries);
144219
provider = new AssignedWorkItemsViewProvider();
145220

@@ -153,7 +228,7 @@ describe('AssignedWorkItemsViewProvider', () => {
153228
});
154229

155230
it('should initialize with JQL promises if JQL entries exist, returns issues', async () => {
156-
const jqlEntries = [forceCastTo<JQLEntry>({ siteId: 'site1', query: 'query1' })];
231+
const jqlEntries = [expansionCastTo<JQLEntry>({ siteId: 'site1', query: 'query1' })];
157232
jest.spyOn(Container.jqlManager, 'getAllDefaultJQLEntries').mockReturnValue(jqlEntries);
158233
provider = new AssignedWorkItemsViewProvider();
159234

@@ -180,18 +255,43 @@ describe('AssignedWorkItemsViewProvider', () => {
180255
});
181256
});
182257

183-
describe('onDidSitesAvailableChange', () => {
184-
it('onDidSitesAvailableChange is registered during construction', async () => {
185-
let onDidSitesAvailableChangeCallback = undefined;
186-
jest.spyOn(Container.siteManager, 'onDidSitesAvailableChange').mockImplementation(
187-
(func: any, parent: any): any => {
188-
onDidSitesAvailableChangeCallback = (...args: any[]) => func.apply(parent, args);
189-
},
190-
);
258+
describe('JiraNotifier', () => {
259+
it("doesn't notify when the provider is fetching for the first time", async () => {
260+
const jqlEntries = [expansionCastTo<JQLEntry>({ siteId: 'site1', query: 'query1' })];
261+
262+
jest.spyOn(Container.jqlManager, 'getAllDefaultJQLEntries').mockReturnValue(jqlEntries);
263+
jest.spyOn(vscode.window, 'showInformationMessage');
191264

192265
provider = new AssignedWorkItemsViewProvider();
193266

194-
expect(onDidSitesAvailableChangeCallback).toBeDefined();
267+
jest.spyOn(JiraNotifierMockClass.LastInstance!, 'ignoreAssignedIssues');
268+
jest.spyOn(JiraNotifierMockClass.LastInstance!, 'notifyForNewAssignedIssues');
269+
270+
PromiseRacerMockClass.LastInstance?.mockData([mockedIssue1, mockedIssue2, mockedIssue3]);
271+
await provider.getChildren();
272+
273+
expect(JiraNotifierMockClass.LastInstance!.ignoreAssignedIssues).toHaveBeenCalled();
274+
expect(JiraNotifierMockClass.LastInstance!.notifyForNewAssignedIssues).not.toHaveBeenCalled();
275+
});
276+
277+
it('it notifies for newly fetched items', async () => {
278+
const jqlEntries = [expansionCastTo<JQLEntry>({ siteId: 'site1', query: 'query1' })];
279+
280+
jest.spyOn(Container.jqlManager, 'getAllDefaultJQLEntries').mockReturnValue(jqlEntries);
281+
jest.spyOn(vscode.window, 'showInformationMessage');
282+
283+
provider = new AssignedWorkItemsViewProvider();
284+
285+
PromiseRacerMockClass.LastInstance?.mockData([mockedIssue1, mockedIssue2, mockedIssue3]);
286+
await provider.getChildren();
287+
288+
jest.spyOn(JiraNotifierMockClass.LastInstance!, 'ignoreAssignedIssues');
289+
jest.spyOn(JiraNotifierMockClass.LastInstance!, 'notifyForNewAssignedIssues');
290+
291+
await provider.getChildren();
292+
293+
expect(JiraNotifierMockClass.LastInstance!.ignoreAssignedIssues).not.toHaveBeenCalled();
294+
expect(JiraNotifierMockClass.LastInstance!.notifyForNewAssignedIssues).toHaveBeenCalled();
195295
});
196296
});
197297
});

src/views/jira/treeViews/jiraAssignedWorkItemsViewProvider.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import { JiraIssueNode, TreeViewIssue, executeJqlQuery, loginToJiraMessageNode }
1717
import { configuration } from '../../../config/configuration';
1818
import { CommandContext, setCommandContext } from '../../../commandContext';
1919
import { SitesAvailableUpdateEvent } from '../../../siteManager';
20-
import { NewIssueMonitor } from '../../../jira/newIssueMonitor';
2120
import { RefreshTimer } from '../../RefreshTimer';
2221
import { viewScreenEvent } from '../../../analytics';
22+
import { JiraNotifier } from './jiraNotifier';
2323

2424
const AssignedWorkItemsViewProviderId = 'atlascode.views.jira.assignedWorkItemsTreeView';
2525

@@ -29,13 +29,15 @@ export class AssignedWorkItemsViewProvider extends Disposable implements TreeDat
2929
private _onDidChangeTreeData = new EventEmitter<TreeItem | undefined | void>();
3030
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
3131

32-
private _disposable: Disposable;
33-
private _initPromises: PromiseRacer<TreeViewIssue[]> | undefined;
34-
private _initChildren: TreeItem[] = [];
35-
private _newIssueMonitor: NewIssueMonitor;
32+
private readonly _disposable: Disposable;
33+
private readonly _initPromises: PromiseRacer<TreeViewIssue[]> | undefined;
34+
private readonly _initChildren: TreeItem[] = [];
35+
private readonly _jiraNotifier = new JiraNotifier();
36+
37+
private _skipNotificationForNextFetch = false;
3638

3739
constructor() {
38-
super(() => this.dispose);
40+
super(() => this.dispose());
3941

4042
setCommandContext(CommandContext.JiraExplorer, false);
4143
setCommandContext(CommandContext.AssignedIssueExplorer, Container.config.jira.explorer.enabled);
@@ -50,8 +52,6 @@ export class AssignedWorkItemsViewProvider extends Disposable implements TreeDat
5052
treeView,
5153
);
5254

53-
this._newIssueMonitor = new NewIssueMonitor(() => Container.jqlManager.getAllDefaultJQLEntries());
54-
5555
const jqlEntries = Container.jqlManager.getAllDefaultJQLEntries();
5656
if (jqlEntries.length) {
5757
this._initPromises = new PromiseRacer(jqlEntries.map(executeJqlQuery));
@@ -72,24 +72,28 @@ export class AssignedWorkItemsViewProvider extends Disposable implements TreeDat
7272
private onConfigurationChanged(e: ConfigurationChangeEvent): void {
7373
if (configuration.changed(e, 'jira.explorer.enabled')) {
7474
setCommandContext(CommandContext.AssignedIssueExplorer, Container.config.jira.explorer.enabled);
75-
this.refresh();
75+
this.refreshWithoutNotifications();
7676
} else if (configuration.changed(e, 'jira.explorer')) {
77-
this.refresh();
77+
this.refreshWithoutNotifications();
7878
}
7979
}
8080

8181
private onSitesDidChange(e: SitesAvailableUpdateEvent): void {
8282
if (e.product.key === ProductJira.key) {
83-
this.refresh();
83+
this.refreshWithoutNotifications();
8484
}
8585
}
8686

8787
public dispose(): void {
8888
this._disposable.dispose();
8989
}
9090

91+
private refreshWithoutNotifications(): void {
92+
this._skipNotificationForNextFetch = true;
93+
this.refresh();
94+
}
95+
9196
private refresh(): void {
92-
this._newIssueMonitor.checkForNewIssues();
9397
this._onDidChangeTreeData.fire();
9498
}
9599

@@ -120,6 +124,8 @@ export class AssignedWorkItemsViewProvider extends Disposable implements TreeDat
120124
}
121125

122126
SearchJiraHelper.appendIssues(issues, AssignedWorkItemsViewProviderId);
127+
this._jiraNotifier.ignoreAssignedIssues(issues);
128+
123129
this._initChildren.push(...this.buildTreeItemsFromIssues(issues));
124130
break;
125131
}
@@ -139,7 +145,16 @@ export class AssignedWorkItemsViewProvider extends Disposable implements TreeDat
139145
}
140146

141147
const allIssues = (await Promise.all(jqlEntries.map(executeJqlQuery))).flat();
148+
149+
if (this._skipNotificationForNextFetch) {
150+
this._skipNotificationForNextFetch = false;
151+
this._jiraNotifier.ignoreAssignedIssues(allIssues);
152+
} else {
153+
this._jiraNotifier.notifyForNewAssignedIssues(allIssues);
154+
}
155+
142156
SearchJiraHelper.setIssues(allIssues, AssignedWorkItemsViewProviderId);
157+
143158
return this.buildTreeItemsFromIssues(allIssues);
144159
}
145160
}

0 commit comments

Comments
 (0)