@nyaruka/temba-components 0.130.4 → 0.130.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/demo/sortable-rules-demo.html +155 -0
- package/dist/temba-components.js +132 -142
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +13 -7
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +1 -0
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_groups.js +149 -1
- package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +1 -0
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +81 -75
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +106 -28
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +21 -25
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +214 -140
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +9 -5
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/test/nodes/split_by_groups.test.js +130 -0
- package/out-tsc/test/nodes/split_by_groups.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_response.test.js +149 -0
- package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
- package/out-tsc/test/temba-field-config.test.js +56 -0
- package/out-tsc/test/temba-field-config.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/field-renderer/select-with-label.png +0 -0
- package/screenshots/truth/list/fields-dragging.png +0 -0
- package/screenshots/truth/list/sortable-dragging.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/select/search-enabled.png +0 -0
- package/screenshots/truth/select/search-selected-focus.png +0 -0
- package/screenshots/truth/select/search-selected.png +0 -0
- package/screenshots/truth/templates/default.png +0 -0
- package/screenshots/truth/templates/unapproved.png +0 -0
- package/src/events.ts +6 -6
- package/src/flow/CanvasNode.ts +15 -13
- package/src/flow/actions/send_msg.ts +1 -0
- package/src/flow/nodes/split_by_groups.ts +190 -1
- package/src/flow/nodes/split_by_llm_categorize.ts +1 -0
- package/src/flow/nodes/wait_for_response.ts +98 -74
- package/src/form/ArrayEditor.ts +112 -28
- package/src/form/select/Select.ts +24 -25
- package/src/list/SortableList.ts +250 -149
- package/src/live/ContactChat.ts +11 -5
- package/test/nodes/split_by_groups.test.ts +165 -0
- package/test/nodes/wait_for_response.test.ts +182 -0
- package/test/temba-field-config.test.ts +69 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/events.ts
CHANGED
|
@@ -57,13 +57,13 @@ export interface ChatStartedEvent extends ContactEvent {
|
|
|
57
57
|
|
|
58
58
|
export interface MsgEvent extends ContactEvent {
|
|
59
59
|
msg: Msg;
|
|
60
|
-
status: string;
|
|
61
|
-
failed_reason?: string;
|
|
62
|
-
failed_reason_display?: string;
|
|
63
|
-
logs_url: string;
|
|
64
|
-
recipient_count?: number;
|
|
65
|
-
created_by?: User;
|
|
66
60
|
optin?: ObjectReference;
|
|
61
|
+
_status?: string;
|
|
62
|
+
_failed_reason?: string;
|
|
63
|
+
_logs_url?: string;
|
|
64
|
+
status?: string; // deprecated
|
|
65
|
+
failed_reason_display?: string; // deprecated
|
|
66
|
+
logs_url?: string; // deprecated
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export interface RunEvent extends ContactEvent {
|
package/src/flow/CanvasNode.ts
CHANGED
|
@@ -66,8 +66,14 @@ export class CanvasNode extends RapidElement {
|
|
|
66
66
|
max-width: 200px;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
.node:
|
|
70
|
-
|
|
69
|
+
.node .action:last-child {
|
|
70
|
+
border-bottom-left-radius: var(--curvature);
|
|
71
|
+
border-bottom-right-radius: var(--curvature);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.node .action:first-child {
|
|
75
|
+
border-top-left-radius: var(--curvature);
|
|
76
|
+
border-top-right-radius: var(--curvature);
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
.node.dragging {
|
|
@@ -226,8 +232,8 @@ export class CanvasNode extends RapidElement {
|
|
|
226
232
|
}
|
|
227
233
|
|
|
228
234
|
.action-exits {
|
|
229
|
-
padding-bottom: 0.
|
|
230
|
-
margin-top: -0.
|
|
235
|
+
padding-bottom: 0.7em;
|
|
236
|
+
margin-top: -0.7em;
|
|
231
237
|
}
|
|
232
238
|
|
|
233
239
|
.category .cn-title {
|
|
@@ -860,7 +866,7 @@ export class CanvasNode extends RapidElement {
|
|
|
860
866
|
class="action-content"
|
|
861
867
|
@mousedown=${(e: MouseEvent) => this.handleActionMouseDown(e, action)}
|
|
862
868
|
@mouseup=${(e: MouseEvent) => this.handleActionMouseUp(e, action)}
|
|
863
|
-
style="cursor: pointer;"
|
|
869
|
+
style="cursor: pointer; background: #fff"
|
|
864
870
|
>
|
|
865
871
|
${this.renderTitle(config, action, index, isRemoving)}
|
|
866
872
|
<div class="body">
|
|
@@ -983,16 +989,12 @@ export class CanvasNode extends RapidElement {
|
|
|
983
989
|
dragHandle="drag-handle"
|
|
984
990
|
@temba-order-changed="${this.handleActionOrderChanged}"
|
|
985
991
|
>
|
|
986
|
-
${
|
|
987
|
-
this.node
|
|
988
|
-
(action) => action.uuid,
|
|
989
|
-
(action, index) => this.renderAction(this.node, action, index)
|
|
992
|
+
${this.node.actions.map((action, index) =>
|
|
993
|
+
this.renderAction(this.node, action, index)
|
|
990
994
|
)}
|
|
991
995
|
</temba-sortable-list>`
|
|
992
|
-
: html`${
|
|
993
|
-
this.node
|
|
994
|
-
(action) => action.uuid,
|
|
995
|
-
(action, index) => this.renderAction(this.node, action, index)
|
|
996
|
+
: html`${this.node.actions.map((action, index) =>
|
|
997
|
+
this.renderAction(this.node, action, index)
|
|
996
998
|
)}`
|
|
997
999
|
: ''}
|
|
998
1000
|
${this.node.router
|
|
@@ -58,6 +58,7 @@ export const send_msg: ActionConfig = {
|
|
|
58
58
|
type: 'array',
|
|
59
59
|
helpText: 'Add dynamic attachments using expressions',
|
|
60
60
|
itemLabel: 'Attachment',
|
|
61
|
+
sortable: true,
|
|
61
62
|
maxItems: 10,
|
|
62
63
|
isEmptyItem: (item: any) => {
|
|
63
64
|
return !item.expression || item.expression.trim() === '';
|
|
@@ -1,7 +1,196 @@
|
|
|
1
1
|
import { COLORS, NodeConfig } from '../types';
|
|
2
|
+
import { Node, Category, Exit, Case } from '../../store/flow-definition.d';
|
|
3
|
+
import { generateUUID } from '../../utils';
|
|
4
|
+
|
|
5
|
+
// Helper function to create a switch router with group cases
|
|
6
|
+
const createGroupRouter = (
|
|
7
|
+
userGroups: { uuid: string; name: string }[],
|
|
8
|
+
existingCategories: Category[] = [],
|
|
9
|
+
existingExits: Exit[] = [],
|
|
10
|
+
existingCases: Case[] = []
|
|
11
|
+
) => {
|
|
12
|
+
const categories: Category[] = [];
|
|
13
|
+
const exits: Exit[] = [];
|
|
14
|
+
const cases: Case[] = [];
|
|
15
|
+
|
|
16
|
+
// Create categories, exits, and cases for each selected group
|
|
17
|
+
userGroups.forEach((group) => {
|
|
18
|
+
// Try to find existing category by group name
|
|
19
|
+
const existingCategory = existingCategories.find(
|
|
20
|
+
(cat) => cat.name === group.name
|
|
21
|
+
);
|
|
22
|
+
const existingExit = existingCategory
|
|
23
|
+
? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
|
|
24
|
+
: null;
|
|
25
|
+
const existingCase = existingCases.find(
|
|
26
|
+
(c) => c.arguments?.[0] === group.uuid
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const exitUuid = existingExit?.uuid || generateUUID();
|
|
30
|
+
const categoryUuid = existingCategory?.uuid || generateUUID();
|
|
31
|
+
const caseUuid = existingCase?.uuid || generateUUID();
|
|
32
|
+
|
|
33
|
+
categories.push({
|
|
34
|
+
uuid: categoryUuid,
|
|
35
|
+
name: group.name,
|
|
36
|
+
exit_uuid: exitUuid
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
exits.push({
|
|
40
|
+
uuid: exitUuid,
|
|
41
|
+
destination_uuid: existingExit?.destination_uuid || null
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cases.push({
|
|
45
|
+
uuid: caseUuid,
|
|
46
|
+
type: 'has_group',
|
|
47
|
+
arguments: [group.uuid, group.name],
|
|
48
|
+
category_uuid: categoryUuid
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Add default "Other" category for contacts not in any selected group
|
|
53
|
+
const existingOtherCategory = existingCategories.find(
|
|
54
|
+
(cat) =>
|
|
55
|
+
cat.name === 'Other' &&
|
|
56
|
+
!userGroups.some((group) => group.name === cat.name)
|
|
57
|
+
);
|
|
58
|
+
const existingOtherExit = existingOtherCategory
|
|
59
|
+
? existingExits.find(
|
|
60
|
+
(exit) => exit.uuid === existingOtherCategory.exit_uuid
|
|
61
|
+
)
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
const otherExitUuid = existingOtherExit?.uuid || generateUUID();
|
|
65
|
+
const otherCategoryUuid = existingOtherCategory?.uuid || generateUUID();
|
|
66
|
+
|
|
67
|
+
categories.push({
|
|
68
|
+
uuid: otherCategoryUuid,
|
|
69
|
+
name: 'Other',
|
|
70
|
+
exit_uuid: otherExitUuid
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
exits.push({
|
|
74
|
+
uuid: otherExitUuid,
|
|
75
|
+
destination_uuid: existingOtherExit?.destination_uuid || null
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
router: {
|
|
80
|
+
type: 'switch' as const,
|
|
81
|
+
cases: cases,
|
|
82
|
+
categories: categories,
|
|
83
|
+
default_category_uuid: otherCategoryUuid,
|
|
84
|
+
operand: '@contact.groups',
|
|
85
|
+
result_name: ''
|
|
86
|
+
},
|
|
87
|
+
exits: exits
|
|
88
|
+
};
|
|
89
|
+
};
|
|
2
90
|
|
|
3
91
|
export const split_by_groups: NodeConfig = {
|
|
4
92
|
type: 'split_by_groups',
|
|
5
93
|
name: 'Split by Group',
|
|
6
|
-
color: COLORS.split
|
|
94
|
+
color: COLORS.split,
|
|
95
|
+
form: {
|
|
96
|
+
groups: {
|
|
97
|
+
type: 'select',
|
|
98
|
+
label: 'Groups',
|
|
99
|
+
helpText:
|
|
100
|
+
'Select the groups to split contacts by. Contacts will be routed based on their group membership.',
|
|
101
|
+
required: true,
|
|
102
|
+
options: [],
|
|
103
|
+
multi: true,
|
|
104
|
+
searchable: true,
|
|
105
|
+
endpoint: '/api/v2/groups.json',
|
|
106
|
+
valueKey: 'uuid',
|
|
107
|
+
nameKey: 'name',
|
|
108
|
+
placeholder: 'Search for groups...',
|
|
109
|
+
allowCreate: true,
|
|
110
|
+
createArbitraryOption: (input: string, options: any[]) => {
|
|
111
|
+
// Check if a group with this name already exists
|
|
112
|
+
const existing = options.find(
|
|
113
|
+
(option) =>
|
|
114
|
+
option.name.toLowerCase().trim() === input.toLowerCase().trim()
|
|
115
|
+
);
|
|
116
|
+
if (!existing && input.trim()) {
|
|
117
|
+
return {
|
|
118
|
+
name: input.trim(),
|
|
119
|
+
arbitrary: true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
layout: ['groups'],
|
|
127
|
+
validate: (formData: any) => {
|
|
128
|
+
const errors: { [key: string]: string } = {};
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
!formData.groups ||
|
|
132
|
+
!Array.isArray(formData.groups) ||
|
|
133
|
+
formData.groups.length === 0
|
|
134
|
+
) {
|
|
135
|
+
errors.groups = 'At least one group is required';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
valid: Object.keys(errors).length === 0,
|
|
140
|
+
errors
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
toFormData: (node: Node) => {
|
|
144
|
+
// Extract groups from the existing node structure
|
|
145
|
+
const groups: { uuid: string; name: string }[] = [];
|
|
146
|
+
|
|
147
|
+
if (node.router?.cases) {
|
|
148
|
+
node.router.cases.forEach((c: Case) => {
|
|
149
|
+
if (c.type === 'has_group' && c.arguments?.length >= 2) {
|
|
150
|
+
groups.push({
|
|
151
|
+
uuid: c.arguments[0],
|
|
152
|
+
name: c.arguments[1]
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
uuid: node.uuid,
|
|
160
|
+
groups: groups
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
fromFormData: (formData: any, originalNode: Node): Node => {
|
|
164
|
+
// Get selected groups
|
|
165
|
+
const selectedGroups = (formData.groups || [])
|
|
166
|
+
.filter((group: any) => group?.uuid || group?.arbitrary)
|
|
167
|
+
.map((group: any) => ({
|
|
168
|
+
uuid: group.uuid || generateUUID(), // Generate UUID for arbitrary groups
|
|
169
|
+
name: group.name
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
// Create router and exits using existing data when possible
|
|
173
|
+
const existingCategories = originalNode.router?.categories || [];
|
|
174
|
+
const existingExits = originalNode.exits || [];
|
|
175
|
+
const existingCases = originalNode.router?.cases || [];
|
|
176
|
+
|
|
177
|
+
const { router, exits } = createGroupRouter(
|
|
178
|
+
selectedGroups,
|
|
179
|
+
existingCategories,
|
|
180
|
+
existingExits,
|
|
181
|
+
existingCases
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Return the complete node
|
|
185
|
+
return {
|
|
186
|
+
uuid: originalNode.uuid,
|
|
187
|
+
actions: originalNode.actions || [],
|
|
188
|
+
router: router,
|
|
189
|
+
exits: exits
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
router: {
|
|
193
|
+
type: 'switch',
|
|
194
|
+
operand: '@contact.groups'
|
|
195
|
+
}
|
|
7
196
|
};
|
|
@@ -47,87 +47,100 @@ const createWaitForResponseRouter = (
|
|
|
47
47
|
cat.name !== 'Timeout'
|
|
48
48
|
);
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
50
|
+
// Track categories as we create them (case-insensitive lookup)
|
|
51
|
+
const createdCategories = new Map<
|
|
52
|
+
string,
|
|
53
|
+
{ uuid: string; name: string; exit_uuid: string }
|
|
54
|
+
>();
|
|
55
|
+
|
|
56
|
+
// Process rules in their original order to preserve rule order
|
|
57
|
+
userRules.forEach((rule, ruleIndex) => {
|
|
53
58
|
const categoryKey = rule.category.trim().toLowerCase();
|
|
54
|
-
|
|
55
|
-
rulesByCategory.set(categoryKey, []);
|
|
56
|
-
}
|
|
57
|
-
rulesByCategory.get(categoryKey)!.push(rule);
|
|
58
|
-
});
|
|
59
|
+
const categoryName = rule.category.trim(); // Use original casing
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
const categoryOrder: string[] = [];
|
|
62
|
-
userRules.forEach((rule) => {
|
|
63
|
-
const categoryKey = rule.category.trim().toLowerCase();
|
|
64
|
-
if (!categoryOrder.includes(categoryKey)) {
|
|
65
|
-
categoryOrder.push(categoryKey);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
61
|
+
let categoryInfo = createdCategories.get(categoryKey);
|
|
68
62
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const rulesForCategory = rulesByCategory.get(categoryKey)!;
|
|
72
|
-
const categoryName = rulesForCategory[0].category.trim(); // Use the first occurrence's casing
|
|
63
|
+
if (!categoryInfo) {
|
|
64
|
+
// First time seeing this category - create it
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
: null;
|
|
66
|
+
// Smart category matching: try by name first, then fall back to position
|
|
67
|
+
let existingCategory = existingUserCategories.find(
|
|
68
|
+
(cat) => cat.name.toLowerCase() === categoryKey
|
|
69
|
+
);
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
// If no match by name, try by position (for category rename scenarios)
|
|
72
|
+
const categoryCreationOrder = Array.from(createdCategories.keys()).length;
|
|
73
|
+
if (
|
|
74
|
+
!existingCategory &&
|
|
75
|
+
categoryCreationOrder < existingUserCategories.length
|
|
76
|
+
) {
|
|
77
|
+
existingCategory = existingUserCategories[categoryCreationOrder];
|
|
78
|
+
}
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
name: categoryName,
|
|
87
|
-
exit_uuid: exitUuid
|
|
88
|
-
});
|
|
80
|
+
const existingExit = existingCategory
|
|
81
|
+
? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
|
|
82
|
+
: null;
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
uuid
|
|
92
|
-
destination_uuid: existingExit?.destination_uuid || null
|
|
93
|
-
});
|
|
84
|
+
const exitUuid = existingExit?.uuid || generateUUID();
|
|
85
|
+
const categoryUuid = existingCategory?.uuid || generateUUID();
|
|
94
86
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
categoryInfo = {
|
|
88
|
+
uuid: categoryUuid,
|
|
89
|
+
name: categoryName,
|
|
90
|
+
exit_uuid: exitUuid
|
|
91
|
+
};
|
|
100
92
|
|
|
101
|
-
|
|
93
|
+
createdCategories.set(categoryKey, categoryInfo);
|
|
102
94
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
// Add category and exit
|
|
96
|
+
categories.push({
|
|
97
|
+
uuid: categoryUuid,
|
|
98
|
+
name: categoryName,
|
|
99
|
+
exit_uuid: exitUuid
|
|
100
|
+
});
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
102
|
+
exits.push({
|
|
103
|
+
uuid: exitUuid,
|
|
104
|
+
destination_uuid: existingExit?.destination_uuid || null
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create case for this rule
|
|
109
|
+
let existingCase = existingCases[ruleIndex];
|
|
110
|
+
|
|
111
|
+
// If we can't find by position, try to find by matching rule content
|
|
112
|
+
if (!existingCase && existingCases.length > 0) {
|
|
113
|
+
existingCase = existingCases.find((case_) => {
|
|
114
|
+
// Find the category for this case
|
|
115
|
+
const caseCategory = existingCategories.find(
|
|
116
|
+
(cat) => cat.uuid === case_.category_uuid
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Match by operator type and category name
|
|
120
|
+
return (
|
|
121
|
+
case_.type === rule.operator &&
|
|
122
|
+
caseCategory?.name.toLowerCase() === categoryKey
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const caseUuid = existingCase?.uuid || generateUUID();
|
|
128
|
+
|
|
129
|
+
// Parse rule value based on operator configuration
|
|
130
|
+
const operatorConfig = getOperatorConfig(rule.operator);
|
|
131
|
+
let arguments_: string[] = [];
|
|
132
|
+
|
|
133
|
+
if (operatorConfig) {
|
|
134
|
+
if (operatorConfig.operands === 0) {
|
|
135
|
+
// No operands needed
|
|
136
|
+
arguments_ = [];
|
|
137
|
+
} else if (operatorConfig.operands === 2) {
|
|
138
|
+
// Split value for two operands (e.g., "1 10" for between)
|
|
139
|
+
arguments_ = rule.value.split(' ').filter((arg: string) => arg.trim());
|
|
128
140
|
} else {
|
|
129
|
-
//
|
|
141
|
+
// Single operand - but split words for operators that expect multiple words
|
|
130
142
|
if (rule.value && rule.value.trim()) {
|
|
143
|
+
// Split on spaces and filter out empty strings
|
|
131
144
|
arguments_ = rule.value
|
|
132
145
|
.trim()
|
|
133
146
|
.split(/\s+/)
|
|
@@ -136,13 +149,23 @@ const createWaitForResponseRouter = (
|
|
|
136
149
|
arguments_ = [];
|
|
137
150
|
}
|
|
138
151
|
}
|
|
152
|
+
} else {
|
|
153
|
+
// Fallback for unknown operators - split on spaces if value exists
|
|
154
|
+
if (rule.value && rule.value.trim()) {
|
|
155
|
+
arguments_ = rule.value
|
|
156
|
+
.trim()
|
|
157
|
+
.split(/\s+/)
|
|
158
|
+
.filter((arg: string) => arg.length > 0);
|
|
159
|
+
} else {
|
|
160
|
+
arguments_ = [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
139
163
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
});
|
|
164
|
+
cases.push({
|
|
165
|
+
uuid: caseUuid,
|
|
166
|
+
type: rule.operator,
|
|
167
|
+
arguments: arguments_,
|
|
168
|
+
category_uuid: categoryInfo.uuid
|
|
146
169
|
});
|
|
147
170
|
});
|
|
148
171
|
|
|
@@ -211,6 +234,7 @@ export const wait_for_response: NodeConfig = {
|
|
|
211
234
|
itemLabel: 'Rule',
|
|
212
235
|
minItems: 0,
|
|
213
236
|
maxItems: 100,
|
|
237
|
+
sortable: true,
|
|
214
238
|
maintainEmptyItem: true, // Explicitly enable empty item maintenance
|
|
215
239
|
isEmptyItem: (item: any) => {
|
|
216
240
|
// Helper function to get operator value from various formats
|