@nyaruka/temba-components 0.156.2 → 0.156.4

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.
@@ -76,6 +76,7 @@ export const send_broadcast: ActionConfig = {
76
76
  required: true,
77
77
  evaluated: true,
78
78
  placeholder: 'Type your message here...',
79
+ maxLength: 640,
79
80
  maxAttachments: 10,
80
81
  accept: '',
81
82
  endpoint: '/api/v2/media.json',
@@ -180,5 +181,52 @@ export const send_broadcast: ActionConfig = {
180
181
  if (formData.text && typeof formData.text === 'string') {
181
182
  formData.text = formData.text.trim();
182
183
  }
184
+ },
185
+ localizable: ['text', 'attachments'],
186
+ toLocalizationFormData: (
187
+ action: SendBroadcast,
188
+ localization: Record<string, any>
189
+ ) => {
190
+ const formData: FormData = {
191
+ uuid: action.uuid
192
+ };
193
+
194
+ if (localization.text && Array.isArray(localization.text)) {
195
+ formData.text = localization.text[0] || '';
196
+ } else {
197
+ formData.text = '';
198
+ }
199
+
200
+ if (localization.attachments && Array.isArray(localization.attachments)) {
201
+ formData.attachments = localization.attachments;
202
+ } else {
203
+ formData.attachments = [];
204
+ }
205
+
206
+ return formData;
207
+ },
208
+ fromLocalizationFormData: (formData: FormData, action: SendBroadcast) => {
209
+ const localization: Record<string, any> = {};
210
+
211
+ if (formData.text && formData.text.trim() !== '') {
212
+ if (formData.text !== action.text) {
213
+ localization.text = [formData.text];
214
+ }
215
+ }
216
+
217
+ const attachments = (formData.attachments || []).filter(
218
+ (att: string) => att && att.trim() !== ''
219
+ );
220
+
221
+ if (attachments.length > 0) {
222
+ if (
223
+ JSON.stringify(attachments) !==
224
+ JSON.stringify(action.attachments || [])
225
+ ) {
226
+ localization.attachments = attachments;
227
+ }
228
+ }
229
+
230
+ return localization;
183
231
  }
184
232
  };
@@ -45,12 +45,13 @@ export const send_email: ActionConfig = {
45
45
  required: true,
46
46
  evaluated: true,
47
47
  placeholder: 'Enter email subject',
48
- maxLength: 255
48
+ maxLength: 1000
49
49
  },
50
50
  body: {
51
51
  type: 'textarea',
52
52
  required: true,
53
53
  evaluated: true,
54
+ maxLength: 10000,
54
55
  minHeight: 175
55
56
  }
56
57
  },
@@ -65,6 +66,46 @@ export const send_email: ActionConfig = {
65
66
  body: formData.body
66
67
  };
67
68
  },
