@nyaruka/temba-components 0.126.0 → 0.128.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/demo/chart/example.html +18 -1
  3. package/demo/data/flows/sample-flow.json +127 -100
  4. package/demo/data/server/opened-tickets-long.json +53 -0
  5. package/demo/sticky-note-demo.html +152 -0
  6. package/dist/locales/es.js +5 -5
  7. package/dist/locales/es.js.map +1 -1
  8. package/dist/locales/fr.js +5 -5
  9. package/dist/locales/fr.js.map +1 -1
  10. package/dist/locales/locale-codes.js +11 -2
  11. package/dist/locales/locale-codes.js.map +1 -1
  12. package/dist/locales/pt.js +5 -5
  13. package/dist/locales/pt.js.map +1 -1
  14. package/dist/temba-components.js +346 -86
  15. package/dist/temba-components.js.map +1 -1
  16. package/out-tsc/src/chart/TembaChart.js +44 -5
  17. package/out-tsc/src/chart/TembaChart.js.map +1 -1
  18. package/out-tsc/src/flow/Editor.js +210 -1
  19. package/out-tsc/src/flow/Editor.js.map +1 -1
  20. package/out-tsc/src/flow/EditorNode.js +98 -142
  21. package/out-tsc/src/flow/EditorNode.js.map +1 -1
  22. package/out-tsc/src/flow/StickyNote.js +272 -0
  23. package/out-tsc/src/flow/StickyNote.js.map +1 -0
  24. package/out-tsc/src/list/RunList.js +2 -1
  25. package/out-tsc/src/list/RunList.js.map +1 -1
  26. package/out-tsc/src/list/SortableList.js +9 -0
  27. package/out-tsc/src/list/SortableList.js.map +1 -1
  28. package/out-tsc/src/locales/es.js +5 -5
  29. package/out-tsc/src/locales/es.js.map +1 -1
  30. package/out-tsc/src/locales/fr.js +5 -5
  31. package/out-tsc/src/locales/fr.js.map +1 -1
  32. package/out-tsc/src/locales/locale-codes.js +11 -2
  33. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  34. package/out-tsc/src/locales/pt.js +5 -5
  35. package/out-tsc/src/locales/pt.js.map +1 -1
  36. package/out-tsc/src/store/AppState.js +33 -0
  37. package/out-tsc/src/store/AppState.js.map +1 -1
  38. package/out-tsc/src/vectoricon/index.js +2 -1
  39. package/out-tsc/src/vectoricon/index.js.map +1 -1
  40. package/out-tsc/temba-modules.js +2 -0
  41. package/out-tsc/temba-modules.js.map +1 -1
  42. package/out-tsc/test/temba-flow-editor-node.test.js +249 -5
  43. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  44. package/out-tsc/test/temba-select.test.js +9 -14
  45. package/out-tsc/test/temba-select.test.js.map +1 -1
  46. package/out-tsc/test/utils.test.js +62 -0
  47. package/out-tsc/test/utils.test.js.map +1 -1
  48. package/package.json +1 -1
  49. package/screenshots/truth/sticky-note/blue.png +0 -0
  50. package/screenshots/truth/sticky-note/gray.png +0 -0
  51. package/screenshots/truth/sticky-note/green.png +0 -0
  52. package/screenshots/truth/sticky-note/pink.png +0 -0
  53. package/screenshots/truth/sticky-note/yellow.png +0 -0
  54. package/src/chart/TembaChart.ts +47 -5
  55. package/src/flow/Editor.ts +252 -2
  56. package/src/flow/EditorNode.ts +98 -160
  57. package/src/flow/StickyNote.ts +284 -0
  58. package/src/list/RunList.ts +2 -1
  59. package/src/list/SortableList.ts +11 -0
  60. package/src/locales/es.ts +18 -13
  61. package/src/locales/fr.ts +18 -13
  62. package/src/locales/locale-codes.ts +11 -2
  63. package/src/locales/pt.ts +18 -13
  64. package/src/store/AppState.ts +51 -1
  65. package/src/store/flow-definition.d.ts +8 -0
  66. package/src/vectoricon/index.ts +2 -1
  67. package/static/svg/index.pdf +137 -0
  68. package/temba-modules.ts +2 -0
  69. package/test/temba-flow-editor-node.test.ts +322 -6
  70. package/test/temba-select.test.ts +10 -17
  71. package/test/utils.test.ts +98 -0
  72. package/web-dev-server.config.mjs +30 -22
