@nyaruka/temba-components 0.142.1 → 0.142.2
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 +11 -0
- package/dist/temba-components.js +825 -654
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +1 -0
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +30 -35
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +13 -8
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +18 -5
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +346 -10
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/NodeTypeSelector.js +2 -0
- package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +3 -1
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js +2 -6
- package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
- package/out-tsc/src/flow/actions/enter_flow.js +2 -2
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +2 -1
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/send_broadcast.js +2 -6
- package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
- package/out-tsc/src/flow/actions/send_email.js +2 -6
- package/out-tsc/src/flow/actions/send_email.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +52 -35
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_field.js +4 -5
- package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_language.js +3 -3
- package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +3 -3
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/actions/start_session.js +2 -2
- package/out-tsc/src/flow/actions/start_session.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm.js +4 -5
- package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_resthook.js +3 -8
- package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_subflow.js +2 -2
- package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_webhook.js +25 -33
- package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +1 -0
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +68 -0
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +17 -2
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1 -1
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/test/temba-canvas-menu.test.js +13 -9
- package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
- package/out-tsc/test/temba-flow-reflow.test.js.map +1 -1
- package/out-tsc/test/temba-node-editor.test.js +9 -10
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +3 -3
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +2 -2
- package/out-tsc/test/temba-simulator.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
- package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
- package/screenshots/truth/canvas-menu/open.png +0 -0
- package/screenshots/truth/node-type-selector/action-mode.png +0 -0
- package/screenshots/truth/node-type-selector/split-mode.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.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/render/feedback-categorization.png +0 -0
- package/src/Icons.ts +1 -0
- package/src/flow/CanvasMenu.ts +38 -39
- package/src/flow/CanvasNode.ts +16 -8
- package/src/flow/Editor.ts +33 -6
- package/src/flow/NodeEditor.ts +373 -10
- package/src/flow/NodeTypeSelector.ts +2 -0
- package/src/flow/Plumber.ts +3 -1
- package/src/flow/actions/add_contact_urn.ts +5 -6
- package/src/flow/actions/enter_flow.ts +2 -2
- package/src/flow/actions/say_msg.ts +2 -1
- package/src/flow/actions/send_broadcast.ts +2 -6
- package/src/flow/actions/send_email.ts +2 -6
- package/src/flow/actions/send_msg.ts +56 -38
- package/src/flow/actions/set_contact_channel.ts +5 -1
- package/src/flow/actions/set_contact_field.ts +10 -5
- package/src/flow/actions/set_contact_language.ts +6 -3
- package/src/flow/actions/set_contact_name.ts +5 -1
- package/src/flow/actions/set_contact_status.ts +5 -1
- package/src/flow/actions/set_run_result.ts +6 -3
- package/src/flow/actions/start_session.ts +2 -2
- package/src/flow/nodes/split_by_llm.ts +5 -5
- package/src/flow/nodes/split_by_resthook.ts +3 -8
- package/src/flow/nodes/split_by_subflow.ts +2 -2
- package/src/flow/nodes/split_by_webhook.ts +26 -34
- package/src/flow/nodes/wait_for_response.ts +1 -0
- package/src/flow/types.ts +25 -2
- package/src/flow/utils.ts +81 -1
- package/src/form/FieldRenderer.ts +32 -3
- package/src/interfaces.ts +1 -0
- package/src/simulator/Simulator.ts +1 -1
- package/test/temba-canvas-menu.test.ts +13 -9
- package/test/temba-flow-reflow.test.ts +4 -2
- package/test/temba-node-editor.test.ts +9 -10
- package/test/temba-node-type-selector.test.ts +3 -3
- package/test/temba-simulator.test.ts +2 -2
|
@@ -9,28 +9,30 @@ import {
|
|
|
9
9
|
} from '../types';
|
|
10
10
|
import { Node, SendMsg } from '../../store/flow-definition';
|
|
11
11
|
import { titleCase } from '../../utils';
|
|
12
|
+
import { renderClamped } from '../utils';
|
|
12
13
|
|
|
13
14
|
export const send_msg: ActionConfig = {
|
|
14
15
|
name: 'Send Message',
|
|
15
16
|
group: ACTION_GROUPS.send,
|
|
16
17
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
18
|
+
hideFromActions: true,
|
|
17
19
|
render: (_node: Node, action: SendMsg) => {
|
|
18
20
|
const text = action.text.replace(/\n/g, '<br>');
|
|
19
21
|
return html`
|
|
20
|
-
${unsafeHTML(text)}
|
|
22
|
+
${renderClamped(html`${unsafeHTML(text)}`, action.text)}
|
|
21
23
|
${(action.quick_replies || [])?.length > 0
|
|
22
24
|
? html`<div class="quick-replies">
|
|
23
25
|
${(action.quick_replies || []).map((reply) => {
|
|
24
26
|
return html`<div class="quick-reply">${reply}</div>`;
|
|
25
27
|
})}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
</div>`
|
|
29
|
+
: null}
|
|
30
|
+
${action.template
|
|
31
|
+
? html`<div
|
|
32
|
+
style="border: 1px solid var(--color-widget-border);padding: 0.5em;margin-top: 1em;border-radius: var(--curvature); display:flex;background: rgba(0,0,0,.03);"
|
|
33
|
+
>
|
|
34
|
+
<temba-icon name="channel_wac"></temba-icon>
|
|
35
|
+
<div style="margin-left:0.5em">${action.template.name}</div>
|
|
34
36
|
</div>`
|
|
35
37
|
: null}
|
|
36
38
|
`;
|
|
@@ -61,6 +63,10 @@ export const send_msg: ActionConfig = {
|
|
|
61
63
|
maxItems: 10,
|
|
62
64
|
evaluated: true
|
|
63
65
|
},
|
|
66
|
+
template: {
|
|
67
|
+
type: 'template-editor',
|
|
68
|
+
endpoint: '/api/internal/templates.json'
|
|
69
|
+
},
|
|
64
70
|
runtime_attachments: {
|
|
65
71
|
type: 'array',
|
|
66
72
|
itemLabel: 'Attachment',
|
|
@@ -94,34 +100,38 @@ export const send_msg: ActionConfig = {
|
|
|
94
100
|
layout: [
|
|
95
101
|
'text',
|
|
96
102
|
{
|
|
97
|
-
type: '
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
type: 'accordion',
|
|
104
|
+
sections: [
|
|
105
|
+
{
|
|
106
|
+
label: 'Quick Replies',
|
|
107
|
+
collapsed: true,
|
|
108
|
+
getValueCount: (formData: FormData) => {
|
|
109
|
+
return formData.quick_replies?.length || 0;
|
|
110
|
+
},
|
|
111
|
+
items: ['quick_replies']
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
label: 'WhatsApp Template',
|
|
115
|
+
collapsed: true,
|
|
116
|
+
getValueCount: (formData: FormData) => {
|
|
117
|
+
return !!formData.template;
|
|
118
|
+
},
|
|
119
|
+
items: ['template']
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: 'Runtime Attachments',
|
|
123
|
+
collapsed: true,
|
|
124
|
+
getValueCount: (formData: FormData) => {
|
|
125
|
+
return (
|
|
126
|
+
formData.runtime_attachments?.filter(
|
|
127
|
+
(item: any) =>
|
|
128
|
+
item && item.expression && item.expression.trim() !== ''
|
|
129
|
+
).length || 0
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
items: ['runtime_attachments']
|
|
133
|
+
}
|
|
134
|
+
]
|
|
125
135
|
}
|
|
126
136
|
],
|
|
127
137
|
toFormData: (action: SendMsg) => {
|
|
@@ -159,7 +169,9 @@ export const send_msg: ActionConfig = {
|
|
|
159
169
|
quick_replies: (action.quick_replies || []).map((reply) => ({
|
|
160
170
|
name: reply,
|
|
161
171
|
value: reply
|
|
162
|
-
}))
|
|
172
|
+
})),
|
|
173
|
+
template: action.template || null,
|
|
174
|
+
template_variables: action.template_variables || []
|
|
163
175
|
};
|
|
164
176
|
},
|
|
165
177
|
fromFormData: (data: FormData) => {
|
|
@@ -196,6 +208,12 @@ export const send_msg: ActionConfig = {
|
|
|
196
208
|
delete (result as any).quick_replies;
|
|
197
209
|
}
|
|
198
210
|
|
|
211
|
+
// Add template and template_variables if a template is selected
|
|
212
|
+
if (data.template) {
|
|
213
|
+
(result as any).template = data.template;
|
|
214
|
+
(result as any).template_variables = data.template_variables || [];
|
|
215
|
+
}
|
|
216
|
+
|
|
199
217
|
return result as SendMsg;
|
|
200
218
|
},
|
|
201
219
|
sanitize: (formData: FormData): void => {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
3
3
|
import { Node, SetContactChannel } from '../../store/flow-definition';
|
|
4
|
+
import { renderClamped } from '../utils';
|
|
4
5
|
|
|
5
6
|
export const set_contact_channel: ActionConfig = {
|
|
6
7
|
name: 'Update Channel',
|
|
7
8
|
group: ACTION_GROUPS.contacts,
|
|
8
9
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
9
10
|
render: (_node: Node, action: SetContactChannel) => {
|
|
10
|
-
return
|
|
11
|
+
return renderClamped(
|
|
12
|
+
html`Set to <strong>${action.channel.name}</strong>`,
|
|
13
|
+
`Set to ${action.channel.name}`
|
|
14
|
+
);
|
|
11
15
|
},
|
|
12
16
|
form: {
|
|
13
17
|
channel: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
3
3
|
import { Node, SetContactField } from '../../store/flow-definition';
|
|
4
|
+
import { renderClamped } from '../utils';
|
|
4
5
|
|
|
5
6
|
export const set_contact_field: ActionConfig = {
|
|
6
7
|
name: 'Update Field',
|
|
@@ -8,12 +9,16 @@ export const set_contact_field: ActionConfig = {
|
|
|
8
9
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
9
10
|
render: (_node: Node, action: SetContactField) => {
|
|
10
11
|
if (action.value) {
|
|
11
|
-
return
|
|
12
|
-
Set <strong>${action.field.name}</strong> to
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
return renderClamped(
|
|
13
|
+
html`Set <strong>${action.field.name}</strong> to
|
|
14
|
+
<strong>${action.value}</strong>`,
|
|
15
|
+
`Set ${action.field.name} to ${action.value}`
|
|
16
|
+
);
|
|
15
17
|
} else {
|
|
16
|
-
return
|
|
18
|
+
return renderClamped(
|
|
19
|
+
html`Clear <strong>${action.field.name}</strong>`,
|
|
20
|
+
`Clear ${action.field.name}`
|
|
21
|
+
);
|
|
17
22
|
}
|
|
18
23
|
},
|
|
19
24
|
form: {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { Node, SetContactLanguage } from '../../store/flow-definition';
|
|
10
10
|
import { getStore } from '../../store/Store';
|
|
11
|
+
import { renderClamped } from '../utils';
|
|
11
12
|
|
|
12
13
|
export const set_contact_language: ActionConfig = {
|
|
13
14
|
name: 'Update Language',
|
|
@@ -17,9 +18,11 @@ export const set_contact_language: ActionConfig = {
|
|
|
17
18
|
const languageNames = new Intl.DisplayNames(['en'], {
|
|
18
19
|
type: 'language'
|
|
19
20
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const name = languageNames.of(action.language) || action.language;
|
|
22
|
+
return renderClamped(
|
|
23
|
+
html`Set to <strong>${name}</strong>`,
|
|
24
|
+
`Set to ${name}`
|
|
25
|
+
);
|
|
23
26
|
},
|
|
24
27
|
form: {
|
|
25
28
|
language: {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
3
3
|
import { Node, SetContactName } from '../../store/flow-definition';
|
|
4
|
+
import { renderClamped } from '../utils';
|
|
4
5
|
|
|
5
6
|
export const set_contact_name: ActionConfig = {
|
|
6
7
|
name: 'Update Name',
|
|
7
8
|
group: ACTION_GROUPS.contacts,
|
|
8
9
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
9
10
|
render: (_node: Node, action: SetContactName) => {
|
|
10
|
-
return
|
|
11
|
+
return renderClamped(
|
|
12
|
+
html`Set to <strong>${action.name}</strong>`,
|
|
13
|
+
`Set to ${action.name}`
|
|
14
|
+
);
|
|
11
15
|
},
|
|
12
16
|
form: {
|
|
13
17
|
name: {
|
|
@@ -2,13 +2,17 @@ import { html } from 'lit-html';
|
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
3
3
|
import { Node, SetContactStatus } from '../../store/flow-definition';
|
|
4
4
|
import { titleCase } from '../../utils';
|
|
5
|
+
import { renderClamped } from '../utils';
|
|
5
6
|
|
|
6
7
|
export const set_contact_status: ActionConfig = {
|
|
7
8
|
name: 'Update Status',
|
|
8
9
|
group: ACTION_GROUPS.contacts,
|
|
9
10
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
10
11
|
render: (_node: Node, action: SetContactStatus) => {
|
|
11
|
-
return
|
|
12
|
+
return renderClamped(
|
|
13
|
+
html`Set to <strong>${titleCase(action.status)}</strong>`,
|
|
14
|
+
`Set to ${titleCase(action.status)}`
|
|
15
|
+
);
|
|
12
16
|
},
|
|
13
17
|
toFormData: (action: SetContactStatus) => {
|
|
14
18
|
return {
|
|
@@ -2,15 +2,18 @@ import { html } from 'lit-html';
|
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
3
3
|
import { Node, SetRunResult } from '../../store/flow-definition';
|
|
4
4
|
import { getStore } from '../../store/Store';
|
|
5
|
+
import { renderClamped } from '../utils';
|
|
5
6
|
|
|
6
7
|
export const set_run_result: ActionConfig = {
|
|
7
8
|
name: 'Save Flow Result',
|
|
8
9
|
group: ACTION_GROUPS.save,
|
|
9
10
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
10
11
|
render: (_node: Node, action: SetRunResult) => {
|
|
11
|
-
return
|
|
12
|
-
Save <strong>${action.value}</strong> as
|
|
13
|
-
|
|
12
|
+
return renderClamped(
|
|
13
|
+
html`Save <strong>${action.value}</strong> as
|
|
14
|
+
<strong>${action.name}</strong>`,
|
|
15
|
+
`Save ${action.value} as ${action.name}`
|
|
16
|
+
);
|
|
14
17
|
},
|
|
15
18
|
form: {
|
|
16
19
|
name: {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
FlowTypes
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { Node, StartSession } from '../../store/flow-definition';
|
|
10
|
-
import { renderNamedObjects } from '../utils';
|
|
10
|
+
import { renderNamedObjects, renderFlowLinks } from '../utils';
|
|
11
11
|
|
|
12
12
|
export const start_session: ActionConfig = {
|
|
13
13
|
name: 'Start Flow',
|
|
@@ -43,7 +43,7 @@ export const start_session: ActionConfig = {
|
|
|
43
43
|
${recipientsDisplay}
|
|
44
44
|
</div>
|
|
45
45
|
<div style="padding: 0px 10px;">
|
|
46
|
-
${
|
|
46
|
+
${renderFlowLinks([action.flow], 'flow')}
|
|
47
47
|
</div>
|
|
48
48
|
</div>
|
|
49
49
|
`;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
categoriesToLocalizationFormData,
|
|
13
13
|
localizationFormDataToCategories
|
|
14
14
|
} from './shared';
|
|
15
|
+
import { renderClamped } from '../utils';
|
|
15
16
|
|
|
16
17
|
export const split_by_llm: NodeConfig = {
|
|
17
18
|
type: 'split_by_llm',
|
|
@@ -24,12 +25,11 @@ export const split_by_llm: NodeConfig = {
|
|
|
24
25
|
const callLlmAction = node.actions?.find(
|
|
25
26
|
(action) => action.type === 'call_llm'
|
|
26
27
|
) as CallLLM;
|
|
28
|
+
const instructions =
|
|
29
|
+
callLlmAction?.instructions || 'Configure AI instructions';
|
|
27
30
|
return html`
|
|
28
|
-
<div
|
|
29
|
-
|
|
30
|
-
style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; max-width: 180px; max-height: 6.2em; margin-bottom:10px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical;"
|
|
31
|
-
>
|
|
32
|
-
${callLlmAction?.instructions || 'Configure AI instructions'}
|
|
31
|
+
<div class="body" style="margin-bottom:10px;">
|
|
32
|
+
${renderClamped(instructions, instructions)}
|
|
33
33
|
</div>
|
|
34
34
|
`;
|
|
35
35
|
},
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
categoriesToLocalizationFormData,
|
|
8
8
|
localizationFormDataToCategories
|
|
9
9
|
} from './shared';
|
|
10
|
+
import { renderClamped } from '../utils';
|
|
10
11
|
|
|
11
12
|
export const split_by_resthook: NodeConfig = {
|
|
12
13
|
type: 'split_by_resthook',
|
|
@@ -47,14 +48,8 @@ export const split_by_resthook: NodeConfig = {
|
|
|
47
48
|
const callResthookAction = node.actions?.find(
|
|
48
49
|
(action) => action.type === 'call_resthook'
|
|
49
50
|
) as CallResthook;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class="body"
|
|
53
|
-
style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
|
|
54
|
-
>
|
|
55
|
-
${callResthookAction?.resthook || 'Configure resthook'}
|
|
56
|
-
</div>
|
|
57
|
-
`;
|
|
51
|
+
const resthook = callResthookAction?.resthook || 'Configure resthook';
|
|
52
|
+
return html` <div class="body">${renderClamped(resthook, resthook)}</div> `;
|
|
58
53
|
},
|
|
59
54
|
toFormData: (node: Node) => {
|
|
60
55
|
// extract data from the existing node structure
|
|
@@ -2,7 +2,7 @@ import { ACTION_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
|
|
|
2
2
|
import { Node } from '../../store/flow-definition';
|
|
3
3
|
import { generateUUID } from '../../utils';
|
|
4
4
|
import { html } from 'lit';
|
|
5
|
-
import {
|
|
5
|
+
import { renderFlowLinks } from '../utils';
|
|
6
6
|
import {
|
|
7
7
|
categoriesToLocalizationFormData,
|
|
8
8
|
localizationFormDataToCategories
|
|
@@ -33,7 +33,7 @@ export const split_by_subflow: NodeConfig = {
|
|
|
33
33
|
) as any;
|
|
34
34
|
return html`
|
|
35
35
|
<div class="body">
|
|
36
|
-
${
|
|
36
|
+
${enterFlowAction?.flow ? renderFlowLinks([enterFlowAction.flow], 'flow') : null}
|
|
37
37
|
</div>
|
|
38
38
|
`;
|
|
39
39
|
},
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
categoriesToLocalizationFormData,
|
|
7
7
|
localizationFormDataToCategories
|
|
8
8
|
} from './shared';
|
|
9
|
+
import { renderClamped } from '../utils';
|
|
9
10
|
|
|
10
11
|
const defaultPost = `@(json(object(
|
|
11
12
|
"contact", object(
|
|
@@ -91,48 +92,39 @@ export const split_by_webhook: NodeConfig = {
|
|
|
91
92
|
}
|
|
92
93
|
},
|
|
93
94
|
layout: [
|
|
94
|
-
// Row with method and URL side by side
|
|
95
95
|
{ type: 'row', items: ['method', 'url'] },
|
|
96
|
-
// Advanced group with nested layouts
|
|
97
96
|
{
|
|
98
|
-
type: '
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
97
|
+
type: 'accordion',
|
|
98
|
+
sections: [
|
|
99
|
+
{
|
|
100
|
+
label: 'Headers',
|
|
101
|
+
collapsed: true,
|
|
102
|
+
getValueCount: (formData: FormData) => {
|
|
103
|
+
return formData.headers?.length || 0;
|
|
104
|
+
},
|
|
105
|
+
items: ['headers']
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
label: 'Body',
|
|
109
|
+
collapsed: true,
|
|
110
|
+
getValueCount: (formData: FormData) => {
|
|
111
|
+
return !!(
|
|
112
|
+
formData.body &&
|
|
113
|
+
formData.body.trim() !== '' &&
|
|
114
|
+
formData.body !== defaultPost
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
items: ['body']
|
|
118
|
+
}
|
|
119
|
+
]
|
|
122
120
|
}
|
|
123
121
|
],
|
|
124
122
|
render: (node: Node) => {
|
|
125
123
|
const callWebhookAction = node.actions?.find(
|
|
126
124
|
(action) => action.type === 'call_webhook'
|
|
127
125
|
) as CallWebhook;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class="body"
|
|
131
|
-
style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
|
|
132
|
-
>
|
|
133
|
-
${callWebhookAction?.url || 'Configure webhook'}
|
|
134
|
-
</div>
|
|
135
|
-
`;
|
|
126
|
+
const url = callWebhookAction?.url || 'Configure webhook';
|
|
127
|
+
return html` <div class="body">${renderClamped(url, url)}</div> `;
|
|
136
128
|
},
|
|
137
129
|
toFormData: (node: Node) => {
|
|
138
130
|
// Extract data from the existing node structure
|
package/src/flow/types.ts
CHANGED
|
@@ -104,6 +104,7 @@ export interface NodeConfig extends FormConfig {
|
|
|
104
104
|
dialogSize?: 'small' | 'medium' | 'large' | 'xlarge';
|
|
105
105
|
action?: ActionConfig;
|
|
106
106
|
showAsAction?: boolean; // if true, show in action dialog instead of splits (default: false - nodes show in splits)
|
|
107
|
+
hideFromSplits?: boolean; // if true, don't show in split dialog (e.g. promoted to context menu)
|
|
107
108
|
flowTypes?: FlowType[]; // which flow types this node is available for (defaults to all if not specified)
|
|
108
109
|
features?: Feature[]; // which features are required for this node (all must be present)
|
|
109
110
|
router?: {
|
|
@@ -261,6 +262,11 @@ export interface MediaFieldConfig extends BaseFieldConfig {
|
|
|
261
262
|
endpoint?: string; // upload endpoint, defaults to DEFAULT_MEDIA_ENDPOINT
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
export interface TemplateEditorFieldConfig extends BaseFieldConfig {
|
|
266
|
+
type: 'template-editor';
|
|
267
|
+
endpoint?: string; // endpoint for fetching templates
|
|
268
|
+
}
|
|
269
|
+
|
|
264
270
|
export type FieldConfig =
|
|
265
271
|
| TextFieldConfig
|
|
266
272
|
| TextareaFieldConfig
|
|
@@ -269,7 +275,8 @@ export type FieldConfig =
|
|
|
269
275
|
| ArrayFieldConfig
|
|
270
276
|
| CheckboxFieldConfig
|
|
271
277
|
| MessageEditorFieldConfig
|
|
272
|
-
| MediaFieldConfig
|
|
278
|
+
| MediaFieldConfig
|
|
279
|
+
| TemplateEditorFieldConfig;
|
|
273
280
|
|
|
274
281
|
// Layout configurations for better form organization
|
|
275
282
|
// Recursive layout system - any layout item can contain other layout items
|
|
@@ -297,7 +304,9 @@ export interface GroupLayoutConfig {
|
|
|
297
304
|
collapsed?: boolean | ((formData: FormData) => boolean); // initial state if collapsible - can be a function
|
|
298
305
|
helpText?: string;
|
|
299
306
|
contentPadding?: string; // CSS padding for group content area
|
|
300
|
-
|
|
307
|
+
bordered?: boolean; // whether to show border around the group (default: true)
|
|
308
|
+
reveal?: boolean; // one-way expand: once clicked, header disappears and items show directly
|
|
309
|
+
getGroupValueCount?: (formData: FormData) => number | boolean; // optional function to get count for bubble display
|
|
301
310
|
}
|
|
302
311
|
|
|
303
312
|
export interface SpacerLayoutConfig {
|
|
@@ -309,10 +318,24 @@ export interface TextLayoutConfig {
|
|
|
309
318
|
text: string;
|
|
310
319
|
}
|
|
311
320
|
|
|
321
|
+
export interface AccordionSection {
|
|
322
|
+
label: string;
|
|
323
|
+
items: LayoutItem[];
|
|
324
|
+
collapsed?: boolean | ((formData: FormData) => boolean);
|
|
325
|
+
getValueCount?: (formData: FormData) => number | boolean;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface AccordionLayoutConfig {
|
|
329
|
+
type: 'accordion';
|
|
330
|
+
sections: AccordionSection[];
|
|
331
|
+
multi?: boolean; // allow multiple sections open at once (default: false)
|
|
332
|
+
}
|
|
333
|
+
|
|
312
334
|
export type LayoutItem =
|
|
313
335
|
| FieldItemConfig
|
|
314
336
|
| RowLayoutConfig
|
|
315
337
|
| GroupLayoutConfig
|
|
338
|
+
| AccordionLayoutConfig
|
|
316
339
|
| SpacerLayoutConfig
|
|
317
340
|
| TextLayoutConfig
|
|
318
341
|
| string; // string is shorthand for field
|
package/src/flow/utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { html } from 'lit-html';
|
|
1
|
+
import { html, TemplateResult } from 'lit-html';
|
|
2
2
|
import { NamedObject, FlowPosition } from '../store/flow-definition';
|
|
3
3
|
import { FlowIssue } from '../store/AppState';
|
|
4
|
+
import { CustomEventType } from '../interfaces';
|
|
4
5
|
|
|
5
6
|
const IS_MAC =
|
|
6
7
|
typeof navigator !== 'undefined' &&
|
|
@@ -32,6 +33,23 @@ export function snapToGrid(value: number): number {
|
|
|
32
33
|
return Math.max(snapped, 0);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Renders content clamped to a maximum number of lines with ellipsis.
|
|
38
|
+
* Hovering shows the full text in a tooltip.
|
|
39
|
+
*/
|
|
40
|
+
export const renderClamped = (
|
|
41
|
+
content: TemplateResult | string,
|
|
42
|
+
titleText: string,
|
|
43
|
+
maxLines: number = 3
|
|
44
|
+
) => {
|
|
45
|
+
return html`<div
|
|
46
|
+
style="display: -webkit-box; -webkit-line-clamp: ${maxLines}; -webkit-box-orient: vertical; overflow: hidden; word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
|
|
47
|
+
title="${titleText}"
|
|
48
|
+
>
|
|
49
|
+
${content}
|
|
50
|
+
</div>`;
|
|
51
|
+
};
|
|
52
|
+
|
|
35
53
|
/**
|
|
36
54
|
* Renders a single line item with optional icon
|
|
37
55
|
*/
|
|
@@ -42,6 +60,7 @@ export const renderLineItem = (name: string, icon?: string) => {
|
|
|
42
60
|
: null}
|
|
43
61
|
<div
|
|
44
62
|
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 250px;"
|
|
63
|
+
title="${name}"
|
|
45
64
|
>
|
|
46
65
|
${name}
|
|
47
66
|
</div>
|
|
@@ -91,6 +110,67 @@ export const renderStringList = (items: string[], icon?: string) => {
|
|
|
91
110
|
return itemElements;
|
|
92
111
|
};
|
|
93
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Renders a single flow as a clickable link that fires a temba-flow-clicked event
|
|
115
|
+
*/
|
|
116
|
+
const renderFlowLink = (flow: NamedObject, icon?: string) => {
|
|
117
|
+
const handleClick = (e: MouseEvent) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
const target = e.currentTarget as HTMLElement;
|
|
121
|
+
const editor = target.closest('temba-flow-editor') as any;
|
|
122
|
+
if (editor) {
|
|
123
|
+
editor.fireCustomEvent(CustomEventType.FlowClicked, {
|
|
124
|
+
uuid: flow.uuid,
|
|
125
|
+
name: flow.name,
|
|
126
|
+
metaKey: e.metaKey,
|
|
127
|
+
ctrlKey: e.ctrlKey
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return html`<div class="linked-name" style="display:flex;items-align:center;">
|
|
133
|
+
${icon
|
|
134
|
+
? html`<temba-icon name=${icon} style="margin-right:0.5em"></temba-icon>`
|
|
135
|
+
: null}
|
|
136
|
+
<div
|
|
137
|
+
@click=${handleClick}
|
|
138
|
+
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 250px; text-decoration: underline; cursor: pointer;"
|
|
139
|
+
>
|
|
140
|
+
${flow.name}
|
|
141
|
+
</div>
|
|
142
|
+
</div>`;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Renders a list of flows as clickable links, showing up to 3 items
|
|
147
|
+
* with a "+X more" indicator if there are more items
|
|
148
|
+
*/
|
|
149
|
+
export const renderFlowLinks = (flows: NamedObject[], icon?: string) => {
|
|
150
|
+
const itemElements = [];
|
|
151
|
+
const maxDisplay = 3;
|
|
152
|
+
|
|
153
|
+
const displayCount =
|
|
154
|
+
flows.length === 4 ? 4 : Math.min(maxDisplay, flows.length);
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < displayCount; i++) {
|
|
157
|
+
itemElements.push(renderFlowLink(flows[i], icon));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (flows.length > maxDisplay && flows.length !== 4) {
|
|
161
|
+
const remainingCount = flows.length - maxDisplay;
|
|
162
|
+
itemElements.push(html`<div
|
|
163
|
+
style="display:flex;items-align:center;margin-top:0.2em;"
|
|
164
|
+
>
|
|
165
|
+
${icon
|
|
166
|
+
? html`<div style="margin-right:0.4em; width: 1em;"></div>`
|
|
167
|
+
: null}
|
|
168
|
+
<div style="font-size:0.8em">+${remainingCount} more</div>
|
|
169
|
+
</div>`);
|
|
170
|
+
}
|
|
171
|
+
return itemElements;
|
|
172
|
+
};
|
|
173
|
+
|
|
94
174
|
export interface Scheme {
|
|
95
175
|
scheme: string;
|
|
96
176
|
name: string;
|