Skip to content

Commit 26dd0c0

Browse files
authored
AXON-1544: Send to Rovo Dev or Start Work from Jira Creation (#1331)
* AXON-1544: Add Dropdown button to CreateIssuePage * AXON-1544: link action button to messaging * AXON-1544: Can trigger Start Work and Rovo Dev from Issue creation * AXON-1544: fix unit tests * AXON-1544: fix spinner styles + add loading * AXON-1544: update changelog * AXON-1544: fix logic ordering * AXON-1544: remove commands from palette * AXON-1544: fix e2e tests * AXON-1544: fix e2e tests 2 * AXON-1544: fix e2e 3 * AXON-1544: add analytics * AXON-1544: e2e
1 parent 2618d00 commit 26dd0c0

File tree

11 files changed

+175
-37
lines changed

11 files changed

+175
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Added development field support
8+
- Added Branch creation and Rovo Dev actions in Create Issue Page
89

910
## What's new in 4.0.9
1011

e2e/page-objects/CreateIssuePage.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@ export class CreateIssuePage {
2020
}
2121

2222
async createIssue() {
23-
const createButton = this.frame.getByRole('button', { name: 'Create' });
23+
const createButton = this.frame.getByRole('button', { name: 'Create', exact: true });
2424
await createButton.scrollIntoViewIfNeeded();
2525
await this.page.waitForTimeout(500);
2626
await createButton.click();
2727
}
2828

2929
async expectIssueCreated(issueKey: string) {
30-
await expect(
31-
this.page.getByRole('dialog', { name: new RegExp(`Issue ${issueKey} has been created`) }),
32-
).toBeVisible();
30+
const newIssueFrame = this.page.frameLocator('iframe.webview').frameLocator(`iframe[title="${issueKey}"]`);
31+
expect(newIssueFrame).toBeTruthy();
3332
}
3433
}

e2e/scenarios/jira/createIssue.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Page } from '@playwright/test';
2-
import { AppNotifications, AtlascodeDrawer, AtlassianSettings, CreateIssuePage } from 'e2e/page-objects';
2+
import { AtlascodeDrawer, AtlassianSettings, CreateIssuePage } from 'e2e/page-objects';
33

