@nyaruka/temba-components 0.130.4 → 0.131.0
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 +10 -13
- package/demo/sortable-rules-demo.html +155 -0
- package/dist/temba-components.js +150 -159
- 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/split_by_random.js +1 -0
- package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +332 -137
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +301 -30
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/select/Omnibox.js +4 -0
- package/out-tsc/src/form/select/Omnibox.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 +522 -8
- 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/split_by_random/editor/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.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/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/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/split_by_random.ts +1 -0
- package/src/flow/nodes/wait_for_response.ts +424 -145
- package/src/form/ArrayEditor.ts +372 -30
- package/src/form/select/Omnibox.ts +3 -0
- 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 +608 -8
- package/test/temba-field-config.test.ts +69 -0
|
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
|
};
|