69
+ localizable: ['subject', 'body'],
70
+ toLocalizationFormData: (
71
+ action: SendEmail,
72
+ localization: Record<string, any>
73
+ ) => {
74
+ const formData: FormData = {
75
+ uuid: action.uuid
76
+ };
77
+
78
+ if (localization.subject && Array.isArray(localization.subject)) {
79
+ formData.subject = localization.subject[0] || '';
80
+ } else {
81
+ formData.subject = '';
82
+ }
83
+
84
+ if (localization.body && Array.isArray(localization.body)) {
85
+ formData.body = localization.body[0] || '';
86
+ } else {
87
+ formData.body = '';
88
+ }
89
+
90
+ return formData;
91
+ },
92
+ fromLocalizationFormData: (formData: FormData, action: SendEmail) => {
93
+ const localization: Record<string, any> = {};
94
+
95
+ if (formData.subject && formData.subject.trim() !== '') {
96
+ if (formData.subject !== action.subject) {
97
+ localization.subject = [formData.subject];
98
+ }
99
+ }
100
+
101
+ if (formData.body && formData.body.trim() !== '') {
102
+ if (formData.body !== action.body) {
103
+ localization.body = [formData.body];
104
+ }
105
+ }
106
+
107
+ return localization;
108
+ },
68
109
  validate: (formData: FormData): ValidationResult => {
69
110
  const errors: { [key: string]: string } = {};
70
111
 
@@ -65,6 +65,7 @@ export const send_msg: ActionConfig = {
65
65
  required: true,
66
66
  evaluated: true,
67
67
  placeholder: 'Type your message here...',
68
+ maxLength: 10000,
68
69
  maxAttachments: 10,
69
70
  accept: '',
70
71
  endpoint: '/api/v2/media.json',
@@ -8,17 +8,14 @@ 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
+ import { getLanguageDisplayName, renderClamped } from '../utils';
12
12
 
13
13
  export const set_contact_language: ActionConfig = {
14
14
  name: 'Update Language',
15
15
  group: ACTION_GROUPS.contacts,
16
16
  flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
17
17
  render: (_node: Node, action: SetContactLanguage) => {
18
- const languageNames = new Intl.DisplayNames(['en'], {
19
- type: 'language'
20
- });
21
- const name = languageNames.of(action.language) || action.language;
18
+ const name = getLanguageDisplayName(action.language);
22
19
  return renderClamped(
23
20
  html`Set to <strong>${name}</strong>`,
24
21
  `Set to ${name}`
@@ -38,12 +35,9 @@ export const set_contact_language: ActionConfig = {
38
35
  const store = getStore();
39
36
  const workspace = store?.getState().workspace;
40
37
  if (workspace?.languages && Array.isArray(workspace.languages)) {
41
- const languageNames = new Intl.DisplayNames(['en'], {
42
- type: 'language'
43
- });
44
38
  return workspace.languages.map((languageCode: string) => ({
45
39
  value: languageCode,
46
- name: languageNames.of(languageCode) || languageCode
40
+ name: getLanguageDisplayName(languageCode)
47
41
  }));
48
42
  }
49
43
  return [];
@@ -53,14 +47,11 @@ export const set_contact_language: ActionConfig = {
53
47
  toFormData: (action: SetContactLanguage) => {
54
48
  // Convert the language code back to the option object format expected by the form
55
49
  if (action.language) {
56
- const languageNames = new Intl.DisplayNames(['en'], {
57
- type: 'language'
58
- });
59
50
  return {
60
51
  language: [
61
52
  {
62
53
  value: action.language,
63
- name: languageNames.of(action.language) || action.language
54
+ name: getLanguageDisplayName(action.language)
64
55
  }
65
56
  ],
66
57
  uuid: action.uuid
@@ -57,6 +57,7 @@ export const set_run_result: ActionConfig = {
57
57
  label: 'Category',
58
58
  helpText: 'Optional category for this result',
59
59
  required: false,
60
+ maxLength: 36,
60
61
  placeholder: 'Enter category...'
61
62
  }
62
63
  },
@@ -189,6 +189,7 @@ export const createRulesItemConfig = () => ({
189
189
  type: 'text' as const,
190
190
  placeholder: 'Category',
191
191
  required: true,
192
+ maxLength: 36,
192
193
  maxWidth: '120px',
193
194
  flavor: 'xsmall' as const
194
195
  }
@@ -18,6 +18,7 @@ import { getOperatorConfig } from '../operators';
18
18
  export const resultNameField: TextFieldConfig = {
19
19
  type: 'text',
20
20
  required: false,
21
+ maxLength: 64,
21
22
  placeholder: '(optional)',
22
23
  helpText: 'The name to use to reference this result in the flow'
23
24
  };
@@ -16,6 +16,7 @@ export const wait_for_audio: NodeConfig = {
16
16
  type: 'text',
17
17
  label: 'Result Name',
18
18
  required: false,
19
+ maxLength: 64,
19
20
  placeholder: '(optional)',
20
21
  helpText: 'The name to use to reference this result in the flow'
21
22
  }
package/src/flow/utils.ts CHANGED
@@ -6,6 +6,17 @@ import { tokenize, TokenType } from '../excellent/tokenizer';
6
6
  import { TOKEN_COLORS } from '../excellent/token-styles';
7
7
  import { messageParser, sessionParser } from '../excellent/helpers';
8
8
 
9
+ const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });
10
+
11
+ export function getLanguageDisplayName(code: string): string {
12
+ if (code === 'und') return 'Unknown';
13
+ try {
14
+ return languageNames.of(code) || code;
15
+ } catch {
16
+ return code;
17
+ }
18
+ }
19
+
9
20
  const IS_MAC =
10
21
  typeof navigator !== 'undefined' &&
11
22
  /Mac|iPod|iPhone|iPad/.test(navigator.platform);
@@ -34,6 +34,9 @@ const TEST_LOCATIONS = [
34
34
  'geo:-1.9536,30.0606' // Kigali
35
35
  ];
36
36
 
37
+ const truncateUrl = (url: string, maxLen = 50): string =>
38
+ url && url.length > maxLen ? url.slice(0, maxLen) + '…' : url;
39
+
37
40
  interface Contact {
38
41
  uuid: string;
39
42
  name?: string;
@@ -1472,18 +1475,20 @@ export class Simulator extends RapidElement {
1472
1475
  const renderedEvent =
1473
1476
  event.type === Events.WEBHOOK_CALLED && this.hasWebhookDetails(event)
1474
1477
  ? html`<div class="webhook-event">
1475
- <div class="webhook-event-text">${rendered}</div>
1476
- <button
1477
- type="button"
1478
- data-webhook-details="true"
1479
- class="webhook-event-log-link"
1480
- title="View webhook call details"
1481
- aria-label="View webhook call details"
1482
- @click=${(clickEvent: MouseEvent) =>
1483
- this.handleWebhookDetailsClick(event, clickEvent)}
1484
- >
1485
- <temba-icon name="log" size="0.8"></temba-icon>
1486
- </button>
1478
+ <div class="webhook-event-text">
1479
+ Called
1480
+ <a
1481
+ href="#"
1482
+ class="webhook-event-url"
1483
+ data-webhook-details="true"
1484
+ title="View webhook call details"
1485
+ @click=${(clickEvent: MouseEvent) => {
1486
+ clickEvent.preventDefault();
1487
+ this.handleWebhookDetailsClick(event, clickEvent);
1488
+ }}
1489
+ >${truncateUrl((event as any).url)}</a
1490
+ >
1491
+ </div>
1487
1492
  </div>`
1488
1493
  : rendered;
1489
1494
 
package/temba-modules.ts CHANGED
@@ -56,6 +56,7 @@ import { Toast } from './src/display/Toast';
56
56
  import { Chat } from './src/display/Chat';
57
57
  import { MediaPicker } from './src/form/MediaPicker';
58
58
  import { Editor } from './src/flow/Editor';
59
+ import { EditorToolbar } from './src/flow/EditorToolbar';
59
60
  import { CanvasNode } from './src/flow/CanvasNode';
60
61
  import { StickyNote } from './src/flow/StickyNote';
61
62
  import { CanvasMenu } from './src/flow/CanvasMenu';
@@ -151,6 +152,7 @@ addCustomElement('temba-toast', Toast);
151
152
  addCustomElement('temba-chat', Chat);
152
153
  addCustomElement('temba-media-picker', MediaPicker);
153
154
  addCustomElement('temba-flow-editor', Editor);
155
+ addCustomElement('temba-editor-toolbar', EditorToolbar);
154
156
  addCustomElement('temba-message-table', MessageTable);
155
157
  addCustomElement('temba-node-editor', NodeEditor);
156
158
  addCustomElement('temba-flow-node', CanvasNode);
@@ -282,6 +282,8 @@ const wireScreenshots = async (page, context, wait, replaceScreenshots) => {
282
282
 
283
283
  await page.exposeFunction('click', async (element) => {
284
284
  if (page.isClosed()) return;
285
+ // reset mouse state to avoid "already pressed" errors
286
+ await page.mouse.up().catch(() => {});
285
287
  const frame = await page.frames().find((f) => {
286
288
  return true;
287
289
  });