@nyaruka/temba-components 0.129.7 → 0.129.8

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 (127) hide show
  1. package/.devcontainer/Dockerfile +11 -4
  2. package/.devcontainer/devcontainer.json +3 -2
  3. package/.github/workflows/build.yml +4 -14
  4. package/CHANGELOG.md +8 -3
  5. package/demo/components/flow/example.html +1 -1
  6. package/demo/components/message-editor/example.html +125 -0
  7. package/demo/components/textinput/completion.html +1 -0
  8. package/demo/data/flows/food-order.json +12 -21
  9. package/demo/data/flows/sample-flow.json +42 -26
  10. package/dist/temba-components.js +506 -218
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/Thumbnail.js +2 -1
  13. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  14. package/out-tsc/src/events.js.map +1 -1
  15. package/out-tsc/src/flow/NodeEditor.js +245 -22
  16. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  17. package/out-tsc/src/flow/actions/call_webhook.js +26 -17
  18. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  19. package/out-tsc/src/flow/actions/send_msg.js +147 -6
  20. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  21. package/out-tsc/src/flow/types.js.map +1 -1
  22. package/out-tsc/src/form/ArrayEditor.js +111 -38
  23. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  24. package/out-tsc/src/form/BaseListEditor.js +19 -4
  25. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  26. package/out-tsc/src/form/FormField.js +1 -1
  27. package/out-tsc/src/form/FormField.js.map +1 -1
  28. package/out-tsc/src/form/KeyValueEditor.js +1 -1
  29. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  30. package/out-tsc/src/form/MediaPicker.js +13 -1
  31. package/out-tsc/src/form/MediaPicker.js.map +1 -1
  32. package/out-tsc/src/form/MessageEditor.js +422 -0
  33. package/out-tsc/src/form/MessageEditor.js.map +1 -0
  34. package/out-tsc/src/form/TextInput.js +12 -5
  35. package/out-tsc/src/form/TextInput.js.map +1 -1
  36. package/out-tsc/src/form/select/Select.js +4 -4
  37. package/out-tsc/src/form/select/Select.js.map +1 -1
  38. package/out-tsc/src/live/ContactChat.js +27 -2
  39. package/out-tsc/src/live/ContactChat.js.map +1 -1
  40. package/out-tsc/temba-modules.js +2 -0
  41. package/out-tsc/temba-modules.js.map +1 -1
  42. package/out-tsc/test/temba-field-config.test.js +4 -2
  43. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  44. package/out-tsc/test/temba-message-editor.test.js +194 -0
  45. package/out-tsc/test/temba-message-editor.test.js.map +1 -0
  46. package/out-tsc/test/temba-node-editor.test.js +71 -0
  47. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  48. package/out-tsc/test/temba-select.test.js +1 -1
  49. package/out-tsc/test/temba-select.test.js.map +1 -1
  50. package/out-tsc/test/temba-textinput.test.js +16 -0
  51. package/out-tsc/test/temba-textinput.test.js.map +1 -1
  52. package/out-tsc/test/temba-webchat.test.js +4 -0
  53. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  54. package/out-tsc/test/utils.test.js +2 -8
  55. package/out-tsc/test/utils.test.js.map +1 -1
  56. package/package.json +7 -4
  57. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  58. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  59. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  60. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  61. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  62. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  63. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  64. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  65. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  66. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  67. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  68. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  69. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  70. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  71. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  72. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  73. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  74. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  75. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  76. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  77. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  78. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  79. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  80. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  81. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  82. package/screenshots/truth/editor/send_msg.png +0 -0
  83. package/screenshots/truth/editor/set_contact_language.png +0 -0
  84. package/screenshots/truth/editor/set_contact_name.png +0 -0
  85. package/screenshots/truth/editor/set_run_result.png +0 -0
  86. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  87. package/screenshots/truth/formfield/no-errors.png +0 -0
  88. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  89. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  90. package/screenshots/truth/message-editor/default.png +0 -0
  91. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  92. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  93. package/screenshots/truth/message-editor/with-completion.png +0 -0
  94. package/screenshots/truth/message-editor/with-properties.png +0 -0
  95. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  96. package/screenshots/truth/textinput/input-form.png +0 -0
  97. package/src/display/Thumbnail.ts +2 -1
  98. package/src/events.ts +5 -0
  99. package/src/flow/NodeEditor.ts +269 -23
  100. package/src/flow/actions/call_webhook.ts +28 -18
  101. package/src/flow/actions/send_msg.ts +170 -6
  102. package/src/flow/types.ts +21 -2
  103. package/src/form/ArrayEditor.ts +120 -42
  104. package/src/form/BaseListEditor.ts +22 -6
  105. package/src/form/FormField.ts +1 -1
  106. package/src/form/KeyValueEditor.ts +1 -1
  107. package/src/form/MediaPicker.ts +13 -1
  108. package/src/form/MessageEditor.ts +449 -0
  109. package/src/form/TextInput.ts +15 -7
  110. package/src/form/select/Select.ts +4 -4
  111. package/src/live/ContactChat.ts +30 -4
  112. package/static/css/temba-components.css +2 -0
  113. package/static/mr/docs/en-us/editor.json +2588 -0
  114. package/stress-test.js +138 -0
  115. package/temba-modules.ts +2 -0
  116. package/test/temba-field-config.test.ts +4 -2
  117. package/test/temba-message-editor.test.ts +300 -0
  118. package/test/temba-node-editor.test.ts +94 -0
  119. package/test/temba-select.test.ts +1 -1
  120. package/test/temba-textinput.test.ts +26 -0
  121. package/test/temba-webchat.test.ts +5 -0
  122. package/test/utils.test.ts +2 -13
  123. package/test-assets/contacts/history.json +19 -0
  124. package/test-assets/style.css +2 -0
  125. package/web-dev-mock.mjs +433 -0
  126. package/web-dev-server.config.mjs +51 -5
  127. package/web-test-runner.config.mjs +9 -4
