@nyaruka/temba-components 0.125.0 → 0.127.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 (35) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/demo/chart/example.html +18 -1
  3. package/demo/data/flows/sample-flow.json +82 -228
  4. package/demo/data/server/opened-tickets-long.json +53 -0
  5. package/dist/temba-components.js +53 -15
  6. package/dist/temba-components.js.map +1 -1
  7. package/out-tsc/src/chart/TembaChart.js +33 -5
  8. package/out-tsc/src/chart/TembaChart.js.map +1 -1
  9. package/out-tsc/src/flow/Editor.js +70 -5
  10. package/out-tsc/src/flow/Editor.js.map +1 -1
  11. package/out-tsc/src/flow/EditorNode.js +136 -4
  12. package/out-tsc/src/flow/EditorNode.js.map +1 -1
  13. package/out-tsc/src/flow/Plumber.js +57 -0
  14. package/out-tsc/src/flow/Plumber.js.map +1 -1
  15. package/out-tsc/src/interfaces.js +1 -0
  16. package/out-tsc/src/interfaces.js.map +1 -1
  17. package/out-tsc/src/store/AppState.js +22 -5
  18. package/out-tsc/src/store/AppState.js.map +1 -1
  19. package/out-tsc/test/temba-chart.test.js +26 -0
  20. package/out-tsc/test/temba-chart.test.js.map +1 -1
  21. package/out-tsc/test/temba-flow-node-drag.test.js +257 -0
  22. package/out-tsc/test/temba-flow-node-drag.test.js.map +1 -0
  23. package/out-tsc/test/temba-select.test.js +3 -2
  24. package/out-tsc/test/temba-select.test.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/chart/TembaChart.ts +35 -5
  27. package/src/flow/Editor.ts +78 -6
  28. package/src/flow/EditorNode.ts +159 -4
  29. package/src/flow/Plumber.ts +65 -0
  30. package/src/interfaces.ts +2 -1
  31. package/src/store/AppState.ts +28 -7
  32. package/test/temba-chart.test.ts +36 -0
  33. package/test/temba-flow-node-drag.test.ts +337 -0
  34. package/test/temba-select.test.ts +3 -2
  35. package/web-dev-server.config.mjs +39 -14
