@nyaruka/temba-components 0.141.1 → 0.142.1
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 +22 -0
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +849 -655
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +3 -1
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/display/Button.js +2 -2
- package/out-tsc/src/display/Button.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +24 -1
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +7 -2
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +654 -66
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +8 -5
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +40 -28
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +2 -1
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/reflow.js +393 -0
- package/out-tsc/src/flow/reflow.js.map +1 -0
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +18 -3
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/Compose.js +5 -0
- package/out-tsc/src/form/Compose.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +1 -3
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/layout/Dialog.js +2 -0
- package/out-tsc/src/layout/Dialog.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +39 -19
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/utils.js +5 -12
- package/out-tsc/src/utils.js.map +1 -1
- package/out-tsc/test/nodes/split_by_run_result.test.js +1 -2
- package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -1
- package/out-tsc/test/temba-canvas-menu.test.js +44 -0
- package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +25 -0
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-zoom.test.js +491 -0
- package/out-tsc/test/temba-flow-editor-zoom.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor.test.js +145 -1
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-node-drag.test.js +123 -0
- package/out-tsc/test/temba-flow-node-drag.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +31 -0
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-flow-reflow.test.js +472 -0
- package/out-tsc/test/temba-flow-reflow.test.js.map +1 -0
- package/out-tsc/test/temba-sortable-list.test.js +93 -0
- package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
- package/package.json +2 -2
- package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
- package/screenshots/truth/actions/enter_flow/editor/basic-flow.png +0 -0
- package/screenshots/truth/actions/enter_flow/editor/long-flow-name.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.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/send_broadcast/editor/contacts-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/set_contact_channel/editor/sms-channel.png +0 -0
- package/screenshots/truth/actions/set_contact_channel/editor/whatsapp-channel.png +0 -0
- package/screenshots/truth/actions/set_contact_field/editor/clear-value.png +0 -0
- package/screenshots/truth/actions/set_contact_field/editor/set-value.png +0 -0
- package/screenshots/truth/actions/set_contact_language/editor/english.png +0 -0
- package/screenshots/truth/actions/set_contact_language/editor/french.png +0 -0
- package/screenshots/truth/actions/set_contact_status/editor/active.png +0 -0
- package/screenshots/truth/actions/set_contact_status/editor/archived.png +0 -0
- package/screenshots/truth/actions/set_contact_status/editor/blocked.png +0 -0
- package/screenshots/truth/actions/set_run_result/editor/expression-value.png +0 -0
- package/screenshots/truth/actions/set_run_result/editor/with-category.png +0 -0
- package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
- package/screenshots/truth/list/fields-dragging.png +0 -0
- package/screenshots/truth/list/sortable-dragging.png +0 -0
- package/screenshots/truth/modax/simple.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/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_menu/editor/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/src/Icons.ts +3 -1
- package/src/display/Button.ts +2 -2
- package/src/display/FloatingTab.ts +1 -1
- package/src/flow/CanvasMenu.ts +28 -3
- package/src/flow/CanvasNode.ts +7 -2
- package/src/flow/Editor.ts +755 -75
- package/src/flow/NodeEditor.ts +8 -4
- package/src/flow/Plumber.ts +65 -35
- package/src/flow/actions/send_msg.ts +2 -1
- package/src/flow/nodes/wait_for_response.ts +1 -1
- package/src/flow/reflow.ts +534 -0
- package/src/flow/types.ts +1 -0
- package/src/flow/utils.ts +19 -3
- package/src/form/Compose.ts +5 -0
- package/src/form/FieldRenderer.ts +1 -3
- package/src/layout/Dialog.ts +2 -0
- package/src/list/SortableList.ts +40 -19
- package/src/utils.ts +5 -12
- package/static/svg/index.svg +1 -1
- package/static/svg/work/traced/expand-06.svg +1 -0
- package/static/svg/work/used/expand-06.svg +3 -0
- package/test/nodes/split_by_run_result.test.ts +1 -2
- package/test/temba-canvas-menu.test.ts +55 -0
- package/test/temba-flow-collision.test.ts +31 -0
- package/test/temba-flow-editor-zoom.test.ts +583 -0
- package/test/temba-flow-editor.test.ts +187 -1
- package/test/temba-flow-node-drag.test.ts +171 -0
- package/test/temba-flow-plumber.test.ts +38 -0
- package/test/temba-flow-reflow.test.ts +703 -0
- package/test/temba-sortable-list.test.ts +120 -0
- package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
- package/screenshots/truth/compose/attachments-with-failures.png +0 -0
- package/screenshots/truth/compose/attachments-with-files-and-failures.png +0 -0
- package/screenshots/truth/contacts/tickets-assignment.png +0 -0
- package/screenshots/truth/contacts/tickets.png +0 -0
- package/screenshots/truth/flow/editor-basic.png +0 -0
- package/screenshots/truth/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/no-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
- package/screenshots/truth/omnibox/selected.png +0 -0
- package/screenshots/truth/select/enabled-multi-selection.png +0 -0
- package/screenshots/truth/select/endpoint-initial-value-updated.png +0 -0
- package/screenshots/truth/select/endpoint-initial-value.png +0 -0
- package/screenshots/truth/select/initial-value.png +0 -0
- package/screenshots/truth/select/multi-reorder-final.png +0 -0
- package/screenshots/truth/select/multi-reorder-initial.png +0 -0
- package/screenshots/truth/select/selected-multi-test.png +0 -0
- package/screenshots/truth/select/value-initial.png +0 -0
- package/screenshots/truth/wait-for-response/rules-editor.png +0 -0
- package/screenshots/truth/wait-for-response/timeout-editor-unchecked.png +0 -0
- package/screenshots/truth/wait-for-response/timeout-editor.png +0 -0
- package/screenshots/truth/webchat/connecting-state.png +0 -0
|
@@ -9,11 +9,14 @@ import { repeat } from 'lit-html/directives/repeat.js';
|
|
|
9
9
|
import { CustomEventType } from '../interfaces';
|
|
10
10
|
import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
|
|
11
11
|
import { TEMBA_COMPONENTS_VERSION } from '../version';
|
|
12
|
-
import { formatIssueMessage, getNodeBounds, calculateReflowPositions, snapToGrid } from './utils';
|
|
12
|
+
import { formatIssueMessage, getNodeBounds, calculateReflowPositions, isRightClick, snapToGrid } from './utils';
|
|
13
13
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
14
|
+
import { calculateLayeredLayout, placeStickyNotes } from './reflow';
|
|
15
|
+
import { FloatingTab } from '../display/FloatingTab';
|
|
14
16
|
import { ACTION_GROUP_METADATA } from './types';
|
|
15
17
|
import { Plumber, calculateFlowchartPath, ARROW_LENGTH, ARROW_HALF_WIDTH, CURSOR_GAP } from './Plumber';
|
|
16
18
|
import { CanvasNode } from './CanvasNode';
|
|
19
|
+
import { Icon } from '../Icons';
|
|
17
20
|
export function findNodeForExit(definition, exitUuid) {
|
|
18
21
|
for (const node of definition.nodes) {
|
|
19
22
|
const exit = node.exits.find((e) => e.uuid === exitUuid);
|
|
@@ -25,7 +28,12 @@ export function findNodeForExit(definition, exitUuid) {
|
|
|
25
28
|
}
|
|
26
29
|
const SAVE_QUIET_TIME = 2000;
|
|
27
30
|
const DRAG_THRESHOLD = 5;
|
|
31
|
+
const AUTO_SCROLL_EDGE_ZONE = 100;
|
|
32
|
+
const AUTO_SCROLL_MAX_SPEED = 15;
|
|
28
33
|
const AUTO_TRANSLATE_MODELS_ENDPOINT = '/api/internal/llms.json';
|
|
34
|
+
// How long the reflow auto-save countdown runs (in ms).
|
|
35
|
+
// Used in both the CSS animation and the JS setTimeout.
|
|
36
|
+
const REFLOW_AUTO_SAVE_DELAY = 5000;
|
|
29
37
|
// Offset for positioning dropped action node relative to mouse cursor
|
|
30
38
|
// Keep small to make drop location close to cursor position
|
|
31
39
|
const DROP_PREVIEW_OFFSET_X = 20;
|
|
@@ -39,6 +47,12 @@ export class Editor extends RapidElement {
|
|
|
39
47
|
get dragging() {
|
|
40
48
|
return this.isDragging;
|
|
41
49
|
}
|
|
50
|
+
clearReflowAutoSaveTimer() {
|
|
51
|
+
if (this.reflowAutoSaveTimer !== null) {
|
|
52
|
+
clearTimeout(this.reflowAutoSaveTimer);
|
|
53
|
+
this.reflowAutoSaveTimer = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
42
56
|
getAvailableLanguages() {
|
|
43
57
|
var _b, _c, _d;
|
|
44
58
|
// Use languages from workspace if available
|
|
@@ -99,6 +113,7 @@ export class Editor extends RapidElement {
|
|
|
99
113
|
width: 100%;
|
|
100
114
|
display: flex;
|
|
101
115
|
padding-top: 20px;
|
|
116
|
+
transform-origin: 0 0;
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
#canvas {
|
|
@@ -608,7 +623,7 @@ export class Editor extends RapidElement {
|
|
|
608
623
|
.save-indicator {
|
|
609
624
|
position: absolute;
|
|
610
625
|
top: 8px;
|
|
611
|
-
right:
|
|
626
|
+
right: 240px;
|
|
612
627
|
padding: 6px 10px;
|
|
613
628
|
z-index: 10000;
|
|
614
629
|
pointer-events: none;
|
|
@@ -619,6 +634,135 @@ export class Editor extends RapidElement {
|
|
|
619
634
|
.save-indicator.visible {
|
|
620
635
|
opacity: 1;
|
|
621
636
|
}
|
|
637
|
+
|
|
638
|
+
.zoom-controls {
|
|
639
|
+
position: absolute;
|
|
640
|
+
top: 8px;
|
|
641
|
+
right: 16px;
|
|
642
|
+
z-index: 4999;
|
|
643
|
+
display: flex;
|
|
644
|
+
align-items: center;
|
|
645
|
+
gap: 2px;
|
|
646
|
+
background: white;
|
|
647
|
+
border-radius: var(--curvature);
|
|
648
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
649
|
+
padding: 4px;
|
|
650
|
+
user-select: none;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.zoom-controls button {
|
|
654
|
+
width: 28px;
|
|
655
|
+
height: 28px;
|
|
656
|
+
border: none;
|
|
657
|
+
background: transparent;
|
|
658
|
+
border-radius: var(--curvature);
|
|
659
|
+
cursor: pointer;
|
|
660
|
+
display: flex;
|
|
661
|
+
align-items: center;
|
|
662
|
+
justify-content: center;
|
|
663
|
+
padding: 0;
|
|
664
|
+
color: #555;
|
|
665
|
+
font-size: 16px;
|
|
666
|
+
line-height: 1;
|
|
667
|
+
outline: none;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.zoom-controls button:hover {
|
|
671
|
+
background: rgba(0, 0, 0, 0.06);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.zoom-controls button:disabled {
|
|
675
|
+
opacity: 0.3;
|
|
676
|
+
cursor: default;
|
|
677
|
+
background: transparent;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.zoom-controls .zoom-level {
|
|
681
|
+
font-size: 12px;
|
|
682
|
+
min-width: 40px;
|
|
683
|
+
text-align: center;
|
|
684
|
+
color: #555;
|
|
685
|
+
font-weight: 500;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.zoom-controls .zoom-divider {
|
|
689
|
+
width: 1px;
|
|
690
|
+
height: 16px;
|
|
691
|
+
background: #e0e0e0;
|
|
692
|
+
margin: 0 2px;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.reflow-card {
|
|
696
|
+
position: absolute;
|
|
697
|
+
top: 16px;
|
|
698
|
+
left: 50%;
|
|
699
|
+
transform: translateX(-50%);
|
|
700
|
+
z-index: 10000;
|
|
701
|
+
background: rgba(0, 0, 0, 0.65);
|
|
702
|
+
backdrop-filter: blur(8px);
|
|
703
|
+
border-radius: 10px;
|
|
704
|
+
padding: 12px 16px 8px;
|
|
705
|
+
display: flex;
|
|
706
|
+
flex-direction: column;
|
|
707
|
+
gap: 8px;
|
|
708
|
+
color: white;
|
|
709
|
+
font-size: 13px;
|
|
710
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.reflow-card .reflow-top {
|
|
714
|
+
display: flex;
|
|
715
|
+
align-items: center;
|
|
716
|
+
gap: 10px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.reflow-card .reflow-label {
|
|
720
|
+
white-space: nowrap;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.reflow-card button {
|
|
724
|
+
border: none;
|
|
725
|
+
border-radius: 6px;
|
|
726
|
+
padding: 6px 14px;
|
|
727
|
+
font-size: 13px;
|
|
728
|
+
font-weight: 500;
|
|
729
|
+
cursor: pointer;
|
|
730
|
+
white-space: nowrap;
|
|
731
|
+
transition: opacity 0.15s ease;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.reflow-card button:hover {
|
|
735
|
+
opacity: 0.85;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.reflow-card .reflow-discard {
|
|
739
|
+
background: rgba(255, 255, 255, 0.2);
|
|
740
|
+
color: white;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.reflow-meter {
|
|
744
|
+
height: 3px;
|
|
745
|
+
border-radius: 2px;
|
|
746
|
+
background: rgba(255, 255, 255, 0.15);
|
|
747
|
+
overflow: hidden;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.reflow-meter-fill {
|
|
751
|
+
height: 100%;
|
|
752
|
+
background: rgba(255, 255, 255, 0.5);
|
|
753
|
+
border-radius: 2px;
|
|
754
|
+
animation: reflow-countdown ${unsafeCSS(REFLOW_AUTO_SAVE_DELAY / 1000)}s
|
|
755
|
+
linear forwards;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
@keyframes reflow-countdown {
|
|
759
|
+
from {
|
|
760
|
+
width: 100%;
|
|
761
|
+
}
|
|
762
|
+
to {
|
|
763
|
+
width: 0%;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
622
766
|
`;
|
|
623
767
|
}
|
|
624
768
|
constructor() {
|
|
@@ -635,6 +779,11 @@ export class Editor extends RapidElement {
|
|
|
635
779
|
this.dragStartPos = { x: 0, y: 0 };
|
|
636
780
|
this.currentDragItem = null;
|
|
637
781
|
this.startPos = { left: 0, top: 0 };
|
|
782
|
+
// Auto-scroll state
|
|
783
|
+
this.autoScrollAnimationId = null;
|
|
784
|
+
this.autoScrollDeltaX = 0;
|
|
785
|
+
this.autoScrollDeltaY = 0;
|
|
786
|
+
this.lastMouseEvent = null;
|
|
638
787
|
// Selection state
|
|
639
788
|
this.selectedItems = new Set();
|
|
640
789
|
this.isSelecting = false;
|
|
@@ -663,6 +812,12 @@ export class Editor extends RapidElement {
|
|
|
663
812
|
this.isLoadingRevisions = false;
|
|
664
813
|
this.isSaving = false;
|
|
665
814
|
this.saveError = null;
|
|
815
|
+
this.zoom = 1.0;
|
|
816
|
+
this.zoomFitted = false;
|
|
817
|
+
this.reflowPending = false;
|
|
818
|
+
this.reflowUnsaved = false;
|
|
819
|
+
this.savedReflowPositions = null;
|
|
820
|
+
this.reflowAutoSaveTimer = null;
|
|
666
821
|
this.preRevertState = null;
|
|
667
822
|
this.translationCache = new Map();
|
|
668
823
|
// NodeEditor state - handles both node and action editing
|
|
@@ -690,11 +845,13 @@ export class Editor extends RapidElement {
|
|
|
690
845
|
this.boundGlobalMouseDown = this.handleGlobalMouseDown.bind(this);
|
|
691
846
|
this.boundKeyDown = this.handleKeyDown.bind(this);
|
|
692
847
|
this.boundCanvasContextMenu = this.handleCanvasContextMenu.bind(this);
|
|
848
|
+
this.boundWheel = this.handleWheel.bind(this);
|
|
693
849
|
}
|
|
694
850
|
firstUpdated(changes) {
|
|
695
851
|
super.firstUpdated(changes);
|
|
696
852
|
this.plumber = new Plumber(this.querySelector('#canvas'), this);
|
|
697
853
|
this.setupGlobalEventListeners();
|
|
854
|
+
this.updateZoomControlPositioning();
|
|
698
855
|
if (changes.has('flow')) {
|
|
699
856
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
700
857
|
this.fetchRevisions();
|
|
@@ -748,10 +905,10 @@ export class Editor extends RapidElement {
|
|
|
748
905
|
const canvas = this.querySelector('#canvas');
|
|
749
906
|
if (canvas) {
|
|
750
907
|
const canvasRect = canvas.getBoundingClientRect();
|
|
751
|
-
const menuX = canvasRect.left + snappedPosition.left - 40;
|
|
908
|
+
const menuX = canvasRect.left + snappedPosition.left * this.zoom - 40;
|
|
752
909
|
const menuY = isDragUp
|
|
753
|
-
? canvasRect.top + snappedPosition.top + 74 // just below placeholder bottom
|
|
754
|
-
: canvasRect.top + snappedPosition.top + 80; // just below placeholder
|
|
910
|
+
? canvasRect.top + snappedPosition.top * this.zoom + 74 // just below placeholder bottom
|
|
911
|
+
: canvasRect.top + snappedPosition.top * this.zoom + 80; // just below placeholder
|
|
755
912
|
const canvasMenu = this.querySelector('temba-canvas-menu');
|
|
756
913
|
if (canvasMenu) {
|
|
757
914
|
canvasMenu.show(menuX, menuY, {
|
|
@@ -825,8 +982,21 @@ export class Editor extends RapidElement {
|
|
|
825
982
|
}
|
|
826
983
|
if (changes.has('dirtyDate')) {
|
|
827
984
|
if (this.dirtyDate) {
|
|
828
|
-
this.
|
|
829
|
-
|
|
985
|
+
if (this.reflowPending) {
|
|
986
|
+
// This dirtyDate is from the reflow itself — suppress save
|
|
987
|
+
this.reflowPending = false;
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
// Normal change — if reflow card was showing, it goes away
|
|
991
|
+
// because these changes will be included in the save
|
|
992
|
+
if (this.reflowUnsaved) {
|
|
993
|
+
this.reflowUnsaved = false;
|
|
994
|
+
this.savedReflowPositions = null;
|
|
995
|
+
this.clearReflowAutoSaveTimer();
|
|
996
|
+
}
|
|
997
|
+
this.isSaving = true;
|
|
998
|
+
this.debouncedSave();
|
|
999
|
+
}
|
|
830
1000
|
}
|
|
831
1001
|
}
|
|
832
1002
|
if (changes.has('saveError') && this.saveError) {
|
|
@@ -861,6 +1031,11 @@ export class Editor extends RapidElement {
|
|
|
861
1031
|
clearTimeout(this.saveTimer);
|
|
862
1032
|
}
|
|
863
1033
|
this.saveTimer = window.setTimeout(() => {
|
|
1034
|
+
// Don't auto-save while a reflow preview is pending user confirmation
|
|
1035
|
+
if (this.reflowUnsaved) {
|
|
1036
|
+
this.saveTimer = null;
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
864
1039
|
const now = new Date();
|
|
865
1040
|
const timeSinceLastChange = now.getTime() - this.dirtyDate.getTime();
|
|
866
1041
|
if (timeSinceLastChange >= SAVE_QUIET_TIME) {
|
|
@@ -998,6 +1173,7 @@ export class Editor extends RapidElement {
|
|
|
998
1173
|
}
|
|
999
1174
|
disconnectedCallback() {
|
|
1000
1175
|
super.disconnectedCallback();
|
|
1176
|
+
this.stopAutoScroll();
|
|
1001
1177
|
if (this.saveTimer !== null) {
|
|
1002
1178
|
clearTimeout(this.saveTimer);
|
|
1003
1179
|
this.saveTimer = null;
|
|
@@ -1006,6 +1182,7 @@ export class Editor extends RapidElement {
|
|
|
1006
1182
|
clearTimeout(this.activityTimer);
|
|
1007
1183
|
this.activityTimer = null;
|
|
1008
1184
|
}
|
|
1185
|
+
this.clearReflowAutoSaveTimer();
|
|
1009
1186
|
document.removeEventListener('mousemove', this.boundMouseMove);
|
|
1010
1187
|
document.removeEventListener('mouseup', this.boundMouseUp);
|
|
1011
1188
|
document.removeEventListener('mousedown', this.boundGlobalMouseDown);
|
|
@@ -1014,6 +1191,10 @@ export class Editor extends RapidElement {
|
|
|
1014
1191
|
if (canvas) {
|
|
1015
1192
|
canvas.removeEventListener('contextmenu', this.boundCanvasContextMenu);
|
|
1016
1193
|
}
|
|
1194
|
+
const editor = this.querySelector('#editor');
|
|
1195
|
+
if (editor) {
|
|
1196
|
+
editor.removeEventListener('wheel', this.boundWheel);
|
|
1197
|
+
}
|
|
1017
1198
|
// Clear all flow-specific data from the store so stale data
|
|
1018
1199
|
// isn't briefly visible when a different flow is opened.
|
|
1019
1200
|
zustand.getState().clearFlowData();
|
|
@@ -1027,6 +1208,10 @@ export class Editor extends RapidElement {
|
|
|
1027
1208
|
if (canvas) {
|
|
1028
1209
|
canvas.addEventListener('contextmenu', this.boundCanvasContextMenu);
|
|
1029
1210
|
}
|
|
1211
|
+
const editor = this.querySelector('#editor');
|
|
1212
|
+
if (editor) {
|
|
1213
|
+
editor.addEventListener('wheel', this.boundWheel, { passive: false });
|
|
1214
|
+
}
|
|
1030
1215
|
// Listen for action edit requests from flow nodes
|
|
1031
1216
|
this.addEventListener(CustomEventType.ActionEditRequested, this.handleActionEditRequested.bind(this));
|
|
1032
1217
|
// Listen for add action requests from flow nodes
|
|
@@ -1074,8 +1259,7 @@ export class Editor extends RapidElement {
|
|
|
1074
1259
|
}
|
|
1075
1260
|
}
|
|
1076
1261
|
handleMouseDown(event) {
|
|
1077
|
-
|
|
1078
|
-
if (event.button !== 0)
|
|
1262
|
+
if (isRightClick(event))
|
|
1079
1263
|
return;
|
|
1080
1264
|
if (this.isReadOnly())
|
|
1081
1265
|
return;
|
|
@@ -1116,8 +1300,7 @@ export class Editor extends RapidElement {
|
|
|
1116
1300
|
}
|
|
1117
1301
|
handleGlobalMouseDown(event) {
|
|
1118
1302
|
var _b;
|
|
1119
|
-
|
|
1120
|
-
if (event.button !== 0)
|
|
1303
|
+
if (isRightClick(event))
|
|
1121
1304
|
return;
|
|
1122
1305
|
// Check if the click is within our canvas
|
|
1123
1306
|
const canvasRect = (_b = this.querySelector('#grid')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
|
|
@@ -1165,8 +1348,8 @@ export class Editor extends RapidElement {
|
|
|
1165
1348
|
if (canvasRect) {
|
|
1166
1349
|
// Clear current selection
|
|
1167
1350
|
this.selectedItems.clear();
|
|
1168
|
-
const relativeX = event.clientX - canvasRect.left;
|
|
1169
|
-
const relativeY = event.clientY - canvasRect.top;
|
|
1351
|
+
const relativeX = (event.clientX - canvasRect.left) / this.zoom;
|
|
1352
|
+
const relativeY = (event.clientY - canvasRect.top) / this.zoom;
|
|
1170
1353
|
this.selectionBox = {
|
|
1171
1354
|
startX: relativeX,
|
|
1172
1355
|
startY: relativeY,
|
|
@@ -1188,6 +1371,135 @@ export class Editor extends RapidElement {
|
|
|
1188
1371
|
this.requestUpdate();
|
|
1189
1372
|
}
|
|
1190
1373
|
}
|
|
1374
|
+
// --- Zoom ---
|
|
1375
|
+
setZoom(newZoom, center) {
|
|
1376
|
+
const clamped = Math.max(0.1, Math.min(1.0, Math.round(newZoom * 100) / 100));
|
|
1377
|
+
if (clamped === this.zoom)
|
|
1378
|
+
return;
|
|
1379
|
+
const editor = this.querySelector('#editor');
|
|
1380
|
+
const oldZoom = this.zoom;
|
|
1381
|
+
this.zoom = clamped;
|
|
1382
|
+
this.plumber.zoom = clamped;
|
|
1383
|
+
this.zoomFitted = false;
|
|
1384
|
+
if (editor && center) {
|
|
1385
|
+
const editorRect = editor.getBoundingClientRect();
|
|
1386
|
+
const ox = center.clientX - editorRect.left;
|
|
1387
|
+
const oy = center.clientY - editorRect.top;
|
|
1388
|
+
// Canvas point under cursor at old zoom
|
|
1389
|
+
const cx = (editor.scrollLeft + ox) / oldZoom;
|
|
1390
|
+
const cy = (editor.scrollTop + oy) / oldZoom;
|
|
1391
|
+
requestAnimationFrame(() => {
|
|
1392
|
+
editor.scrollLeft = cx * clamped - ox;
|
|
1393
|
+
editor.scrollTop = cy * clamped - oy;
|
|
1394
|
+
this.plumber.repaintEverything();
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
requestAnimationFrame(() => this.plumber.repaintEverything());
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
zoomIn() {
|
|
1402
|
+
this.setZoom(this.zoom + 0.05);
|
|
1403
|
+
}
|
|
1404
|
+
zoomOut() {
|
|
1405
|
+
this.setZoom(this.zoom - 0.05);
|
|
1406
|
+
}
|
|
1407
|
+
zoomToFit() {
|
|
1408
|
+
var _b;
|
|
1409
|
+
if (!this.definition || this.definition.nodes.length === 0)
|
|
1410
|
+
return;
|
|
1411
|
+
const editor = this.querySelector('#editor');
|
|
1412
|
+
if (!editor)
|
|
1413
|
+
return;
|
|
1414
|
+
// Calculate bounding box of all content in canvas coordinates
|
|
1415
|
+
let minX = Infinity;
|
|
1416
|
+
let minY = Infinity;
|
|
1417
|
+
let maxX = -Infinity;
|
|
1418
|
+
let maxY = -Infinity;
|
|
1419
|
+
this.definition.nodes.forEach((node) => {
|
|
1420
|
+
var _b;
|
|
1421
|
+
const ui = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid];
|
|
1422
|
+
if (!(ui === null || ui === void 0 ? void 0 : ui.position))
|
|
1423
|
+
return;
|
|
1424
|
+
const el = this.querySelector(`[id="${node.uuid}"]`);
|
|
1425
|
+
if (!el)
|
|
1426
|
+
return;
|
|
1427
|
+
const w = el.offsetWidth;
|
|
1428
|
+
const h = el.offsetHeight;
|
|
1429
|
+
minX = Math.min(minX, ui.position.left);
|
|
1430
|
+
minY = Math.min(minY, ui.position.top);
|
|
1431
|
+
maxX = Math.max(maxX, ui.position.left + w);
|
|
1432
|
+
maxY = Math.max(maxY, ui.position.top + h);
|
|
1433
|
+
});
|
|
1434
|
+
const stickies = ((_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.stickies) || {};
|
|
1435
|
+
Object.entries(stickies).forEach(([uuid, sticky]) => {
|
|
1436
|
+
if (!sticky.position)
|
|
1437
|
+
return;
|
|
1438
|
+
const el = this.querySelector(`temba-sticky-note[uuid="${uuid}"]`);
|
|
1439
|
+
if (!el)
|
|
1440
|
+
return;
|
|
1441
|
+
const w = el.offsetWidth;
|
|
1442
|
+
const h = el.offsetHeight;
|
|
1443
|
+
minX = Math.min(minX, sticky.position.left);
|
|
1444
|
+
minY = Math.min(minY, sticky.position.top);
|
|
1445
|
+
maxX = Math.max(maxX, sticky.position.left + w);
|
|
1446
|
+
maxY = Math.max(maxY, sticky.position.top + h);
|
|
1447
|
+
});
|
|
1448
|
+
if (minX === Infinity)
|
|
1449
|
+
return;
|
|
1450
|
+
const contentWidth = maxX - minX;
|
|
1451
|
+
const contentHeight = maxY - minY;
|
|
1452
|
+
const padding = 40;
|
|
1453
|
+
const availWidth = editor.clientWidth - padding * 2;
|
|
1454
|
+
const availHeight = editor.clientHeight - padding * 2;
|
|
1455
|
+
const scaleX = availWidth / contentWidth;
|
|
1456
|
+
const scaleY = availHeight / contentHeight;
|
|
1457
|
+
let fitZoom = Math.min(scaleX, scaleY, 1.0);
|
|
1458
|
+
fitZoom = Math.max(fitZoom, 0.1);
|
|
1459
|
+
fitZoom = Math.round(fitZoom * 20) / 20; // round to nearest 0.05
|
|
1460
|
+
this.zoom = fitZoom;
|
|
1461
|
+
this.plumber.zoom = fitZoom;
|
|
1462
|
+
this.zoomFitted = true;
|
|
1463
|
+
// Center of content in canvas coordinates, plus grid/canvas margin offset
|
|
1464
|
+
const centerX = (minX + maxX) / 2 + 40;
|
|
1465
|
+
const centerY = (minY + maxY) / 2 + 40;
|
|
1466
|
+
requestAnimationFrame(() => {
|
|
1467
|
+
editor.scrollLeft = centerX * fitZoom - editor.clientWidth / 2;
|
|
1468
|
+
editor.scrollTop = centerY * fitZoom - editor.clientHeight / 2;
|
|
1469
|
+
this.plumber.repaintEverything();
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
zoomToFull() {
|
|
1473
|
+
this.setZoom(1.0);
|
|
1474
|
+
}
|
|
1475
|
+
/** Adjust zoom control right offset and floating tab positions */
|
|
1476
|
+
updateZoomControlPositioning() {
|
|
1477
|
+
requestAnimationFrame(() => {
|
|
1478
|
+
const editor = this.querySelector('#editor');
|
|
1479
|
+
const zoomControls = this.querySelector('.zoom-controls');
|
|
1480
|
+
if (editor && zoomControls) {
|
|
1481
|
+
// Match right spacing to the top spacing (8px) by accounting for
|
|
1482
|
+
// the scrollbar width
|
|
1483
|
+
const scrollbarWidth = editor.offsetWidth - editor.clientWidth;
|
|
1484
|
+
zoomControls.style.right = `${8 + scrollbarWidth}px`;
|
|
1485
|
+
}
|
|
1486
|
+
if (zoomControls) {
|
|
1487
|
+
const rect = zoomControls.getBoundingClientRect();
|
|
1488
|
+
FloatingTab.START_TOP = rect.bottom + 8;
|
|
1489
|
+
FloatingTab.updateAllPositions();
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
handleWheel(event) {
|
|
1494
|
+
if (!event.ctrlKey && !event.metaKey)
|
|
1495
|
+
return;
|
|
1496
|
+
event.preventDefault();
|
|
1497
|
+
const delta = event.deltaY > 0 ? -0.05 : 0.05;
|
|
1498
|
+
this.setZoom(this.zoom + delta, {
|
|
1499
|
+
clientX: event.clientX,
|
|
1500
|
+
clientY: event.clientY
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1191
1503
|
showDeleteConfirmation() {
|
|
1192
1504
|
const itemCount = this.selectedItems.size;
|
|
1193
1505
|
const itemType = itemCount === 1 ? 'item' : 'items';
|
|
@@ -1212,6 +1524,127 @@ export class Editor extends RapidElement {
|
|
|
1212
1524
|
document.body.removeChild(dialog);
|
|
1213
1525
|
});
|
|
1214
1526
|
}
|
|
1527
|
+
performReflow() {
|
|
1528
|
+
var _b, _c, _d;
|
|
1529
|
+
if (!this.definition || this.definition.nodes.length === 0)
|
|
1530
|
+
return;
|
|
1531
|
+
// Save current positions for discard (nodes + stickies)
|
|
1532
|
+
const savedPositions = {};
|
|
1533
|
+
for (const node of this.definition.nodes) {
|
|
1534
|
+
const ui = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid];
|
|
1535
|
+
if (ui === null || ui === void 0 ? void 0 : ui.position) {
|
|
1536
|
+
savedPositions[node.uuid] = { ...ui.position };
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const stickies = ((_c = this.definition._ui) === null || _c === void 0 ? void 0 : _c.stickies) || {};
|
|
1540
|
+
for (const [uuid, sticky] of Object.entries(stickies)) {
|
|
1541
|
+
if (sticky.position) {
|
|
1542
|
+
savedPositions[uuid] = { ...sticky.position };
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
this.savedReflowPositions = savedPositions;
|
|
1546
|
+
// Save old node positions before reflow for sticky proximity calculation
|
|
1547
|
+
const oldNodePositions = {};
|
|
1548
|
+
for (const node of this.definition.nodes) {
|
|
1549
|
+
const ui = (_d = this.definition._ui) === null || _d === void 0 ? void 0 : _d.nodes[node.uuid];
|
|
1550
|
+
if (ui === null || ui === void 0 ? void 0 : ui.position) {
|
|
1551
|
+
oldNodePositions[node.uuid] = { ...ui.position };
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
// Identify start node (first in sorted array)
|
|
1555
|
+
const startNodeUuid = this.definition.nodes[0].uuid;
|
|
1556
|
+
// Gather node sizes from DOM
|
|
1557
|
+
const nodeSizes = new Map();
|
|
1558
|
+
const getNodeSize = (uuid) => {
|
|
1559
|
+
const element = this.querySelector(`[id="${uuid}"]`);
|
|
1560
|
+
if (element) {
|
|
1561
|
+
const size = {
|
|
1562
|
+
width: element.offsetWidth,
|
|
1563
|
+
height: element.offsetHeight
|
|
1564
|
+
};
|
|
1565
|
+
nodeSizes.set(uuid, size);
|
|
1566
|
+
return size;
|
|
1567
|
+
}
|
|
1568
|
+
const fallback = { width: 200, height: 100 };
|
|
1569
|
+
nodeSizes.set(uuid, fallback);
|
|
1570
|
+
return fallback;
|
|
1571
|
+
};
|
|
1572
|
+
// Compute new layout
|
|
1573
|
+
const newPositions = calculateLayeredLayout(this.definition.nodes, this.definition._ui.nodes, startNodeUuid, getNodeSize);
|
|
1574
|
+
// Place sticky notes next to their closest nodes
|
|
1575
|
+
if (Object.keys(stickies).length > 0) {
|
|
1576
|
+
const stickySizes = new Map();
|
|
1577
|
+
for (const uuid of Object.keys(stickies)) {
|
|
1578
|
+
const el = this.querySelector(`temba-sticky-note[uuid="${uuid}"]`);
|
|
1579
|
+
if (el) {
|
|
1580
|
+
stickySizes.set(uuid, {
|
|
1581
|
+
width: el.offsetWidth,
|
|
1582
|
+
height: el.offsetHeight
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
else {
|
|
1586
|
+
stickySizes.set(uuid, { width: 182, height: 100 });
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
const stickyPositions = placeStickyNotes(stickies, oldNodePositions, newPositions, nodeSizes, stickySizes, startNodeUuid);
|
|
1590
|
+
// Merge sticky positions into newPositions
|
|
1591
|
+
Object.assign(newPositions, stickyPositions);
|
|
1592
|
+
}
|
|
1593
|
+
// Cancel any in-flight save timer so it doesn't persist the reflowed
|
|
1594
|
+
// layout before the user has a chance to Save or Discard.
|
|
1595
|
+
if (this.saveTimer !== null) {
|
|
1596
|
+
clearTimeout(this.saveTimer);
|
|
1597
|
+
this.saveTimer = null;
|
|
1598
|
+
}
|
|
1599
|
+
// Suppress the auto-save from this updateCanvasPositions call
|
|
1600
|
+
this.reflowPending = true;
|
|
1601
|
+
this.reflowUnsaved = true;
|
|
1602
|
+
// Apply new positions
|
|
1603
|
+
getStore().getState().updateCanvasPositions(newPositions);
|
|
1604
|
+
// Update canvas size and repaint connections
|
|
1605
|
+
this.updateCanvasSize();
|
|
1606
|
+
requestAnimationFrame(() => {
|
|
1607
|
+
this.plumber.repaintEverything();
|
|
1608
|
+
});
|
|
1609
|
+
// Scroll to top-left so the start node is visible
|
|
1610
|
+
const editor = this.querySelector('#editor');
|
|
1611
|
+
if (editor) {
|
|
1612
|
+
editor.scrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
|
1613
|
+
}
|
|
1614
|
+
// Start auto-save countdown (duration shared with CSS animation)
|
|
1615
|
+
this.clearReflowAutoSaveTimer();
|
|
1616
|
+
this.reflowAutoSaveTimer = setTimeout(() => {
|
|
1617
|
+
this.reflowAutoSaveTimer = null;
|
|
1618
|
+
if (this.reflowUnsaved) {
|
|
1619
|
+
this.reflowUnsaved = false;
|
|
1620
|
+
this.savedReflowPositions = null;
|
|
1621
|
+
this.saveChanges();
|
|
1622
|
+
}
|
|
1623
|
+
}, REFLOW_AUTO_SAVE_DELAY);
|
|
1624
|
+
}
|
|
1625
|
+
handleReflowDiscard() {
|
|
1626
|
+
this.reflowUnsaved = false;
|
|
1627
|
+
this.clearReflowAutoSaveTimer();
|
|
1628
|
+
if (this.savedReflowPositions) {
|
|
1629
|
+
// Cancel any pending save timer before reverting
|
|
1630
|
+
if (this.saveTimer !== null) {
|
|
1631
|
+
clearTimeout(this.saveTimer);
|
|
1632
|
+
this.saveTimer = null;
|
|
1633
|
+
}
|
|
1634
|
+
// Suppress the auto-save from reverting positions
|
|
1635
|
+
this.reflowPending = true;
|
|
1636
|
+
getStore().getState().updateCanvasPositions(this.savedReflowPositions);
|
|
1637
|
+
this.savedReflowPositions = null;
|
|
1638
|
+
// Clear dirty state since we reverted to the saved version
|
|
1639
|
+
setTimeout(() => {
|
|
1640
|
+
getStore().getState().setDirtyDate(null);
|
|
1641
|
+
this.isSaving = false;
|
|
1642
|
+
}, 0);
|
|
1643
|
+
requestAnimationFrame(() => {
|
|
1644
|
+
this.plumber.repaintEverything();
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1215
1648
|
deleteNodes(uuids) {
|
|
1216
1649
|
// Remove nodes from the definition - CanvasNode will handle plumber cleanup
|
|
1217
1650
|
if (uuids.length > 0) {
|
|
@@ -1233,8 +1666,8 @@ export class Editor extends RapidElement {
|
|
|
1233
1666
|
const canvasRect = (_b = this.querySelector('#canvas')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
|
|
1234
1667
|
if (!canvasRect)
|
|
1235
1668
|
return;
|
|
1236
|
-
const relativeX = event.clientX - canvasRect.left;
|
|
1237
|
-
const relativeY = event.clientY - canvasRect.top;
|
|
1669
|
+
const relativeX = (event.clientX - canvasRect.left) / this.zoom;
|
|
1670
|
+
const relativeY = (event.clientY - canvasRect.top) / this.zoom;
|
|
1238
1671
|
this.selectionBox = {
|
|
1239
1672
|
...this.selectionBox,
|
|
1240
1673
|
endX: relativeX,
|
|
@@ -1259,13 +1692,12 @@ export class Editor extends RapidElement {
|
|
|
1259
1692
|
if (nodeElement) {
|
|
1260
1693
|
const position = (_c = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid]) === null || _c === void 0 ? void 0 : _c.position;
|
|
1261
1694
|
if (position) {
|
|
1262
|
-
const rect = nodeElement.getBoundingClientRect();
|
|
1263
1695
|
const canvasRect = (_d = this.querySelector('#canvas')) === null || _d === void 0 ? void 0 : _d.getBoundingClientRect();
|
|
1264
1696
|
if (canvasRect) {
|
|
1265
1697
|
const nodeLeft = position.left;
|
|
1266
1698
|
const nodeTop = position.top;
|
|
1267
|
-
const nodeRight = nodeLeft +
|
|
1268
|
-
const nodeBottom = nodeTop +
|
|
1699
|
+
const nodeRight = nodeLeft + nodeElement.offsetWidth;
|
|
1700
|
+
const nodeBottom = nodeTop + nodeElement.offsetHeight;
|
|
1269
1701
|
// Check if selection box intersects with node
|
|
1270
1702
|
if (boxLeft < nodeRight &&
|
|
1271
1703
|
boxRight > nodeLeft &&
|
|
@@ -1490,8 +1922,8 @@ export class Editor extends RapidElement {
|
|
|
1490
1922
|
const canvas = this.querySelector('#canvas');
|
|
1491
1923
|
if (canvas) {
|
|
1492
1924
|
const canvasRect = canvas.getBoundingClientRect();
|
|
1493
|
-
const relativeX = event.clientX - canvasRect.left;
|
|
1494
|
-
const relativeY = event.clientY - canvasRect.top;
|
|
1925
|
+
const relativeX = (event.clientX - canvasRect.left) / this.zoom;
|
|
1926
|
+
const relativeY = (event.clientY - canvasRect.top) / this.zoom;
|
|
1495
1927
|
const placeholderWidth = 200;
|
|
1496
1928
|
const placeholderHeight = 64;
|
|
1497
1929
|
const arrowLength = ARROW_LENGTH;
|
|
@@ -1527,37 +1959,119 @@ export class Editor extends RapidElement {
|
|
|
1527
1959
|
// Handle item dragging
|
|
1528
1960
|
if (!this.isMouseDown || !this.currentDragItem)
|
|
1529
1961
|
return;
|
|
1530
|
-
|
|
1531
|
-
const
|
|
1962
|
+
this.lastMouseEvent = event;
|
|
1963
|
+
const deltaX = event.clientX - this.dragStartPos.x + this.autoScrollDeltaX;
|
|
1964
|
+
const deltaY = event.clientY - this.dragStartPos.y + this.autoScrollDeltaY;
|
|
1532
1965
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
1533
1966
|
// Only start dragging if we've moved beyond the threshold
|
|
1534
1967
|
if (!this.isDragging && distance > DRAG_THRESHOLD) {
|
|
1535
1968
|
this.isDragging = true;
|
|
1969
|
+
this.startAutoScroll();
|
|
1536
1970
|
}
|
|
1537
1971
|
// If we're actually dragging, update positions
|
|
1538
1972
|
if (this.isDragging) {
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1973
|
+
this.updateDragPositions();
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
updateDragPositions() {
|
|
1977
|
+
if (!this.currentDragItem || !this.lastMouseEvent)
|
|
1978
|
+
return;
|
|
1979
|
+
// Convert screen + scroll delta to canvas delta
|
|
1980
|
+
const deltaX = (this.lastMouseEvent.clientX -
|
|
1981
|
+
this.dragStartPos.x +
|
|
1982
|
+
this.autoScrollDeltaX) /
|
|
1983
|
+
this.zoom;
|
|
1984
|
+
const deltaY = (this.lastMouseEvent.clientY -
|
|
1985
|
+
this.dragStartPos.y +
|
|
1986
|
+
this.autoScrollDeltaY) /
|
|
1987
|
+
this.zoom;
|
|
1988
|
+
const itemsToMove = this.selectedItems.has(this.currentDragItem.uuid) &&
|
|
1989
|
+
this.selectedItems.size > 1
|
|
1990
|
+
? Array.from(this.selectedItems)
|
|
1991
|
+
: [this.currentDragItem.uuid];
|
|
1992
|
+
itemsToMove.forEach((uuid) => {
|
|
1993
|
+
const element = this.querySelector(`[uuid="${uuid}"]`);
|
|
1994
|
+
if (element) {
|
|
1995
|
+
const type = element.tagName === 'TEMBA-FLOW-NODE' ? 'node' : 'sticky';
|
|
1996
|
+
const position = this.getPosition(uuid, type);
|
|
1997
|
+
if (position) {
|
|
1998
|
+
element.style.left = `${position.left + deltaX}px`;
|
|
1999
|
+
element.style.top = `${position.top + deltaY}px`;
|
|
2000
|
+
element.classList.add('dragging');
|
|
1558
2001
|
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
this.plumber.revalidate(itemsToMove);
|
|
2005
|
+
}
|
|
2006
|
+
startAutoScroll() {
|
|
2007
|
+
if (this.autoScrollAnimationId !== null)
|
|
2008
|
+
return;
|
|
2009
|
+
const editor = this.querySelector('#editor');
|
|
2010
|
+
if (!editor)
|
|
2011
|
+
return;
|
|
2012
|
+
const tick = () => {
|
|
2013
|
+
if (!this.isDragging || !this.lastMouseEvent) {
|
|
2014
|
+
this.autoScrollAnimationId = null;
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const editorRect = editor.getBoundingClientRect();
|
|
2018
|
+
const mouseX = this.lastMouseEvent.clientX;
|
|
2019
|
+
const mouseY = this.lastMouseEvent.clientY;
|
|
2020
|
+
let scrollDx = 0;
|
|
2021
|
+
let scrollDy = 0;
|
|
2022
|
+
// Left edge
|
|
2023
|
+
const distFromLeft = mouseX - editorRect.left;
|
|
2024
|
+
if (distFromLeft >= 0 && distFromLeft < AUTO_SCROLL_EDGE_ZONE) {
|
|
2025
|
+
const ratio = 1 - distFromLeft / AUTO_SCROLL_EDGE_ZONE;
|
|
2026
|
+
scrollDx = -(ratio * AUTO_SCROLL_MAX_SPEED);
|
|
2027
|
+
}
|
|
2028
|
+
// Right edge
|
|
2029
|
+
const distFromRight = editorRect.right - mouseX;
|
|
2030
|
+
if (distFromRight >= 0 && distFromRight < AUTO_SCROLL_EDGE_ZONE) {
|
|
2031
|
+
const ratio = 1 - distFromRight / AUTO_SCROLL_EDGE_ZONE;
|
|
2032
|
+
scrollDx = ratio * AUTO_SCROLL_MAX_SPEED;
|
|
2033
|
+
}
|
|
2034
|
+
// Top edge
|
|
2035
|
+
const distFromTop = mouseY - editorRect.top;
|
|
2036
|
+
if (distFromTop >= 0 && distFromTop < AUTO_SCROLL_EDGE_ZONE) {
|
|
2037
|
+
const ratio = 1 - distFromTop / AUTO_SCROLL_EDGE_ZONE;
|
|
2038
|
+
scrollDy = -(ratio * AUTO_SCROLL_MAX_SPEED);
|
|
2039
|
+
}
|
|
2040
|
+
// Bottom edge
|
|
2041
|
+
const distFromBottom = editorRect.bottom - mouseY;
|
|
2042
|
+
if (distFromBottom >= 0 && distFromBottom < AUTO_SCROLL_EDGE_ZONE) {
|
|
2043
|
+
const ratio = 1 - distFromBottom / AUTO_SCROLL_EDGE_ZONE;
|
|
2044
|
+
scrollDy = ratio * AUTO_SCROLL_MAX_SPEED;
|
|
2045
|
+
}
|
|
2046
|
+
if (scrollDx !== 0 || scrollDy !== 0) {
|
|
2047
|
+
const beforeScrollLeft = editor.scrollLeft;
|
|
2048
|
+
const beforeScrollTop = editor.scrollTop;
|
|
2049
|
+
// Expand canvas if scrolling toward bottom/right edges
|
|
2050
|
+
// Convert from scroll space to canvas space for expandCanvas
|
|
2051
|
+
if (scrollDx > 0 || scrollDy > 0) {
|
|
2052
|
+
const neededWidth = (editor.scrollLeft + editor.clientWidth + scrollDx) / this.zoom;
|
|
2053
|
+
const neededHeight = (editor.scrollTop + editor.clientHeight + scrollDy) / this.zoom;
|
|
2054
|
+
getStore().getState().expandCanvas(neededWidth, neededHeight);
|
|
2055
|
+
}
|
|
2056
|
+
editor.scrollLeft += scrollDx;
|
|
2057
|
+
editor.scrollTop += scrollDy;
|
|
2058
|
+
// Track actual scroll delta (browser clamps at boundaries)
|
|
2059
|
+
const actualDx = editor.scrollLeft - beforeScrollLeft;
|
|
2060
|
+
const actualDy = editor.scrollTop - beforeScrollTop;
|
|
2061
|
+
this.autoScrollDeltaX += actualDx;
|
|
2062
|
+
this.autoScrollDeltaY += actualDy;
|
|
2063
|
+
if (actualDx !== 0 || actualDy !== 0) {
|
|
2064
|
+
this.updateDragPositions();
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
this.autoScrollAnimationId = requestAnimationFrame(tick);
|
|
2068
|
+
};
|
|
2069
|
+
this.autoScrollAnimationId = requestAnimationFrame(tick);
|
|
2070
|
+
}
|
|
2071
|
+
stopAutoScroll() {
|
|
2072
|
+
if (this.autoScrollAnimationId !== null) {
|
|
2073
|
+
cancelAnimationFrame(this.autoScrollAnimationId);
|
|
2074
|
+
this.autoScrollAnimationId = null;
|
|
1561
2075
|
}
|
|
1562
2076
|
}
|
|
1563
2077
|
handleMouseUp(event) {
|
|
@@ -1577,10 +2091,14 @@ export class Editor extends RapidElement {
|
|
|
1577
2091
|
// Handle item drag completion
|
|
1578
2092
|
if (!this.isMouseDown || !this.currentDragItem)
|
|
1579
2093
|
return;
|
|
2094
|
+
this.stopAutoScroll();
|
|
1580
2095
|
// If we were actually dragging, handle the drag end
|
|
1581
2096
|
if (this.isDragging) {
|
|
1582
|
-
|
|
1583
|
-
const
|
|
2097
|
+
// Convert screen + scroll delta to canvas delta
|
|
2098
|
+
const deltaX = (event.clientX - this.dragStartPos.x + this.autoScrollDeltaX) /
|
|
2099
|
+
this.zoom;
|
|
2100
|
+
const deltaY = (event.clientY - this.dragStartPos.y + this.autoScrollDeltaY) /
|
|
2101
|
+
this.zoom;
|
|
1584
2102
|
// Determine what items were moved
|
|
1585
2103
|
const itemsToMove = this.selectedItems.has(this.currentDragItem.uuid) &&
|
|
1586
2104
|
this.selectedItems.size > 1
|
|
@@ -1635,6 +2153,9 @@ export class Editor extends RapidElement {
|
|
|
1635
2153
|
this.isMouseDown = false;
|
|
1636
2154
|
this.currentDragItem = null;
|
|
1637
2155
|
this.canvasMouseDown = false;
|
|
2156
|
+
this.autoScrollDeltaX = 0;
|
|
2157
|
+
this.autoScrollDeltaY = 0;
|
|
2158
|
+
this.lastMouseEvent = null;
|
|
1638
2159
|
}
|
|
1639
2160
|
updateCanvasSize() {
|
|
1640
2161
|
var _b;
|
|
@@ -1652,9 +2173,9 @@ export class Editor extends RapidElement {
|
|
|
1652
2173
|
if (ui && ui.position) {
|
|
1653
2174
|
const nodeElement = this.querySelector(`[id="${node.uuid}"]`);
|
|
1654
2175
|
if (nodeElement) {
|
|
1655
|
-
|
|
1656
|
-
maxWidth = Math.max(maxWidth, ui.position.left +
|
|
1657
|
-
maxHeight = Math.max(maxHeight, ui.position.top +
|
|
2176
|
+
// Use offsetWidth/offsetHeight (unaffected by ancestor transforms)
|
|
2177
|
+
maxWidth = Math.max(maxWidth, ui.position.left + nodeElement.offsetWidth);
|
|
2178
|
+
maxHeight = Math.max(maxHeight, ui.position.top + nodeElement.offsetHeight);
|
|
1658
2179
|
}
|
|
1659
2180
|
}
|
|
1660
2181
|
});
|
|
@@ -1701,18 +2222,19 @@ export class Editor extends RapidElement {
|
|
|
1701
2222
|
return;
|
|
1702
2223
|
}
|
|
1703
2224
|
const canvasRect = canvas.getBoundingClientRect();
|
|
1704
|
-
const relativeX = event.clientX - canvasRect.left - 10;
|
|
1705
|
-
const relativeY = event.clientY - canvasRect.top - 10;
|
|
2225
|
+
const relativeX = (event.clientX - canvasRect.left) / this.zoom - 10;
|
|
2226
|
+
const relativeY = (event.clientY - canvasRect.top) / this.zoom - 10;
|
|
1706
2227
|
// Snap position to grid
|
|
1707
2228
|
const snappedLeft = snapToGrid(relativeX);
|
|
1708
2229
|
const snappedTop = snapToGrid(relativeY);
|
|
1709
2230
|
// Show the canvas menu at the mouse position (use viewport coordinates)
|
|
1710
2231
|
const canvasMenu = this.querySelector('temba-canvas-menu');
|
|
1711
2232
|
if (canvasMenu) {
|
|
2233
|
+
const hasNodes = this.definition && this.definition.nodes.length > 0;
|
|
1712
2234
|
canvasMenu.show(event.clientX, event.clientY, {
|
|
1713
2235
|
x: snappedLeft,
|
|
1714
2236
|
y: snappedTop
|
|
1715
|
-
});
|
|
2237
|
+
}, true, hasNodes);
|
|
1716
2238
|
}
|
|
1717
2239
|
}
|
|
1718
2240
|
handleEmptyFlowClick(event) {
|
|
@@ -1737,6 +2259,10 @@ export class Editor extends RapidElement {
|
|
|
1737
2259
|
handleCanvasMenuSelection(event) {
|
|
1738
2260
|
const selection = event.detail;
|
|
1739
2261
|
const store = getStore();
|
|
2262
|
+
if (selection.action === 'reflow') {
|
|
2263
|
+
this.performReflow();
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
1740
2266
|
if (selection.action === 'sticky') {
|
|
1741
2267
|
// Create new sticky note
|
|
1742
2268
|
store.getState().createStickyNote({
|
|
@@ -2063,8 +2589,9 @@ export class Editor extends RapidElement {
|
|
|
2063
2589
|
(_d = getStore()) === null || _d === void 0 ? void 0 : _d.getState().updateNodeUIConfig(updatedNode.uuid, uiConfig);
|
|
2064
2590
|
}
|
|
2065
2591
|
// Check for collisions and reflow in case node size changed
|
|
2592
|
+
const nodeUuid = updatedNode.uuid;
|
|
2066
2593
|
requestAnimationFrame(() => {
|
|
2067
|
-
this.checkCollisionsAndReflow([
|
|
2594
|
+
this.checkCollisionsAndReflow([nodeUuid]);
|
|
2068
2595
|
});
|
|
2069
2596
|
}
|
|
2070
2597
|
}
|
|
@@ -2094,11 +2621,9 @@ export class Editor extends RapidElement {
|
|
|
2094
2621
|
if (!canvas)
|
|
2095
2622
|
return { left: 0, top: 0 };
|
|
2096
2623
|
const canvasRect = canvas.getBoundingClientRect();
|
|
2097
|
-
//
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
const left = mouseX - canvasRect.left - DROP_PREVIEW_OFFSET_X;
|
|
2101
|
-
const top = mouseY - canvasRect.top - DROP_PREVIEW_OFFSET_Y;
|
|
2624
|
+
// Convert viewport coordinates to canvas coordinates, accounting for zoom
|
|
2625
|
+
const left = (mouseX - canvasRect.left) / this.zoom - DROP_PREVIEW_OFFSET_X;
|
|
2626
|
+
const top = (mouseY - canvasRect.top) / this.zoom - DROP_PREVIEW_OFFSET_Y;
|
|
2102
2627
|
// Apply grid snapping only if requested (for final drop position)
|
|
2103
2628
|
if (applyGridSnapping) {
|
|
2104
2629
|
return {
|
|
@@ -3154,13 +3679,14 @@ export class Editor extends RapidElement {
|
|
|
3154
3679
|
const editorRect = editor.getBoundingClientRect();
|
|
3155
3680
|
const editorCenterX = editorRect.width / 2;
|
|
3156
3681
|
const editorCenterY = editorRect.height / 2;
|
|
3157
|
-
//
|
|
3158
|
-
const
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3161
|
-
//
|
|
3162
|
-
|
|
3163
|
-
const
|
|
3682
|
+
// Use offsetWidth/offsetHeight (unaffected by ancestor transforms)
|
|
3683
|
+
const nodeCenterX = nodeElement.offsetLeft + nodeElement.offsetWidth / 2;
|
|
3684
|
+
const nodeCenterY = nodeElement.offsetTop + nodeElement.offsetHeight / 2;
|
|
3685
|
+
// Calculate the scroll position needed to center the node.
|
|
3686
|
+
// Multiply by zoom because scroll operates in visual (transformed) space
|
|
3687
|
+
// while offsetLeft/offsetTop are in layout space.
|
|
3688
|
+
const targetScrollX = nodeCenterX * this.zoom - editorCenterX;
|
|
3689
|
+
const targetScrollY = nodeCenterY * this.zoom - editorCenterY;
|
|
3164
3690
|
// Smooth scroll the editor container to the target position
|
|
3165
3691
|
editor.scrollTo({
|
|
3166
3692
|
left: Math.max(0, targetScrollX),
|
|
@@ -3206,8 +3732,9 @@ export class Editor extends RapidElement {
|
|
|
3206
3732
|
<div
|
|
3207
3733
|
id="grid"
|
|
3208
3734
|
class="${this.viewingRevision ? 'viewing-revision' : ''}"
|
|
3209
|
-
style="min-width
|
|
3210
|
-
.width}px; height:${this
|
|
3735
|
+
style="min-width:${100 / this.zoom}%;min-height:${100 /
|
|
3736
|
+
this.zoom}%;width:${this.canvasSize.width}px; height:${this
|
|
3737
|
+
.canvasSize.height}px;transform:scale(${this.zoom})"
|
|
3211
3738
|
>
|
|
3212
3739
|
<div
|
|
3213
3740
|
id="canvas"
|
|
@@ -3273,6 +3800,55 @@ export class Editor extends RapidElement {
|
|
|
3273
3800
|
<div class="save-indicator ${this.isSaving ? 'visible' : ''}">
|
|
3274
3801
|
<temba-loading units="3" size="8"></temba-loading>
|
|
3275
3802
|
</div>
|
|
3803
|
+
<div class="zoom-controls">
|
|
3804
|
+
<button
|
|
3805
|
+
@click=${this.zoomToFit}
|
|
3806
|
+
?disabled=${this.zoomFitted}
|
|
3807
|
+
title="Zoom to fit"
|
|
3808
|
+
>
|
|
3809
|
+
<temba-icon name=${Icon.zoom_fit} size="1"></temba-icon>
|
|
3810
|
+
</button>
|
|
3811
|
+
<div class="zoom-divider"></div>
|
|
3812
|
+
<button
|
|
3813
|
+
@click=${this.zoomOut}
|
|
3814
|
+
?disabled=${this.zoom <= 0.1}
|
|
3815
|
+
title="Zoom out"
|
|
3816
|
+
>
|
|
3817
|
+
−
|
|
3818
|
+
</button>
|
|
3819
|
+
<span class="zoom-level">${Math.round(this.zoom * 100)}%</span>
|
|
3820
|
+
<button
|
|
3821
|
+
@click=${this.zoomIn}
|
|
3822
|
+
?disabled=${this.zoom >= 1.0}
|
|
3823
|
+
title="Zoom in"
|
|
3824
|
+
>
|
|
3825
|
+
+
|
|
3826
|
+
</button>
|
|
3827
|
+
<div class="zoom-divider"></div>
|
|
3828
|
+
<button
|
|
3829
|
+
@click=${this.zoomToFull}
|
|
3830
|
+
?disabled=${this.zoom >= 1.0}
|
|
3831
|
+
title="Zoom to 100%"
|
|
3832
|
+
>
|
|
3833
|
+
<temba-icon name=${Icon.zoom_in} size="1"></temba-icon>
|
|
3834
|
+
</button>
|
|
3835
|
+
</div>
|
|
3836
|
+
${this.reflowUnsaved
|
|
3837
|
+
? html `<div class="reflow-card">
|
|
3838
|
+
<div class="reflow-top">
|
|
3839
|
+
<span class="reflow-label">Unsaved layout changes</span>
|
|
3840
|
+
<button
|
|
3841
|
+
class="reflow-discard"
|
|
3842
|
+
@click=${this.handleReflowDiscard}
|
|
3843
|
+
>
|
|
3844
|
+
Discard
|
|
3845
|
+
</button>
|
|
3846
|
+
</div>
|
|
3847
|
+
<div class="reflow-meter">
|
|
3848
|
+
<div class="reflow-meter-fill"></div>
|
|
3849
|
+
</div>
|
|
3850
|
+
</div>`
|
|
3851
|
+
: ''}
|
|
3276
3852
|
</div>
|
|
3277
3853
|
|
|
3278
3854
|
${this.editingNode || this.editingAction
|
|
@@ -3409,6 +3985,18 @@ __decorate([
|
|
|
3409
3985
|
__decorate([
|
|
3410
3986
|
state()
|
|
3411
3987
|
], Editor.prototype, "saveError", void 0);
|
|
3988
|
+
__decorate([
|
|
3989
|
+
state()
|
|
3990
|
+
], Editor.prototype, "zoom", void 0);
|
|
3991
|
+
__decorate([
|
|
3992
|
+
state()
|
|
3993
|
+
], Editor.prototype, "zoomFitted", void 0);
|
|
3994
|
+
__decorate([
|
|
3995
|
+
state()
|
|
3996
|
+
], Editor.prototype, "reflowPending", void 0);
|
|
3997
|
+
__decorate([
|
|
3998
|
+
state()
|
|
3999
|
+
], Editor.prototype, "reflowUnsaved", void 0);
|
|
3412
4000
|
__decorate([
|
|
3413
4001
|
state()
|
|
3414
4002
|
], Editor.prototype, "editingNode", void 0);
|