@nyaruka/temba-components 0.127.0 → 0.129.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 (109) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/demo/chart/horizontal-demo.html +81 -0
  3. package/demo/components/datepicker/example.html +63 -0
  4. package/demo/components/datepicker/range-picker-demo.html +161 -0
  5. package/demo/data/flows/sample-flow.json +127 -100
  6. package/demo/index.html +8 -0
  7. package/demo/static/css/prism.css +2 -0
  8. package/demo/static/js/prism-loader.js +12 -0
  9. package/demo/sticky-note-demo.html +152 -0
  10. package/demo/styles.css +71 -1
  11. package/dist/locales/es.js +5 -5
  12. package/dist/locales/es.js.map +1 -1
  13. package/dist/locales/fr.js +5 -5
  14. package/dist/locales/fr.js.map +1 -1
  15. package/dist/locales/locale-codes.js +11 -2
  16. package/dist/locales/locale-codes.js.map +1 -1
  17. package/dist/locales/pt.js +5 -5
  18. package/dist/locales/pt.js.map +1 -1
  19. package/dist/temba-components.js +509 -87
  20. package/dist/temba-components.js.map +1 -1
  21. package/out-tsc/src/chart/TembaChart.js +136 -62
  22. package/out-tsc/src/chart/TembaChart.js.map +1 -1
  23. package/out-tsc/src/datepicker/DatePicker.js +11 -1
  24. package/out-tsc/src/datepicker/DatePicker.js.map +1 -1
  25. package/out-tsc/src/datepicker/RangePicker.js +595 -0
  26. package/out-tsc/src/datepicker/RangePicker.js.map +1 -0
  27. package/out-tsc/src/flow/Editor.js +210 -1
  28. package/out-tsc/src/flow/Editor.js.map +1 -1
  29. package/out-tsc/src/flow/EditorNode.js +98 -139
  30. package/out-tsc/src/flow/EditorNode.js.map +1 -1
  31. package/out-tsc/src/flow/StickyNote.js +272 -0
  32. package/out-tsc/src/flow/StickyNote.js.map +1 -0
  33. package/out-tsc/src/interfaces.js +1 -0
  34. package/out-tsc/src/interfaces.js.map +1 -1
  35. package/out-tsc/src/list/RunList.js +2 -1
  36. package/out-tsc/src/list/RunList.js.map +1 -1
  37. package/out-tsc/src/list/SortableList.js +9 -0
  38. package/out-tsc/src/list/SortableList.js.map +1 -1
  39. package/out-tsc/src/locales/es.js +5 -5
  40. package/out-tsc/src/locales/es.js.map +1 -1
  41. package/out-tsc/src/locales/fr.js +5 -5
  42. package/out-tsc/src/locales/fr.js.map +1 -1
  43. package/out-tsc/src/locales/locale-codes.js +11 -2
  44. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  45. package/out-tsc/src/locales/pt.js +5 -5
  46. package/out-tsc/src/locales/pt.js.map +1 -1
  47. package/out-tsc/src/store/AppState.js +33 -0
  48. package/out-tsc/src/store/AppState.js.map +1 -1
  49. package/out-tsc/src/vectoricon/index.js +2 -1
  50. package/out-tsc/src/vectoricon/index.js.map +1 -1
  51. package/out-tsc/temba-modules.js +5 -1
  52. package/out-tsc/temba-modules.js.map +1 -1
  53. package/out-tsc/test/temba-chart.test.js +36 -0
  54. package/out-tsc/test/temba-chart.test.js.map +1 -1
  55. package/out-tsc/test/temba-datepicker.test.js +1 -1
  56. package/out-tsc/test/temba-datepicker.test.js.map +1 -1
  57. package/out-tsc/test/temba-flow-editor-node.test.js +249 -5
  58. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  59. package/out-tsc/test/temba-range-picker.test.js +123 -0
  60. package/out-tsc/test/temba-range-picker.test.js.map +1 -0
  61. package/out-tsc/test/temba-select.test.js +10 -16
  62. package/out-tsc/test/temba-select.test.js.map +1 -1
  63. package/out-tsc/test/temba-webchat.test.js +4 -0
  64. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  65. package/out-tsc/test/utils.test.js +62 -0
  66. package/out-tsc/test/utils.test.js.map +1 -1
  67. package/package.json +1 -1
  68. package/screenshots/truth/datepicker/range-picker-all.png +0 -0
  69. package/screenshots/truth/datepicker/range-picker-button-states.png +0 -0
  70. package/screenshots/truth/datepicker/range-picker-default.png +0 -0
  71. package/screenshots/truth/datepicker/range-picker-editing-start.png +0 -0
  72. package/screenshots/truth/datepicker/range-picker-initial-values.png +0 -0
  73. package/screenshots/truth/datepicker/range-picker-min-max.png +0 -0
  74. package/screenshots/truth/datepicker/range-picker-week.png +0 -0
  75. package/screenshots/truth/datepicker/range-picker-year.png +0 -0
  76. package/screenshots/truth/sticky-note/blue.png +0 -0
  77. package/screenshots/truth/sticky-note/gray.png +0 -0
  78. package/screenshots/truth/sticky-note/green.png +0 -0
  79. package/screenshots/truth/sticky-note/pink.png +0 -0
  80. package/screenshots/truth/sticky-note/yellow.png +0 -0
  81. package/screenshots/truth/webchat/connected-state.png +0 -0
  82. package/src/chart/TembaChart.ts +144 -66
  83. package/src/datepicker/DatePicker.ts +9 -1
  84. package/src/datepicker/RangePicker.ts +602 -0
  85. package/src/flow/Editor.ts +252 -2
  86. package/src/flow/EditorNode.ts +98 -156
  87. package/src/flow/StickyNote.ts +284 -0
  88. package/src/interfaces.ts +2 -1
  89. package/src/list/RunList.ts +2 -1
  90. package/src/list/SortableList.ts +11 -0
  91. package/src/locales/es.ts +18 -13
  92. package/src/locales/fr.ts +18 -13
  93. package/src/locales/locale-codes.ts +11 -2
  94. package/src/locales/pt.ts +18 -13
  95. package/src/store/AppState.ts +51 -1
  96. package/src/store/flow-definition.d.ts +8 -0
  97. package/src/vectoricon/index.ts +2 -1
  98. package/static/svg/index.pdf +137 -0
  99. package/temba-modules.ts +5 -1
  100. package/test/temba-chart.test.ts +47 -0
  101. package/test/temba-datepicker.test.ts +1 -1
  102. package/test/temba-flow-editor-node.test.ts +322 -6
  103. package/test/temba-range-picker.test.ts +193 -0
  104. package/test/temba-select.test.ts +11 -19
  105. package/test/temba-webchat.test.ts +7 -0
  106. package/test/utils.test.ts +98 -0
  107. package/web-dev-server.config.mjs +30 -22
  108. package/web-test-runner.config.mjs +2 -0
  109. package/demo/datepicker/example.html +0 -69