44
const NEW_ISSUE_SUMMARY = 'Test Issue Created via E2E Test';
55
const NEW_ISSUE_KEY = 'BTS-7';
@@ -19,5 +19,4 @@ export async function createIssue(page: Page) {
1919
await page.waitForTimeout(1_000);
2020

2121
await createIssuePage.expectIssueCreated(NEW_ISSUE_KEY);
22-
await new AppNotifications(page).expectNotification(`Issue ${NEW_ISSUE_KEY} has been created`);
2322
}

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@
202202
},
203203
{
204204
"command": "atlascode.jira.createIssue.startWork",
205-
"title": "Create and start work",
205+
"title": "Create and create branch",
206206
"category": "Jira"
207207
},
208208
{
@@ -1093,16 +1093,20 @@
10931093
{
10941094
"command": "atlascode.jira.createIssue.generateCode",
10951095
"when": "false"
1096+
},
1097+
{
1098+
"command": "atlascode.jira.expandCreateWorkItem",
1099+
"when": "false"
10961100
}
10971101
],
10981102
"webview/context": [
10991103
{
11001104
"command": "atlascode.jira.createIssue.startWork",
1101-
"when": "webviewId == 'atlascode.views.jira.createWorkItemWebview' && webviewSection == 'createButton'"
1105+
"when": "webviewSection == 'createButton'"
11021106
},
11031107
{
11041108
"command": "atlascode.jira.createIssue.generateCode",
1105-
"when": "atlascode:rovoDevEnabled && webviewId == 'atlascode.views.jira.createWorkItemWebview' && webviewSection == 'createButton'"
1109+
"when": "atlascode:rovoDevEnabled && webviewSection == 'createButton'"
11061110
},
11071111
{
11081112
"command": "atlascode.jira.copyImageElement",

src/container.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,14 @@ export class Container {
260260
context.subscriptions.push(new CustomJQLViewProvider());
261261
context.subscriptions.push((this._assignedWorkItemsView = new AssignedWorkItemsViewProvider()));
262262

263-
context.subscriptions.push(
264-
(this._createWorkItemWebviewProvider = new CreateWorkItemWebviewProvider(context, context.extensionPath)),
265-
);
266-
263+
if (this.featureFlagClient.checkGate(Features.CreateWorkItemWebviewV2)) {
264+
context.subscriptions.push(
265+
(this._createWorkItemWebviewProvider = new CreateWorkItemWebviewProvider(
266+
context,
267+
context.extensionPath,
268+
)),
269+
);
270+
}
267271
this._onboardingProvider = new OnboardingProvider();
268272

269273
this.refreshRovoDev(context);

src/ipc/issueActions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export interface CreateSelectOptionAction extends Action {
111111
export interface CreateIssueAction extends Action {
112112
site: DetailedSiteInfo;
113113
issueData: any;
114+
onCreateAction?: 'createAndView' | 'createAndStartWork' | 'createAndGenerateCode';
114115
}
115116

116117
export interface CreateIssueLinkAction extends Action {

src/webviews/components/App.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,9 @@ div[role='dialog'],
932932
background: var(--vscode-button-background) !important;
933933
border-radius: 2px !important;
934934
}
935+
.ac-button-spinner {
936+
--ds-icon-subtle: var(--vscode-button-foreground) !important;
937+
}
935938

936939
.ac-button span {
937940
color: var(--vscode-button-foreground) !important;

src/webviews/components/issue/create-issue-screen/CreateIssuePage.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import Button from '@atlaskit/button';
2-
import LoadingButton from '@atlaskit/button/loading-button';
32
import Form, { ErrorMessage, Field, FormFooter, FormHeader, RequiredAsterisk } from '@atlaskit/form';
43
import Page from '@atlaskit/page';
54
import Select, { components } from '@atlaskit/select';
@@ -27,20 +26,23 @@ import {
2726
CommonEditorViewState,
2827
emptyCommonEditorState,
2928
} from '../AbstractIssueEditorPage';
29+
import { CreateIssueButton } from './actions/CreateIssueButton';
3030
import { Panel } from './Panel';
3131

3232
type Emit = CommonEditorPageEmit;
3333
type Accept = CommonEditorPageAccept | CreateIssueData;
3434
interface ViewState extends CommonEditorViewState, CreateIssueData {
3535
createdIssue: IssueKeyAndSite<DetailedSiteInfo>;
3636
formKey: string;
37+
onCreateAction: 'createAndView' | 'createAndStartWork' | 'createAndGenerateCode';
3738
}
3839

3940
const emptyState: ViewState = {
4041
...emptyCommonEditorState,
4142
...emptyCreateIssueData,
4243
createdIssue: { key: '', siteDetails: emptySiteInfo },
4344
formKey: v4(),
45+
onCreateAction: 'createAndView',
4446
};
4547

4648
const fallbackTimerDuration = 5000; // 5 seconds
@@ -108,6 +110,7 @@ export default class CreateIssuePage extends AbstractIssueEditorPage<Emit, Accep
108110
private attachingInProgress = false;
109111
private initialFieldValues: FieldValues = {};
110112
private suggestionFallbackTimer: NodeJS.Timeout | null = null;
113+
private formRef = React.createRef<HTMLFormElement>();
111114

112115
getProjectKey(): string {
113116
return this.state.fieldValues['project'].key;
@@ -236,6 +239,15 @@ export default class CreateIssuePage extends AbstractIssueEditorPage<Emit, Accep
236239
});
237240
break;
238241
}
242+
case 'createIssueWithAction': {
243+
handled = true;
244+
245+
this.setState({ onCreateAction: e.action }, () => {
246+
this.formRef.current?.requestSubmit();
247+
this.setState({ onCreateAction: 'createAndView' });
248+
});
249+
break;
250+
}
239251
}
240252
}
241253

@@ -300,6 +312,7 @@ export default class CreateIssuePage extends AbstractIssueEditorPage<Emit, Accep
300312
action: 'createIssue',
301313
site: this.state.siteDetails,
302314
issueData: this.state.fieldValues,
315+
onCreateAction: this.state.onCreateAction,
303316
});
304317

305318
return undefined;
@@ -506,7 +519,7 @@ export default class CreateIssuePage extends AbstractIssueEditorPage<Emit, Accep
506519
<Form name="create-issue" key={this.state.formKey} onSubmit={this.handleSubmit}>
507520
{(frmArgs: any) => {
508521
return (
509-
<form {...frmArgs.formProps}>
522+
<form {...frmArgs.formProps} ref={this.formRef}>
510523
<FormHeader title={this.formHeader()}>
511524
<p>
512525
Required fields are marked with an asterisk <RequiredAsterisk />
@@ -544,15 +557,18 @@ export default class CreateIssuePage extends AbstractIssueEditorPage<Emit, Accep
544557
</Panel>
545558
)}
546559
<FormFooter actions={{}}>
547-
<LoadingButton
560+
<CreateIssueButton
548561
type="submit"
549-
spacing="compact"
562+
name="Create"
550563
className="ac-button"
551-
isDisabled={this.state.isSomethingLoading}
552-
isLoading={this.state.loadingField === 'submitButton'}
564+
disabled={this.state.isSomethingLoading}
565+
isLoading={
566+
this.state.isSomethingLoading &&
567+
this.state.loadingField === 'submitButton'
568+
}
553569
>
554570
Create
555-
</LoadingButton>
571+
</CreateIssueButton>
556572
</FormFooter>
557573
</form>
558574
);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
2+
import Spinner from '@atlaskit/spinner';
3+
import React, { ButtonHTMLAttributes } from 'react';
4+
5+
type CreateIssueButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & { isLoading?: boolean };
6+
7+
export const CreateIssueButton: React.FC<CreateIssueButtonProps> = (props) => {
8+
return (
9+
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }} className="ac-button">
10+
<button
11+
style={{
12+
border: 'none',
13+
padding: '6px 12px',
14+
color: 'var(--vscode-button-foreground)',
15+
cursor: 'pointer',
16+
}}
17+
{...props}
18+
>
19+
{props.children}
20+
</button>
21+
<div
22+
style={{
23+
width: '1px',
24+
height: 'var(--vscode-font-size)',
25+
backgroundColor: 'var(--vscode-button-foreground)',
26+
}}
27+
/>
28+
<button
29+
style={{
30+
border: 'none',
31+
padding: '6px',
32+
color: 'var(--vscode-button-foreground)',
33+
cursor: 'pointer',
34+
}}
35+
className="ac-button ac-button-spinner"
36+
data-vscode-context='{"webviewSection": "createButton", "preventDefaultContextMenuItems": true}'
37+
onClick={(e) => {
38+
e.preventDefault();
39+
e.target.dispatchEvent(
40+
new MouseEvent('contextmenu', {
41+
bubbles: true,
42+
clientX: e.clientX,
43+
clientY: e.clientY,
44+
}),
45+
);
46+
e.stopPropagation();
47+
}}
48+
>
49+
{props.isLoading ? (
50+
<Spinner size="small" label="Loading create issue options" />
51+
) : (
52+
<ChevronDownIcon label="Create issue options" size="small" />
53+
)}
54+
</button>
55+
</div>
56+
);
57+
};

