@nyaruka/temba-components 0.156.1 → 0.156.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.156.1",
3
+ "version": "0.156.3",
4
4
  "description": "Web components to support rapidpro and related projects",
5
5
  "author": "Nyaruka <code@nyaruka.coim>",
6
6
  "main": "dist/index.js",
@@ -583,34 +583,36 @@ export class Chat extends RapidElement {
583
583
  text-align: center;
584
584
  font-size: 11px;
585
585
  color: #8e8e93;
586
+ max-width: 100%;
587
+ overflow: hidden;
586
588
  }
587
589
 
588
590
  .event .webhook-event {
589
591
  display: inline-flex;
590
- align-items: center;
592
+ align-items: flex-start;
591
593
  gap: 6px;
594
+ max-width: 100%;
595
+ min-width: 0;
592
596
  }
593
597
 
594
- .event .webhook-event-log-link {
595
- all: unset;
596
- display: inline-flex;
597
- align-items: center;
598
- justify-content: center;
599
- color: #9ca3af;
600
- cursor: pointer;
601
- line-height: 1;
602
- border-radius: 4px;
603
- transition: color var(--animation-time, 200ms) ease;
598
+ .event .webhook-event-text {
599
+ overflow: hidden;
600
+ display: -webkit-box;
601
+ -webkit-line-clamp: 2;
602
+ -webkit-box-orient: vertical;
603
+ word-break: break-all;
604
+ min-width: 0;
605
+ padding: 4px 0;
604
606
  }
605
607
 
606
- .event .webhook-event-log-link:hover,
607
- .event .webhook-event-log-link:focus-visible {
608
- color: var(--color-primary-dark, #007aff);
608
+ .event .webhook-event-url {
609
+ color: inherit;
610
+ text-decoration: underline;
611
+ cursor: pointer;
609
612
  }
610
613
 
611
- .event .webhook-event-log-link:focus-visible {
612
- outline: 1px solid currentColor;
613
- outline-offset: 2px;
614
+ .event .webhook-event-url:hover {
615
+ text-decoration: none;
614
616
  }
615
617
 
616
618
  .event p {
@@ -389,7 +389,12 @@ export const renderResthookCalled = (event: any): TemplateResult | null => {
389
389
  };
390
390
 
391
391
  export const renderWebhookCalled = (event: any): TemplateResult | null => {
392
- return html`<div>Called <strong>${event.url}</strong></div>`;
392
+ const maxLen = 50;
393
+ const displayUrl =
394
+ event.url && event.url.length > maxLen
395
+ ? event.url.slice(0, maxLen) + '...'
396
+ : event.url;
397
+ return html`<div>Called <strong>${displayUrl}</strong></div>`;
393
398
  };
394
399
 
395
400
  export const renderServiceCalled = (event: any): TemplateResult | null => {
@@ -361,6 +361,9 @@ export class CanvasNode extends RapidElement {
361
361
 
362
362
  .router .body {
363
363
  padding: 0.75em;
364
+ }
365
+
366
+ .router .body > div {
364
367
  max-width: 180px;
365
368
  }
366
369
 
@@ -368,7 +371,32 @@ export class CanvasNode extends RapidElement {
368
371
  font-weight: 500;
369
372
  display: inline-block;
370
373
  }
371
-
374
+
375
+ .router {
376
+ display: flex;
377
+ flex-direction: column;
378
+ }
379
+
380
+ .rules-count {
381
+ position: absolute;
382
+ right: 4px;
383
+ top: 50%;
384
+ transform: translateY(-50%);
385
+ background: #fff8dc;
386
+ border-radius: 10px;
387
+ min-width: 18px;
388
+ height: 18px;
389
+ padding: 0 5px;
390
+ font-size: 11px;
391
+ font-weight: 600;
392
+ color: #333;
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: center;
396
+ line-height: 1;
397
+ box-sizing: border-box;
398
+ }
399
+
372
400
  .exit-wrapper {
373
401
  display: flex;
374
402
  justify-content: center;
@@ -1611,9 +1639,10 @@ export class CanvasNode extends RapidElement {
1611
1639
  ? ACTION_GROUP_METADATA[config.group]?.color ||
1612
1640
  SPLIT_GROUP_METADATA[config.group]?.color
1613
1641
  : '#aaaaaa';
1642
+ const untranslatedRules = this.getUntranslatedRulesCount();
1614
1643
  return html`<div
1615
1644
  class="cn-title ${isRemoving ? 'removing' : ''}"
1616
- style="background:${color}"
1645
+ style="background:${color}; position: relative;"
1617
1646
  >
1618
1647
  <div class="title-spacer"></div>
1619
1648
  <div class="name">
@@ -1630,6 +1659,9 @@ export class CanvasNode extends RapidElement {
1630
1659
  >
1631
1660
 
1632
1661
  </div>
1662
+ ${untranslatedRules > 0
1663
+ ? html`<div class="rules-count">${untranslatedRules}</div>`
1664
+ : null}
1633
1665
  </div>`;
1634
1666
  }
1635
1667
 
@@ -1752,6 +1784,21 @@ export class CanvasNode extends RapidElement {
1752
1784
  return result;
1753
1785
  }
1754
1786
 
1787
+ private getUntranslatedRulesCount(): number {
1788
+ if (!this.isTranslating || !this.ui?.config?.localizeRules) return 0;
1789
+ const cases = this.node?.router?.cases;
1790
+ if (!cases?.length) return 0;
1791
+
1792
+ const langLocalization =
1793
+ this.flowDefinition?.localization?.[this.languageCode] || {};
1794
+
1795
+ return cases.filter((c) => {
1796
+ if (!c.arguments?.length || !c.arguments.some((a) => a)) return false;
1797
+ const localized = langLocalization[c.uuid]?.arguments;
1798
+ return !Array.isArray(localized) || !localized.some((a: string) => a);
1799
+ }).length;
1800
+ }
1801
+
1755
1802
  private renderRouter(router: Router, ui: NodeUI) {
1756
1803
  const nodeConfig = NODE_CONFIG[ui.type];
1757
1804
  if (nodeConfig) {
@@ -705,6 +705,12 @@ export class Editor extends RapidElement {
705
705
  border: 1px solid #d7dce2;
706
706
  }
707
707
 
708
+ .language-pill.complete {
709
+ background: #d4f5e0;
710
+ color: #1a7f37;
711
+ --icon-color: #1a7f37;
712
+ }
713
+
708
714
  .language-pill-caret {
709
715
  margin-left: 1px;
710
716
  --icon-color: currentColor;
@@ -720,6 +726,10 @@ export class Editor extends RapidElement {
720
726
  white-space: nowrap;
721
727
  }
722
728
 
729
+ .language-pill.complete .language-percent {
730
+ color: #1a7f37;
731
+ }
732
+
723
733
  .toolbar-zoom-level {
724
734
  font-size: 12px;
725
735
  min-width: 40px;
@@ -1717,12 +1727,16 @@ export class Editor extends RapidElement {
1717
1727
  }
1718
1728
 
1719
1729
  if (changes.has('showMessageTable') && !this.showMessageTable && this.plumber) {
1720
- // Canvas was re-added to the DOM; rebind the plumber and repaint connections
1730
+ // Canvas was re-added to the DOM; rebind the plumber, listeners, and repaint
1721
1731
  requestAnimationFrame(() => {
1722
1732
  const canvas = this.querySelector('#canvas');
1723
1733
  if (canvas) {
1724
1734
  this.plumber.setContainer(canvas as HTMLElement);
1725
1735
  this.plumber.repaintEverything();
1736
+ canvas.addEventListener('contextmenu', this.boundCanvasContextMenu);
1737
+ canvas.addEventListener('touchstart', this.boundCanvasTouchStart, {
1738
+ passive: false
1739
+ });
1726
1740
  }
1727
1741
  });
1728
1742
  }
@@ -6072,13 +6086,6 @@ export class Editor extends RapidElement {
6072
6086
  );
6073
6087
  const hasTranslations = progress.total > 0;
6074
6088
  const hasPendingTranslations = remainingTranslations > 0;
6075
- const autoTranslateButtonLabel = this.autoTranslating
6076
- ? 'Stop Auto Translate'
6077
- : 'Auto Translate';
6078
- const showAutoTranslate = !isBaseSelected && hasPendingTranslations;
6079
- const disableTranslationControls = Boolean(this.viewingRevision);
6080
- const autoTranslateButtonDisabled =
6081
- disableTranslationControls || (!this.autoTranslating && !hasTranslations);
6082
6089
 
6083
6090
  return html`
6084
6091
  <temba-floating-window
@@ -6116,25 +6123,13 @@ export class Editor extends RapidElement {
6116
6123
  ></temba-option>`
6117
6124
  )}
6118
6125
  </temba-select>
6119
- ${showAutoTranslate || this.autoTranslating
6120
- ? html`<button
6121
- class="auto-translate-button"
6122
- type="button"
6123
- ?disabled=${autoTranslateButtonDisabled}
6124
- @click=${this.handleAutoTranslateClick}
6125
- >
6126
- ${autoTranslateButtonLabel}
6127
- </button>`
6128
- : ''}
6126
+ ${''/* auto translate button hidden pending backend changes */}
6129
6127
  </div>
6130
6128
  <div
6131
6129
  class="localization-progress ${isBaseSelected ? 'disabled' : ''}"
6132
6130
  >
6133
6131
  <div class="localization-progress-summary">
6134
- ${this.autoTranslating
6135
- ? html`<temba-loading units="3" size="8"></temba-loading>
6136
- <span>Auto translating remaining text…</span>`
6137
- : isBaseSelected
6132
+ ${isBaseSelected
6138
6133
  ? html`<span
6139
6134
  >Select a language to see translation progress.</span
6140
6135
  >`
@@ -6146,11 +6141,7 @@ export class Editor extends RapidElement {
6146
6141
  html`<span>${progress.localized} of ${progress.total} items translated</span>`
6147
6142
  : html`<span>All items are translated.</span>`}
6148
6143
  </div>
6149
- ${this.autoTranslateError
6150
- ? html`<div class="auto-translate-error">
6151
- ${this.autoTranslateError}
6152
- </div>`
6153
- : ''}
6144
+ ${''/* auto translate error hidden pending backend changes */}
6154
6145
  <div class="localization-progress-bar-row">
6155
6146
  <div
6156
6147
  class="localization-progress-trigger"
@@ -6326,12 +6317,20 @@ export class Editor extends RapidElement {
6326
6317
  `;
6327
6318
  }
6328
6319
 
6320
+ const isComplete = option.percent === 100;
6321
+ const optionBg = isComplete ? '#d4f5e0' : '';
6322
+ const optionHoverBg = isComplete ? '#c0edce' : '';
6323
+ const optionRadius = isComplete ? 'border-radius:4px;' : '';
6324
+ const percentColor = isComplete ? 'color:#1a7f37;' : 'color:#5f6b7a;';
6325
+
6329
6326
  return html`
6330
6327
  <div
6331
- style="display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 10px;"
6328
+ style="display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 10px; ${optionBg ? `background:${optionBg};` : ''} ${optionRadius}"
6329
+ @mouseenter=${isComplete ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.background = optionHoverBg; } : null}
6330
+ @mouseleave=${isComplete ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.background = optionBg; } : null}
6332
6331
  >
6333
- <span>${option.name}</span>
6334
- <span style="font-size:11px; font-weight:600; color:#5f6b7a;"
6332
+ <span style="${isComplete ? 'color:#1a7f37;' : ''}">${option.name}</span>
6333
+ <span style="font-size:11px; font-weight:600; ${percentColor}"
6335
6334
  >${option.percent ?? 0}%</span
6336
6335
  >
6337
6336
  </div>
@@ -6426,7 +6425,7 @@ export class Editor extends RapidElement {
6426
6425
  'Change language',
6427
6426
  html`
6428
6427
  <button
6429
- class="language-pill ${isBaseSelected ? 'primary' : ''}"
6428
+ class="language-pill ${isBaseSelected ? 'primary' : percent === 100 ? 'complete' : ''}"
6430
6429
  id="language-btn"
6431
6430
  @click=${this.handleLanguageIconClick}
6432
6431
  aria-label="Change language"
@@ -6561,35 +6560,9 @@ export class Editor extends RapidElement {
6561
6560
  `;
6562
6561
  }
6563
6562
 
6564
- private renderToolbarTranslationTools(hasTranslations: boolean): TemplateResult {
6565
- const disableTranslationControls = Boolean(this.viewingRevision);
6566
- const autoTranslateLabel = this.autoTranslating
6567
- ? 'Stop auto translate'
6568
- : 'Auto translate';
6569
- return html`
6570
- <div class="toolbar-translation">
6571
- ${this.renderToolbarTip(
6572
- autoTranslateLabel,
6573
- html`
6574
- <button
6575
- class="toolbar-btn language-tool ${this.autoTranslating
6576
- ? 'active'
6577
- : ''}"
6578
- @click=${this.handleAutoTranslateClick}
6579
- ?disabled=${disableTranslationControls ||
6580
- (!this.autoTranslating && !hasTranslations)}
6581
- aria-label=${autoTranslateLabel}
6582
- >
6583
- <temba-icon
6584
- name=${this.autoTranslating ? 'progress_spinner' : Icon.ai}
6585
- size="0.9"
6586
- ?spin=${this.autoTranslating}
6587
- ></temba-icon>
6588
- </button>
6589
- `
6590
- )}
6591
- </div>
6592
- `;
6563
+ private renderToolbarTranslationTools(_hasTranslations: boolean): TemplateResult {
6564
+ // auto translate button hidden pending backend changes
6565
+ return html``;
6593
6566
  }
6594
6567
 
6595
6568
  /**
@@ -6747,7 +6720,6 @@ export class Editor extends RapidElement {
6747
6720
 
6748
6721
  return html`${style} ${this.renderIssuesWindow()}
6749
6722
  ${this.renderRevisionsWindow()} ${this.renderLocalizationWindow()}
6750
- ${this.renderAutoTranslateDialog()}
6751
6723
  <div id="editor-container">
6752
6724
  ${this.renderToolbar()}
6753
6725
  <div id="editor">
@@ -221,10 +221,23 @@ function getActionSearchTexts(action: Action): string[] {
221
221
  return texts;
222
222
  }
223
223
 
224
- function getTableMessageSearchTexts(action: SendMsg): string[] {
224
+ function getTableSearchTexts(action: Action): string[] {
225
225
  const texts: string[] = [];
226
- if (action.text) texts.push(action.text);
227
- if (action.quick_replies) texts.push(...action.quick_replies);
226
+ const config = ACTION_CONFIG[action.type];
227
+ if (!config?.localizable) return texts;
228
+ const a = action as Record<string, any>;
229
+ for (const key of config.localizable) {
230
+ const val = a[key];
231
+ if (typeof val === 'string' && val.trim()) {
232
+ texts.push(val);
233
+ } else if (Array.isArray(val)) {
234
+ for (const item of val) {
235
+ if (typeof item === 'string' && item.trim()) {
236
+ texts.push(item);
237
+ }
238
+ }
239
+ }
240
+ }
228
241
  return texts;
229
242
  }
230
243
 
@@ -729,20 +742,37 @@ export class FlowSearch extends LitElement {
729
742
  const nodeUI = this.definition._ui?.nodes[node.uuid];
730
743
  const nodeType = nodeUI?.type || 'execute_actions';
731
744
 
732
- // Message table rows: one row per send_msg action
745
+ // Message table rows: one row per action with localizable fields
733
746
  if (node.actions) {
734
747
  for (const action of node.actions) {
735
- if (action.type !== 'send_msg') {
748
+ const actionConfig = ACTION_CONFIG[action.type];
749
+ if (
750
+ action.type !== 'send_msg' &&
751
+ (!actionConfig?.localizable || actionConfig.localizable.length === 0)
752
+ ) {
736
753
  continue;
737
754
  }
738
755
 
739
- const actionConfig = ACTION_CONFIG[action.type];
740
- const searchAction = localizeAction(
756
+ // Search both original and localized texts, but only add one result per action
757
+ const originalTexts = getTableSearchTexts(action);
758
+ const localizedAction = localizeAction(
741
759
  action,
742
760
  langLocalization?.[action.uuid]
743
- ) as SendMsg;
744
- const texts = getTableMessageSearchTexts(searchAction);
745
- for (const text of texts) {
761
+ );
762
+ const localizedTexts = getTableSearchTexts(localizedAction);
763
+
764
+ // Deduplicate: combine both, originals first
765
+ const allTexts: string[] = [];
766
+ const seen = new Set<string>();
767
+ for (const text of [...originalTexts, ...localizedTexts]) {
768
+ if (!seen.has(text)) {
769
+ seen.add(text);
770
+ allTexts.push(text);
771
+ }
772
+ }
773
+
774
+ let found = false;
775
+ for (const text of allTexts) {
746
776
  const idx = text.toLowerCase().indexOf(query);
747
777
  if (idx !== -1) {
748
778
  results.push({
@@ -754,9 +784,11 @@ export class FlowSearch extends LitElement {
754
784
  matchStart: idx,
755
785
  matchLength: query.length
756
786
  });
787
+ found = true;
757
788
  break;
758
789
  }
759
790
  }
791
+ if (found) continue;
760
792
  }
761
793
  }
762
794