@nyaruka/temba-components 0.163.0 → 0.165.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 (66) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/DEV_DATA.md +11 -11
  3. package/README.md +4 -4
  4. package/TEST_OPTIMIZATION.md +8 -11
  5. package/dist/locales/es.js +4 -0
  6. package/dist/locales/es.js.map +1 -1
  7. package/dist/locales/fr.js +4 -0
  8. package/dist/locales/fr.js.map +1 -1
  9. package/dist/locales/pt.js +4 -0
  10. package/dist/locales/pt.js.map +1 -1
  11. package/dist/static/svg/index.svg +1 -1
  12. package/dist/temba-components.js +5266 -3031
  13. package/dist/temba-components.js.map +1 -1
  14. package/orca/setup.sh +2 -2
  15. package/package.json +13 -18
  16. package/scripts/dev-data-sync.mjs +4 -4
  17. package/src/Icons.ts +3 -2
  18. package/src/display/Chat.ts +3 -2
  19. package/src/display/Lightbox.ts +57 -109
  20. package/src/display/ProgressBar.ts +15 -2
  21. package/src/display/Thumbnail.ts +34 -7
  22. package/src/form/Compose.ts +31 -1
  23. package/src/form/ContactSearch.ts +229 -126
  24. package/src/interfaces.ts +131 -2
  25. package/src/layout/Card.ts +336 -0
  26. package/src/layout/CardLayout.ts +425 -0
  27. package/src/layout/CardStack.ts +131 -0
  28. package/src/layout/Dialog.ts +33 -24
  29. package/src/layout/HeaderBar.ts +41 -0
  30. package/src/layout/PageHeader.ts +5 -6
  31. package/src/layout/Resizer.ts +20 -0
  32. package/src/list/BroadcastList.ts +912 -0
  33. package/src/list/CampaignList.ts +144 -0
  34. package/src/list/ContactList.ts +3 -1
  35. package/src/list/ContentList.ts +46 -10
  36. package/src/list/FieldList.ts +1057 -0
  37. package/src/list/FlowList.ts +31 -7
  38. package/src/list/MsgList.ts +18 -12
  39. package/src/list/SortableList.ts +52 -8
  40. package/src/list/TembaList.ts +8 -0
  41. package/src/list/TembaMenu.ts +5 -38
  42. package/src/list/TriggerList.ts +538 -0
  43. package/src/live/CampaignEvents.ts +1108 -0
  44. package/src/live/ContactChat.ts +7 -1
  45. package/src/live/ContactDetails.ts +20 -14
  46. package/src/live/ContactFieldEditor.ts +7 -3
  47. package/src/live/ContactFields.ts +1 -1
  48. package/src/live/ContactNameFetch.ts +5 -2
  49. package/src/live/ContactNotepad.ts +88 -2
  50. package/src/live/ContactStoreElement.ts +15 -3
  51. package/src/live/ContactTimeline.ts +28 -7
  52. package/src/locales/es.ts +4 -0
  53. package/src/locales/fr.ts +4 -0
  54. package/src/locales/pt.ts +4 -0
  55. package/src/utils.ts +19 -0
  56. package/static/svg/index.svg +1 -1
  57. package/static/svg/packs/2-temba/star-filled.svg +3 -0
  58. package/static/svg/work/traced/star-filled.svg +1 -0
  59. package/static/svg/work/used/star-filled.svg +3 -0
  60. package/stress-test.js +3 -3
  61. package/temba-modules.ts +18 -0
  62. package/web-dev-server.config.mjs +54 -0
  63. package/web-test-runner.config.mjs +1 -1
  64. package/xliff/es.xlf +12 -0
  65. package/xliff/fr.xlf +12 -0
  66. package/xliff/pt.xlf +12 -0
@@ -226,12 +226,33 @@ export class ContactSearch extends FieldElement {
226
226
  border: 1px solid var(--color-borders);
227
227
  border-radius: var(--curvature);
228
228
  }
