@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/temba-components.js +825 -654
  3. package/dist/temba-components.js.map +1 -1
  4. package/out-tsc/src/Icons.js +1 -0
  5. package/out-tsc/src/Icons.js.map +1 -1
  6. package/out-tsc/src/flow/CanvasMenu.js +30 -35
  7. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +13 -8
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +18 -5
  11. package/out-tsc/src/flow/Editor.js.map +1 -1
  12. package/out-tsc/src/flow/NodeEditor.js +346 -10
  13. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeTypeSelector.js +2 -0
  15. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
  16. package/out-tsc/src/flow/Plumber.js +3 -1
  17. package/out-tsc/src/flow/Plumber.js.map +1 -1
  18. package/out-tsc/src/flow/actions/add_contact_urn.js +2 -6
  19. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  20. package/out-tsc/src/flow/actions/enter_flow.js +2 -2
  21. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -1
  22. package/out-tsc/src/flow/actions/say_msg.js +2 -1
  23. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  24. package/out-tsc/src/flow/actions/send_broadcast.js +2 -6
  25. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  26. package/out-tsc/src/flow/actions/send_email.js +2 -6
  27. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  28. package/out-tsc/src/flow/actions/send_msg.js +52 -35
  29. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  30. package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
  31. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  32. package/out-tsc/src/flow/actions/set_contact_field.js +4 -5
  33. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  34. package/out-tsc/src/flow/actions/set_contact_language.js +3 -3
  35. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  36. package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
  37. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  38. package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
  39. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  40. package/out-tsc/src/flow/actions/set_run_result.js +3 -3
  41. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  42. package/out-tsc/src/flow/actions/start_session.js +2 -2
  43. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  44. package/out-tsc/src/flow/nodes/split_by_llm.js +4 -5
  45. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  46. package/out-tsc/src/flow/nodes/split_by_resthook.js +3 -8
  47. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
  48. package/out-tsc/src/flow/nodes/split_by_subflow.js +2 -2
  49. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  50. package/out-tsc/src/flow/nodes/split_by_webhook.js +25 -33
  51. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  52. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -0
  53. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  54. package/out-tsc/src/flow/types.js.map +1 -1
  55. package/out-tsc/src/flow/utils.js +68 -0
  56. package/out-tsc/src/flow/utils.js.map +1 -1
  57. package/out-tsc/src/form/FieldRenderer.js +17 -2
  58. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  59. package/out-tsc/src/interfaces.js +1 -0
  60. package/out-tsc/src/interfaces.js.map +1 -1
  61. package/out-tsc/src/simulator/Simulator.js +1 -1
  62. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  63. package/out-tsc/test/temba-canvas-menu.test.js +13 -9
  64. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
  65. package/out-tsc/test/temba-flow-reflow.test.js.map +1 -1
  66. package/out-tsc/test/temba-node-editor.test.js +9 -10
  67. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  68. package/out-tsc/test/temba-node-type-selector.test.js +3 -3
  69. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  70. package/out-tsc/test/temba-simulator.test.js +2 -2
  71. package/out-tsc/test/temba-simulator.test.js.map +1 -1
  72. package/package.json +1 -1
  73. package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
  74. package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
  75. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  76. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  77. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  78. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  79. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  80. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  81. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  82. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  83. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  84. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  85. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  86. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  87. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  88. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  89. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  90. package/screenshots/truth/canvas-menu/open.png +0 -0
  91. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  92. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  93. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  94. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  95. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  96. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  97. package/src/Icons.ts +1 -0
  98. package/src/flow/CanvasMenu.ts +38 -39
  99. package/src/flow/CanvasNode.ts +16 -8
  100. package/src/flow/Editor.ts +33 -6
  101. package/src/flow/NodeEditor.ts +373 -10
  102. package/src/flow/NodeTypeSelector.ts +2 -0
  103. package/src/flow/Plumber.ts +3 -1
  104. package/src/flow/actions/add_contact_urn.ts +5 -6
  105. package/src/flow/actions/enter_flow.ts +2 -2
  106. package/src/flow/actions/say_msg.ts +2 -1
  107. package/src/flow/actions/send_broadcast.ts +2 -6
  108. package/src/flow/actions/send_email.ts +2 -6
  109. package/src/flow/actions/send_msg.ts +56 -38
  110. package/src/flow/actions/set_contact_channel.ts +5 -1
  111. package/src/flow/actions/set_contact_field.ts +10 -5
  112. package/src/flow/actions/set_contact_language.ts +6 -3
  113. package/src/flow/actions/set_contact_name.ts +5 -1
  114. package/src/flow/actions/set_contact_status.ts +5 -1
  115. package/src/flow/actions/set_run_result.ts +6 -3
  116. package/src/flow/actions/start_session.ts +2 -2
  117. package/src/flow/nodes/split_by_llm.ts +5 -5
  118. package/src/flow/nodes/split_by_resthook.ts +3 -8
  119. package/src/flow/nodes/split_by_subflow.ts +2 -2
  120. package/src/flow/nodes/split_by_webhook.ts +26 -34
  121. package/src/flow/nodes/wait_for_response.ts +1 -0
  122. package/src/flow/types.ts +25 -2
  123. package/src/flow/utils.ts +81 -1
  124. package/src/form/FieldRenderer.ts +32 -3
  125. package/src/interfaces.ts +1 -0
  126. package/src/simulator/Simulator.ts +1 -1
  127. package/test/temba-canvas-menu.test.ts +13 -9
  128. package/test/temba-flow-reflow.test.ts +4 -2
  129. package/test/temba-node-editor.test.ts +9 -10
  130. package/test/temba-node-type-selector.test.ts +3 -3
  131. 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
