@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/locales/es.js +3 -1
  3. package/dist/locales/es.js.map +1 -1
  4. package/dist/locales/fr.js +3 -1
  5. package/dist/locales/fr.js.map +1 -1
  6. package/dist/locales/pt.js +3 -1
  7. package/dist/locales/pt.js.map +1 -1
  8. package/dist/temba-components.js +2791 -1668
  9. package/dist/temba-components.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/Icons.ts +2 -0
  12. package/src/display/Button.ts +8 -6
  13. package/src/display/Chat.ts +541 -191
  14. package/src/display/Dropdown.ts +40 -17
  15. package/src/display/Label.ts +25 -4
  16. package/src/display/TembaDate.ts +11 -2
  17. package/src/display/Tip.ts +157 -18
  18. package/src/events/eventRenderers.ts +678 -268
  19. package/src/events.ts +16 -1
  20. package/src/flow/NodeEditor.ts +82 -9
  21. package/src/flow/actions/add_contact_urn.ts +1 -0
  22. package/src/flow/actions/play_audio.ts +1 -0
  23. package/src/flow/actions/send_broadcast.ts +17 -1
  24. package/src/flow/actions/send_msg.ts +12 -1
  25. package/src/flow/actions/set_run_result.ts +1 -0
  26. package/src/flow/actions/start_session.ts +10 -1
  27. package/src/flow/nodes/shared-rules.ts +2 -0
  28. package/src/flow/nodes/split_by_airtime.ts +6 -0
  29. package/src/flow/nodes/split_by_expression.ts +1 -0
  30. package/src/flow/nodes/split_by_llm.ts +2 -0
  31. package/src/flow/nodes/split_by_llm_categorize.ts +3 -1
  32. package/src/flow/nodes/split_by_random.ts +2 -1
  33. package/src/flow/nodes/split_by_resthook.ts +4 -1
  34. package/src/flow/nodes/split_by_webhook.ts +9 -1
  35. package/src/flow/types.ts +3 -0
  36. package/src/flow/utils.ts +49 -0
  37. package/src/form/DatePicker.ts +2 -1
  38. package/src/layout/Card.ts +9 -0
  39. package/src/layout/CardLayout.ts +426 -21
  40. package/src/layout/CardStack.ts +4 -1
  41. package/src/list/ContactList.ts +22 -17
  42. package/src/list/ContentList.ts +632 -29
  43. package/src/list/MsgList.ts +4 -2
  44. package/src/list/SortableList.ts +5 -1
  45. package/src/list/TembaMenu.ts +67 -6
  46. package/src/live/ContactChat.ts +655 -336
  47. package/src/live/ContactDetails.ts +961 -88
  48. package/src/live/ContactFieldEditor.ts +46 -11
  49. package/src/live/ContactFields.ts +57 -16
  50. package/src/live/ContactStoreElement.ts +15 -8
  51. package/src/locales/es.ts +3 -2
  52. package/src/locales/fr.ts +3 -2
  53. package/src/locales/pt.ts +3 -2
  54. package/src/simulator/Simulator.ts +28 -0
  55. package/src/styles/pillVariants.ts +13 -1
  56. package/web-test-runner.config.mjs +2 -0
  57. package/xliff/es.xlf +6 -3
  58. package/xliff/fr.xlf +6 -3
  59. package/xliff/pt.xlf +6 -3
