@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html, fixture, expect } from '@open-wc/testing';
|
|
2
2
|
import { Editor } from '../src/flow/Editor';
|
|
3
3
|
import { Plumber } from '../src/flow/Plumber';
|
|
4
|
-
import { stub, restore } from 'sinon';
|
|
4
|
+
import { stub, restore, useFakeTimers } from 'sinon';
|
|
5
5
|
import { zustand } from '../src/store/AppState';
|
|
6
6
|
import { TEMBA_COMPONENTS_VERSION } from '../src/version';
|
|
7
7
|
|
|
@@ -1086,4 +1086,190 @@ describe('Editor', () => {
|
|
|
1086
1086
|
).to.equal('Save failed with status 403.');
|
|
1087
1087
|
});
|
|
1088
1088
|
});
|
|
1089
|
+
|
|
1090
|
+
describe('reflow card', () => {
|
|
1091
|
+
let clock: any;
|
|
1092
|
+
|
|
1093
|
+
beforeEach(() => {
|
|
1094
|
+
clock = useFakeTimers();
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
afterEach(() => {
|
|
1098
|
+
clock.restore();
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it('renders only Discard button and meter when reflowUnsaved', async () => {
|
|
1102
|
+
editor = await fixture(html`
|
|
1103
|
+
<temba-flow-editor>
|
|
1104
|
+
<div id="canvas"></div>
|
|
1105
|
+
</temba-flow-editor>
|
|
1106
|
+
`);
|
|
1107
|
+
|
|
1108
|
+
(editor as any).canvasSize = { width: 800, height: 600 };
|
|
1109
|
+
(editor as any).reflowUnsaved = true;
|
|
1110
|
+
await editor.updateComplete;
|
|
1111
|
+
|
|
1112
|
+
const card = editor.querySelector('.reflow-card');
|
|
1113
|
+
expect(card).to.exist;
|
|
1114
|
+
|
|
1115
|
+
const discardBtn = card.querySelector('.reflow-discard');
|
|
1116
|
+
expect(discardBtn).to.exist;
|
|
1117
|
+
expect(discardBtn.textContent.trim()).to.equal('Discard');
|
|
1118
|
+
|
|
1119
|
+
const saveBtn = card.querySelector('.reflow-save');
|
|
1120
|
+
expect(saveBtn).to.not.exist;
|
|
1121
|
+
|
|
1122
|
+
const meter = card.querySelector('.reflow-meter');
|
|
1123
|
+
expect(meter).to.exist;
|
|
1124
|
+
|
|
1125
|
+
const meterFill = card.querySelector('.reflow-meter-fill');
|
|
1126
|
+
expect(meterFill).to.exist;
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it('does not render reflow card when reflowUnsaved is false', async () => {
|
|
1130
|
+
editor = await fixture(html`
|
|
1131
|
+
<temba-flow-editor>
|
|
1132
|
+
<div id="canvas"></div>
|
|
1133
|
+
</temba-flow-editor>
|
|
1134
|
+
`);
|
|
1135
|
+
|
|
1136
|
+
(editor as any).canvasSize = { width: 800, height: 600 };
|
|
1137
|
+
(editor as any).reflowUnsaved = false;
|
|
1138
|
+
await editor.updateComplete;
|
|
1139
|
+
|
|
1140
|
+
const card = editor.querySelector('.reflow-card');
|
|
1141
|
+
expect(card).to.not.exist;
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
it('auto-saves after countdown expires', async () => {
|
|
1145
|
+
editor = await fixture(html`
|
|
1146
|
+
<temba-flow-editor>
|
|
1147
|
+
<div id="canvas"></div>
|
|
1148
|
+
</temba-flow-editor>
|
|
1149
|
+
`);
|
|
1150
|
+
|
|
1151
|
+
const saveStub = stub(editor as any, 'saveChanges').resolves();
|
|
1152
|
+
(editor as any).reflowUnsaved = true;
|
|
1153
|
+
(editor as any).savedReflowPositions = { 'node-1': { left: 0, top: 0 } };
|
|
1154
|
+
|
|
1155
|
+
// Start the auto-save timer
|
|
1156
|
+
(editor as any).clearReflowAutoSaveTimer();
|
|
1157
|
+
(editor as any).reflowAutoSaveTimer = setTimeout(() => {
|
|
1158
|
+
(editor as any).reflowAutoSaveTimer = null;
|
|
1159
|
+
if ((editor as any).reflowUnsaved) {
|
|
1160
|
+
(editor as any).reflowUnsaved = false;
|
|
1161
|
+
(editor as any).savedReflowPositions = null;
|
|
1162
|
+
(editor as any).saveChanges();
|
|
1163
|
+
}
|
|
1164
|
+
}, 5000);
|
|
1165
|
+
|
|
1166
|
+
// Advance time just before the deadline
|
|
1167
|
+
clock.tick(4999);
|
|
1168
|
+
expect(saveStub).to.not.have.been.called;
|
|
1169
|
+
expect((editor as any).reflowUnsaved).to.be.true;
|
|
1170
|
+
|
|
1171
|
+
// Advance past the deadline
|
|
1172
|
+
clock.tick(1);
|
|
1173
|
+
expect(saveStub).to.have.been.calledOnce;
|
|
1174
|
+
expect((editor as any).reflowUnsaved).to.be.false;
|
|
1175
|
+
expect((editor as any).savedReflowPositions).to.be.null;
|
|
1176
|
+
|
|
1177
|
+
saveStub.restore();
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
it('cancels auto-save timer when Discard is clicked', async () => {
|
|
1181
|
+
editor = await fixture(html`
|
|
1182
|
+
<temba-flow-editor>
|
|
1183
|
+
<div id="canvas"></div>
|
|
1184
|
+
</temba-flow-editor>
|
|
1185
|
+
`);
|
|
1186
|
+
|
|
1187
|
+
const saveStub = stub(editor as any, 'saveChanges').resolves();
|
|
1188
|
+
(editor as any).reflowUnsaved = true;
|
|
1189
|
+
// Don't set savedReflowPositions — we're testing timer cancellation,
|
|
1190
|
+
// not the position-revert logic (which needs the store).
|
|
1191
|
+
|
|
1192
|
+
// Start the auto-save timer
|
|
1193
|
+
(editor as any).reflowAutoSaveTimer = setTimeout(() => {
|
|
1194
|
+
(editor as any).saveChanges();
|
|
1195
|
+
}, 5000);
|
|
1196
|
+
|
|
1197
|
+
// Click discard
|
|
1198
|
+
(editor as any).handleReflowDiscard();
|
|
1199
|
+
|
|
1200
|
+
expect((editor as any).reflowUnsaved).to.be.false;
|
|
1201
|
+
expect((editor as any).reflowAutoSaveTimer).to.be.null;
|
|
1202
|
+
|
|
1203
|
+
// Advance past the original deadline — save should NOT fire
|
|
1204
|
+
clock.tick(6000);
|
|
1205
|
+
expect(saveStub).to.not.have.been.called;
|
|
1206
|
+
|
|
1207
|
+
saveStub.restore();
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
it('cancels auto-save timer when a normal edit dismisses card', async () => {
|
|
1211
|
+
editor = await fixture(html`
|
|
1212
|
+
<temba-flow-editor>
|
|
1213
|
+
<div id="canvas"></div>
|
|
1214
|
+
</temba-flow-editor>
|
|
1215
|
+
`);
|
|
1216
|
+
|
|
1217
|
+
const saveStub = stub(editor as any, 'saveChanges').resolves();
|
|
1218
|
+
(editor as any).reflowUnsaved = true;
|
|
1219
|
+
(editor as any).savedReflowPositions = { 'node-1': { left: 0, top: 0 } };
|
|
1220
|
+
|
|
1221
|
+
// Start the auto-save timer
|
|
1222
|
+
(editor as any).reflowAutoSaveTimer = setTimeout(() => {
|
|
1223
|
+
(editor as any).saveChanges();
|
|
1224
|
+
}, 5000);
|
|
1225
|
+
|
|
1226
|
+
// Simulate a normal edit triggering the dirtyDate handler
|
|
1227
|
+
(editor as any).dirtyDate = new Date();
|
|
1228
|
+
const changes = new Map();
|
|
1229
|
+
changes.set('dirtyDate', null);
|
|
1230
|
+
(editor as any).updated(changes);
|
|
1231
|
+
|
|
1232
|
+
expect((editor as any).reflowUnsaved).to.be.false;
|
|
1233
|
+
expect((editor as any).reflowAutoSaveTimer).to.be.null;
|
|
1234
|
+
|
|
1235
|
+
// The debouncedSave from the normal edit will call saveChanges,
|
|
1236
|
+
// but the reflow timer should not fire separately
|
|
1237
|
+
clock.tick(6000);
|
|
1238
|
+
|
|
1239
|
+
saveStub.restore();
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
it('clears auto-save timer in disconnectedCallback', async () => {
|
|
1243
|
+
editor = await fixture(html`
|
|
1244
|
+
<temba-flow-editor>
|
|
1245
|
+
<div id="canvas"></div>
|
|
1246
|
+
</temba-flow-editor>
|
|
1247
|
+
`);
|
|
1248
|
+
|
|
1249
|
+
// Start the auto-save timer
|
|
1250
|
+
(editor as any).reflowAutoSaveTimer = setTimeout(() => {}, 5000);
|
|
1251
|
+
expect((editor as any).reflowAutoSaveTimer).to.not.be.null;
|
|
1252
|
+
|
|
1253
|
+
// Remove the editor from DOM
|
|
1254
|
+
editor.remove();
|
|
1255
|
+
|
|
1256
|
+
expect((editor as any).reflowAutoSaveTimer).to.be.null;
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
it('clears existing timer when performReflow is called again', () => {
|
|
1260
|
+
editor = new Editor();
|
|
1261
|
+
|
|
1262
|
+
// Set up a first timer
|
|
1263
|
+
const firstTimer = setTimeout(() => {}, 5000);
|
|
1264
|
+
(editor as any).reflowAutoSaveTimer = firstTimer;
|
|
1265
|
+
|
|
1266
|
+
// Call clearReflowAutoSaveTimer (which performReflow calls)
|
|
1267
|
+
(editor as any).clearReflowAutoSaveTimer();
|
|
1268
|
+
|
|
1269
|
+
expect((editor as any).reflowAutoSaveTimer).to.be.null;
|
|
1270
|
+
|
|
1271
|
+
// The old timer should be cleared — advancing time should not fire it
|
|
1272
|
+
clock.tick(6000);
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1089
1275
|
});
|
|
@@ -334,4 +334,175 @@ describe('temba-flow-node drag and drop functionality', () => {
|
|
|
334
334
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
335
335
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
336
336
|
});
|
|
337
|
+
|
|
338
|
+
describe('auto-scroll during drag', () => {
|
|
339
|
+
const AUTO_SCROLL_EDGE_ZONE = 100;
|
|
340
|
+
const AUTO_SCROLL_MAX_SPEED = 15;
|
|
341
|
+
|
|
342
|
+
function calculateScrollSpeed(
|
|
343
|
+
mousePos: number,
|
|
344
|
+
edgeStart: number,
|
|
345
|
+
edgeEnd: number
|
|
346
|
+
): { dx: number; dy: number } {
|
|
347
|
+
const dx = 0;
|
|
348
|
+
const dy = 0;
|
|
349
|
+
|
|
350
|
+
// Left/top edge
|
|
351
|
+
const distFromStart = mousePos - edgeStart;
|
|
352
|
+
if (distFromStart >= 0 && distFromStart < AUTO_SCROLL_EDGE_ZONE) {
|
|
353
|
+
const ratio = 1 - distFromStart / AUTO_SCROLL_EDGE_ZONE;
|
|
354
|
+
return { dx: -(ratio * AUTO_SCROLL_MAX_SPEED), dy: 0 };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Right/bottom edge
|
|
358
|
+
const distFromEnd = edgeEnd - mousePos;
|
|
359
|
+
if (distFromEnd >= 0 && distFromEnd < AUTO_SCROLL_EDGE_ZONE) {
|
|
360
|
+
const ratio = 1 - distFromEnd / AUTO_SCROLL_EDGE_ZONE;
|
|
361
|
+
return { dx: ratio * AUTO_SCROLL_MAX_SPEED, dy: 0 };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { dx, dy };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
it('should calculate scroll speed based on distance to edge', () => {
|
|
368
|
+
const edgeStart = 0;
|
|
369
|
+
const edgeEnd = 800;
|
|
370
|
+
|
|
371
|
+
// At the very left edge (distance = 0), speed should be max
|
|
372
|
+
let result = calculateScrollSpeed(0, edgeStart, edgeEnd);
|
|
373
|
+
assert.equal(result.dx, -AUTO_SCROLL_MAX_SPEED);
|
|
374
|
+
|
|
375
|
+
// At the edge zone boundary, speed should be 0
|
|
376
|
+
result = calculateScrollSpeed(AUTO_SCROLL_EDGE_ZONE, edgeStart, edgeEnd);
|
|
377
|
+
assert.equal(result.dx, 0);
|
|
378
|
+
|
|
379
|
+
// Halfway into the zone, speed should be half of max
|
|
380
|
+
result = calculateScrollSpeed(
|
|
381
|
+
AUTO_SCROLL_EDGE_ZONE / 2,
|
|
382
|
+
edgeStart,
|
|
383
|
+
edgeEnd
|
|
384
|
+
);
|
|
385
|
+
assert.closeTo(result.dx, -AUTO_SCROLL_MAX_SPEED / 2, 0.01);
|
|
386
|
+
|
|
387
|
+
// At the very right edge (distance = 0), speed should be max positive
|
|
388
|
+
result = calculateScrollSpeed(800, edgeStart, edgeEnd);
|
|
389
|
+
assert.equal(result.dx, AUTO_SCROLL_MAX_SPEED);
|
|
390
|
+
|
|
391
|
+
// Halfway into the right edge zone
|
|
392
|
+
result = calculateScrollSpeed(
|
|
393
|
+
edgeEnd - AUTO_SCROLL_EDGE_ZONE / 2,
|
|
394
|
+
edgeStart,
|
|
395
|
+
edgeEnd
|
|
396
|
+
);
|
|
397
|
+
assert.closeTo(result.dx, AUTO_SCROLL_MAX_SPEED / 2, 0.01);
|
|
398
|
+
|
|
399
|
+
// In the middle of the viewport, no scrolling
|
|
400
|
+
result = calculateScrollSpeed(400, edgeStart, edgeEnd);
|
|
401
|
+
assert.equal(result.dx, 0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should not scroll when mouse is outside the viewport', () => {
|
|
405
|
+
const edgeStart = 0;
|
|
406
|
+
const edgeEnd = 800;
|
|
407
|
+
|
|
408
|
+
// Mouse is to the left of the viewport
|
|
409
|
+
const result = calculateScrollSpeed(-10, edgeStart, edgeEnd);
|
|
410
|
+
assert.equal(result.dx, 0);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should account for scroll delta in drag position calculation', () => {
|
|
414
|
+
// Simulate the formula: deltaX = (clientX - startX) + autoScrollDeltaX
|
|
415
|
+
const dragStartX = 400;
|
|
416
|
+
const currentClientX = 450;
|
|
417
|
+
const autoScrollDeltaX = 200;
|
|
418
|
+
|
|
419
|
+
const deltaX = currentClientX - dragStartX + autoScrollDeltaX;
|
|
420
|
+
|
|
421
|
+
// Without auto-scroll, delta would be 50. With 200px of scroll, it's 250.
|
|
422
|
+
assert.equal(deltaX, 250);
|
|
423
|
+
|
|
424
|
+
const originalLeft = 100;
|
|
425
|
+
const newLeft = originalLeft + deltaX;
|
|
426
|
+
assert.equal(newLeft, 350);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should accumulate scroll deltas correctly over multiple frames', () => {
|
|
430
|
+
let autoScrollDeltaX = 0;
|
|
431
|
+
let autoScrollDeltaY = 0;
|
|
432
|
+
|
|
433
|
+
// Simulate several frames of scrolling
|
|
434
|
+
const scrollIncrements = [
|
|
435
|
+
{ dx: 5, dy: 3 },
|
|
436
|
+
{ dx: 8, dy: 6 },
|
|
437
|
+
{ dx: 10, dy: 9 },
|
|
438
|
+
{ dx: 0, dy: 12 } // only vertical scrolling
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
scrollIncrements.forEach(({ dx, dy }) => {
|
|
442
|
+
autoScrollDeltaX += dx;
|
|
443
|
+
autoScrollDeltaY += dy;
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
assert.equal(autoScrollDeltaX, 23);
|
|
447
|
+
assert.equal(autoScrollDeltaY, 30);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should clamp scroll delta when at scroll boundaries', () => {
|
|
451
|
+
// Simulate a scroll container at its left boundary
|
|
452
|
+
let scrollLeft = 0;
|
|
453
|
+
const requestedDx = -10;
|
|
454
|
+
|
|
455
|
+
const beforeScrollLeft = scrollLeft;
|
|
456
|
+
scrollLeft = Math.max(0, scrollLeft + requestedDx);
|
|
457
|
+
const actualDx = scrollLeft - beforeScrollLeft;
|
|
458
|
+
|
|
459
|
+
// At the boundary, actual delta should be 0
|
|
460
|
+
assert.equal(actualDx, 0);
|
|
461
|
+
|
|
462
|
+
// Simulate a scroll container not at boundary
|
|
463
|
+
scrollLeft = 50;
|
|
464
|
+
const beforeScrollLeft2 = scrollLeft;
|
|
465
|
+
scrollLeft = Math.max(0, scrollLeft + requestedDx);
|
|
466
|
+
const actualDx2 = scrollLeft - beforeScrollLeft2;
|
|
467
|
+
|
|
468
|
+
// Should have scrolled the full amount
|
|
469
|
+
assert.equal(actualDx2, -10);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should reset scroll deltas after drag ends', () => {
|
|
473
|
+
let autoScrollDeltaX = 150;
|
|
474
|
+
let autoScrollDeltaY = 200;
|
|
475
|
+
|
|
476
|
+
// Simulate drag end reset
|
|
477
|
+
autoScrollDeltaX = 0;
|
|
478
|
+
autoScrollDeltaY = 0;
|
|
479
|
+
|
|
480
|
+
assert.equal(autoScrollDeltaX, 0);
|
|
481
|
+
assert.equal(autoScrollDeltaY, 0);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should handle simultaneous horizontal and vertical auto-scroll', () => {
|
|
485
|
+
// Simulate mouse in the bottom-right corner of the viewport
|
|
486
|
+
const edgeEnd = 800;
|
|
487
|
+
const mousePos = 780; // 20px from the right/bottom edge
|
|
488
|
+
|
|
489
|
+
const distFromEnd = edgeEnd - mousePos;
|
|
490
|
+
assert.isTrue(distFromEnd < AUTO_SCROLL_EDGE_ZONE);
|
|
491
|
+
|
|
492
|
+
const ratio = 1 - distFromEnd / AUTO_SCROLL_EDGE_ZONE;
|
|
493
|
+
const scrollSpeed = ratio * AUTO_SCROLL_MAX_SPEED;
|
|
494
|
+
|
|
495
|
+
// Both axes should get the same speed when equidistant from edges
|
|
496
|
+
assert.isAbove(scrollSpeed, 0);
|
|
497
|
+
|
|
498
|
+
// Apply to both axes
|
|
499
|
+
let autoScrollDeltaX = 0;
|
|
500
|
+
let autoScrollDeltaY = 0;
|
|
501
|
+
autoScrollDeltaX += scrollSpeed;
|
|
502
|
+
autoScrollDeltaY += scrollSpeed;
|
|
503
|
+
|
|
504
|
+
assert.equal(autoScrollDeltaX, autoScrollDeltaY);
|
|
505
|
+
assert.isAbove(autoScrollDeltaX, 0);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
337
508
|
});
|
|
@@ -120,6 +120,44 @@ describe('Plumber', () => {
|
|
|
120
120
|
expect((plumber as any).sources.size).to.equal(0);
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
describe('zoom property', () => {
|
|
125
|
+
it('defaults to 1.0', () => {
|
|
126
|
+
expect(plumber.zoom).to.equal(1.0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('is writable', () => {
|
|
130
|
+
plumber.zoom = 0.75;
|
|
131
|
+
expect(plumber.zoom).to.equal(0.75);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('toCanvas', () => {
|
|
136
|
+
it('returns identity at zoom 1.0', () => {
|
|
137
|
+
plumber.zoom = 1.0;
|
|
138
|
+
expect((plumber as any).toCanvas(100)).to.equal(100);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('divides by zoom at 0.5', () => {
|
|
142
|
+
plumber.zoom = 0.5;
|
|
143
|
+
expect((plumber as any).toCanvas(100)).to.equal(200);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('handles minimum zoom 0.1', () => {
|
|
147
|
+
plumber.zoom = 0.1;
|
|
148
|
+
expect((plumber as any).toCanvas(50)).to.equal(500);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('handles negative viewport diffs', () => {
|
|
152
|
+
plumber.zoom = 0.5;
|
|
153
|
+
expect((plumber as any).toCanvas(-100)).to.equal(-200);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('handles zero', () => {
|
|
157
|
+
plumber.zoom = 0.5;
|
|
158
|
+
expect((plumber as any).toCanvas(0)).to.equal(0);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
123
161
|
});
|
|
124
162
|
|
|
125
163
|
describe('calculateFlowchartPath', () => {
|