@@ -1,3 +1,4 @@
1
+ import '../temba-modules';
1
2
  import { html, fixture, expect } from '@open-wc/testing';
2
3
  import { EditorNode } from '../src/flow/EditorNode';
3
4
  import {
@@ -8,9 +9,7 @@ import {
8
9
  Router
9
10
  } from '../src/store/flow-definition.d';
10
11
  import { stub, restore } from 'sinon';
11
-
12
- // Register the component
13
- customElements.define('temba-editor-node', EditorNode);
12
+ import { CustomEventType } from '../src/interfaces';
14
13
 
15
14
  describe('EditorNode', () => {
16
15
  let editorNode: EditorNode;
@@ -55,7 +54,7 @@ describe('EditorNode', () => {
55
54
  quick_replies: []
56
55
  };
57
56
 
58
- const result = (editorNode as any).renderAction(mockNode, action);
57
+ const result = (editorNode as any).renderAction(mockNode, action, 0);
59
58
  expect(result).to.exist;
60
59
  });
61
60
 
@@ -71,7 +70,7 @@ describe('EditorNode', () => {
71
70
  uuid: 'action-1'
72
71
  };
73
72
 
74
- const result = (editorNode as any).renderAction(mockNode, action);
73
+ const result = (editorNode as any).renderAction(mockNode, action, 1);
75
74
  expect(result).to.exist;
76
75
  });
77
76
  });
@@ -327,7 +326,8 @@ describe('EditorNode', () => {
327
326
  // Test renderAction
328
327
  const actionResult = (editorNode as any).renderAction(
329
328
  mockNode,
330
- mockNode.actions[0]
329
+ mockNode.actions[0],
330
+ 0
331
331
  );
332
332
  expect(actionResult).to.exist;
333
333
 
@@ -341,4 +341,320 @@ describe('EditorNode', () => {
341
341
  expect(mockNode.exits).to.have.length(1);
342
342
  });
343
343
  });
