Skip to content

Commit 5a627f5

Browse files
Axon-1349: clone issue (#1231)
* AXON-1349: clone issue * AXON-1349: clone issue * AXON-1349: clone issue * AXON-1349: Conditional Checkboxes Implementation * AXON-1349: proper field mapping * AXON-1349: update form styles * AXON-1349: update styles * AXON-1349: update components, styles * AXON-1349: add tests * AXON-1349: update changelog * AXON-1349: Fixed checkbox initialization * AXON-1349: add progress notification * AXON-1349: refactor
1 parent 58b1f47 commit 5a627f5

File tree

9 files changed

+909
-1
lines changed

9 files changed

+909
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
## What's new in 4.0.6
1010

11+
### Features
12+
13+
- Added clone jira work items support
14+
1115
### Improvements
1216

1317
- Added validation checks to ensure the Jira API Token hasn't expired

src/ipc/issueActions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ export interface OpenStartWorkPageAction extends Action {
129129
issue: MinimalIssue<DetailedSiteInfo>;
130130
}
131131

132+
export interface CloneIssueAction extends Action {
133+
action: 'cloneIssue';
134+
site: DetailedSiteInfo;
135+
issueData: any;
136+
}
137+
132138
export interface WorklogData {
133139
comment: string;
134140
started: string;
@@ -298,6 +304,10 @@ export function isOpenStartWorkPageAction(a: Action): a is OpenStartWorkPageActi
298304
return (<OpenStartWorkPageAction>a).issue !== undefined;
299305
}
300306

307+
export function isCloneIssue(a: Action): a is CloneIssueAction {
308+
return a && a.action === 'cloneIssue';
309+
}
310+
301311
export function isUpdateAiSettings(a: Action): a is UpdateAiSettingsAction {
302312
return a && a.action === 'updateAiSettings' && (<UpdateAiSettingsAction>a).newState !== undefined;
303313
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { User } from '@atlassianlabs/jira-pi-common-models';
2+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3+
import React from 'react';
4+
5+
import CloneForm from './CloneForm';
6+
7+
jest.mock('./UserPickerField', () => {
8+
return function MockUserPickerField({ value, onChange, label, placeholder, required }: any) {
9+
return (
10+
<div data-testid={`user-picker-${label?.toLowerCase().replace(/\s+/g, '-')}`}>
11+
<label>{label}</label>
12+
<input
13+
data-testid={`input-${label?.toLowerCase().replace(/\s+/g, '-')}`}
14+
value={value?.displayName || ''}
15+
onChange={(e) => onChange({ displayName: e.target.value, accountId: 'test-id' })}
16+
placeholder={placeholder}
17+
required={required}
18+
/>
19+
</div>
20+
);
21+
};
22+
});
23+
24+
describe('CloneForm', () => {
25+
const mockCurrentUser: User = {
26+
accountId: 'current-user-id',
27+
displayName: 'Current User',
28+
active: true,
29+
emailAddress: '[email protected]',
30+
key: 'current-user-key',
31+
self: 'https://example.atlassian.net/rest/api/3/user?accountId=current-user-id',
32+
timeZone: 'UTC',
33+
avatarUrls: {
34+
'48x48': 'avatar-url',
35+
'24x24': 'avatar-url-24',
36+
'16x16': 'avatar-url-16',
37+
'32x32': 'avatar-url-32',
38+
},
39+
};
40+
41+
const mockOriginalAssignee: User = {
42+
accountId: 'assignee-id',
43+
displayName: 'Original Assignee',
44+
active: true,
45+
emailAddress: '[email protected]',
46+
key: 'assignee-key',
47+
self: 'https://example.atlassian.net/rest/api/3/user?accountId=assignee-id',
48+
timeZone: 'UTC',
49+
avatarUrls: {
50+
'48x48': 'assignee-avatar-url',
51+
'24x24': 'assignee-avatar-url-24',
52+
'16x16': 'assignee-avatar-url-16',
53+
'32x32': 'assignee-avatar-url-32',
54+
},
55+
};
56+
57+
const mockFetchUsers = jest.fn().mockResolvedValue([]);
58+
59+
const defaultProps = {
60+
onClone: jest.fn(),
61+
onCancel: jest.fn(),
62+
currentUser: mockCurrentUser,
63+
originalSummary: 'Original Issue Summary',
64+
originalAssignee: mockOriginalAssignee,
65+
originalDescription: 'Original Description',
66+
fetchUsers: mockFetchUsers,
67+
hasDescription: true,
68+
hasLinkedIssues: true,
69+
hasChildIssues: false,
70+
};
71+
72+
beforeEach(() => {
73+
jest.clearAllMocks();
74+
});
75+
76+
it('renders without crashing', () => {
77+
render(<CloneForm {...defaultProps} />);
78+
expect(screen.getByText('Clone Issue')).toBeTruthy();
79+
});
80+
81+
it('displays the correct title and instructions', () => {
82+
render(<CloneForm {...defaultProps} />);
83+
expect(screen.getByText('Clone Issue')).toBeTruthy();
84+
expect(screen.getByText('Required fields are marked with an asterisk *')).toBeTruthy();
85+
});
86+
87+
it('pre-fills summary with CLONE prefix', () => {
88+
render(<CloneForm {...defaultProps} />);
89+
const summaryInput = screen.getByDisplayValue('CLONE - Original Issue Summary');
90+
expect(summaryInput).toBeTruthy();
91+
});
92+
93+
it('renders all required form fields', () => {
94+
render(<CloneForm {...defaultProps} />);
95+
96+
expect(screen.getByDisplayValue('CLONE - Original Issue Summary')).toBeTruthy();
97+
expect(screen.getByTestId('input-assignee-')).toBeTruthy();
98+
expect(screen.getByTestId('input-reporter')).toBeTruthy();
99+
});
100+
101+
it('pre-fills assignee with original assignee', () => {
102+
render(<CloneForm {...defaultProps} />);
103+
const assigneeInput = screen.getByTestId('input-assignee-') as HTMLInputElement;
104+
expect(assigneeInput.value).toBe('Original Assignee');
105+
});
106+
107+
it('pre-fills reporter with current user', () => {
108+
render(<CloneForm {...defaultProps} />);
109+
const reporterInput = screen.getByTestId('input-reporter') as HTMLInputElement;
110+
expect(reporterInput.value).toBe('Current User');
111+
});
112+
113+
it('renders include section when options are available', () => {
114+
render(<CloneForm {...defaultProps} />);
115+
expect(screen.getByText('Include')).toBeTruthy();
116+
expect(screen.getByText('Description')).toBeTruthy();
117+
expect(screen.getByText('Linked issues')).toBeTruthy();
118+
});
119+
120+
it('does not render include section when no options are available', () => {
121+
const propsWithoutOptions = {
122+
...defaultProps,
123+
hasDescription: false,
124+
hasLinkedIssues: false,
125+
hasChildIssues: false,
126+
};
127+
render(<CloneForm {...propsWithoutOptions} />);
128+
expect(screen.queryByText('Include')).toBeFalsy();
129+
});
130+
131+
it('renders only available include options', () => {
132+
const propsWithLimitedOptions = {
133+
...defaultProps,
134+
hasDescription: true,
135+
hasLinkedIssues: false,
136+
hasChildIssues: true,
137+
};
138+
render(<CloneForm {...propsWithLimitedOptions} />);
139+
140+
expect(screen.getByText('Description')).toBeTruthy();
141+
expect(screen.queryByText('Linked issues')).toBeFalsy();
142+
expect(screen.getByText('Child issues')).toBeTruthy();
143+
});
144+
145+
it('renders action buttons', () => {
146+
render(<CloneForm {...defaultProps} />);
147+
expect(screen.getByText('Cancel')).toBeTruthy();
148+
expect(screen.getByText('Clone')).toBeTruthy();
149+
});
150+
151+
it('calls onCancel when Cancel button is clicked', () => {
152+
render(<CloneForm {...defaultProps} />);
153+
const cancelButton = screen.getByText('Cancel');
154+
fireEvent.click(cancelButton);
155+
expect(defaultProps.onCancel).toHaveBeenCalledTimes(1);
156+
});
157+
158+
it('calls onClone with correct data when form is submitted', async () => {
159+
render(<CloneForm {...defaultProps} />);
160+
161+
const summaryInput = screen.getByDisplayValue('CLONE - Original Issue Summary');
162+
fireEvent.change(summaryInput, { target: { value: 'Updated Summary' } });
163+
164+
const assigneeInput = screen.getByTestId('input-assignee-');
165+
fireEvent.change(assigneeInput, { target: { value: 'New Assignee' } });
166+
167+
const reporterInput = screen.getByTestId('input-reporter');
168+
fireEvent.change(reporterInput, { target: { value: 'New Reporter' } });
169+
170+
const descriptionCheckbox = screen.getByLabelText('Description');
171+
fireEvent.click(descriptionCheckbox);
172+
173+
const linkedIssuesCheckbox = screen.getByLabelText('Linked issues');
174+
fireEvent.click(linkedIssuesCheckbox);
175+
176+
const cloneButton = screen.getByText('Clone');
177+
fireEvent.click(cloneButton);
178+
179+
await waitFor(() => {
180+
expect(defaultProps.onClone).toHaveBeenCalledWith({
181+
summary: 'Updated Summary',
182+
assignee: { displayName: 'New Assignee', accountId: 'test-id' },
183+
reporter: { displayName: 'New Reporter', accountId: 'test-id' },
184+
cloneOptions: {
185+
includeDescription: true,
186+
includeLinkedIssues: true,
187+
includeChildIssues: false,
188+
},
189+
});
190+
});
191+
});
192+
193+
it('handles checkbox state changes correctly', () => {
194+
render(<CloneForm {...defaultProps} />);
195+
196+
const descriptionCheckbox = screen.getByLabelText('Description') as HTMLInputElement;
197+
const linkedIssuesCheckbox = screen.getByLabelText('Linked issues') as HTMLInputElement;
198+
199+
expect(descriptionCheckbox.checked).toBeFalsy();
200+
expect(linkedIssuesCheckbox.checked).toBeFalsy();
201+
202+
fireEvent.click(descriptionCheckbox);
203+
expect(descriptionCheckbox.checked).toBeTruthy();
204+
205+
fireEvent.click(linkedIssuesCheckbox);
206+
expect(linkedIssuesCheckbox.checked).toBeTruthy();
207+
});
208+
209+
it('handles empty original assignee correctly', () => {
210+
const propsWithoutAssignee = {
211+
...defaultProps,
212+
originalAssignee: null,
213+
};
214+
render(<CloneForm {...propsWithoutAssignee} />);
215+
216+
const assigneeInput = screen.getByTestId('input-assignee-') as HTMLInputElement;
217+
expect(assigneeInput.value).toBe('');
218+
});
219+
220+
it('passes fetchUsers prop to UserPickerField components', () => {
221+
render(<CloneForm {...defaultProps} />);
222+
223+
expect(screen.getByTestId('user-picker-assignee-')).toBeTruthy();
224+
expect(screen.getByTestId('user-picker-reporter')).toBeTruthy();
225+
});
226+
227+
it('handles form submission with default values', async () => {
228+
render(<CloneForm {...defaultProps} />);
229+
230+
const cloneButton = screen.getByText('Clone');
231+
fireEvent.click(cloneButton);
232+
233+
await waitFor(() => {
234+
expect(defaultProps.onClone).toHaveBeenCalledWith({
235+
summary: 'CLONE - Original Issue Summary',
236+
assignee: mockOriginalAssignee,
237+
reporter: mockCurrentUser,
238+
cloneOptions: {
239+
includeDescription: false,
240+
includeLinkedIssues: false,
241+
includeChildIssues: false,
242+
},
243+
});
244+
});
245+
});
246+
247+
it('updates state when form fields change', () => {
248+
render(<CloneForm {...defaultProps} />);
249+
250+
const summaryInput = screen.getByDisplayValue('CLONE - Original Issue Summary') as HTMLInputElement;
251+
fireEvent.change(summaryInput, { target: { value: 'New Summary' } });
252+
expect(summaryInput.value).toBe('New Summary');
253+
});
254+
255+
it('renders with correct styling classes', () => {
256+
const { container } = render(<CloneForm {...defaultProps} />);
257+
258+
const paperElement = container.querySelector('.MuiPaper-root');
259+
expect(paperElement).toBeTruthy();
260+
});
261+
});

0 commit comments

Comments
 (0)