Skip to content

Commit 367b4df

Browse files
committed
feat(chaining): event creation drawer (#6297)
1 parent 3490704 commit 367b4df

26 files changed

Lines changed: 228 additions & 64 deletions

File tree

openaev-api/src/main/java/io/openaev/api/chaining/ConditionMapper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ private static ConditionOutput toConditionOutput(Condition c) {
8686
.keySubtype(c.getKeySubtype())
8787
.type(c.getType() != null ? c.getType().name() : null)
8888
.value(c.getValue())
89+
.caseSensitive(c.isCaseSensitive())
8990
.conditionParentId(parentId)
9091
.mappingType(c.getMappingType())
9192
.build();
@@ -120,6 +121,7 @@ public static Condition toCondition(ConditionCreateInput input, Condition condit
120121
.keySubtype(input.getKeySubtype())
121122
.type(input.getType())
122123
.value(input.getValue())
124+
.caseSensitive(input.getCaseSensitive() == null || input.getCaseSensitive())
123125
.conditionParent(conditionParent)
124126
.mappingType(resolveMappingType(input))
125127
.build();

openaev-api/src/main/java/io/openaev/api/chaining/dto/ConditionCreateInput.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ public class ConditionCreateInput {
4444
@JsonProperty("condition_value")
4545
private String value;
4646

47+
/** Whether the comparison is case-sensitive (default: true) */
48+
@Schema(description = "Whether the comparison is case-sensitive")
49+
@JsonProperty("condition_case_sensitive")
50+
private Boolean caseSensitive;
51+
4752
/** Condition key: Property to be mapped */
4853
@Schema(description = "Property to be mapped")
4954
@JsonProperty("condition_key")

openaev-api/src/main/java/io/openaev/api/chaining/dto/ConditionOutput.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public class ConditionOutput {
3232
@JsonProperty("condition_value")
3333
private String value;
3434

35+
@JsonProperty("condition_case_sensitive")
36+
private boolean caseSensitive;
37+
3538
@JsonProperty("condition_parent_id")
3639
private String conditionParentId;
3740

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.openaev.migration;
2+
3+
import java.sql.Statement;
4+
import org.flywaydb.core.api.migration.BaseJavaMigration;
5+
import org.flywaydb.core.api.migration.Context;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class V5_27__Add_condition_case_sensitive extends BaseJavaMigration {
10+
11+
@Override
12+
public void migrate(Context context) throws Exception {
13+
try (Statement statement = context.getConnection().createStatement()) {
14+
statement.execute(
15+
"ALTER TABLE conditions ADD COLUMN IF NOT EXISTS condition_case_sensitive BOOLEAN NOT NULL DEFAULT TRUE");
16+
}
17+
}
18+
}

openaev-api/src/main/java/io/openaev/utils/ConditionUtils.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,30 @@ public boolean evaluateLeafCondition(String actualValue, Condition filter) {
8282
return true;
8383
}
8484
String target = filter.getValue();
85+
boolean caseSensitive = filter.isCaseSensitive();
8586

8687
switch (type) {
8788
case IS_NULL:
8889
return actualValue == null;
8990
case IS_NOT_NULL:
9091
return actualValue != null;
9192
case EQ:
92-
return actualValue != null && actualValue.equalsIgnoreCase(target);
93+
return actualValue != null
94+
&& (caseSensitive ? actualValue.equals(target) : actualValue.equalsIgnoreCase(target));
9395
case NEQ:
94-
return actualValue != null && !actualValue.equalsIgnoreCase(target);
96+
return actualValue != null
97+
&& (caseSensitive
98+
? !actualValue.equals(target)
99+
: !actualValue.equalsIgnoreCase(target));
95100
case IN, NIN:
96101
if (actualValue == null || target == null) {
97102
return false;
98103
}
99104
List<String> targetList = Arrays.asList(target.split("\\s*,\\s*"));
100-
boolean contains = targetList.stream().anyMatch(actualValue::equalsIgnoreCase);
105+
boolean contains =
106+
caseSensitive
107+
? targetList.stream().anyMatch(actualValue::equals)
108+
: targetList.stream().anyMatch(actualValue::equalsIgnoreCase);
101109
return (type == ConditionType.IN) == contains;
102110
case GT, GTE, LT, LTE:
103111
return handleNumericComparison(actualValue, target, type);

openaev-front/src/admin/components/chaining/logic/Logic.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const Logic = ({ workflowId, context }: LogicProps) => {
2222
const [validAssets, setValidAssets] = useState<ScopeAssetOutput[]>([]);
2323
// Track whether existing steps/events exist
2424
const [hasExistingData, setHasExistingData] = useState<boolean | null>(null);
25+
// Count of existing events (used to generate default names)
26+
const [eventCount, setEventCount] = useState(0);
2527
// Key to force LogicFlow re-mount after adding a step
2628
const [refreshKey, setRefreshKey] = useState(0);
2729
// Drawer navigation state (shared with ChainingFlowConfiguration)
@@ -56,6 +58,7 @@ const Logic = ({ workflowId, context }: LogicProps) => {
5658
const steps: StepOutput[] = stepsRes.data ?? [];
5759
const events: EventOutput[] = conditionsRes.data ?? [];
5860
setHasExistingData(steps.length > 0 || events.length > 0);
61+
setEventCount(events.length);
5962
});
6063
}, [workflowId]);
6164

@@ -64,6 +67,12 @@ const Logic = ({ workflowId, context }: LogicProps) => {
6467
setRefreshKey(k => k + 1);
6568
}, []);
6669

70+
const handleEventCreated = useCallback(() => {
71+
setHasExistingData(true);
72+
setEventCount(c => c + 1);
73+
setRefreshKey(k => k + 1);
74+
}, []);
75+
6776
const handleOpenDrawer = useCallback(() => {
6877
setDrawerView('choose');
6978
}, []);
@@ -119,6 +128,8 @@ const Logic = ({ workflowId, context }: LogicProps) => {
119128
editingEvent={editingEvent}
120129
onEditingEventChange={setEditingEvent}
121130
onStepCreated={handleStepCreated}
131+
onEventCreated={handleEventCreated}
132+
eventCount={eventCount}
122133
/>
123134
</div>
124135
);

openaev-front/src/admin/components/chaining/logic/chaining_flow/ChainingFlowConfiguration.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ interface ChainingFlowConfigurationProps {
4848
meta: EventMeta;
4949
} | null) => void;
5050
onStepCreated: () => void;
51+
onEventCreated: () => void;
52+
eventCount: number;
5153
}
5254

5355
const ChainingFlowConfiguration = ({
@@ -60,6 +62,8 @@ const ChainingFlowConfiguration = ({
6062
editingEvent,
6163
onEditingEventChange,
6264
onStepCreated,
65+
onEventCreated,
66+
eventCount,
6367
}: ChainingFlowConfigurationProps) => {
6468
const { t } = useFormatter();
6569

@@ -253,11 +257,13 @@ const ChainingFlowConfiguration = ({
253257
} else {
254258
await createCondition(event);
255259
MESSAGING$.notifySuccess(t('Event added successfully.'));
260+
onEventCreated();
256261
}
257262
handleCloseAll();
258263
onStepCreated();
259264
} catch {
260-
MESSAGING$.notifyError(t('Failed to create event.'));
265+
if (editingEvent) MESSAGING$.notifyError(t('Failed to update event.'));
266+
else MESSAGING$.notifyError(t('Failed to create event.'));
261267
}
262268
};
263269

@@ -295,6 +301,7 @@ const ChainingFlowConfiguration = ({
295301
onSave={handleSaveEvent}
296302
initialData={editingEvent?.meta.formData}
297303
isEditing={!!editingEvent}
304+
defaultEventName={!editingEvent ? `Event ${eventCount + 1}` : undefined}
298305
/>
299306
</>
300307
);

openaev-front/src/admin/components/chaining/logic/events/ConfigureEventDetail.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface Props {
1313
onSave: (data: EventFormData) => void;
1414
initialData?: EventFormData;
1515
isEditing?: boolean;
16+
defaultEventName?: string;
1617
}
1718

1819
const ConfigureEventDetail: FunctionComponent<Props> = ({
@@ -22,26 +23,28 @@ const ConfigureEventDetail: FunctionComponent<Props> = ({
2223
onSave,
2324
initialData,
2425
isEditing = false,
26+
defaultEventName,
2527
}) => {
2628
const { t } = useFormatter();
2729

2830
return (
2931
<Drawer
3032
open={open}
3133
handleClose={onClose}
32-
title={t('Add Events')}
34+
title={isEditing ? t('Update Event') : t('Add Event')}
3335
>
3436
<div>
3537
<DrawerBreadcrumb
3638
parentLabel={t('Add Component')}
37-
currentLabel={t('Add Event')}
39+
currentLabel={isEditing ? t('Update Event') : t('Add Event')}
3840
onBack={onBack}
3941
/>
4042
<EventCreationForm
4143
onSubmit={onSave}
4244
onCancel={onClose}
4345
initialData={initialData}
4446
submitLabel={isEditing ? t('Update Event') : t('Add Event')}
47+
defaultName={defaultEventName}
4548
/>
4649
</div>
4750
</Drawer>

openaev-front/src/admin/components/chaining/logic/events/EventConditionRow.tsx

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type SelectChangeEvent,
1111
Switch,
1212
TextField,
13+
Tooltip,
1314
Typography,
1415
} from '@mui/material';
1516
import { useTheme } from '@mui/material/styles';
@@ -162,28 +163,30 @@ const EventConditionRow: FunctionComponent<Props> = ({
162163
}}
163164
>
164165
{showCaseSensitive && (
165-
<div style={{
166-
display: 'flex',
167-
alignItems: 'center',
168-
gap: 2,
169-
}}
170-
>
171-
<Switch
172-
size="small"
173-
checked={condition.caseSensitive}
174-
onChange={handleCaseSensitiveToggle}
175-
color="primary"
176-
/>
177-
<Typography
178-
variant="caption"
179-
sx={{
180-
fontWeight: 600,
181-
whiteSpace: 'nowrap',
182-
}}
166+
<Tooltip title={condition.caseSensitive ? t('Case-sensitive') : t('Case-insensitive')}>
167+
<div style={{
168+
display: 'flex',
169+
alignItems: 'center',
170+
gap: 2,
171+
}}
183172
>
184-
{t('Aa')}
185-
</Typography>
186-
</div>
173+
<Switch
174+
size="small"
175+
checked={condition.caseSensitive}
176+
onChange={handleCaseSensitiveToggle}
177+
color="primary"
178+
/>
179+
<Typography
180+
variant="caption"
181+
sx={{
182+
fontWeight: 600,
183+
whiteSpace: 'nowrap',
184+
}}
185+
>
186+
{t('Aa')}
187+
</Typography>
188+
</div>
189+
</Tooltip>
187190
)}
188191

189192
{/* Delete button — only visible when more than one condition */}

openaev-front/src/admin/components/chaining/logic/events/EventCreationForm.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,22 @@ interface EventCreationFormProps {
3030
onCancel: () => void;
3131
initialData?: EventFormData;
3232
submitLabel?: string;
33+
defaultName?: string;
3334
}
3435

3536
const EventCreationForm: FunctionComponent<EventCreationFormProps> = ({
3637
onSubmit,
3738
onCancel,
3839
initialData,
3940
submitLabel,
41+
defaultName,
4042
}) => {
4143
const { t } = useFormatter();
4244
const methods = useForm<EventBaseInput>({
4345
mode: 'onChange',
4446
resolver: zodResolver(eventBaseSchema),
4547
defaultValues: {
46-
event_name: initialData?.name ?? '',
48+
event_name: initialData?.name ?? defaultName ?? '',
4749
event_description: initialData?.description ?? '',
4850
},
4951
});
@@ -69,11 +71,14 @@ const EventCreationForm: FunctionComponent<EventCreationFormProps> = ({
6971

7072
const handleAddConditionGroup = useCallback(() => {
7173
setConditionGroups(prev => [...prev, createEmptyGroup('AND')]);
72-
setGroupOperators(prev => [...prev, 'AND']);
74+
// Preserve the current operator so the new gap stays in sync with the others
75+
setGroupOperators(prev => [...prev, prev[0] ?? 'AND']);
7376
}, []);
7477

7578
const handleUpdateGroupOperator = useCallback((gapIndex: number, op: LogicalOperator) => {
76-
setGroupOperators(prev => prev.map((o, i) => (i === gapIndex ? op : o)));
79+
// The backend stores a single root operator for all groups.
80+
// All gap operators must stay in sync — update every gap to the new value.
81+
setGroupOperators(prev => prev.map(() => op));
7782
}, []);
7883

7984
const cloneGroup = (conditionGroup: ConditionGroup): ConditionGroup => ({
@@ -107,20 +112,25 @@ const EventCreationForm: FunctionComponent<EventCreationFormProps> = ({
107112
const [moved] = srcGroup.conditions.splice(sourceIdx, 1); // remove from source
108113
dstGroup.conditions.splice(destinationIdx, 0, moved); // add in destination
109114

110-
// Remove any top-level group that became empty after the drag
111-
const nonEmptyIndices: number[] = [];
112-
const cleaned = next.filter((g, i) => {
113-
const keep = g.conditions.length > 0 || g.subGroups.length > 0;
114-
if (keep) nonEmptyIndices.push(i);
115-
return keep;
116-
});
115+
// Remove top-level groups that became empty after the drag.
116+
const emptyIndices = next
117+
.map((_, i) => i)
118+
.filter(i => next[i].conditions.length === 0 && next[i].subGroups.length === 0)
119+
.reverse();
120+
121+
let groups = next;
122+
let ops = groupOperators;
117123

118-
// Keep only the operators between groups that both survived
119-
setConditionGroups(cleaned.length > 0 ? cleaned : next);
120-
if (cleaned.length < next.length) {
121-
setGroupOperators(prev => prev.filter((_, i) => nonEmptyIndices.includes(i)));
124+
for (const k of emptyIndices) {
125+
if (groups.length <= 1) break; // always keep at least one group
126+
const opIndex = Math.max(0, k - 1);
127+
groups = groups.filter((_, i) => i !== k);
128+
ops = ops.filter((_, i) => i !== opIndex);
122129
}
123-
}, [conditionGroups]);
130+
131+
setConditionGroups(groups);
132+
setGroupOperators(ops);
133+
}, [conditionGroups, groupOperators]);
124134

125135
const conditionsValid = isEventFormValid({
126136
name: 'placeholder',

0 commit comments

Comments
 (0)