@nyaruka/temba-components 0.167.0 → 0.168.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/locales/es.js +3 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +3 -1
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +3 -1
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +2791 -1668
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/Icons.ts +2 -0
- package/src/display/Button.ts +8 -6
- package/src/display/Chat.ts +541 -191
- package/src/display/Dropdown.ts +40 -17
- package/src/display/Label.ts +25 -4
- package/src/display/TembaDate.ts +11 -2
- package/src/display/Tip.ts +157 -18
- package/src/events/eventRenderers.ts +678 -268
- package/src/events.ts +16 -1
- package/src/flow/NodeEditor.ts +82 -9
- package/src/flow/actions/add_contact_urn.ts +1 -0
- package/src/flow/actions/play_audio.ts +1 -0
- package/src/flow/actions/send_broadcast.ts +17 -1
- package/src/flow/actions/send_msg.ts +12 -1
- package/src/flow/actions/set_run_result.ts +1 -0
- package/src/flow/actions/start_session.ts +10 -1
- package/src/flow/nodes/shared-rules.ts +2 -0
- package/src/flow/nodes/split_by_airtime.ts +6 -0
- package/src/flow/nodes/split_by_expression.ts +1 -0
- package/src/flow/nodes/split_by_llm.ts +2 -0
- package/src/flow/nodes/split_by_llm_categorize.ts +3 -1
- package/src/flow/nodes/split_by_random.ts +2 -1
- package/src/flow/nodes/split_by_resthook.ts +4 -1
- package/src/flow/nodes/split_by_webhook.ts +9 -1
- package/src/flow/types.ts +3 -0
- package/src/flow/utils.ts +49 -0
- package/src/form/DatePicker.ts +2 -1
- package/src/layout/Card.ts +9 -0
- package/src/layout/CardLayout.ts +426 -21
- package/src/layout/CardStack.ts +4 -1
- package/src/list/ContactList.ts +22 -17
- package/src/list/ContentList.ts +632 -29
- package/src/list/MsgList.ts +4 -2
- package/src/list/SortableList.ts +5 -1
- package/src/list/TembaMenu.ts +67 -6
- package/src/live/ContactChat.ts +655 -336
- package/src/live/ContactDetails.ts +961 -88
- package/src/live/ContactFieldEditor.ts +46 -11
- package/src/live/ContactFields.ts +57 -16
- package/src/live/ContactStoreElement.ts +15 -8
- package/src/locales/es.ts +3 -2
- package/src/locales/fr.ts +3 -2
- package/src/locales/pt.ts +3 -2
- package/src/simulator/Simulator.ts +28 -0
- package/src/styles/pillVariants.ts +13 -1
- package/web-test-runner.config.mjs +2 -0
- package/xliff/es.xlf +6 -3
- package/xliff/fr.xlf +6 -3
- package/xliff/pt.xlf +6 -3
package/src/events.ts
CHANGED
|
@@ -11,6 +11,18 @@ export interface ContactLanguageChangedEvent extends ContactEvent {
|
|
|
11
11
|
language: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// ephemeral state event - arrives over the socket but is never persisted,
|
|
15
|
+
// updates the contact's current flow rather than recording history
|
|
16
|
+
export interface ContactFlowChangedEvent extends ContactEvent {
|
|
17
|
+
flow: ObjectReference | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ephemeral state event - arrives over the socket but is never persisted,
|
|
21
|
+
// updates the contact's last seen rather than recording history
|
|
22
|
+
export interface ContactLastSeenChangedEvent extends ContactEvent {
|
|
23
|
+
last_seen_on: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export interface ContactStatusChangedEvent extends ContactEvent {
|
|
15
27
|
status: string;
|
|
16
28
|
}
|
|
@@ -62,6 +74,7 @@ export interface TicketEvent extends ContactEvent {
|
|
|
62
74
|
// ticket_opened
|
|
63
75
|
uuid: string;
|
|
64
76
|
topic?: ObjectReference;
|
|
77
|
+
assignee?: User;
|
|
65
78
|
};
|
|
66
79
|
ticket_uuid?: string; // all other event types
|
|
67
80
|
assignee?: User;
|
|
@@ -75,7 +88,9 @@ export interface NameChangedEvent extends ContactEvent {
|
|
|
75
88
|
|
|
76
89
|
export interface UpdateFieldEvent extends ContactEvent {
|
|
77
90
|
field: { key: string; name: string };
|
|
78
|
-
|
|
91
|
+
// engine field values always carry text; typed representations are
|
|
92
|
+
// present when the value parses as that type (see goflow's Value)
|
|
93
|
+
value: { text: string; datetime?: string; number?: string };
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
export interface ContactGroupsEvent extends ContactEvent {
|
package/src/flow/NodeEditor.ts
CHANGED
|
@@ -1269,17 +1269,17 @@ export class NodeEditor extends RapidElement {
|
|
|
1269
1269
|
// array/object handling, an over-long value here is emitted into the definition
|
|
1270
1270
|
// and rejected by the backend (goflow caps result names at 64 and quick replies
|
|
1271
1271
|
// at 1000 chars).
|
|
1272
|
+
const itemLength = (item: any): number => {
|
|
1273
|
+
if (typeof item === 'string') return item.length;
|
|
1274
|
+
if (item && typeof item === 'object') {
|
|
1275
|
+
const text = item.value ?? item.name;
|
|
1276
|
+
return typeof text === 'string' ? text.length : 0;
|
|
1277
|
+
}
|
|
1278
|
+
return 0;
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1272
1281
|
const maxLength = (fieldConfig as any).maxLength;
|
|
1273
1282
|
if (maxLength) {
|
|
1274
|
-
const itemLength = (item: any): number => {
|
|
1275
|
-
if (typeof item === 'string') return item.length;
|
|
1276
|
-
if (item && typeof item === 'object') {
|
|
1277
|
-
const text = item.value ?? item.name;
|
|
1278
|
-
return typeof text === 'string' ? text.length : 0;
|
|
1279
|
-
}
|
|
1280
|
-
return 0;
|
|
1281
|
-
};
|
|
1282
|
-
|
|
1283
1283
|
const exceedsMax = Array.isArray(value)
|
|
1284
1284
|
? value.some((item) => itemLength(item) > maxLength)
|
|
1285
1285
|
: itemLength(value) > maxLength;
|
|
@@ -1290,6 +1290,79 @@ export class NodeEditor extends RapidElement {
|
|
|
1290
1290
|
} must be no more than ${maxLength} characters`;
|
|
1291
1291
|
}
|
|
1292
1292
|
}
|
|
1293
|
+
|
|
1294
|
+
// Check maxLength declared on array item sub-fields (e.g. rule categories,
|
|
1295
|
+
// case arguments) - these live in itemConfig which the top-level check
|
|
1296
|
+
// above doesn't reach, so without this an over-long value is emitted into
|
|
1297
|
+
// the definition and rejected by the backend.
|
|
1298
|
+
if (
|
|
1299
|
+
fieldConfig.type === 'array' &&
|
|
1300
|
+
fieldConfig.itemConfig &&
|
|
1301
|
+
Array.isArray(value)
|
|
1302
|
+
) {
|
|
1303
|
+
Object.entries(fieldConfig.itemConfig).forEach(
|
|
1304
|
+
([subFieldName, subFieldConfig]) => {
|
|
1305
|
+
const subMaxLength = (subFieldConfig as any).maxLength;
|
|
1306
|
+
if (!subMaxLength || errors[fieldName]) {
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const exceeds = value.some((item: any) => {
|
|
1310
|
+
// skip sub-fields hidden for this item (e.g. a rule value for
|
|
1311
|
+
// an operator that takes no operands) - they aren't emitted
|
|
1312
|
+
const visible = (subFieldConfig as any).conditions?.visible;
|
|
1313
|
+
if (visible && !visible(item)) {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
// measure like the top-level check so option-object values
|
|
1317
|
+
// (e.g. a select or tag input inside an array) are covered
|
|
1318
|
+
const subValue = item?.[subFieldName];
|
|
1319
|
+
return Array.isArray(subValue)
|
|
1320
|
+
? subValue.some((sub) => itemLength(sub) > subMaxLength)
|
|
1321
|
+
: itemLength(subValue) > subMaxLength;
|
|
1322
|
+
});
|
|
1323
|
+
if (exceeds) {
|
|
1324
|
+
errors[fieldName] = `${
|
|
1325
|
+
(subFieldConfig as any).label || subFieldName
|
|
1326
|
+
} must be no more than ${subMaxLength} characters`;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Check key-value fields against their declared limits (e.g. goflow caps
|
|
1333
|
+
// webhook headers at 100 entries, names at 100 chars and values at 1000).
|
|
1334
|
+
if (fieldConfig.type === 'key-value' && Array.isArray(value)) {
|
|
1335
|
+
const rows = value.filter(
|
|
1336
|
+
(item: any) =>
|
|
1337
|
+
((item?.key || '') as string).trim() !== '' ||
|
|
1338
|
+
((item?.value || '') as string).trim() !== ''
|
|
1339
|
+
);
|
|
1340
|
+
const label = (fieldConfig as any).label || fieldName;
|
|
1341
|
+
if (fieldConfig.maxItems && rows.length > fieldConfig.maxItems) {
|
|
1342
|
+
errors[fieldName] =
|
|
1343
|
+
`${label} can have no more than ${fieldConfig.maxItems} entries`;
|
|
1344
|
+
} else if (
|
|
1345
|
+
fieldConfig.keyMaxLength &&
|
|
1346
|
+
rows.some(
|
|
1347
|
+
(item: any) =>
|
|
1348
|
+
(item.key || '').length > fieldConfig.keyMaxLength
|
|
1349
|
+
)
|
|
1350
|
+
) {
|
|
1351
|
+
errors[fieldName] = `${
|
|
1352
|
+
fieldConfig.keyPlaceholder || 'Key'
|
|
1353
|
+
} must be no more than ${fieldConfig.keyMaxLength} characters`;
|
|
1354
|
+
} else if (
|
|
1355
|
+
fieldConfig.valueMaxLength &&
|
|
1356
|
+
rows.some(
|
|
1357
|
+
(item: any) =>
|
|
1358
|
+
(item.value || '').length > fieldConfig.valueMaxLength
|
|
1359
|
+
)
|
|
1360
|
+
) {
|
|
1361
|
+
errors[fieldName] = `${
|
|
1362
|
+
fieldConfig.valuePlaceholder || 'Value'
|
|
1363
|
+
} must be no more than ${fieldConfig.valueMaxLength} characters`;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1293
1366
|
});
|
|
1294
1367
|
}
|
|
1295
1368
|
|
|
@@ -4,7 +4,10 @@ import { Node, SendBroadcast } from '../../store/flow-definition';
|
|
|
4
4
|
import {
|
|
5
5
|
renderMixedList,
|
|
6
6
|
renderClamped,
|
|
7
|
-
renderHighlightedText
|
|
7
|
+
renderHighlightedText,
|
|
8
|
+
validateRecipients,
|
|
9
|
+
validateTemplateVariables,
|
|
10
|
+
validateWith
|
|
8
11
|
} from '../utils';
|
|
9
12
|
import { Icon } from '../../Icons';
|
|
10
13
|
import { CustomEventType } from '../../interfaces';
|
|
@@ -182,6 +185,19 @@ export const send_broadcast: ActionConfig = {
|
|
|
182
185
|
formData.text = formData.text.trim();
|
|
183
186
|
}
|
|
184
187
|
},
|
|
188
|
+
validate: validateWith((formData, errors) => {
|
|
189
|
+
const recipientsError = validateRecipients(formData.recipients || []);
|
|
190
|
+
if (recipientsError) {
|
|
191
|
+
errors.recipients = recipientsError;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const templateVariablesError = validateTemplateVariables(
|
|
195
|
+
formData.template_variables || []
|
|
196
|
+
);
|
|
197
|
+
if (templateVariablesError) {
|
|
198
|
+
errors.template = templateVariablesError;
|
|
199
|
+
}
|
|
200
|
+
}),
|
|
185
201
|
localizable: ['text', 'attachments'],
|
|
186
202
|
toLocalizationFormData: (
|
|
187
203
|
action: SendBroadcast,
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { Node, SendMsg } from '../../store/flow-definition';
|
|
10
10
|
import { titleCase } from '../../utils';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
renderClamped,
|
|
13
|
+
renderHighlightedText,
|
|
14
|
+
validateTemplateVariables
|
|
15
|
+
} from '../utils';
|
|
12
16
|
import { splitSMS } from '../../display/sms';
|
|
13
17
|
|
|
14
18
|
const ATTACHMENT_TYPE_NAMES: Record<string, string> = {
|
|
@@ -268,6 +272,13 @@ export const send_msg: ActionConfig = {
|
|
|
268
272
|
}
|
|
269
273
|
}
|
|
270
274
|
|
|
275
|
+
const templateVariablesError = validateTemplateVariables(
|
|
276
|
+
formData.template_variables || []
|
|
277
|
+
);
|
|
278
|
+
if (templateVariablesError) {
|
|
279
|
+
errors.template = templateVariablesError;
|
|
280
|
+
}
|
|
281
|
+
|
|
271
282
|
return {
|
|
272
283
|
valid: Object.keys(errors).length === 0,
|
|
273
284
|
errors
|
|
@@ -10,7 +10,8 @@ import { Node, StartSession } from '../../store/flow-definition';
|
|
|
10
10
|
import {
|
|
11
11
|
renderMixedList,
|
|
12
12
|
renderFlowLinks,
|
|
13
|
-
renderHighlightedText
|
|
13
|
+
renderHighlightedText,
|
|
14
|
+
validateRecipients
|
|
14
15
|
} from '../utils';
|
|
15
16
|
import { Icon } from '../../Icons';
|
|
16
17
|
import { CustomEventType } from '../../interfaces';
|
|
@@ -158,6 +159,7 @@ export const start_session: ActionConfig = {
|
|
|
158
159
|
evaluated: true,
|
|
159
160
|
label: 'Contact Query',
|
|
160
161
|
helpText: 'Only one matching contact will be started',
|
|
162
|
+
maxLength: 10000,
|
|
161
163
|
placeholder: 'household_id = @fields.household_id',
|
|
162
164
|
conditions: {
|
|
163
165
|
visible: (formData: FormData) => {
|
|
@@ -194,6 +196,13 @@ export const start_session: ActionConfig = {
|
|
|
194
196
|
errors.recipients = 'At least one contact or group must be selected';
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
if (startType === 'manual') {
|
|
200
|
+
const recipientsError = validateRecipients(formData.recipients || []);
|
|
201
|
+
if (recipientsError) {
|
|
202
|
+
errors.recipients = recipientsError;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
197
206
|
// Check if query has a query string
|
|
198
207
|
if (
|
|
199
208
|
startType === 'query' &&
|
|
@@ -172,6 +172,7 @@ export const createRulesItemConfig = () => ({
|
|
|
172
172
|
type: 'text' as const,
|
|
173
173
|
placeholder: value1Placeholder,
|
|
174
174
|
evaluated: true,
|
|
175
|
+
maxLength: 10000,
|
|
175
176
|
flavor: 'xsmall' as const,
|
|
176
177
|
conditions: {
|
|
177
178
|
visible: value1VisibilityCondition
|
|
@@ -181,6 +182,7 @@ export const createRulesItemConfig = () => ({
|
|
|
181
182
|
type: 'text' as const,
|
|
182
183
|
placeholder: value2Placeholder,
|
|
183
184
|
evaluated: true,
|
|
185
|
+
maxLength: 10000,
|
|
184
186
|
flavor: 'xsmall' as const,
|
|
185
187
|
conditions: {
|
|
186
188
|
visible: value2VisibilityCondition
|
|
@@ -87,6 +87,7 @@ export const split_by_airtime: NodeConfig = {
|
|
|
87
87
|
|
|
88
88
|
if (duplicates.length > 0) {
|
|
89
89
|
errors.amounts = `Duplicate currencies found: ${duplicates.join(', ')}`;
|
|
90
|
+
return;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
for (const item of validAmounts) {
|
|
@@ -95,6 +96,11 @@ export const split_by_airtime: NodeConfig = {
|
|
|
95
96
|
errors.amounts = 'All amounts must be valid positive numbers';
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
99
|
+
// the engine caps amounts at 1e15
|
|
100
|
+
if (Number(amount) > 1e15) {
|
|
101
|
+
errors.amounts = 'Amounts must be 1,000,000,000,000,000 or less';
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
98
104
|
}
|
|
99
105
|
}),
|
|
100
106
|
render: (node: Node) => {
|
|
@@ -57,6 +57,7 @@ export const split_by_llm: NodeConfig = {
|
|
|
57
57
|
helpText: 'The input the AI will process',
|
|
58
58
|
required: true,
|
|
59
59
|
evaluated: true,
|
|
60
|
+
maxLength: 10000,
|
|
60
61
|
placeholder: '@input'
|
|
61
62
|
},
|
|
62
63
|
instructions: {
|
|
@@ -66,6 +67,7 @@ export const split_by_llm: NodeConfig = {
|
|
|
66
67
|
'Tell the AI what to do with the input. The result can be referenced as **`@locals._llm_output`**',
|
|
67
68
|
required: true,
|
|
68
69
|
evaluated: true,
|
|
70
|
+
maxLength: 10000,
|
|
69
71
|
placeholder: 'Enter instructions for the AI model...',
|
|
70
72
|
minHeight: 130
|
|
71
73
|
}
|
|
@@ -33,6 +33,7 @@ export const split_by_llm_categorize: NodeConfig = {
|
|
|
33
33
|
helpText: 'The input to categorize (usually @input)',
|
|
34
34
|
required: true,
|
|
35
35
|
evaluated: true,
|
|
36
|
+
maxLength: 10000,
|
|
36
37
|
placeholder: '@input'
|
|
37
38
|
},
|
|
38
39
|
categories: {
|
|
@@ -50,7 +51,8 @@ export const split_by_llm_categorize: NodeConfig = {
|
|
|
50
51
|
name: {
|
|
51
52
|
type: 'text',
|
|
52
53
|
placeholder: 'Category name',
|
|
53
|
-
required: true
|
|
54
|
+
required: true,
|
|
55
|
+
maxLength: 36
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
},
|
|
@@ -87,7 +87,10 @@ export const split_by_resthook: NodeConfig = {
|
|
|
87
87
|
const existingCases = originalNode.router?.cases || [];
|
|
88
88
|
|
|
89
89
|
const { router, exits } = createSuccessFailureRouter(
|
|
90
|
-
|
|
90
|
+
// default() guards against @webhook being null (e.g. the call was
|
|
91
|
+
// skipped because the request exceeded the engine's size limit) which
|
|
92
|
+
// would otherwise produce lookup errors when the router evaluates
|
|
93
|
+
'@(default(webhook.status, 0))',
|
|
91
94
|
{
|
|
92
95
|
type: 'has_number_between',
|
|
93
96
|
arguments: ['200', '299']
|
|
@@ -79,6 +79,7 @@ export const split_by_webhook: NodeConfig = {
|
|
|
79
79
|
type: 'text',
|
|
80
80
|
required: true,
|
|
81
81
|
evaluated: true,
|
|
82
|
+
maxLength: 8192,
|
|
82
83
|
placeholder: 'https://example.com/webhook'
|
|
83
84
|
},
|
|
84
85
|
headers: {
|
|
@@ -86,6 +87,9 @@ export const split_by_webhook: NodeConfig = {
|
|
|
86
87
|
sortable: true,
|
|
87
88
|
keyPlaceholder: 'Header name',
|
|
88
89
|
valuePlaceholder: 'Header value',
|
|
90
|
+
maxItems: 100,
|
|
91
|
+
keyMaxLength: 100,
|
|
92
|
+
valueMaxLength: 8192,
|
|
89
93
|
minRows: 0,
|
|
90
94
|
dependsOn: ['method'],
|
|
91
95
|
computeValue: (
|
|
@@ -108,6 +112,7 @@ export const split_by_webhook: NodeConfig = {
|
|
|
108
112
|
body: {
|
|
109
113
|
type: 'textarea',
|
|
110
114
|
evaluated: true,
|
|
115
|
+
maxLength: 20000,
|
|
111
116
|
placeholder: 'Request body content (JSON, XML, etc.)',
|
|
112
117
|
minHeight: 200,
|
|
113
118
|
dependsOn: ['method'],
|
|
@@ -248,7 +253,10 @@ export const split_by_webhook: NodeConfig = {
|
|
|
248
253
|
const existingCases = originalNode.router?.cases || [];
|
|
249
254
|
|
|
250
255
|
const { router, exits } = createSuccessFailureRouter(
|
|
251
|
-
|
|
256
|
+
// default() guards against @webhook being null (e.g. the call was
|
|
257
|
+
// skipped because the request exceeded the engine's size limit) which
|
|
258
|
+
// would otherwise produce lookup errors when the router evaluates
|
|
259
|
+
'@(default(webhook.status, 0))',
|
|
252
260
|
{
|
|
253
261
|
type: 'has_number_between',
|
|
254
262
|
arguments: ['200', '299']
|
package/src/flow/types.ts
CHANGED
|
@@ -257,6 +257,9 @@ export interface KeyValueFieldConfig extends BaseFieldConfig {
|
|
|
257
257
|
valuePlaceholder?: string;
|
|
258
258
|
minRows?: number;
|
|
259
259
|
readOnlyKeys?: boolean;
|
|
260
|
+
maxItems?: number;
|
|
261
|
+
keyMaxLength?: number;
|
|
262
|
+
valueMaxLength?: number;
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
export interface ArrayFieldConfig extends BaseFieldConfig {
|
package/src/flow/utils.ts
CHANGED
|
@@ -28,6 +28,55 @@ export function validateWith(
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Validates a mixed recipients list (contacts, groups and expressions) against
|
|
33
|
+
* the engine's limits: at most 100 of each kind and expressions at most 1000
|
|
34
|
+
* characters. Returns an error message or null if the list is valid. The type
|
|
35
|
+
* predicates mirror how fromFormData partitions the list into the action's
|
|
36
|
+
* contacts / groups / legacy_vars.
|
|
37
|
+
*/
|
|
38
|
+
export function validateRecipients(recipients: any[]): string | null {
|
|
39
|
+
const contacts = recipients.filter(
|
|
40
|
+
(r: any) => r.type === 'contact' || (!r.type && !r.expression && r.id)
|
|
41
|
+
);
|
|
42
|
+
const groups = recipients.filter((r: any) => r.type === 'group');
|
|
43
|
+
const expressions = recipients.filter(
|
|
44
|
+
(r: any) => r.type === 'expression' || r.expression
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (contacts.length > 100) {
|
|
48
|
+
return 'You can only add up to 100 contacts';
|
|
49
|
+
}
|
|
50
|
+
if (groups.length > 100) {
|
|
51
|
+
return 'You can only add up to 100 groups';
|
|
52
|
+
}
|
|
53
|
+
if (expressions.length > 100) {
|
|
54
|
+
return 'You can only add up to 100 expressions';
|
|
55
|
+
}
|
|
56
|
+
const tooLong = expressions.some((e: any) => {
|
|
57
|
+
const value = e.value || e.name || e.id || '';
|
|
58
|
+
return typeof value === 'string' && value.length > 1000;
|
|
59
|
+
});
|
|
60
|
+
if (tooLong) {
|
|
61
|
+
return 'Expressions must be no more than 1000 characters';
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validates template variables against the engine's limit of 10000 characters
|
|
68
|
+
* per variable. Returns an error message or null if they're valid.
|
|
69
|
+
*/
|
|
70
|
+
export function validateTemplateVariables(variables: any[]): string | null {
|
|
71
|
+
const tooLong = variables.some(
|
|
72
|
+
(v: any) => typeof v === 'string' && v.length > 10000
|
|
73
|
+
);
|
|
74
|
+
if (tooLong) {
|
|
75
|
+
return 'Template variables must be no more than 10000 characters';
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
31
80
|
function makeTextToLocalizationFormData(fields: readonly string[]) {
|
|
32
81
|
return (
|
|
33
82
|
action: { uuid: string },
|
package/src/form/DatePicker.ts
CHANGED
|
@@ -29,7 +29,7 @@ export class DatePicker extends FieldElement {
|
|
|
29
29
|
|
|
30
30
|
.input-wrapper {
|
|
31
31
|
padding: var(--temba-textinput-padding);
|
|
32
|
-
flex-grow:
|
|
32
|
+
flex-grow: 999;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.tz {
|
|
@@ -65,6 +65,7 @@ export class DatePicker extends FieldElement {
|
|
|
65
65
|
flex-direction: row;
|
|
66
66
|
align-items: center;
|
|
67
67
|
padding: 0.4em 0em;
|
|
68
|
+
flex-grow: 1;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
.container:focus-within {
|
package/src/layout/Card.ts
CHANGED
|
@@ -18,6 +18,11 @@ export class Card extends RapidElement {
|
|
|
18
18
|
|
|
19
19
|
:host {
|
|
20
20
|
display: block;
|
|
21
|
+
/* as a grid/flex item (cards flatten into the card stack's
|
|
22
|
+
grid through its slots) the automatic minimum is
|
|
23
|
+
min-content — allow shrinking so long unbreakable content
|
|
24
|
+
inside ellipsizes instead of widening the card */
|
|
25
|
+
min-width: 0;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/* an empty panel drops its card entirely — in tab (plain) mode the
|
|
@@ -115,6 +120,10 @@ export class Card extends RapidElement {
|
|
|
115
120
|
.body {
|
|
116
121
|
display: grid;
|
|
117
122
|
grid-template-rows: 1fr;
|
|
123
|
+
/* the implicit column track is auto-sized and would grow to
|
|
124
|
+
the content's max-content (one long unbreakable line widens
|
|
125
|
+
the card instead of ellipsizing) — pin it to the card width */
|
|
126
|
+
grid-template-columns: minmax(0, 1fr);
|
|
118
127
|
transition: grid-template-rows 200ms ease;
|
|
119
128
|
}
|
|
120
129
|
|