@@ -56,7 +56,8 @@ export class Thumbnail extends RapidElement {
56
56
  background-repeat: no-repeat;
57
57
  border-radius: var(--curvature);
58
58
  max-height: calc(var(--thumb-size, 4em) * 2);
59
- width: var(--thumb-size, 4em);
59
+ max-width: calc(var(--thumb-size, 4em) * 2);
60
+ height: var(--thumb-size, 4em);
60
61
  display: flex;
61
62
  align-items: center;
62
63
  justify-content: center;
package/src/events.ts CHANGED
@@ -57,6 +57,11 @@ export interface FlowEvent extends ContactEvent {
57
57
  status: string;
58
58
  }
59
59
 
60
+ export interface RunEvent extends ContactEvent {
61
+ flow: ObjectReference;
62
+ status: string;
63
+ }
64
+
60
65
  export interface URNsChangedEvent extends ContactEvent {
61
66
  urns: string[];
62
67
  }
@@ -14,6 +14,7 @@ import {
14
14
  SelectFieldConfig,
15
15
  CheckboxFieldConfig,
16
16
  TextareaFieldConfig,
17
+ MessageEditorFieldConfig,
17
18
  LayoutItem,
18
19
  RowLayoutConfig,
19
20
  GroupLayoutConfig
@@ -31,6 +32,10 @@ export class NodeEditor extends RapidElement {
31
32
  gap: 15px;
32
33
  min-width: 400px;
33
34
  padding-bottom: 40px;
35
+
36
+ --color-bubble-bg: rgba(255, 255, 255, 0.8);
37
+ --color-bubble-border: #999;
38
+ --color-bubble-text: #777;
34
39
  }
35
40
 
36
41
  .form-field {
@@ -39,10 +44,6 @@ export class NodeEditor extends RapidElement {
39
44
  }
40
45
 
41
46
  .form-field label {
42
- font-weight: 500;
43
- margin-bottom: 6px;
44
- color: #333;
45
- font-size: 14px;
46
47
  }
47
48
 
48
49
  .field-errors {
@@ -103,10 +104,16 @@ export class NodeEditor extends RapidElement {
103
104
  border-color: var(--color-error, tomato);
104
105
  }
105
106
 
107
+ .form-group.has-bubble {
108
+ border-width: 1px;
109
+ border-color: var(--color-bubble-border, #aaa);
110
+ }
111
+
106
112
  .form-group-header {
107
113
  background: #f8f9fa;
108
- padding: 12px 15px;
114
+ padding: 8px 10px;
109
115
  border-bottom: 1px solid #e0e0e0;
116
+
110
117
  display: flex;
111
118
  align-items: center;
112
119
  justify-content: space-between;
@@ -114,6 +121,17 @@ export class NodeEditor extends RapidElement {
114
121
  user-select: none;
115
122
  }
116
123
 
124
+ .form-group.has-bubble .form-group-header {
125
+ }
126
+
127
+ .collapsed .form-group-header {
128
+ border: none;
129
+ }
130
+
131
+ .form-group-header:hover {
132
+ background: rgba(0, 0, 0, 0.05);
133
+ }
134
+
117
135
  .form-group-header.collapsible:hover {
118
136
  background: #f1f3f4;
119
137
  }
@@ -124,7 +142,7 @@ export class NodeEditor extends RapidElement {
124
142
 
125
143
  .form-group-title {
126
144
  font-weight: 500;
127
- color: #333;
145
+ color: var(--color-label, #777);
128
146
  font-size: 14px;
129
147
  display: flex;
130
148
  }
@@ -147,13 +165,13 @@ export class NodeEditor extends RapidElement {
147
165
  }
148
166
 
149
167
  .form-group-content {
150
- padding: 15px;
168
+ padding: 6px;
151
169
  display: flex;
152
170
  flex-direction: column;
153
171
  gap: 15px;
154
172
  overflow: hidden;
155
- transition: all 0.3s ease;
156
- max-height: 1000px; /* Large enough to accommodate most content */
173
+ transition: all 0.2s ease-in-out;
174
+
157
175
  opacity: 1;
158
176
  }
159
177
 
@@ -166,9 +184,14 @@ export class NodeEditor extends RapidElement {
166
184
 
167
185
  .group-toggle-icon {
168
186
  color: #666;
169
- transition: transform 0.3s ease;
187
+ transition: transform 0.3s ease, opacity 0.3s ease;
170
188
  cursor: pointer;
171
189
  transform: rotate(0deg);
190
+ opacity: 1;
191
+ }
192
+
193
+ .group-toggle-icon.faded {
194
+ opacity: 0;
172
195
  }
173
196
 
174
197
  .group-toggle-icon.expanded {
@@ -187,6 +210,58 @@ export class NodeEditor extends RapidElement {
187
210
  color: var(--color-error, tomato);
188
211
  margin-right: 8px;
189
212
  }
213
+
214
+ .group-count-bubble {
215
+ border-radius: 50%;
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ font-size: 11px;
220
+ font-weight: 600;
221
+ padding: 4px;
222
+ min-width: 12px;
223
+ min-height: 12px;
224
+ position: absolute;
225
+ top: 50%;
226
+ left: 50%;
227
+ transform: translate(-50%, -50%);
228
+ line-height: 0px;
229
+ opacity: 1;
230
+ transition: opacity 0.3s ease;
231
+ background: var(--color-bubble-bg, #fff);
232
+ border: 1px solid var(--color-bubble-border, #777);
233
+ color: var(--color-bubble-text, #000);
234
+ }
235
+
236
+ .group-count-bubble.hidden {
237
+ opacity: 0;
238
+ pointer-events: none;
239
+ }
240
+
241
+ .group-checkmark-icon {
242
+ position: absolute;
243
+ top: 50%;
244
+ left: 50%;
245
+ transform: translate(-50%, -50%);
246
+ opacity: 1;
247
+ transition: opacity 0.3s ease;
248
+ border-radius: 50%;
249
+ color: var(--color-bubble-text, #000);
250
+ background: var(--color-bubble-bg, #fff);
251
+ border: 1px solid var(--color-bubble-border, #777);
252
+ padding: 0.2em;
253
+ }
254
+
255
+ .group-checkmark-icon.hidden {
256
+ opacity: 0;
257
+ pointer-events: none;
258
+ }
259
+
260
+ .group-toggle-container {
261
+ position: relative;
262
+ display: flex;
263
+ align-items: center;
264
+ }
190
265
  `;
191
266
  }
192
267
 
@@ -214,6 +289,9 @@ export class NodeEditor extends RapidElement {
214
289
  @state()
215
290
  private groupCollapseState: { [key: string]: boolean } = {};
216
291
 
292
+ @state()
293
+ private groupHoverState: { [key: string]: boolean } = {};
294
+
217
295
  connectedCallback(): void {
218
296
  super.connectedCallback();
219
297
  this.initializeFormData();
@@ -241,6 +319,7 @@ export class NodeEditor extends RapidElement {
241
319
  this.formData = {};
242
320
  this.errors = {};
243
321
  this.groupCollapseState = {};
322
+ this.groupHoverState = {};
244
323
  }
245
324
 
246
325
  private initializeFormData(): void {
@@ -484,7 +563,7 @@ export class NodeEditor extends RapidElement {
484
563
  ) {
485
564
  errors[fieldName] = `${
486
565
  (fieldConfig as any).label || fieldName
487
- } is required`;
566
+ } is required.`;
488
567
  }
489
568
 
490
569
  // Check minLength for text fields
@@ -518,6 +597,11 @@ export class NodeEditor extends RapidElement {
518
597
  if (actionConfig?.validate) {
519
598
  // Convert form data back to action for validation
520
599
  let actionForValidation: Action;
600
+
601
+ if (actionConfig.sanitize) {
602
+ actionConfig.sanitize(this.formData);
603
+ }
604
+
521
605
  if (actionConfig.fromFormData) {
522
606
  actionForValidation = actionConfig.fromFormData(this.formData);
523
607
  } else {
@@ -834,10 +918,49 @@ export class NodeEditor extends RapidElement {
834
918
  // Check for computed values in dependent fields
835
919
  this.updateComputedFields(propertyName);
836
920
 
921
+ // Re-evaluate group collapse states that depend on form data
922
+ this.updateGroupCollapseStates();
923
+
837
924
  // Trigger re-render to handle conditional field visibility
838
925
  this.requestUpdate();
839
926
  }
840
927
 
928
+ private updateGroupCollapseStates(): void {
929
+ if (!this.action) return;
930
+
931
+ const config = ACTION_CONFIG[this.action.type];
932
+ if (!config?.layout) return;
933
+
934
+ this.updateGroupCollapseStatesRecursive(config.layout);
935
+ }
936
+
937
+ private updateGroupCollapseStatesRecursive(items: LayoutItem[]): void {
938
+ items.forEach((item) => {
939
+ if (typeof item === 'object' && item.type === 'group') {
940
+ const { label, collapsed, collapsible } = item;
941
+
942
+ // Only update if the group is collapsible and has a function-based collapsed property
943
+ if (collapsible && typeof collapsed === 'function') {
944
+ const newCollapsedState = collapsed(this.formData);
945
+
946
+ // Only update if the state has changed to avoid unnecessary re-renders
947
+ if (this.groupCollapseState[label] !== newCollapsedState) {
948
+ this.groupCollapseState = {
949
+ ...this.groupCollapseState,
950
+ [label]: newCollapsedState
951
+ };
952
+ }
953
+ }
954
+
955
+ // Recursively check nested items
956
+ this.updateGroupCollapseStatesRecursive(item.items);
957
+ } else if (typeof item === 'object' && item.type === 'row') {
958
+ // Recursively check items in rows
959
+ this.updateGroupCollapseStatesRecursive(item.items);
960
+ }
961
+ });
962
+ }
963
+
841
964
  private updateComputedFields(changedFieldName: string): void {
842
965
  if (!this.action) return;
843
966
 
@@ -980,6 +1103,7 @@ export class NodeEditor extends RapidElement {
980
1103
  nameKey="${selectConfig.nameKey || 'name'}"
981
1104
  endpoint="${selectConfig.endpoint || ''}"
982
1105
  .helpText="${config.helpText || ''}"
1106
+ flavor="${selectConfig.flavor || 'small'}"
983
1107
  @change="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
984
1108
  >
985
1109
  ${selectConfig.options?.map((option: any) => {
@@ -1028,9 +1152,11 @@ export class NodeEditor extends RapidElement {
1028
1152
  .sortable="${config.sortable}"
1029
1153
  .itemLabel="${config.itemLabel || 'Item'}"
1030
1154
  .minItems="${config.minItems || 0}"
1155
+ .maxItems="${config.maxItems || 0}"
1031
1156
  .onItemChange="${config.onItemChange}"
1032
- @change="${(e: CustomEvent) =>
1033
- this.handleNewFieldChange(fieldName, e.detail.value)}"
1157
+ .isEmptyItemFn="${config.isEmptyItem}"
1158
+ @change="${(e: Event) =>
1159
+ this.handleNewFieldChange(fieldName, (e.target as any).value)}"
1034
1160
  ></temba-array-editor>
1035
1161
  ${errors.length
1036
1162
  ? html`<div class="field-errors">${errors.join(', ')}</div>`
@@ -1057,6 +1183,30 @@ export class NodeEditor extends RapidElement {
1057
1183
  </div>`;
1058
1184
  }
1059
1185
 
1186
+ case 'message-editor': {
1187
+ const messageConfig = config as MessageEditorFieldConfig;
1188
+ return html`<temba-message-editor
1189
+ name="${fieldName}"
1190
+ label="${config.label}"
1191
+ ?required="${config.required}"
1192
+ .errors="${errors}"
1193
+ .value="${value || ''}"
1194
+ .attachments="${this.formData.attachments || []}"
1195
+ placeholder="${messageConfig.placeholder || ''}"
1196
+ .helpText="${config.helpText || ''}"
1197
+ ?autogrow="${messageConfig.autogrow}"
1198
+ ?gsm="${messageConfig.gsm}"
1199
+ ?disableCompletion="${messageConfig.disableCompletion}"
1200
+ counter="${messageConfig.counter || ''}"
1201
+ accept="${messageConfig.accept || ''}"
1202
+ endpoint="${messageConfig.endpoint || ''}"
1203
+ max-attachments="${messageConfig.maxAttachments || 3}"
1204
+ minHeight="${messageConfig.minHeight || 60}"
1205
+ @change="${(e: Event) =>
1206
+ this.handleMessageEditorChange(fieldName, e)}"
1207
+ ></temba-message-editor>`;
1208
+ }
1209
+
1060
1210
  default:
1061
1211
  return html`<div>Unsupported field type: ${(config as any).type}</div>`;
1062
1212
  }
@@ -1069,6 +1219,20 @@ export class NodeEditor extends RapidElement {
1069
1219
  };
1070
1220
  }
1071
1221
 
1222
+ private handleGroupMouseEnter(groupLabel: string): void {
1223
+ this.groupHoverState = {
1224
+ ...this.groupHoverState,
1225
+ [groupLabel]: true
1226
+ };
1227
+ }
1228
+
1229
+ private handleGroupMouseLeave(groupLabel: string): void {
1230
+ this.groupHoverState = {
1231
+ ...this.groupHoverState,
1232
+ [groupLabel]: false
1233
+ };
1234
+ }
1235
+
1072
1236
  private expandGroupsWithErrors(errors: { [key: string]: string }): void {
1073
1237
  if (!this.action) return;
1074
1238
 
@@ -1191,19 +1355,25 @@ export class NodeEditor extends RapidElement {
1191
1355
  items,
1192
1356
  collapsible = false,
1193
1357
  collapsed = false,
1194
- helpText
1358
+ helpText,
1359
+ getGroupValueCount
1195
1360
  } = groupConfig;
1196
1361
 
1197
1362
  // Initialize collapse state if not set
1198
1363
  if (collapsible && !(label in this.groupCollapseState)) {
1364
+ // Evaluate collapsed property - can be boolean or function
1365
+ const initialCollapsed =
1366
+ typeof collapsed === 'function' ? collapsed(this.formData) : collapsed;
1367
+
1199
1368
  this.groupCollapseState = {
1200
1369
  ...this.groupCollapseState,
1201
- [label]: collapsed
1370
+ [label]: initialCollapsed
1202
1371
  };
1203
1372
  }
1204
1373
 
1205
1374
  const isCollapsed = collapsible
1206
- ? this.groupCollapseState[label] ?? collapsed
1375
+ ? this.groupCollapseState[label] ??
1376
+ (typeof collapsed === 'function' ? collapsed(this.formData) : collapsed)
1207
1377
  : false;
1208
1378
 
1209
1379
  // Check if any field in this group has errors
@@ -1212,10 +1382,41 @@ export class NodeEditor extends RapidElement {
1212
1382
  (fieldName) => this.errors[fieldName]
1213
1383
  );
1214
1384
 
1385
+ // Calculate count for bubble display
1386
+ let valueCount = 0;
1387
+ let showBubble = false;
1388
+ let showCheckmark = false;
1389
+ let hasValue = false;
1390
+ const isHovered = this.groupHoverState[label] ?? false;
1391
+
1392
+ if (getGroupValueCount && collapsible) {
1393
+ try {
1394
+ const result = getGroupValueCount(this.formData);
1395
+
1396
+ if (typeof result === 'boolean') {
1397
+ // Boolean result - show checkmark when true
1398
+ showCheckmark = result && isCollapsed && !isHovered;
1399
+ hasValue = result;
1400
+ } else if (typeof result === 'number') {
1401
+ // Numeric result - show count bubble
1402
+ valueCount = result;
1403
+ showBubble = valueCount > 0 && isCollapsed && !isHovered;
1404
+ hasValue = valueCount > 0;
1405
+ }
1406
+ } catch (error) {
1407
+ console.error(
1408
+ `Error calculating group value count for ${label}:`,
1409
+ error
1410
+ );
1411
+ }
1412
+ }
1413
+
1215
1414
  return html`
1216
1415
  <div
1217
1416
  class="form-group ${collapsible ? 'collapsible' : ''} ${groupHasErrors
1218
1417
  ? 'has-errors'
1418
+ : ''} ${isCollapsed ? 'collapsed' : 'expanded'} ${hasValue
1419
+ ? 'has-bubble'
1219
1420
  : ''}"
1220
1421
  >
1221
1422
  <div
@@ -1223,6 +1424,12 @@ export class NodeEditor extends RapidElement {
1223
1424
  @click=${collapsible
1224
1425
  ? () => this.handleGroupToggle(label)
1225
1426
  : undefined}
1427
+ @mouseenter=${collapsible
1428
+ ? () => this.handleGroupMouseEnter(label)
1429
+ : undefined}
1430
+ @mouseleave=${collapsible
1431
+ ? () => this.handleGroupMouseLeave(label)
1432
+ : undefined}
1226
1433
  >
1227
1434
  <div class="form-group-info">
1228
1435
  <div class="form-group-title">${label}</div>
@@ -1238,13 +1445,28 @@ export class NodeEditor extends RapidElement {
1238
1445
  ></temba-icon>`
1239
1446
  : ''}
1240
1447
  ${collapsible && !groupHasErrors
1241
- ? html`<temba-icon
1242
- name="arrow_right"
1243
- size="1.5"
1244
- class="group-toggle-icon ${isCollapsed
1245
- ? 'collapsed'
1246
- : 'expanded'}"
1247
- ></temba-icon>`
1448
+ ? html`<div class="group-toggle-container">
1449
+ <temba-icon
1450
+ name="arrow_right"
1451
+ size="1.5"
1452
+ class="group-toggle-icon ${isCollapsed
1453
+ ? 'collapsed'
1454
+ : 'expanded'} ${showBubble || showCheckmark ? 'faded' : ''}"
1455
+ ></temba-icon>
1456
+ ${showCheckmark
1457
+ ? html`<temba-icon
1458
+ name="check"
1459
+ size="1"
1460
+ class="group-checkmark-icon"
1461
+ ></temba-icon>`
1462
+ : showBubble
1463
+ ? html`<div
1464
+ class="group-count-bubble ${!showBubble ? 'hidden' : ''}"
1465
+ >
1466
+ ${valueCount}
1467
+ </div>`
1468
+ : ''}
1469
+ </div>`
1248
1470
  : ''}
1249
1471
  </div>
1250
1472
  <div
@@ -1305,6 +1527,30 @@ export class NodeEditor extends RapidElement {
1305
1527
  this.errors = newErrors;
1306
1528
  }
1307
1529
 
1530
+ // Re-evaluate group collapse states that depend on form data
1531
+ this.updateGroupCollapseStates();
1532
+
1533
+ // Trigger re-render
1534
+ this.requestUpdate();
1535
+ }
1536
+ private handleMessageEditorChange(fieldName: string, event: Event): void {
1537
+ const target = event.target as any;
1538
+
1539
+ // Update both text and attachments from the message editor
1540
+ this.formData = {
1541
+ ...this.formData,
1542
+ [fieldName]: target.value,
1543
+ attachments: target.attachments || []
1544
+ };
1545
+
1546
+ // Clear any existing errors for both fields
1547
+ if (this.errors[fieldName]) {
1548
+ const newErrors = { ...this.errors };
1549
+ delete newErrors[fieldName];
1550
+ delete newErrors.attachments;
1551
+ this.errors = newErrors;
1552
+ }
1553
+
1308
1554
  // Trigger re-render
1309
1555
  this.requestUpdate();
1310
1556
  }
@@ -2,6 +2,19 @@ import { html } from 'lit-html';
2
2
  import { ActionConfig, COLORS } from '../types';
3
3
  import { Node, CallWebhook } from '../../store/flow-definition';
4
4
 
5
+ const defaultPost = `@(json(object(
6
+ "contact", object(
7
+ "uuid", contact.uuid,
8
+ "name", contact.name,
9
+ "urn", contact.urn
10
+ ),
11
+ "flow", object(
12
+ "uuid", run.flow.uuid,
13
+ "name", run.flow.name
14
+ ),
15
+ "results", foreach_value(results, extract_object, "value", "category")
16
+ )))`;
17
+
5
18
  export const call_webhook: ActionConfig = {
6
19
  name: 'Call Webhook',
7
20
  color: COLORS.call,
@@ -19,7 +32,7 @@ export const call_webhook: ActionConfig = {
19
32
  required: true,
20
33
  options: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],
21
34
  maxWidth: '120px',
22
- searchable: true
35
+ searchable: false
23
36
  },
24
37
  url: {
25
38
  type: 'text',
@@ -51,23 +64,10 @@ export const call_webhook: ActionConfig = {
51
64
  ? values.method[0].value || values.method[0].name
52
65
  : values.method;
53
66
 
54
- const defaultTemplate = `@(json(object(
55
- "contact", object(
56
- "uuid", contact.uuid,
57
- "name", contact.name,
58
- "urn", contact.urn
59
- ),
60
- "flow", object(
61
- "uuid", run.flow.uuid,
62
- "name", run.flow.name
63
- ),
64
- "results", foreach_value(results, extract_object, "value", "category")
65
- )))`;
66
-
67
67
  if (method === 'POST') {
68
68
  // For POST, provide the template if body is empty or was never set by user
69
69
  if (!currentValue || currentValue.trim() === '') {
70
- return defaultTemplate;
70
+ return defaultPost;
71
71
  }
72
72
  } else {
73
73
  // For non-POST methods, clear the body if it was auto-generated or empty
@@ -80,7 +80,7 @@ export const call_webhook: ActionConfig = {
80
80
  isOriginallyEmpty ||
81
81
  !currentValue ||
82
82
  currentValue.trim() === '' ||
83
- currentValue.trim() === defaultTemplate.trim()
83
+ currentValue.trim() === defaultPost.trim()
84
84
  ) {
85
85
  return '';
86
86
  }
@@ -100,7 +100,10 @@ export const call_webhook: ActionConfig = {
100
100
  items: ['headers'],
101
101
  collapsible: true,
102
102
  collapsed: true,
103
- helpText: 'Configure authentication or custom headers'
103
+ helpText: 'Configure authentication or custom headers',
104
+ getGroupValueCount: (formData: any) => {
105
+ return formData.headers?.length + 10 || 0;
106
+ }
104
107
  },
105
108
  {
106
109
  type: 'group',
@@ -108,7 +111,14 @@ export const call_webhook: ActionConfig = {
108
111
  items: ['body'],
109
112
  collapsible: true,
110
113
  collapsed: true,
111
- helpText: 'Configure the request payload'
114
+ helpText: 'Configure the request payload',
115
+ getGroupValueCount: (formData: any) => {
116
+ return !!(
117
+ formData.body &&
118
+ formData.body.trim() !== '' &&
119
+ formData.body !== defaultPost
120
+ );
121
+ }
112
122
  }
113
123
  ],
114
124
  toFormData: (action: CallWebhook) => {