@@ -292,9 +292,15 @@ export class TembaChart extends RapidElement {
292
292
  @property({ type: String })
293
293
  xType: 'category' | 'time' = 'category';
294
294
 
295
+ @property({ type: Number })
296
+ xMaxTicks: number = 10;
297
+
295
298
  @property({ type: String })
296
299
  yType: 'count' | 'duration' = 'count';
297
300
 
301
+ @property({ type: String })
302
+ xFormat: 'MMM yy' | 'MMM yyyy' | 'MMM dd' | 'DD' | 'EEE' | 'auto' = 'auto';
303
+
298
304
  @property({ type: Boolean })
299
305
  hideOther: boolean = false;
300
306
 
@@ -581,6 +587,9 @@ export class TembaChart extends RapidElement {
581
587
 
582
588
  const percentFormatter = (value: number): string => {
583
589
  const pct = grandTotal ? (value / grandTotal) * 100 : 0;
590
+ if (pct === 0) {
591
+ return '';
592
+ }
584
593
  return `${Math.round(pct)}%`;
585
594
  };
586
595
 
@@ -604,6 +613,21 @@ export class TembaChart extends RapidElement {
604
613
 
605
614
  this.chart.update();
606
615
  } else {
616
+ let format = this.xFormat;
617
+ if (this.xType === 'time' && this.xFormat === 'auto') {
618
+ const firstDate = this.data.labels[0];
619
+ const lastDate = this.data.labels[this.data.labels.length - 1];
620
+
621
+ const first = Date.parse(firstDate);
622
+ const last = Date.parse(lastDate);
623
+
624
+ const dayDiff = Math.ceil((last - first) / (1000 * 60 * 60 * 24));
625
+ format = 'MMM dd';
626
+ if (dayDiff > 365) {
627
+ format = 'MMM yyyy';
628
+ }
629
+ }
630
+
607
631
  const chartData = {
608
632
  type: this.chartType,
609
633
  data: {
@@ -664,6 +688,7 @@ export class TembaChart extends RapidElement {
664
688
  },
665
689
  ticks: {
666
690
  display: !this.showPercent,
691
+
667
692
  ...(this.yType === 'duration' &&
668
693
  !this.showPercent && {
669
694
  callback: (value: any) => formatDurationFromSeconds(value)
@@ -674,11 +699,16 @@ export class TembaChart extends RapidElement {
674
699
  type: this.xType,
675
700
  grid: { display: false },
676
701
  stacked: true,
677
- time: {
678
- unit: 'day',
679
- tooltipFormat: 'DDD',
680
- displayFormats: { day: 'MMM dd' }
681
- }
702
+ ticks: {
703
+ maxTicksLimit: this.xMaxTicks
704
+ },
705
+ ...(this.xType === 'time' && {
706
+ time: {
707
+ unit: 'day',
708
+ tooltipFormat: 'DDD',
709
+ displayFormats: { day: format }
710
+ }
711
+ })
682
712
  }
683
713
  }
684
714
  }
@@ -9,15 +9,20 @@ import { RapidElement } from '../RapidElement';
9
9
  import { Plumber } from './Plumber';
10
10
  import { EditorNode } from './EditorNode';
11
11
 
12
+ const SAVE_QUIET_TIME = 500;
13
+
12
14
  export class Editor extends RapidElement {
13
- // Unfortunately, jsplumb requires that we be in light DOM
15
+ // unfortunately, jsplumb requires that we be in light DOM
14
16
  createRenderRoot() {
15
17
  return this;
16
18
  }
17
19
 
18
- // This is the master plumber
20
+ // this is the master plumber
19
21
  private plumber: Plumber;
20
22
 
23
+ // timer for debounced saving
24
+ private saveTimer: number | null = null;
25
+
21
26
  @property({ type: String })
22
27
  public flow: string;
23
28
 
@@ -30,6 +35,9 @@ export class Editor extends RapidElement {
30
35
  @fromStore(zustand, (state: AppState) => state.canvasSize)
31
36
  private canvasSize!: { width: number; height: number };
32
37
 
38
+ @fromStore(zustand, (state: AppState) => state.dirtyDate)
39
+ private dirtyDate!: Date;
40
+
33
41
  static get styles() {
34
42
  return css`
35
43
  #editor {
@@ -40,7 +48,7 @@ export class Editor extends RapidElement {
40
48
  #grid {
41
49
  position: relative;
42
50
  background-color: #f9f9f9;
43
- background-position: 13px 13px;
51
+ background-position: 10px 10px;
44
52
  background-image: linear-gradient(
45
53
  0deg,
46
54
  transparent 24%,
@@ -84,11 +92,11 @@ export class Editor extends RapidElement {
84
92
  }
85
93
 
86
94
  .jtk-endpoint {
87
- z-index: 1;
95
+ z-index: 600;
88
96
  }
89
97
 
90
98
  .plumb-source {
91
- z-index: 300;
99
+ z-index: 600;
92
100
  border: 0px solid var(--color-connectors);
93
101
  }
94
102
 
@@ -112,7 +120,7 @@ export class Editor extends RapidElement {
112
120
 
113
121
  .plumb-target {
114
122
  margin-top: -6px;
115
- z-index: 200;
123
+ z-index: 600;
116
124
  opacity: 0;
117
125
  cursor: pointer;
118
126
  }
@@ -120,6 +128,29 @@ export class Editor extends RapidElement {
120
128
  body .plumb-connector path {
121
129
  stroke: var(--color-connectors) !important;
122
130
  stroke-width: 3px;
131
+ z-index: 10;
132
+ }
133
+
134
+ body .plumb-connector {
135
+ z-index: 10;
136
+ }
137
+
138
+ body .plumb-connector.elevated {
139
+ z-index: 550;
140
+ }
141
+
142
+ body .plumb-connector.elevated path {
143
+ stroke: var(--color-connectors) !important;
144
+ stroke-width: 3px;
145
+ z-index: 550;
146
+ }
147
+
148
+ body .plumb-connector.elevated .plumb-arrow {
149
+ fill: var(--color-connectors);
150
+ stroke: var(--color-connectors);
151
+ stroke-width: 0px;
152
+ margin-top: 6px;
153
+ z-index: 550;
123
154
  }
124
155
 
125
156
  body .plumb-connector .plumb-arrow {
@@ -127,6 +158,7 @@ export class Editor extends RapidElement {
127
158
  stroke: var(--color-connectors);
128
159
  stroke-width: 0px;
129
160
  margin-top: 6px;
161
+ z-index: 10;
130
162
  }
131
163
 
132
164
  body svg.jtk-connector.jtk-hover path {
@@ -137,6 +169,7 @@ export class Editor extends RapidElement {
137
169
  body .plumb-connector.jtk-hover .plumb-arrow {
138
170
  fill: var(--color-success) !important;
139
171
  stroke-width: 0px;
172
+ z-index: 10;
140
173
  }
141
174
  `;
142
175
  }
@@ -162,6 +195,45 @@ export class Editor extends RapidElement {
162
195
  if (changes.has('canvasSize')) {
163
196
  // console.log('Setting canvas size', this.canvasSize);
164
197
  }
198
+
199
+ if (changes.has('dirtyDate')) {
200
+ if (this.dirtyDate) {
201
+ this.debouncedSave();
202
+ }
203
+ }
204
+ }
205
+
206
+ private debouncedSave(): void {
207
+ // Clear any existing timer
208
+ if (this.saveTimer !== null) {
209
+ clearTimeout(this.saveTimer);
210
+ }
211
+
212
+ this.saveTimer = window.setTimeout(() => {
213
+ const now = new Date();
214
+ const timeSinceLastChange = now.getTime() - this.dirtyDate.getTime();
215
+
216
+ if (timeSinceLastChange >= SAVE_QUIET_TIME) {
217
+ this.saveChanges();
218
+ this.saveTimer = null;
219
+ } else {
220
+ this.debouncedSave();
221
+ }
222
+ }, SAVE_QUIET_TIME);
223
+ }
224
+
225
+ private saveChanges(): void {
226
+ // post the flow definition to the server
227
+ getStore().postJSON(`/flow/revisions/${this.flow}`, this.definition);
228
+ getStore().getState().setDirtyDate(null);
229
+ }
230
+
231
+ disconnectedCallback(): void {
232
+ super.disconnectedCallback();
233
+ if (this.saveTimer !== null) {
234
+ clearTimeout(this.saveTimer);
235
+ this.saveTimer = null;
236
+ }
165
237
  }
166
238
 
167
239
  public render(): TemplateResult {
@@ -1,7 +1,7 @@
1
1
  import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
2
  import { EDITOR_CONFIG, UIConfig } from './config';
3
3
  import { Action, Exit, Node, NodeUI, Router } from '../store/flow-definition';
4
- import { state } from 'lit/decorators.js';
4
+ import { property } from 'lit/decorators.js';
5
5
  import { RapidElement } from '../RapidElement';
6
6
  import { getClasses } from '../utils';
7
7
  import { Plumber } from './Plumber';
@@ -12,15 +12,31 @@ export class EditorNode extends RapidElement {
12
12
  return this;
13
13
  }
14
14
 
15
- @state()
15
+ @property({ type: Object })
16
16
  private plumber: Plumber;
17
17
 
18
- @state()
18
+ @property({ type: Object })
19
19
  private node: Node;
20
20
 
21
- @state()
21
+ @property({ type: Object })
22
22
  private ui: NodeUI;
23
23
 
24
+ // Drag state properties
25
+ private isDragging = false;
26
+ private dragStartPos = { x: 0, y: 0 };
27
+ private nodeStartPos = { left: 0, top: 0 };
28
+
29
+ // Bound event handlers to maintain proper 'this' context
30
+ private boundMouseMove = this.handleMouseMove.bind(this);
31
+ private boundMouseUp = this.handleMouseUp.bind(this);
32
+
33
+ /**
34
+ * Snaps a coordinate value to the nearest 20px grid position
35
+ */
36
+ private snapToGrid(value: number): number {
37
+ return Math.round(value / 20) * 20;
38
+ }
39
+
24
40
  static get styles() {
25
41
  return css`
26
42
  .node {
@@ -31,6 +47,19 @@ export class EditorNode extends RapidElement {
31
47
  border-radius: calc(var(--curvature) * 1.5);
32
48
  overflow: hidden;
33
49
  color: #333;
50
+ cursor: move;
51
+ user-select: none;
52
+ z-index: 500;
53
+ }
54
+
55
+ .node:hover {
56
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
57
+ }
58
+
59
+ .node.dragging {
60
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4);
61
+ transform: scale(1.02);
62
+ z-index: 1000;
34
63
  }
35
64
 
36
65
  .action {
@@ -129,9 +158,131 @@ export class EditorNode extends RapidElement {
129
158
  this.ui.position.left + rect.width,
130
159
  this.ui.position.top + rect.height
131
160
  );
161
+
162
+ // Add drag event listeners to the node
163
+ this.addDragEventListeners();
132
164
  }
133
165
  }
134
166
 
167
+ private addDragEventListeners(): void {
168
+ const nodeElement = this.querySelector('.node') as HTMLElement;
169
+ if (!nodeElement) return;
170
+
171
+ nodeElement.addEventListener('mousedown', this.handleMouseDown.bind(this));
172
+ document.addEventListener('mousemove', this.boundMouseMove);
173
+ document.addEventListener('mouseup', this.boundMouseUp);
174
+ }
175
+
176
+ private handleMouseDown(event: MouseEvent): void {
177
+ // Only start dragging if clicking on the node itself, not on exits or other interactive elements
178
+ const target = event.target as HTMLElement;
179
+ if (target.classList.contains('exit') || target.closest('.exit')) {
180
+ return;
181
+ }
182
+
183
+ this.isDragging = true;
184
+ this.dragStartPos = { x: event.clientX, y: event.clientY };
185
+ this.nodeStartPos = {
186
+ left: this.ui.position.left,
187
+ top: this.ui.position.top
188
+ };
189
+
190
+ // Add dragging class for visual feedback
191
+ const nodeElement = this.querySelector('.node') as HTMLElement;
192
+ if (nodeElement) {
193
+ nodeElement.classList.add('dragging');
194
+ }
195
+
196
+ // Elevate connections for this node during dragging
197
+ if (this.plumber) {
198
+ this.plumber.elevateNodeConnections(this.node.uuid);
199
+ }
200
+
201
+ event.preventDefault();
202
+ event.stopPropagation();
203
+ }
204
+
205
+ private handleMouseMove(event: MouseEvent): void {
206
+ if (!this.isDragging) return;
207
+
208
+ const deltaX = event.clientX - this.dragStartPos.x;
209
+ const deltaY = event.clientY - this.dragStartPos.y;
210
+
211
+ const newLeft = this.nodeStartPos.left + deltaX;
212
+ const newTop = this.nodeStartPos.top + deltaY;
213
+
214
+ // Update the UI position temporarily (for visual feedback)
215
+ const nodeElement = this.querySelector('.node') as HTMLElement;
216
+ if (nodeElement) {
217
+ nodeElement.style.left = `${newLeft}px`;
218
+ nodeElement.style.top = `${newTop}px`;
219
+ }
220
+
221
+ // Repaint connections during dragging for smooth updates
222
+ if (this.plumber) {
223
+ this.plumber.repaintEverything();
224
+ }
225
+ }
226
+
227
+ private handleMouseUp(event: MouseEvent): void {
228
+ if (!this.isDragging) return;
229
+
230
+ this.isDragging = false;
231
+
232
+ // Remove dragging class
233
+ const nodeElement = this.querySelector('.node') as HTMLElement;
234
+ if (nodeElement) {
235
+ nodeElement.classList.remove('dragging');
236
+ }
237
+
238
+ // Restore normal z-index for connections
239
+ if (this.plumber) {
240
+ this.plumber.restoreNodeConnections(this.node.uuid);
241
+ }
242
+
243
+ const deltaX = event.clientX - this.dragStartPos.x;
244
+ const deltaY = event.clientY - this.dragStartPos.y;
245
+
246
+ const newLeft = this.nodeStartPos.left + deltaX;
247
+ const newTop = this.nodeStartPos.top + deltaY;
248
+
249
+ // Snap to 20px grid for final position
250
+ const snappedLeft = this.snapToGrid(newLeft);
251
+ const snappedTop = this.snapToGrid(newTop);
252
+
253
+ // Update the store with the new snapped position
254
+ const newPosition = { left: snappedLeft, top: snappedTop };
255
+ getStore()
256
+ .getState()
257
+ .updateCanvasPositions({
258
+ [this.node.uuid]: newPosition
259
+ });
260
+
261
+ // Repaint connections if plumber is available
262
+ if (this.plumber) {
263
+ this.plumber.repaintEverything();
264
+ }
265
+
266
+ getStore().getState().updateNodePosition(this.node.uuid, newPosition);
267
+
268
+ // Fire a custom event with the new coordinates
269
+ /*this.fireCustomEvent(CustomEventType.Moved, {
270
+ nodeId: this.node.uuid,
271
+ position: newPosition,
272
+ oldPosition: {
273
+ left: this.nodeStartPos.left,
274
+ top: this.nodeStartPos.top
275
+ }
276
+ });*/
277
+ }
278
+
279
+ disconnectedCallback(): void {
280
+ super.disconnectedCallback();
281
+ // Clean up event listeners
282
+ document.removeEventListener('mousemove', this.boundMouseMove);
283
+ document.removeEventListener('mouseup', this.boundMouseUp);
284
+ }
285
+
135
286
  private renderTitle(config: UIConfig) {
136
287
  return html`<div class="title" style="background:${config.color}">
137
288
  ${config.name}
@@ -198,6 +349,10 @@ export class EditorNode extends RapidElement {
198
349
  }
199
350
 
200
351
  public render() {
352
+ if (!this.node || !this.ui) {
353
+ return html`<div class="node">Loading...</div>`;
354
+ }
355
+
201
356
  return html`
202
357
  <div
203
358
  id="${this.node.uuid}"
@@ -132,4 +132,69 @@ export class Plumber {
132
132
  this.pendingConnections.push({ fromId, toId });
133
133
  this.processPendingConnections();
134
134
  }
135
+
136
+ public repaintEverything() {
137
+ if (this.jsPlumb) {
138
+ this.jsPlumb.repaintEverything();
139
+ }
140
+ }
141
+ public elevateNodeConnections(nodeId: string) {
142
+ if (!this.jsPlumb) return;
143
+
144
+ // Get all connections
145
+ const connections = this.jsPlumb.getConnections();
146
+
147
+ // Get the node element to find its exit elements
148
+ const nodeElement = document.getElementById(nodeId);
149
+ const exitElements = nodeElement
150
+ ? nodeElement.querySelectorAll('.exit')
151
+ : [];
152
+ const exitIds = Array.from(exitElements).map((exit) => exit.id);
153
+
154
+ connections.forEach((connection) => {
155
+ const sourceId = connection.source.id;
156
+ const targetId = connection.target.id;
157
+
158
+ // Check if this connection involves the dragged node:
159
+ // - Incoming: target is the node itself
160
+ // - Outgoing: source is one of the node's exits
161
+ if (targetId === nodeId || exitIds.includes(sourceId)) {
162
+ // Add elevated class to the connector element
163
+ const connectorElement = connection.connector.canvas;
164
+ if (connectorElement) {
165
+ connectorElement.classList.add('elevated');
166
+ }
167
+ }
168
+ });
169
+ }
170
+
171
+ public restoreNodeConnections(nodeId: string) {
172
+ if (!this.jsPlumb) return;
173
+
174
+ // Get all connections
175
+ const connections = this.jsPlumb.getConnections();
176
+
177
+ // Get the node element to find its exit elements
178
+ const nodeElement = document.getElementById(nodeId);
179
+ const exitElements = nodeElement
180
+ ? nodeElement.querySelectorAll('.exit')
181
+ : [];
182
+ const exitIds = Array.from(exitElements).map((exit) => exit.id);
183
+
184
+ connections.forEach((connection) => {
185
+ const sourceId = connection.source.id;
186
+ const targetId = connection.target.id;
187
+
188
+ // Check if this connection involves the node:
189
+ // - Incoming: target is the node itself
190
+ // - Outgoing: source is one of the node's exits
191
+ if (targetId === nodeId || exitIds.includes(sourceId)) {
192
+ // Remove elevated class from the connector element
193
+ const connectorElement = connection.connector.canvas;
194
+ if (connectorElement) {
195
+ connectorElement.classList.remove('elevated');
196
+ }
197
+ }
198
+ });
199
+ }
135
200
  }
package/src/interfaces.ts CHANGED
@@ -284,5 +284,6 @@ export enum CustomEventType {
284
284
  Error = 'temba-error',
285
285
  Interrupt = 'temba-interrupt',
286
286
  Opened = 'temba-opened',
287
- TicketUpdated = 'temba-ticket-updated'
287
+ TicketUpdated = 'temba-ticket-updated',
288
+ Moved = 'temba-moved'
288
289
  }
@@ -65,6 +65,8 @@ export interface AppState {
65
65
  workspace: Workspace;
66
66
  isTranslating: boolean;
67
67
 
68
+ dirtyDate: Date | null;
69
+
68
70
  canvasSize: { width: number; height: number };
69
71
 
70
72
  fetchRevision: (endpoint: string, id?: string) => void;
@@ -77,10 +79,11 @@ export interface AppState {
77
79
  setFlowContents: (flow: FlowContents) => void;
78
80
  setFlowInfo: (info: FlowInfo) => void;
79
81
  setLanguageCode: (languageCode: string) => void;
80
- setTestUpdate: () => void;
82
+ setDirtyDate: (date: Date) => void;
81
83
  expandCanvas: (width: number, height: number) => void;
82
84
 
83
85
  updateCanvasPositions: (positions: CanvasPositions) => void;
86
+ updateNodePosition(uuid: string, newPosition: FlowPosition): void;
84
87
  removeNodes: (uuids: string[]) => void;
85
88
  }
86
89
 
@@ -94,6 +97,13 @@ export const zustand = createStore<AppState>()(
94
97
  flowDefinition: null,
95
98
  flowInfo: null,
96
99
  isTranslating: false,
100
+ dirtyDate: null,
101
+
102
+ setDirtyDate: (date: Date) => {
103
+ set((state: AppState) => {
104
+ state.dirtyDate = date;
105
+ });
106
+ },
97
107
 
98
108
  fetchRevision: async (endpoint: string, id: string = null) => {
99
109
  if (!id) {
@@ -178,12 +188,6 @@ export const zustand = createStore<AppState>()(
178
188
  });
179
189
  },
180
190
 
181
- setTestUpdate: () => {
182
- set((state: AppState) => {
183
- state.flowDefinition.name = 'Bloop!';
184
- });
185
- },
186
-
187
191
  expandCanvas: (width: number, height: number) => {
188
192
  set((state: AppState) => {
189
193
  const minWidth = Math.max(
@@ -211,6 +215,23 @@ export const zustand = createStore<AppState>()(
211
215
  });
212
216
  },
213
217
 
218
+ updateNodePosition: (uuid: string, newPosition: FlowPosition) => {
219
+ set((state: AppState) => {
220
+ if (state.flowDefinition._ui.nodes[uuid]) {
221
+ state.flowDefinition._ui.nodes[uuid].position = newPosition;
222
+ } else {
223
+ // If the node doesn't exist in _ui, we can add it
224
+ state.flowDefinition._ui.nodes[uuid] = {
225
+ position: newPosition,
226
+ type: null,
227
+ config: {}
228
+ };
229
+ }
230
+
231
+ state.dirtyDate = new Date();
232
+ });
233
+ },
234
+
214
235
  removeNodes: (uuids: string[]) => {
215
236
  set((state: AppState) => {
216
237
  for (const uuid of uuids) {
@@ -146,6 +146,42 @@ describe('temba-chart', () => {
146
146
  expect(tickCallback.call({}, 604800, 9, [])).to.equal('7d'); // 1 week in seconds
147
147
  expect(tickCallback.call({}, 1209600, 10, [])).to.equal('14d'); // 2 weeks in seconds
148
148
  });
149
+
150
+ it('applies xFormat when xType is time', async () => {
151
+ const chart: TembaChart = await getChart({
152
+ xType: 'time',
153
+ xFormat: 'DD'
154
+ });
155
+
156
+ chart.data = sampleData;
157
+ await chart.updateComplete;
158
+
159
+ // Wait for the chart to be created after data is set
160
+ await new Promise((resolve) => setTimeout(resolve, 50));
161
+
162
+ expect(chart.chart).to.exist;
163
+ expect(chart.chart.options.scales.x.type).to.equal('time');
164
+ expect(
165
+ (chart.chart.options.scales.x as any).time.displayFormats.day
166
+ ).to.equal('DD');
167
+ });
168
+
169
+ it('does not include time config when xType is category', async () => {
170
+ const chart: TembaChart = await getChart({
171
+ xType: 'category',
172
+ xFormat: 'DD'
173
+ });
174
+
175
+ chart.data = sampleData;
176
+ await chart.updateComplete;
177
+
178
+ // Wait for the chart to be created after data is set
179
+ await new Promise((resolve) => setTimeout(resolve, 50));
180
+
181
+ expect(chart.chart).to.exist;
182
+ expect(chart.chart.options.scales.x.type).to.equal('category');
183
+ expect((chart.chart.options.scales.x as any).time).to.be.undefined;
184
+ });
149
185
  });
150
186
 
151
187
  describe('formatDurationFromSeconds', () => {