Skip to content

Commit c69ad97

Browse files
authored
fix(aci): Fix bug with github action settings (#112742)
# Description This PR will fix an issue where github settings weren't showing up in the form component. https://github.com/user-attachments/assets/643cf8f4-05d4-4fb2-a75e-014072b69fa7 Fixes [ISWF-851](https://linear.app/getsentry/issue/ISWF-851/alert-builder-github-action-settings-does-not-show-correct)
1 parent dbc367e commit c69ad97

File tree

3 files changed

+232
-7
lines changed

3 files changed

+232
-7
lines changed

static/app/views/automations/components/actionNodeList.spec.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from 'sentry/types/workflowEngine/actions';
1616
import {ActionNodeList} from 'sentry/views/automations/components/actionNodeList';
1717
import {AutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext';
18+
import {AutomationFormProvider} from 'sentry/views/automations/components/forms/context';
1819

1920
const slackActionHandler = ActionHandlerFixture();
2021
const actionHandlers: ActionHandler[] = [
@@ -186,11 +187,13 @@ describe('ActionNodeList', () => {
186187
});
187188
const jiraAction = ActionFixture({type: ActionType.JIRA});
188189
render(
189-
<Form model={model}>
190-
<AutomationBuilderErrorContext.Provider value={defaultErrorContextProps}>
191-
<ActionNodeList {...defaultProps} actions={[jiraAction]} />
192-
</AutomationBuilderErrorContext.Provider>
193-
</Form>,
190+
<AutomationFormProvider>
191+
<Form model={model}>
192+
<AutomationBuilderErrorContext.Provider value={defaultErrorContextProps}>
193+
<ActionNodeList {...defaultProps} actions={[jiraAction]} />
194+
</AutomationBuilderErrorContext.Provider>
195+
</Form>
196+
</AutomationFormProvider>,
194197
{
195198
organization,
196199
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import {
2+
ActionFilterFixture,
3+
ActionFixture,
4+
AutomationFixture,
5+
} from 'sentry-fixture/automations';
6+
import {ActionHandlerFixture} from 'sentry-fixture/workflowEngine';
7+
8+
import {
9+
render,
10+
renderGlobalModal,
11+
screen,
12+
userEvent,
13+
} from 'sentry-test/reactTestingLibrary';
14+
15+
import type {Action} from 'sentry/types/workflowEngine/actions';
16+
import {ActionGroup, ActionType} from 'sentry/types/workflowEngine/actions';
17+
import type {Automation} from 'sentry/types/workflowEngine/automations';
18+
import {ActionNodeContext} from 'sentry/views/automations/components/actionNodes';
19+
import {TicketActionSettingsButton} from 'sentry/views/automations/components/actions/ticketActionSettingsButton';
20+
import {AutomationFormProvider} from 'sentry/views/automations/components/forms/context';
21+
22+
function renderComponent({
23+
action,
24+
automation,
25+
}: {
26+
action: Action;
27+
automation?: Automation;
28+
}) {
29+
const handler = ActionHandlerFixture({
30+
type: ActionType.JIRA,
31+
handlerGroup: ActionGroup.TICKET_CREATION,
32+
});
33+
34+
renderGlobalModal();
35+
36+
render(
37+
<AutomationFormProvider automation={automation}>
38+
<ActionNodeContext.Provider
39+
value={{
40+
action,
41+
actionId: `actionFilters.0.action.${action.id}`,
42+
handler,
43+
onUpdate: jest.fn(),
44+
}}
45+
>
46+
<TicketActionSettingsButton />
47+
</ActionNodeContext.Provider>
48+
</AutomationFormProvider>
49+
);
50+
}
51+
52+
/**
53+
* Mock the integration config API to return string fields for the given field
54+
* names. When the modal opens, instance values for these fields will be set as
55+
* the form input defaults, so we can assert on them.
56+
*/
57+
function mockIntegrationConfig(integrationId: string, fieldNames: string[] = []) {
58+
return MockApiClient.addMockResponse({
59+
url: `/organizations/org-slug/integrations/${integrationId}/`,
60+
body: {
61+
createIssueConfig: fieldNames.map(name => ({
62+
name,
63+
label: name,
64+
type: 'string',
65+
})),
66+
},
67+
});
68+
}
69+
70+
describe('TicketActionSettingsButton', () => {
71+
it('uses additional_fields and dynamic_form_fields from ticketAction.data', async () => {
72+
mockIntegrationConfig('int-1', ['project', 'issuetype']);
73+
74+
const action = ActionFixture({
75+
id: '42',
76+
type: ActionType.JIRA,
77+
integrationId: 'int-1',
78+
data: {
79+
additional_fields: {project: '10000', issuetype: '10001'},
80+
dynamic_form_fields: [{name: 'priority', label: 'Priority', type: 'select'}],
81+
},
82+
});
83+
84+
renderComponent({action});
85+
86+
await userEvent.click(screen.getByRole('button', {name: 'Action Settings'}));
87+
88+
// Modal opens and form fields render with instance values as defaults
89+
expect(await screen.findByRole('textbox', {name: 'project'})).toHaveValue('10000');
90+
expect(screen.getByRole('textbox', {name: 'issuetype'})).toHaveValue('10001');
91+
});
92+
93+
it('falls back to savedActionData with camelCase keys from API response', async () => {
94+
mockIntegrationConfig('int-1', ['project', 'reporter']);
95+
96+
const action = ActionFixture({
97+
id: '42',
98+
type: ActionType.JIRA,
99+
integrationId: 'int-1',
100+
data: {},
101+
});
102+
103+
const automation = AutomationFixture({
104+
actionFilters: [
105+
ActionFilterFixture({
106+
actions: [
107+
ActionFixture({
108+
id: '42',
109+
data: {
110+
additionalFields: {project: 'CAMEL', reporter: 'me'},
111+
dynamicFormFields: [{name: 'camelField', label: 'Camel', type: 'text'}],
112+
},
113+
}),
114+
],
115+
}),
116+
],
117+
});
118+
119+
renderComponent({action, automation});
120+
121+
await userEvent.click(screen.getByRole('button', {name: 'Action Settings'}));
122+
123+
expect(await screen.findByRole('textbox', {name: 'project'})).toHaveValue('CAMEL');
124+
expect(screen.getByRole('textbox', {name: 'reporter'})).toHaveValue('me');
125+
});
126+
127+
it('falls back to savedActionData with snake_case keys from frontend write', async () => {
128+
mockIntegrationConfig('int-1', ['project', 'reporter']);
129+
130+
const action = ActionFixture({
131+
id: '42',
132+
type: ActionType.JIRA,
133+
integrationId: 'int-1',
134+
data: {},
135+
});
136+
137+
const automation = AutomationFixture({
138+
actionFilters: [
139+
ActionFilterFixture({
140+
actions: [
141+
ActionFixture({
142+
id: '42',
143+
data: {
144+
additional_fields: {project: 'SNAKE', reporter: 'them'},
145+
dynamic_form_fields: [{name: 'snakeField', label: 'Snake', type: 'text'}],
146+
},
147+
}),
148+
],
149+
}),
150+
],
151+
});
152+
153+
renderComponent({action, automation});
154+
155+
await userEvent.click(screen.getByRole('button', {name: 'Action Settings'}));
156+
157+
expect(await screen.findByRole('textbox', {name: 'project'})).toHaveValue('SNAKE');
158+
expect(screen.getByRole('textbox', {name: 'reporter'})).toHaveValue('them');
159+
});
160+
161+
it('uses savedActionData dynamic_form_fields when ticketAction has empty array', async () => {
162+
mockIntegrationConfig('int-1', ['project']);
163+
164+
const action = ActionFixture({
165+
id: '42',
166+
type: ActionType.JIRA,
167+
integrationId: 'int-1',
168+
data: {
169+
additional_fields: {project: 'DIRECT'},
170+
dynamic_form_fields: [],
171+
},
172+
});
173+
174+
const automation = AutomationFixture({
175+
actionFilters: [
176+
ActionFilterFixture({
177+
actions: [
178+
ActionFixture({
179+
id: '42',
180+
data: {
181+
dynamicFormFields: [
182+
{name: 'fromSaved', label: 'From Saved', type: 'select'},
183+
],
184+
},
185+
}),
186+
],
187+
}),
188+
],
189+
});
190+
191+
renderComponent({action, automation});
192+
193+
await userEvent.click(screen.getByRole('button', {name: 'Action Settings'}));
194+
195+
expect(await screen.findByRole('textbox', {name: 'project'})).toHaveValue('DIRECT');
196+
});
197+
});

static/app/views/automations/components/actions/ticketActionSettingsButton.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {useMemo} from 'react';
2+
13
import {Button} from '@sentry/scraps/button';
24

35
import {openModal} from 'sentry/actionCreators/modal';
@@ -11,9 +13,11 @@ import {
1113
actionNodesMap,
1214
useActionNodeContext,
1315
} from 'sentry/views/automations/components/actionNodes';
16+
import {useAutomationFormContext} from 'sentry/views/automations/components/forms/context';
1417

1518
export function TicketActionSettingsButton() {
1619
const {action, onUpdate} = useActionNodeContext();
20+
const {automation} = useAutomationFormContext();
1721

1822
const ticketAction = action as TicketCreationAction;
1923

@@ -42,10 +46,31 @@ export function TicketActionSettingsButton() {
4246
});
4347
};
4448

49+
// Find saved action data from the API response
50+
const savedActionData = useMemo(() => {
51+
if (!automation) return undefined;
52+
53+
for (const af of automation.actionFilters) {
54+
const found = af.actions?.find(a => a.id === action.id);
55+
if (found) return found.data;
56+
}
57+
58+
return undefined;
59+
}, [automation, action.id]);
60+
61+
const additionalFields =
62+
ticketAction.data.additional_fields ??
63+
savedActionData?.additionalFields ??
64+
savedActionData?.additional_fields;
65+
66+
const dynamicFormFields = ticketAction.data.dynamic_form_fields?.length
67+
? ticketAction.data.dynamic_form_fields
68+
: (savedActionData?.dynamicFormFields ?? savedActionData?.dynamic_form_fields ?? []);
69+
4570
const instance = {
46-
...ticketAction.data.additional_fields,
71+
...additionalFields,
4772
integration: ticketAction.integrationId,
48-
dynamic_form_fields: ticketAction.data.dynamic_form_fields || [],
73+
dynamic_form_fields: dynamicFormFields,
4974
} as TicketActionData;
5075

5176
return (

0 commit comments

Comments
 (0)