- ${action.template
27
- ? html`<div
28
- 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);"
29
- >
30
- <temba-icon name="channel_wac"></temba-icon>
31
- <div style="margin-left:0.5em">${action.template.name}</div>
32
- </div>`
33
- : null}
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: 'group',
98
- label: 'Quick Replies',
99
- items: ['quick_replies'],
100
- collapsible: true,
101
- collapsed: (formData: FormData) => {
102
- // Collapse only if there are no quick replies
103
- return !formData.quick_replies || formData.quick_replies.length === 0;
104
- },
105
- getGroupValueCount: (formData: FormData) => {
106
- return formData.quick_replies?.length || 0;
107
- }
108
- },
109
- {
110
- type: 'group',
111
- label: 'Runtime Attachments',
112
- items: ['runtime_attachments'],
113
- collapsible: true,
114
- collapsed: true,
115
- helpText: 'Add dynamic attachments that are evaluated at runtime',
116
- contentPadding: '12px',
117
- getGroupValueCount: (formData: FormData) => {
118
- return (
119
- formData.runtime_attachments?.filter(
120
- (item: any) =>
121
- item && item.expression && item.expression.trim() !== ''
122
- ).length || 0
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 html`<div>Set to <strong>${action.channel.name}</strong></div>`;
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 html`<div>
12
- Set <strong>${action.field.name}</strong> to
13
- <strong>${action.value}</strong>
14
- </div>`;
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 html`<div>Clear <strong>${action.field.name}</strong></div>`;
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
- return html`<div>
21
- Set to <strong>${languageNames.of(action.language)}</strong>
22
- </div>`;
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 html`<div>Set to <strong>${action.name}</strong></div>`;
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 html`<div>Set to <strong>${titleCase(action.status)}</strong></div>`;
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 html`<div>
12
- Save <strong>${action.value}</strong> as <strong>${action.name}</strong>
13
- </div>`;
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
- ${renderNamedObjects([action.flow], 'flow')}
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
- class="body"
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
- return html`
51
- <div
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 { renderNamedObjects } from '../utils';
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
- ${renderNamedObjects([enterFlowAction?.flow], 'flow')}
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: 'group',
99
- label: 'Headers',
100
- items: ['headers'],
101
- collapsible: true,
102
- collapsed: true,
103
- helpText: 'Configure authentication or custom headers',
104
- getGroupValueCount: (formData: FormData) => {
105
- return formData.headers?.length || 0;
106
- }
107
- },
108
- {
109
- type: 'group',
110
- label: 'Body',
111
- items: ['body'],
112
- collapsible: true,
113
- collapsed: true,
114
- helpText: 'Configure the request payload',
115
- getGroupValueCount: (formData: FormData) => {
116
- return !!(
117
- formData.body &&
118
- formData.body.trim() !== '' &&
119
- formData.body !== defaultPost
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
- return html`
129
- <div
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
@@ -82,6 +82,7 @@ export const wait_for_response: NodeConfig = {
82
82
  name: 'Wait for Response',
83
83
  group: SPLIT_GROUPS.wait,
84
84
  flowTypes: [FlowTypes.MESSAGE],
85
+ hideFromSplits: true,
85
86
  dialogSize: 'large',
86
87
  form: {
87
88
  rules: createRulesArrayConfig(
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
- getGroupValueCount?: (formData: FormData) => number; // optional function to get count for bubble display
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;