Skip to content

Commit e633087

Browse files
authored
ref(aci): highlight conflicting conditions in migrated automations (#95190)
- instead of having a `findConflictingActionFilterConditions` helper to find conflicts in action filters specifically, i made this function more generic so that it can also be used by triggers. - this properly accounts for migrated automations since event frequency conditions used to be in the WHEN (triggers) but have been moved to the IF (action filters) - added a `AutomationBuilderConflictContext` - updated conflicting condition arrays to be sets, since we're only using them for lookups - added a `conflictReason` prop so that we can add more specific reasons, such as duplicate triggers <img width="1158" alt="Screenshot 2025-07-09 at 4 56 47 PM" src="https://github.com/user-attachments/assets/46ac2b6d-41ee-472c-94cb-3cab007353c9" />
1 parent 37068fd commit e633087

File tree

7 files changed

+370
-233
lines changed

7 files changed

+370
-233
lines changed

static/app/types/workflowEngine/dataConditions.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,3 @@ export interface DataConditionHandler {
106106
type: DataConditionType;
107107
handlerSubgroup?: DataConditionHandlerSubgroupType;
108108
}
109-
110-
// for keeping track of conflicting condition ids in the UI
111-
export interface ConflictingConditions {
112-
conflictingActionFilters: Record<string, string[]>;
113-
conflictingTriggers: string[];
114-
}

static/app/views/automations/components/automationBuilder.tsx

Lines changed: 70 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {t, tct} from 'sentry/locale';
1212
import {space} from 'sentry/styles/space';
1313
import type {SelectValue} from 'sentry/types/core';
1414
import type {
15-
ConflictingConditions,
1615
DataConditionGroup,
1716
DataConditionGroupLogicType,
1817
} from 'sentry/types/workflowEngine/dataConditions';
@@ -21,6 +20,7 @@ import useApi from 'sentry/utils/useApi';
2120
import useOrganization from 'sentry/utils/useOrganization';
2221
import {FILTER_MATCH_OPTIONS} from 'sentry/views/automations/components/actionFilters/constants';
2322
import ActionNodeList from 'sentry/views/automations/components/actionNodeList';
23+
import {AutomationBuilderConflictContext} from 'sentry/views/automations/components/automationBuilderConflictContext';
2424
import {useAutomationBuilderContext} from 'sentry/views/automations/components/automationBuilderContext';
2525
import DataConditionNodeList from 'sentry/views/automations/components/dataConditionNodeList';
2626
import {TRIGGER_MATCH_OPTIONS} from 'sentry/views/automations/components/triggers/constants';
@@ -36,86 +36,83 @@ export default function AutomationBuilder() {
3636
fetchOrgMembers(api, organization.slug);
3737
}, [api, organization]);
3838

39-
const {conflictingTriggers, conflictingActionFilters} =
40-
useMemo((): ConflictingConditions => {
41-
return findConflictingConditions(state.triggers, state.actionFilters);
42-
}, [state]);
39+
const conflictData = useMemo(() => {
40+
return findConflictingConditions(state.triggers, state.actionFilters);
41+
}, [state]);
4342

