@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.
- package/CHANGELOG.md +25 -0
- package/demo/chart/horizontal-demo.html +81 -0
- package/demo/components/datepicker/example.html +63 -0
- package/demo/components/datepicker/range-picker-demo.html +161 -0
- package/demo/data/flows/sample-flow.json +127 -100
- package/demo/index.html +8 -0
- package/demo/static/css/prism.css +2 -0
- package/demo/static/js/prism-loader.js +12 -0
- package/demo/sticky-note-demo.html +152 -0
- package/demo/styles.css +71 -1
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +11 -2
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +509 -87
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +136 -62
- package/out-tsc/src/chart/TembaChart.js.map +1 -1
- package/out-tsc/src/datepicker/DatePicker.js +11 -1
- package/out-tsc/src/datepicker/DatePicker.js.map +1 -1
- package/out-tsc/src/datepicker/RangePicker.js +595 -0
- package/out-tsc/src/datepicker/RangePicker.js.map +1 -0
- package/out-tsc/src/flow/Editor.js +210 -1
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/EditorNode.js +98 -139
- package/out-tsc/src/flow/EditorNode.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +272 -0
- package/out-tsc/src/flow/StickyNote.js.map +1 -0
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/list/RunList.js +2 -1
- package/out-tsc/src/list/RunList.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +9 -0
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +11 -2
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/store/AppState.js +33 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/vectoricon/index.js +2 -1
- package/out-tsc/src/vectoricon/index.js.map +1 -1
- package/out-tsc/temba-modules.js +5 -1
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-chart.test.js +36 -0
- package/out-tsc/test/temba-chart.test.js.map +1 -1
- package/out-tsc/test/temba-datepicker.test.js +1 -1
- package/out-tsc/test/temba-datepicker.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +249 -5
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-range-picker.test.js +123 -0
- package/out-tsc/test/temba-range-picker.test.js.map +1 -0
- package/out-tsc/test/temba-select.test.js +10 -16
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-webchat.test.js +4 -0
- package/out-tsc/test/temba-webchat.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +62 -0
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/datepicker/range-picker-all.png +0 -0
- package/screenshots/truth/datepicker/range-picker-button-states.png +0 -0
- package/screenshots/truth/datepicker/range-picker-default.png +0 -0
- package/screenshots/truth/datepicker/range-picker-editing-start.png +0 -0
- package/screenshots/truth/datepicker/range-picker-initial-values.png +0 -0
- package/screenshots/truth/datepicker/range-picker-min-max.png +0 -0
- package/screenshots/truth/datepicker/range-picker-week.png +0 -0
- package/screenshots/truth/datepicker/range-picker-year.png +0 -0
- package/screenshots/truth/sticky-note/blue.png +0 -0
- package/screenshots/truth/sticky-note/gray.png +0 -0
- package/screenshots/truth/sticky-note/green.png +0 -0
- package/screenshots/truth/sticky-note/pink.png +0 -0
- package/screenshots/truth/sticky-note/yellow.png +0 -0
- package/screenshots/truth/webchat/connected-state.png +0 -0
- package/src/chart/TembaChart.ts +144 -66
- package/src/datepicker/DatePicker.ts +9 -1
- package/src/datepicker/RangePicker.ts +602 -0
- package/src/flow/Editor.ts +252 -2
- package/src/flow/EditorNode.ts +98 -156
- package/src/flow/StickyNote.ts +284 -0
- package/src/interfaces.ts +2 -1
- package/src/list/RunList.ts +2 -1
- package/src/list/SortableList.ts +11 -0
- package/src/locales/es.ts +18 -13
- package/src/locales/fr.ts +18 -13
- package/src/locales/locale-codes.ts +11 -2
- package/src/locales/pt.ts +18 -13
- package/src/store/AppState.ts +51 -1
- package/src/store/flow-definition.d.ts +8 -0
- package/src/vectoricon/index.ts +2 -1
- package/static/svg/index.pdf +137 -0
- package/temba-modules.ts +5 -1
- package/test/temba-chart.test.ts +47 -0
- package/test/temba-datepicker.test.ts +1 -1
- package/test/temba-flow-editor-node.test.ts +322 -6
- package/test/temba-range-picker.test.ts +193 -0
- package/test/temba-select.test.ts +11 -19
- package/test/temba-webchat.test.ts +7 -0
- package/test/utils.test.ts +98 -0
- package/web-dev-server.config.mjs +30 -22
- package/web-test-runner.config.mjs +2 -0
- package/demo/datepicker/example.html +0 -69
package/temba-modules.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { TembaSlider } from './src/slider/TembaSlider';
|
|
|
36
36
|
import { RunList } from './src/list/RunList';
|
|
37
37
|
import { FlowStoreElement } from './src/store/FlowStoreElement';
|
|
38
38
|
import { ContactNameFetch } from './src/contacts/ContactNameFetch';
|
|
39
|
-
import DatePicker from './src/datepicker/DatePicker';
|
|
39
|
+
import { DatePicker } from './src/datepicker/DatePicker';
|
|
40
40
|
import { FieldManager } from './src/fields/FieldManager';
|
|
41
41
|
import { SortableList } from './src/list/SortableList';
|
|
42
42
|
import { ContentMenu } from './src/list/ContentMenu';
|
|
@@ -57,6 +57,7 @@ import { Chat } from './src/chat/Chat';
|
|
|
57
57
|
import { MediaPicker } from './src/mediapicker/MediaPicker';
|
|
58
58
|
import { Editor } from './src/flow/Editor';
|
|
59
59
|
import { EditorNode } from './src/flow/EditorNode';
|
|
60
|
+
import { StickyNote } from './src/flow/StickyNote';
|
|
60
61
|
import { ContactNotepad } from './src/contacts/ContactNotepad';
|
|
61
62
|
import { ProgressBar } from './src/progress/ProgressBar';
|
|
62
63
|
import { StartProgress } from './src/progress/StartProgress';
|
|
@@ -65,6 +66,7 @@ import { PopupSelect } from './src/select/PopupSelect';
|
|
|
65
66
|
import { UserSelect } from './src/select/UserSelect';
|
|
66
67
|
import { WorkspaceSelect } from './src/select/WorkspaceSelect';
|
|
67
68
|
import { TembaChart } from './src/chart/TembaChart';
|
|
69
|
+
import { RangePicker } from './src/datepicker/RangePicker';
|
|
68
70
|
|
|
69
71
|
export function addCustomElement(name: string, comp: any) {
|
|
70
72
|
if (!window.customElements.get(name)) {
|
|
@@ -77,6 +79,7 @@ addCustomElement('temba-alert', Alert);
|
|
|
77
79
|
addCustomElement('temba-store', Store);
|
|
78
80
|
addCustomElement('temba-textinput', TextInput);
|
|
79
81
|
addCustomElement('temba-datepicker', DatePicker);
|
|
82
|
+
addCustomElement('temba-range-picker', RangePicker);
|
|
80
83
|
addCustomElement('temba-date', TembaDate);
|
|
81
84
|
addCustomElement('temba-completion', Completion);
|
|
82
85
|
addCustomElement('temba-checkbox', Checkbox);
|
|
@@ -132,6 +135,7 @@ addCustomElement('temba-chat', Chat);
|
|
|
132
135
|
addCustomElement('temba-media-picker', MediaPicker);
|
|
133
136
|
addCustomElement('temba-flow-editor', Editor);
|
|
134
137
|
addCustomElement('temba-flow-node', EditorNode);
|
|
138
|
+
addCustomElement('temba-sticky-note', StickyNote);
|
|
135
139
|
addCustomElement('temba-contact-notepad', ContactNotepad);
|
|
136
140
|
addCustomElement('temba-progress', ProgressBar);
|
|
137
141
|
addCustomElement('temba-start-progress', StartProgress);
|
package/test/temba-chart.test.ts
CHANGED
|
@@ -182,6 +182,30 @@ describe('temba-chart', () => {
|
|
|
182
182
|
expect(chart.chart.options.scales.x.type).to.equal('category');
|
|
183
183
|
expect((chart.chart.options.scales.x as any).time).to.be.undefined;
|
|
184
184
|
});
|
|
185
|
+
|
|
186
|
+
it('configures scales correctly for horizontal charts', async () => {
|
|
187
|
+
const chart: TembaChart = await getChart();
|
|
188
|
+
|
|
189
|
+
// Test vertical chart (default)
|
|
190
|
+
chart.data = sampleData;
|
|
191
|
+
await chart.updateComplete;
|
|
192
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
193
|
+
|
|
194
|
+
// In vertical charts: x-axis should be for categories, y-axis for values
|
|
195
|
+
expect((chart.chart.options.scales as any).x.type).to.equal('category');
|
|
196
|
+
expect((chart.chart.options.scales as any).y.min).to.equal(0);
|
|
197
|
+
expect((chart.chart.options.scales as any).y.stacked).to.equal(true);
|
|
198
|
+
|
|
199
|
+
// Test horizontal chart
|
|
200
|
+
chart.horizontal = true;
|
|
201
|
+
await chart.updateComplete;
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
203
|
+
|
|
204
|
+
// In horizontal charts: x-axis should be for values, y-axis for categories
|
|
205
|
+
expect((chart.chart.options.scales as any).x.min).to.equal(0);
|
|
206
|
+
expect((chart.chart.options.scales as any).x.stacked).to.equal(true);
|
|
207
|
+
expect((chart.chart.options.scales as any).y.type).to.equal('category');
|
|
208
|
+
});
|
|
185
209
|
});
|
|
186
210
|
|
|
187
211
|
describe('formatDurationFromSeconds', () => {
|
|
@@ -232,4 +256,27 @@ describe('formatDurationFromSeconds', () => {
|
|
|
232
256
|
expect(formatDurationFromSeconds(1209600)).to.equal('14d'); // 2 weeks
|
|
233
257
|
expect(formatDurationFromSeconds(2678400)).to.equal('31d'); // ~1 month
|
|
234
258
|
});
|
|
259
|
+
|
|
260
|
+
it('supports horizontal bar charts', async () => {
|
|
261
|
+
const chart: TembaChart = await getChart();
|
|
262
|
+
|
|
263
|
+
// Test that horizontal property defaults to false
|
|
264
|
+
expect(chart.horizontal).to.equal(false);
|
|
265
|
+
|
|
266
|
+
// Set horizontal to true
|
|
267
|
+
chart.horizontal = true;
|
|
268
|
+
chart.data = sampleData;
|
|
269
|
+
await chart.updateComplete;
|
|
270
|
+
|
|
271
|
+
// Wait for the chart to be created after data is set
|
|
272
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
273
|
+
|
|
274
|
+
// Test that the chart was created with horizontal configuration
|
|
275
|
+
expect(chart.horizontal).to.equal(true);
|
|
276
|
+
expect(chart.chart).to.exist;
|
|
277
|
+
|
|
278
|
+
// Test that the chart configuration includes indexAxis: 'y' for horizontal bars
|
|
279
|
+
const chartConfig = chart.chart.options;
|
|
280
|
+
expect(chartConfig.indexAxis).to.equal('y');
|
|
281
|
+
});
|
|
235
282
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fixture, expect, assert } from '@open-wc/testing';
|
|
2
|
-
import DatePicker from '../src/datepicker/DatePicker';
|
|
2
|
+
import { DatePicker } from '../src/datepicker/DatePicker';
|
|
3
3
|
import { assertScreenshot, getAttributes, getClip } from './utils.test';
|
|
4
4
|
|
|
5
5
|
export const getPickerHTML = (attrs: any = {}) => {
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { fixture, expect, assert } from '@open-wc/testing';
|
|
2
|
+
import { RangePicker } from '../src/datepicker/RangePicker';
|
|
3
|
+
import { assertScreenshot, getAttributes, getClip } from './utils.test';
|
|
4
|
+
import { DateTime } from 'luxon';
|
|
5
|
+
|
|
6
|
+
export const getRangePickerHTML = (attrs: any = {}) => {
|
|
7
|
+
return `<temba-range-picker ${getAttributes(attrs)}></temba-range-picker>`;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const createRangePicker = async (def: string) => {
|
|
11
|
+
const parentNode = document.createElement('div');
|
|
12
|
+
parentNode.setAttribute('style', 'width: 600px;');
|
|
13
|
+
parentNode.id = 'parent';
|
|
14
|
+
const picker: RangePicker = await fixture(def, { parentNode });
|
|
15
|
+
return picker;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('temba-range-picker', () => {
|
|
19
|
+
it('can create a range picker', async () => {
|
|
20
|
+
const picker: RangePicker = await createRangePicker(getRangePickerHTML());
|
|
21
|
+
assert.instanceOf(picker, RangePicker);
|
|
22
|
+
|
|
23
|
+
// Should have default range (last month)
|
|
24
|
+
expect(picker.selectedRange).to.equal('M');
|
|
25
|
+
expect(picker.startDate).to.not.be.empty;
|
|
26
|
+
expect(picker.endDate).to.not.be.empty;
|
|
27
|
+
|
|
28
|
+
await assertScreenshot('datepicker/range-picker-default', getClip(picker));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('can be initialized with start and end dates', async () => {
|
|
32
|
+
const picker: RangePicker = await createRangePicker(
|
|
33
|
+
getRangePickerHTML({ start: '2024-01-01', end: '2024-01-31' })
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(picker.startDate).to.equal('2024-01-01');
|
|
37
|
+
expect(picker.endDate).to.equal('2024-01-31');
|
|
38
|
+
expect(picker.selectedRange).to.equal('');
|
|
39
|
+
|
|
40
|
+
await assertScreenshot(
|
|
41
|
+
'datepicker/range-picker-initial-values',
|
|
42
|
+
getClip(picker)
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('can set min and max dates', async () => {
|
|
47
|
+
const picker: RangePicker = await createRangePicker(
|
|
48
|
+
getRangePickerHTML({
|
|
49
|
+
start: '2024-06-01',
|
|
50
|
+
end: '2024-06-30',
|
|
51
|
+
min: '2024-01-01',
|
|
52
|
+
max: '2024-12-31'
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(picker.minDate).to.equal('2024-01-01');
|
|
57
|
+
expect(picker.maxDate).to.equal('2024-12-31');
|
|
58
|
+
|
|
59
|
+
await assertScreenshot('datepicker/range-picker-min-max', getClip(picker));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('can set range using buttons', async () => {
|
|
63
|
+
const picker: RangePicker = await createRangePicker(getRangePickerHTML());
|
|
64
|
+
|
|
65
|
+
// Click Week button
|
|
66
|
+
const weekBtn = picker.shadowRoot?.querySelector(
|
|
67
|
+
'.range-btn'
|
|
68
|
+
) as HTMLButtonElement;
|
|
69
|
+
weekBtn.click();
|
|
70
|
+
await picker.updateComplete;
|
|
71
|
+
|
|
72
|
+
expect(picker.selectedRange).to.equal('W');
|
|
73
|
+
expect(picker.startDate).to.equal(
|
|
74
|
+
DateTime.now().minus({ days: 6 }).toISODate()
|
|
75
|
+
);
|
|
76
|
+
expect(picker.endDate).to.equal(DateTime.now().toISODate());
|
|
77
|
+
|
|
78
|
+
await assertScreenshot('datepicker/range-picker-week', getClip(picker));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('can set year range using button', async () => {
|
|
82
|
+
const picker: RangePicker = await createRangePicker(getRangePickerHTML());
|
|
83
|
+
|
|
84
|
+
// Click Year button (3rd button)
|
|
85
|
+
const yearBtn = picker.shadowRoot?.querySelectorAll(
|
|
86
|
+
'.range-btn'
|
|
87
|
+
)[2] as HTMLButtonElement;
|
|
88
|
+
yearBtn.click();
|
|
89
|
+
await picker.updateComplete;
|
|
90
|
+
|
|
91
|
+
expect(picker.selectedRange).to.equal('Y');
|
|
92
|
+
expect(picker.startDate).to.equal(
|
|
93
|
+
DateTime.now().minus({ years: 1 }).plus({ days: 1 }).toISODate()
|
|
94
|
+
);
|
|
95
|
+
expect(picker.endDate).to.equal(DateTime.now().toISODate());
|
|
96
|
+
|
|
97
|
+
await assertScreenshot('datepicker/range-picker-year', getClip(picker));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('can set all range using button', async () => {
|
|
101
|
+
const picker: RangePicker = await createRangePicker(getRangePickerHTML());
|
|
102
|
+
|
|
103
|
+
// Click All button (4th button)
|
|
104
|
+
const allBtn = picker.shadowRoot?.querySelectorAll(
|
|
105
|
+
'.range-btn'
|
|
106
|
+
)[3] as HTMLButtonElement;
|
|
107
|
+
allBtn.click();
|
|
108
|
+
await picker.updateComplete;
|
|
109
|
+
|
|
110
|
+
expect(picker.selectedRange).to.equal('ALL');
|
|
111
|
+
expect(picker.startDate).to.equal('2012-01-01');
|
|
112
|
+
expect(picker.endDate).to.equal(DateTime.now().toISODate());
|
|
113
|
+
|
|
114
|
+
await assertScreenshot('datepicker/range-picker-all', getClip(picker));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('enforces valid date ranges', async () => {
|
|
118
|
+
const picker: RangePicker = await createRangePicker(
|
|
119
|
+
getRangePickerHTML({ start: '2024-06-01', end: '2024-06-30' })
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Verify initial state is valid
|
|
123
|
+
expect(
|
|
124
|
+
DateTime.fromISO(picker.endDate) >= DateTime.fromISO(picker.startDate)
|
|
125
|
+
).to.be.true;
|
|
126
|
+
|
|
127
|
+
// The validation logic is internal and triggered through user interaction
|
|
128
|
+
// We can verify the component has the correct min/max constraints
|
|
129
|
+
expect(picker.startDate).to.equal('2024-06-01');
|
|
130
|
+
expect(picker.endDate).to.equal('2024-06-30');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('enforces min/max date constraints', async () => {
|
|
134
|
+
const picker: RangePicker = await createRangePicker(
|
|
135
|
+
getRangePickerHTML({ min: '2024-01-01', max: '2024-12-31' })
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(picker.minDate).to.equal('2024-01-01');
|
|
139
|
+
expect(picker.maxDate).to.equal('2024-12-31');
|
|
140
|
+
|
|
141
|
+
// Min/max are enforced through the temba-datepicker components
|
|
142
|
+
// when user interacts with the date inputs
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('shows correct button selection states', async () => {
|
|
146
|
+
const picker: RangePicker = await createRangePicker(getRangePickerHTML());
|
|
147
|
+
|
|
148
|
+
// Initially should have M selected
|
|
149
|
+
const monthBtn = picker.shadowRoot?.querySelectorAll(
|
|
150
|
+
'.range-btn'
|
|
151
|
+
)[1] as HTMLButtonElement;
|
|
152
|
+
expect(monthBtn.classList.contains('selected')).to.be.true;
|
|
153
|
+
|
|
154
|
+
// Click week button
|
|
155
|
+
const weekBtn = picker.shadowRoot?.querySelector(
|
|
156
|
+
'.range-btn'
|
|
157
|
+
) as HTMLButtonElement;
|
|
158
|
+
weekBtn.click();
|
|
159
|
+
await picker.updateComplete;
|
|
160
|
+
|
|
161
|
+
expect(weekBtn.classList.contains('selected')).to.be.true;
|
|
162
|
+
expect(monthBtn.classList.contains('selected')).to.be.false;
|
|
163
|
+
|
|
164
|
+
await assertScreenshot(
|
|
165
|
+
'datepicker/range-picker-button-states',
|
|
166
|
+
getClip(picker)
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('can click to edit dates', async () => {
|
|
171
|
+
const picker: RangePicker = await createRangePicker(getRangePickerHTML());
|
|
172
|
+
|
|
173
|
+
// Click on start date display
|
|
174
|
+
const startDisplay = picker.shadowRoot?.querySelector(
|
|
175
|
+
'.date-display'
|
|
176
|
+
) as HTMLElement;
|
|
177
|
+
startDisplay.click();
|
|
178
|
+
await picker.updateComplete;
|
|
179
|
+
|
|
180
|
+
expect(picker.editingStart).to.be.true;
|
|
181
|
+
|
|
182
|
+
// Should show temba-datepicker for start
|
|
183
|
+
const startPicker = picker.shadowRoot?.querySelector(
|
|
184
|
+
'temba-datepicker.start-picker'
|
|
185
|
+
);
|
|
186
|
+
expect(startPicker).to.not.be.null;
|
|
187
|
+
|
|
188
|
+
await assertScreenshot(
|
|
189
|
+
'datepicker/range-picker-editing-start',
|
|
190
|
+
getClip(picker)
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|