@nyaruka/temba-components 0.164.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 (49) hide show
  1. package/CHANGELOG.md +13 -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 +1555 -484
  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/ProgressBar.ts +15 -2
  19. package/src/form/Compose.ts +31 -1
  20. package/src/form/ContactSearch.ts +229 -126
  21. package/src/interfaces.ts +52 -0
  22. package/src/layout/Card.ts +25 -0
  23. package/src/layout/CardLayout.ts +3 -3
  24. package/src/layout/HeaderBar.ts +4 -1
  25. package/src/list/BroadcastList.ts +912 -0
  26. package/src/list/ContentList.ts +46 -10
  27. package/src/list/FieldList.ts +1057 -0
  28. package/src/list/SortableList.ts +7 -2
  29. package/src/list/TembaList.ts +8 -0
  30. package/src/list/TembaMenu.ts +5 -38
  31. package/src/live/CampaignEvents.ts +33 -19
  32. package/src/live/ContactChat.ts +7 -1
  33. package/src/live/ContactDetails.ts +20 -13
  34. package/src/live/ContactFieldEditor.ts +7 -3
  35. package/src/live/ContactStoreElement.ts +3 -1
  36. package/src/live/ContactTimeline.ts +28 -7
  37. package/src/locales/es.ts +4 -0
  38. package/src/locales/fr.ts +4 -0
  39. package/src/locales/pt.ts +4 -0
  40. package/static/svg/index.svg +1 -1
  41. package/static/svg/packs/2-temba/star-filled.svg +3 -0
  42. package/static/svg/work/traced/star-filled.svg +1 -0
  43. package/static/svg/work/used/star-filled.svg +3 -0
  44. package/stress-test.js +3 -3
  45. package/temba-modules.ts +4 -0
  46. package/web-test-runner.config.mjs +1 -1
  47. package/xliff/es.xlf +12 -0
  48. package/xliff/fr.xlf +12 -0
  49. package/xliff/pt.xlf +12 -0
package/orca/setup.sh CHANGED
@@ -3,7 +3,7 @@
3
3
  # Orca setup hook for a temba-components worktree.
4
4
  # Symlinks shared utility files from nyaruka/utils, ensures the devcontainer
5
5
  # is built/running (recreating it if the bind mounts are stale), and installs
6
- # pnpm dependencies inside the worktree.
6
+ # bun dependencies inside the worktree.
7
7
  #
8
8
  # Usage:
9
9
  # ./orca/setup.sh # run directly
@@ -75,7 +75,7 @@ esac
75
75
  # Install dependencies inside the worktree.
76
76
  docker exec "$CONTAINER_NAME" bash -c '
77
77
  cd "/workspaces/worktrees/temba-components/'"$WORKSPACE_NAME"'"
78
- pnpm install
78
+ bun install
79
79
  '
80
80
 
81
81
  echo "Worktree '$WORKSPACE_NAME' ready for development"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.164.0",
3
+ "version": "0.165.0",
4
4
  "description": "Web components to support rapidpro and related projects",
5
5
  "author": "Nyaruka <code@nyaruka.coim>",
6
6
  "main": "dist/index.js",
@@ -9,25 +9,25 @@
9
9
  "license": "AGPL-3.0-only",
10
10
  "repository": "https://github.com/nyaruka/temba-components/",