229
+
230
+ .interrupt-confirm {
231
+ display: flex;
232
+ align-items: center;
233
+ margin-top: 0.5em;
234
+ margin-left: 0.6em;
235
+ cursor: pointer;
236
+ }
229
237
  `;
230
238
  }
231
239
 
232
240
  @property({ type: Boolean })
233
241
  in_a_flow: boolean;
234
242
 
243
+ // recipients are locked, e.g. starting a specific contact from their read page -
244
+ // no recipient editing, filters or query editing
245
+ @property({ type: Boolean })
246
+ fixed: boolean;
247
+
248
+ // name of the flow the fixed contact is currently in, if any
249
+ @property({ type: String, attribute: 'current_flow' })
250
+ currentFlow: string;
251
+
252
+ // whether the user has explicitly confirmed interrupting the current flow
253
+ @property({ type: Boolean })
254
+ interruptConfirmed = false;
255
+
235
256
  @property({ type: Boolean })
236
257
  started_previously: boolean;
237
258
 
@@ -310,6 +331,30 @@ export class ContactSearch extends FieldElement {
310
331
  }
311
332
  }
312
333
 
334
+ protected willUpdate(changedProperties: Map<string, any>) {
335
+ super.willUpdate(changedProperties);
336
+
337
+ // reconcile the in_a_flow exclusion; outside of fixed mode it is user-controlled
338
+ // via the filter checkbox, so we only ever clear it when the option goes away
339
+ if (
340
+ changedProperties.has('in_a_flow') ||
341
+ changedProperties.has('interruptConfirmed') ||
342
+ changedProperties.has('fixed') ||
343
+ changedProperties.has('currentFlow')
344
+ ) {
345
+ if (!this.in_a_flow) {
346
+ delete this.exclusions['in_a_flow'];
347
+ } else if (this.fixed && this.currentFlow) {
348
+ // a fixed contact already in a flow stays excluded until interruption is confirmed
349
+ if (this.interruptConfirmed) {
350
+ delete this.exclusions['in_a_flow'];
351
+ } else {
352
+ this.exclusions['in_a_flow'] = true;
353
+ }
354
+ }
355
+ }
356
+ }
357
+
313
358
  public updated(changedProperties: Map<string, any>) {
314
359
  super.updated(changedProperties);
315
360
 
@@ -317,12 +362,6 @@ export class ContactSearch extends FieldElement {
317
362
  this.handleRecipientsChanged();
318
363
  }
319
364
 
320
- // if we remove the in_a_flow option, make sure it's not part of our exclusions
321
- if (changedProperties.has('in_a_flow') && !this.in_a_flow) {
322
- delete this.exclusions['in_a_flow'];
323
- this.requestUpdate('exclusions');
324
- }
325
-
326
365
  if (
327
366
  (changedProperties.has('query') && this.advanced) ||
328
367
  (changedProperties.has('refreshKey') && this.refreshKey !== '0')
@@ -455,6 +494,32 @@ export class ContactSearch extends FieldElement {
455
494
  }
456
495
  }
457
496
 
497
+ private async handleInterruptChanged(evt: any) {
498
+ const checkbox = evt.target as Checkbox;
499
+ this.interruptConfirmed = checkbox.checked;
500
+
501
+ // wait for willUpdate() to reconcile our exclusions
502
+ await this.updateComplete;
503
+
504
+ this.setValue({
505
+ advanced: this.advanced,
506
+ query: this.query,
507
+ exclusions: this.exclusions,
508
+ recipients: this.recipients
509
+ });
510
+
511
+ // we already know whether our fixed contacts will be included so no need to re-run
512
+ // the search - just let the modal know, counting contacts only since a group entry
513
+ // doesn't tell us its membership size
514
+ const contactCount = this.recipients.filter(
515
+ (value: OmniOption) => value.type === 'contact'
516
+ ).length;
517
+ this.fireCustomEvent(CustomEventType.ContentChanged, {
518
+ ...(this.summary || {}),
519
+ total: this.interruptConfirmed ? contactCount : 0
520
+ });
521
+ }
522
+
458
523
  private handleExclusionChanged(evt: any) {
459
524
  if (evt.target.tagName === 'TEMBA-CHECKBOX') {
460
525
  const ex = JSON.stringify(this.exclusions);
@@ -487,7 +552,7 @@ export class ContactSearch extends FieldElement {
487
552
 
488
553
  public renderWidget(): TemplateResult {
489
554
  let summary: TemplateResult;
490
- if (this.summary) {
555
+ if (this.summary && !this.fixed) {
491
556
  if (!this.summary.error) {
492
557
  const count = this.summary.total || 0;
493
558
 
@@ -544,132 +609,170 @@ export class ContactSearch extends FieldElement {
544
609
  )}`;
