@nyaruka/temba-components 0.141.0 → 0.142.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/CHANGELOG.md +22 -0
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +859 -656
- 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 +665 -67
- 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/live/ContactChat.js +10 -1
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/version.js +9 -0
- package/out-tsc/src/version.js.map +1 -0
- 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-contact-chat.test.js +12 -0
- package/out-tsc/test/temba-contact-chat.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 +164 -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 +1 -1
- package/rollup.components.mjs +7 -1
- 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 +769 -76
- 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/live/ContactChat.ts +10 -1
- package/src/store/flow-definition.d.ts +1 -0
- package/src/version.ts +10 -0
- 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/temba-canvas-menu.test.ts +55 -0
- package/test/temba-contact-chat.test.ts +17 -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 +211 -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/web-dev-server.config.mjs +5 -1
- package/web-test-runner.config.mjs +4 -1
- 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
|
@@ -8,11 +8,15 @@ import { RapidElement } from '../RapidElement';
|
|
|
8
8
|
import { repeat } from 'lit-html/directives/repeat.js';
|
|
9
9
|
import { CustomEventType } from '../interfaces';
|
|
10
10
|
import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
|
|
11
|
-
import {
|
|
11
|
+
import { TEMBA_COMPONENTS_VERSION } from '../version';
|
|
12
|
+
import { formatIssueMessage, getNodeBounds, calculateReflowPositions, isRightClick, snapToGrid } from './utils';
|
|
12
13
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
14
|
+
import { calculateLayeredLayout, placeStickyNotes } from './reflow';
|
|
15
|
+
import { FloatingTab } from '../display/FloatingTab';
|
|
13
16
|
import { ACTION_GROUP_METADATA } from './types';
|
|
14
17
|
import { Plumber, calculateFlowchartPath, ARROW_LENGTH, ARROW_HALF_WIDTH, CURSOR_GAP } from './Plumber';
|
|
15
18
|
import { CanvasNode } from './CanvasNode';
|
|
19
|
+
import { Icon } from '../Icons';
|
|
16
20
|
export function findNodeForExit(definition, exitUuid) {
|
|
17
21
|
for (const node of definition.nodes) {
|
|
18
22
|
const exit = node.exits.find((e) => e.uuid === exitUuid);
|
|
@@ -24,7 +28,12 @@ export function findNodeForExit(definition, exitUuid) {
|
|
|
24
28
|
}
|
|
25
29
|
const SAVE_QUIET_TIME = 2000;
|
|
26
30
|
const DRAG_THRESHOLD = 5;
|
|
31
|
+
const AUTO_SCROLL_EDGE_ZONE = 100;
|
|
32
|
+
const AUTO_SCROLL_MAX_SPEED = 15;
|
|
27
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;
|
|
28
37
|
// Offset for positioning dropped action node relative to mouse cursor
|
|
29
38
|
// Keep small to make drop location close to cursor position
|
|
30
39
|
const DROP_PREVIEW_OFFSET_X = 20;
|
|
@@ -38,6 +47,12 @@ export class Editor extends RapidElement {
|
|
|
38
47
|
get dragging() {
|
|
39
48
|
return this.isDragging;
|
|
40
49
|
}
|
|
50
|
+
clearReflowAutoSaveTimer() {
|
|
51
|
+
if (this.reflowAutoSaveTimer !== null) {
|
|
52
|
+
clearTimeout(this.reflowAutoSaveTimer);
|
|
53
|
+
this.reflowAutoSaveTimer = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
41
56
|
getAvailableLanguages() {
|
|
42
57
|
var _b, _c, _d;
|
|
43
58
|
// Use languages from workspace if available
|
|
@@ -98,6 +113,7 @@ export class Editor extends RapidElement {
|
|
|
98
113
|
width: 100%;
|
|
99
114
|
display: flex;
|
|
100
115
|
padding-top: 20px;
|
|
116
|
+
transform-origin: 0 0;
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
#canvas {
|
|
@@ -607,7 +623,7 @@ export class Editor extends RapidElement {
|
|
|
607
623
|
.save-indicator {
|
|
608
624
|
position: absolute;
|
|
609
625
|
top: 8px;
|
|
610
|
-
right:
|
|
626
|
+
right: 240px;
|
|
611
627
|
padding: 6px 10px;
|
|
612
628
|
z-index: 10000;
|
|
613
629
|
pointer-events: none;
|
|
@@ -618,6 +634,135 @@ export class Editor extends RapidElement {
|
|
|
618
634
|
.save-indicator.visible {
|
|
619
635
|
opacity: 1;
|
|
620
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
|
+
}
|
|
621
766
|
`;
|
|
622
767
|
}
|
|
623
768
|
constructor() {
|
|
@@ -634,6 +779,11 @@ export class Editor extends RapidElement {
|
|
|
634
779
|
this.dragStartPos = { x: 0, y: 0 };
|
|
635
780
|
this.currentDragItem = null;
|
|
636
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;
|
|
637
787
|
// Selection state
|
|
638
788
|
this.selectedItems = new Set();
|
|
639
789
|
this.isSelecting = false;
|
|
@@ -662,6 +812,12 @@ export class Editor extends RapidElement {
|
|
|
662
812
|
this.isLoadingRevisions = false;
|
|
663
813
|
this.isSaving = false;
|
|
664
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;
|
|
665
821
|
this.preRevertState = null;
|
|
666
822
|
this.translationCache = new Map();
|
|
667
823
|
// NodeEditor state - handles both node and action editing
|
|
@@ -689,11 +845,13 @@ export class Editor extends RapidElement {
|
|
|
689
845
|
this.boundGlobalMouseDown = this.handleGlobalMouseDown.bind(this);
|
|
690
846
|
this.boundKeyDown = this.handleKeyDown.bind(this);
|
|
691
847
|
this.boundCanvasContextMenu = this.handleCanvasContextMenu.bind(this);
|
|
848
|
+
this.boundWheel = this.handleWheel.bind(this);
|
|
692
849
|
}
|
|
693
850
|
firstUpdated(changes) {
|
|
694
851
|
super.firstUpdated(changes);
|
|
695
852
|
this.plumber = new Plumber(this.querySelector('#canvas'), this);
|
|
696
853
|
this.setupGlobalEventListeners();
|
|
854
|
+
this.updateZoomControlPositioning();
|
|
697
855
|
if (changes.has('flow')) {
|
|
698
856
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
699
857
|
this.fetchRevisions();
|
|
@@ -747,10 +905,10 @@ export class Editor extends RapidElement {
|
|
|
747
905
|
const canvas = this.querySelector('#canvas');
|
|
748
906
|
if (canvas) {
|
|
749
907
|
const canvasRect = canvas.getBoundingClientRect();
|
|
750
|
-
const menuX = canvasRect.left + snappedPosition.left - 40;
|
|
908
|
+
const menuX = canvasRect.left + snappedPosition.left * this.zoom - 40;
|
|
751
909
|
const menuY = isDragUp
|
|
752
|
-
? canvasRect.top + snappedPosition.top + 74 // just below placeholder bottom
|
|
753
|
-
: 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
|
|
754
912
|
const canvasMenu = this.querySelector('temba-canvas-menu');
|
|
755
913
|
if (canvasMenu) {
|
|
756
914
|
canvasMenu.show(menuX, menuY, {
|
|
@@ -824,8 +982,21 @@ export class Editor extends RapidElement {
|
|
|
824
982
|
}
|
|
825
983
|
if (changes.has('dirtyDate')) {
|
|
826
984
|
if (this.dirtyDate) {
|
|
827
|
-
this.
|
|
828
|
-
|
|
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
|
+
}
|
|
829
1000
|
}
|
|
830
1001
|
}
|
|
831
1002
|
if (changes.has('saveError') && this.saveError) {
|
|
@@ -860,6 +1031,11 @@ export class Editor extends RapidElement {
|
|
|
860
1031
|
clearTimeout(this.saveTimer);
|
|
861
1032
|
}
|
|
862
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
|
+
}
|
|
863
1039
|
const now = new Date();
|
|
864
1040
|
const timeSinceLastChange = now.getTime() - this.dirtyDate.getTime();
|
|
865
1041
|
if (timeSinceLastChange >= SAVE_QUIET_TIME) {
|
|
@@ -871,8 +1047,17 @@ export class Editor extends RapidElement {
|
|
|
871
1047
|
}
|
|
872
1048
|
}, SAVE_QUIET_TIME);
|
|
873
1049
|
}
|
|
1050
|
+
definitionForSave(definition) {
|
|
1051
|
+
return {
|
|
1052
|
+
...definition,
|
|
1053
|
+
_ui: {
|
|
1054
|
+
...definition._ui,
|
|
1055
|
+
editor: TEMBA_COMPONENTS_VERSION
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
874
1059
|
saveChanges(definitionOverride) {
|
|
875
|
-
const definition = definitionOverride || this.definition;
|
|
1060
|
+
const definition = this.definitionForSave(definitionOverride || this.definition);
|
|
876
1061
|
this.isSaving = true;
|
|
877
1062
|
return getStore()
|
|
878
1063
|
.postJSON(`/flow/revisions/${this.flow}/`, definition)
|
|
@@ -988,6 +1173,7 @@ export class Editor extends RapidElement {
|
|
|
988
1173
|
}
|
|
989
1174
|
disconnectedCallback() {
|
|
990
1175
|
super.disconnectedCallback();
|
|
1176
|
+
this.stopAutoScroll();
|
|
991
1177
|
if (this.saveTimer !== null) {
|
|
992
1178
|
clearTimeout(this.saveTimer);
|
|
993
1179
|
this.saveTimer = null;
|
|
@@ -996,6 +1182,7 @@ export class Editor extends RapidElement {
|
|
|
996
1182
|
clearTimeout(this.activityTimer);
|
|
997
1183
|
this.activityTimer = null;
|
|
998
1184
|
}
|
|
1185
|
+
this.clearReflowAutoSaveTimer();
|
|
999
1186
|
document.removeEventListener('mousemove', this.boundMouseMove);
|
|
1000
1187
|
document.removeEventListener('mouseup', this.boundMouseUp);
|
|
1001
1188
|
document.removeEventListener('mousedown', this.boundGlobalMouseDown);
|
|
@@ -1004,6 +1191,10 @@ export class Editor extends RapidElement {
|
|
|
1004
1191
|
if (canvas) {
|
|
1005
1192
|
canvas.removeEventListener('contextmenu', this.boundCanvasContextMenu);
|
|
1006
1193
|
}
|
|
1194
|
+
const editor = this.querySelector('#editor');
|
|
1195
|
+
if (editor) {
|
|
1196
|
+
editor.removeEventListener('wheel', this.boundWheel);
|
|
1197
|
+
}
|
|
1007
1198
|
// Clear all flow-specific data from the store so stale data
|
|
1008
1199
|
// isn't briefly visible when a different flow is opened.
|
|
1009
1200
|
zustand.getState().clearFlowData();
|
|
@@ -1017,6 +1208,10 @@ export class Editor extends RapidElement {
|
|
|
1017
1208
|
if (canvas) {
|
|
1018
1209
|
canvas.addEventListener('contextmenu', this.boundCanvasContextMenu);
|
|
1019
1210
|
}
|
|
1211
|
+
const editor = this.querySelector('#editor');
|
|
1212
|
+
if (editor) {
|
|
1213
|
+
editor.addEventListener('wheel', this.boundWheel, { passive: false });
|
|
1214
|
+
}
|
|
1020
1215
|
// Listen for action edit requests from flow nodes
|
|
1021
1216
|
this.addEventListener(CustomEventType.ActionEditRequested, this.handleActionEditRequested.bind(this));
|
|
1022
1217
|
// Listen for add action requests from flow nodes
|
|
@@ -1064,8 +1259,7 @@ export class Editor extends RapidElement {
|
|
|
1064
1259
|
}
|
|
1065
1260
|
}
|
|
1066
1261
|
handleMouseDown(event) {
|
|
1067
|
-
|
|
1068
|
-
if (event.button !== 0)
|
|
1262
|
+
if (isRightClick(event))
|
|
1069
1263
|
return;
|
|
1070
1264
|
if (this.isReadOnly())
|
|
1071
1265
|
return;
|
|
@@ -1106,8 +1300,7 @@ export class Editor extends RapidElement {
|
|
|
1106
1300
|
}
|
|
1107
1301
|
handleGlobalMouseDown(event) {
|
|
1108
1302
|
var _b;
|
|
1109
|
-
|
|
1110
|
-
if (event.button !== 0)
|
|
1303
|
+
if (isRightClick(event))
|
|
1111
1304
|
return;
|
|
1112
1305
|
// Check if the click is within our canvas
|
|
1113
1306
|
const canvasRect = (_b = this.querySelector('#grid')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
|
|
@@ -1155,8 +1348,8 @@ export class Editor extends RapidElement {
|
|
|
1155
1348
|
if (canvasRect) {
|
|
1156
1349
|
// Clear current selection
|
|
1157
1350
|
this.selectedItems.clear();
|
|
1158
|
-
const relativeX = event.clientX - canvasRect.left;
|
|
1159
|
-
const relativeY = event.clientY - canvasRect.top;
|
|
1351
|
+
const relativeX = (event.clientX - canvasRect.left) / this.zoom;
|
|
1352
|
+
const relativeY = (event.clientY - canvasRect.top) / this.zoom;
|
|
1160
1353
|
this.selectionBox = {
|
|
1161
1354
|
startX: relativeX,
|
|
1162
1355
|
startY: relativeY,
|
|
@@ -1178,6 +1371,135 @@ export class Editor extends RapidElement {
|
|
|
1178
1371
|
this.requestUpdate();
|
|
1179
1372
|
}
|
|
1180
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
|
+
}
|
|
1181
1503
|
showDeleteConfirmation() {
|
|
1182
1504
|
const itemCount = this.selectedItems.size;
|
|
1183
1505
|
const itemType = itemCount === 1 ? 'item' : 'items';
|
|
@@ -1202,6 +1524,127 @@ export class Editor extends RapidElement {
|
|
|
1202
1524
|
document.body.removeChild(dialog);
|
|
1203
1525
|
});
|
|
1204
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
|
+
}
|
|
1205
1648
|
deleteNodes(uuids) {
|
|
1206
1649
|
// Remove nodes from the definition - CanvasNode will handle plumber cleanup
|
|
1207
1650
|
if (uuids.length > 0) {
|
|
@@ -1223,8 +1666,8 @@ export class Editor extends RapidElement {
|
|
|
1223
1666
|
const canvasRect = (_b = this.querySelector('#canvas')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
|
|
1224
1667
|
if (!canvasRect)
|
|
1225
1668
|
return;
|
|
1226
|
-
const relativeX = event.clientX - canvasRect.left;
|
|
1227
|
-
const relativeY = event.clientY - canvasRect.top;
|
|
1669
|
+
const relativeX = (event.clientX - canvasRect.left) / this.zoom;
|
|
1670
|
+
const relativeY = (event.clientY - canvasRect.top) / this.zoom;
|
|
1228
1671
|
this.selectionBox = {
|
|
1229
1672
|
...this.selectionBox,
|
|
1230
1673
|
endX: relativeX,
|
|
@@ -1249,13 +1692,12 @@ export class Editor extends RapidElement {
|
|
|
1249
1692
|
if (nodeElement) {
|
|
1250
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;
|
|
1251
1694
|
if (position) {
|
|
1252
|
-
const rect = nodeElement.getBoundingClientRect();
|
|
1253
1695
|
const canvasRect = (_d = this.querySelector('#canvas')) === null || _d === void 0 ? void 0 : _d.getBoundingClientRect();
|
|
1254
1696
|
if (canvasRect) {
|
|
1255
1697
|
const nodeLeft = position.left;
|
|
1256
1698
|
const nodeTop = position.top;
|
|
1257
|
-
const nodeRight = nodeLeft +
|
|
1258
|
-
const nodeBottom = nodeTop +
|
|
1699
|
+
const nodeRight = nodeLeft + nodeElement.offsetWidth;
|
|
1700
|
+
const nodeBottom = nodeTop + nodeElement.offsetHeight;
|
|
1259
1701
|
// Check if selection box intersects with node
|
|
1260
1702
|
if (boxLeft < nodeRight &&
|
|
1261
1703
|
boxRight > nodeLeft &&
|
|
@@ -1480,8 +1922,8 @@ export class Editor extends RapidElement {
|
|
|
1480
1922
|
const canvas = this.querySelector('#canvas');
|
|
1481
1923
|
if (canvas) {
|
|
1482
1924
|
const canvasRect = canvas.getBoundingClientRect();
|
|
1483
|
-
const relativeX = event.clientX - canvasRect.left;
|
|
1484
|
-
const relativeY = event.clientY - canvasRect.top;
|
|
1925
|
+
const relativeX = (event.clientX - canvasRect.left) / this.zoom;
|
|
1926
|
+
const relativeY = (event.clientY - canvasRect.top) / this.zoom;
|
|
1485
1927
|
const placeholderWidth = 200;
|
|
1486
1928
|
const placeholderHeight = 64;
|
|
1487
1929
|
const arrowLength = ARROW_LENGTH;
|
|
@@ -1517,37 +1959,119 @@ export class Editor extends RapidElement {
|
|
|
1517
1959
|
// Handle item dragging
|
|
1518
1960
|
if (!this.isMouseDown || !this.currentDragItem)
|
|
1519
1961
|
return;
|
|
1520
|
-
|
|
1521
|
-
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;
|
|
1522
1965
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
1523
1966
|
// Only start dragging if we've moved beyond the threshold
|
|
1524
1967
|
if (!this.isDragging && distance > DRAG_THRESHOLD) {
|
|
1525
1968
|
this.isDragging = true;
|
|
1969
|
+
this.startAutoScroll();
|
|
1526
1970
|
}
|
|
1527
1971
|
// If we're actually dragging, update positions
|
|
1528
1972
|
if (this.isDragging) {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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');
|
|
1548
2001
|
}
|
|
1549
|
-
}
|
|
1550
|
-
|
|
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;
|
|
1551
2075
|
}
|
|
1552
2076
|
}
|
|
1553
2077
|
handleMouseUp(event) {
|
|
@@ -1567,10 +2091,14 @@ export class Editor extends RapidElement {
|
|
|
1567
2091
|
// Handle item drag completion
|
|
1568
2092
|
if (!this.isMouseDown || !this.currentDragItem)
|
|
1569
2093
|
return;
|
|
2094
|
+
this.stopAutoScroll();
|
|
1570
2095
|
// If we were actually dragging, handle the drag end
|
|
1571
2096
|
if (this.isDragging) {
|
|
1572
|
-
|
|
1573
|
-
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;
|
|
1574
2102
|
// Determine what items were moved
|
|
1575
2103
|
const itemsToMove = this.selectedItems.has(this.currentDragItem.uuid) &&
|
|
1576
2104
|
this.selectedItems.size > 1
|
|
@@ -1625,6 +2153,9 @@ export class Editor extends RapidElement {
|
|
|
1625
2153
|
this.isMouseDown = false;
|
|
1626
2154
|
this.currentDragItem = null;
|
|
1627
2155
|
this.canvasMouseDown = false;
|
|
2156
|
+
this.autoScrollDeltaX = 0;
|
|
2157
|
+
this.autoScrollDeltaY = 0;
|
|
2158
|
+
this.lastMouseEvent = null;
|
|
1628
2159
|
}
|
|
1629
2160
|
updateCanvasSize() {
|
|
1630
2161
|
var _b;
|
|
@@ -1642,9 +2173,9 @@ export class Editor extends RapidElement {
|
|
|
1642
2173
|
if (ui && ui.position) {
|
|
1643
2174
|
const nodeElement = this.querySelector(`[id="${node.uuid}"]`);
|
|
1644
2175
|
if (nodeElement) {
|
|
1645
|
-
|
|
1646
|
-
maxWidth = Math.max(maxWidth, ui.position.left +
|
|
1647
|
-
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);
|
|
1648
2179
|
}
|
|
1649
2180
|
}
|
|
1650
2181
|
});
|
|
@@ -1691,18 +2222,19 @@ export class Editor extends RapidElement {
|
|
|
1691
2222
|
return;
|
|
1692
2223
|
}
|
|
1693
2224
|
const canvasRect = canvas.getBoundingClientRect();
|
|
1694
|
-
const relativeX = event.clientX - canvasRect.left - 10;
|
|
1695
|
-
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;
|
|
1696
2227
|
// Snap position to grid
|
|
1697
2228
|
const snappedLeft = snapToGrid(relativeX);
|
|
1698
2229
|
const snappedTop = snapToGrid(relativeY);
|
|
1699
2230
|
// Show the canvas menu at the mouse position (use viewport coordinates)
|
|
1700
2231
|
const canvasMenu = this.querySelector('temba-canvas-menu');
|
|
1701
2232
|
if (canvasMenu) {
|
|
2233
|
+
const hasNodes = this.definition && this.definition.nodes.length > 0;
|
|
1702
2234
|
canvasMenu.show(event.clientX, event.clientY, {
|
|
1703
2235
|
x: snappedLeft,
|
|
1704
2236
|
y: snappedTop
|
|
1705
|
-
});
|
|
2237
|
+
}, true, hasNodes);
|
|
1706
2238
|
}
|
|
1707
2239
|
}
|
|
1708
2240
|
handleEmptyFlowClick(event) {
|
|
@@ -1727,6 +2259,10 @@ export class Editor extends RapidElement {
|
|
|
1727
2259
|
handleCanvasMenuSelection(event) {
|
|
1728
2260
|
const selection = event.detail;
|
|
1729
2261
|
const store = getStore();
|
|
2262
|
+
if (selection.action === 'reflow') {
|
|
2263
|
+
this.performReflow();
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
1730
2266
|
if (selection.action === 'sticky') {
|
|
1731
2267
|
// Create new sticky note
|
|
1732
2268
|
store.getState().createStickyNote({
|
|
@@ -2053,8 +2589,9 @@ export class Editor extends RapidElement {
|
|
|
2053
2589
|
(_d = getStore()) === null || _d === void 0 ? void 0 : _d.getState().updateNodeUIConfig(updatedNode.uuid, uiConfig);
|
|
2054
2590
|
}
|
|
2055
2591
|
// Check for collisions and reflow in case node size changed
|
|
2592
|
+
const nodeUuid = updatedNode.uuid;
|
|
2056
2593
|
requestAnimationFrame(() => {
|
|
2057
|
-
this.checkCollisionsAndReflow([
|
|
2594
|
+
this.checkCollisionsAndReflow([nodeUuid]);
|
|
2058
2595
|
});
|
|
2059
2596
|
}
|
|
2060
2597
|
}
|
|
@@ -2084,11 +2621,9 @@ export class Editor extends RapidElement {
|
|
|
2084
2621
|
if (!canvas)
|
|
2085
2622
|
return { left: 0, top: 0 };
|
|
2086
2623
|
const canvasRect = canvas.getBoundingClientRect();
|
|
2087
|
-
//
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const left = mouseX - canvasRect.left - DROP_PREVIEW_OFFSET_X;
|
|
2091
|
-
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;
|
|
2092
2627
|
// Apply grid snapping only if requested (for final drop position)
|
|
2093
2628
|
if (applyGridSnapping) {
|
|
2094
2629
|
return {
|
|
@@ -3144,13 +3679,14 @@ export class Editor extends RapidElement {
|
|
|
3144
3679
|
const editorRect = editor.getBoundingClientRect();
|
|
3145
3680
|
const editorCenterX = editorRect.width / 2;
|
|
3146
3681
|
const editorCenterY = editorRect.height / 2;
|
|
3147
|
-
//
|
|
3148
|
-
const
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
//
|
|
3152
|
-
|
|
3153
|
-
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;
|
|
3154
3690
|
// Smooth scroll the editor container to the target position
|
|
3155
3691
|
editor.scrollTo({
|
|
3156
3692
|
left: Math.max(0, targetScrollX),
|
|
@@ -3196,8 +3732,9 @@ export class Editor extends RapidElement {
|
|
|
3196
3732
|
<div
|
|
3197
3733
|
id="grid"
|
|
3198
3734
|
class="${this.viewingRevision ? 'viewing-revision' : ''}"
|
|
3199
|
-
style="min-width
|
|
3200
|
-
.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})"
|
|
3201
3738
|
>
|
|
3202
3739
|
<div
|
|
3203
3740
|
id="canvas"
|
|
@@ -3263,6 +3800,55 @@ export class Editor extends RapidElement {
|
|
|
3263
3800
|
<div class="save-indicator ${this.isSaving ? 'visible' : ''}">
|
|
3264
3801
|
<temba-loading units="3" size="8"></temba-loading>
|
|
3265
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
|
+
: ''}
|
|
3266
3852
|
</div>
|
|
3267
3853
|
|
|
3268
3854
|
${this.editingNode || this.editingAction
|
|
@@ -3399,6 +3985,18 @@ __decorate([
|
|
|
3399
3985
|
__decorate([
|
|
3400
3986
|
state()
|
|
3401
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);
|
|
3402
4000
|
__decorate([
|
|
3403
4001
|
state()
|
|
3404
4002
|
], Editor.prototype, "editingNode", void 0);
|