@nyaruka/temba-components 0.129.11 → 0.130.1
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 +13 -4
- package/demo/components/flow/example.html +5 -1
- package/demo/data/flows/sample-flow.json +144 -80
- package/dist/temba-components.js +290 -346
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +3 -35
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +44 -11
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_groups.js +14 -2
- package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js +1 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
- package/out-tsc/src/flow/actions/add_input_labels.js +2 -1
- package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
- package/out-tsc/src/flow/actions/remove_contact_groups.js +1 -1
- package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
- package/out-tsc/src/flow/actions/send_email.js +9 -0
- package/out-tsc/src/flow/actions/send_email.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +7 -8
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js +25 -4
- package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_field.js +51 -1
- package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_language.js +70 -2
- package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_name.js +27 -2
- package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_status.js +32 -2
- package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +13 -11
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/actions/split_by_expression_example.js +4 -4
- package/out-tsc/src/flow/actions/split_by_expression_example.js.map +1 -1
- package/out-tsc/src/flow/forms/index.js +2 -0
- package/out-tsc/src/flow/forms/index.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_random.js +117 -0
- package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_ticket.js +0 -1
- package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_webhook.js +1 -3
- package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +9 -25
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +6 -64
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +35 -58
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/utils.js +3 -0
- package/out-tsc/src/utils.js.map +1 -1
- package/out-tsc/test/nodes/split_by_random.test.js +0 -6
- package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
- package/out-tsc/test/temba-field-renderer.test.js +6 -3
- package/out-tsc/test/temba-field-renderer.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +18 -0
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
- 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/editor/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/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/editor/set_contact_language.png +0 -0
- package/screenshots/truth/editor/set_contact_name.png +0 -0
- package/screenshots/truth/editor/set_run_result.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/src/flow/CanvasNode.ts +2 -39
- package/src/flow/NodeEditor.ts +54 -13
- package/src/flow/actions/add_contact_groups.ts +17 -2
- package/src/flow/actions/add_contact_urn.ts +1 -1
- package/src/flow/actions/add_input_labels.ts +2 -1
- package/src/flow/actions/remove_contact_groups.ts +1 -1
- package/src/flow/actions/send_email.ts +11 -1
- package/src/flow/actions/send_msg.ts +20 -11
- package/src/flow/actions/set_contact_channel.ts +28 -5
- package/src/flow/actions/set_contact_field.ts +56 -2
- package/src/flow/actions/set_contact_language.ts +74 -3
- package/src/flow/actions/set_contact_name.ts +31 -3
- package/src/flow/actions/set_contact_status.ts +36 -3
- package/src/flow/actions/set_run_result.ts +13 -15
- package/src/flow/actions/split_by_expression_example.ts +4 -4
- package/src/flow/forms/index.ts +1 -0
- package/src/flow/nodes/split_by_random.ts +148 -0
- package/src/flow/nodes/split_by_ticket.ts +0 -1
- package/src/flow/nodes/split_by_webhook.ts +1 -3
- package/src/flow/types.ts +2 -1
- package/src/form/ArrayEditor.ts +6 -20
- package/src/form/FieldRenderer.ts +6 -65
- package/src/form/select/Select.ts +38 -66
- package/src/store/flow-definition.d.ts +6 -1
- package/src/utils.ts +4 -0
- package/static/api/fields.json +93 -1208
- package/static/api/workspace.json +23 -0
- package/test/nodes/split_by_random.test.ts +0 -7
- package/test/temba-field-renderer.test.ts +26 -13
- package/test/utils.test.ts +20 -0
- package/web-dev-server.config.mjs +2 -0
- package/web-test-runner.config.mjs +37 -0
|
@@ -1,13 +1,67 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
|
-
import { ActionConfig, COLORS } from '../types';
|
|
2
|
+
import { ActionConfig, COLORS, ValidationResult } from '../types';
|
|
3
3
|
import { Node, SetContactField } from '../../store/flow-definition';
|
|
4
4
|
|
|
5
5
|
export const set_contact_field: ActionConfig = {
|
|
6
|
-
name: 'Update
|
|
6
|
+
name: 'Update Field',
|
|
7
7
|
color: COLORS.update,
|
|
8
8
|
render: (_node: Node, action: SetContactField) => {
|
|
9
9
|
return html`<div>
|
|
10
10
|
Set <b>${action.field.name}</b> to <b>${action.value}</b>
|
|
11
11
|
</div>`;
|
|
12
|
+
},
|
|
13
|
+
form: {
|
|
14
|
+
field: {
|
|
15
|
+
type: 'select',
|
|
16
|
+
label: 'Field',
|
|
17
|
+
required: true,
|
|
18
|
+
searchable: true,
|
|
19
|
+
clearable: false,
|
|
20
|
+
nameKey: 'name',
|
|
21
|
+
valueKey: 'key',
|
|
22
|
+
endpoint: '/api/v2/fields.json',
|
|
23
|
+
helpText: 'Select the contact field to update',
|
|
24
|
+
allowCreate: true,
|
|
25
|
+
createArbitraryOption: (input: string) => ({ key: input, name: input })
|
|
26
|
+
},
|
|
27
|
+
value: {
|
|
28
|
+
type: 'text',
|
|
29
|
+
label: 'Value',
|
|
30
|
+
placeholder: 'Enter field value...',
|
|
31
|
+
required: true,
|
|
32
|
+
evaluated: true,
|
|
33
|
+
helpText:
|
|
34
|
+
'The new value for the contact field. You can use expressions like @contact.name'
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
fromFormData: (formData: SetContactField): SetContactField => {
|
|
38
|
+
const field = formData.field[0];
|
|
39
|
+
return {
|
|
40
|
+
uuid: formData.uuid,
|
|
41
|
+
type: 'set_contact_field',
|
|
42
|
+
field: { name: field.name, key: field.key },
|
|
43
|
+
value: formData.value
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
validate: (formData: SetContactField): ValidationResult => {
|
|
47
|
+
const errors: { [key: string]: string } = {};
|
|
48
|
+
|
|
49
|
+
if (!formData.field) {
|
|
50
|
+
errors.field = 'Field is required';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!formData.value || formData.value.trim() === '') {
|
|
54
|
+
errors.value = 'Field value is required';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
valid: Object.keys(errors).length === 0,
|
|
59
|
+
errors
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
sanitize: (formData: SetContactField): void => {
|
|
63
|
+
if (formData.value && typeof formData.value === 'string') {
|
|
64
|
+
formData.value = formData.value.trim();
|
|
65
|
+
}
|
|
12
66
|
}
|
|
13
67
|
};
|
|
@@ -1,11 +1,82 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
|
-
import { ActionConfig, COLORS } from '../types';
|
|
2
|
+
import { ActionConfig, COLORS, ValidationResult } from '../types';
|
|
3
3
|
import { Node, SetContactLanguage } from '../../store/flow-definition';
|
|
4
|
+
import { getStore } from '../../store/Store';
|
|
4
5
|
|
|
5
6
|
export const set_contact_language: ActionConfig = {
|
|
6
|
-
name: 'Update
|
|
7
|
+
name: 'Update Language',
|
|
7
8
|
color: COLORS.update,
|
|
8
9
|
render: (_node: Node, action: SetContactLanguage) => {
|
|
9
|
-
|
|
10
|
+
const languageNames = new Intl.DisplayNames(['en'], {
|
|
11
|
+
type: 'language'
|
|
12
|
+
});
|
|
13
|
+
return html`<div>Set to <b>${languageNames.of(action.language)}</b></div>`;
|
|
14
|
+
},
|
|
15
|
+
form: {
|
|
16
|
+
language: {
|
|
17
|
+
type: 'select',
|
|
18
|
+
label: 'Language',
|
|
19
|
+
required: true,
|
|
20
|
+
searchable: true,
|
|
21
|
+
clearable: false,
|
|
22
|
+
valueKey: 'value',
|
|
23
|
+
nameKey: 'name',
|
|
24
|
+
helpText: 'Select the language to set for the contact',
|
|
25
|
+
getDynamicOptions: () => {
|
|
26
|
+
const store = getStore();
|
|
27
|
+
const workspace = store?.getState().workspace;
|
|
28
|
+
if (workspace?.languages && Array.isArray(workspace.languages)) {
|
|
29
|
+
const languageNames = new Intl.DisplayNames(['en'], {
|
|
30
|
+
type: 'language'
|
|
31
|
+
});
|
|
32
|
+
return workspace.languages.map((languageCode: string) => ({
|
|
33
|
+
value: languageCode,
|
|
34
|
+
name: languageNames.of(languageCode) || languageCode
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
toFormData: (action: SetContactLanguage) => {
|
|
42
|
+
// Convert the language code back to the option object format expected by the form
|
|
43
|
+
if (action.language) {
|
|
44
|
+
const languageNames = new Intl.DisplayNames(['en'], {
|
|
45
|
+
type: 'language'
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
language: [
|
|
49
|
+
{
|
|
50
|
+
value: action.language,
|
|
51
|
+
name: languageNames.of(action.language) || action.language
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
uuid: action.uuid
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
language: null,
|
|
59
|
+
uuid: action.uuid
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
fromFormData: (formData: any): SetContactLanguage => {
|
|
63
|
+
return {
|
|
64
|
+
uuid: formData.uuid,
|
|
65
|
+
type: 'set_contact_language',
|
|
66
|
+
language: formData.language[0].value
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
validate: (formData: any): ValidationResult => {
|
|
71
|
+
const errors: { [key: string]: string } = {};
|
|
72
|
+
|
|
73
|
+
if (!formData.language) {
|
|
74
|
+
errors.language = 'Language is required';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
valid: Object.keys(errors).length === 0,
|
|
79
|
+
errors
|
|
80
|
+
};
|
|
10
81
|
}
|
|
11
82
|
};
|
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
|
-
import { ActionConfig, COLORS } from '../types';
|
|
2
|
+
import { ActionConfig, COLORS, ValidationResult } from '../types';
|
|
3
3
|
import { Node, SetContactName } from '../../store/flow-definition';
|
|
4
4
|
|
|
5
5
|
export const set_contact_name: ActionConfig = {
|
|
6
|
-
name: 'Update
|
|
6
|
+
name: 'Update Name',
|
|
7
7
|
color: COLORS.update,
|
|
8
8
|
render: (_node: Node, action: SetContactName) => {
|
|
9
|
-
return html`<div>Set
|
|
9
|
+
return html`<div>Set to <b>${action.name}</b></div>`;
|
|
10
|
+
},
|
|
11
|
+
form: {
|
|
12
|
+
name: {
|
|
13
|
+
type: 'text',
|
|
14
|
+
label: 'Name',
|
|
15
|
+
placeholder: 'Enter contact name...',
|
|
16
|
+
required: true,
|
|
17
|
+
evaluated: true,
|
|
18
|
+
helpText:
|
|
19
|
+
'The new name for the contact. You can use expressions like @contact.name'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
validate: (formData: SetContactName): ValidationResult => {
|
|
23
|
+
const errors: { [key: string]: string } = {};
|
|
24
|
+
|
|
25
|
+
if (!formData.name || formData.name.trim() === '') {
|
|
26
|
+
errors.name = 'Name is required';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
valid: Object.keys(errors).length === 0,
|
|
31
|
+
errors
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
sanitize: (formData: SetContactName): void => {
|
|
35
|
+
if (formData.name && typeof formData.name === 'string') {
|
|
36
|
+
formData.name = formData.name.trim();
|
|
37
|
+
}
|
|
10
38
|
}
|
|
11
39
|
};
|
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
|
-
import { ActionConfig, COLORS } from '../types';
|
|
2
|
+
import { ActionConfig, COLORS, ValidationResult } from '../types';
|
|
3
3
|
import { Node, SetContactStatus } from '../../store/flow-definition';
|
|
4
|
+
import { titleCase } from '../../utils';
|
|
4
5
|
|
|
5
6
|
export const set_contact_status: ActionConfig = {
|
|
6
|
-
name: 'Update
|
|
7
|
+
name: 'Update Status',
|
|
7
8
|
color: COLORS.update,
|
|
8
9
|
render: (_node: Node, action: SetContactStatus) => {
|
|
9
|
-
return html`<div>Set
|
|
10
|
+
return html`<div>Set to <b>${titleCase(action.status)}</b></div>`;
|
|
11
|
+
},
|
|
12
|
+
form: {
|
|
13
|
+
status: {
|
|
14
|
+
type: 'select',
|
|
15
|
+
label: 'Status',
|
|
16
|
+
required: true,
|
|
17
|
+
searchable: false,
|
|
18
|
+
clearable: false,
|
|
19
|
+
options: [
|
|
20
|
+
{ value: 'active', name: 'Active' },
|
|
21
|
+
{ value: 'archived', name: 'Archived' },
|
|
22
|
+
{ value: 'stopped', name: 'Stopped' },
|
|
23
|
+
{ value: 'blocked', name: 'Blocked' }
|
|
24
|
+
],
|
|
25
|
+
helpText: 'Select the status to set for the contact'
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
validate: (formData: SetContactStatus): ValidationResult => {
|
|
29
|
+
const errors: { [key: string]: string } = {};
|
|
30
|
+
|
|
31
|
+
if (!formData.status) {
|
|
32
|
+
errors.status = 'Status is required';
|
|
33
|
+
} else if (
|
|
34
|
+
!['active', 'archived', 'stopped', 'blocked'].includes(formData.status)
|
|
35
|
+
) {
|
|
36
|
+
errors.status = 'Invalid status selected';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
valid: Object.keys(errors).length === 0,
|
|
41
|
+
errors
|
|
42
|
+
};
|
|
10
43
|
}
|
|
11
44
|
};
|
|
@@ -7,7 +7,9 @@ export const set_run_result: ActionConfig = {
|
|
|
7
7
|
name: 'Save Flow Result',
|
|
8
8
|
color: COLORS.save,
|
|
9
9
|
render: (_node: Node, action: SetRunResult) => {
|
|
10
|
-
return html`<div>
|
|
10
|
+
return html`<div>
|
|
11
|
+
Save <b>${action.value}</b> as <b>${action.name}</b>
|
|
12
|
+
</div>`;
|
|
11
13
|
},
|
|
12
14
|
form: {
|
|
13
15
|
name: {
|
|
@@ -28,7 +30,15 @@ export const set_run_result: ActionConfig = {
|
|
|
28
30
|
},
|
|
29
31
|
searchable: true,
|
|
30
32
|
clearable: false,
|
|
31
|
-
|
|
33
|
+
getDynamicOptions: () => {
|
|
34
|
+
const store = getStore();
|
|
35
|
+
return store
|
|
36
|
+
? store
|
|
37
|
+
.getState()
|
|
38
|
+
.getFlowResults()
|
|
39
|
+
.map((r) => ({ value: r.name, name: r.name }))
|
|
40
|
+
: [];
|
|
41
|
+
}
|
|
32
42
|
},
|
|
33
43
|
value: {
|
|
34
44
|
type: 'text',
|
|
@@ -48,21 +58,9 @@ export const set_run_result: ActionConfig = {
|
|
|
48
58
|
},
|
|
49
59
|
layout: ['name', 'value', 'category'],
|
|
50
60
|
toFormData: (action: SetRunResult) => {
|
|
51
|
-
// Get existing flow results to populate the select options
|
|
52
|
-
const store = getStore();
|
|
53
|
-
const flowResults = store ? store.getState().getFlowResults() : [];
|
|
54
|
-
|
|
55
|
-
// Update the form configuration with dynamic options
|
|
56
|
-
const config = set_run_result;
|
|
57
|
-
if (config.form && config.form.name && config.form.name.type === 'select') {
|
|
58
|
-
(config.form.name as any).options = flowResults.map(
|
|
59
|
-
(result) => result.name
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
61
|
return {
|
|
64
62
|
uuid: action.uuid,
|
|
65
|
-
name: action.name
|
|
63
|
+
name: action.name ? [{ name: action.name, value: action.name }] : [],
|
|
66
64
|
value: action.value || '',
|
|
67
65
|
category: action.category || ''
|
|
68
66
|
};
|
|
@@ -36,10 +36,10 @@ export const split_by_expression: ActionConfig = {
|
|
|
36
36
|
label: 'Operator',
|
|
37
37
|
required: true,
|
|
38
38
|
options: [
|
|
39
|
-
{ value: 'contains',
|
|
40
|
-
{ value: 'equals',
|
|
41
|
-
{ value: 'starts_with',
|
|
42
|
-
{ value: 'regex',
|
|
39
|
+
{ value: 'contains', name: 'contains' },
|
|
40
|
+
{ value: 'equals', name: 'equals' },
|
|
41
|
+
{ value: 'starts_with', name: 'starts with' },
|
|
42
|
+
{ value: 'regex', name: 'regex' }
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
operand: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// No unified contact form exports - each action has its own form configuration
|
|
@@ -1,9 +1,157 @@
|
|
|
1
1
|
import { COLORS, NodeConfig } from '../types';
|
|
2
|
+
import { Node, Category, Exit } from '../../store/flow-definition.d';
|
|
3
|
+
import { generateUUID } from '../../utils';
|
|
4
|
+
|
|
5
|
+
// Helper function to create a random router with categories
|
|
6
|
+
const createRandomRouter = (
|
|
7
|
+
userCategories: string[],
|
|
8
|
+
existingCategories: Category[] = [],
|
|
9
|
+
existingExits: Exit[] = []
|
|
10
|
+
) => {
|
|
11
|
+
const categories: Category[] = [];
|
|
12
|
+
const exits: Exit[] = [];
|
|
13
|
+
|
|
14
|
+
// Create categories and exits for user-defined buckets
|
|
15
|
+
userCategories.forEach((categoryName) => {
|
|
16
|
+
// Try to find existing category by name
|
|
17
|
+
const existingCategory = existingCategories.find(
|
|
18
|
+
(cat) => cat.name === categoryName
|
|
19
|
+
);
|
|
20
|
+
const existingExit = existingCategory
|
|
21
|
+
? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
|
|
22
|
+
: null;
|
|
23
|
+
|
|
24
|
+
const exitUuid = existingExit?.uuid || generateUUID();
|
|
25
|
+
const categoryUuid = existingCategory?.uuid || generateUUID();
|
|
26
|
+
|
|
27
|
+
categories.push({
|
|
28
|
+
uuid: categoryUuid,
|
|
29
|
+
name: categoryName,
|
|
30
|
+
exit_uuid: exitUuid
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
exits.push({
|
|
34
|
+
uuid: exitUuid,
|
|
35
|
+
destination_uuid: existingExit?.destination_uuid || null
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
router: {
|
|
41
|
+
type: 'random' as const,
|
|
42
|
+
categories: categories
|
|
43
|
+
},
|
|
44
|
+
exits: exits
|
|
45
|
+
};
|
|
46
|
+
};
|
|
2
47
|
|
|
3
48
|
export const split_by_random: NodeConfig = {
|
|
4
49
|
type: 'split_by_random',
|
|
5
50
|
name: 'Split by Random',
|
|
6
51
|
color: COLORS.split,
|
|
52
|
+
form: {
|
|
53
|
+
categories: {
|
|
54
|
+
type: 'array',
|
|
55
|
+
label: 'Buckets',
|
|
56
|
+
helpText: 'Define the buckets to randomly split contacts into',
|
|
57
|
+
required: true,
|
|
58
|
+
itemLabel: 'Bucket',
|
|
59
|
+
minItems: 2,
|
|
60
|
+
maxItems: 10,
|
|
61
|
+
isEmptyItem: (item: any) => {
|
|
62
|
+
return !item.name || item.name.trim() === '';
|
|
63
|
+
},
|
|
64
|
+
itemConfig: {
|
|
65
|
+
name: {
|
|
66
|
+
type: 'text',
|
|
67
|
+
placeholder: 'Bucket name',
|
|
68
|
+
required: true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
layout: ['categories'],
|
|
74
|
+
validate: (formData: any) => {
|
|
75
|
+
const errors: { [key: string]: string } = {};
|
|
76
|
+
|
|
77
|
+
// Check for duplicate category names
|
|
78
|
+
if (formData.categories && Array.isArray(formData.categories)) {
|
|
79
|
+
const categories = formData.categories.filter(
|
|
80
|
+
(item: any) => item?.name && item.name.trim() !== ''
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Ensure minimum buckets
|
|
84
|
+
if (categories.length < 2) {
|
|
85
|
+
errors.categories = 'At least 2 buckets are required for random split';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find all categories that have duplicates (case-insensitive)
|
|
89
|
+
const duplicateCategories = [];
|
|
90
|
+
const lowerCaseMap = new Map();
|
|
91
|
+
|
|
92
|
+
// First pass: map lowercase names to all original cases
|
|
93
|
+
categories.forEach((category) => {
|
|
94
|
+
const lowerName = category.name.trim().toLowerCase();
|
|
95
|
+
if (!lowerCaseMap.has(lowerName)) {
|
|
96
|
+
lowerCaseMap.set(lowerName, []);
|
|
97
|
+
}
|
|
98
|
+
lowerCaseMap.get(lowerName).push(category.name.trim());
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Second pass: collect all names that appear more than once
|
|
102
|
+
lowerCaseMap.forEach((originalNames) => {
|
|
103
|
+
if (originalNames.length > 1) {
|
|
104
|
+
duplicateCategories.push(...originalNames);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (duplicateCategories.length > 0) {
|
|
109
|
+
const uniqueDuplicates = [...new Set(duplicateCategories)];
|
|
110
|
+
errors.categories = `Duplicate bucket names found: ${uniqueDuplicates.join(
|
|
111
|
+
', '
|
|
112
|
+
)}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
valid: Object.keys(errors).length === 0,
|
|
118
|
+
errors
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
toFormData: (node: Node) => {
|
|
122
|
+
// Extract categories from the existing node structure
|
|
123
|
+
const categories =
|
|
124
|
+
node.router?.categories?.map((cat) => ({ name: cat.name })) || [];
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
uuid: node.uuid,
|
|
128
|
+
categories: categories
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
fromFormData: (formData: any, originalNode: Node): Node => {
|
|
132
|
+
// Get user categories
|
|
133
|
+
const userCategories = (formData.categories || [])
|
|
134
|
+
.filter((item: any) => item?.name?.trim())
|
|
135
|
+
.map((item: any) => item.name.trim());
|
|
136
|
+
|
|
137
|
+
// Create router and exits using existing data when possible
|
|
138
|
+
const existingCategories = originalNode.router?.categories || [];
|
|
139
|
+
const existingExits = originalNode.exits || [];
|
|
140
|
+
|
|
141
|
+
const { router, exits } = createRandomRouter(
|
|
142
|
+
userCategories,
|
|
143
|
+
existingCategories,
|
|
144
|
+
existingExits
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Return the complete node
|
|
148
|
+
return {
|
|
149
|
+
uuid: originalNode.uuid,
|
|
150
|
+
actions: originalNode.actions || [],
|
|
151
|
+
router: router,
|
|
152
|
+
exits: exits
|
|
153
|
+
};
|
|
154
|
+
},
|
|
7
155
|
router: {
|
|
8
156
|
type: 'random'
|
|
9
157
|
}
|
|
@@ -136,9 +136,7 @@ export const split_by_webhook: NodeConfig = {
|
|
|
136
136
|
|
|
137
137
|
return {
|
|
138
138
|
uuid: node.uuid,
|
|
139
|
-
method: callWebhookAction?.method
|
|
140
|
-
? [{ value: callWebhookAction.method, name: callWebhookAction.method }]
|
|
141
|
-
: [{ value: 'GET', name: 'GET' }],
|
|
139
|
+
method: callWebhookAction?.method || 'GET',
|
|
142
140
|
url: callWebhookAction?.url || '',
|
|
143
141
|
headers: callWebhookAction?.headers || [],
|
|
144
142
|
body: callWebhookAction?.body || ''
|
package/src/flow/types.ts
CHANGED
|
@@ -143,7 +143,7 @@ export interface TextareaFieldConfig extends BaseFieldConfig {
|
|
|
143
143
|
|
|
144
144
|
export interface SelectFieldConfig extends BaseFieldConfig {
|
|
145
145
|
type: 'select';
|
|
146
|
-
options?: string[] | { value: string;
|
|
146
|
+
options?: string[] | { value: string; name: string }[];
|
|
147
147
|
multi?: boolean;
|
|
148
148
|
clearable?: boolean;
|
|
149
149
|
searchable?: boolean;
|
|
@@ -158,6 +158,7 @@ export interface SelectFieldConfig extends BaseFieldConfig {
|
|
|
158
158
|
flavor?: 'small' | 'large';
|
|
159
159
|
createArbitraryOption?: (input: string, options: any[]) => any;
|
|
160
160
|
allowCreate?: boolean;
|
|
161
|
+
getDynamicOptions?: () => Array<{ value: string; name: string }>;
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
export interface KeyValueFieldConfig extends BaseFieldConfig {
|
package/src/form/ArrayEditor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { html, css, TemplateResult } from 'lit';
|
|
2
2
|
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
-
import { FieldConfig
|
|
3
|
+
import { FieldConfig } from '../flow/types';
|
|
4
4
|
import { BaseListEditor, ListItem } from './BaseListEditor';
|
|
5
5
|
import { FieldRenderer } from './FieldRenderer';
|
|
6
6
|
|
|
@@ -116,12 +116,13 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// For select fields, ensure we return the right type
|
|
119
|
-
if (config.type === 'select') {
|
|
119
|
+
/*if (config.type === 'select') {
|
|
120
|
+
console.log('computeFieldValue select', currentValue, config);
|
|
120
121
|
const selectConfig = config as SelectFieldConfig;
|
|
121
122
|
if (currentValue === undefined || currentValue === null) {
|
|
122
123
|
return selectConfig.multi ? [] : '';
|
|
123
124
|
}
|
|
124
|
-
}
|
|
125
|
+
}*/
|
|
125
126
|
|
|
126
127
|
return currentValue;
|
|
127
128
|
}
|
|
@@ -144,23 +145,8 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
144
145
|
|
|
145
146
|
// Handle different field types and their change events
|
|
146
147
|
if (config.type === 'select') {
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
if (target.multi || target.emails || target.tags) {
|
|
150
|
-
value = target.values || [];
|
|
151
|
-
} else {
|
|
152
|
-
// Single select: extract value from first selected option
|
|
153
|
-
const values = target.values || [];
|
|
154
|
-
value =
|
|
155
|
-
values.length > 0 && values[0]
|
|
156
|
-
? values[0].value !== undefined
|
|
157
|
-
? values[0].value
|
|
158
|
-
: values[0]
|
|
159
|
-
: '';
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
value = target.value;
|
|
163
|
-
}
|
|
148
|
+
// Use consistent temba-select value normalization
|
|
149
|
+
value = target.values;
|
|
164
150
|
} else {
|
|
165
151
|
// For other field types, use the target value directly
|
|
166
152
|
value = target.value;
|
|
@@ -207,73 +207,17 @@ export class FieldRenderer {
|
|
|
207
207
|
style
|
|
208
208
|
} = context;
|
|
209
209
|
|
|
210
|
-
//
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const valueArray = Array.isArray(value) ? value : value ? [value] : [];
|
|
215
|
-
return valueArray.map((val) => {
|
|
216
|
-
if (typeof val === 'string') {
|
|
217
|
-
// Convert string values to option objects
|
|
218
|
-
return { name: val, value: val };
|
|
219
|
-
}
|
|
220
|
-
return val;
|
|
221
|
-
});
|
|
222
|
-
} else {
|
|
223
|
-
// Single select: use the value as-is
|
|
224
|
-
return value || '';
|
|
225
|
-
}
|
|
226
|
-
})();
|
|
227
|
-
|
|
228
|
-
if (typeof normalizedValue === 'string') {
|
|
229
|
-
return html`<temba-select
|
|
230
|
-
name="${fieldName}"
|
|
231
|
-
?required="${config.required}"
|
|
232
|
-
.errors="${errors}"
|
|
233
|
-
value="${config.multi ? '' : normalizedValue}"
|
|
234
|
-
.values="${config.multi ? normalizedValue : undefined}"
|
|
235
|
-
?multi="${config.multi}"
|
|
236
|
-
?searchable="${config.searchable}"
|
|
237
|
-
?tags="${config.tags}"
|
|
238
|
-
?emails="${config.emails}"
|
|
239
|
-
?clearable="${config.clearable || false}"
|
|
240
|
-
label="${showLabel ? config.label : ''}"
|
|
241
|
-
placeholder="${config.placeholder || ''}"
|
|
242
|
-
maxItems="${config.maxItems || 0}"
|
|
243
|
-
valueKey="${config.valueKey || 'value'}"
|
|
244
|
-
nameKey="${config.nameKey || 'name'}"
|
|
245
|
-
endpoint="${config.endpoint || ''}"
|
|
246
|
-
.helpText="${config.helpText || ''}"
|
|
247
|
-
flavor="${flavor || config.flavor || 'small'}"
|
|
248
|
-
class="${extraClasses}"
|
|
249
|
-
style="${style}"
|
|
250
|
-
.getName=${config.getName}
|
|
251
|
-
.createArbitraryOption=${config.createArbitraryOption}
|
|
252
|
-
?allowCreate="${config.allowCreate || false}"
|
|
253
|
-
@change="${onChange || (() => {})}"
|
|
254
|
-
>
|
|
255
|
-
${config.options?.map((option: any) => {
|
|
256
|
-
if (typeof option === 'string') {
|
|
257
|
-
return html`<temba-option
|
|
258
|
-
name="${option}"
|
|
259
|
-
value="${option}"
|
|
260
|
-
></temba-option>`;
|
|
261
|
-
} else {
|
|
262
|
-
return html`<temba-option
|
|
263
|
-
name="${option.label || option.name}"
|
|
264
|
-
value="${option.value}"
|
|
265
|
-
></temba-option>`;
|
|
266
|
-
}
|
|
267
|
-
})}
|
|
268
|
-
</temba-select>`;
|
|
269
|
-
}
|
|
210
|
+
// Get options - use dynamic options if available, otherwise use static options
|
|
211
|
+
const optionsToRender = config.getDynamicOptions
|
|
212
|
+
? config.getDynamicOptions()
|
|
213
|
+
: config.options;
|
|
270
214
|
|
|
271
215
|
return html`<temba-select
|
|
272
216
|
name="${fieldName}"
|
|
273
217
|
label="${showLabel ? config.label : ''}"
|
|
274
218
|
?required="${config.required}"
|
|
275
219
|
.errors="${errors}"
|
|
276
|
-
.values
|
|
220
|
+
.values=${value}
|
|
277
221
|
?multi="${config.multi}"
|
|
278
222
|
?searchable="${config.searchable}"
|
|
279
223
|
?tags="${config.tags}"
|
|
@@ -293,7 +237,7 @@ export class FieldRenderer {
|
|
|
293
237
|
?allowCreate="${config.allowCreate || false}"
|
|
294
238
|
@change="${onChange || (() => {})}"
|
|
295
239
|
>
|
|
296
|
-
${
|
|
240
|
+
${optionsToRender?.map((option: any) => {
|
|
297
241
|
if (typeof option === 'string') {
|
|
298
242
|
return html`<temba-option
|
|
299
243
|
name="${option}"
|
|
@@ -445,9 +389,6 @@ export class FieldRenderer {
|
|
|
445
389
|
}
|
|
446
390
|
}
|
|
447
391
|
|
|
448
|
-
/**
|
|
449
|
-
* Context object for field rendering that provides additional options
|
|
450
|
-
*/
|
|
451
392
|
export interface FieldRenderContext {
|
|
452
393
|
/** Array of error messages for the field */
|
|
453
394
|
errors?: string[];
|