@nyaruka/temba-components 0.156.2 → 0.156.4
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 +21 -0
- package/dist/temba-components.js +560 -391
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Chat.ts +19 -17
- package/src/events/eventRenderers.ts +6 -1
- package/src/flow/CanvasNode.ts +69 -2
- package/src/flow/Editor.ts +260 -510
- package/src/flow/EditorToolbar.ts +566 -0
- package/src/flow/FlowSearch.ts +42 -10
- package/src/flow/MessageTable.ts +332 -122
- package/src/flow/NodeEditor.ts +29 -4
- package/src/flow/StickyNote.ts +29 -5
- package/src/flow/actions/say_msg.ts +1 -0
- package/src/flow/actions/send_broadcast.ts +48 -0
- package/src/flow/actions/send_email.ts +42 -1
- package/src/flow/actions/send_msg.ts +1 -0
- package/src/flow/actions/set_contact_language.ts +4 -13
- package/src/flow/actions/set_run_result.ts +1 -0
- package/src/flow/nodes/shared-rules.ts +1 -0
- package/src/flow/nodes/shared.ts +1 -0
- package/src/flow/nodes/wait_for_audio.ts +1 -0
- package/src/flow/utils.ts +11 -0
- package/src/simulator/Simulator.ts +17 -12
- package/temba-modules.ts +2 -0
- package/web-test-runner.config.mjs +2 -0
package/src/flow/MessageTable.ts
CHANGED
|
@@ -2,14 +2,15 @@ import { html, TemplateResult } from 'lit-html';
|
|
|
2
2
|
import { css } from 'lit';
|
|
3
3
|
import { RapidElement } from '../RapidElement';
|
|
4
4
|
import {
|
|
5
|
+
Action,
|
|
5
6
|
Category,
|
|
6
7
|
FlowDefinition,
|
|
7
|
-
Node
|
|
8
|
-
SendMsg
|
|
8
|
+
Node
|
|
9
9
|
} from '../store/flow-definition';
|
|
10
10
|
import { AppState, fromStore, zustand } from '../store/AppState';
|
|
11
11
|
import { CustomEventType } from '../interfaces';
|
|
12
12
|
import { renderHighlightedText } from './utils';
|
|
13
|
+
import { Icon } from '../Icons';
|
|
13
14
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
14
15
|
import { ACTION_GROUP_METADATA, SPLIT_GROUP_METADATA } from './types';
|
|
15
16
|
import { getOperatorConfig } from './operators';
|
|
@@ -18,7 +19,7 @@ import { getTranslatableCategoriesForNode } from './categoryLocalization';
|
|
|
18
19
|
interface MessageEntry {
|
|
19
20
|
kind: 'message';
|
|
20
21
|
node: Node;
|
|
21
|
-
action:
|
|
22
|
+
action: Action & Record<string, any>;
|
|
22
23
|
nodeIndex: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -66,6 +67,7 @@ export class MessageTable extends RapidElement {
|
|
|
66
67
|
padding: 12px 16px;
|
|
67
68
|
border-bottom: 1px solid #f0f0f0;
|
|
68
69
|
vertical-align: top;
|
|
70
|
+
position: relative;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
.message-table td.translation-td {
|
|
@@ -145,12 +147,27 @@ export class MessageTable extends RapidElement {
|
|
|
145
147
|
flex: 1;
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
.message-table td.rail-td {
|
|
151
|
+
padding: 8px 8px 8px 20px;
|
|
152
|
+
height: 1px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.message-table td.rail-td::before {
|
|
156
|
+
content: '';
|
|
157
|
+
position: absolute;
|
|
158
|
+
left: 10px;
|
|
159
|
+
top: 8px;
|
|
160
|
+
bottom: 8px;
|
|
161
|
+
width: 4px;
|
|
162
|
+
background: var(--node-rail-color, #d1d5db);
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
.message-cell {
|
|
149
166
|
cursor: pointer;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
border-radius: 6px;
|
|
168
|
+
padding: 12px 12px;
|
|
169
|
+
min-height: 100%;
|
|
170
|
+
box-sizing: border-box;
|
|
154
171
|
transition: background 0.15s;
|
|
155
172
|
word-wrap: break-word;
|
|
156
173
|
line-height: 1.5;
|
|
@@ -158,16 +175,6 @@ export class MessageTable extends RapidElement {
|
|
|
158
175
|
color: #333;
|
|
159
176
|
}
|
|
160
177
|
|
|
161
|
-
.message-cell::before {
|
|
162
|
-
content: '';
|
|
163
|
-
position: absolute;
|
|
164
|
-
left: 0;
|
|
165
|
-
top: 0;
|
|
166
|
-
bottom: 0;
|
|
167
|
-
width: 4px;
|
|
168
|
-
background: var(--node-rail-color, #d1d5db);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
178
|
.message-cell:hover {
|
|
172
179
|
background: #f5f8ff;
|
|
173
180
|
}
|
|
@@ -314,6 +321,33 @@ export class MessageTable extends RapidElement {
|
|
|
314
321
|
background: #fff;
|
|
315
322
|
}
|
|
316
323
|
|
|
324
|
+
.attachments {
|
|
325
|
+
display: flex;
|
|
326
|
+
flex-wrap: wrap;
|
|
327
|
+
gap: 4px;
|
|
328
|
+
margin-top: 0.5em;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.attachments.standalone {
|
|
332
|
+
margin-top: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.attachment-icon {
|
|
336
|
+
display: inline-flex;
|
|
337
|
+
align-items: center;
|
|
338
|
+
justify-content: center;
|
|
339
|
+
width: 32px;
|
|
340
|
+
height: 32px;
|
|
341
|
+
border: 1px solid #d1d5db;
|
|
342
|
+
border-radius: 6px;
|
|
343
|
+
background: #fafafa;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.attachment-icon temba-icon {
|
|
347
|
+
--icon-size: 18px;
|
|
348
|
+
--icon-color: #888;
|
|
349
|
+
}
|
|
350
|
+
|
|
317
351
|
.empty-state {
|
|
318
352
|
display: flex;
|
|
319
353
|
align-items: center;
|
|
@@ -345,11 +379,15 @@ export class MessageTable extends RapidElement {
|
|
|
345
379
|
for (const node of this.definition.nodes) {
|
|
346
380
|
nodeIndex++;
|
|
347
381
|
for (const action of node.actions || []) {
|
|
348
|
-
|
|
382
|
+
const actionConfig = ACTION_CONFIG[action.type];
|
|
383
|
+
if (
|
|
384
|
+
action.type === 'send_msg' ||
|
|
385
|
+
(actionConfig?.localizable && actionConfig.localizable.length > 0)
|
|
386
|
+
) {
|
|
349
387
|
entries.push({
|
|
350
388
|
kind: 'message',
|
|
351
389
|
node,
|
|
352
|
-
action
|
|
390
|
+
action,
|
|
353
391
|
nodeIndex
|
|
354
392
|
});
|
|
355
393
|
}
|
|
@@ -478,6 +516,189 @@ export class MessageTable extends RapidElement {
|
|
|
478
516
|
return typeof localization.name === 'string' ? localization.name : null;
|
|
479
517
|
}
|
|
480
518
|
|
|
519
|
+
private static ATTACHMENT_ICONS: Record<string, string> = {
|
|
520
|
+
image: Icon.attachment_image,
|
|
521
|
+
audio: Icon.attachment_audio,
|
|
522
|
+
video: Icon.attachment_video,
|
|
523
|
+
application: Icon.attachment_document
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
private getTranslatedField(actionUuid: string, field: string): string | null {
|
|
527
|
+
if (!this.isTranslating || !this.languageCode) return null;
|
|
528
|
+
const localization =
|
|
529
|
+
this.definition?.localization?.[this.languageCode]?.[actionUuid];
|
|
530
|
+
if (localization?.[field] && Array.isArray(localization[field])) {
|
|
531
|
+
return localization[field][0] || null;
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private getTranslatedArrayField(actionUuid: string, field: string): string[] {
|
|
537
|
+
if (!this.isTranslating || !this.languageCode) return [];
|
|
538
|
+
const localization =
|
|
539
|
+
this.definition?.localization?.[this.languageCode]?.[actionUuid];
|
|
540
|
+
if (Array.isArray(localization?.[field])) {
|
|
541
|
+
return localization[field];
|
|
542
|
+
}
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private hasAnyTranslation(entry: MessageEntry): boolean {
|
|
547
|
+
const config = ACTION_CONFIG[entry.action.type];
|
|
548
|
+
if (!config?.localizable) return false;
|
|
549
|
+
const localization =
|
|
550
|
+
this.definition?.localization?.[this.languageCode]?.[entry.action.uuid];
|
|
551
|
+
if (!localization) return false;
|
|
552
|
+
return config.localizable.some((key) => {
|
|
553
|
+
const val = localization[key];
|
|
554
|
+
if (Array.isArray(val)) {
|
|
555
|
+
return val.some((v: any) => typeof v === 'string' && v.trim());
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Whether an action should render as paired rows (one per localizable field).
|
|
563
|
+
* Used for actions with multiple text fields like send_email.
|
|
564
|
+
*/
|
|
565
|
+
private usesPairedRows(action: Action): boolean {
|
|
566
|
+
const config = ACTION_CONFIG[action.type];
|
|
567
|
+
if (!config?.localizable) return false;
|
|
568
|
+
// Count string-type localizable fields (exclude arrays like attachments, quick_replies)
|
|
569
|
+
const textFields = config.localizable.filter((key) => {
|
|
570
|
+
const fieldConfig = config.form?.[key];
|
|
571
|
+
return fieldConfig && (fieldConfig.type === 'text' || fieldConfig.type === 'textarea');
|
|
572
|
+
});
|
|
573
|
+
return textFields.length > 1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Get the localizable text fields for paired-row rendering.
|
|
578
|
+
*/
|
|
579
|
+
private getPairedFields(action: Action & Record<string, any>): Array<{
|
|
580
|
+
key: string;
|
|
581
|
+
label: string;
|
|
582
|
+
original: string;
|
|
583
|
+
translated: string | null;
|
|
584
|
+
}> {
|
|
585
|
+
const config = ACTION_CONFIG[action.type];
|
|
586
|
+
if (!config?.localizable || !config.form) return [];
|
|
587
|
+
return config.localizable
|
|
588
|
+
.filter((key) => {
|
|
589
|
+
const fieldConfig = config.form?.[key];
|
|
590
|
+
return fieldConfig && (fieldConfig.type === 'text' || fieldConfig.type === 'textarea');
|
|
591
|
+
})
|
|
592
|
+
.map((key) => {
|
|
593
|
+
const fieldConfig = config.form![key];
|
|
594
|
+
const label = typeof fieldConfig.label === 'string' ? fieldConfig.label : key;
|
|
595
|
+
return {
|
|
596
|
+
key,
|
|
597
|
+
label,
|
|
598
|
+
original: action[key] || '',
|
|
599
|
+
translated: this.getTranslatedField(action.uuid, key)
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private renderAttachments(attachments: string[]): TemplateResult {
|
|
605
|
+
if (!attachments || attachments.length === 0) return html``;
|
|
606
|
+
return html`<div class="attachments">
|
|
607
|
+
${attachments.map((att) => {
|
|
608
|
+
const colonIdx = att.indexOf(':');
|
|
609
|
+
if (colonIdx < 0) return html``;
|
|
610
|
+
const contentType = att.substring(0, colonIdx);
|
|
611
|
+
const baseType = contentType.split('/')[0];
|
|
612
|
+
const iconName = MessageTable.ATTACHMENT_ICONS[baseType] || Icon.attachment;
|
|
613
|
+
return html`<div class="attachment-icon">
|
|
614
|
+
<temba-icon name="${iconName}"></temba-icon>
|
|
615
|
+
</div>`;
|
|
616
|
+
})}
|
|
617
|
+
</div>`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private renderOriginalContent(entry: MessageEntry): TemplateResult {
|
|
621
|
+
const action = entry.action;
|
|
622
|
+
const config = ACTION_CONFIG[action.type];
|
|
623
|
+
const localizable = config?.localizable || [];
|
|
624
|
+
|
|
625
|
+
const parts: TemplateResult[] = [];
|
|
626
|
+
|
|
627
|
+
// Render all localizable text fields
|
|
628
|
+
if (config?.form) {
|
|
629
|
+
for (const key of localizable) {
|
|
630
|
+
const fc = config.form[key];
|
|
631
|
+
if (fc && (fc.type === 'text' || fc.type === 'textarea' || fc.type === 'message-editor')) {
|
|
632
|
+
const text = action[key] || '';
|
|
633
|
+
if (text) {
|
|
634
|
+
parts.push(renderHighlightedText(this.stripLeadingLineBreaks(text), true));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Quick replies (send_msg)
|
|
641
|
+
const quickReplies: string[] = action.quick_replies || [];
|
|
642
|
+
if (quickReplies.length > 0) {
|
|
643
|
+
parts.push(html`<div class="quick-replies ${parts.length === 0 ? 'standalone' : ''}">${quickReplies.map(
|
|
644
|
+
(reply) => html`<div class="quick-reply">${reply}</div>`
|
|
645
|
+
)}</div>`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Attachments
|
|
649
|
+
const attachments: string[] = action.attachments || [];
|
|
650
|
+
if (attachments.length > 0) {
|
|
651
|
+
parts.push(this.renderAttachments(attachments));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (parts.length === 0) {
|
|
655
|
+
return html`<span style="color: #bbb; font-style: italic;">Empty</span>`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return html`${parts}`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private renderTranslatedContent(entry: MessageEntry): TemplateResult {
|
|
662
|
+
const action = entry.action;
|
|
663
|
+
const config = ACTION_CONFIG[action.type];
|
|
664
|
+
const localizable = config?.localizable || [];
|
|
665
|
+
|
|
666
|
+
const parts: TemplateResult[] = [];
|
|
667
|
+
|
|
668
|
+
// Translated text fields
|
|
669
|
+
if (config?.form) {
|
|
670
|
+
for (const key of localizable) {
|
|
671
|
+
const fc = config.form[key];
|
|
672
|
+
if (fc && (fc.type === 'text' || fc.type === 'textarea' || fc.type === 'message-editor')) {
|
|
673
|
+
const translatedText = this.getTranslatedField(action.uuid, key);
|
|
674
|
+
if (typeof translatedText === 'string') {
|
|
675
|
+
parts.push(renderHighlightedText(this.stripLeadingLineBreaks(translatedText), true));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Translated quick replies
|
|
682
|
+
const translatedQuickReplies = this.getTranslatedQuickReplies(action.uuid);
|
|
683
|
+
if (translatedQuickReplies.length > 0) {
|
|
684
|
+
parts.push(html`<div class="quick-replies ${parts.length === 0 ? 'standalone' : ''}">${translatedQuickReplies.map(
|
|
685
|
+
(reply) => html`<div class="quick-reply">${reply}</div>`
|
|
686
|
+
)}</div>`);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Translated attachments
|
|
690
|
+
const translatedAttachments = this.getTranslatedArrayField(action.uuid, 'attachments');
|
|
691
|
+
if (translatedAttachments.length > 0) {
|
|
692
|
+
parts.push(this.renderAttachments(translatedAttachments));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (parts.length === 0) {
|
|
696
|
+
return html`No translation`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return html`${parts}`;
|
|
700
|
+
}
|
|
701
|
+
|
|
481
702
|
private handleBaseTextClick(entry: MessageEntry): void {
|
|
482
703
|
this.fireCustomEvent(CustomEventType.ActionEditRequested, {
|
|
483
704
|
action: entry.action,
|
|
@@ -573,6 +794,55 @@ export class MessageTable extends RapidElement {
|
|
|
573
794
|
);
|
|
574
795
|
}
|
|
575
796
|
|
|
797
|
+
private renderPairedRows(
|
|
798
|
+
items: Array<{ original: TemplateResult; translated: TemplateResult | null }>,
|
|
799
|
+
entry: TableEntry,
|
|
800
|
+
handleBaseClick: () => void,
|
|
801
|
+
handleTranslationClick: () => void,
|
|
802
|
+
showTranslation: boolean
|
|
803
|
+
): TemplateResult {
|
|
804
|
+
return html`
|
|
805
|
+
${items.map(
|
|
806
|
+
(item, idx) => html`
|
|
807
|
+
<tr
|
|
808
|
+
class="category-row localization-paired-row ${idx === 0 ? 'localization-paired-first' : ''} ${idx === items.length - 1 ? 'localization-paired-last' : ''}"
|
|
809
|
+
style=${`--node-rail-color: ${this.getEntryRailColor(entry)};`}
|
|
810
|
+
data-node-uuid=${entry.node.uuid}
|
|
811
|
+
data-entry-kind=${entry.kind}
|
|
812
|
+
data-action-uuid=${entry.kind === 'message' ? entry.action.uuid : ''}
|
|
813
|
+
>
|
|
814
|
+
<td>
|
|
815
|
+
<div
|
|
816
|
+
class="message-cell category-message-cell"
|
|
817
|
+
@click=${handleBaseClick}
|
|
818
|
+
title="Click to edit"
|
|
819
|
+
>
|
|
820
|
+
<div class="category-item category-original-item">
|
|
821
|
+
<span>${item.original}</span>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
</td>
|
|
825
|
+
${showTranslation
|
|
826
|
+
? html`<td class="translation-td">
|
|
827
|
+
<div
|
|
828
|
+
class="translation-cell category-translation-cell"
|
|
829
|
+
@click=${handleTranslationClick}
|
|
830
|
+
title="Click to edit translation"
|
|
831
|
+
>
|
|
832
|
+
<div
|
|
833
|
+
class="category-item category-translation-item ${item.translated !== null ? '' : 'missing'}"
|
|
834
|
+
>
|
|
835
|
+
<span>${item.translated !== null ? item.translated : 'No translation'}</span>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
</td>`
|
|
839
|
+
: ''}
|
|
840
|
+
</tr>
|
|
841
|
+
`
|
|
842
|
+
)}
|
|
843
|
+
`;
|
|
844
|
+
}
|
|
845
|
+
|
|
576
846
|
public render(): TemplateResult {
|
|
577
847
|
const entries = this.getEntries();
|
|
578
848
|
|
|
@@ -595,33 +865,6 @@ export class MessageTable extends RapidElement {
|
|
|
595
865
|
<tbody>
|
|
596
866
|
${entries.map((entry) => {
|
|
597
867
|
const isGroupEntry = entry.kind === 'localization-group';
|
|
598
|
-
const translatedText =
|
|
599
|
-
showTranslation && entry.kind === 'message'
|
|
600
|
-
? this.getTranslatedText(entry.action.uuid)
|
|
601
|
-
: null;
|
|
602
|
-
const translatedQuickReplies =
|
|
603
|
-
showTranslation && entry.kind === 'message'
|
|
604
|
-
? this.getTranslatedQuickReplies(entry.action.uuid)
|
|
605
|
-
: [];
|
|
606
|
-
const groupTranslations =
|
|
607
|
-
showTranslation && isGroupEntry
|
|
608
|
-
? this.getGroupTranslations(entry)
|
|
609
|
-
: null;
|
|
610
|
-
const hasGroupTranslation =
|
|
611
|
-
Array.isArray(groupTranslations) &&
|
|
612
|
-
groupTranslations.some((item) => !!item.translated);
|
|
613
|
-
const hasMessageTranslation =
|
|
614
|
-
entry.kind === 'message' &&
|
|
615
|
-
(!!translatedText || translatedQuickReplies.length > 0);
|
|
616
|
-
const hasTranslation =
|
|
617
|
-
entry.kind === 'message'
|
|
618
|
-
? hasMessageTranslation
|
|
619
|
-
: hasGroupTranslation;
|
|
620
|
-
const translationCellClass = isGroupEntry
|
|
621
|
-
? 'translation-cell category-translation-cell'
|
|
622
|
-
: `translation-cell ${hasTranslation
|
|
623
|
-
? 'has-translation'
|
|
624
|
-
: 'missing-translation'}`;
|
|
625
868
|
|
|
626
869
|
const handleBaseClick = () => {
|
|
627
870
|
if (entry.kind === 'message') this.handleBaseTextClick(entry);
|
|
@@ -632,55 +875,48 @@ export class MessageTable extends RapidElement {
|
|
|
632
875
|
else if (isGroupEntry) this.handleGroupTranslationClick(entry);
|
|
633
876
|
};
|
|
634
877
|
|
|
878
|
+
// Localization group entries (rules/categories) - always paired rows
|
|
635
879
|
if (isGroupEntry && showTranslation) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
<div
|
|
666
|
-
class="category-item category-translation-item ${item.translated
|
|
667
|
-
? ''
|
|
668
|
-
: 'missing'}"
|
|
669
|
-
>
|
|
670
|
-
<span>${item.isRule && item.operatorName
|
|
671
|
-
? html`<span class="rule-operator">${item.operatorName}</span> `
|
|
672
|
-
: ''}${item.translated
|
|
673
|
-
? renderHighlightedText(item.translated, true)
|
|
674
|
-
: 'No translation'}</span>
|
|
675
|
-
</div>
|
|
676
|
-
</div>
|
|
677
|
-
</td>
|
|
678
|
-
</tr>
|
|
679
|
-
`
|
|
680
|
-
)}
|
|
681
|
-
`;
|
|
880
|
+
const groupTranslations = this.getGroupTranslations(entry);
|
|
881
|
+
const items = groupTranslations.map((item) => ({
|
|
882
|
+
original: html`${item.isRule && item.operatorName
|
|
883
|
+
? html`<span class="rule-operator">${item.operatorName}</span> `
|
|
884
|
+
: ''}${renderHighlightedText(item.original, true)}`,
|
|
885
|
+
translated: item.translated !== null
|
|
886
|
+
? html`${item.isRule && item.operatorName
|
|
887
|
+
? html`<span class="rule-operator">${item.operatorName}</span> `
|
|
888
|
+
: ''}${renderHighlightedText(item.translated, true)}`
|
|
889
|
+
: null
|
|
890
|
+
}));
|
|
891
|
+
return this.renderPairedRows(items, entry, handleBaseClick, handleTranslationClickFn, showTranslation);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Multi-field actions (e.g. send_email with subject + body) - paired rows
|
|
895
|
+
if (
|
|
896
|
+
entry.kind === 'message' &&
|
|
897
|
+
this.usesPairedRows(entry.action)
|
|
898
|
+
) {
|
|
899
|
+
const fields = this.getPairedFields(entry.action);
|
|
900
|
+
const items = fields.map((f) => ({
|
|
901
|
+
original: html`${f.original
|
|
902
|
+
? renderHighlightedText(this.stripLeadingLineBreaks(f.original), true)
|
|
903
|
+
: html`<span style="color: #bbb; font-style: italic;">Empty</span>`}`,
|
|
904
|
+
translated: f.translated !== null
|
|
905
|
+
? html`${renderHighlightedText(this.stripLeadingLineBreaks(f.translated), true)}`
|
|
906
|
+
: null
|
|
907
|
+
}));
|
|
908
|
+
return this.renderPairedRows(items, entry, handleBaseClick, handleTranslationClickFn, showTranslation);
|
|
682
909
|
}
|
|
683
910
|
|
|
911
|
+
// Single-row entries (send_msg, send_broadcast, etc.)
|
|
912
|
+
const hasTranslation =
|
|
913
|
+
entry.kind === 'message' &&
|
|
914
|
+
showTranslation &&
|
|
915
|
+
this.hasAnyTranslation(entry);
|
|
916
|
+
const translationCellClass = `translation-cell ${hasTranslation
|
|
917
|
+
? 'has-translation'
|
|
918
|
+
: 'missing-translation'}`;
|
|
919
|
+
|
|
684
920
|
return html`
|
|
685
921
|
<tr
|
|
686
922
|
style=${`--node-rail-color: ${this.getEntryRailColor(entry)};`}
|
|
@@ -690,22 +926,14 @@ export class MessageTable extends RapidElement {
|
|
|
690
926
|
? entry.action.uuid
|
|
691
927
|
: ''}
|
|
692
928
|
>
|
|
693
|
-
<td>
|
|
929
|
+
<td class="rail-td">
|
|
694
930
|
<div
|
|
695
931
|
class="message-cell"
|
|
696
932
|
@click=${handleBaseClick}
|
|
697
|
-
title="Click to edit
|
|
933
|
+
title="Click to edit"
|
|
698
934
|
>
|
|
699
935
|
${entry.kind === 'message'
|
|
700
|
-
?
|
|
701
|
-
this.stripLeadingLineBreaks(entry.action.text || ''),
|
|
702
|
-
true
|
|
703
|
-
)}${(entry.action.quick_replies || [])
|
|
704
|
-
.length > 0
|
|
705
|
-
? html`<div class="quick-replies">${(entry.action.quick_replies || []).map(
|
|
706
|
-
(reply) => html`<div class="quick-reply">${reply}</div>`
|
|
707
|
-
)}</div>`
|
|
708
|
-
: ''}`
|
|
936
|
+
? this.renderOriginalContent(entry)
|
|
709
937
|
: isGroupEntry
|
|
710
938
|
? html`
|
|
711
939
|
<div class="category-stack category-stack-original">
|
|
@@ -736,25 +964,7 @@ export class MessageTable extends RapidElement {
|
|
|
736
964
|
title="Click to edit translation"
|
|
737
965
|
>
|
|
738
966
|
${entry.kind === 'message'
|
|
739
|
-
?
|
|
740
|
-
? renderHighlightedText(
|
|
741
|
-
this.stripLeadingLineBreaks(translatedText),
|
|
742
|
-
true
|
|
743
|
-
)
|
|
744
|
-
: translatedQuickReplies.length === 0
|
|
745
|
-
? 'No translation'
|
|
746
|
-
: ''}${translatedQuickReplies.length > 0
|
|
747
|
-
? html`<div
|
|
748
|
-
class="quick-replies ${translatedText
|
|
749
|
-
? ''
|
|
750
|
-
: 'standalone'}"
|
|
751
|
-
>
|
|
752
|
-
${translatedQuickReplies.map(
|
|
753
|
-
(reply) =>
|
|
754
|
-
html`<div class="quick-reply">${reply}</div>`
|
|
755
|
-
)}
|
|
756
|
-
</div>`
|
|
757
|
-
: ''}`
|
|
967
|
+
? this.renderTranslatedContent(entry)
|
|
758
968
|
: 'No translation'}
|
|
759
969
|
</div>
|
|
760
970
|
</td>`
|
package/src/flow/NodeEditor.ts
CHANGED
|
@@ -1928,6 +1928,15 @@ export class NodeEditor extends RapidElement {
|
|
|
1928
1928
|
|
|
1929
1929
|
switch (item.type) {
|
|
1930
1930
|
case 'field':
|
|
1931
|
+
// In translation mode, skip fields not in the localizable list
|
|
1932
|
+
if (
|
|
1933
|
+
this.isTranslating &&
|
|
1934
|
+
Array.isArray((config as ActionConfig).localizable) &&
|
|
1935
|
+
!(config as ActionConfig).localizable!.includes(item.field)
|
|
1936
|
+
) {
|
|
1937
|
+
renderedFields.add(item.field);
|
|
1938
|
+
return html``;
|
|
1939
|
+
}
|
|
1931
1940
|
if (config.form![item.field] && !renderedFields.has(item.field)) {
|
|
1932
1941
|
renderedFields.add(item.field);
|
|
1933
1942
|
const fieldConfig = config.form![item.field] as FieldConfig;
|
|
@@ -2484,6 +2493,14 @@ export class NodeEditor extends RapidElement {
|
|
|
2484
2493
|
!renderedFields.has(fieldName) &&
|
|
2485
2494
|
!gutterFields.has(fieldName)
|
|
2486
2495
|
) {
|
|
2496
|
+
// In translation mode, skip fields not in the localizable list
|
|
2497
|
+
if (
|
|
2498
|
+
this.isTranslating &&
|
|
2499
|
+
Array.isArray((config as ActionConfig).localizable) &&
|
|
2500
|
+
!(config as ActionConfig).localizable!.includes(fieldName)
|
|
2501
|
+
) {
|
|
2502
|
+
return html``;
|
|
2503
|
+
}
|
|
2487
2504
|
return this.renderNewField(
|
|
2488
2505
|
fieldName,
|
|
2489
2506
|
fieldConfig as FieldConfig,
|
|
@@ -2497,13 +2514,21 @@ export class NodeEditor extends RapidElement {
|
|
|
2497
2514
|
} else {
|
|
2498
2515
|
// Default rendering without layout
|
|
2499
2516
|
return html`
|
|
2500
|
-
${Object.entries(config.form).map(([fieldName, fieldConfig]) =>
|
|
2501
|
-
|
|
2517
|
+
${Object.entries(config.form).map(([fieldName, fieldConfig]) => {
|
|
2518
|
+
// In translation mode, skip fields not in the localizable list
|
|
2519
|
+
if (
|
|
2520
|
+
this.isTranslating &&
|
|
2521
|
+
Array.isArray((config as ActionConfig).localizable) &&
|
|
2522
|
+
!(config as ActionConfig).localizable!.includes(fieldName)
|
|
2523
|
+
) {
|
|
2524
|
+
return html``;
|
|
2525
|
+
}
|
|
2526
|
+
return this.renderNewField(
|
|
2502
2527
|
fieldName,
|
|
2503
2528
|
fieldConfig as FieldConfig,
|
|
2504
2529
|
this.formData[fieldName]
|
|
2505
|
-
)
|
|
2506
|
-
)}
|
|
2530
|
+
);
|
|
2531
|
+
})}
|
|
2507
2532
|
`;
|
|
2508
2533
|
}
|
|
2509
2534
|
}
|
package/src/flow/StickyNote.ts
CHANGED
|
@@ -26,9 +26,13 @@ export class StickyNote extends RapidElement {
|
|
|
26
26
|
@property({ type: Boolean })
|
|
27
27
|
private removing = false;
|
|
28
28
|
|
|
29
|
-
// On touch devices, contenteditable starts false to prevent Apple
|
|
30
|
-
// Scribble from hijacking touches. It is set to true on explicit tap.
|
|
31
|
-
|
|
29
|
+
// On Apple touch devices, contenteditable starts false to prevent Apple
|
|
30
|
+
// Pencil Scribble from hijacking touches. It is set to true on explicit tap.
|
|
31
|
+
// Only Apple platforms have Scribble, so we avoid disabling contenteditable
|
|
32
|
+
// on Windows touchscreen laptops where it would block mouse-based editing.
|
|
33
|
+
private isTouchDevice =
|
|
34
|
+
navigator.maxTouchPoints > 0 &&
|
|
35
|
+
/iPad|iPhone|Macintosh/.test(navigator.userAgent);
|
|
32
36
|
private editingField: HTMLElement | null = null;
|
|
33
37
|
private removalTimeout: number | null = null;
|
|
34
38
|
|
|
@@ -88,6 +92,24 @@ export class StickyNote extends RapidElement {
|
|
|
88
92
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
:host(.drag-copy) .sticky-note {
|
|
96
|
+
outline: 3px dashed var(--color-primary, #3b82f6);
|
|
97
|
+
outline-offset: 2px;
|
|
98
|
+
opacity: 0.7;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.sticky-note.selected {
|
|
102
|
+
cursor: var(--shift-held-cursor, move) !important;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.sticky-note.selected .remove-button {
|
|
106
|
+
cursor: pointer !important;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.sticky-note.selected [contenteditable] {
|
|
110
|
+
cursor: text !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
/* Color themes */
|
|
92
114
|
.sticky-note.yellow {
|
|
93
115
|
--sticky-color: #fef08a;
|
|
@@ -191,7 +213,7 @@ export class StickyNote extends RapidElement {
|
|
|
191
213
|
/* Drag icon */
|
|
192
214
|
.sticky-title-container > .drag-handle {
|
|
193
215
|
--icon-color: var(--sticky-border-color);
|
|
194
|
-
cursor: move;
|
|
216
|
+
cursor: var(--shift-held-cursor, move);
|
|
195
217
|
max-width: 20px;
|
|
196
218
|
padding-left: 8px;
|
|
197
219
|
padding-top: 10px;
|
|
@@ -769,7 +791,9 @@ export class StickyNote extends RapidElement {
|
|
|
769
791
|
<div
|
|
770
792
|
class="sticky-note ${this.data.color} ${this.dragging
|
|
771
793
|
? 'dragging'
|
|
772
|
-
: ''} ${this.removing ? 'removing' : ''}
|
|
794
|
+
: ''} ${this.removing ? 'removing' : ''} ${this.selected
|
|
795
|
+
? 'selected'
|
|
796
|
+
: ''}"
|
|
773
797
|
style="${style}"
|
|
774
798
|
data-uuid="${this.uuid}"
|
|
775
799
|
>
|