Skip to content

Commit 6ed7626

Browse files
authored
AXON-1414: keep fields data (#1249)
1 parent bee1e24 commit 6ed7626

File tree

2 files changed

+196
-1
lines changed

2 files changed

+196
-1
lines changed

src/webviews/components/issue/AbstractIssueEditorPage.test.tsx

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@ const mockAsyncSelect = jest.fn(({ defaultValue, ...props }) => (
2424
</div>
2525
));
2626

27+
// Mock Select to capture props
28+
const mockSelect = jest.fn(({ defaultValue, ...props }) => (
29+
<div className="ac-form-select-container" data-testid="select" data-default-value={JSON.stringify(defaultValue)}>
30+
Mocked Select
31+
</div>
32+
));
33+
2734
jest.mock('@atlaskit/select', () => ({
2835
__esModule: true,
29-
default: jest.fn(),
36+
default: (props: any) => mockSelect(props),
3037
AsyncSelect: (props: any) => mockAsyncSelect(props),
3138
components: {
3239
Option: ({ children, ...props }: any) => <div {...props}>{children}</div>,
@@ -80,6 +87,33 @@ class TestIssueEditorPage extends AbstractIssueEditorPage<
8087
return this.getInputMarkup(parentField, editMode, currentIssueType);
8188
}
8289

90+
renderIssueLinksField(editMode: boolean = false) {
91+
const issueLinksField: FieldUI = {
92+
key: 'issuelinks',
93+
name: 'Linked Issues',
94+
required: false,
95+
uiType: UIType.IssueLinks,
96+
displayOrder: 2,
97+
valueType: ValueType.IssueLinks,
98+
advanced: true,
99+
isArray: true,
100+
schema: 'issuelinks',
101+
};
102+
103+
const currentIssueType: IssueType = {
104+
id: '1',
105+
name: 'Story',
106+
iconUrl: 'story-icon.png',
107+
subtask: false,
108+
avatarId: 1,
109+
description: 'Story issue type',
110+
self: 'https://test.atlassian.net/rest/api/3/issuetype/1',
111+
epic: false,
112+
};
113+
114+
return this.getInputMarkup(issueLinksField, editMode, currentIssueType);
115+
}
116+
83117
override render() {
84118
return <div data-testid="test-container">{this.renderParentField(false)}</div>;
85119
}
@@ -116,6 +150,7 @@ describe('AbstractIssueEditorPage', () => {
116150
beforeEach(() => {
117151
jest.clearAllMocks();
118152
mockAsyncSelect.mockClear();
153+
mockSelect.mockClear();
119154
});
120155

121156
describe('Parent Field', () => {
@@ -224,4 +259,155 @@ describe('AbstractIssueEditorPage', () => {
224259
});
225260
});
226261
});
262+
263+
describe('IssueLinks Field (Linked Issues)', () => {
264+
describe('Non-Edit Mode (Create Issue)', () => {
265+
class TestIssueEditorPageIssueLinks extends TestIssueEditorPage {
266+
override render() {
267+
return <div data-testid="test-container">{this.renderIssueLinksField(false)}</div>;
268+
}
269+
}
270+
271+
it('should pass link type value as defaultValue to Select when issuelinks.type exists', () => {
272+
const linkTypeValue = {
273+
id: '10000',
274+
name: 'Blocks',
275+
inward: 'is blocked by',
276+
outward: 'blocks',
277+
type: 'outward',
278+
};
279+
280+
const component = new TestIssueEditorPageIssueLinks({});
281+
component.state = {
282+
...emptyCommonEditorState,
283+
siteDetails: mockSiteDetailsCloud,
284+
fieldValues: {
285+
issuelinks: {
286+
type: linkTypeValue,
287+
},
288+
project: { key: 'TEST' },
289+
},
290+
selectFieldOptions: {
291+
issuelinks: [linkTypeValue],
292+
},
293+
};
294+
295+
render(component.render());
296+
297+
// Check that Select was called with correct defaultValue for link type
298+
expect(mockSelect).toHaveBeenCalled();
299+
const selectCallArgs = mockSelect.mock.calls[0][0];
300+
expect(selectCallArgs.defaultValue).toEqual(linkTypeValue);
301+
});
302+
303+
it('should pass undefined as defaultValue to Select when issuelinks.type is missing', () => {
304+
const component = new TestIssueEditorPageIssueLinks({});
305+
component.state = {
306+
...emptyCommonEditorState,
307+
siteDetails: mockSiteDetailsCloud,
308+
fieldValues: {
309+
project: { key: 'TEST' },
310+
},
311+
selectFieldOptions: {
312+
issuelinks: [],
313+
},
314+
};
315+
316+
render(component.render());
317+
318+
// Check that Select was called with undefined defaultValue
319+
expect(mockSelect).toHaveBeenCalled();
320+
const selectCallArgs = mockSelect.mock.calls[0][0];
321+
expect(selectCallArgs.defaultValue).toBeUndefined();
322+
});
323+
324+
it('should pass linked issues array as defaultValue to AsyncSelect when issuelinks.issue exists', () => {
325+
const linkedIssues = [
326+
{ key: 'PROJ-123', summary: 'First linked issue' },
327+
{ key: 'PROJ-456', summary: 'Second linked issue' },
328+
];
329+
330+
const component = new TestIssueEditorPageIssueLinks({});
331+
component.state = {
332+
...emptyCommonEditorState,
333+
siteDetails: mockSiteDetailsCloud,
334+
fieldValues: {
335+
issuelinks: {
336+
issue: linkedIssues,
337+
},
338+
project: { key: 'TEST' },
339+
},
340+
selectFieldOptions: {
341+
issuelinks: [],
342+
},
343+
};
344+
345+
render(component.render());
346+
347+
// Check that AsyncSelect was called with correct defaultValue for linked issues
348+
expect(mockAsyncSelect).toHaveBeenCalled();
349+
const asyncSelectCallArgs = mockAsyncSelect.mock.calls[0][0];
350+
expect(asyncSelectCallArgs.defaultValue).toEqual(linkedIssues);
351+
});
352+
353+
it('should pass undefined as defaultValue to AsyncSelect when issuelinks.issue is missing', () => {
354+
const component = new TestIssueEditorPageIssueLinks({});
355+
component.state = {
356+
...emptyCommonEditorState,
357+
siteDetails: mockSiteDetailsCloud,
358+
fieldValues: {
359+
project: { key: 'TEST' },
360+
},
361+
selectFieldOptions: {
362+
issuelinks: [],
363+
},
364+
};
365+
366+
render(component.render());
367+
368+
// Check that AsyncSelect was called with undefined defaultValue
369+
expect(mockAsyncSelect).toHaveBeenCalled();
370+
const asyncSelectCallArgs = mockAsyncSelect.mock.calls[0][0];
371+
expect(asyncSelectCallArgs.defaultValue).toBeUndefined();
372+
});
373+
374+
it('should preserve both link type and linked issues when re-rendering', () => {
375+
const linkTypeValue = {
376+
id: '10001',
377+
name: 'Relates',
378+
inward: 'relates to',
379+
outward: 'relates to',
380+
type: 'inward',
381+
};
382+
383+
const linkedIssues = [{ key: 'PROJ-789', summary: 'Related issue' }];
384+
385+
const component = new TestIssueEditorPageIssueLinks({});
386+
component.state = {
387+
...emptyCommonEditorState,
388+
siteDetails: mockSiteDetailsCloud,
389+
fieldValues: {
390+
issuelinks: {
391+
type: linkTypeValue,
392+
issue: linkedIssues,
393+
},
394+
project: { key: 'TEST' },
395+
},
396+
selectFieldOptions: {
397+
issuelinks: [linkTypeValue],
398+
},
399+
};
400+
401+
render(component.render());
402+
403+
expect(mockSelect).toHaveBeenCalled();
404+
const selectCallArgs = mockSelect.mock.calls[0][0];
405+
expect(selectCallArgs.defaultValue).toEqual(linkTypeValue);
406+
407+
expect(mockAsyncSelect).toHaveBeenCalled();
408+
const asyncSelectCallArgs = mockAsyncSelect.mock.calls[0][0];
409+
expect(asyncSelectCallArgs.defaultValue).toEqual(linkedIssues);
410+
});
411+
});
412+
});
227413
});

src/webviews/components/issue/AbstractIssueEditorPage.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,8 @@ export abstract class AbstractIssueEditorPage<
943943
);
944944
} else {
945945
const validateFunc = field.required ? FieldValidators.validateSingleSelect : undefined;
946+
const defaultLinkType = this.state.fieldValues[field.key]?.type || undefined;
947+
946948
return (
947949
<React.Fragment>
948950
<Field
@@ -962,6 +964,8 @@ export abstract class AbstractIssueEditorPage<
962964
<div>
963965
<Select
964966
{...fieldArgs.fieldProps}
967+
key={`${field.key}.type-${defaultLinkType?.id || 'empty'}`}
968+
defaultValue={defaultLinkType}
965969
isMulti={false}
966970
isClearable={!field.required}
967971
className="ac-form-select-container"
@@ -996,9 +1000,14 @@ export abstract class AbstractIssueEditorPage<
9961000
</Field>
9971001
<Field id={`${field.key}.issue`} name={`${field.key}.issue`}>
9981002
{(fieldArgs: any) => {
1003+
const defaultLinkedIssues = this.state.fieldValues[field.key]?.issue || undefined;
1004+
const linkedIssuesKey = `${field.key}.issue-${JSON.stringify(defaultLinkedIssues || [])}`;
1005+
9991006
return (
10001007
<AsyncSelect
10011008
{...fieldArgs.fieldProps}
1009+
key={linkedIssuesKey}
1010+
defaultValue={defaultLinkedIssues}
10021011
isClearable={true}
10031012
isMulti={true}
10041013
className="ac-form-select-container"

0 commit comments

Comments
 (0)