@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
@@ -3,6 +3,8 @@ import { customElement, property } from 'lit/decorators.js';
3
3
  import { FieldConfig } from '../flow/types';
4
4
  import { BaseListEditor, ListItem } from './BaseListEditor';
5
5
  import { FieldRenderer } from './FieldRenderer';
6
+ import '../list/SortableList';
7
+ import { Icon } from '../Icons';
6
8
 
7
9
  @customElement('temba-array-editor')
8
10
  export class TembaArrayEditor extends BaseListEditor<ListItem> {
@@ -23,6 +25,9 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
23
25
  @property({ type: Function })
24
26
  isEmptyItemFn?: (item: any) => boolean;
25
27
 
28
+ @property({ type: Boolean })
29
+ sortable = false;
30
+
26
31
  @property({ type: Boolean })
27
32
  maintainEmptyItem = true; // Enable by default for better UX
28
33
 
@@ -103,6 +108,77 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
103
108
  this.updateValue(updatedItems);
104
109
  }
105
110
 
111
+ private handleOrderChanged(event: CustomEvent): void {
112
+ const detail = event.detail;
113
+
114
+ // Handle swap-based logic from SortableList
115
+ if (detail.swap && Array.isArray(detail.swap) && detail.swap.length === 2) {
116
+ const [fromIdx, toIdx] = detail.swap;
117
+
118
+ // Only reorder if the indexes are different and valid
119
+ if (
120
+ fromIdx !== toIdx &&
121
+ fromIdx >= 0 &&
122
+ toIdx >= 0 &&
123
+ fromIdx < this._items.length &&
124
+ toIdx < this._items.length
125
+ ) {
126
+ const updatedItems = [...this._items];
127
+ // Move the item using splice operations
128
+ const movedItem = updatedItems.splice(fromIdx, 1)[0];
129
+ updatedItems.splice(toIdx, 0, movedItem);
130
+ this.updateValue(updatedItems);
131
+ }
132
+ }
133
+ }
134
+
135
+ renderWidget(): TemplateResult {
136
+ const items = this.displayItems;
137
+
138
+ const itemsContent = items.map((item, index) => {
139
+ const renderedItem = this.renderItem(item, index);
140
+
141
+ if (this.sortable && !this.isEmptyItem(item)) {
142
+ // Wrap non-empty items with sortable class and unique ID for drag-and-drop
143
+ return html`
144
+ <div class="sortable" id="array-item-${index}">${renderedItem}</div>
145
+ `;
146
+ } else {
147
+ // Non-sortable items or empty items don't get the sortable wrapper
148
+ return renderedItem;
149
+ }
150
+ });
151
+
152
+ if (this.sortable) {
153
+ return html`
154
+ <div class=${this.getContainerClass()}>
155
+ <temba-sortable-list
156
+ dragHandle="drag-handle"
157
+ gap="0.4em"
158
+ @temba-order-changed=${this.handleOrderChanged}
159
+ style="display: grid; grid-template-columns: 1fr; gap: 8px;"
160
+ >
161
+ ${itemsContent}
162
+ </temba-sortable-list>
163
+ ${this.shouldShowAddButton() ? this.renderAddButton() : ''}
164
+ </div>
165
+ `;
166
+ } else {
167
+ // Non-sortable rendering (original behavior)
168
+ return html`
169
+ <div class=${this.getContainerClass()}>
170
+ <div
171
+ class="list-items"
172
+ style="display: grid; grid-template-columns: 1fr; gap: 8px;"
173
+ >
174
+ ${itemsContent}
175
+ </div>
176
+ ${this.shouldShowAddButton() ? this.renderAddButton() : ''}
177
+ </div>
178
+ `;
179
+ }
180
+ }
181
+
106
182
  private computeFieldValue(
107
183
  itemIndex: number,
108
184
  fieldName: string,
@@ -210,11 +286,9 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
210
286
 
211
287
  fieldElements.push(html`
212
288
  <div
213
- class="field ${config.width ||
214
- config.maxWidth ||
215
- config.type === 'select'
216
- ? 'field-fixed'
217
- : 'field-flex'}"
289
+ style="${config.width || config.maxWidth || config.type === 'select'
290
+ ? 'flex:none'
291
+ : 'flex:1'}"
218
292
  >
219
293
  ${this.renderArrayField(index, fieldName, config)}
220
294
  </div>
@@ -234,11 +308,31 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
234
308
 
235
309
  return html`
236
310
  <div class="array-item">
237
- <div class="item-fields">
311
+ <div
312
+ class="item-fields ${canRemove ? '' : 'removable'}"
313
+ style="display: flex; gap: 12px; align-items: center"
314
+ >
315
+ ${this.sortable
316
+ ? html`<temba-icon
317
+ name=${Icon.sort}
318
+ style="margin-right: -6px;"
319
+ class="drag-handle"
320
+ ></temba-icon>`
321
+ : null}
238
322
  ${fieldElements}
239
323
  <button
240
324
  @click=${canRemove ? () => this.removeItem(index) : undefined}
241
- class="remove-btn ${canRemove ? '' : 'invisible'}"
325
+ class="remove-btn"
326
+ style="
327
+ padding: 4px;
328
+ border: 1px solid #ccc;
329
+ border-radius: 4px;
330
+ background: white;
331
+ cursor: pointer;
332
+ background: #fefefe;
333
+ color: #999;
334
+ font-size: 14px;
335
+ "
242
336
  ?disabled=${!canRemove}
243
337
  >
244
338
  <temba-icon name="x"></temba-icon>
@@ -256,12 +350,6 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
256
350
  return css`
257
351
  ${super.styles}
258
352
 
259
- .array-editor {
260
- }
261
-
262
- .array-item {
263
- }
264
-
265
353
  .item-header {
266
354
  display: flex;
267
355
  justify-content: space-between;
@@ -273,24 +361,10 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
273
361
  color: #333;
274
362
  }
275
363
 
276
- .item-fields {
277
- display: flex;
278
- gap: 12px;
279
- align-items: center;
280
- }
281
-
282
364
  .field {
283
365
  /* Base field styles */
284
366
  }
285
367
 
286
- .field-flex {
287
- flex: 1; /* Grow to fill remaining space */
288
- }
289
-
290
- .field-fixed {
291
- flex: none; /* Don't grow, use content/maxWidth size */
292
- }
293
-
294
368
  .spacer {
295
369
  /* Empty spacer to maintain layout alignment */
296
370
  }
@@ -315,10 +389,20 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
315
389
  color: #999;
316
390
  }
317
391
 
318
- .remove-btn.invisible {
392
+ .removable .remove-btn {
319
393
  visibility: hidden;
320
394
  cursor: default;
321
395
  }
396
+
397
+ .removable .drag-handle {
398
+ visibility: hidden;
399
+ cursor: default;
400
+ }
401
+
402
+ .drag-handle {
403
+ cursor: grab;
404
+ color: #ccc;
405
+ }
322
406
  `;
323
407
  }
324
408
  }