@@ -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 = [
@@ -649,7 +649,6 @@ describe('temba-select', () => {
649
649
  // assert.equal(select.visibleOptions.length, 2);
650
650
  });
651
651
 
652
- // this test is flaky on CI, skip it for now
653
652
  xit('pages through cursor results', async () => {
654
653
  const select = await createSelect(
655
654
  clock,
@@ -662,27 +661,15 @@ describe('temba-select', () => {
662
661
 
663
662
  await openSelect(clock, select);
664
663
 
665
- // Wait for pagination to complete - keep checking until fetching is false
666
- // and we have the expected number of results (15 = 3 pages * 5 items)
667
- let attempts = 0;
668
- const maxAttempts = 10;
669
- while (select.fetching || select.visibleOptions.length < 15) {
670
- if (attempts >= maxAttempts) {
671
- throw new Error(
672
- `Pagination did not complete after ${maxAttempts} attempts. fetching: ${select.fetching}, visibleOptions: ${select.visibleOptions.length}`
673
- );
674
- }
675
- await select.updateComplete;
676
- clock.runAll();
677
- attempts++;
678
- await delay(100);
679
- }
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);
680
667
 
681
668
  // should have all three pages visible right away
682
669
  assert.equal(select.visibleOptions.length, 15);
683
670
  });
684
671
 
685
- xit('shows cached results', async () => {
672
+ it('shows cached results', async () => {
686
673
  const select = await createSelect(
687
674
  clock,
688
675
  getSelectHTML([], {
@@ -695,13 +682,18 @@ describe('temba-select', () => {
695
682
 
696
683
  // wait for updates from fetching three pages
697
684
  await openSelect(clock, select);
685
+ await waitForSelectPagination(select, clock, 15, 50);
698
686
  assert.equal(select.visibleOptions.length, 15);
699
687
 
700
688
  // close and reopen
701
689
  select.blur();
702
690
  await clock.tick(250);
691
+ // Ensure the select is properly closed before reopening
692
+ await select.updateComplete;
703
693
 
704
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);
705
697
  assert.equal(select.visibleOptions.length, 15);
706
698
 
707
699
  // close and reopen once more (previous bug failed on third opening)
@@ -210,6 +210,13 @@ describe('temba-webchat', () => {
210
210
  status: 'connected'
211
211
  });
212
212
 
213
+ // the cursor is blinking, we need to account for it in our screenshot by making it transparent
214
+ const inputField = webChat.shadowRoot.querySelector(
215
+ '.input'
216
+ ) as HTMLInputElement;
217
+ expect(inputField).to.exist;
218
+ inputField.style.caretColor = 'transparent';
219
+
213
220
  await assertScreenshot('webchat/connected-state', getClip(webChat));
214
221
  });
215
222
  });
@@ -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
 
@@ -152,6 +152,8 @@ const wireScreenshots = async (page, context, wait, replaceScreenshots) => {
152
152
 
153
153
  // Only wait for network idle if explicitly requested
154
154
  if (wait) {
155
+ await page.waitForNetworkIdle();
156
+ } else {
155
157
  await page.waitForNetworkIdle({ idleTime: 100, timeout: 1000 });
156
158
  }
157
159
 
@@ -1,69 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en-GB">
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Date Picker Examples</title>
6
- <link
7
- href="/static/css/temba-components.css"
8
- rel="stylesheet"
9
- type="text/css"
10
- />
11
- <link
12
- href="https://fonts.googleapis.com/css?family=Roboto+Mono:300|Roboto:300,400,500"
13
- rel="stylesheet"
14
- />
15
- <link
16
- href="../styles.css"
17
- rel="stylesheet"
18
- type="text/css"
19
- />
20
- </head>
21
- <body>
22
- <h1>Date Picker Examples</h1>
23
- <p><a href="../index.html">← Back to main demo</a></p>
24
-
25
- <div class="example">
26
- <h3>Date and Time Picker</h3>
27
- <p>A date picker that includes time selection</p>
28
- <temba-datepicker
29
- time
30
- value="2020-01-20T14:00+00:00"
31
- timezone="UTC"
32
- ></temba-datepicker>
33
- </div>
34
-
35
- <div class="example">
36
- <h3>Date Only Picker</h3>
37
- <p>A date picker without time selection</p>
38
- <temba-datepicker
39
- value="2022-10-09"
40
- ></temba-datepicker>
41
- </div>
42
-
43
- <div class="example">
44
- <h3>Date Picker with Timezone</h3>
45
- <p>A date picker with a specific timezone</p>
46
- <temba-datepicker
47
- time
48
- value="2022-10-02 01:00:00+00:00"
49
- timezone="Africa/Lagos"
50
- ></temba-datepicker>
51
- </div>
52
-
53
- <div class="example">
54
- <h3>Empty Date Picker</h3>
55
- <p>A date picker with no initial value</p>
56
- <temba-datepicker></temba-datepicker>
57
- </div>
58
-
59
- <script>
60
- function handleDateChange(event) {
61
- console.log('Date changed:', event.target.value);
62
- }
63
- </script>
64
-
65
- <script type="module">
66
- import '../../out-tsc/temba-modules.js';
67
- </script>
68
- </body>
69
- </html>