11
11
  "scripts": {
12
- "start": "concurrently --kill-others --names tsc,web-dev-server \"pnpm tsc:watch\" \"web-dev-server --app-index demo/index.html --node-resolve --watch --port 3010 --esbuild-target auto\"",
12
+ "start": "concurrently --kill-others --names tsc,web-dev-server \"bun run tsc:watch\" \"web-dev-server --app-index demo/index.html --node-resolve --watch --port 3010 --esbuild-target auto\"",
13
13
  "tsc:watch": "tsc --watch",
14
- "build": "rimraf dist && pnpm svg && tsc && rollup -c rollup.components.mjs ",
14
+ "build": "rimraf dist && bun run svg && tsc && rollup -c rollup.components.mjs ",
15
15
  "build-wc": "rollup -c rollup.webchat.mjs",
16
- "dev": "pnpm build && cp -R ./dist/* ../temba/rapidpro/node_modules/@nyaruka/temba-components/dist/ && cp -R ./dist/* ../floweditor/node_modules/@nyaruka/temba-components/dist/",
17
- "pre-commit": "pnpm locale:extract && pnpm locale:build && pnpm svg && git add ./src/Icons.ts ./static/svg/index.svg ./xliff ./src/locales && lint-staged",
16
+ "dev": "bun run build && cp -R ./dist/* ../temba/rapidpro/node_modules/@nyaruka/temba-components/dist/ && cp -R ./dist/* ../floweditor/node_modules/@nyaruka/temba-components/dist/",
17
+ "pre-commit": "bun run locale:extract && bun run locale:build && bun run svg && git add ./src/Icons.ts ./static/svg/index.svg ./xliff ./src/locales && lint-staged",
18
18
  "format:eslint": "eslint --ext .ts . --fix --ignore-path .gitignore --ignore-pattern \"src/locales/\"",
19
19
  "format:prettier": "prettier \"**/*.js\" \"**/*.ts\" \"!src/locales/**\" --config .prettierrc --write --ignore-path .gitignore",
20
- "format": "pnpm format:eslint && pnpm format:prettier",
20
+ "format": "bun run format:eslint && bun run format:prettier",
21
21
  "test": "wtr --node-resolve",
22
22
  "test:fast": "wtr --node-resolve --fast",
23
23
  "test:coverage": "wtr --node-resolve --coverage",
24
24
  "check-coverage": "node check-coverage.js",
25
- "validate": "pnpm format && pnpm build && pnpm test:coverage && pnpm check-coverage",
25
+ "validate": "bun run format && bun run build && bun run test:coverage && bun run check-coverage",
26
26
  "test:watch": "wtr --node-resolve --watch",
27
27
  "stress-test": "node stress-test.js",
28
28
  "svg": "rimraf static/svg/work && node svg.js --resolution=150 --output='./static/svg/index.svg' --usage='./src/Icons.ts'",
29
29
  "svg-wc": "rimraf static/svg/work && node svg.js --resolution=150 --output='./static/svg/webchat.svg' --usage='./src/webchat/index.ts'",
30
- "version": "pnpm run build && auto-changelog -p && git add CHANGELOG.md",
30
+ "version": "bun run build && auto-changelog -p && git add CHANGELOG.md",
31
31
  "locale:extract": "lit-localize extract --config localize.config.json",
32
32
  "locale:build": "echo 'Building localizations..' && lit-localize build --config localize.config.json > dist/locale.out 2>&1",
33
33
  "dev-data:sync": "node scripts/dev-data-sync.mjs copy",
@@ -112,14 +112,9 @@
112
112
  "typescript": "5.9.3",
113
113
  "yargs": "^17.7.2"
114
114
  },
115
- "pnpm": {
116
- "overrides": {
117
- "trim": "^0.0.3",
118
- "prismjs": "^1.23.0",
119
- "@open-wc/testing-helpers": "3.0.0",
120
- "@types/minimatch": "5.1.2",
121
- "@types/chai": "4.3.20"
122
- }
123
- },
124
- "packageManager": "pnpm@9.15.4"
115
+ "overrides": {
116
+ "@open-wc/testing-helpers": "3.0.0",
117
+ "@types/minimatch": "5.1.2",
118
+ "@types/chai": "4.3.20"
119
+ }
125
120
  }