344
+
345
+ describe('drag and drop functionality', () => {
346
+ let editorNode: EditorNode;
347
+
348
+ beforeEach(() => {
349
+ editorNode = new EditorNode();
350
+ });
351
+
352
+ it('renders actions with sortable class and proper IDs', async () => {
353
+ const mockNode: Node = {
354
+ uuid: 'sortable-test-node',
355
+ actions: [
356
+ {
357
+ type: 'send_msg',
358
+ uuid: 'action-1',
359
+ text: 'Hello',
360
+ quick_replies: []
361
+ } as any,
362
+ {
363
+ type: 'send_msg',
364
+ uuid: 'action-2',
365
+ text: 'World',
366
+ quick_replies: []
367
+ } as any
368
+ ],
369
+ exits: []
370
+ };
371
+
372
+ // Test that renderAction includes sortable class and proper ID
373
+ const result1 = (editorNode as any).renderAction(
374
+ mockNode,
375
+ mockNode.actions[0],
376
+ 0
377
+ );
378
+ const result2 = (editorNode as any).renderAction(
379
+ mockNode,
380
+ mockNode.actions[1],
381
+ 1
382
+ );
383
+
384
+ expect(result1).to.exist;
385
+ expect(result2).to.exist;
386
+
387
+ // Render the template to check the actual DOM
388
+ const container1 = await fixture(html`<div>${result1}</div>`);
389
+ const container2 = await fixture(html`<div>${result2}</div>`);
390
+
391
+ const actionElement1 = container1.querySelector('.action');
392
+ const actionElement2 = container2.querySelector('.action');
393
+
394
+ expect(actionElement1).to.exist;
395
+ expect(actionElement1?.classList.contains('sortable')).to.be.true;
396
+ expect(actionElement1?.id).to.equal('action-0');
397
+
398
+ expect(actionElement2).to.exist;
399
+ expect(actionElement2?.classList.contains('sortable')).to.be.true;
400
+ expect(actionElement2?.id).to.equal('action-1');
401
+ });
402
+
403
+ it('includes drag handle in rendered actions', async () => {
404
+ const mockNode: Node = {
405
+ uuid: 'drag-handle-test',
406
+ actions: [
407
+ {
408
+ type: 'send_msg',
409
+ uuid: 'action-1',
410
+ text: 'Hello',
411
+ quick_replies: []
412
+ } as any
413
+ ],
414
+ exits: []
415
+ };
416
+
417
+ let editorNode: EditorNode = await fixture(
418
+ html`<temba-flow-node
419
+ .node=${mockNode}
420
+ .ui=${{ position: { left: 0, top: 0 } }}
421
+ ></temba-flow-node>`
422
+ );
423
+
424
+ // No drag handle should be present if only one action
425
+ let dragHandle = editorNode.querySelector('.drag-handle');
426
+ expect(dragHandle).to.not.exist;
427
+
428
+ // Now add a second action to verify drag handle appears
429
+ mockNode.actions.push({
430
+ type: 'send_msg',
431
+ uuid: 'action-2',
432
+ text: 'World',
433
+ quick_replies: []
434
+ } as any);
435
+
436
+ editorNode = await fixture(
437
+ html`<temba-flow-node
438
+ .node=${mockNode}
439
+ .ui=${{ position: { left: 0, top: 0 } }}
440
+ ></temba-flow-node>`
441
+ );
442
+
443
+ dragHandle = editorNode.querySelector('.drag-handle');
444
+ expect(dragHandle).to.exist;
445
+ });
446
+
447
+ it('renders SortableList when actions are present', async () => {
448
+ const mockNode: Node = {
449
+ uuid: 'sortable-list-test',
450
+ actions: [
451
+ {
452
+ type: 'send_msg',
453
+ uuid: 'action-1',
454
+ text: 'Hello',
455
+ quick_replies: []
456
+ } as any,
457
+ {
458
+ type: 'send_msg',
459
+ uuid: 'action-2',
460
+ text: 'World',
461
+ quick_replies: []
462
+ } as any
463
+ ],
464
+ exits: []
465
+ };
466
+
467
+ const mockUI: NodeUI = {
468
+ position: { left: 100, top: 200 },
469
+ type: 'execute_actions'
470
+ };
471
+
472
+ // Set properties on the component
473
+ (editorNode as any).node = mockNode;
474
+ (editorNode as any).ui = mockUI;
475
+
476
+ const renderResult = editorNode.render();
477
+
478
+ // Render the template to check the actual DOM
479
+ const container = await fixture(html`<div>${renderResult}</div>`);
480
+
481
+ const sortableList = container.querySelector('temba-sortable-list');
482
+ expect(sortableList).to.exist;
483
+ });
484
+
485
+ it('does not render SortableList when no actions', async () => {
486
+ const mockNode: Node = {
487
+ uuid: 'no-actions-test',
488
+ actions: [],
489
+ exits: [{ uuid: 'exit-1' }]
490
+ };
491
+
492
+ const mockUI: NodeUI = {
493
+ position: { left: 100, top: 200 },
494
+ type: 'execute_actions'
495
+ };
496
+
497
+ // Set properties on the component
498
+ (editorNode as any).node = mockNode;
499
+ (editorNode as any).ui = mockUI;
500
+
501
+ const renderResult = editorNode.render();
502
+
503
+ // Check that template does not include temba-sortable-list
504
+ expect(renderResult.strings.join('')).to.not.contain(
505
+ 'temba-sortable-list'
506
+ );
507
+ });
508
+
509
+ it('handles order changed events correctly', async () => {
510
+ const mockNode: Node = {
511
+ uuid: 'order-test',
512
+ actions: [
513
+ {
514
+ type: 'send_msg',
515
+ uuid: 'action-1',
516
+ text: 'First',
517
+ quick_replies: []
518
+ } as any,
519
+ {
520
+ type: 'send_msg',
521
+ uuid: 'action-2',
522
+ text: 'Second',
523
+ quick_replies: []
524
+ } as any,
525
+ {
526
+ type: 'send_msg',
527
+ uuid: 'action-3',
528
+ text: 'Third',
529
+ quick_replies: []
530
+ } as any
531
+ ],
532
+ exits: []
533
+ };
534
+
535
+ (editorNode as any).node = mockNode;
536
+
537
+ // Create a mock order changed event (swap first and last actions)
538
+ const orderChangedEvent = new CustomEvent(CustomEventType.OrderChanged, {
539
+ detail: { swap: [0, 2] }
540
+ });
541
+
542
+ // Call the handler directly
543
+ (editorNode as any).handleActionOrderChanged(orderChangedEvent);
544
+
545
+ // Verify the actions were reordered correctly
546
+ expect((editorNode as any).node.actions).to.have.length(3);
547
+ expect(((editorNode as any).node.actions[0] as any).text).to.equal(
548
+ 'Second'
549
+ );
550
+ expect(((editorNode as any).node.actions[1] as any).text).to.equal(
551
+ 'Third'
552
+ );
553
+ expect(((editorNode as any).node.actions[2] as any).text).to.equal(
554
+ 'First'
555
+ );
556
+ });
557
+
558
+ it('preserves action data during reordering', () => {
559
+ const mockNode: Node = {
560
+ uuid: 'preserve-test',
561
+ actions: [
562
+ {
563
+ type: 'send_msg',
564
+ uuid: 'action-1',
565
+ text: 'Message 1',
566
+ quick_replies: ['Yes', 'No']
567
+ } as any,
568
+ {
569
+ type: 'send_msg',
570
+ uuid: 'action-2',
571
+ text: 'Message 2',
572
+ quick_replies: []
573
+ } as any
574
+ ],
575
+ exits: []
576
+ };
577
+
578
+ (editorNode as any).node = mockNode;
579
+
580
+ // Swap the two actions
581
+ const orderChangedEvent = new CustomEvent(CustomEventType.OrderChanged, {
582
+ detail: { swap: [0, 1] }
583
+ });
584
+
585
+ (editorNode as any).handleActionOrderChanged(orderChangedEvent);
586
+
587
+ // Verify all action data is preserved
588
+ expect((editorNode as any).node.actions).to.have.length(2);
589
+ expect(((editorNode as any).node.actions[0] as any).text).to.equal(
590
+ 'Message 2'
591
+ );
592
+ expect(
593
+ ((editorNode as any).node.actions[0] as any).quick_replies
594
+ ).to.deep.equal([]);
595
+ expect(((editorNode as any).node.actions[1] as any).text).to.equal(
596
+ 'Message 1'
597
+ );
598
+ expect(
599
+ ((editorNode as any).node.actions[1] as any).quick_replies
600
+ ).to.deep.equal(['Yes', 'No']);
601
+ });
602
+
603
+ it('integrates with SortableList for full drag functionality', async () => {
604
+ const mockNode: Node = {
605
+ uuid: 'integration-drag-test',
606
+ actions: [
607
+ {
608
+ type: 'send_msg',
609
+ uuid: 'action-1',
610
+ text: 'First Action',
611
+ quick_replies: []
612
+ } as any,
613
+ {
614
+ type: 'send_msg',
615
+ uuid: 'action-2',
616
+ text: 'Second Action',
617
+ quick_replies: []
618
+ } as any,
619
+ {
620
+ type: 'send_msg',
621
+ uuid: 'action-3',
622
+ text: 'Third Action',
623
+ quick_replies: []
624
+ } as any
625
+ ],
626
+ exits: []
627
+ };
628
+
629
+ const mockUI: NodeUI = {
630
+ position: { left: 100, top: 200 },
631
+ type: 'execute_actions'
632
+ };
633
+
634
+ // Set properties on the component
635
+ (editorNode as any).node = mockNode;
636
+ (editorNode as any).ui = mockUI;
637
+
638
+ // Render the full component
639
+ const renderResult = editorNode.render();
640
+ const container = await fixture(html`<div>${renderResult}</div>`);
641
+
642
+ // Find the sortable list
643
+ const sortableList = container.querySelector('temba-sortable-list');
644
+ expect(sortableList).to.exist;
645
+
646
+ // Verify all actions are rendered as sortable items
647
+ const sortableItems = container.querySelectorAll('.sortable');
648
+ expect(sortableItems).to.have.length(3);
649
+
650
+ // Verify each action has correct ID and structure
651
+ expect(sortableItems[0].id).to.equal('action-0');
652
+ expect(sortableItems[1].id).to.equal('action-1');
653
+ expect(sortableItems[2].id).to.equal('action-2');
654
+
655
+ // Verify drag handles are present
656
+ const dragHandles = container.querySelectorAll('.drag-handle');
657
+ expect(dragHandles).to.have.length(3);
658
+ });
659
+ });
344
660
  });