src/webviews/createIssueWebview.test.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ jest.mock('../container', () => ({
4949
createOrShow: jest.fn(),
5050
},
5151
machineId: 'test-machine-id',
52+
rovodevWebviewProvider: {
53+
setPromptTextWithFocus: jest.fn(),
54+
},
55+
isRovoDevEnabled: true,
5256
},
5357
}));
5458

@@ -82,6 +86,11 @@ jest.mock('form-data', () => ({
8286
jest.mock('../commands/jira/showIssue', () => ({
8387
showIssue: jest.fn(),
8488
}));
89+
90+
jest.mock('../commands/jira/startWorkOnIssue', () => ({
91+
startWorkOnIssue: jest.fn(),
92+
}));
93+
8594
// Added feature flag mock as with jiraIssueWebview.test.ts
8695
jest.mock('src/util/featureFlags', () => ({
8796
FeatureFlagClient: {
@@ -965,6 +974,7 @@ describe('CreateIssueWebview', () => {
965974

966975
describe('onMessageReceived', () => {
967976
const mockShowIssue = require('../commands/jira/showIssue').showIssue;
977+
const mockStartWorkOnIssue = require('../commands/jira/startWorkOnIssue').startWorkOnIssue;
968978

969979
const mockMessage = {
970980
action: 'createIssue',
@@ -991,26 +1001,41 @@ describe('CreateIssueWebview', () => {
9911001
});
9921002
});
9931003

994-
it('should show VS Code notification on successful issue creation', async () => {
1004+
it('should show Jira Issue on successful issue creation', async () => {
9951005
await (webview as any).onMessageReceived(mockMessage);
9961006

997-
expect(mockWindow.showInformationMessage).toHaveBeenCalledWith(
998-
'Issue TEST-123 has been created',
999-
'Open Issue',
1000-
);
1007+
expect(mockShowIssue).toHaveBeenCalledWith({
1008+
key: 'TEST-123',
1009+
siteDetails: mockSiteDetails,
1010+
});
10011011
});
10021012

1003-
it('should call showIssue when user clicks Open Issue button', async () => {
1004-
mockWindow.showInformationMessage = jest.fn().mockResolvedValue('Open Issue');
1005-
await (webview as any).onMessageReceived(mockMessage);
1013+
it('should call startWorkOnIssue when user clicks Create and create branch button', async () => {
1014+
const message = { ...mockMessage, onCreateAction: 'createAndStartWork' as const };
1015+
await (webview as any).onMessageReceived(message);
10061016
await new Promise((resolve) => setTimeout(resolve, 0));
10071017

1008-
expect(mockShowIssue).toHaveBeenCalledWith({
1018+
expect(mockStartWorkOnIssue).toHaveBeenCalledWith({
10091019
key: 'TEST-123',
10101020
siteDetails: mockSiteDetails,
10111021
});
10121022
});
10131023

1024+
it('should call Rovo Dev webview provider when user clicks Create and generate code button', async () => {
1025+
const message = { ...mockMessage, onCreateAction: 'createAndGenerateCode' as const };
1026+
const issueUrl = `${mockSiteDetails.baseLinkUrl}/browse/TEST-123`;
1027+
await (webview as any).onMessageReceived(message);
1028+
await new Promise((resolve) => setTimeout(resolve, 0));
1029+
expect(Container.rovodevWebviewProvider.setPromptTextWithFocus).toHaveBeenCalledWith(
1030+
'Work on the attached Jira work item',
1031+
{
1032+
contextType: 'jiraWorkItem',
1033+
name: 'TEST-123',
1034+
url: issueUrl,
1035+
},
1036+
);
1037+
});
1038+
10141039
it('should handle refresh action', async () => {
10151040
const refreshMessage = { action: 'refresh' };
10161041
const forceUpdateSpy = jest.spyOn(webview, 'forceUpdateFields').mockImplementation(async () => {});

0 commit comments

Comments
 (0)