545
610
  }
546
611
 
612
+ const interruptConfirm =
613
+ this.fixed && this.currentFlow && this.in_a_flow && !this.fetching
614
+ ? html`<div
615
+ class="interrupt-confirm"
616
+ @click=${this.handleActivityLabelClicked}
617
+ >
618
+ <temba-checkbox
619
+ name="interrupt"
620
+ ?checked=${this.interruptConfirmed}
621
+ @change=${this.handleInterruptChanged}
622
+ ></temba-checkbox>
623
+ <div>
624
+ ${this.flow && this.flow.name === this.currentFlow
625
+ ? html`${msg("It's okay to restart")}
626
+ <b>${this.currentFlow}</b> ${msg('from the beginning')}`
627
+ : html`${msg("It's okay to interrupt")}
628
+ <b>${this.currentFlow}</b> ${msg('and start this one')}`}
629
+ </div>
630
+ </div>`
631
+ : null;
632
+
547
633
  return html`
548
634
  ${
549
- this.advanced
550
- ? html`<div class="query">
551
- <temba-textinput
552
- .helpText=${this.helpText}
553
- .widgetOnly=${this.widgetOnly}
554
- .errors=${this.errors}
555
- name=${this.name}
556
- .inputRoot=${this}
557
- @input=${this.handleQueryChange}
558
- placeholder=${this.placeholder}
559
- .value=${this.query}
560
- textarea
561
- autogrow
562
- >
563
- </temba-textinput>
564
- </div>`
565
- : html`<temba-omnibox
566
- placeholder="Search for contacts or groups"
567
- widget_only=""
568
- groups=""
569
- contacts=""
570
- label="Recipients"
571
- help_text="The contacts to send the message to."
572
- .errors=${this.errors}
573
- id="recipients"
574
- name="recipients"
575
- .values=${this.recipients}
576
- endpoint="/contact/omnibox/?"
577
- @change=${this.handleRecipientsChanged}
578
- >
579
- </temba-omnibox>
580
-
581
- ${this.not_seen_since_days ||
582
- this.in_a_flow ||
583
- this.started_previously
584
- ? html`
585
- <div class="filters">
586
- <div
587
- style="display:flex;font-size:1em;margin-bottom:0.5em"
588
- >
589
- <temba-icon size="1" name="filter"></temba-icon>
590
- <div style="margin-left:0.5em">
591
- Only include contacts who...
635
+ this.fixed
636
+ ? null
637
+ : this.advanced
638
+ ? html`<div class="query">
639
+ <temba-textinput
640
+ .helpText=${this.helpText}
641
+ .widgetOnly=${this.widgetOnly}
642
+ .errors=${this.errors}
643
+ name=${this.name}
644
+ .inputRoot=${this}
645
+ @input=${this.handleQueryChange}
646
+ placeholder=${this.placeholder}
647
+ .value=${this.query}
648
+ textarea
649
+ autogrow
650
+ >
651
+ </temba-textinput>
652
+ </div>`
653
+ : html`<temba-omnibox
654
+ placeholder="Search for contacts or groups"
655
+ widget_only=""
656
+ groups=""
657
+ contacts=""
658
+ label="Recipients"
659
+ help_text="The contacts to send the message to."
660
+ .errors=${this.errors}
661
+ id="recipients"
662
+ name="recipients"
663
+ .values=${this.recipients}
664
+ endpoint="/contact/omnibox/?"
665
+ @change=${this.handleRecipientsChanged}
666
+ >
667
+ </temba-omnibox>
668
+
669
+ ${this.not_seen_since_days ||
670
+ this.in_a_flow ||
671
+ this.started_previously
672
+ ? html`
673
+ <div class="filters">
674
+ <div
675
+ style="display:flex;font-size:1em;margin-bottom:0.5em"
676
+ >
677
+ <temba-icon size="1" name="filter"></temba-icon>
678
+ <div style="margin-left:0.5em">
679
+ Only include contacts who...
680
+ </div>
592
681
  </div>
593
- </div>
594
- ${this.in_a_flow
595
- ? html`<temba-checkbox
596
- name="in_a_flow"
597
- label="${msg('Are not currently in a flow')}"
598
- ?checked=${this.exclusions['in_a_flow']}
599
- @change=${this.handleExclusionChanged}
600
- ></temba-checkbox>`
601
- : null}
602
- ${this.not_seen_since_days
603
- ? html`
604
- <div
605
- class="activity-select"
606
- @click=${this.handleActivityLabelClicked}
607
- >
608
- <temba-checkbox
609
- style="display:inline;"
610
- name="not_seen_since_days"
611
- ?checked=${notSeenSinceDays}
612
- @change=${this.handleExclusionChanged}
682
+ ${this.in_a_flow
683
+ ? html`<temba-checkbox
684
+ name="in_a_flow"
685
+ label="${msg('Are not currently in a flow')}"
686
+ ?checked=${this.exclusions['in_a_flow']}
687
+ @change=${this.handleExclusionChanged}
688
+ ></temba-checkbox>`
689
+ : null}
690
+ ${this.not_seen_since_days
691
+ ? html`
692
+ <div
693
+ class="activity-select"
694
+ @click=${this.handleActivityLabelClicked}
613
695
  >
614
- </temba-checkbox>
615
-
616
- <div>
617
- ${msg('Have sent a message in the last')}
696
+ <temba-checkbox
697
+ style="display:inline;"
698
+ name="not_seen_since_days"
699
+ ?checked=${notSeenSinceDays}
700
+ @change=${this.handleExclusionChanged}
701
+ >
702
+ </temba-checkbox>
703
+
704
+ <div>
705
+ ${msg('Have sent a message in the last')}
706
+ </div>
707
+
708
+ <temba-select
709
+ style="margin-left:0.5em"
710
+ class="small-select"
711
+ @change=${this.handleActivityLevelChanged}
712
+ ?disabled=${!notSeenSinceDays}
713
+ >
714
+ <temba-option
715
+ name="90 days"
716
+ value="90"
717
+ ?selected=${notSeenSinceDays === 90}
718
+ ></temba-option>
719
+ <temba-option
720
+ name="180 days"
721
+ value="180"
722
+ ?selected=${notSeenSinceDays === 180}
723
+ ></temba-option>
724
+ <temba-option
725
+ name="Year"
726
+ value="365"
727
+ ?selected=${notSeenSinceDays === 365}
728
+ ></temba-option>
729
+ </temba-select>
730
+ <div></div>
618
731
  </div>
619
-
620
- <temba-select
621
- style="margin-left:0.5em"
622
- class="small-select"
623
- @change=${this.handleActivityLevelChanged}
624
- ?disabled=${!notSeenSinceDays}
625
- >
626
- <temba-option
627
- name="90 days"
628
- value="90"
629
- ?selected=${notSeenSinceDays === 90}
630
- ></temba-option>
631
- <temba-option
632
- name="180 days"
633
- value="180"
634
- ?selected=${notSeenSinceDays === 180}
635
- ></temba-option>
636
- <temba-option
637
- name="Year"
638
- value="365"
639
- ?selected=${notSeenSinceDays === 365}
640
- ></temba-option>
641
- </temba-select>
642
- <div></div>
643
- </div>
644
- `
645
- : null}
646
- ${this.started_previously
647
- ? html`<temba-checkbox
648
- name="started_previously"
649
- label="${msg(
650
- 'Have not started this flow in the last 90 days'
651
- )}"
652
- ?checked=${this.exclusions['started_previously']}
653
- @change=${this.handleExclusionChanged}
654
- ></temba-checkbox>`
655
- : null}
656
- </div>
657
- `
658
- : null} `
732
+ `
733
+ : null}
734
+ ${this.started_previously
735
+ ? html`<temba-checkbox
736
+ name="started_previously"
737
+ label="${msg(
738
+ 'Have not started this flow in the last 90 days'
739
+ )}"
740
+ ?checked=${this.exclusions['started_previously']}
741
+ @change=${this.handleExclusionChanged}
742
+ ></temba-checkbox>`
743
+ : null}
744
+ </div>
745
+ `
746
+ : null} `
659
747
  }
660
748
  </div>
661
- <div
662
- class="results ${getClasses({
663
- fetching: this.fetching,
664
- initialized: this.initialized || this.fetching,
665
- empty:
666
- ((this.summary && this.summary.error) || !this.summary) &&
667
- !this.fetching
668
- })}"
669
- >
670
- <temba-loading units="6" size="8"></temba-loading>
671
- <div class="summary ${this.expanded ? 'expanded' : ''}">${summary}</div>
672
- </div>
749
+ ${interruptConfirm}
750
+ ${
751
+ this.fixed
752
+ ? html`<div
753
+ class="results ${getClasses({
754
+ fetching: this.fetching,
755
+ initialized: this.fetching,
756
+ empty: !this.fetching
757
+ })}"
758
+ >
759
+ <temba-loading units="6" size="8"></temba-loading>
760
+ </div>`
761
+ : html`<div
762
+ class="results ${getClasses({
763
+ fetching: this.fetching,
764
+ initialized: this.initialized || this.fetching,
765
+ empty:
766
+ ((this.summary && this.summary.error) || !this.summary) &&
767
+ !this.fetching
768
+ })}"
769
+ >
770
+ <temba-loading units="6" size="8"></temba-loading>
771
+ <div class="summary ${this.expanded ? 'expanded' : ''}">
772
+ ${summary}
773
+ </div>
774
+ </div>`
775
+ }
673
776
  ${blockers}
674
777
  ${
675
778
  !blockers && this.summary && this.summary.warnings
package/src/interfaces.ts CHANGED
@@ -54,6 +54,25 @@ export interface ScheduledEvent {
54
54
  message?: string;
55
55
  }
56
56
 
57
+ // one event definition in a campaign's schedule, offset from a date field
58
+ // on the contact - consumed by the temba-campaign-events component
59
+ export interface CampaignScheduleEvent {
60
+ uuid: string;
61
+ type: 'message' | 'flow';
62
+ status: 'ready' | 'scheduling';
63
+ offset: number;
64
+ unit: string;
65
+ offset_display: string;
66
+ delivery_hour_display?: string;
67
+ relative_to: { key: string; name: string; system?: boolean };
68
+ flow?: ObjectReference;
69
+ message?: string;
70
+ count: number;
71
+ edit_url: string;
72
+ delete_url: string;
73
+ fires_url: string;
74
+ }
75
+
57
76
  export interface NamedUser extends User {
58
77
  name: string;
59
78
  }
@@ -120,6 +139,111 @@ export interface Flow {
120
139
  activity: number[];
121
140
  }
122
141
 
142
+ /** A single row in the campaign CRUDL list
143
+ * (`campaigns/campaign_list.html`). */
144
+ export interface Campaign {
145
+ uuid: string;
146
+ name: string;
147
+ /** The contact group the campaign schedules against. */
148
+ group: ObjectReference;
149
+ /** Number of events (messages / flow starts) in the campaign. */
150
+ events: number;
151
+ /** Contacts currently in the campaign's group. */
152
+ contacts: number;
153
+ modified_on: string;
154
+ }
155
+
156
+ /** A single row in the trigger CRUDL list
157
+ * (`triggers/trigger_list.html`): what starts the flow (type +
158
+ * per-type details), any channel / group filters, and the flow it
159
+ * starts. Triggers have no uuid — rows key off the numeric id. */
160
+ export interface Trigger {
161
+ id: number;
162
+ /** Trigger type slug — drives the leading row icon and the
163
+ * details cell (`keyword`, `catch_all`, `schedule`,
164
+ * `inbound_call`, `missed_call`, `new_conversation`, `referral`,
165
+ * `closed_ticket`, `opt_in`, `opt_out`). */
166
+ type: string;
167
+ /** The flow the trigger starts. */
168
+ flow: ObjectReference;
169
+ /** Channel the trigger is limited to, with its type icon. */
170
+ channel?: (ObjectReference & { icon?: string }) | null;
171
+ /** Groups the trigger is limited to. */
172
+ groups?: ObjectReference[];
173
+ /** Groups the trigger excludes. */
174
+ exclude_groups?: ObjectReference[];
175
+ /** Contacts a scheduled trigger starts directly. */
176
+ contacts?: ObjectReference[];
177
+ /** Keyword triggers only. */
178
+ keywords?: string[];
179
+ /** Keyword match type — `F` (starts with) or `O` (matches). */
180
+ match_type?: string | null;
181
+ /** Referral triggers only. */
182
+ referrer_id?: string | null;
183
+ /** Scheduled triggers only — `display` is the server-rendered
184
+ * human schedule ("each week on Monday"); `next_fire` is unset
185
+ * once the schedule is exhausted or paused. */
186
+ schedule?: {
187
+ repeat_period?: string;
188
+ display?: string;
189
+ next_fire?: string | null;
190
+ } | null;
191
+ priority?: number;
192
+ created_on?: string;
193
+ }
194
+
195
+ /** A single row in the broadcast CRUDL lists — both the scheduled
196
+ * list (`msgs/broadcast_scheduled.html`) and the sent list
197
+ * (`msgs/broadcast_list.html`). Broadcasts key off the numeric id;
198
+ * content fields carry the base-language translation. */
199
+ export interface Broadcast {
200
+ id: number;
201
+ /** Server status slug — `pending`, `queued`, `started`,
202
+ * `completed`, `failed` or `interrupted`. Scheduled broadcasts
203
+ * that haven't fired yet sit at `pending`. */
204
+ status?: string;
205
+ /** Send progress for an in-flight broadcast — total recipients
206
+ * (-1 until the org's contact count resolves) and messages
207
+ * created so far. */
208
+ progress?: { total: number; started: number };
209
+ /** Base-language message text. */
210
+ text?: string;
211
+ /** Base-language attachments (`{content_type, url}` objects or
212
+ * `contentType:url` strings). */
213
+ attachments?: (string | Attachment)[];
214
+ /** Base-language quick replies. */
215
+ quick_replies?: string[];
216
+ /** The opt-in the broadcast requests, when it is one. */
217
+ optin?: ObjectReference | null;
218
+ /** The WhatsApp template the broadcast sends, when it uses one. */
219
+ template?: ObjectReference | null;
220
+ /** Recipient groups. */
221
+ groups?: ObjectReference[];
222
+ /** Recipient contacts. */
223
+ contacts?: ObjectReference[];
224
+ /** Raw recipient URNs (editors/admins only). */
225
+ urns?: string[];
226
+ /** Recipient contact query, for query-addressed broadcasts. */
227
+ query?: string | null;
228
+ /** Human-readable exclusion descriptions ("Skip inactive
229
+ * contacts", ...) rendered in the detail view. */
230
+ exclusions?: string[];
231
+ /** Scheduled broadcasts only — `display` is the server-rendered
232
+ * human repeat ("each week on Monday, Wednesday"); `next_fire` is
233
+ * null once the schedule is exhausted or paused. */
234
+ schedule?: {
235
+ repeat_period?: string;
236
+ display?: string;
237
+ next_fire?: string | null;
238
+ } | null;
239
+ /** Messages created by a sent broadcast. */
240
+ msg_count?: number;
241
+ created_on?: string;
242
+ /** Email of the user who created the broadcast. */
243
+ created_by?: string | null;
244
+ modified_on?: string;
245
+ }
246
+
123
247
  export interface Msg {
124
248
  /** Numeric id — present on persisted messages (the CRUDL list
125
249
  * keys rows off it); absent on outbound drafts. */
@@ -137,7 +261,10 @@ export interface Msg {
137
261
  /** Message type as exposed by the messages CRUDL endpoint
138
262
  * (`text` / `optin` / …); mirrors `type` for that surface. */
139
263
  msg_type?: string;
140
- attachments: string[];
264
+ /** Attachments as exposed by the messages CRUDL endpoint, which
265
+ * serializes each as a `{content_type, url}` object; some other
266
+ * message surfaces carry them as `contentType:url` strings. */
267
+ attachments: (string | Attachment)[];
141
268
  /** Labels applied to the message. */
142
269
  labels?: ObjectReference[];
143
270
  /** The flow the message was sent from, when there is one. */
@@ -192,6 +319,8 @@ export interface ContactGroup {
192
319
  export interface URN {
193
320
  scheme: string;
194
321
  path: string;
322
+ display?: string | null;
323
+ channel?: ObjectReference | null;
195
324
  }
196
325
 
197
326
  export interface Group {
@@ -224,7 +353,7 @@ export interface Contact {
224
353
  uuid: string;
225
354
  stopped: boolean;
226
355
  blocked: boolean;
227
- urns: string[];
356
+ urns: URN[];
228
357
  language?: string;
229
358
  fields: { [key: string]: string };
230
359
  groups: Group[];