@nyaruka/temba-components 0.168.1 → 0.170.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 +19 -0
- package/dist/locales/es.js +1 -0
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +1 -0
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +1 -0
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +1329 -1378
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Chat.ts +2 -2
- package/src/display/ContactName.ts +123 -1
- package/src/display/Dropdown.ts +11 -7
- package/src/display/Options.ts +27 -14
- package/src/display/TembaDate.ts +9 -2
- package/src/form/Compose.ts +37 -28
- package/src/interfaces.ts +13 -3
- package/src/list/BroadcastList.ts +3 -1
- package/src/list/ContactList.ts +113 -9
- package/src/list/ContentList.ts +351 -8
- package/src/list/ContentMenu.ts +12 -6
- package/src/list/TembaList.ts +99 -29
- package/src/list/TicketList.ts +87 -18
- package/src/live/ContactChat.ts +13 -1
- package/src/live/ContactDetails.ts +32 -1
- package/src/live/ContactFields.ts +34 -1
- package/src/live/ContactNotepad.ts +46 -11
- package/src/live/ContactStoreElement.ts +103 -1
- package/src/live/ContactWatch.ts +415 -0
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/pt.ts +1 -0
- package/src/simulator/Simulator.ts +1 -0
- package/temba-modules.ts +0 -4
- package/xliff/es.xlf +3 -0
- package/xliff/fr.xlf +3 -0
- package/xliff/pt.xlf +3 -0
- package/src/live/ContactBadges.ts +0 -207
- package/src/live/ContactNameFetch.ts +0 -35
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { Contact } from '../interfaces';
|
|
2
|
+
import { Events } from '../events/eventRenderers';
|
|
3
|
+
import { getStore } from '../store/Store';
|
|
4
|
+
import { RealtimeSubscription, subscribeToContactHistory } from './Realtime';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Central interest registry for live contact state. Components declare what
|
|
8
|
+
* they care about - specific event types or '*' for everything - and this
|
|
9
|
+
* module owns the rest: it holds the single socket subscription per contact
|
|
10
|
+
* (the firehose on "history:<contact-uuid>"), fetches the contact so watchers
|
|
11
|
+
* get an initial value without something having to change, and fans each
|
|
12
|
+
* event out to just the watchers whose interest matches.
|
|
13
|
+
*
|
|
14
|
+
* const watch = watchContact(uuid, [Events.CONTACT_NAME_CHANGED], (event, contact) => {
|
|
15
|
+
* ...
|
|
16
|
+
* });
|
|
17
|
+
* watch.unsubscribe();
|
|
18
|
+
*
|
|
19
|
+
* Every delivery hands the watcher the current contact alongside the event
|
|
20
|
+
* that triggered it, so watchers can read state without knowing where the
|
|
21
|
+
* data originally came from. The contact is kept current centrally: scalar
|
|
22
|
+
* events are applied to it directly, while events whose full effect isn't in
|
|
23
|
+
* the event payload (see refetchTypes) trigger a refetch. Initial values and
|
|
24
|
+
* refetches arrive through the same handler as live changes, as a delivery
|
|
25
|
+
* with no event, so watchers have a single code path. The fetch happens on
|
|
26
|
+
* every (re)subscribe, including after reconnects, so watchers also catch up
|
|
27
|
+
* on anything missed while offline. When the last watcher for a contact
|
|
28
|
+
* unsubscribes the socket subscription and cached contact are dropped.
|
|
29
|
+
*
|
|
30
|
+
* Wildcard watchers are event-stream consumers (e.g. chat renders history) -
|
|
31
|
+
* they get every live event but no eventless deliveries.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// same shape components previously fetched themselves - urns are expanded and
|
|
35
|
+
// priority ordered so watchers can pick the messaging destination
|
|
36
|
+
const CONTACT_ENDPOINT =
|
|
37
|
+
'/api/v2/contacts.json?expand_urns=true&urn_order=priority&uuid=';
|
|
38
|
+
|
|
39
|
+
// how long we wait for an event burst (e.g. one flow sprint updating several
|
|
40
|
+
// fields) to settle before refetching
|
|
41
|
+
const REFETCH_DEBOUNCE = 100;
|
|
42
|
+
|
|
43
|
+
// a fetch can fail on an otherwise healthy socket, which would leave watchers
|
|
44
|
+
// with no contact at all until something else triggers a fetch - retry a few
|
|
45
|
+
// times, backing off between attempts
|
|
46
|
+
const FETCH_RETRIES = 3;
|
|
47
|
+
const FETCH_RETRY_DELAY = 1000;
|
|
48
|
+
|
|
49
|
+
// the contact-state events - everything that changes what a contact *is*, as
|
|
50
|
+
// opposed to recording something that happened to them. The default interest
|
|
51
|
+
// for components that render contact data
|
|
52
|
+
export const CONTACT_STATE_TYPES = [
|
|
53
|
+
Events.CONTACT_NAME_CHANGED,
|
|
54
|
+
Events.CONTACT_URNS_CHANGED,
|
|
55
|
+
Events.CONTACT_FIELD_CHANGED,
|
|
56
|
+
Events.CONTACT_GROUPS_CHANGED,
|
|
57
|
+
Events.CONTACT_LANGUAGE_CHANGED,
|
|
58
|
+
Events.CONTACT_STATUS_CHANGED,
|
|
59
|
+
Events.CONTACT_FLOW_CHANGED,
|
|
60
|
+
Events.CONTACT_LAST_SEEN_CHANGED
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export type ContactEventHandler = (event: any, contact: Contact) => void;
|
|
64
|
+
|
|
65
|
+
interface Watcher {
|
|
66
|
+
types: string[] | '*';
|
|
67
|
+
onEvent: ContactEventHandler;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface WatchedContact {
|
|
71
|
+
watchers: Watcher[];
|
|
72
|
+
sub: RealtimeSubscription;
|
|
73
|
+
contact: Contact;
|
|
74
|
+
fetchSeq: number;
|
|
75
|
+
// fetches in flight - their responses predate anything arriving now, so
|
|
76
|
+
// events landing while one is outstanding need a refetch to survive
|
|
77
|
+
fetching: number;
|
|
78
|
+
refetchTimer: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const watched = new Map<string, WatchedContact>();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Serializes an engine field value from an event the way the read API
|
|
85
|
+
* would - both read the same engine value dict, keyed by the field's type
|
|
86
|
+
* (the API calls number fields "numeric"). A value that didn't parse as the
|
|
87
|
+
* field's type serializes as null, same as the API. Returns undefined when
|
|
88
|
+
* the field definition isn't known, since without it there's no way to tell
|
|
89
|
+
* which engine value the API would have read.
|
|
90
|
+
*/
|
|
91
|
+
const serializeFieldValue = (key: string, value: any): string | undefined => {
|
|
92
|
+
if (!value) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const fieldType = getStore()?.getContactField(key)?.value_type;
|
|
96
|
+
if (!fieldType) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const engineType = fieldType === 'numeric' ? 'number' : fieldType;
|
|
100
|
+
const engineValue = value[engineType];
|
|
101
|
+
return engineValue != null ? String(engineValue) : null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// how live events patch the cached contact so every delivery carries current
|
|
105
|
+
// state. Returning false means the event couldn't be applied and the contact
|
|
106
|
+
// needs a refetch to be current
|
|
107
|
+
const appliers: {
|
|
108
|
+
[type: string]: (contact: Contact, event: any) => boolean | void;
|
|
109
|
+
} = {
|
|
110
|
+
[Events.CONTACT_NAME_CHANGED]: (contact, event) => {
|
|
111
|
+
contact.name = event.name;
|
|
112
|
+
},
|
|
113
|
+
[Events.CONTACT_LANGUAGE_CHANGED]: (contact, event) => {
|
|
114
|
+
contact.language = event.language;
|
|
115
|
+
},
|
|
116
|
+
[Events.CONTACT_STATUS_CHANGED]: (contact, event) => {
|
|
117
|
+
contact.status = event.status;
|
|
118
|
+
},
|
|
119
|
+
[Events.CONTACT_FLOW_CHANGED]: (contact, event) => {
|
|
120
|
+
contact.flow = event.flow || null;
|
|
121
|
+
},
|
|
122
|
+
[Events.CONTACT_LAST_SEEN_CHANGED]: (contact, event) => {
|
|
123
|
+
// last seen only ever moves forward - ignore out of order deliveries
|
|
124
|
+
if (
|
|
125
|
+
!contact.last_seen_on ||
|
|
126
|
+
new Date(event.last_seen_on) > new Date(contact.last_seen_on)
|
|
127
|
+
) {
|
|
128
|
+
contact.last_seen_on = event.last_seen_on;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
[Events.CONTACT_FIELD_CHANGED]: (contact, event) => {
|
|
132
|
+
const key = event.field?.key;
|
|
133
|
+
if (!key) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const value = serializeFieldValue(key, event.value);
|
|
137
|
+
if (value === undefined) {
|
|
138
|
+
// no field definition to serialize against - don't guess at a value
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// replace rather than mutate so earlier deliveries keep their values
|
|
142
|
+
contact.fields = { ...contact.fields, [key]: value };
|
|
143
|
+
},
|
|
144
|
+
[Events.CONTACT_GROUPS_CHANGED]: (contact, event) => {
|
|
145
|
+
const added = event.groups_added || [];
|
|
146
|
+
const removed = new Set(
|
|
147
|
+
(event.groups_removed || []).map((group: any) => group.uuid)
|
|
148
|
+
);
|
|
149
|
+
contact.groups = [
|
|
150
|
+
...(contact.groups || []).filter(
|
|
151
|
+
(group) =>
|
|
152
|
+
!removed.has(group.uuid) &&
|
|
153
|
+
!added.some((a: any) => a.uuid === group.uuid)
|
|
154
|
+
),
|
|
155
|
+
// group references can arrive without a name, but consumers sort on it
|
|
156
|
+
...added.map((group: any) => ({
|
|
157
|
+
uuid: group.uuid,
|
|
158
|
+
name: group.name || ''
|
|
159
|
+
}))
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// events whose new state can't be patched into the cached contact from the
|
|
165
|
+
// event alone - urns arrive as raw strings while the contact carries them
|
|
166
|
+
// expanded with their channels, which picking a messaging destination needs.
|
|
167
|
+
// Their arrival triggers a (debounced) refetch, which re-primes every
|
|
168
|
+
// watcher with current values.
|
|
169
|
+
const refetchTypes = new Set<string>([Events.CONTACT_URNS_CHANGED]);
|
|
170
|
+
|
|
171
|
+
const matches = (watcher: Watcher, type: string): boolean => {
|
|
172
|
+
return watcher.types === '*' || watcher.types.includes(type);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// watchers are page components we don't control - one of them throwing can't
|
|
176
|
+
// be allowed to cost the others their delivery
|
|
177
|
+
const deliver = (watcher: Watcher, event: any, contact: Contact) => {
|
|
178
|
+
try {
|
|
179
|
+
watcher.onEvent(event, contact);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('contact watcher failed', error);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Hands every non-wildcard watcher the current contact as an eventless
|
|
187
|
+
* delivery.
|
|
188
|
+
*/
|
|
189
|
+
const primeAll = (entry: WatchedContact) => {
|
|
190
|
+
for (const watcher of [...entry.watchers]) {
|
|
191
|
+
if (watcher.types !== '*') {
|
|
192
|
+
deliver(watcher, null, entry.contact);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const clearRefetch = (entry: WatchedContact) => {
|
|
198
|
+
if (entry.refetchTimer) {
|
|
199
|
+
window.clearTimeout(entry.refetchTimer);
|
|
200
|
+
entry.refetchTimer = null;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// a fetch that couldn't happen leaves watchers with no value at all, so try
|
|
205
|
+
// again with a backoff - anything that supersedes us in the meantime (a newer
|
|
206
|
+
// fetch, a local write, the last watcher leaving) cancels it
|
|
207
|
+
const retryFetch = (
|
|
208
|
+
uuid: string,
|
|
209
|
+
entry: WatchedContact,
|
|
210
|
+
attempt: number,
|
|
211
|
+
seq: number
|
|
212
|
+
) => {
|
|
213
|
+
if (attempt >= FETCH_RETRIES) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
window.setTimeout(
|
|
218
|
+
() => {
|
|
219
|
+
if (watched.get(uuid) === entry && entry.fetchSeq === seq) {
|
|
220
|
+
fetchContact(uuid, entry, attempt + 1);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
FETCH_RETRY_DELAY * Math.pow(2, attempt)
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const fetchContact = (uuid: string, entry: WatchedContact, attempt = 0) => {
|
|
228
|
+
const store = getStore();
|
|
229
|
+
if (!store) {
|
|
230
|
+
// the store element can be added to the page after the components that
|
|
231
|
+
// watch through it - come back for it instead of leaving watchers empty
|
|
232
|
+
// for good. Claim a seq like a real fetch does so a later call supersedes
|
|
233
|
+
// this chain instead of running alongside it
|
|
234
|
+
retryFetch(uuid, entry, attempt, ++entry.fetchSeq);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// a pending debounce would only land behind us with the same data
|
|
239
|
+
clearRefetch(entry);
|
|
240
|
+
|
|
241
|
+
const seq = ++entry.fetchSeq;
|
|
242
|
+
entry.fetching++;
|
|
243
|
+
store
|
|
244
|
+
// skip the store cache in both directions - we always want current data
|
|
245
|
+
// and the cache holds this url in a different shape for components that
|
|
246
|
+
// still fetch it themselves
|
|
247
|
+
.getUrl(`${CONTACT_ENDPOINT}${uuid}`, { force: true, skipCache: true })
|
|
248
|
+
// two-argument form so a throw out of the success handler isn't taken
|
|
249
|
+
// for a failed fetch and retried on top of itself
|
|
250
|
+
.then(
|
|
251
|
+
(response) => {
|
|
252
|
+
entry.fetching--;
|
|
253
|
+
|
|
254
|
+
// ignore responses that arrive after everyone left or a newer fetch
|
|
255
|
+
if (watched.get(uuid) !== entry || entry.fetchSeq !== seq) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const contact = response.json?.results?.[0] || null;
|
|
260
|
+
if (!contact) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
entry.contact = contact;
|
|
265
|
+
primeAll(entry);
|
|
266
|
+
},
|
|
267
|
+
() => {
|
|
268
|
+
entry.fetching--;
|
|
269
|
+
|
|
270
|
+
if (watched.get(uuid) !== entry || entry.fetchSeq !== seq) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
retryFetch(uuid, entry, attempt, seq);
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const scheduleRefetch = (uuid: string, entry: WatchedContact) => {
|
|
280
|
+
clearRefetch(entry);
|
|
281
|
+
entry.refetchTimer = window.setTimeout(() => {
|
|
282
|
+
entry.refetchTimer = null;
|
|
283
|
+
if (watched.get(uuid) === entry) {
|
|
284
|
+
fetchContact(uuid, entry);
|
|
285
|
+
}
|
|
286
|
+
}, REFETCH_DEBOUNCE);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const handleEvent = (uuid: string, entry: WatchedContact, event: any) => {
|
|
290
|
+
const apply = appliers[event.type];
|
|
291
|
+
const applied = entry.contact && apply ? apply(entry.contact, event) : null;
|
|
292
|
+
|
|
293
|
+
// a state event we couldn't fully apply leaves the contact stale - there
|
|
294
|
+
// was no snapshot to patch, a fetch that predates the event is still in
|
|
295
|
+
// flight, or the applier couldn't resolve what it needed. Scheduling ahead
|
|
296
|
+
// of the fan-out keeps a throwing watcher from costing us the refetch, and
|
|
297
|
+
// the debounce collapses a burst into a single fetch
|
|
298
|
+
const stale =
|
|
299
|
+
!!apply && (!entry.contact || entry.fetching > 0 || applied === false);
|
|
300
|
+
if (stale || refetchTypes.has(event.type)) {
|
|
301
|
+
scheduleRefetch(uuid, entry);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const watcher of [...entry.watchers]) {
|
|
305
|
+
if (matches(watcher, event.type)) {
|
|
306
|
+
deliver(watcher, event, entry.contact);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export const watchContact = (
|
|
312
|
+
uuid: string,
|
|
313
|
+
types: string[] | '*',
|
|
314
|
+
onEvent: ContactEventHandler
|
|
315
|
+
): RealtimeSubscription => {
|
|
316
|
+
let entry = watched.get(uuid);
|
|
317
|
+
if (!entry) {
|
|
318
|
+
const newEntry: WatchedContact = {
|
|
319
|
+
watchers: [],
|
|
320
|
+
sub: null,
|
|
321
|
+
contact: null,
|
|
322
|
+
fetchSeq: 0,
|
|
323
|
+
fetching: 0,
|
|
324
|
+
refetchTimer: null
|
|
325
|
+
};
|
|
326
|
+
watched.set(uuid, newEntry);
|
|
327
|
+
newEntry.sub = subscribeToContactHistory(
|
|
328
|
+
uuid,
|
|
329
|
+
null,
|
|
330
|
+
(event) => handleEvent(uuid, newEntry, event),
|
|
331
|
+
// fires on every (re)subscribe incl. after reconnects - (re)fetch so
|
|
332
|
+
// watchers see anything that changed while we weren't listening
|
|
333
|
+
() => fetchContact(uuid, newEntry)
|
|
334
|
+
);
|
|
335
|
+
entry = newEntry;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const watcher: Watcher = { types, onEvent };
|
|
339
|
+
entry.watchers.push(watcher);
|
|
340
|
+
|
|
341
|
+
// late joiners on an already-fetched contact get their initial delivery
|
|
342
|
+
// without waiting for another fetch - async so it mirrors the fetch path
|
|
343
|
+
if (entry.contact && types !== '*') {
|
|
344
|
+
const contact = entry.contact;
|
|
345
|
+
Promise.resolve().then(() => {
|
|
346
|
+
if (entry.watchers.includes(watcher)) {
|
|
347
|
+
deliver(watcher, null, contact);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
unsubscribe: () => {
|
|
354
|
+
const index = entry.watchers.indexOf(watcher);
|
|
355
|
+
if (index < 0) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
entry.watchers.splice(index, 1);
|
|
359
|
+
if (entry.watchers.length === 0) {
|
|
360
|
+
dropEntry(uuid, entry);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Pushes a fresh copy of a contact into the registry, priming its watchers.
|
|
368
|
+
* Components that write contact changes call this with the server's response
|
|
369
|
+
* so every watcher on the page reflects an edit immediately, without waiting
|
|
370
|
+
* for the change to echo back over the socket. A no-op for unwatched
|
|
371
|
+
* contacts.
|
|
372
|
+
*/
|
|
373
|
+
export const updateContact = (uuid: string, contact: Contact) => {
|
|
374
|
+
const entry = watched.get(uuid);
|
|
375
|
+
if (entry) {
|
|
376
|
+
// discard any in-flight fetch and any pending refetch - both started
|
|
377
|
+
// before this write and could land after it with pre-write data
|
|
378
|
+
entry.fetchSeq++;
|
|
379
|
+
clearRefetch(entry);
|
|
380
|
+
// the caller keeps using the object they handed us (it's their data and
|
|
381
|
+
// the store's cache entry) while appliers patch ours in place - copy the
|
|
382
|
+
// parts they touch so a live event can't reach back into it
|
|
383
|
+
entry.contact = {
|
|
384
|
+
...contact,
|
|
385
|
+
groups: (contact.groups || []).map((group) => ({ ...group })),
|
|
386
|
+
fields: { ...contact.fields }
|
|
387
|
+
};
|
|
388
|
+
primeAll(entry);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Forces a refetch of a watched contact, re-priming its watchers. A no-op
|
|
394
|
+
* for unwatched contacts.
|
|
395
|
+
*/
|
|
396
|
+
export const refreshContact = (uuid: string) => {
|
|
397
|
+
const entry = watched.get(uuid);
|
|
398
|
+
if (entry) {
|
|
399
|
+
fetchContact(uuid, entry);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const dropEntry = (uuid: string, entry: WatchedContact) => {
|
|
404
|
+
watched.delete(uuid);
|
|
405
|
+
clearRefetch(entry);
|
|
406
|
+
entry.sub.unsubscribe();
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// for tests - real pages just unwatch
|
|
410
|
+
export const resetContactWatches = () => {
|
|
411
|
+
for (const [uuid, entry] of [...watched.entries()]) {
|
|
412
|
+
entry.watchers.length = 0;
|
|
413
|
+
dropEntry(uuid, entry);
|
|
414
|
+
}
|
|
415
|
+
};
|
package/src/locales/es.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's7722a91d3a512442': str`Last seen ${0}`,
|
|
16
16
|
's22965bb9808befb0': `Interrupt`,
|
|
17
|
+
's6dbbe2646b239ca5': `Closed`,
|
|
17
18
|
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
18
19
|
's3eb2567092b4d7c1': `from the beginning`,
|
|
19
20
|
's28f37776b3901438': `It's okay to interrupt`,
|
package/src/locales/fr.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's7722a91d3a512442': str`Last seen ${0}`,
|
|
16
16
|
's22965bb9808befb0': `Interrupt`,
|
|
17
|
+
's6dbbe2646b239ca5': `Closed`,
|
|
17
18
|
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
18
19
|
's3eb2567092b4d7c1': `from the beginning`,
|
|
19
20
|
's28f37776b3901438': `It's okay to interrupt`,
|
package/src/locales/pt.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's7722a91d3a512442': str`Last seen ${0}`,
|
|
16
16
|
's22965bb9808befb0': `Interrupt`,
|
|
17
|
+
's6dbbe2646b239ca5': `Closed`,
|
|
17
18
|
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
18
19
|
's3eb2567092b4d7c1': `from the beginning`,
|
|
19
20
|
's28f37776b3901438': `It's okay to interrupt`,
|
package/temba-modules.ts
CHANGED
|
@@ -30,13 +30,11 @@ import { ContactUrn } from './src/display/ContactUrn';
|
|
|
30
30
|
import { ContactFields } from './src/live/ContactFields';
|
|
31
31
|
import { ContactFieldEditor } from './src/live/ContactFieldEditor';
|
|
32
32
|
|
|
33
|
-
import { ContactBadges } from './src/live/ContactBadges';
|
|
34
33
|
import { ContactTimeline } from './src/live/ContactTimeline';
|
|
35
34
|
import { CampaignEvents } from './src/live/CampaignEvents';
|
|
36
35
|
import { TembaSlider } from './src/form/TembaSlider';
|
|
37
36
|
import { RunList } from './src/list/RunList';
|
|
38
37
|
import { FlowStoreElement } from './src/store/FlowStoreElement';
|
|
39
|
-
import { ContactNameFetch } from './src/live/ContactNameFetch';
|
|
40
38
|
import { DatePicker } from './src/form/DatePicker';
|
|
41
39
|
import { FieldManager } from './src/live/FieldManager';
|
|
42
40
|
import { SortableList } from './src/list/SortableList';
|
|
@@ -125,7 +123,6 @@ addCustomElement('temba-button', Button);
|
|
|
125
123
|
addCustomElement('temba-omnibox', Omnibox);
|
|
126
124
|
addCustomElement('temba-tip', Tip);
|
|
127
125
|
addCustomElement('temba-contact-name', ContactName);
|
|
128
|
-
addCustomElement('temba-contact-name-fetch', ContactNameFetch);
|
|
129
126
|
addCustomElement('temba-contact-field', ContactFieldEditor);
|
|
130
127
|
addCustomElement('temba-contact-fields', ContactFields);
|
|
131
128
|
addCustomElement('temba-field-manager', FieldManager);
|
|
@@ -162,7 +159,6 @@ addCustomElement('temba-icon', VectorIcon);
|
|
|
162
159
|
addCustomElement('temba-dropdown', Dropdown);
|
|
163
160
|
addCustomElement('temba-tabs', TabPane);
|
|
164
161
|
addCustomElement('temba-tab', Tab);
|
|
165
|
-
addCustomElement('temba-contact-badges', ContactBadges);
|
|
166
162
|
addCustomElement('temba-contact-timeline', ContactTimeline);
|
|
167
163
|
addCustomElement('temba-campaign-events', CampaignEvents);
|
|
168
164
|
addCustomElement('temba-slider', TembaSlider);
|
package/xliff/es.xlf
CHANGED
package/xliff/fr.xlf
CHANGED
package/xliff/pt.xlf
CHANGED