@nyaruka/temba-components 0.157.0 → 0.157.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/temba-components.js +1450 -1370
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Button.ts +102 -121
- package/src/display/Chat.ts +60 -9
- package/src/display/Dropdown.ts +11 -0
- package/src/display/Label.ts +1 -3
- package/src/display/LeafletMap.ts +4 -3
- package/src/display/TembaUser.ts +9 -3
- package/src/events/eventRenderers.ts +152 -67
- package/src/flow/AutoTranslate.ts +2 -2
- package/src/flow/Editor.ts +4 -4
- package/src/flow/NodeEditor.ts +2 -2
- package/src/flow/NodeTypeSelector.ts +0 -5
- package/src/flow/actions/set_contact_language.ts +5 -4
- package/src/flow/nodes/split_by_llm_categorize.ts +1 -6
- package/src/flow/utils.ts +1 -20
- package/src/form/ColorPicker.ts +5 -3
- package/src/form/select/Omnibox.ts +1 -3
- package/src/form/select/Select.ts +5 -4
- package/src/interfaces.ts +1 -0
- package/src/languages.ts +56 -0
- package/src/layout/Dialog.ts +1 -3
- package/src/list/ContentMenu.ts +1 -2
- package/src/list/SortableList.ts +1 -4
- package/src/list/TembaMenu.ts +159 -113
- package/src/live/ContactBadges.ts +2 -1
- package/src/live/ContactChat.ts +16 -1
- package/src/live/ContactDetails.ts +2 -1
- package/src/live/ContactFieldEditor.ts +0 -2
- package/src/store/AppState.ts +3 -21
- package/src/store/Store.ts +0 -29
- package/src/styles/designTokens.ts +31 -18
- package/src/styles/pillVariants.ts +24 -13
- package/static/css/temba-components.css +82 -55
- package/web-dev-server.config.mjs +0 -1
- package/web-test-runner.config.mjs +0 -1
|
@@ -106,17 +106,46 @@ const flowPill = (flow: any) =>
|
|
|
106
106
|
|
|
107
107
|
const fieldPill = (field: any) => renderEntityPill('field', field.name);
|
|
108
108
|
|
|
109
|
+
const topicPill = (topic: any) =>
|
|
110
|
+
renderEntityPill('topic', topic.name, {
|
|
111
|
+
href: `/ticket/${topic.uuid}/open/`
|
|
112
|
+
});
|
|
113
|
+
|
|
109
114
|
/**
|
|
110
|
-
* Renders a
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
115
|
+
* Renders a user as a plain text link to the "All" ticket folder
|
|
116
|
+
* filtered by that assignee. Used for actor attribution in the
|
|
117
|
+
* chat history (ticket assigned / opened / closed events).
|
|
118
|
+
*
|
|
119
|
+
* The richer avatar-chip variant is parked in git history (see
|
|
120
|
+
* userPill) — we'll bring it back if denser surfaces need it.
|
|
114
121
|
*/
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
const userLink = (user: any): TemplateResult => {
|
|
123
|
+
const name =
|
|
124
|
+
user.name || [user.first_name, user.last_name].filter(Boolean).join(' ');
|
|
125
|
+
return html`<a
|
|
126
|
+
href="/ticket/all/open/?assignee=${user.uuid}"
|
|
127
|
+
onclick="goto(event, this)"
|
|
128
|
+
title=${name}
|
|
129
|
+
>${name}</a
|
|
119
130
|
>`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Renders a contact-data value (field text, name, URN, language,
|
|
135
|
+
* status, amount, etc.) as bold inline text, truncated with an
|
|
136
|
+
* ellipsis past a reasonable width. Pills are reserved for system-
|
|
137
|
+
* level objects (group, flow, field, topic, …) — these are just
|
|
138
|
+
* values, so the chip chrome would over-state them. The full value
|
|
139
|
+
* is exposed via `title` for hover inspection.
|
|
140
|
+
*/
|
|
141
|
+
const valueText = (value: string | number) => {
|
|
142
|
+
const str = String(value);
|
|
143
|
+
return html`<span
|
|
144
|
+
title=${str}
|
|
145
|
+
style="display: inline-block; max-width: 18em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; font-weight: 600;"
|
|
146
|
+
>${str}</span
|
|
147
|
+
>`;
|
|
148
|
+
};
|
|
120
149
|
|
|
121
150
|
/**
|
|
122
151
|
* Inline-flex wrapper style that text + pills share. Without it,
|
|
@@ -124,8 +153,13 @@ const valuePill = (value: string | number) =>
|
|
|
124
153
|
* pills sit slightly above and the text appears to "float". flex
|
|
125
154
|
* centering keeps the row of words and pills on one cross-axis.
|
|
126
155
|
*/
|
|
156
|
+
// min-height keeps the row a consistent height across renderers
|
|
157
|
+
// whether or not they contain a pill (which is ~22px tall) — without
|
|
158
|
+
// it, plain-text events like "Cleared language" sit a few pixels
|
|
159
|
+
// shorter than "Removed from <group pill>" and the chat history
|
|
160
|
+
// looks unevenly spaced.
|
|
127
161
|
const eventLineStyle =
|
|
128
|
-
'display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: 2px 4px;';
|
|
162
|
+
'display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: 2px 4px; min-height: 24px;';
|
|
129
163
|
|
|
130
164
|
const renderInfoList = (
|
|
131
165
|
singular: string,
|
|
@@ -180,9 +214,13 @@ export const renderChatStartedEvent = (
|
|
|
180
214
|
};
|
|
181
215
|
|
|
182
216
|
export const renderUpdateEvent = (event: UpdateFieldEvent): TemplateResult => {
|
|
183
|
-
|
|
217
|
+
// Treat both a missing value object and an empty-string text as a
|
|
218
|
+
// cleared field — backfill / reset payloads sometimes arrive as
|
|
219
|
+
// `value: { text: '' }`, which would otherwise render with an
|
|
220
|
+
// empty value pill.
|
|
221
|
+
return event.value && event.value.text
|
|
184
222
|
? html`<div style=${eventLineStyle}>
|
|
185
|
-
Updated ${fieldPill(event.field)} to ${
|
|
223
|
+
Updated ${fieldPill(event.field)} to ${valueText(event.value.text)}
|
|
186
224
|
</div>`
|
|
187
225
|
: html`<div style=${eventLineStyle}>
|
|
188
226
|
Cleared ${fieldPill(event.field)}
|
|
@@ -190,18 +228,24 @@ export const renderUpdateEvent = (event: UpdateFieldEvent): TemplateResult => {
|
|
|
190
228
|
};
|
|
191
229
|
|
|
192
230
|
export const renderNameChanged = (event: NameChangedEvent): TemplateResult => {
|
|
231
|
+
if (!event.name) {
|
|
232
|
+
return html`<div style=${eventLineStyle}>Cleared name</div>`;
|
|
233
|
+
}
|
|
193
234
|
return html`<div style=${eventLineStyle}>
|
|
194
|
-
Updated name to ${
|
|
235
|
+
Updated name to ${valueText(event.name)}
|
|
195
236
|
</div>`;
|
|
196
237
|
};
|
|
197
238
|
|
|
198
239
|
export const renderContactURNsChanged = (
|
|
199
240
|
event: URNsChangedEvent
|
|
200
241
|
): TemplateResult => {
|
|
242
|
+
if (!event.urns || event.urns.length === 0) {
|
|
243
|
+
return html`<div style=${eventLineStyle}>Cleared URNs</div>`;
|
|
244
|
+
}
|
|
201
245
|
return html`<div style=${eventLineStyle}>
|
|
202
246
|
Updated URNs to
|
|
203
247
|
${oxfordFn(event.urns, (urn: string) =>
|
|
204
|
-
|
|
248
|
+
valueText(urn.split(':')[1].split('?')[0])
|
|
205
249
|
)}
|
|
206
250
|
</div>`;
|
|
207
251
|
};
|
|
@@ -212,83 +256,110 @@ export const renderTicketAction = (
|
|
|
212
256
|
): TemplateResult => {
|
|
213
257
|
const ticketUUID = event.ticket?.uuid || event.ticket_uuid;
|
|
214
258
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<strong>${event._user ? event._user.name : 'Someone'}</strong> added a
|
|
221
|
-
note
|
|
222
|
-
<temba-date
|
|
223
|
-
value=${event.created_on.toISOString()}
|
|
224
|
-
display="relative"
|
|
225
|
-
></temba-date>
|
|
226
|
-
</div>
|
|
227
|
-
<div style="white-space: pre-wrap;">${event.note}</div>
|
|
228
|
-
</div>`
|
|
229
|
-
: null;
|
|
230
|
-
|
|
259
|
+
// Notes in the real chat history now go through Chat.ts#renderNote
|
|
260
|
+
// (see ContactChat.ts: ticket_note_added bypasses prerender). This
|
|
261
|
+
// path only runs for non-chat consumers (e.g. the flow Simulator).
|
|
262
|
+
// Render notes as a simple inline italic line — the chat-bubble
|
|
263
|
+
// styling lives in Chat.ts so the two can't drift.
|
|
231
264
|
if (action === 'noted') {
|
|
232
|
-
return
|
|
265
|
+
return event.note
|
|
266
|
+
? html`<div style="white-space: pre-wrap; font-style: italic;">
|
|
267
|
+
${event.note}
|
|
268
|
+
</div>`
|
|
269
|
+
: null;
|
|
233
270
|
}
|
|
234
271
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
272
|
+
// closed → ticket is in the closed folder now; reopened → it's
|
|
273
|
+
// back in open. Linking the word "ticket" lets a reader jump to
|
|
274
|
+
// wherever the ticket actually lives.
|
|
275
|
+
const folder = action === 'closed' ? 'closed' : 'open';
|
|
276
|
+
const href = `/ticket/all/${folder}/${ticketUUID}/`;
|
|
277
|
+
return event._user
|
|
278
|
+
? html`<div style=${eventLineStyle}>
|
|
279
|
+
${userLink(event._user)} ${action} a <a href=${href}>ticket</a>
|
|
239
280
|
</div>`
|
|
240
|
-
: html`<div>
|
|
241
|
-
A
|
|
242
|
-
<strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
|
|
243
|
-
was <strong>${action}</strong>
|
|
281
|
+
: html`<div style=${eventLineStyle}>
|
|
282
|
+
A <a href=${href}>ticket</a> was ${action}
|
|
244
283
|
</div>`;
|
|
245
|
-
|
|
246
|
-
return html`<div style="${actionNote ? 'margin-bottom: 1em;' : ''}">
|
|
247
|
-
${description}
|
|
248
|
-
</div>
|
|
249
|
-
${actionNote}`;
|
|
250
284
|
};
|
|
251
285
|
|
|
252
286
|
export const renderTicketAssigneeChanged = (
|
|
253
287
|
event: TicketEvent
|
|
254
288
|
): TemplateResult => {
|
|
289
|
+
const ticketUUID = event.ticket?.uuid || event.ticket_uuid;
|
|
290
|
+
// Link the word "ticket" in assignee events too, so the noun is
|
|
291
|
+
// consistently interactive across open / close / reopen / assigned
|
|
292
|
+
// rows (the contact-history page can show events from any of the
|
|
293
|
+
// contact's tickets, so the jump-to-ticket affordance is useful).
|
|
294
|
+
const ticketLink = html`<a href="/ticket/all/open/${ticketUUID}/"
|
|
295
|
+
>ticket</a
|
|
296
|
+
>`;
|
|
297
|
+
const ticketLinkCapitalized = html`<a href="/ticket/all/open/${ticketUUID}/"
|
|
298
|
+
>This ticket</a
|
|
299
|
+
>`;
|
|
255
300
|
if (event._user) {
|
|
256
301
|
if (event.assignee) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
302
|
+
// Self-assignment ("took the ticket") reads naturally as one
|
|
303
|
+
// user link + verb, rather than "<user> assigned to <same user>".
|
|
304
|
+
// Match on uuid when present, falling back to email — depending
|
|
305
|
+
// on the API surface a user payload may carry one or the other.
|
|
306
|
+
const sameUser =
|
|
307
|
+
(event._user.uuid && event._user.uuid === event.assignee.uuid) ||
|
|
308
|
+
(event._user.email && event._user.email === event.assignee.email);
|
|
309
|
+
if (sameUser) {
|
|
310
|
+
return html`<div style=${eventLineStyle}>
|
|
311
|
+
${userLink(event._user)} took this ${ticketLink}
|
|
312
|
+
</div>`;
|
|
313
|
+
}
|
|
314
|
+
return html`<div style=${eventLineStyle}>
|
|
315
|
+
${userLink(event._user)} assigned this ${ticketLink} to
|
|
316
|
+
${userLink(event.assignee)}
|
|
260
317
|
</div>`;
|
|
261
318
|
} else {
|
|
262
|
-
return html`<div>
|
|
263
|
-
|
|
319
|
+
return html`<div style=${eventLineStyle}>
|
|
320
|
+
${userLink(event._user)} unassigned this ${ticketLink}
|
|
264
321
|
</div>`;
|
|
265
322
|
}
|
|
266
323
|
} else {
|
|
267
324
|
if (event.assignee) {
|
|
268
|
-
return html`<div>
|
|
269
|
-
|
|
325
|
+
return html`<div style=${eventLineStyle}>
|
|
326
|
+
${ticketLinkCapitalized} was assigned to ${userLink(event.assignee)}
|
|
270
327
|
</div>`;
|
|
271
328
|
} else {
|
|
272
|
-
return html`<div>
|
|
329
|
+
return html`<div style=${eventLineStyle}>
|
|
330
|
+
${ticketLinkCapitalized} was unassigned
|
|
331
|
+
</div>`;
|
|
273
332
|
}
|
|
274
333
|
}
|
|
275
334
|
};
|
|
276
335
|
|
|
277
336
|
export const renderTicketOpened = (event: TicketEvent): TemplateResult => {
|
|
278
|
-
|
|
337
|
+
const ticketUUID = event.ticket.uuid;
|
|
338
|
+
const href = `/ticket/all/open/${ticketUUID}/`;
|
|
339
|
+
// ticket.topic is optional in events.ts — guard so a payload
|
|
340
|
+
// without one degrades to "A ticket was opened" rather than
|
|
341
|
+
// throwing inside topicPill.
|
|
342
|
+
const topic = event.ticket.topic;
|
|
343
|
+
const tail = topic ? html` in ${topicPill(topic)}` : null;
|
|
344
|
+
return event._user
|
|
345
|
+
? html`<div style=${eventLineStyle}>
|
|
346
|
+
${userLink(event._user)} opened a <a href=${href}>ticket</a>${tail}
|
|
347
|
+
</div>`
|
|
348
|
+
: html`<div style=${eventLineStyle}>
|
|
349
|
+
A <a href=${href}>ticket</a> was opened${tail}
|
|
350
|
+
</div>`;
|
|
279
351
|
};
|
|
280
352
|
|
|
281
353
|
export const renderContactGroupsEvent = (
|
|
282
354
|
event: ContactGroupsEvent
|
|
283
355
|
): TemplateResult => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} else if (groupsEvent.groups_removed) {
|
|
356
|
+
if (event.groups_added) {
|
|
357
|
+
return renderInfoList('Added to', 'Added to', event.groups_added);
|
|
358
|
+
} else if (event.groups_removed) {
|
|
288
359
|
return renderInfoList(
|
|
289
360
|
'Removed from',
|
|
290
361
|
'Removed from',
|
|
291
|
-
|
|
362
|
+
event.groups_removed
|
|
292
363
|
);
|
|
293
364
|
}
|
|
294
365
|
};
|
|
@@ -299,23 +370,31 @@ export const renderAirtimeTransferredEvent = (
|
|
|
299
370
|
if (parseFloat(event.amount) === 0) {
|
|
300
371
|
return html`<div>Airtime transfer failed</div>`;
|
|
301
372
|
}
|
|
302
|
-
return html`<div>
|
|
303
|
-
Transferred
|
|
373
|
+
return html`<div style=${eventLineStyle}>
|
|
374
|
+
Transferred ${valueText(event.amount)} ${event.currency} of airtime
|
|
304
375
|
</div>`;
|
|
305
376
|
};
|
|
306
377
|
|
|
307
378
|
export const renderContactLanguageChangedEvent = (
|
|
308
379
|
event: ContactLanguageChangedEvent
|
|
309
380
|
): TemplateResult => {
|
|
310
|
-
|
|
311
|
-
|
|
381
|
+
if (!event.language) {
|
|
382
|
+
return html`<div style=${eventLineStyle}>Cleared language</div>`;
|
|
383
|
+
}
|
|
384
|
+
return html`<div style=${eventLineStyle}>
|
|
385
|
+
Language updated to ${valueText(event.language)}
|
|
312
386
|
</div>`;
|
|
313
387
|
};
|
|
314
388
|
|
|
315
389
|
export const renderContactStatusChangedEvent = (
|
|
316
390
|
event: ContactStatusChangedEvent
|
|
317
391
|
): TemplateResult => {
|
|
318
|
-
|
|
392
|
+
if (!event.status) {
|
|
393
|
+
return html`<div style=${eventLineStyle}>Cleared status</div>`;
|
|
394
|
+
}
|
|
395
|
+
return html`<div style=${eventLineStyle}>
|
|
396
|
+
Status updated to ${valueText(event.status)}
|
|
397
|
+
</div>`;
|
|
319
398
|
};
|
|
320
399
|
|
|
321
400
|
export const renderCallEvent = (event: CallEvent): TemplateResult => {
|
|
@@ -561,7 +640,7 @@ export const renderEvent = (
|
|
|
561
640
|
content = renderTicketAction(event as TicketEvent, 'closed');
|
|
562
641
|
break;
|
|
563
642
|
case Events.TICKET_OPENED:
|
|
564
|
-
content =
|
|
643
|
+
content = renderTicketOpened(event as TicketEvent);
|
|
565
644
|
break;
|
|
566
645
|
case Events.TICKET_NOTE_ADDED:
|
|
567
646
|
content = renderTicketAction(event as TicketEvent, 'noted');
|
|
@@ -569,11 +648,17 @@ export const renderEvent = (
|
|
|
569
648
|
case Events.TICKET_REOPENED:
|
|
570
649
|
content = renderTicketAction(event as TicketEvent, 'reopened');
|
|
571
650
|
break;
|
|
572
|
-
case Events.TICKET_TOPIC_CHANGED:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
651
|
+
case Events.TICKET_TOPIC_CHANGED: {
|
|
652
|
+
// event.topic is optional — guard so a payload without one
|
|
653
|
+
// degrades cleanly instead of throwing inside topicPill.
|
|
654
|
+
const newTopic = (event as TicketEvent).topic;
|
|
655
|
+
content = newTopic
|
|
656
|
+
? html`<div style=${eventLineStyle}>
|
|
657
|
+
Topic changed to ${topicPill(newTopic)}
|
|
658
|
+
</div>`
|
|
659
|
+
: null;
|
|
576
660
|
break;
|
|
661
|
+
}
|
|
577
662
|
default:
|
|
578
663
|
return null;
|
|
579
664
|
}
|
|
@@ -6,7 +6,7 @@ import { getStore } from '../store/Store';
|
|
|
6
6
|
import { AppState, fromStore, zustand } from '../store/AppState';
|
|
7
7
|
import { FlowDefinition } from '../store/flow-definition';
|
|
8
8
|
import { TranslationEntry, buildTranslationBundles } from './flow-translations';
|
|
9
|
-
import {
|
|
9
|
+
import { getLanguageName } from '../languages';
|
|
10
10
|
import { LLMModel, hasLLMRole } from './flow-utils';
|
|
11
11
|
|
|
12
12
|
interface TranslationModel {
|
|
@@ -701,7 +701,7 @@ export class AutoTranslate extends RapidElement {
|
|
|
701
701
|
}
|
|
702
702
|
|
|
703
703
|
const selected = this.selectedModel ? [this.selectedModel] : [];
|
|
704
|
-
const languageName =
|
|
704
|
+
const languageName = getLanguageName(this.languageCode);
|
|
705
705
|
const aiClause = this.brand
|
|
706
706
|
? html`${this.brand} uses AI for automatic translation, which can make
|
|
707
707
|
mistakes,`
|
package/src/flow/Editor.ts
CHANGED
|
@@ -22,12 +22,12 @@ import {
|
|
|
22
22
|
} from '../utils';
|
|
23
23
|
import { TEMBA_COMPONENTS_VERSION } from '../version';
|
|
24
24
|
import {
|
|
25
|
-
getLanguageDisplayName,
|
|
26
25
|
getNodeBounds,
|
|
27
26
|
calculateReflowPositions,
|
|
28
27
|
NodeBounds,
|
|
29
28
|
snapToGrid
|
|
30
29
|
} from './utils';
|
|
30
|
+
import { getLanguageName } from '../languages';
|
|
31
31
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
32
32
|
import { PRIMARY_LANGUAGE_OPTION_VALUE } from './EditorToolbar';
|
|
33
33
|
import {
|
|
@@ -1711,7 +1711,7 @@ export class Editor extends RapidElement {
|
|
|
1711
1711
|
// Use languages from workspace if available
|
|
1712
1712
|
if (this.workspace?.languages && this.workspace.languages.length > 0) {
|
|
1713
1713
|
return this.workspace.languages
|
|
1714
|
-
.map((code) => ({ code, name:
|
|
1714
|
+
.map((code) => ({ code, name: getLanguageName(code) }))
|
|
1715
1715
|
.filter((lang) => lang.code && lang.name);
|
|
1716
1716
|
}
|
|
1717
1717
|
|
|
@@ -1722,7 +1722,7 @@ export class Editor extends RapidElement {
|
|
|
1722
1722
|
) {
|
|
1723
1723
|
return this.definition._ui.languages.map((lang: any) => {
|
|
1724
1724
|
const code = typeof lang === 'string' ? lang : lang.iso || lang.code;
|
|
1725
|
-
return { code, name:
|
|
1725
|
+
return { code, name: getLanguageName(code) };
|
|
1726
1726
|
});
|
|
1727
1727
|
}
|
|
1728
1728
|
|
|
@@ -3644,7 +3644,7 @@ export class Editor extends RapidElement {
|
|
|
3644
3644
|
const baseLanguage = this.definition?.language;
|
|
3645
3645
|
const baseLanguageName =
|
|
3646
3646
|
availableLanguages.find((lang) => lang.code === baseLanguage)?.name ||
|
|
3647
|
-
(baseLanguage ?
|
|
3647
|
+
(baseLanguage ? getLanguageName(baseLanguage) : '') ||
|
|
3648
3648
|
'Primary language';
|
|
3649
3649
|
const isBaseSelected =
|
|
3650
3650
|
!this.languageCode ||
|
package/src/flow/NodeEditor.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
fromStore,
|
|
38
38
|
zustand
|
|
39
39
|
} from '../store/AppState';
|
|
40
|
-
import {
|
|
40
|
+
import { getLanguageName } from '../languages';
|
|
41
41
|
|
|
42
42
|
export class NodeEditor extends RapidElement {
|
|
43
43
|
static get styles() {
|
|
@@ -2656,7 +2656,7 @@ export class NodeEditor extends RapidElement {
|
|
|
2656
2656
|
const dialogSize = config?.dialogSize || 'medium'; // Default to 'large' if not specified
|
|
2657
2657
|
|
|
2658
2658
|
const languageName = this.isTranslating
|
|
2659
|
-
?
|
|
2659
|
+
? getLanguageName(this.languageCode)
|
|
2660
2660
|
: '';
|
|
2661
2661
|
|
|
2662
2662
|
let header = config?.name || 'Edit';
|
|
@@ -213,11 +213,6 @@ export class NodeTypeSelector extends RapidElement {
|
|
|
213
213
|
background: rgba(0, 0, 0, 0.03);
|
|
214
214
|
border-radius: 0 0 var(--curvature) var(--curvature);
|
|
215
215
|
}
|
|
216
|
-
|
|
217
|
-
temba-button {
|
|
218
|
-
--button-y: 0.5em;
|
|
219
|
-
--button-x: 1.25em;
|
|
220
|
-
}
|
|
221
216
|
`;
|
|
222
217
|
}
|
|
223
218
|
|
|
@@ -8,14 +8,15 @@ import {
|
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { Node, SetContactLanguage } from '../../store/flow-definition';
|
|
10
10
|
import { getStore } from '../../store/Store';
|
|
11
|
-
import {
|
|
11
|
+
import { renderClamped } from '../utils';
|
|
12
|
+
import { getLanguageName } from '../../languages';
|
|
12
13
|
|
|
13
14
|
export const set_contact_language: ActionConfig = {
|
|
14
15
|
name: 'Update Language',
|
|
15
16
|
group: ACTION_GROUPS.contacts,
|
|
16
17
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
17
18
|
render: (_node: Node, action: SetContactLanguage) => {
|
|
18
|
-
const name =
|
|
19
|
+
const name = getLanguageName(action.language);
|
|
19
20
|
return renderClamped(
|
|
20
21
|
html`Set to <strong>${name}</strong>`,
|
|
21
22
|
`Set to ${name}`
|
|
@@ -37,7 +38,7 @@ export const set_contact_language: ActionConfig = {
|
|
|
37
38
|
if (workspace?.languages && Array.isArray(workspace.languages)) {
|
|
38
39
|
return workspace.languages.map((languageCode: string) => ({
|
|
39
40
|
value: languageCode,
|
|
40
|
-
name:
|
|
41
|
+
name: getLanguageName(languageCode)
|
|
41
42
|
}));
|
|
42
43
|
}
|
|
43
44
|
return [];
|
|
@@ -51,7 +52,7 @@ export const set_contact_language: ActionConfig = {
|
|
|
51
52
|
language: [
|
|
52
53
|
{
|
|
53
54
|
value: action.language,
|
|
54
|
-
name:
|
|
55
|
+
name: getLanguageName(action.language)
|
|
55
56
|
}
|
|
56
57
|
],
|
|
57
58
|
uuid: action.uuid
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FormData,
|
|
3
|
-
NodeConfig,
|
|
4
|
-
ACTION_GROUPS,
|
|
5
|
-
FlowTypes
|
|
6
|
-
} from '../types';
|
|
1
|
+
import { FormData, NodeConfig, ACTION_GROUPS, FlowTypes } from '../types';
|
|
7
2
|
import { CallLLM, Node } from '../../store/flow-definition';
|
|
8
3
|
import { generateUUID, createMultiCategoryRouter } from '../../utils';
|
|
9
4
|
import { html } from 'lit';
|
package/src/flow/utils.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html, TemplateResult } from 'lit-html';
|
|
2
2
|
import { iconToPillType } from '../styles/pillVariants';
|
|
3
3
|
import { Action, NamedObject, FlowPosition } from '../store/flow-definition';
|
|
4
|
-
import { FlowIssue
|
|
4
|
+
import { FlowIssue } from '../store/AppState';
|
|
5
5
|
import { CustomEventType } from '../interfaces';
|
|
6
6
|
import { tokenize, TokenType } from '../excellent/tokenizer';
|
|
7
7
|
import { TOKEN_COLORS } from '../excellent/token-styles';
|
|
@@ -107,25 +107,6 @@ export function resolveFromLocalizationFormData(
|
|
|
107
107
|
return undefined;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
const intlLanguageNames = new Intl.DisplayNames(['en'], { type: 'language' });
|
|
111
|
-
|
|
112
|
-
export function getLanguageDisplayName(code: string): string {
|
|
113
|
-
if (code === 'und') return 'Unknown';
|
|
114
|
-
|
|
115
|
-
// Prefer names from the RapidPro languages endpoint, which supplies
|
|
116
|
-
// ISO 639-3 codes (e.g. prd, pst) that Intl.DisplayNames doesn't cover.
|
|
117
|
-
const storeName = zustand.getState().languageNames?.[code];
|
|
118
|
-
if (storeName) {
|
|
119
|
-
return storeName;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
return intlLanguageNames.of(code) || code;
|
|
124
|
-
} catch {
|
|
125
|
-
return code;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
110
|
const IS_MAC =
|
|
130
111
|
typeof navigator !== 'undefined' &&
|
|
131
112
|
/Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
package/src/form/ColorPicker.ts
CHANGED
|
@@ -29,7 +29,6 @@ export class ColorPicker extends FieldElement {
|
|
|
29
29
|
:host {
|
|
30
30
|
color: var(--color-text);
|
|
31
31
|
display: inline-block;
|
|
32
|
-
--curvature: 0.55em;
|
|
33
32
|
width: 100%;
|
|
34
33
|
|
|
35
34
|
--temba-textinput-padding: 0.4em;
|
|
@@ -37,13 +36,16 @@ export class ColorPicker extends FieldElement {
|
|
|
37
36
|
|
|
38
37
|
temba-textinput {
|
|
39
38
|
margin-left: 0.3em;
|
|
40
|
-
width:
|
|
39
|
+
width: 7em;
|
|
40
|
+
--temba-textinput-min-height: 0;
|
|
41
|
+
--temba-textinput-padding: 4px 8px;
|
|
42
|
+
--temba-textinput-font-size: 12.5px;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
.wrapper {
|
|
44
46
|
border: 1px solid var(--color-widget-border);
|
|
45
47
|
padding: calc(var(--curvature) / 2);
|
|
46
|
-
border-radius:
|
|
48
|
+
border-radius: var(--curvature);
|
|
47
49
|
transition: all calc(var(--transition-speed) * 2) var(--bounce);
|
|
48
50
|
|
|
49
51
|
display: flex;
|
|
@@ -120,9 +120,7 @@ export class Omnibox extends Select<OmniOption> {
|
|
|
120
120
|
option.count !== undefined &&
|
|
121
121
|
option.count !== null
|
|
122
122
|
) {
|
|
123
|
-
return html`<div
|
|
124
|
-
style="display:flex; align-items:center; gap:6px;"
|
|
125
|
-
>
|
|
123
|
+
return html`<div style="display:flex; align-items:center; gap:6px;">
|
|
126
124
|
${base}<span
|
|
127
125
|
style="opacity:0.7; font-size:11px; font-variant-numeric: tabular-nums; font-weight: var(--w-medium);"
|
|
128
126
|
>${option.count.toLocaleString()}</span
|
|
@@ -74,7 +74,8 @@ const ENDPOINT_PILL_TYPES: { pattern: RegExp; type: string }[] = [
|
|
|
74
74
|
{ pattern: /\/contacts(\.json|\/|\?|$)/, type: 'contact' },
|
|
75
75
|
{ pattern: /\/labels(\.json|\/|\?|$)/, type: 'label' },
|
|
76
76
|
{ pattern: /\/flows(\.json|\/|\?|$)/, type: 'flow' },
|
|
77
|
-
{ pattern: /\/fields(\.json|\/|\?|$)/, type: 'field' }
|
|
77
|
+
{ pattern: /\/fields(\.json|\/|\?|$)/, type: 'field' },
|
|
78
|
+
{ pattern: /\/topics(\.json|\/|\?|$)/, type: 'topic' }
|
|
78
79
|
];
|
|
79
80
|
|
|
80
81
|
export class Select<T extends SelectOption> extends FieldElement {
|
|
@@ -2204,9 +2205,9 @@ export class Select<T extends SelectOption> extends FieldElement {
|
|
|
2204
2205
|
class="option-name"
|
|
2205
2206
|
style="display:flex; align-items:center; gap:6px;"
|
|
2206
2207
|
>
|
|
2207
|
-
${icon
|
|
2208
|
-
|
|
2209
|
-
|
|
2208
|
+
${icon ? html`<temba-icon name="${icon}"></temba-icon>` : null}<span
|
|
2209
|
+
>${this.renderHighlightedName(option)}</span
|
|
2210
|
+
>
|
|
2210
2211
|
</div>
|
|
2211
2212
|
`;
|
|
2212
2213
|
}
|
package/src/interfaces.ts
CHANGED
package/src/languages.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const intlLanguageNames = new Intl.DisplayNames(['en'], {
|
|
2
|
+
type: 'language',
|
|
3
|
+
fallback: 'none'
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
const ADDITIONAL_LANGUAGE_NAMES: { [code: string]: string } = {
|
|
7
|
+
aab: 'Alumu-Tesu',
|
|
8
|
+
aac: 'Ari',
|
|
9
|
+
aas: 'Aasáx',
|
|
10
|
+
abp: 'Abellen Ayta',
|
|
11
|
+
acf: 'Saint Lucian Creole French',
|
|
12
|
+
aec: 'Saidi Arabic',
|
|
13
|
+
afb: 'Gulf Arabic',
|
|
14
|
+
apd: 'Sudanese Arabic',
|
|
15
|
+
ayl: 'Libyan Arabic',
|
|
16
|
+
blk: "Pa'o Karen",
|
|
17
|
+
bog: 'Bamako Sign Language',
|
|
18
|
+
bzs: 'Brazilian Sign Language',
|
|
19
|
+
csn: 'Colombian Sign Language',
|
|
20
|
+
dag: 'Dagbani',
|
|
21
|
+
ecs: 'Ecuadorian Sign Language',
|
|
22
|
+
frk: 'Frankish',
|
|
23
|
+
fsl: 'French Sign Language',
|
|
24
|
+
fuv: 'Nigerian Fulfulde',
|
|
25
|
+
gcr: 'Guianese Creole French',
|
|
26
|
+
gpe: 'Ghanaian Pidgin English',
|
|
27
|
+
gux: 'Gourmanchéma',
|
|
28
|
+
ise: 'Italian Sign Language',
|
|
29
|
+
ksw: "S'gaw Karen",
|
|
30
|
+
kun: 'Kunama',
|
|
31
|
+
kyu: 'Western Kayah',
|
|
32
|
+
laj: 'Lango',
|
|
33
|
+
nyj: 'Nyanga',
|
|
34
|
+
prd: 'Parsi-Dari',
|
|
35
|
+
prl: 'Peruvian Sign Language',
|
|
36
|
+
pst: 'Central Pashto',
|
|
37
|
+
rop: 'Kriol',
|
|
38
|
+
tdt: 'Tetun Dili',
|
|
39
|
+
toi: 'Tonga',
|
|
40
|
+
tuv: 'Turkana',
|
|
41
|
+
vsl: 'Venezuelan Sign Language'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function getLanguageName(code: string): string {
|
|
45
|
+
if (!code) return '';
|
|
46
|
+
if (code === 'und') return 'Unknown';
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const name = intlLanguageNames.of(code);
|
|
50
|
+
if (name) return name;
|
|
51
|
+
} catch {
|
|
52
|
+
// fall through to additional lookup
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return ADDITIONAL_LANGUAGE_NAMES[code] || code;
|
|
56
|
+
}
|
package/src/layout/Dialog.ts
CHANGED
|
@@ -181,8 +181,6 @@ export class Dialog extends ResizeElement {
|
|
|
181
181
|
|
|
182
182
|
temba-button {
|
|
183
183
|
margin-left: 10px;
|
|
184
|
-
--button-y: 0.4em;
|
|
185
|
-
--button-x: 1em;
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
.dialog-body temba-loading {
|
|
@@ -555,7 +553,7 @@ export class Dialog extends ResizeElement {
|
|
|
555
553
|
?destructive=${button.type == 'primary' && this.destructive}
|
|
556
554
|
?primary=${button.type == 'primary' && !this.destructive}
|
|
557
555
|
?secondary=${button.type == 'secondary'}
|
|
558
|
-
?submitting=${this.submitting}
|
|
556
|
+
?submitting=${this.submitting && button.type == 'primary'}
|
|
559
557
|
?disabled=${this.disabled && !button.closes}
|
|
560
558
|
index=${index}
|
|
561
559
|
@click=${this.handleClick}
|