@@ -5,12 +5,12 @@ import { Options } from '../src/options/Options';
5
5
  import { Select, SelectOption } from '../src/select/Select';
6
6
  import {
7
7
  assertScreenshot,
8
- delay,
9
8
  getClip,
10
9
  getOptions,
11
10
  loadStore,
12
11
  openAndClick,
13
- openSelect
12
+ openSelect,
13
+ waitForSelectPagination
14
14
  } from './utils.test';
15
15
 
16
16
  const colors = [
@@ -661,21 +661,9 @@ describe('temba-select', () => {
661
661
 
662
662
  await openSelect(clock, select);
663
663
 
664
- // Wait for pagination to complete - keep checking until fetching is false
665
- // and we have the expected number of results (15 = 3 pages * 5 items)
666
- let attempts = 0;
667
- const maxAttempts = 10;
668
- while (select.fetching || select.visibleOptions.length < 15) {
669
- if (attempts >= maxAttempts) {
670
- throw new Error(
671
- `Pagination did not complete after ${maxAttempts} attempts. fetching: ${select.fetching}, visibleOptions: ${select.visibleOptions.length}`
672
- );
673
- }
674
- await select.updateComplete;
675
- clock.runAll();
676
- attempts++;
677
- await delay(100);
678
- }
664
+ // Wait for pagination to complete using our improved helper
665
+ // Use more attempts for this test since pagination can be slow in CI
666
+ await waitForSelectPagination(select, clock, 15, 50);
679
667
 
680
668
  // should have all three pages visible right away
681
669
  assert.equal(select.visibleOptions.length, 15);
@@ -694,13 +682,18 @@ describe('temba-select', () => {
694
682
 
695
683
  // wait for updates from fetching three pages
696
684
  await openSelect(clock, select);
685
+ await waitForSelectPagination(select, clock, 15, 50);
697
686
  assert.equal(select.visibleOptions.length, 15);
698
687
 
699
688
  // close and reopen
700
689
  select.blur();
701
690
  await clock.tick(250);
691
+ // Ensure the select is properly closed before reopening
692
+ await select.updateComplete;
702
693
 
703
694
  await openSelect(clock, select);
695
+ // Cached results should be available immediately, but give some time for rendering
696
+ await waitForSelectPagination(select, clock, 15, 30);
704
697
  assert.equal(select.visibleOptions.length, 15);
705
698
 
706
699
  // close and reopen once more (previous bug failed on third opening)
@@ -180,6 +180,26 @@ export const delay = (millis: number) => {
180
180
  });
181
181
  };
182
182
 
183
+ // Enhanced wait utility for more robust testing
184
+ export const waitForCondition = async (
185
+ predicate: () => boolean,
186
+ maxAttempts: number = 20,
187
+ delayMs: number = 50
188
+ ): Promise<void> => {
189
+ let attempts = 0;
190
+ while (!predicate() && attempts < maxAttempts) {
191
+ await delay(delayMs);
192
+ attempts++;
193
+ }
194
+ if (!predicate()) {
195
+ throw new Error(
196
+ `Condition not met after ${maxAttempts} attempts (${
197
+ maxAttempts * delayMs
198
+ }ms)`
199
+ );
200
+ }
201
+ };
202
+
183
203
  export const assertScreenshot = async (
184
204
  filename: string,
185
205
  clip: Clip,
@@ -294,6 +314,31 @@ export const clickOption = async (
294
314
  index: number
295
315
  ) => {
296
316
  const options = getOptions(select);
317
+ if (!options) {
318
+ throw new Error('No options element found');
319
+ }
320
+
321
+ // Wait for the specific option to be available, but only if it's not already there
322
+ const existingOption = options.shadowRoot?.querySelector(
323
+ `[data-option-index="${index}"]`
324
+ );
325
+ if (!existingOption) {
326
+ try {
327
+ await waitForCondition(
328
+ () => {
329
+ const option = options.shadowRoot?.querySelector(
330
+ `[data-option-index="${index}"]`
331
+ );
332
+ return !!option;
333
+ },
334
+ 10,
335
+ 25
336
+ );
337
+ } catch (e) {
338
+ throw new Error(`Option at index ${index} not found after waiting`);
339
+ }
340
+ }
341
+
297
342
  const option = options.shadowRoot.querySelector(
298
343
  `[data-option-index="${index}"]`
299
344
  ) as HTMLDivElement;
@@ -318,6 +363,27 @@ export const openSelect = async (clock: any, select: Select<SelectOption>) => {
318
363
  // reduce wait time for options to become visible
319
364
  await waitFor(25);
320
365
  clock.runAll();
366
+
367
+ // For non-endpoint selects, options might be immediately available
368
+ // For endpoint selects, we need to wait for them to load
369
+ const hasEndpoint = select.getAttribute('endpoint');
370
+ if (hasEndpoint) {
371
+ try {
372
+ // Wait for options to be properly rendered and visible (but only for endpoint selects)
373
+ await waitForCondition(
374
+ () => {
375
+ const options = select.shadowRoot.querySelector(
376
+ 'temba-options[visible]'
377
+ );
378
+ return options && options.isConnected;
379
+ },
380
+ 10,
381
+ 25
382
+ );
383
+ } catch (e) {
384
+ // If condition fails, continue - some tests might not need options to be visible immediately
385
+ }
386
+ }
321
387
  };
322
388
 
323
389
  export const openAndClick = async (
@@ -367,3 +433,35 @@ export const updateComponent = async (
367
433
  export const getValidText = () => {
368
434
  return 'sà-wàd-dee!';
369
435
  };
436
+
437
+ // Helper for waiting for select pagination to complete
438
+ export const waitForSelectPagination = async (
439
+ select: Select<SelectOption>,
440
+ clock: any,
441
+ expectedCount: number,
442
+ maxAttempts: number = 30
443
+ ): Promise<void> => {
444
+ let attempts = 0;
445
+ while (attempts < maxAttempts) {
446
+ // Ensure we're not still fetching
447
+ if (!select.fetching && select.visibleOptions.length >= expectedCount) {
448
+ return;
449
+ }
450
+
451
+ await select.updateComplete;
452
+ clock.runAll();
453
+
454
+ // Give more time between attempts for slow CI environments
455
+ await delay(75);
456
+
457
+ attempts++;
458
+ }
459
+
460
+ throw new Error(
461
+ `Pagination did not complete after ${maxAttempts} attempts (${
462
+ maxAttempts * 75
463
+ }ms). ` +
464
+ `Expected ${expectedCount} options, got ${select.visibleOptions.length}. ` +
465
+ `Fetching: ${select.fetching}`
466
+ );
467
+ };
@@ -15,30 +15,38 @@ export default {
15
15
  {
16
16
  name: 'flow-files',
17
17
  serve(context) {
18
- if (context.request.method === 'POST') {
19
- let body = '';
20
- context.req.on('data', chunk => {
21
- body += chunk.toString();
22
- });
23
-
24
- context.req.on('end', () => {
18
+ if (context.request.method === 'POST' && context.path.startsWith('/flow/revisions/')) {
19
+ return new Promise((resolve) => {
20
+ let body = '';
25
21
  const parts = context.path.split('/');
26
22
  const uuid = parts[3];
27
-
28
- // read in the body
29
- context.contentType = 'application/json';
30
- if (body) {
31
- fs.writeFileSync(
32
- path.resolve(`./demo/data/flows/${uuid}.json`), JSON.stringify({ definition: JSON.parse(body) }, null, 2)
33
- );
34
-
35
- context.body = {
36
- status: 'success',
37
- message: `Flow ${uuid} saved successfully.`,
38
- };
39
- } else {
40
- console.log(`No body received for flow ${uuid}.`);
41
- }
23
+ context.req.on('data', chunk => {
24
+ body += chunk.toString();
25
+ });
26
+ context.req.on('end', () => {
27
+ context.contentType = 'application/json';
28
+ if (body) {
29
+ fs.writeFileSync(
30
+ path.resolve(`./demo/data/flows/${uuid}.json`),
31
+ JSON.stringify({ definition: JSON.parse(body) }, null, 2)
32
+ );
33
+ console.log(`Flow ${uuid} saved successfully.`);
34
+ context.body = {
35
+ status: 'success',
36
+ message: `Flow ${uuid} saved successfully.`,
37
+ definition: JSON.parse(body),
38
+ };
39
+ context.status = 200;
40
+ } else {
41
+ console.log(`No body received for flow ${uuid}.`);
42
+ context.body = {
43
+ status: 'error',
44
+ message: `No body received for flow ${uuid}.`,
45
+ };
46
+ context.status = 400;
47
+ }
48
+ resolve();
49
+ });
42
50
  });
43
51
  }
44
52