4443
return (
45-
<Flex direction="column" gap={space(1)}>
46-
<Step>
47-
<StepLead>
48-
{/* TODO: Only make this a selector of "all" is originally selected */}
49-
{tct('[when:When] [selector] of the following occur', {
50-
when: <ConditionBadge />,
51-
selector: (
52-
<EmbeddedWrapper>
53-
<EmbeddedSelectField
54-
styles={{
55-
control: (provided: any) => ({
56-
...provided,
57-
minHeight: '21px',
58-
height: '21px',
59-
}),
60-
}}
61-
inline={false}
62-
isSearchable={false}
63-
isClearable={false}
64-
name="triggers.logicType"
65-
value={state.triggers.logicType}
66-
onChange={(option: SelectValue<DataConditionGroupLogicType>) =>
67-
actions.updateWhenLogicType(option.value)
68-
}
69-
required
70-
flexibleControlStateSize
71-
options={TRIGGER_MATCH_OPTIONS}
72-
size="xs"
73-
/>
74-
</EmbeddedWrapper>
75-
),
76-
})}
77-
</StepLead>
78-
</Step>
79-
<DataConditionNodeList
80-
handlerGroup={DataConditionHandlerGroupType.WORKFLOW_TRIGGER}
81-
placeholder={t('Select a trigger...')}
82-
conditions={state.triggers.conditions}
83-
group="triggers"
84-
onAddRow={type => actions.addWhenCondition(type)}
85-
onDeleteRow={index => actions.removeWhenCondition(index)}
86-
updateCondition={(id, comparison) => actions.updateWhenCondition(id, comparison)}
87-
conflictingConditionIds={conflictingTriggers}
88-
/>
89-
{state.actionFilters.map(actionFilter => (
90-
<ActionFilterBlock
91-
key={`actionFilters.${actionFilter.id}`}
92-
actionFilter={actionFilter}
93-
conflictingConditions={conflictingActionFilters[actionFilter.id] || []}
44+
<AutomationBuilderConflictContext.Provider value={conflictData}>
45+
<Flex direction="column" gap={space(1)}>
46+
<Step>
47+
<StepLead>
48+
{/* TODO: Only make this a selector of "all" is originally selected */}
49+
{tct('[when:When] [selector] of the following occur', {
50+
when: <ConditionBadge />,
51+
selector: (
52+
<EmbeddedWrapper>
53+
<EmbeddedSelectField
54+
styles={{
55+
control: (provided: any) => ({
56+
...provided,
57+
minHeight: '21px',
58+
height: '21px',
59+
}),
60+
}}
61+
inline={false}
62+
isSearchable={false}
63+
isClearable={false}
64+
name={`${state.triggers.id}.logicType`}
65+
value={state.triggers.logicType}
66+
onChange={(option: SelectValue<DataConditionGroupLogicType>) =>
67+
actions.updateWhenLogicType(option.value)
68+
}
69+
required
70+
flexibleControlStateSize
71+
options={TRIGGER_MATCH_OPTIONS}
72+
size="xs"
73+
/>
74+
</EmbeddedWrapper>
75+
),
76+
})}
77+
</StepLead>
78+
</Step>
79+
<DataConditionNodeList
80+
handlerGroup={DataConditionHandlerGroupType.WORKFLOW_TRIGGER}
81+
placeholder={t('Select a trigger...')}
82+
conditions={state.triggers.conditions}
83+
groupId={state.triggers.id}
84+
onAddRow={type => actions.addWhenCondition(type)}
85+
onDeleteRow={index => actions.removeWhenCondition(index)}
86+
updateCondition={(id, comparison) =>
87+
actions.updateWhenCondition(id, comparison)
88+
}
9489
/>
95-
))}
96-
<span>
97-
<PurpleTextButton
98-
borderless
99-
icon={<IconAdd />}
100-
size="xs"
101-
onClick={() => actions.addIf()}
102-
>
103-
{t('If/Then Block')}
104-
</PurpleTextButton>
105-
</span>
106-
</Flex>
90+
{state.actionFilters.map(actionFilter => (
91+
<ActionFilterBlock
92+
key={`actionFilters.${actionFilter.id}`}
93+
actionFilter={actionFilter}
94+
/>
95+
))}
96+
<span>
97+
<PurpleTextButton
98+
borderless
99+
icon={<IconAdd />}
100+
size="xs"
101+
onClick={() => actions.addIf()}
102+
>
103+
{t('If/Then Block')}
104+
</PurpleTextButton>
105+
</span>
106+
</Flex>
107+
</AutomationBuilderConflictContext.Provider>
107108
);
108109
}
109110

110111
interface ActionFilterBlockProps {
111112
actionFilter: DataConditionGroup;
112-
conflictingConditions: string[];
113113
}
114114

115-
function ActionFilterBlock({
116-
actionFilter,
117-
conflictingConditions = [],
118-
}: ActionFilterBlockProps) {
115+
function ActionFilterBlock({actionFilter}: ActionFilterBlockProps) {
119116
const {actions} = useAutomationBuilderContext();
120117

121118
return (
@@ -164,15 +161,14 @@ function ActionFilterBlock({
164161
</Flex>
165162
<DataConditionNodeList
166163
handlerGroup={DataConditionHandlerGroupType.ACTION_FILTER}
167-
placeholder={t('Filter by...')}
168-
group={`actionFilters.${actionFilter.id}`}
164+
placeholder={t('Any event')}
165+
groupId={actionFilter.id}
169166
conditions={actionFilter?.conditions || []}
170167
onAddRow={type => actions.addIfCondition(actionFilter.id, type)}
171168
onDeleteRow={id => actions.removeIfCondition(actionFilter.id, id)}
172169
updateCondition={(id, params) =>
173170
actions.updateIfCondition(actionFilter.id, id, params)
174171
}
175-
conflictingConditionIds={conflictingConditions}
176172
/>
177173
</Flex>
178174
</Step>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {createContext, useContext} from 'react';
2+
3+
export interface ConflictingConditions {
4+
conflictReason: string | null;
5+
conflictingConditionGroups: Record<string, Set<string>>;
6+
}
7+
8+
export const AutomationBuilderConflictContext =
9+
createContext<ConflictingConditions | null>(null);
10+
11+
export const useAutomationBuilderConflictContext = () => {
12+
const context = useContext(AutomationBuilderConflictContext);
13+
if (!context) {
14+
throw new Error(
15+
'useAutomationBuilderConflictContext was called outside of AutomationBuilder'
16+
);
17+
}
18+
return context;
19+
};

0 commit comments

Comments
 (0)