@nyaruka/temba-components 0.139.0 → 0.140.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.
- package/.github/workflows/cla.yml +1 -1
- package/.github/workflows/copilot-setup-steps.yml +6 -1
- package/CHANGELOG.md +17 -0
- package/demo/data/flows/sample-flow.json +24 -0
- package/dist/temba-components.js +562 -296
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +10 -7
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/Dropdown.js +3 -1
- package/out-tsc/src/display/Dropdown.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +3 -3
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +163 -5
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +64 -22
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +142 -8
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +118 -10
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +13 -4
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/audio-player.js +112 -0
- package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
- package/out-tsc/src/flow/actions/enter_flow.js +43 -0
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
- package/out-tsc/src/flow/actions/play_audio.js +57 -4
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +86 -3
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/config.js +11 -3
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
- package/out-tsc/src/flow/nodes/terminal.js +7 -0
- package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
- package/out-tsc/src/flow/operators.js +21 -5
- package/out-tsc/src/flow/operators.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +79 -3
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +4 -2
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +49 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/Dialog.js +52 -7
- package/out-tsc/src/layout/Dialog.js.map +1 -1
- package/out-tsc/src/live/TembaChart.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +10 -4
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +89 -3
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/actions/play_audio.test.js +118 -0
- package/out-tsc/test/actions/play_audio.test.js.map +1 -0
- package/out-tsc/test/actions/say_msg.test.js +158 -0
- package/out-tsc/test/actions/say_msg.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
- package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
- package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
- package/out-tsc/test/temba-flow-collision.test.js +261 -6
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +6 -6
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
- package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/src/display/Chat.ts +13 -7
- package/src/display/Dropdown.ts +3 -1
- package/src/display/FloatingTab.ts +3 -3
- package/src/display/Thumbnail.ts +162 -2
- package/src/flow/CanvasNode.ts +69 -23
- package/src/flow/Editor.ts +156 -13
- package/src/flow/NodeEditor.ts +137 -9
- package/src/flow/StickyNote.ts +14 -4
- package/src/flow/actions/audio-player.ts +127 -0
- package/src/flow/actions/enter_flow.ts +44 -0
- package/src/flow/actions/play_audio.ts +64 -5
- package/src/flow/actions/say_msg.ts +94 -4
- package/src/flow/config.ts +11 -3
- package/src/flow/nodes/shared-rules.ts +1 -1
- package/src/flow/nodes/terminal.ts +9 -0
- package/src/flow/nodes/wait_for_audio.ts +88 -0
- package/src/flow/nodes/wait_for_dial.ts +176 -0
- package/src/flow/nodes/wait_for_digits.ts +86 -2
- package/src/flow/nodes/wait_for_menu.ts +209 -3
- package/src/flow/operators.ts +23 -5
- package/src/flow/types.ts +23 -1
- package/src/flow/utils.ts +82 -3
- package/src/form/ArrayEditor.ts +4 -2
- package/src/form/FieldRenderer.ts +64 -1
- package/src/interfaces.ts +2 -1
- package/src/layout/Dialog.ts +53 -7
- package/src/live/TembaChart.ts +1 -1
- package/src/simulator/Simulator.ts +13 -4
- package/src/store/AppState.ts +105 -1
- package/src/store/flow-definition.d.ts +2 -0
- package/test/actions/play_audio.test.ts +155 -0
- package/test/actions/say_msg.test.ts +196 -0
- package/test/nodes/wait_for_audio.test.ts +182 -0
- package/test/nodes/wait_for_dial.test.ts +382 -0
- package/test/nodes/wait_for_digits.test.ts +233 -109
- package/test/nodes/wait_for_menu.test.ts +383 -0
- package/test/temba-flow-collision.test.ts +286 -6
- package/test/temba-node-type-selector.test.ts +6 -6
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
package/src/flow/CanvasNode.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { getClasses } from '../utils';
|
|
|
9
9
|
import { Plumber } from './Plumber';
|
|
10
10
|
import { getStore } from '../store/Store';
|
|
11
11
|
import { CustomEventType } from '../interfaces';
|
|
12
|
-
import { AppState, fromStore, zustand } from '../store/AppState';
|
|
12
|
+
import { AppState, FlowIssue, fromStore, zustand } from '../store/AppState';
|
|
13
13
|
|
|
14
14
|
const DRAG_THRESHOLD = 5;
|
|
15
15
|
|
|
@@ -49,6 +49,12 @@ export class CanvasNode extends RapidElement {
|
|
|
49
49
|
@fromStore(zustand, (state: AppState) => state.getCurrentActivity())
|
|
50
50
|
private activity!: any;
|
|
51
51
|
|
|
52
|
+
@fromStore(zustand, (state: AppState) => state.issuesByNode)
|
|
53
|
+
private issuesByNode!: Map<string, FlowIssue[]>;
|
|
54
|
+
|
|
55
|
+
@fromStore(zustand, (state: AppState) => state.issuesByAction)
|
|
56
|
+
private issuesByAction!: Map<string, FlowIssue[]>;
|
|
57
|
+
|
|
52
58
|
// Track exits that are in "removing" state
|
|
53
59
|
private exitRemovalTimeouts: Map<string, number> = new Map();
|
|
54
60
|
|
|
@@ -185,6 +191,12 @@ export class CanvasNode extends RapidElement {
|
|
|
185
191
|
pointer-events: none !important;
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
/* Issue indicators - hatched red title bar */
|
|
195
|
+
.action-content.has-issues .cn-title,
|
|
196
|
+
.node.has-issues > .router .cn-title {
|
|
197
|
+
background: repeating-linear-gradient(120deg, tomato, tomato 6px, #ff7056 0, #ff7056 18px) !important;
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
.action.sortable {
|
|
189
201
|
display: flex;
|
|
190
202
|
align-items: stretch;
|
|
@@ -548,14 +560,17 @@ export class CanvasNode extends RapidElement {
|
|
|
548
560
|
// make our initial connections
|
|
549
561
|
// We use setTimeout to allow for DOM updates to complete before querying for exits
|
|
550
562
|
this.connectionTimeout = window.setTimeout(() => {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
this.plumber.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
563
|
+
// Terminal nodes have no visible exits
|
|
564
|
+
if (this.ui?.type !== 'terminal') {
|
|
565
|
+
for (const exit of this.node.exits) {
|
|
566
|
+
this.plumber.makeSource(exit.uuid);
|
|
567
|
+
if (exit.destination_uuid) {
|
|
568
|
+
this.plumber.connectIds(
|
|
569
|
+
this.node.uuid,
|
|
570
|
+
exit.uuid,
|
|
571
|
+
exit.destination_uuid
|
|
572
|
+
);
|
|
573
|
+
}
|
|
559
574
|
}
|
|
560
575
|
}
|
|
561
576
|
// Note: revalidation is handled by plumber's processPendingConnections which calls repaintEverything
|
|
@@ -949,6 +964,11 @@ export class CanvasNode extends RapidElement {
|
|
|
949
964
|
this.requestUpdate();
|
|
950
965
|
}
|
|
951
966
|
|
|
967
|
+
private getTopCenter(el: Element): { x: number; y: number } {
|
|
968
|
+
const rect = el.getBoundingClientRect();
|
|
969
|
+
return { x: rect.left + rect.width / 2, y: rect.top };
|
|
970
|
+
}
|
|
971
|
+
|
|
952
972
|
private handleActionMouseDown(event: MouseEvent, action: Action): void {
|
|
953
973
|
// Don't handle clicks on the remove button, drag handle, or when action is in removing state
|
|
954
974
|
const target = event.target as HTMLElement;
|
|
@@ -1002,10 +1022,18 @@ export class CanvasNode extends RapidElement {
|
|
|
1002
1022
|
// Only fire the action edit event if we haven't dragged beyond the threshold
|
|
1003
1023
|
// AND either there's no Editor parent (test case) or the Editor didn't drag the node
|
|
1004
1024
|
if (distance <= DRAG_THRESHOLD && (!editor || !editorWasDragging)) {
|
|
1025
|
+
// Use top-center of the action element as the dialog origin
|
|
1026
|
+
const actionEl = event.currentTarget as Element;
|
|
1027
|
+
const origin = actionEl
|
|
1028
|
+
? this.getTopCenter(actionEl)
|
|
1029
|
+
: { x: event.clientX, y: event.clientY };
|
|
1030
|
+
|
|
1005
1031
|
// Fire event to request action editing
|
|
1006
1032
|
this.fireCustomEvent(CustomEventType.ActionEditRequested, {
|
|
1007
1033
|
action,
|
|
1008
|
-
nodeUuid: this.node.uuid
|
|
1034
|
+
nodeUuid: this.node.uuid,
|
|
1035
|
+
originX: origin.x,
|
|
1036
|
+
originY: origin.y
|
|
1009
1037
|
});
|
|
1010
1038
|
}
|
|
1011
1039
|
}
|
|
@@ -1125,17 +1153,24 @@ export class CanvasNode extends RapidElement {
|
|
|
1125
1153
|
// Using literal 5 instead of DRAG_THRESHOLD since it's not imported
|
|
1126
1154
|
// Fire event to request node editing if the node has a router
|
|
1127
1155
|
if (this.node.router) {
|
|
1156
|
+
// Use top-center of the node as the dialog origin
|
|
1157
|
+
const origin = this.getTopCenter(this);
|
|
1158
|
+
|
|
1128
1159
|
// If router node has exactly one action, open the action editor directly
|
|
1129
1160
|
if (this.node.actions && this.node.actions.length === 1) {
|
|
1130
1161
|
this.fireCustomEvent(CustomEventType.ActionEditRequested, {
|
|
1131
1162
|
action: this.node.actions[0],
|
|
1132
|
-
nodeUuid: this.node.uuid
|
|
1163
|
+
nodeUuid: this.node.uuid,
|
|
1164
|
+
originX: origin.x,
|
|
1165
|
+
originY: origin.y
|
|
1133
1166
|
});
|
|
1134
1167
|
} else {
|
|
1135
1168
|
// Otherwise open the node editor as before
|
|
1136
1169
|
this.fireCustomEvent(CustomEventType.NodeEditRequested, {
|
|
1137
1170
|
node: this.node,
|
|
1138
|
-
nodeUI: this.ui
|
|
1171
|
+
nodeUI: this.ui,
|
|
1172
|
+
originX: origin.x,
|
|
1173
|
+
originY: origin.y
|
|
1139
1174
|
});
|
|
1140
1175
|
}
|
|
1141
1176
|
}
|
|
@@ -1310,13 +1345,11 @@ export class CanvasNode extends RapidElement {
|
|
|
1310
1345
|
const color = config.group
|
|
1311
1346
|
? ACTION_GROUP_METADATA[config.group]?.color
|
|
1312
1347
|
: '#aaaaaa';
|
|
1348
|
+
const isTerminal = this.ui?.type === 'terminal';
|
|
1313
1349
|
return html`<div class="cn-title" style="background:${color}">
|
|
1314
|
-
${
|
|
1315
|
-
? html`<
|
|
1316
|
-
|
|
1317
|
-
name="sort"
|
|
1318
|
-
></temba-icon>`
|
|
1319
|
-
: this.node?.actions?.length > 1
|
|
1350
|
+
${isTerminal
|
|
1351
|
+
? html`<div class="title-spacer"></div>`
|
|
1352
|
+
: this.ui?.type === 'execute_actions' || this.node?.actions?.length > 1
|
|
1320
1353
|
? html`<temba-icon
|
|
1321
1354
|
class="drag-handle ${this.isReadOnly() ? 'read-only-hidden' : ''}"
|
|
1322
1355
|
name="sort"
|
|
@@ -1325,7 +1358,9 @@ export class CanvasNode extends RapidElement {
|
|
|
1325
1358
|
|
|
1326
1359
|
<div class="name">${isRemoving ? 'Remove?' : config.name}</div>
|
|
1327
1360
|
<div
|
|
1328
|
-
class="remove-button ${this.isReadOnly()
|
|
1361
|
+
class="remove-button ${isTerminal || this.isReadOnly()
|
|
1362
|
+
? 'read-only-hidden'
|
|
1363
|
+
: ''}"
|
|
1329
1364
|
@click=${(e: MouseEvent) =>
|
|
1330
1365
|
this.handleActionRemoveClick(e, action, index)}
|
|
1331
1366
|
title="Remove action"
|
|
@@ -1438,6 +1473,7 @@ export class CanvasNode extends RapidElement {
|
|
|
1438
1473
|
const displayAction = this.getLocalizedAction(action);
|
|
1439
1474
|
|
|
1440
1475
|
if (config) {
|
|
1476
|
+
const hasIssues = this.issuesByAction?.has(action.uuid);
|
|
1441
1477
|
const classes = [
|
|
1442
1478
|
'action',
|
|
1443
1479
|
'sortable',
|
|
@@ -1452,7 +1488,7 @@ export class CanvasNode extends RapidElement {
|
|
|
1452
1488
|
|
|
1453
1489
|
return html`<div class="${classes}" id="action-${index}">
|
|
1454
1490
|
<div
|
|
1455
|
-
class="action-content"
|
|
1491
|
+
class="action-content ${hasIssues ? 'has-issues' : ''}"
|
|
1456
1492
|
@mousedown=${(e: MouseEvent) =>
|
|
1457
1493
|
!isDisabled && this.handleActionMouseDown(e, action)}
|
|
1458
1494
|
@mouseup=${(e: MouseEvent) =>
|
|
@@ -1628,13 +1664,19 @@ export class CanvasNode extends RapidElement {
|
|
|
1628
1664
|
const activeCount =
|
|
1629
1665
|
(this.activity?.nodes && this.activity.nodes[this.node.uuid]) || 0;
|
|
1630
1666
|
|
|
1667
|
+
// Check for node-level issues or action-level issues on any action in this node
|
|
1668
|
+
const nodeHasIssues =
|
|
1669
|
+
this.issuesByNode?.has(this.node.uuid) ||
|
|
1670
|
+
this.node.actions?.some((a) => this.issuesByAction?.has(a.uuid));
|
|
1671
|
+
|
|
1631
1672
|
return html`
|
|
1632
1673
|
<div
|
|
1633
1674
|
id="${this.node.uuid}"
|
|
1634
1675
|
class=${getClasses({
|
|
1635
1676
|
node: true,
|
|
1636
1677
|
'execute-actions': this.ui.type === 'execute_actions',
|
|
1637
|
-
'non-localizable': isNodeDisabled
|
|
1678
|
+
'non-localizable': isNodeDisabled,
|
|
1679
|
+
'has-issues': nodeHasIssues
|
|
1638
1680
|
})}
|
|
1639
1681
|
style="left:${this.ui.position.left}px;top:${this.ui.position.top}px"
|
|
1640
1682
|
>
|
|
@@ -1643,7 +1685,9 @@ export class CanvasNode extends RapidElement {
|
|
|
1643
1685
|
${activeCount.toLocaleString()}
|
|
1644
1686
|
</div>`
|
|
1645
1687
|
: ''}
|
|
1646
|
-
${nodeConfig &&
|
|
1688
|
+
${nodeConfig &&
|
|
1689
|
+
nodeConfig.type !== 'execute_actions' &&
|
|
1690
|
+
nodeConfig.type !== 'terminal'
|
|
1647
1691
|
? html`<div class="router" style="position: relative;">
|
|
1648
1692
|
<div
|
|
1649
1693
|
@mousedown=${(e: MouseEvent) => this.handleNodeMouseDown(e)}
|
|
@@ -1661,7 +1705,7 @@ export class CanvasNode extends RapidElement {
|
|
|
1661
1705
|
: null}
|
|
1662
1706
|
</div>
|
|
1663
1707
|
</div>`
|
|
1664
|
-
: this.node.actions
|
|
1708
|
+
: this.node.actions?.length > 0
|
|
1665
1709
|
? this.ui.type === 'execute_actions'
|
|
1666
1710
|
? html`<temba-sortable-list
|
|
1667
1711
|
dragHandle="drag-handle"
|
|
@@ -1691,6 +1735,8 @@ export class CanvasNode extends RapidElement {
|
|
|
1691
1735
|
${this.renderRouter(this.node.router, this.ui)}
|
|
1692
1736
|
${this.renderCategories(this.node)}
|
|
1693
1737
|
</div>`
|
|
1738
|
+
: this.ui.type === 'terminal'
|
|
1739
|
+
? ''
|
|
1694
1740
|
: html`<div class="action-exits">
|
|
1695
1741
|
${repeat(
|
|
1696
1742
|
this.node.exits,
|
package/src/flow/Editor.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { getStore } from '../store/Store';
|
|
12
12
|
import {
|
|
13
13
|
AppState,
|
|
14
|
+
FlowIssue,
|
|
14
15
|
fromStore,
|
|
15
16
|
zustand,
|
|
16
17
|
FLOW_SPEC_VERSION
|
|
@@ -19,6 +20,13 @@ import { RapidElement } from '../RapidElement';
|
|
|
19
20
|
import { repeat } from 'lit-html/directives/repeat.js';
|
|
20
21
|
import { CustomEventType, Workspace } from '../interfaces';
|
|
21
22
|
import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
|
|
23
|
+
import {
|
|
24
|
+
formatIssueMessage,
|
|
25
|
+
getNodeBounds,
|
|
26
|
+
calculateReflowPositions,
|
|
27
|
+
NodeBounds,
|
|
28
|
+
snapToGrid
|
|
29
|
+
} from './utils';
|
|
22
30
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
23
31
|
|
|
24
32
|
interface Revision {
|
|
@@ -49,12 +57,6 @@ import { Dialog } from '../layout/Dialog';
|
|
|
49
57
|
|
|
50
58
|
import { CanvasMenu, CanvasMenuSelection } from './CanvasMenu';
|
|
51
59
|
import { NodeTypeSelector, NodeTypeSelection } from './NodeTypeSelector';
|
|
52
|
-
import {
|
|
53
|
-
getNodeBounds,
|
|
54
|
-
calculateReflowPositions,
|
|
55
|
-
NodeBounds,
|
|
56
|
-
snapToGrid
|
|
57
|
-
} from './utils';
|
|
58
60
|
import { FloatingWindow } from '../layout/FloatingWindow';
|
|
59
61
|
|
|
60
62
|
export function findNodeForExit(
|
|
@@ -173,6 +175,9 @@ export class Editor extends RapidElement {
|
|
|
173
175
|
@fromStore(zustand, (state: AppState) => state.getCurrentActivity())
|
|
174
176
|
private activityData!: any;
|
|
175
177
|
|
|
178
|
+
@fromStore(zustand, (state: AppState) => state.flowInfo?.issues || [])
|
|
179
|
+
private flowIssues!: FlowIssue[];
|
|
180
|
+
|
|
176
181
|
// Drag state
|
|
177
182
|
@state()
|
|
178
183
|
private isDragging = false;
|
|
@@ -217,6 +222,9 @@ export class Editor extends RapidElement {
|
|
|
217
222
|
private connectionSourceX: number | null = null;
|
|
218
223
|
private connectionSourceY: number | null = null;
|
|
219
224
|
|
|
225
|
+
@state()
|
|
226
|
+
private issuesWindowHidden = true;
|
|
227
|
+
|
|
220
228
|
@state()
|
|
221
229
|
private localizationWindowHidden = true;
|
|
222
230
|
|
|
@@ -269,6 +277,8 @@ export class Editor extends RapidElement {
|
|
|
269
277
|
@state()
|
|
270
278
|
private editingAction: Action | null = null;
|
|
271
279
|
|
|
280
|
+
private dialogOrigin: { x: number; y: number } | null = null;
|
|
281
|
+
|
|
272
282
|
@state()
|
|
273
283
|
private isCreatingNewNode = false;
|
|
274
284
|
|
|
@@ -357,6 +367,10 @@ export class Editor extends RapidElement {
|
|
|
357
367
|
-webkit-font-smoothing: antialiased;
|
|
358
368
|
}
|
|
359
369
|
|
|
370
|
+
temba-floating-tab {
|
|
371
|
+
--floating-tab-right: 15px;
|
|
372
|
+
}
|
|
373
|
+
|
|
360
374
|
#grid {
|
|
361
375
|
position: relative;
|
|
362
376
|
background-color: #f9f9f9;
|
|
@@ -490,7 +504,7 @@ export class Editor extends RapidElement {
|
|
|
490
504
|
font-weight: 600;
|
|
491
505
|
line-height: 0.9;
|
|
492
506
|
cursor: pointer;
|
|
493
|
-
z-index:
|
|
507
|
+
z-index: 10;
|
|
494
508
|
pointer-events: auto;
|
|
495
509
|
white-space: nowrap;
|
|
496
510
|
user-select: none;
|
|
@@ -805,6 +819,26 @@ export class Editor extends RapidElement {
|
|
|
805
819
|
color: #9ca3af;
|
|
806
820
|
white-space: nowrap;
|
|
807
821
|
}
|
|
822
|
+
|
|
823
|
+
.issue-list-item {
|
|
824
|
+
display: flex;
|
|
825
|
+
align-items: center;
|
|
826
|
+
gap: 8px;
|
|
827
|
+
padding: 8px;
|
|
828
|
+
border-radius: 4px;
|
|
829
|
+
cursor: pointer;
|
|
830
|
+
font-size: 13px;
|
|
831
|
+
color: #333;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.issue-list-item:hover {
|
|
835
|
+
background: #fff5f5;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.issue-list-item temba-icon {
|
|
839
|
+
color: tomato;
|
|
840
|
+
flex-shrink: 0;
|
|
841
|
+
}
|
|
808
842
|
`;
|
|
809
843
|
}
|
|
810
844
|
|
|
@@ -1219,6 +1253,7 @@ export class Editor extends RapidElement {
|
|
|
1219
1253
|
if (event.button !== 0) return;
|
|
1220
1254
|
|
|
1221
1255
|
if (this.isReadOnly()) return;
|
|
1256
|
+
this.blurActiveContentEditable();
|
|
1222
1257
|
|
|
1223
1258
|
const element = event.currentTarget as HTMLElement;
|
|
1224
1259
|
// Only start dragging if clicking on the element itself, not on exits or other interactive elements
|
|
@@ -1288,8 +1323,22 @@ export class Editor extends RapidElement {
|
|
|
1288
1323
|
this.handleCanvasMouseDown(event);
|
|
1289
1324
|
}
|
|
1290
1325
|
|
|
1326
|
+
private blurActiveContentEditable(): void {
|
|
1327
|
+
let active: Element | null = document.activeElement;
|
|
1328
|
+
while (active?.shadowRoot?.activeElement) {
|
|
1329
|
+
active = active.shadowRoot.activeElement;
|
|
1330
|
+
}
|
|
1331
|
+
if (
|
|
1332
|
+
active instanceof HTMLElement &&
|
|
1333
|
+
active.getAttribute('contenteditable') === 'true'
|
|
1334
|
+
) {
|
|
1335
|
+
active.blur();
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1291
1339
|
private handleCanvasMouseDown(event: MouseEvent): void {
|
|
1292
1340
|
if (this.isReadOnly()) return;
|
|
1341
|
+
this.blurActiveContentEditable();
|
|
1293
1342
|
|
|
1294
1343
|
const target = event.target as HTMLElement;
|
|
1295
1344
|
if (target.id === 'canvas' || target.id === 'grid') {
|
|
@@ -2160,6 +2209,10 @@ export class Editor extends RapidElement {
|
|
|
2160
2209
|
private handleActionEditRequested(event: CustomEvent): void {
|
|
2161
2210
|
// For action editing, we set the action and find the corresponding node
|
|
2162
2211
|
this.editingAction = event.detail.action;
|
|
2212
|
+
this.dialogOrigin =
|
|
2213
|
+
event.detail.originX != null
|
|
2214
|
+
? { x: event.detail.originX, y: event.detail.originY }
|
|
2215
|
+
: null;
|
|
2163
2216
|
|
|
2164
2217
|
// Find the node that contains this action
|
|
2165
2218
|
const nodeUuid = event.detail.nodeUuid;
|
|
@@ -2205,6 +2258,10 @@ export class Editor extends RapidElement {
|
|
|
2205
2258
|
private handleNodeEditRequested(event: CustomEvent): void {
|
|
2206
2259
|
this.editingNode = event.detail.node;
|
|
2207
2260
|
this.editingNodeUI = event.detail.nodeUI;
|
|
2261
|
+
this.dialogOrigin =
|
|
2262
|
+
event.detail.originX != null
|
|
2263
|
+
? { x: event.detail.originX, y: event.detail.originY }
|
|
2264
|
+
: null;
|
|
2208
2265
|
}
|
|
2209
2266
|
|
|
2210
2267
|
private handleNodeDeleted(event: CustomEvent): void {
|
|
@@ -2294,6 +2351,7 @@ export class Editor extends RapidElement {
|
|
|
2294
2351
|
this.editingNode = null;
|
|
2295
2352
|
this.editingNodeUI = null;
|
|
2296
2353
|
this.editingAction = null;
|
|
2354
|
+
this.dialogOrigin = null;
|
|
2297
2355
|
}
|
|
2298
2356
|
|
|
2299
2357
|
private handleActionEditCanceled(): void {
|
|
@@ -2948,6 +3006,7 @@ export class Editor extends RapidElement {
|
|
|
2948
3006
|
|
|
2949
3007
|
this.localizationWindowHidden = false;
|
|
2950
3008
|
this.revisionsWindowHidden = true;
|
|
3009
|
+
this.issuesWindowHidden = true;
|
|
2951
3010
|
|
|
2952
3011
|
const alreadySelected = languages.some(
|
|
2953
3012
|
(lang) => lang.code === this.languageCode
|
|
@@ -3210,11 +3269,47 @@ export class Editor extends RapidElement {
|
|
|
3210
3269
|
this.autoTranslating = false;
|
|
3211
3270
|
}
|
|
3212
3271
|
|
|
3272
|
+
private handleIssuesTabClick(): void {
|
|
3273
|
+
this.issuesWindowHidden = false;
|
|
3274
|
+
this.revisionsWindowHidden = true;
|
|
3275
|
+
this.localizationWindowHidden = true;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
private handleIssuesWindowClosed(): void {
|
|
3279
|
+
this.issuesWindowHidden = true;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
private handleIssueItemClick(issue: FlowIssue): void {
|
|
3283
|
+
const issuesWindow = document.getElementById(
|
|
3284
|
+
'issues-window'
|
|
3285
|
+
) as FloatingWindow;
|
|
3286
|
+
issuesWindow?.handleClose();
|
|
3287
|
+
this.issuesWindowHidden = true;
|
|
3288
|
+
|
|
3289
|
+
this.focusNode(issue.node_uuid);
|
|
3290
|
+
|
|
3291
|
+
const node = this.definition.nodes.find((n) => n.uuid === issue.node_uuid);
|
|
3292
|
+
if (!node) return;
|
|
3293
|
+
|
|
3294
|
+
if (issue.action_uuid) {
|
|
3295
|
+
const action = node.actions?.find((a) => a.uuid === issue.action_uuid);
|
|
3296
|
+
if (action) {
|
|
3297
|
+
this.editingAction = action;
|
|
3298
|
+
this.editingNode = node;
|
|
3299
|
+
this.editingNodeUI = this.definition._ui.nodes[issue.node_uuid];
|
|
3300
|
+
}
|
|
3301
|
+
} else {
|
|
3302
|
+
this.editingNode = node;
|
|
3303
|
+
this.editingNodeUI = this.definition._ui.nodes[issue.node_uuid];
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3213
3307
|
private handleRevisionsTabClick(): void {
|
|
3214
3308
|
if (this.revisionsWindowHidden) {
|
|
3215
3309
|
this.fetchRevisions();
|
|
3216
3310
|
this.revisionsWindowHidden = false;
|
|
3217
|
-
this.
|
|
3311
|
+
this.issuesWindowHidden = true;
|
|
3312
|
+
this.localizationWindowHidden = true;
|
|
3218
3313
|
}
|
|
3219
3314
|
}
|
|
3220
3315
|
|
|
@@ -3328,6 +3423,51 @@ export class Editor extends RapidElement {
|
|
|
3328
3423
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
3329
3424
|
}
|
|
3330
3425
|
|
|
3426
|
+
private renderIssuesTab(): TemplateResult | string {
|
|
3427
|
+
if (!this.flowIssues?.length) return '';
|
|
3428
|
+
return html`
|
|
3429
|
+
<temba-floating-tab
|
|
3430
|
+
id="issues-tab"
|
|
3431
|
+
icon="alert_warning"
|
|
3432
|
+
label="Flow Issues"
|
|
3433
|
+
color="tomato"
|
|
3434
|
+
order="1"
|
|
3435
|
+
.hidden=${!this.issuesWindowHidden}
|
|
3436
|
+
@temba-button-clicked=${this.handleIssuesTabClick}
|
|
3437
|
+
></temba-floating-tab>
|
|
3438
|
+
`;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
private renderIssuesWindow(): TemplateResult | string {
|
|
3442
|
+
if (!this.flowIssues?.length) return '';
|
|
3443
|
+
return html`
|
|
3444
|
+
<temba-floating-window
|
|
3445
|
+
id="issues-window"
|
|
3446
|
+
header="Flow Issues"
|
|
3447
|
+
.width=${360}
|
|
3448
|
+
.maxHeight=${600}
|
|
3449
|
+
.top=${75}
|
|
3450
|
+
color="tomato"
|
|
3451
|
+
.hidden=${this.issuesWindowHidden}
|
|
3452
|
+
@temba-dialog-hidden=${this.handleIssuesWindowClosed}
|
|
3453
|
+
>
|
|
3454
|
+
<div style="display:flex; flex-direction:column; gap:2px;">
|
|
3455
|
+
${this.flowIssues.map(
|
|
3456
|
+
(issue) => html`
|
|
3457
|
+
<div
|
|
3458
|
+
class="issue-list-item"
|
|
3459
|
+
@click=${() => this.handleIssueItemClick(issue)}
|
|
3460
|
+
>
|
|
3461
|
+
<temba-icon name="alert_warning" size="1.2"></temba-icon>
|
|
3462
|
+
<span>${formatIssueMessage(issue)}</span>
|
|
3463
|
+
</div>
|
|
3464
|
+
`
|
|
3465
|
+
)}
|
|
3466
|
+
</div>
|
|
3467
|
+
</temba-floating-window>
|
|
3468
|
+
`;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3331
3471
|
private renderRevisionsTab(): TemplateResult | string {
|
|
3332
3472
|
return html`
|
|
3333
3473
|
<temba-floating-tab
|
|
@@ -3335,7 +3475,7 @@ export class Editor extends RapidElement {
|
|
|
3335
3475
|
icon="revisions"
|
|
3336
3476
|
label="Revisions"
|
|
3337
3477
|
color="rgb(142, 94, 167)"
|
|
3338
|
-
order="
|
|
3478
|
+
order="2"
|
|
3339
3479
|
.hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
|
|
3340
3480
|
@temba-button-clicked=${this.handleRevisionsTabClick}
|
|
3341
3481
|
></temba-floating-tab>
|
|
@@ -3633,7 +3773,7 @@ export class Editor extends RapidElement {
|
|
|
3633
3773
|
icon="language"
|
|
3634
3774
|
label="Translate Flow"
|
|
3635
3775
|
color="#6b7280"
|
|
3636
|
-
order="
|
|
3776
|
+
order="3"
|
|
3637
3777
|
.hidden=${!this.localizationWindowHidden}
|
|
3638
3778
|
@temba-button-clicked=${this.handleLocalizationTabClick}
|
|
3639
3779
|
></temba-floating-tab>
|
|
@@ -3691,8 +3831,9 @@ export class Editor extends RapidElement {
|
|
|
3691
3831
|
|
|
3692
3832
|
const stickies = this.definition?._ui?.stickies || {};
|
|
3693
3833
|
|
|
3694
|
-
return html`${style} ${this.
|
|
3695
|
-
${this.
|
|
3834
|
+
return html`${style} ${this.renderIssuesWindow()}
|
|
3835
|
+
${this.renderRevisionsWindow()} ${this.renderLocalizationWindow()}
|
|
3836
|
+
${this.renderAutoTranslateDialog()}
|
|
3696
3837
|
<div id="editor">
|
|
3697
3838
|
<div
|
|
3698
3839
|
id="grid"
|
|
@@ -3782,6 +3923,7 @@ export class Editor extends RapidElement {
|
|
|
3782
3923
|
.node=${this.editingNode}
|
|
3783
3924
|
.nodeUI=${this.editingNodeUI}
|
|
3784
3925
|
.action=${this.editingAction}
|
|
3926
|
+
.dialogOrigin=${this.dialogOrigin}
|
|
3785
3927
|
@temba-node-saved=${(e: CustomEvent) =>
|
|
3786
3928
|
this.handleNodeSaved(e.detail.node, e.detail.uiConfig)}
|
|
3787
3929
|
@temba-action-saved=${(e: CustomEvent) =>
|
|
@@ -3797,6 +3939,7 @@ export class Editor extends RapidElement {
|
|
|
3797
3939
|
.features=${this.features}
|
|
3798
3940
|
></temba-node-type-selector>`
|
|
3799
3941
|
: ''}
|
|
3800
|
-
${this.
|
|
3942
|
+
${this.renderIssuesTab()} ${this.renderRevisionsTab()}
|
|
3943
|
+
${this.renderLocalizationTab()} `;
|
|
3801
3944
|
}
|
|
3802
3945
|
}
|