@@ -247,13 +247,15 @@ export class Select<T extends SelectOption> extends FieldElement {
247
247
 
248
248
  .input-wrapper:focus-within {
249
249
  min-width: 1px;
250
+ display: flex;
250
251
  }
251
252
 
252
253
  .input-wrapper {
253
254
  min-width: 1px;
254
255
  margin-left: 6px;
255
256
  margin-right: -6px;
256
- display: flex;
257
+ display: none;
258
+ pointer-events: none;
257
259
  }
258
260
 
259
261
  .multi .input-wrapper {
@@ -312,6 +314,7 @@ export class Select<T extends SelectOption> extends FieldElement {
312
314
  display: none;
313
315
  line-height: var(--temba-select-selected-line-height);
314
316
  margin-left: 6px;
317
+ pointer-events: none;
315
318
  }
316
319
 
317
320
  .empty .placeholder {
@@ -371,10 +374,6 @@ export class Select<T extends SelectOption> extends FieldElement {
371
374
  pointer-events: none;
372
375
  padding: 0px;
373
376
  }
374
-
375
- .ghost .remove-item {
376
- display: none !important;
377
- }
378
377
  `;
379
378
  }
380
379
 
@@ -1566,6 +1565,7 @@ export class Select<T extends SelectOption> extends FieldElement {
1566
1565
  public removeValue(valueToRemove: any) {
1567
1566
  const oldValues = [...this.values];
1568
1567
  const idx = this.values.indexOf(valueToRemove);
1568
+
1569
1569
  if (idx > -1) {
1570
1570
  this.values.splice(idx, 1);
1571
1571
 
@@ -1683,10 +1683,13 @@ export class Select<T extends SelectOption> extends FieldElement {
1683
1683
 
1684
1684
  const input = this.searchable
1685
1685
  ? html`
1686
- <div class="input-wrapper">
1686
+ <div
1687
+ class="input-wrapper"
1688
+ style="${this.focused ? 'display:flex;' : ''}"
1689
+ >
1687
1690
  <input
1688
1691
  class="searchbox"
1689
- style=${this.inputStyle ? styleMap(this.inputStyle) : ''}
1692
+ style="${this.inputStyle ? styleMap(this.inputStyle) : ''};"
1690
1693
  @input=${this.handleInput}
1691
1694
  @keydown=${this.handleKeyDown}
1692
1695
  @click=${this.handleClick}
@@ -1704,15 +1707,11 @@ export class Select<T extends SelectOption> extends FieldElement {
1704
1707
  <temba-sortable-list
1705
1708
  horizontal
1706
1709
  @temba-order-changed=${this.handleOrderChanged}
1707
- .prepareGhost=${(item: any) => {
1708
- item.style.transform = 'scale(1)';
1709
- item.querySelector('.remove-item').style.display = 'none';
1710
- }}
1711
1710
  >
1712
1711
  ${this.values.map(
1713
1712
  (selected: any, index: number) => html`
1714
1713
  <div
1715
- class="selected-item sortable ${index === this.selectedIndex
1714
+ class="sortable selected-item ${index === this.selectedIndex
1716
1715
  ? 'focused'
1717
1716
  : ''} ${this.draggingId === `selected-${index}`
1718
1717
  ? 'dragging'
@@ -1770,7 +1769,7 @@ export class Select<T extends SelectOption> extends FieldElement {
1770
1769
  </div>
1771
1770
  `
1772
1771
  )}
1773
- ${this.searchable && this.focused ? input : null}
1772
+ ${input}
1774
1773
  </temba-sortable-list>
1775
1774
  `
1776
1775
  : html`${this.values.map(
@@ -1794,15 +1793,9 @@ export class Select<T extends SelectOption> extends FieldElement {
1794
1793
  ? html`
1795
1794
  <div
1796
1795
  class="remove-item"
1797
- style="
1798
- cursor: pointer;
1799
- display: inline-block;
1800
- padding: 3px 6px;
1801
- border-right: 1px solid rgba(100,100,100,0.2);
1802
- margin: 0;
1803
- background: rgba(100,100,100,0.05);
1804
- margin-top:1px;
1805
- "
1796
+ style="cursor: pointer; display: inline-block; padding: 3px 6px;
1797
+ border-right: 1px solid rgba(100,100,100,0.2);
1798
+ margin: 0; background: rgba(100,100,100,0.05); margin-top:1px;"
1806
1799
  @click=${(evt: MouseEvent) => {
1807
1800
  evt.preventDefault();
1808
1801
  evt.stopPropagation();
@@ -1834,9 +1827,9 @@ export class Select<T extends SelectOption> extends FieldElement {
1834
1827
  class="select-container ${classes}"
1835
1828
  @click=${this.handleContainerClick}
1836
1829
  >
1837
- <div class="left-side" >
1830
+ <div class="left-side">
1838
1831
  <slot name="prefix"></slot>
1839
- <div class="selected" >
1832
+ <div class="selected">
1840
1833
  ${
1841
1834
  this.resolving
1842
1835
  ? html`<temba-loading
@@ -1851,7 +1844,13 @@ export class Select<T extends SelectOption> extends FieldElement {
1851
1844
 
1852
1845
  ${clear}
1853
1846
 
1854
- <slot name="right"></slot>
1847
+ <slot name="right">${
1848
+ this.fetching
1849
+ ? html`<temba-loading
1850
+ style="position:absolute;background:rgba(255,255,255,0.7);padding:2px;border-radius:var(--curvature);right:25px"
1851
+ ></temba-loading>`
1852
+ : null
1853
+ }</slot>
1855
1854
  ${
1856
1855
  !this.tags && !this.emails
1857
1856
  ? html`<div