@nyaruka/temba-components 0.157.0 → 0.158.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 +16 -0
- package/dist/temba-components.js +1617 -1590
- package/dist/temba-components.js.map +1 -1
- package/orca/setup.sh +81 -0
- package/orca.yaml +3 -0
- 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 +151 -71
- package/src/flow/AutoTranslate.ts +2 -2
- package/src/flow/CanvasNode.ts +14 -6
- package/src/flow/DragManager.ts +4 -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 +2 -20
- package/src/form/ColorPicker.ts +5 -3
- package/src/form/DatePicker.ts +2 -1
- 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/layout/Tab.ts +0 -15
- package/src/layout/TabPane.ts +73 -163
- 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 +22 -3
- package/src/live/ContactDetails.ts +42 -36
- package/src/live/ContactFieldEditor.ts +35 -57
- package/src/live/ContactFields.ts +1 -2
- package/src/live/ContactNotepad.ts +9 -1
- package/src/live/ContactPending.ts +1 -0
- package/src/store/AppState.ts +3 -21
- package/src/store/Store.ts +0 -29
- package/src/styles/designTokens.ts +33 -18
- package/src/styles/pillVariants.ts +24 -13
- package/static/css/temba-components.css +84 -55
- package/web-dev-server.config.mjs +0 -1
- package/web-test-runner.config.mjs +0 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
UpdateFieldEvent,
|
|
14
14
|
URNsChangedEvent
|
|
15
15
|
} from '../events';
|
|
16
|
+
import { getLanguageName } from '../languages';
|
|
16
17
|
import { oxfordFn } from '../utils';
|
|
17
18
|
|
|
18
19
|
export enum Events {
|
|
@@ -106,17 +107,46 @@ const flowPill = (flow: any) =>
|
|
|
106
107
|
|
|
107
108
|
const fieldPill = (field: any) => renderEntityPill('field', field.name);
|
|
108
109
|
|
|
110
|
+
const topicPill = (topic: any) =>
|
|
111
|
+
renderEntityPill('topic', topic.name, {
|
|
112
|
+
href: `/ticket/${topic.uuid}/open/`
|
|
113
|
+
});
|
|
114
|
+
|
|
109
115
|
/**
|
|
110
|
-
* Renders a
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
116
|
+
* Renders a user as a plain text link to the "All" ticket folder
|
|
117
|
+
* filtered by that assignee. Used for actor attribution in the
|
|
118
|
+
* chat history (ticket assigned / opened / closed events).
|
|
119
|
+
*
|
|
120
|
+
* The richer avatar-chip variant is parked in git history (see
|
|
121
|
+
* userPill) — we'll bring it back if denser surfaces need it.
|
|
114
122
|
*/
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
const userLink = (user: any): TemplateResult => {
|
|
124
|
+
const name =
|
|
125
|
+
user.name || [user.first_name, user.last_name].filter(Boolean).join(' ');
|
|
126
|
+
return html`<a
|
|
127
|
+
href="/ticket/all/open/?assignee=${user.uuid}"
|
|
128
|
+
onclick="goto(event, this)"
|
|
129
|
+
title=${name}
|
|
130
|
+
>${name}</a
|
|
119
131
|
>`;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Renders a contact-data value (field text, name, URN, language,
|
|
136
|
+
* status, amount, etc.) as bold inline text, truncated with an
|
|
137
|
+
* ellipsis past a reasonable width. Pills are reserved for system-
|
|
138
|
+
* level objects (group, flow, field, topic, …) — these are just
|
|
139
|
+
* values, so the chip chrome would over-state them. The full value
|
|
140
|
+
* is exposed via `title` for hover inspection.
|
|
141
|
+
*/
|
|
142
|
+
const valueText = (value: string | number) => {
|
|
143
|
+
const str = String(value);
|
|
144
|
+
return html`<span
|
|
145
|
+
title=${str}
|
|
146
|
+
style="display: inline-block; max-width: 18em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; font-weight: 600;"
|
|
147
|
+
>${str}</span
|
|
148
|
+
>`;
|
|
149
|
+
};
|
|
120
150
|
|
|
121
151
|
/**
|
|
122
152
|
* Inline-flex wrapper style that text + pills share. Without it,
|
|
@@ -124,8 +154,13 @@ const valuePill = (value: string | number) =>
|
|
|
124
154
|
* pills sit slightly above and the text appears to "float". flex
|
|
125
155
|
* centering keeps the row of words and pills on one cross-axis.
|
|
126
156
|
*/
|
|
157
|
+
// min-height keeps the row a consistent height across renderers
|
|
158
|
+
// whether or not they contain a pill (which is ~22px tall) — without
|
|
159
|
+
// it, plain-text events like "Cleared language" sit a few pixels
|
|
160
|
+
// shorter than "Removed from <group pill>" and the chat history
|
|
161
|
+
// looks unevenly spaced.
|
|
127
162
|
const eventLineStyle =
|
|
128
|
-
'display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: 2px 4px;';
|
|
163
|
+
'display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: 2px 4px; min-height: 24px;';
|
|
129
164
|
|
|
130
165
|
const renderInfoList = (
|
|
131
166
|
singular: string,
|
|
@@ -180,9 +215,13 @@ export const renderChatStartedEvent = (
|
|
|
180
215
|
};
|
|
181
216
|
|
|
182
217
|
export const renderUpdateEvent = (event: UpdateFieldEvent): TemplateResult => {
|
|
183
|
-
|
|
218
|
+
// Treat both a missing value object and an empty-string text as a
|
|
219
|
+
// cleared field — backfill / reset payloads sometimes arrive as
|
|
220
|
+
// `value: { text: '' }`, which would otherwise render with an
|
|
221
|
+
// empty value pill.
|
|
222
|
+
return event.value && event.value.text
|
|
184
223
|
? html`<div style=${eventLineStyle}>
|
|
185
|
-
Updated ${fieldPill(event.field)} to ${
|
|
224
|
+
Updated ${fieldPill(event.field)} to ${valueText(event.value.text)}
|
|
186
225
|
</div>`
|
|
187
226
|
: html`<div style=${eventLineStyle}>
|
|
188
227
|
Cleared ${fieldPill(event.field)}
|
|
@@ -190,18 +229,24 @@ export const renderUpdateEvent = (event: UpdateFieldEvent): TemplateResult => {
|
|
|
190
229
|
};
|
|
191
230
|
|
|
192
231
|
export const renderNameChanged = (event: NameChangedEvent): TemplateResult => {
|
|
232
|
+
if (!event.name) {
|
|
233
|
+
return html`<div style=${eventLineStyle}>Cleared name</div>`;
|
|
234
|
+
}
|
|
193
235
|
return html`<div style=${eventLineStyle}>
|
|
194
|
-
Updated name to ${
|
|
236
|
+
Updated name to ${valueText(event.name)}
|
|
195
237
|
</div>`;
|
|
196
238
|
};
|
|
197
239
|
|
|
198
240
|
export const renderContactURNsChanged = (
|
|
199
241
|
event: URNsChangedEvent
|
|
200
242
|
): TemplateResult => {
|
|
243
|
+
if (!event.urns || event.urns.length === 0) {
|
|
244
|
+
return html`<div style=${eventLineStyle}>Cleared URNs</div>`;
|
|
245
|
+
}
|
|
201
246
|
return html`<div style=${eventLineStyle}>
|
|
202
247
|
Updated URNs to
|
|
203
248
|
${oxfordFn(event.urns, (urn: string) =>
|
|
204
|
-
|
|
249
|
+
valueText(urn.split(':')[1].split('?')[0])
|
|
205
250
|
)}
|
|
206
251
|
</div>`;
|
|
207
252
|
};
|
|
@@ -212,84 +257,105 @@ export const renderTicketAction = (
|
|
|
212
257
|
): TemplateResult => {
|
|
213
258
|
const ticketUUID = event.ticket?.uuid || event.ticket_uuid;
|
|
214
259
|
|
|
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
|
-
|
|
260
|
+
// Notes in the real chat history now go through Chat.ts#renderNote
|
|
261
|
+
// (see ContactChat.ts: ticket_note_added bypasses prerender). This
|
|
262
|
+
// path only runs for non-chat consumers (e.g. the flow Simulator).
|
|
263
|
+
// Render notes as a simple inline italic line — the chat-bubble
|
|
264
|
+
// styling lives in Chat.ts so the two can't drift.
|
|
231
265
|
if (action === 'noted') {
|
|
232
|
-
return
|
|
266
|
+
return event.note
|
|
267
|
+
? html`<div style="white-space: pre-wrap; font-style: italic;">
|
|
268
|
+
${event.note}
|
|
269
|
+
</div>`
|
|
270
|
+
: null;
|
|
233
271
|
}
|
|
234
272
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
273
|
+
// closed → ticket is in the closed folder now; reopened → it's
|
|
274
|
+
// back in open. Linking the word "ticket" lets a reader jump to
|
|
275
|
+
// wherever the ticket actually lives.
|
|
276
|
+
const folder = action === 'closed' ? 'closed' : 'open';
|
|
277
|
+
const href = `/ticket/all/${folder}/${ticketUUID}/`;
|
|
278
|
+
return event._user
|
|
279
|
+
? html`<div style=${eventLineStyle}>
|
|
280
|
+
${userLink(event._user)} ${action} a <a href=${href}>ticket</a>
|
|
239
281
|
</div>`
|
|
240
|
-
: html`<div>
|
|
241
|
-
A
|
|
242
|
-
<strong><a href="/ticket/all/closed/${ticketUUID}/">ticket</a></strong>
|
|
243
|
-
was <strong>${action}</strong>
|
|
282
|
+
: html`<div style=${eventLineStyle}>
|
|
283
|
+
A <a href=${href}>ticket</a> was ${action}
|
|
244
284
|
</div>`;
|
|
245
|
-
|
|
246
|
-
return html`<div style="${actionNote ? 'margin-bottom: 1em;' : ''}">
|
|
247
|
-
${description}
|
|
248
|
-
</div>
|
|
249
|
-
${actionNote}`;
|
|
250
285
|
};
|
|
251
286
|
|
|
252
287
|
export const renderTicketAssigneeChanged = (
|
|
253
288
|
event: TicketEvent
|
|
254
289
|
): TemplateResult => {
|
|
290
|
+
const ticketUUID = event.ticket?.uuid || event.ticket_uuid;
|
|
291
|
+
// Link the word "ticket" in assignee events too, so the noun is
|
|
292
|
+
// consistently interactive across open / close / reopen / assigned
|
|
293
|
+
// rows (the contact-history page can show events from any of the
|
|
294
|
+
// contact's tickets, so the jump-to-ticket affordance is useful).
|
|
295
|
+
const ticketLink = html`<a href="/ticket/all/open/${ticketUUID}/">ticket</a>`;
|
|
296
|
+
const ticketLinkCapitalized = html`<a href="/ticket/all/open/${ticketUUID}/"
|
|
297
|
+
>This ticket</a
|
|
298
|
+
>`;
|
|
255
299
|
if (event._user) {
|
|
256
300
|
if (event.assignee) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
301
|
+
// Self-assignment ("took the ticket") reads naturally as one
|
|
302
|
+
// user link + verb, rather than "<user> assigned to <same user>".
|
|
303
|
+
// Match on uuid when present, falling back to email — depending
|
|
304
|
+
// on the API surface a user payload may carry one or the other.
|
|
305
|
+
const sameUser =
|
|
306
|
+
(event._user.uuid && event._user.uuid === event.assignee.uuid) ||
|
|
307
|
+
(event._user.email && event._user.email === event.assignee.email);
|
|
308
|
+
if (sameUser) {
|
|
309
|
+
return html`<div style=${eventLineStyle}>
|
|
310
|
+
${userLink(event._user)} took this ${ticketLink}
|
|
311
|
+
</div>`;
|
|
312
|
+
}
|
|
313
|
+
return html`<div style=${eventLineStyle}>
|
|
314
|
+
${userLink(event._user)} assigned this ${ticketLink} to
|
|
315
|
+
${userLink(event.assignee)}
|
|
260
316
|
</div>`;
|
|
261
317
|
} else {
|
|
262
|
-
return html`<div>
|
|
263
|
-
|
|
318
|
+
return html`<div style=${eventLineStyle}>
|
|
319
|
+
${userLink(event._user)} unassigned this ${ticketLink}
|
|
264
320
|
</div>`;
|
|
265
321
|
}
|
|
266
322
|
} else {
|
|
267
323
|
if (event.assignee) {
|
|
268
|
-
return html`<div>
|
|
269
|
-
|
|
324
|
+
return html`<div style=${eventLineStyle}>
|
|
325
|
+
${ticketLinkCapitalized} was assigned to ${userLink(event.assignee)}
|
|
270
326
|
</div>`;
|
|
271
327
|
} else {
|
|
272
|
-
return html`<div>
|
|
328
|
+
return html`<div style=${eventLineStyle}>
|
|
329
|
+
${ticketLinkCapitalized} was unassigned
|
|
330
|
+
</div>`;
|
|
273
331
|
}
|
|
274
332
|
}
|
|
275
333
|
};
|
|
276
334
|
|
|
277
335
|
export const renderTicketOpened = (event: TicketEvent): TemplateResult => {
|
|
278
|
-
|
|
336
|
+
const ticketUUID = event.ticket.uuid;
|
|
337
|
+
const href = `/ticket/all/open/${ticketUUID}/`;
|
|
338
|
+
// ticket.topic is optional in events.ts — guard so a payload
|
|
339
|
+
// without one degrades to "A ticket was opened" rather than
|
|
340
|
+
// throwing inside topicPill.
|
|
341
|
+
const topic = event.ticket.topic;
|
|
342
|
+
const tail = topic ? html` in ${topicPill(topic)}` : null;
|
|
343
|
+
return event._user
|
|
344
|
+
? html`<div style=${eventLineStyle}>
|
|
345
|
+
${userLink(event._user)} opened a <a href=${href}>ticket</a>${tail}
|
|
346
|
+
</div>`
|
|
347
|
+
: html`<div style=${eventLineStyle}>
|
|
348
|
+
A <a href=${href}>ticket</a> was opened${tail}
|
|
349
|
+
</div>`;
|
|
279
350
|
};
|
|
280
351
|
|
|
281
352
|
export const renderContactGroupsEvent = (
|
|
282
353
|
event: ContactGroupsEvent
|
|
283
354
|
): TemplateResult => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return renderInfoList(
|
|
289
|
-
'Removed from',
|
|
290
|
-
'Removed from',
|
|
291
|
-
groupsEvent.groups_removed
|
|
292
|
-
);
|
|
355
|
+
if (event.groups_added) {
|
|
356
|
+
return renderInfoList('Added to', 'Added to', event.groups_added);
|
|
357
|
+
} else if (event.groups_removed) {
|
|
358
|
+
return renderInfoList('Removed from', 'Removed from', event.groups_removed);
|
|
293
359
|
}
|
|
294
360
|
};
|
|
295
361
|
|
|
@@ -299,23 +365,31 @@ export const renderAirtimeTransferredEvent = (
|
|
|
299
365
|
if (parseFloat(event.amount) === 0) {
|
|
300
366
|
return html`<div>Airtime transfer failed</div>`;
|
|
301
367
|
}
|
|
302
|
-
return html`<div>
|
|
303
|
-
Transferred
|
|
368
|
+
return html`<div style=${eventLineStyle}>
|
|
369
|
+
Transferred ${valueText(event.amount)} ${event.currency} of airtime
|
|
304
370
|
</div>`;
|
|
305
371
|
};
|
|
306
372
|
|
|
307
373
|
export const renderContactLanguageChangedEvent = (
|
|
308
374
|
event: ContactLanguageChangedEvent
|
|
309
375
|
): TemplateResult => {
|
|
310
|
-
|
|
311
|
-
|
|
376
|
+
if (!event.language) {
|
|
377
|
+
return html`<div style=${eventLineStyle}>Cleared language</div>`;
|
|
378
|
+
}
|
|
379
|
+
return html`<div style=${eventLineStyle}>
|
|
380
|
+
Language updated to ${valueText(getLanguageName(event.language))}
|
|
312
381
|
</div>`;
|
|
313
382
|
};
|
|
314
383
|
|
|
315
384
|
export const renderContactStatusChangedEvent = (
|
|
316
385
|
event: ContactStatusChangedEvent
|
|
317
386
|
): TemplateResult => {
|
|
318
|
-
|
|
387
|
+
if (!event.status) {
|
|
388
|
+
return html`<div style=${eventLineStyle}>Cleared status</div>`;
|
|
389
|
+
}
|
|
390
|
+
return html`<div style=${eventLineStyle}>
|
|
391
|
+
Status updated to ${valueText(event.status)}
|
|
392
|
+
</div>`;
|
|
319
393
|
};
|
|
320
394
|
|
|
321
395
|
export const renderCallEvent = (event: CallEvent): TemplateResult => {
|
|
@@ -561,7 +635,7 @@ export const renderEvent = (
|
|
|
561
635
|
content = renderTicketAction(event as TicketEvent, 'closed');
|
|
562
636
|
break;
|
|
563
637
|
case Events.TICKET_OPENED:
|
|
564
|
-
content =
|
|
638
|
+
content = renderTicketOpened(event as TicketEvent);
|
|
565
639
|
break;
|
|
566
640
|
case Events.TICKET_NOTE_ADDED:
|
|
567
641
|
content = renderTicketAction(event as TicketEvent, 'noted');
|
|
@@ -569,11 +643,17 @@ export const renderEvent = (
|
|
|
569
643
|
case Events.TICKET_REOPENED:
|
|
570
644
|
content = renderTicketAction(event as TicketEvent, 'reopened');
|
|
571
645
|
break;
|
|
572
|
-
case Events.TICKET_TOPIC_CHANGED:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
646
|
+
case Events.TICKET_TOPIC_CHANGED: {
|
|
647
|
+
// event.topic is optional — guard so a payload without one
|
|
648
|
+
// degrades cleanly instead of throwing inside topicPill.
|
|
649
|
+
const newTopic = (event as TicketEvent).topic;
|
|
650
|
+
content = newTopic
|
|
651
|
+
? html`<div style=${eventLineStyle}>
|
|
652
|
+
Topic changed to ${topicPill(newTopic)}
|
|
653
|
+
</div>`
|
|
654
|
+
: null;
|
|
576
655
|
break;
|
|
656
|
+
}
|
|
577
657
|
default:
|
|
578
658
|
return null;
|
|
579
659
|
}
|
|
@@ -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/CanvasNode.ts
CHANGED
|
@@ -219,13 +219,18 @@ export class CanvasNode extends RapidElement {
|
|
|
219
219
|
background: repeating-linear-gradient(120deg, tomato, tomato 6px, #ff7056 0, #ff7056 18px) !important;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
/* Disable links on actions/nodes with issues
|
|
222
|
+
/* Disable links on actions/nodes with issues so clicks fall through
|
|
223
|
+
to open the editor instead of navigating. */
|
|
223
224
|
.action-content.has-issues .linked-name div,
|
|
224
225
|
.node.has-issues > .router .linked-name div {
|
|
225
226
|
text-decoration: none !important;
|
|
226
227
|
cursor: default !important;
|
|
227
228
|
pointer-events: none;
|
|
228
229
|
}
|
|
230
|
+
.action-content.has-issues .linked-pill,
|
|
231
|
+
.node.has-issues > .router .linked-pill {
|
|
232
|
+
pointer-events: none;
|
|
233
|
+
}
|
|
229
234
|
|
|
230
235
|
.action.sortable {
|
|
231
236
|
display: flex;
|
|
@@ -1094,13 +1099,16 @@ export class CanvasNode extends RapidElement {
|
|
|
1094
1099
|
}
|
|
1095
1100
|
|
|
1096
1101
|
/**
|
|
1097
|
-
* Returns true if the click target is inside a `.linked-name`
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1102
|
+
* Returns true if the click target is inside a `.linked-name` or
|
|
1103
|
+
* `.linked-pill` whose containing action/node has no issues. Active
|
|
1104
|
+
* links handle their own navigation, so click-vs-drag and node-edit
|
|
1105
|
+
* handlers bail out on them. When the action/node has issues, links are
|
|
1106
|
+
* visually disabled (see CSS) and clicks fall through to open the
|
|
1107
|
+
* editor instead.
|
|
1101
1108
|
*/
|
|
1102
1109
|
private isActiveLink(target: HTMLElement, action?: Action): boolean {
|
|
1103
|
-
if (!target.closest('.linked-name')
|
|
1110
|
+
if (!target.closest('.linked-name') && !target.closest('.linked-pill'))
|
|
1111
|
+
return false;
|
|
1104
1112
|
if (action) return !this.issuesByAction?.has(action.uuid);
|
|
1105
1113
|
return !(
|
|
1106
1114
|
this.issuesByNode?.has(this.node.uuid) ||
|
package/src/flow/DragManager.ts
CHANGED
|
@@ -127,7 +127,8 @@ export class DragManager {
|
|
|
127
127
|
if (
|
|
128
128
|
target.classList.contains('exit') ||
|
|
129
129
|
target.closest('.exit') ||
|
|
130
|
-
target.closest('.linked-name')
|
|
130
|
+
target.closest('.linked-name') ||
|
|
131
|
+
target.closest('.linked-pill')
|
|
131
132
|
) {
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
@@ -186,7 +187,8 @@ export class DragManager {
|
|
|
186
187
|
if (
|
|
187
188
|
target.classList.contains('exit') ||
|
|
188
189
|
target.closest('.exit') ||
|
|
189
|
-
target.closest('.linked-name')
|
|
190
|
+
target.closest('.linked-name') ||
|
|
191
|
+
target.closest('.linked-pill')
|
|
190
192
|
) {
|
|
191
193
|
return;
|
|
192
194
|
}
|
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);
|
|
@@ -405,6 +386,7 @@ const renderLinkedObject = (
|
|
|
405
386
|
|
|
406
387
|
const pillType = iconToPillType(icon);
|
|
407
388
|
return html`<temba-label
|
|
389
|
+
class="linked-pill"
|
|
408
390
|
icon=${icon || ''}
|
|
409
391
|
type=${pillType || 'neutral'}
|
|
410
392
|
clickable
|
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;
|
package/src/form/DatePicker.ts
CHANGED
|
@@ -19,6 +19,7 @@ export class DatePicker extends FieldElement {
|
|
|
19
19
|
.container {
|
|
20
20
|
border-radius: var(--curvature);
|
|
21
21
|
border: 1px solid var(--color-widget-border);
|
|
22
|
+
background: var(--color-widget-bg);
|
|
22
23
|
display: flex;
|
|
23
24
|
cursor: pointer;
|
|
24
25
|
box-shadow: var(--widget-box-shadow);
|
|
@@ -59,7 +60,7 @@ export class DatePicker extends FieldElement {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
.tz-wrapper {
|
|
62
|
-
background:
|
|
63
|
+
background: var(--sunken);
|
|
63
64
|
display: flex;
|
|
64
65
|
flex-direction: row;
|
|
65
66
|
align-items: center;
|
|
@@ -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
|