@@ -117,7 +117,7 @@ function showStatus() {
117
117
 
118
118
  if (!fs.existsSync(DEV_DATA_DIR)) {
119
119
  console.log('❌ No development data directory found');
120
- console.log('💡 Run the dev server to initialize, or use "pnpm dev-data:reset"');
120
+ console.log('💡 Run the dev server to initialize, or use "bun run dev-data:reset"');
121
121
  return;
122
122
  }
123
123
 
@@ -175,8 +175,8 @@ switch (command) {
175
175
  console.log(' reset/wipe - Wipe dev data and restore defaults');
176
176
  console.log(' status - Show current status');
177
177
  console.log('');
178
- console.log('Usage: pnpm dev-data:copy');
179
- console.log(' pnpm dev-data:reset');
180
- console.log(' pnpm dev-data:status');
178
+ console.log('Usage: bun run dev-data:copy');
179
+ console.log(' bun run dev-data:reset');
180
+ console.log(' bun run dev-data:status');
181
181
  process.exit(1);
182
182
  }
package/src/Icons.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-duplicate-enum-values */
2
- // for cache busting we dynamically generate a fingerprint, use pnpm svg to update
3
- export const SVG_FINGERPRINT = '45268f1573f1e352c7d62232863725bb';
2
+ // for cache busting we dynamically generate a fingerprint, use bun run svg to update
3
+ export const SVG_FINGERPRINT = '27198f759c371df944f8ec5de0a29dd6';
4
4
 
5
5
  // only icons below are included in the sprite sheet
6
6
  export enum Icon {
@@ -132,6 +132,7 @@ export enum Icon {
132
132
  progress_spinner = 'refresh-cw-04',
133
133
  refresh = 'refresh-cw-05',
134
134
  featured = 'star-01',
135
+ featured_filled = 'star-filled',
135
136
  quick_replies = 'dotpoints-01',
136
137
  recording = 'microphone-01',
137
138
  resend = 'refresh-cw-05',
@@ -108,6 +108,12 @@ export class ProgressBar extends RapidElement {
108
108
  transition: flex-basis 2s;
109
109
  }
110
110
 
111
+ /* At 0% an animated meter keeps a sliver of bar so its moving
112
+ stripes show the work is underway rather than a dead track. */
113
+ .meter.zero > span {
114
+ min-width: 1.25em;
115
+ }
116
+
111
117
  .meter .incomplete {
112
118
  flex-grow: 1;
113
119
  }
@@ -163,6 +169,11 @@ export class ProgressBar extends RapidElement {
163
169
  @property({ type: Boolean })
164
170
  showPercentage = false;
165
171
 
172
+ /** Suppresses the trailing percentage / countdown box outright —
173
+ * for compact placements where the meter alone is the display. */
174
+ @property({ type: Boolean })
175
+ hidePercentage = false;
176
+
166
177
  @property({ type: String })
167
178
  message: string;
168
179
 
@@ -194,7 +205,8 @@ export class ProgressBar extends RapidElement {
194
205
  const meterClasses = [
195
206
  'meter',
196
207
  this.done ? 'done' : '',
197
- this.animated ? '' : 'static'
208
+ this.animated ? '' : 'static',
209
+ this.animated && !this.done && this.pct === 0 ? 'zero' : ''
198
210
  ]
199
211
  .filter(Boolean)
200
212
  .join(' ');
@@ -208,7 +220,8 @@ export class ProgressBar extends RapidElement {
208
220
  <div class="incomplete"></div>
209
221
  </div>
210
222
 
211
- ${this.showPercentage || this.showEstimatedCompletion
223
+ ${(this.showPercentage || this.showEstimatedCompletion) &&
224
+ !this.hidePercentage
212
225
  ? html`<div class="etc">
213
226
  <div>
214
227
  ${this.estimatedCompletionDate &&
@@ -406,11 +406,25 @@ export class Compose extends FieldElement {
406
406
  if (!this.hasAttribute('tabindex')) {
407
407
  this.setAttribute('tabindex', '-1');
408
408
  }
409
+ // both phases: capture claims a send-Enter before the rich editor's
410
+ // own keydown inserts a newline (which would flash before the send
411
+ // clears it); bubble keeps Enter-to-send working from anywhere else
412
+ // in the compose after inner widgets have had their shot at it
413
+ this.addEventListener(
414
+ 'keydown',
415
+ this.handleHostKeyDown as EventListener,
416
+ true
417
+ );
409
418
  this.addEventListener('keydown', this.handleHostKeyDown as EventListener);
410
419
  }
411
420
 
412
421
  disconnectedCallback() {
413
422
  super.disconnectedCallback();
423
+ this.removeEventListener(
424
+ 'keydown',
425
+ this.handleHostKeyDown as EventListener,
426
+ true
427
+ );
414
428
  this.removeEventListener(
415
429
  'keydown',
416
430
  this.handleHostKeyDown as EventListener
@@ -449,10 +463,23 @@ export class Compose extends FieldElement {
449
463
  }
450
464
 
451
465
  if (evt.key === 'Enter' && !evt.shiftKey) {
466
+ // an Enter confirming an IME composition belongs to the editor
467
+ if (evt.isComposing || evt.keyCode === 229) {
468
+ return;
469
+ }
452
470
  if (this.showShortcuts) {
453
471
  return;
454
472
  }
455
473
  const editor = this.getMessageEditor();
474
+ // during capture only editor presses are claimed — anything else
475
+ // (quick replies, attachments) waits for the bubble so the widget
476
+ // it targets keeps first claim on the event
477
+ if (
478
+ evt.eventPhase === Event.CAPTURING_PHASE &&
479
+ !(editor && evt.composedPath().includes(editor))
480
+ ) {
481
+ return;
482
+ }
456
483
  if (editor) {
457
484
  const richEdit = editor.getRichEditor();
458
485
  if (richEdit && richEdit.hasVisibleOptions()) {
@@ -460,7 +487,10 @@ export class Compose extends FieldElement {
460
487
  }
461
488
  }
462
489
  evt.preventDefault();
463
- evt.stopPropagation();
490
+ // immediate: the handler is registered for both phases, and an
491
+ // at-target event would otherwise reach the second registration
492
+ // and send twice
493
+ evt.stopImmediatePropagation();
464
494
  this.triggerSend();
465
495
  }
466
496
  };
@@ -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