@@ -37,12 +37,21 @@ export class ContactFieldEditor extends RapidElement {
37
37
  @property({ type: String })
38
38
  iconClass = '';
39
39
 
40
+ @property({ type: String })
41
+ valueIcon = '';
42
+
43
+ @property({ type: String })
44
+ valueIconLabel = '';
45
+
40
46
  @property({ type: String })
41
47
  status: Status = Status.Ready;
42
48
 
43
49
  @property({ type: Boolean })
44
50
  disabled = false;
45
51
 
52
+ @property({ type: Boolean })
53
+ searchable = true;
54
+
46
55
  @property({ type: Boolean })
47
56
  dirty = false;
48
57
 
@@ -60,7 +69,7 @@ export class ContactFieldEditor extends RapidElement {
60
69
  --color-widget-bg: transparent;
61
70
  --color-widget-bg-focused: #fff;
62
71
  --widget-box-shadow: none;
63
- padding-bottom: 0.6em;
72
+ padding-bottom: var(--contact-field-padding-bottom, 0.6em);
64
73
  border-bottom: var(--contact-field-separator, 1px solid #ececec);
65
74
  }
66
75
 
@@ -94,7 +103,7 @@ export class ContactFieldEditor extends RapidElement {
94
103
  }
95
104
 
96
105
  .wrapper {
97
- margin-bottom: 0.5em;
106
+ margin-bottom: var(--contact-field-wrapper-margin-bottom, 0.5em);
98
107
  }
99
108
 
100
109
  .field-label {
@@ -123,10 +132,19 @@ export class ContactFieldEditor extends RapidElement {
123
132
  }
124
133
 
125
134
  .disabled .value {
126
- color: var(--text-1);
135
+ color: var(--contact-field-value-color, var(--text-1));
127
136
  margin-left: 0.25em;
128
137
  margin-top: 0.35em;
129
138
  min-height: 1.75em;
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 8px;
142
+ }
143
+
144
+ .value-prefix {
145
+ display: flex;
146
+ align-items: center;
147
+ --icon-color: var(--contact-field-value-icon-color, var(--text-3));
130
148
  }
131
149
 
132
150
  .postfix {
@@ -337,9 +355,12 @@ export class ContactFieldEditor extends RapidElement {
337
355
  evt.stopPropagation();
338
356
  }
339
357
 
340
- public handleResponse(response: WebResponse) {
358
+ public handleResponse(response: WebResponse, savedValue?: string) {
341
359
  if (response.status === 200) {
342
- this.value = response.json.fields[this.key];
360
+ this.value =
361
+ savedValue !== undefined
362
+ ? savedValue
363
+ : response.json.fields?.[this.key] || '';
343
364
  // this.status = Status.Success;
344
365
  // on success lets go back to ready state for now
345
366
  this.status = Status.Ready;
@@ -452,12 +473,14 @@ export class ContactFieldEditor extends RapidElement {
452
473
  @click=${this.handleIconClick}
453
474
  >
454
475
  ${state}
455
- <temba-icon
456
- class="search"
457
- icon-action="search"
458
- name="${Icon.search}"
459
- animateclick="pulse"
460
- ></temba-icon>
476
+ ${this.searchable
477
+ ? html`<temba-icon
478
+ class="search"
479
+ icon-action="search"
480
+ name="${Icon.search}"
481
+ animateclick="pulse"
482
+ ></temba-icon>`
483
+ : null}
461
484
  <temba-icon
462
485
  class="copy"
463
486
  icon-action="copy"
@@ -528,6 +551,18 @@ export class ContactFieldEditor extends RapidElement {
528
551
  >
529
552
  <div class="label"><div class="name">${this.name}</div></div>
530
553
  <div class="value">
554
+ ${this.valueIcon
555
+ ? html`<temba-tip
556
+ class="value-prefix"
557
+ text=${this.valueIconLabel}
558
+ position="top"
559
+ >
560
+ <temba-icon
561
+ name=${this.valueIcon}
562
+ aria-label=${this.valueIconLabel}
563
+ ></temba-icon>
564
+ </temba-tip>`
565
+ : null}
531
566
  ${this.type === 'datetime'
532
567
  ? this.value
533
568
  ? html`<temba-date
@@ -10,7 +10,7 @@ import { getClasses, postJSON } from '../utils';
10
10
  import { ContactFieldEditor } from './ContactFieldEditor';
11
11
  import { ContactStoreElement } from './ContactStoreElement';
12
12
  import { Checkbox } from '../form/Checkbox';
13
- import { CustomEventType } from '../interfaces';
13
+ import { ContactField, CustomEventType } from '../interfaces';
14
14
 
15
15
  const MIN_FOR_FILTER = 10;
16
16
 
@@ -123,13 +123,47 @@ export class ContactFields extends ContactStoreElement {
123
123
  }
124
124
 
125
125
  private isAgent(): boolean {
126
- return this.role === 'T';
126
+ return this.role === 'T' || this.role?.toLowerCase() === 'agent';
127
+ }
128
+
129
+ private getAgentAccess(field: ContactField): 'none' | 'view' | 'edit' {
130
+ const access = field.agent_access?.toLowerCase();
131
+ if (access === 'n' || access === 'none') return 'none';
132
+ if (access === 'e' || access === 'edit') return 'edit';
133
+ return 'view';
134
+ }
135
+
136
+ private canViewField(field: ContactField): boolean {
137
+ return !this.isAgent() || this.getAgentAccess(field) !== 'none';
138
+ }
139
+
140
+ private canEditField(field: ContactField): boolean {
141
+ return (
142
+ !this.disabled &&
143
+ (!this.isAgent() || this.getAgentAccess(field) === 'edit')
144
+ );
145
+ }
146
+
147
+ private getVisibleFieldEntries(): [string, string][] {
148
+ if (!this.data || !this.fieldsReady) return [];
149
+ return Object.entries(this.data.fields).filter(
150
+ ([key]: [string, string]) => {
151
+ const field = this.store.getContactField(key);
152
+ return !!field && this.canViewField(field);
153
+ }
154
+ );
127
155
  }
128
156
 
129
157
  public willUpdate(changed: PropertyValues): void {
130
158
  super.willUpdate(changed);
131
- if (changed.has('data') && this.data) {
132
- if (Object.keys(this.data.fields).length <= MIN_FOR_FILTER) {
159
+ if (
160
+ (changed.has('data') ||
161
+ changed.has('fieldsReady') ||
162
+ changed.has('role')) &&
163
+ this.data &&
164
+ this.fieldsReady
165
+ ) {
166
+ if (this.getVisibleFieldEntries().length <= MIN_FOR_FILTER) {
133
167
  this.showAll = true;
134
168
  }
135
169
  }
@@ -139,19 +173,29 @@ export class ContactFields extends ContactStoreElement {
139
173
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
140
174
  ): void {
141
175
  super.updated(changes);
142
- if (changes.has('data') && this.data) {
176
+ if (
177
+ (changes.has('data') ||
178
+ changes.has('fieldsReady') ||
179
+ changes.has('role')) &&
180
+ this.data &&
181
+ this.fieldsReady
182
+ ) {
143
183
  this.fireCustomEvent(CustomEventType.DetailsChanged, {
144
- count: Object.values(this.data.fields).filter((value) => !!value).length
184
+ count: this.getVisibleFieldEntries().filter(([, value]) => !!value)
185
+ .length
145
186
  });
146
187
  }
147
188
  }
148
189
 
149
190
  public handleFieldChanged(evt: InputEvent) {
150
191
  const field = evt.currentTarget as ContactFieldEditor;
192
+ const definition = this.store.getContactField(field.key);
193
+ if (!definition || !this.canEditField(definition)) return;
194
+
151
195
  const value = field.value;
152
196
 
153
197
  // TODO: Use contact.postChanges instead of postJSON
154
- postJSON(this.endpoint + this.data.uuid, {
198
+ postJSON(this.writeEndpoint + this.data.uuid, {
155
199
  fields: { [field.key]: value }
156
200
  })
157
201
  .then((response: any) => {
@@ -179,9 +223,8 @@ export class ContactFields extends ContactStoreElement {
179
223
  // guard against keys with no matching field definition (mid-load races
180
224
  // or fields that have since been deleted) so a single missing
181
225
  // definition can't throw and blank out the entire tab
182
- const fieldsToShow = Object.entries(this.data.fields)
183
- .filter(([key]: [string, string]) => !!this.store.getContactField(key))
184
- .sort((a: [string, string], b: [string, string]) => {
226
+ const fieldsToShow = this.getVisibleFieldEntries().sort(
227
+ (a: [string, string], b: [string, string]) => {
185
228
  const [ak] = a;
186
229
  const [bk] = b;
187
230
  const fieldA = this.store.getContactField(ak);
@@ -243,7 +286,8 @@ export class ContactFields extends ContactStoreElement {
243
286
  }
244
287
 
245
288
  return ak.localeCompare(bk);
246
- });
289
+ }
290
+ );
247
291
 
248
292
  if (fieldsToShow.length == 0) {
249
293
  return html`<slot name="empty"></slot>`;
@@ -260,17 +304,14 @@ export class ContactFields extends ContactStoreElement {
260
304
  type=${field.value_type}
261
305
  @change=${this.handleFieldChanged}
262
306
  timezone=${this.timezone}
263
- ?disabled=${(this.isAgent() && field.agent_access === 'view') ||
264
- this.disabled
265
- ? true
266
- : false}
307
+ ?disabled=${!this.canEditField(field)}
267
308
  ></temba-contact-field>`;
268
309
  });
269
310
 
270
311
  return html`
271
312
  <div class=${getClasses({ disabled: this.disabled })}>
272
313
  <div class="fields ${this.showAll ? 'show-all' : ''}">${fields}</div>
273
- ${Object.keys(this.data.fields).length >= MIN_FOR_FILTER
314
+ ${fieldsToShow.length >= MIN_FOR_FILTER
274
315
  ? html`<div class="toggle">
275
316
  <div style="flex-grow: 1"></div>
276
317
  <div>
@@ -18,14 +18,21 @@ export class ContactStoreElement extends EndpointMonitorElement {
18
18
  @property({ type: Object, attribute: false })
19
19
  data: Contact;
20
20
 
21
- // expand_urns resolves each URN against a channel so we know which one
22
- // will be used when messaging the contact
21
+ // Resolve each URN against a channel while retaining the user's priority
22
+ // order. Consumers can select the first channel-backed URN for messaging.
23
23
  @property({ type: String })
24
- endpoint = '/api/v2/contacts.json?expand_urns=true&uuid=';
24
+ endpoint = '/api/v2/contacts.json?expand_urns=true&urn_order=priority&uuid=';
25
+
26
+ // Writes go through the internal API, which responds with the same shape
27
+ // as the read endpoint above.
28
+ @property({ type: String })
29
+ writeEndpoint = '/api/internal/contacts.json?uuid=';
25
30
 
26
31
  prepareData(data: any) {
27
- if (data && data.length > 0) {
28
- data = data[0];
32
+ if (data) {
33
+ data = Array.isArray(data) ? data[0] : data;
34
+ }
35
+ if (data) {
29
36
  data.groups.forEach((group: Group) => {
30
37
  group.is_dynamic = this.store.isDynamicGroup(group.uuid);
31
38
  });
@@ -53,16 +60,16 @@ export class ContactStoreElement extends EndpointMonitorElement {
53
60
  // clear our cache so we don't have any races
54
61
  this.store.removeFromCache(`${this.endpoint}${this.contact}`);
55
62
  return this.store
56
- .postJSON(`${this.endpoint}${this.contact}`, payload)
63
+ .postJSON(`${this.writeEndpoint}${this.contact}`, payload)
57
64
  .then((response) => {
58
65
  this.setContact(response.json);
59
66
  });
60
67
  }
61
68
 
62
- public setContact(contact: any) {
69
+ public setContact(contact: any, contactId = this.contact) {
63
70
  // make sure contact data is properly prepped
64
71
  this.data = this.prepareData([contact]);
65
- this.store.updateCache(`${this.endpoint}${this.contact}`, this.data);
72
+ this.store.updateCache(`${this.endpoint}${contactId}`, this.data);
66
73
  }
67
74
 
68
75
  public willUpdate(changed: PropertyValues): void {
package/src/locales/es.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Re-generate this file by running lit-localize
4
4
 
5
5
 
6
-
6
+ import {str} from '@lit/localize';
7
7
 
8
8
  /* eslint-disable no-irregular-whitespace */
9
9
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -12,7 +12,8 @@
12
12
  'scf1453991c986b25': `Tab para completar, enter para seleccionar`,
13
13
  's73b4d70c02f4b4e0': `No options`,
14
14
  'sbc913d7dc0f33877': `to add`,
15
- 's81f17cfc89a04338': `Interrupt flow`,
15
+ 's7722a91d3a512442': str`Last seen ${0}`,
16
+ 's22965bb9808befb0': `Interrupt`,
16
17
  's122d4de68bcfcdf4': `It's okay to restart`,
17
18
  's3eb2567092b4d7c1': `from the beginning`,
18
19
  's28f37776b3901438': `It's okay to interrupt`,
package/src/locales/fr.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Re-generate this file by running lit-localize
4
4
 
5
5
 
6
-
6
+ import {str} from '@lit/localize';
7
7
 
8
8
  /* eslint-disable no-irregular-whitespace */
9
9
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -12,7 +12,8 @@
12
12
  's73b4d70c02f4b4e0': `No options`,
13
13
  'scf1453991c986b25': `Tab to complete, enter to select`,
14
14
  'sbc913d7dc0f33877': `to add`,
15
- 's81f17cfc89a04338': `Interrupt flow`,
15
+ 's7722a91d3a512442': str`Last seen ${0}`,
16
+ 's22965bb9808befb0': `Interrupt`,
16
17
  's122d4de68bcfcdf4': `It's okay to restart`,
17
18
  's3eb2567092b4d7c1': `from the beginning`,
18
19
  's28f37776b3901438': `It's okay to interrupt`,
package/src/locales/pt.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Re-generate this file by running lit-localize
4
4
 
5
5
 
6
-
6
+ import {str} from '@lit/localize';
7
7
 
8
8
  /* eslint-disable no-irregular-whitespace */
9
9
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -12,7 +12,8 @@
12
12
  's73b4d70c02f4b4e0': `No options`,
13
13
  'scf1453991c986b25': `Tab to complete, enter to select`,
14
14
  'sbc913d7dc0f33877': `to add`,
15
- 's81f17cfc89a04338': `Interrupt flow`,
15
+ 's7722a91d3a512442': str`Last seen ${0}`,
16
+ 's22965bb9808befb0': `Interrupt`,
16
17
  's122d4de68bcfcdf4': `It's okay to restart`,
17
18
  's3eb2567092b4d7c1': `from the beginning`,
18
19
  's28f37776b3901438': `It's okay to interrupt`,
@@ -822,6 +822,10 @@ export class Simulator extends RapidElement {
822
822
  private previousEventCount = 0;
823
823
  private chat: Chat = null;
824
824
 
825
+ // bumped on reset so in-flight sprint responses started against the old
826
+ // session are discarded instead of polluting the restarted one
827
+ private sessionGeneration = 0;
828
+
825
829
  @property({ type: Object })
826
830
  private session: Session | null = null;
827
831
 
@@ -1207,10 +1211,17 @@ export class Simulator extends RapidElement {
1207
1211
  }
1208
1212
  };
1209
1213
 
1214
+ const generation = this.sessionGeneration;
1210
1215
  try {
1211
1216
  const response = await postJSON(this.endpoint, body);
1217
+ if (generation !== this.sessionGeneration) {
1218
+ return;
1219
+ }
1212
1220
  this.updateRunContext(response.json as RunContext);
1213
1221
  } catch (error) {
1222
+ if (generation !== this.sessionGeneration) {
1223
+ return;
1224
+ }
1214
1225
  console.error('Failed to start simulation:', error);
1215
1226
  const errorEvent = {
1216
1227
  uuid: generateUUIDv7(),
@@ -1510,6 +1521,9 @@ export class Simulator extends RapidElement {
1510
1521
  }
1511
1522
 
1512
1523
  private async handleReset() {
1524
+ // invalidate any in-flight sprint responses from the old session
1525
+ this.sessionGeneration++;
1526
+
1513
1527
  // reset simulation state
1514
1528
  this.events = [];
1515
1529
  this.session = null;
@@ -2068,15 +2082,29 @@ export class Simulator extends RapidElement {
2068
2082
  }
2069
2083
  };
2070
2084
 
2085
+ const generation = this.sessionGeneration;
2071
2086
  try {
2072
2087
  const response = await postJSON(this.endpoint, body);
2073
2088
 
2074
2089
  // add a small delay before showing the reply to simulate typing
2075
2090
  await new Promise((resolve) => setTimeout(resolve, 400));
2076
2091
 
2092
+ // the simulation was reset while we were waiting, this reply belongs
2093
+ // to the old session. we deliberately don't touch `sprinting` here:
2094
+ // handleReset already cleared it when it bumped the generation (and
2095
+ // clearing it now could stomp a sprint the new session has started).
2096
+ // anything else that invalidates a generation must likewise reset
2097
+ // `sprinting` or the input stays disabled.
2098
+ if (generation !== this.sessionGeneration) {
2099
+ return;
2100
+ }
2101
+
2077
2102
  // pass null for msgInEvt since we already added it
2078
2103
  this.updateRunContext(response.json as RunContext, null);
2079
2104
  } catch (error) {
2105
+ if (generation !== this.sessionGeneration) {
2106
+ return;
2107
+ }
2080
2108
  console.error('Failed to resume simulation:', error);
2081
2109
  const errorEvent = {
2082
2110
  uuid: generateUUIDv7(),
@@ -43,7 +43,8 @@ export const PILL_TYPES: ReadonlySet<string> = new Set([
43
43
  'keyword',
44
44
  'channel',
45
45
  'topic',
46
- 'campaign'
46
+ 'campaign',
47
+ 'removed'
47
48
  ]);
48
49
 
49
50
  /** Default icon name for each pill variant. Used when a consumer
@@ -145,6 +146,17 @@ export const pillVariants = css`
145
146
  border-color: #fde68a;
146
147
  --icon-color: var(--field);
147
148
  }
149
+ .pill-removed {
150
+ /* Removal pill — used when an entity is taken away (e.g. removed
151
+ from a group in the chat history). Fixed at the Tailwind red
152
+ ramp (red-100 bg / red-200 border / red-800 text) for the same
153
+ reason .pill-field is fixed at yellow: color-mixing a light red
154
+ into white washes out before it reads as "red". */
155
+ background: #fee2e2;
156
+ color: #991b1b;
157
+ border-color: #fecaca;
158
+ --icon-color: #991b1b;
159
+ }
148
160
  .pill-keyword {
149
161
  background: var(--sunken);
150
162
  color: var(--text-1);
@@ -432,6 +432,8 @@ export default {
432
432
  files: ['**/test/**/*.test.ts', '!**/test/utils.test.ts'],
433
433
  nodeResolve: true,
434
434
  concurrency: 4,
435
+ // screenshot-heavy test files can exceed the default 120s on slower CI runners
436
+ testsFinishTimeout: 240000,
435
437
  filterBrowserLogs(log) {
436
438
  return !log.args.some(
437
439
  (arg) => typeof arg === 'string' && arg.includes('Lit is in dev mode')
package/xliff/es.xlf CHANGED
@@ -24,9 +24,6 @@
24
24
  <trans-unit id="sea4f08110bb8f15d">
25
25
  <source>Remove</source>
26
26
  </trans-unit>
27
- <trans-unit id="s81f17cfc89a04338">
28
- <source>Interrupt flow</source>
29
- </trans-unit>
30
27
  <trans-unit id="s122d4de68bcfcdf4">
31
28
  <source>It's okay to restart</source>
32
29
  </trans-unit>
@@ -39,6 +36,12 @@
39
36
  <trans-unit id="s6838d20b10f7512a">
40
37
  <source>and start this one</source>
41
38
  </trans-unit>
39
+ <trans-unit id="s22965bb9808befb0">
40
+ <source>Interrupt</source>
41
+ </trans-unit>
42
+ <trans-unit id="s7722a91d3a512442">
43
+ <source>Last seen <x id="0" equiv-text="${lastSeen.duration}"/></source>
44
+ </trans-unit>
42
45
  </body>
43
46
  </file>
44
47
  </xliff>
package/xliff/fr.xlf CHANGED
@@ -23,9 +23,6 @@
23
23
  <trans-unit id="sea4f08110bb8f15d">
24
24
  <source>Remove</source>
25
25
  </trans-unit>
26
- <trans-unit id="s81f17cfc89a04338">
27
- <source>Interrupt flow</source>
28
- </trans-unit>
29
26
  <trans-unit id="s122d4de68bcfcdf4">
30
27
  <source>It's okay to restart</source>
31
28
  </trans-unit>
@@ -38,6 +35,12 @@
38
35
  <trans-unit id="s6838d20b10f7512a">
39
36
  <source>and start this one</source>
40
37
  </trans-unit>
38
+ <trans-unit id="s22965bb9808befb0">
39
+ <source>Interrupt</source>
40
+ </trans-unit>
41
+ <trans-unit id="s7722a91d3a512442">
42
+ <source>Last seen <x id="0" equiv-text="${lastSeen.duration}"/></source>
43
+ </trans-unit>
41
44
  </body>
42
45
  </file>
43
46
  </xliff>
package/xliff/pt.xlf CHANGED
@@ -23,9 +23,6 @@
23
23
  <trans-unit id="sea4f08110bb8f15d">
24
24
  <source>Remove</source>
25
25
  </trans-unit>
26
- <trans-unit id="s81f17cfc89a04338">
27
- <source>Interrupt flow</source>
28
- </trans-unit>
29
26
  <trans-unit id="s122d4de68bcfcdf4">
30
27
  <source>It's okay to restart</source>
31
28
  </trans-unit>
@@ -38,6 +35,12 @@
38
35
  <trans-unit id="s6838d20b10f7512a">
39
36
  <source>and start this one</source>
40
37
  </trans-unit>
38
+ <trans-unit id="s22965bb9808befb0">
39
+ <source>Interrupt</source>
40
+ </trans-unit>
41
+ <trans-unit id="s7722a91d3a512442">
42
+ <source>Last seen <x id="0" equiv-text="${lastSeen.duration}"/></source>
43
+ </trans-unit>
41
44
  </body>
42
45
  </file>
43
46
  </xliff>