@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/demo/sortable-rules-demo.html +155 -0
  3. package/dist/temba-components.js +132 -142
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/events.js.map +1 -1
  6. package/out-tsc/src/flow/CanvasNode.js +13 -7
  7. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  8. package/out-tsc/src/flow/actions/send_msg.js +1 -0
  9. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  10. package/out-tsc/src/flow/nodes/split_by_groups.js +149 -1
  11. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  12. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +1 -0
  13. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  14. package/out-tsc/src/flow/nodes/wait_for_response.js +81 -75
  15. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  16. package/out-tsc/src/form/ArrayEditor.js +106 -28
  17. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  18. package/out-tsc/src/form/select/Select.js +21 -25
  19. package/out-tsc/src/form/select/Select.js.map +1 -1
  20. package/out-tsc/src/list/SortableList.js +214 -140
  21. package/out-tsc/src/list/SortableList.js.map +1 -1
  22. package/out-tsc/src/live/ContactChat.js +9 -5
  23. package/out-tsc/src/live/ContactChat.js.map +1 -1
  24. package/out-tsc/test/nodes/split_by_groups.test.js +130 -0
  25. package/out-tsc/test/nodes/split_by_groups.test.js.map +1 -0
  26. package/out-tsc/test/nodes/wait_for_response.test.js +149 -0
  27. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  28. package/out-tsc/test/temba-field-config.test.js +56 -0
  29. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  30. package/package.json +1 -1
  31. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  32. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  33. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  34. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  35. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  36. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  37. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  38. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  39. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  40. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  41. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  42. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  43. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  44. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  45. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  46. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  47. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  48. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  49. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  50. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  51. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  52. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  53. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  54. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  55. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  56. package/screenshots/truth/editor/wait.png +0 -0
  57. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  58. package/screenshots/truth/list/fields-dragging.png +0 -0
  59. package/screenshots/truth/list/sortable-dragging.png +0 -0
  60. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  61. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  62. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  63. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  64. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  65. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  66. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  67. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  68. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  69. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  70. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  71. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  72. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  73. package/screenshots/truth/select/search-enabled.png +0 -0
  74. package/screenshots/truth/select/search-selected-focus.png +0 -0
  75. package/screenshots/truth/select/search-selected.png +0 -0
  76. package/screenshots/truth/templates/default.png +0 -0
  77. package/screenshots/truth/templates/unapproved.png +0 -0
  78. package/src/events.ts +6 -6
  79. package/src/flow/CanvasNode.ts +15 -13
  80. package/src/flow/actions/send_msg.ts +1 -0
  81. package/src/flow/nodes/split_by_groups.ts +190 -1
  82. package/src/flow/nodes/split_by_llm_categorize.ts +1 -0
  83. package/src/flow/nodes/wait_for_response.ts +98 -74
  84. package/src/form/ArrayEditor.ts +112 -28
  85. package/src/form/select/Select.ts +24 -25
  86. package/src/list/SortableList.ts +250 -149
  87. package/src/live/ContactChat.ts +11 -5
  88. package/test/nodes/split_by_groups.test.ts +165 -0
  89. package/test/nodes/wait_for_response.test.ts +182 -0
  90. package/test/temba-field-config.test.ts +69 -0
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 {
@@ -66,8 +66,14 @@ export class CanvasNode extends RapidElement {
66
66
  max-width: 200px;
67
67
  }
68
68
 
69
- .node:hover {
70
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
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.6em;
230
- margin-top: -0.8em;
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
- ${repeat(
987
- this.node.actions,
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`${repeat(
993
- this.node.actions,
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
  };
@@ -31,6 +31,7 @@ export const split_by_llm_categorize: NodeConfig = {
31
31
  label: 'Categories',
32
32
  helpText: 'Define the categories for classification',
33
33
  required: true,
34
+ sortable: true,
34
35
  itemLabel: 'Category',
35
36
  minItems: 1,
36
37
  maxItems: 10,
@@ -47,87 +47,100 @@ const createWaitForResponseRouter = (
47
47
  cat.name !== 'Timeout'
48
48
  );
49
49
 
50
- // Group rules by category name (case-insensitive) to merge them
51
- const rulesByCategory = new Map<string, any[]>();
52
- userRules.forEach((rule) => {
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
- if (!rulesByCategory.has(categoryKey)) {
55
- rulesByCategory.set(categoryKey, []);
56
- }
57
- rulesByCategory.get(categoryKey)!.push(rule);
58
- });
59
+ const categoryName = rule.category.trim(); // Use original casing
59
60
 
60
- // Track category creation order to preserve UUID mapping
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
- // Create categories, exits, and cases for each unique category
70
- categoryOrder.forEach((categoryKey, categoryIndex) => {
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
- // Try to find existing category by position/index to preserve UUIDs when names change
75
- const existingCategory = existingUserCategories[categoryIndex];
76
- const existingExit = existingCategory
77
- ? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
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
- const exitUuid = existingExit?.uuid || generateUUID();
81
- const categoryUuid = existingCategory?.uuid || generateUUID();
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
- // Create single category for all rules with this category name
84
- categories.push({
85
- uuid: categoryUuid,
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
- exits.push({
91
- uuid: exitUuid,
92
- destination_uuid: existingExit?.destination_uuid || null
93
- });
84
+ const exitUuid = existingExit?.uuid || generateUUID();
85
+ const categoryUuid = existingCategory?.uuid || generateUUID();
94
86
 
95
- // Create a case for each rule in this category
96
- rulesForCategory.forEach((rule) => {
97
- // Try to find existing case for this rule by looking at the original rule order
98
- const originalRuleIndex = userRules.findIndex((r) => r === rule);
99
- const existingCase = existingCases[originalRuleIndex];
87
+ categoryInfo = {
88
+ uuid: categoryUuid,
89
+ name: categoryName,
90
+ exit_uuid: exitUuid
91
+ };
100
92
 
101
- const caseUuid = existingCase?.uuid || generateUUID();
93
+ createdCategories.set(categoryKey, categoryInfo);
102
94
 
103
- // Parse rule value based on operator configuration
104
- const operatorConfig = getOperatorConfig(rule.operator);
105
- let arguments_: string[] = [];
95
+ // Add category and exit
96
+ categories.push({
97
+ uuid: categoryUuid,
98
+ name: categoryName,
99
+ exit_uuid: exitUuid
100
+ });
106
101
 
107
- if (operatorConfig) {
108
- if (operatorConfig.operands === 0) {
109
- // No operands needed
110
- arguments_ = [];
111
- } else if (operatorConfig.operands === 2) {
112
- // Split value for two operands (e.g., "1 10" for between)
113
- arguments_ = rule.value
114
- .split(' ')
115
- .filter((arg: string) => arg.trim());
116
- } else {
117
- // Single operand - but split words for operators that expect multiple words
118
- if (rule.value && rule.value.trim()) {
119
- // Split on spaces and filter out empty strings
120
- arguments_ = rule.value
121
- .trim()
122
- .split(/\s+/)
123
- .filter((arg: string) => arg.length > 0);
124
- } else {
125
- arguments_ = [];
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
- // Fallback for unknown operators - split on spaces if value exists
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
- cases.push({
141
- uuid: caseUuid,
142
- type: rule.operator,
143
- arguments: arguments_,
144
- category_uuid: categoryUuid
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